aboutsummaryrefslogtreecommitdiff
path: root/git2d
diff options
context:
space:
mode:
Diffstat (limited to 'git2d')
-rw-r--r--git2d/bare.c300
-rw-r--r--git2d/bare.h72
-rw-r--r--git2d/cmd1.c124
-rw-r--r--git2d/cmd2.c121
-rw-r--r--git2d/cmd_commit.c403
-rw-r--r--git2d/cmd_diff.c276
-rw-r--r--git2d/cmd_init.c65
-rw-r--r--git2d/cmd_ref.c113
-rw-r--r--git2d/cmd_tree.c120
-rw-r--r--git2d/main.c81
-rw-r--r--git2d/rw.c34
-rw-r--r--git2d/session.c143
-rw-r--r--git2d/x.h55
13 files changed, 1907 insertions, 0 deletions
diff --git a/git2d/bare.c b/git2d/bare.c
new file mode 100644
index 0000000..307f3d8
--- /dev/null
+++ b/git2d/bare.c
@@ -0,0 +1,300 @@
+/*-
+ * SPDX-License-Identifier: MIT
+ * SPDX-FileCopyrightText: Copyright (c) 2022 Frank Smit <https://61924.nl/>
+ */
+
+#include <string.h>
+#include <stdbool.h>
+
+#include "bare.h"
+
+#define UNUSED(x) (void)(x)
+
+enum {
+ U8SZ = 1,
+ U16SZ = 2,
+ U32SZ = 4,
+ U64SZ = 8,
+ MAXVARINTSZ = 10,
+};
+
+bare_error bare_put_uint(struct bare_writer *ctx, uint64_t x)
+{
+ uint64_t i = 0;
+ uint8_t b[MAXVARINTSZ];
+
+ while (x >= 0x80) {
+ b[i] = (uint8_t) x | 0x80;
+ x >>= 7;
+ i++;
+ }
+
+ b[i] = (uint8_t) x;
+ i++;
+
+ return ctx->write(ctx->buffer, b, i);
+}
+
+bare_error bare_get_uint(struct bare_reader *ctx, uint64_t *x)
+{
+ bare_error err = BARE_ERROR_NONE;
+
+ uint8_t shift = 0;
+ uint64_t result = 0;
+
+ for (uint8_t i = 0; i < 10; i++) {
+ uint8_t b;
+
+ err = ctx->read(ctx->buffer, &b, U8SZ);
+ if (err != BARE_ERROR_NONE) {
+ break;
+ }
+
+ if (b < 0x80) {
+ result |= (uint64_t) b << shift;
+ break;
+ } else {
+ result |= ((uint64_t) b & 0x7f) << shift;
+ shift += 7;
+ }
+ }
+
+ *x = result;
+
+ return err;
+}
+
+bare_error bare_put_int(struct bare_writer *ctx, int64_t x)
+{
+ uint64_t ux = (uint64_t) x << 1;
+
+ if (x < 0) {
+ ux = ~ux;
+ }
+
+ return bare_put_uint(ctx, ux);
+}
+
+bare_error bare_get_int(struct bare_reader *ctx, int64_t *x)
+{
+ uint64_t ux;
+
+ bare_error err = bare_get_uint(ctx, &ux);
+
+ if (err == BARE_ERROR_NONE) {
+ *x = (int64_t) (ux >> 1);
+
+ if ((ux & 1) != 0) {
+ *x = ~(*x);
+ }
+ }
+
+ return err;
+}
+
+bare_error bare_put_u8(struct bare_writer *ctx, uint8_t x)
+{
+ return ctx->write(ctx->buffer, &x, U8SZ);
+}
+
+bare_error bare_get_u8(struct bare_reader *ctx, uint8_t *x)
+{
+ return ctx->read(ctx->buffer, x, U8SZ);
+}
+
+bare_error bare_put_u16(struct bare_writer *ctx, uint16_t x)
+{
+ return ctx->write(ctx->buffer, (uint8_t[U16SZ]) {
+ x, x >> 8}
+ , U16SZ);
+}
+
+bare_error bare_get_u16(struct bare_reader *ctx, uint16_t *x)
+{
+ bare_error err = ctx->read(ctx->buffer, x, U16SZ);
+
+ if (err == BARE_ERROR_NONE) {
+ *x = (uint16_t) ((uint8_t *) x)[0]
+ | (uint16_t) ((uint8_t *) x)[1] << 8;
+ }
+
+ return err;
+}
+
+bare_error bare_put_u32(struct bare_writer *ctx, uint32_t x)
+{
+ uint8_t buf[U32SZ];
+
+ buf[0] = (uint8_t) (x);
+ buf[1] = (uint8_t) (x >> 8);
+ buf[2] = (uint8_t) (x >> 16);
+ buf[3] = (uint8_t) (x >> 24);
+
+ return ctx->write(ctx->buffer, buf, U32SZ);
+}
+
+bare_error bare_get_u32(struct bare_reader *ctx, uint32_t *x)
+{
+ bare_error err = ctx->read(ctx->buffer, x, U32SZ);
+
+ if (err == BARE_ERROR_NONE) {
+ *x = (uint32_t) (((uint8_t *) x)[0])
+ | (uint32_t) (((uint8_t *) x)[1] << 8)
+ | (uint32_t) (((uint8_t *) x)[2] << 16)
+ | (uint32_t) (((uint8_t *) x)[3] << 24);
+ }
+
+ return err;
+}
+
+bare_error bare_put_u64(struct bare_writer *ctx, uint64_t x)
+{
+ uint8_t buf[U64SZ];
+
+ buf[0] = x;
+ buf[1] = x >> 8;
+ buf[2] = x >> 16;
+ buf[3] = x >> 24;
+ buf[4] = x >> 32;
+ buf[5] = x >> 40;
+ buf[6] = x >> 48;
+ buf[7] = x >> 56;
+
+ return ctx->write(ctx->buffer, buf, U64SZ);
+}
+
+bare_error bare_get_u64(struct bare_reader *ctx, uint64_t *x)
+{
+ bare_error err = ctx->read(ctx->buffer, x, U64SZ);
+
+ if (err == BARE_ERROR_NONE) {
+ *x = (uint64_t) ((uint8_t *) x)[0]
+ | (uint64_t) ((uint8_t *) x)[1] << 8 | (uint64_t) ((uint8_t *) x)[2] << 16 | (uint64_t) ((uint8_t *) x)[3] << 24 | (uint64_t) ((uint8_t *) x)[4] << 32 | (uint64_t) ((uint8_t *) x)[5] << 40 | (uint64_t) ((uint8_t *) x)[6] << 48 | (uint64_t) ((uint8_t *) x)[7] << 56;
+ }
+
+ return err;
+}
+
+bare_error bare_put_i8(struct bare_writer *ctx, int8_t x)
+{
+ return bare_put_u8(ctx, x);
+}
+
+bare_error bare_get_i8(struct bare_reader *ctx, int8_t *x)
+{
+ return bare_get_u8(ctx, (uint8_t *) x);
+}
+
+bare_error bare_put_i16(struct bare_writer *ctx, int16_t x)
+{
+ return bare_put_u16(ctx, x);
+}
+
+bare_error bare_get_i16(struct bare_reader *ctx, int16_t *x)
+{
+ return bare_get_u16(ctx, (uint16_t *) x);
+}
+
+bare_error bare_put_i32(struct bare_writer *ctx, int32_t x)
+{
+ return bare_put_u32(ctx, x);
+}
+
+bare_error bare_get_i32(struct bare_reader *ctx, int32_t *x)
+{
+ return bare_get_u32(ctx, (uint32_t *) x);
+}
+
+bare_error bare_put_i64(struct bare_writer *ctx, int64_t x)
+{
+ return bare_put_u64(ctx, x);
+}
+
+bare_error bare_get_i64(struct bare_reader *ctx, int64_t *x)
+{
+ return bare_get_u64(ctx, (uint64_t *) x);
+}
+
+bare_error bare_put_f32(struct bare_writer *ctx, float x)
+{
+ uint32_t b;
+ memcpy(&b, &x, U32SZ);
+
+ return bare_put_u32(ctx, b);
+}
+
+bare_error bare_get_f32(struct bare_reader *ctx, float *x)
+{
+ return ctx->read(ctx->buffer, x, U32SZ);
+}
+
+bare_error bare_put_f64(struct bare_writer *ctx, double x)
+{
+ uint64_t b;
+ memcpy(&b, &x, U64SZ);
+
+ return bare_put_u64(ctx, b);
+}
+
+bare_error bare_get_f64(struct bare_reader *ctx, double *x)
+{
+ return ctx->read(ctx->buffer, x, U64SZ);
+}
+
+bare_error bare_put_bool(struct bare_writer *ctx, bool x)
+{
+ return bare_put_u8(ctx, (uint8_t) x);
+}
+
+bare_error bare_get_bool(struct bare_reader *ctx, bool *x)
+{
+ return bare_get_u8(ctx, (uint8_t *) x);
+}
+
+bare_error bare_put_fixed_data(struct bare_writer *ctx, const uint8_t *src, uint64_t sz)
+{
+ return ctx->write(ctx->buffer, (void *)src, sz);
+}
+
+bare_error bare_get_fixed_data(struct bare_reader *ctx, uint8_t *dst, uint64_t sz)
+{
+ return ctx->read(ctx->buffer, dst, sz);
+}
+
+bare_error bare_put_data(struct bare_writer *ctx, const uint8_t *src, uint64_t sz)
+{
+ bare_error err = BARE_ERROR_NONE;
+
+ err = bare_put_uint(ctx, sz);
+
+ if (err == BARE_ERROR_NONE) {
+ err = bare_put_fixed_data(ctx, src, sz);
+ }
+
+ return err;
+}
+
+bare_error bare_get_data(struct bare_reader *ctx, uint8_t *dst, uint64_t sz)
+{
+ bare_error err = BARE_ERROR_NONE;
+ uint64_t ssz = 0;
+
+ err = bare_get_uint(ctx, &ssz);
+
+ if (err == BARE_ERROR_NONE) {
+ err = ssz <= sz ? bare_get_fixed_data(ctx, dst, ssz)
+ : BARE_ERROR_BUFFER_TOO_SMALL;
+ }
+
+ return err;
+}
+
+bare_error bare_put_str(struct bare_writer *ctx, const char *src, uint64_t sz)
+{
+ return bare_put_data(ctx, (uint8_t *) src, sz);
+}
+
+bare_error bare_get_str(struct bare_reader *ctx, char *dst, uint64_t sz)
+{
+ return bare_get_data(ctx, (uint8_t *) dst, sz);
+}
diff --git a/git2d/bare.h b/git2d/bare.h
new file mode 100644
index 0000000..e049dd0
--- /dev/null
+++ b/git2d/bare.h
@@ -0,0 +1,72 @@
+/*-
+ * SPDX-License-Identifier: MIT
+ * SPDX-FileCopyrightText: Copyright (c) 2022 Frank Smit <https://61924.nl/>
+ */
+
+#ifndef BARE_H
+#define BARE_H
+
+#include <stdint.h>
+#include <stdbool.h>
+
+typedef enum {
+ BARE_ERROR_NONE,
+ BARE_ERROR_WRITE_FAILED,
+ BARE_ERROR_READ_FAILED,
+ BARE_ERROR_BUFFER_TOO_SMALL,
+ BARE_ERROR_INVALID_UTF8,
+} bare_error;
+
+typedef bare_error(*bare_write_func) (void *buffer, const void *src, uint64_t sz);
+typedef bare_error(*bare_read_func) (void *buffer, void *dst, uint64_t sz);
+
+struct bare_writer {
+ void *buffer;
+ bare_write_func write;
+};
+
+struct bare_reader {
+ void *buffer;
+ bare_read_func read;
+};
+
+bare_error bare_put_uint(struct bare_writer *ctx, uint64_t x); /* varuint */
+bare_error bare_get_uint(struct bare_reader *ctx, uint64_t * x); /* varuint */
+bare_error bare_put_u8(struct bare_writer *ctx, uint8_t x);
+bare_error bare_get_u8(struct bare_reader *ctx, uint8_t * x);
+bare_error bare_put_u16(struct bare_writer *ctx, uint16_t x);
+bare_error bare_get_u16(struct bare_reader *ctx, uint16_t * x);
+bare_error bare_put_u32(struct bare_writer *ctx, uint32_t x);
+bare_error bare_get_u32(struct bare_reader *ctx, uint32_t * x);
+bare_error bare_put_u64(struct bare_writer *ctx, uint64_t x);
+bare_error bare_get_u64(struct bare_reader *ctx, uint64_t * x);
+
+bare_error bare_put_int(struct bare_writer *ctx, int64_t x); /* varint */
+bare_error bare_get_int(struct bare_reader *ctx, int64_t * x); /* varint */
+bare_error bare_put_i8(struct bare_writer *ctx, int8_t x);
+bare_error bare_get_i8(struct bare_reader *ctx, int8_t * x);
+bare_error bare_put_i16(struct bare_writer *ctx, int16_t x);
+bare_error bare_get_i16(struct bare_reader *ctx, int16_t * x);
+bare_error bare_put_i32(struct bare_writer *ctx, int32_t x);
+bare_error bare_get_i32(struct bare_reader *ctx, int32_t * x);
+bare_error bare_put_i64(struct bare_writer *ctx, int64_t x);
+bare_error bare_get_i64(struct bare_reader *ctx, int64_t * x);
+
+bare_error bare_put_f32(struct bare_writer *ctx, float x);
+bare_error bare_get_f32(struct bare_reader *ctx, float *x);
+bare_error bare_put_f64(struct bare_writer *ctx, double x);
+bare_error bare_get_f64(struct bare_reader *ctx, double *x);
+
+bare_error bare_put_bool(struct bare_writer *ctx, bool x);
+bare_error bare_get_bool(struct bare_reader *ctx, bool *x);
+
+bare_error bare_put_fixed_data(struct bare_writer *ctx, const uint8_t * src, uint64_t sz);
+bare_error bare_get_fixed_data(struct bare_reader *ctx, uint8_t * dst, uint64_t sz);
+bare_error bare_put_data(struct bare_writer *ctx, const uint8_t * src, uint64_t sz);
+bare_error bare_get_data(struct bare_reader *ctx, uint8_t * dst, uint64_t sz);
+bare_error bare_put_str(struct bare_writer *ctx, const char *src, uint64_t sz);
+bare_error bare_get_str(struct bare_reader *ctx, char *dst, uint64_t sz);
+
+/* Note that the _str implementation here does not check for UTF-8 validity. */
+
+#endif /* BARE_H */
diff --git a/git2d/cmd1.c b/git2d/cmd1.c
new file mode 100644
index 0000000..ec3d1ad
--- /dev/null
+++ b/git2d/cmd1.c
@@ -0,0 +1,124 @@
+/*-
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+ */
+
+#include "x.h"
+
+int cmd_index(git_repository *repo, struct bare_writer *writer)
+{
+ /* HEAD tree */
+
+ git_object *obj = NULL;
+ int err = git_revparse_single(&obj, repo, "HEAD^{tree}");
+ if (err != 0) {
+ bare_put_uint(writer, 4);
+ return -1;
+ }
+ git_tree *tree = (git_tree *) obj;
+
+ /* README */
+
+ git_tree_entry *entry = NULL;
+ err = git_tree_entry_bypath(&entry, tree, "README.md");
+ if (err != 0) {
+ bare_put_uint(writer, 5);
+ git_tree_free(tree);
+ return -1;
+ }
+ git_otype objtype = git_tree_entry_type(entry);
+ if (objtype != GIT_OBJECT_BLOB) {
+ bare_put_uint(writer, 6);
+ git_tree_entry_free(entry);
+ git_tree_free(tree);
+ return -1;
+ }
+ git_object *obj2 = NULL;
+ err = git_tree_entry_to_object(&obj2, repo, entry);
+ if (err != 0) {
+ bare_put_uint(writer, 7);
+ git_tree_entry_free(entry);
+ git_tree_free(tree);
+ return -1;
+ }
+ git_blob *blob = (git_blob *) obj2;
+ const void *content = git_blob_rawcontent(blob);
+ if (content == NULL) {
+ bare_put_uint(writer, 8);
+ git_blob_free(blob);
+ git_tree_entry_free(entry);
+ git_tree_free(tree);
+ return -1;
+ }
+ bare_put_uint(writer, 0);
+ bare_put_data(writer, content, git_blob_rawsize(blob));
+
+ /* Commits */
+
+ /* TODO BUG: This might be a different commit from the displayed README due to races */
+
+ git_revwalk *walker = NULL;
+ if (git_revwalk_new(&walker, repo) != 0) {
+ bare_put_uint(writer, 9);
+ git_blob_free(blob);
+ git_tree_entry_free(entry);
+ git_tree_free(tree);
+ return -1;
+ }
+
+ if (git_revwalk_push_head(walker) != 0) {
+ bare_put_uint(writer, 9);
+ git_revwalk_free(walker);
+ git_blob_free(blob);
+ git_tree_entry_free(entry);
+ git_tree_free(tree);
+ return -1;
+ }
+
+ int count = 0;
+ git_oid oid;
+ while (count < 3 && git_revwalk_next(&oid, walker) == 0) {
+ git_commit *commit = NULL;
+ if (git_commit_lookup(&commit, repo, &oid) != 0)
+ break;
+
+ const char *msg = git_commit_summary(commit);
+ const git_signature *author = git_commit_author(commit);
+
+ /* ID */
+ bare_put_data(writer, oid.id, GIT_OID_RAWSZ);
+
+ /* Title */
+ size_t msg_len = msg ? strlen(msg) : 0;
+ bare_put_data(writer, (const uint8_t *)(msg ? msg : ""), msg_len);
+
+ /* Author's name */
+ const char *author_name = author ? author->name : "";
+ bare_put_data(writer, (const uint8_t *)author_name, strlen(author_name));
+
+ /* Author's email */
+ const char *author_email = author ? author->email : "";
+ bare_put_data(writer, (const uint8_t *)author_email, strlen(author_email));
+
+ /* Author's date */
+ /* TODO: Pass the integer instead of a string */
+ time_t time = git_commit_time(commit);
+ char timebuf[64];
+ struct tm *tm = localtime(&time);
+ if (tm)
+ strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", tm);
+ else
+ strcpy(timebuf, "unknown");
+ bare_put_data(writer, (const uint8_t *)timebuf, strlen(timebuf));
+
+ git_commit_free(commit);
+ count++;
+ }
+
+ git_revwalk_free(walker);
+ git_blob_free(blob);
+ git_tree_entry_free(entry);
+ git_tree_free(tree);
+
+ return 0;
+}
diff --git a/git2d/cmd2.c b/git2d/cmd2.c
new file mode 100644
index 0000000..33947c6
--- /dev/null
+++ b/git2d/cmd2.c
@@ -0,0 +1,121 @@
+/*-
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+ */
+
+#include "x.h"
+
+int cmd_treeraw(git_repository *repo, struct bare_reader *reader, struct bare_writer *writer)
+{
+ /* Path */
+ char path[4096] = { 0 };
+ int err = bare_get_data(reader, (uint8_t *) path, sizeof(path) - 1);
+ if (err != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ return -1;
+ }
+ path[sizeof(path) - 1] = '\0';
+
+ /* HEAD^{tree} */
+ git_object *head_obj = NULL;
+ err = git_revparse_single(&head_obj, repo, "HEAD^{tree}");
+ if (err != 0) {
+ bare_put_uint(writer, 4);
+ return -1;
+ }
+ git_tree *tree = (git_tree *) head_obj;
+
+ /* Path in tree */
+ git_tree_entry *entry = NULL;
+ git_otype objtype;
+ if (strlen(path) == 0) {
+ entry = NULL;
+ objtype = GIT_OBJECT_TREE;
+ } else {
+ err = git_tree_entry_bypath(&entry, tree, path);
+ if (err != 0) {
+ bare_put_uint(writer, 3);
+ git_tree_free(tree);
+ return 0;
+ }
+ objtype = git_tree_entry_type(entry);
+ }
+
+ if (objtype == GIT_OBJECT_TREE) {
+ /* Tree */
+ git_object *tree_obj = NULL;
+ if (entry == NULL) {
+ tree_obj = (git_object *) tree;
+ } else {
+ err = git_tree_entry_to_object(&tree_obj, repo, entry);
+ if (err != 0) {
+ bare_put_uint(writer, 7);
+ goto cleanup;
+ }
+ }
+ git_tree *subtree = (git_tree *) tree_obj;
+
+ size_t count = git_tree_entrycount(subtree);
+ bare_put_uint(writer, 0);
+ bare_put_uint(writer, 1);
+ bare_put_uint(writer, count);
+ for (size_t i = 0; i < count; i++) {
+ const git_tree_entry *subentry = git_tree_entry_byindex(subtree, i);
+ const char *name = git_tree_entry_name(subentry);
+ git_otype type = git_tree_entry_type(subentry);
+ uint32_t mode = git_tree_entry_filemode(subentry);
+
+ uint8_t entry_type = 0;
+ uint64_t size = 0;
+
+ if (type == GIT_OBJECT_TREE) {
+ entry_type = 1;
+ } else if (type == GIT_OBJECT_BLOB) {
+ entry_type = 2;
+
+ git_object *subobj = NULL;
+ if (git_tree_entry_to_object(&subobj, repo, subentry) == 0) {
+ git_blob *b = (git_blob *) subobj;
+ size = git_blob_rawsize(b);
+ git_blob_free(b);
+ }
+ }
+
+ bare_put_uint(writer, entry_type);
+ bare_put_uint(writer, mode);
+ bare_put_uint(writer, size);
+ bare_put_data(writer, (const uint8_t *)name, strlen(name));
+ }
+ if (entry != NULL) {
+ git_tree_free(subtree);
+ }
+ } else if (objtype == GIT_OBJECT_BLOB) {
+ /* Blob */
+ git_object *blob_obj = NULL;
+ err = git_tree_entry_to_object(&blob_obj, repo, entry);
+ if (err != 0) {
+ bare_put_uint(writer, 7);
+ goto cleanup;
+ }
+ git_blob *blob = (git_blob *) blob_obj;
+ const void *content = git_blob_rawcontent(blob);
+ if (content == NULL) {
+ bare_put_uint(writer, 8);
+ git_blob_free(blob);
+ goto cleanup;
+ }
+ bare_put_uint(writer, 0);
+ bare_put_uint(writer, 2);
+ bare_put_data(writer, content, git_blob_rawsize(blob));
+ git_blob_free(blob);
+ } else {
+ /* Unknown */
+ bare_put_uint(writer, -1);
+ }
+
+ cleanup:
+ if (entry != NULL)
+ git_tree_entry_free(entry);
+ git_tree_free(tree);
+ return 0;
+}
diff --git a/git2d/cmd_commit.c b/git2d/cmd_commit.c
new file mode 100644
index 0000000..4d4d0bf
--- /dev/null
+++ b/git2d/cmd_commit.c
@@ -0,0 +1,403 @@
+/*-
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+ */
+
+#include "x.h"
+
+static int append_buf(char **data, size_t *len, size_t *cap, const char *src, size_t n)
+{
+ if (n == 0)
+ return 0;
+ size_t need = *len + n;
+ if (need > *cap) {
+ size_t newcap = *cap ? *cap * 2 : 256;
+ while (newcap < need)
+ newcap *= 2;
+ char *p = (char *)realloc(*data, newcap);
+ if (!p)
+ return -1;
+ *data = p;
+ *cap = newcap;
+ }
+ memcpy(*data + *len, src, n);
+ *len += n;
+ return 0;
+}
+
+int cmd_commit_tree_oid(git_repository *repo, struct bare_reader *reader, struct bare_writer *writer)
+{
+ char hex[64] = { 0 };
+ if (bare_get_data(reader, (uint8_t *) hex, sizeof(hex) - 1) != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ return -1;
+ }
+ git_oid oid;
+ if (git_oid_fromstr(&oid, hex) != 0) {
+ bare_put_uint(writer, 14);
+ return -1;
+ }
+ git_commit *commit = NULL;
+ if (git_commit_lookup(&commit, repo, &oid) != 0) {
+ bare_put_uint(writer, 14);
+ return -1;
+ }
+ git_tree *tree = NULL;
+ if (git_commit_tree(&tree, commit) != 0) {
+ git_commit_free(commit);
+ bare_put_uint(writer, 14);
+ return -1;
+ }
+ const git_oid *toid = git_tree_id(tree);
+ bare_put_uint(writer, 0);
+ bare_put_data(writer, toid->id, GIT_OID_RAWSZ);
+ git_tree_free(tree);
+ git_commit_free(commit);
+ return 0;
+}
+
+int cmd_commit_create(git_repository *repo, struct bare_reader *reader, struct bare_writer *writer)
+{
+ char treehex[64] = { 0 };
+ if (bare_get_data(reader, (uint8_t *) treehex, sizeof(treehex) - 1) != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ return -1;
+ }
+ git_oid tree_oid;
+ if (git_oid_fromstr(&tree_oid, treehex) != 0) {
+ bare_put_uint(writer, 15);
+ return -1;
+ }
+ uint64_t pcnt = 0;
+ if (bare_get_uint(reader, &pcnt) != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ return -1;
+ }
+ git_commit **parents = NULL;
+ if (pcnt > 0) {
+ parents = (git_commit **) calloc(pcnt, sizeof(git_commit *));
+ if (!parents) {
+ bare_put_uint(writer, 15);
+ return -1;
+ }
+ for (uint64_t i = 0; i < pcnt; i++) {
+ char phex[64] = { 0 };
+ if (bare_get_data(reader, (uint8_t *) phex, sizeof(phex) - 1) != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ goto fail;
+ }
+ git_oid poid;
+ if (git_oid_fromstr(&poid, phex) != 0) {
+ bare_put_uint(writer, 15);
+ goto fail;
+ }
+ if (git_commit_lookup(&parents[i], repo, &poid) != 0) {
+ bare_put_uint(writer, 15);
+ goto fail;
+ }
+ }
+ }
+ char aname[512] = { 0 };
+ char aemail[512] = { 0 };
+ if (bare_get_data(reader, (uint8_t *) aname, sizeof(aname) - 1) != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ goto fail;
+ }
+ if (bare_get_data(reader, (uint8_t *) aemail, sizeof(aemail) - 1) != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ goto fail;
+ }
+ int64_t when = 0;
+ int64_t tzoff = 0;
+ if (bare_get_i64(reader, &when) != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ goto fail;
+ }
+ if (bare_get_i64(reader, &tzoff) != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ goto fail;
+ }
+ char *message = NULL;
+ {
+ uint64_t msz = 0;
+ if (bare_get_uint(reader, &msz) != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ goto fail;
+ }
+ message = (char *)malloc(msz + 1);
+ if (!message) {
+ bare_put_uint(writer, 15);
+ goto fail;
+ }
+ if (bare_get_fixed_data(reader, (uint8_t *) message, msz) != BARE_ERROR_NONE) {
+ free(message);
+ bare_put_uint(writer, 11);
+ goto fail;
+ }
+ message[msz] = '\0';
+ }
+ git_signature *sig = NULL;
+ if (git_signature_new(&sig, aname, aemail, (git_time_t) when, (int)tzoff) != 0) {
+ free(message);
+ bare_put_uint(writer, 19);
+ goto fail;
+ }
+ git_tree *tree = NULL;
+ if (git_tree_lookup(&tree, repo, &tree_oid) != 0) {
+ git_signature_free(sig);
+ free(message);
+ bare_put_uint(writer, 19);
+ goto fail;
+ }
+ git_oid out;
+ int rc = git_commit_create(&out, repo, NULL, sig, sig, NULL, message, tree,
+ (int)pcnt, (const git_commit **)parents);
+ git_tree_free(tree);
+ git_signature_free(sig);
+ free(message);
+ if (rc != 0) {
+ bare_put_uint(writer, 19);
+ goto fail;
+ }
+ bare_put_uint(writer, 0);
+ bare_put_data(writer, out.id, GIT_OID_RAWSZ);
+ if (parents) {
+ for (uint64_t i = 0; i < pcnt; i++)
+ if (parents[i])
+ git_commit_free(parents[i]);
+ free(parents);
+ }
+ return 0;
+ fail:
+ if (parents) {
+ for (uint64_t i = 0; i < pcnt; i++)
+ if (parents[i])
+ git_commit_free(parents[i]);
+ free(parents);
+ }
+ return -1;
+}
+
+int cmd_update_ref(git_repository *repo, struct bare_reader *reader, struct bare_writer *writer)
+{
+ char refname[4096] = { 0 };
+ char commithex[64] = { 0 };
+ if (bare_get_data(reader, (uint8_t *) refname, sizeof(refname) - 1) != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ return -1;
+ }
+ if (bare_get_data(reader, (uint8_t *) commithex, sizeof(commithex) - 1)
+ != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ return -1;
+ }
+ git_oid oid;
+ if (git_oid_fromstr(&oid, commithex) != 0) {
+ bare_put_uint(writer, 18);
+ return -1;
+ }
+ git_reference *out = NULL;
+ int rc = git_reference_create(&out, repo, refname, &oid, 1, NULL);
+ if (rc != 0) {
+ bare_put_uint(writer, 18);
+ return -1;
+ }
+ git_reference_free(out);
+ bare_put_uint(writer, 0);
+ return 0;
+}
+
+int cmd_commit_info(git_repository *repo, struct bare_reader *reader, struct bare_writer *writer)
+{
+ char hex[64] = { 0 };
+ if (bare_get_data(reader, (uint8_t *) hex, sizeof(hex) - 1) != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ return -1;
+ }
+ git_oid oid;
+ if (git_oid_fromstr(&oid, hex) != 0) {
+ bare_put_uint(writer, 14);
+ return -1;
+ }
+ git_commit *commit = NULL;
+ if (git_commit_lookup(&commit, repo, &oid) != 0) {
+ bare_put_uint(writer, 14);
+ return -1;
+ }
+
+ const git_signature *author = git_commit_author(commit);
+ const git_signature *committer = git_commit_committer(commit);
+
+ const char *aname = author && author->name ? author->name : "";
+ const char *aemail = author && author->email ? author->email : "";
+ git_time_t awhen = author ? author->when.time : 0;
+ int aoffset = author ? author->when.offset : 0;
+
+ const char *cname = committer && committer->name ? committer->name : "";
+ const char *cemail = committer && committer->email ? committer->email : "";
+ git_time_t cwhen = committer ? committer->when.time : 0;
+ int coffset = committer ? committer->when.offset : 0;
+
+ const char *message = git_commit_message(commit);
+ if (!message) message = "";
+
+ bare_put_uint(writer, 0);
+ /* Commit ID */
+ const git_oid *cid = git_commit_id(commit);
+ bare_put_data(writer, cid->id, GIT_OID_RAWSZ);
+ /* Author */
+ bare_put_data(writer, (const uint8_t *)aname, strlen(aname));
+ bare_put_data(writer, (const uint8_t *)aemail, strlen(aemail));
+ bare_put_i64(writer, (int64_t)awhen);
+ bare_put_i64(writer, (int64_t)aoffset);
+ /* Committer */
+ bare_put_data(writer, (const uint8_t *)cname, strlen(cname));
+ bare_put_data(writer, (const uint8_t *)cemail, strlen(cemail));
+ bare_put_i64(writer, (int64_t)cwhen);
+ bare_put_i64(writer, (int64_t)coffset);
+ /* Message */
+ bare_put_data(writer, (const uint8_t *)message, strlen(message));
+ /* Parents */
+ uint32_t pcnt = git_commit_parentcount(commit);
+ bare_put_uint(writer, (uint64_t)pcnt);
+ for (uint32_t i = 0; i < pcnt; i++) {
+ const git_commit *p = NULL;
+ if (git_commit_parent((git_commit **)&p, commit, i) == 0 && p) {
+ const git_oid *po = git_commit_id(p);
+ bare_put_data(writer, po->id, GIT_OID_RAWSZ);
+ git_commit_free((git_commit *)p);
+ } else {
+ uint8_t zero[GIT_OID_RAWSZ] = {0};
+ bare_put_data(writer, zero, GIT_OID_RAWSZ);
+ }
+ }
+
+ /* Structured diff */
+ git_tree *tree = NULL;
+ if (git_commit_tree(&tree, commit) != 0) {
+ git_commit_free(commit);
+ bare_put_uint(writer, 15);
+ return -1;
+ }
+ git_diff *diff = NULL;
+ if (pcnt == 0) {
+ if (git_diff_tree_to_tree(&diff, repo, NULL, tree, NULL) != 0) {
+ git_tree_free(tree);
+ git_commit_free(commit);
+ bare_put_uint(writer, 15);
+ return -1;
+ }
+ } else {
+ git_commit *parent = NULL;
+ git_tree *ptree = NULL;
+ if (git_commit_parent(&parent, commit, 0) != 0 || git_commit_tree(&ptree, parent) != 0) {
+ if (parent) git_commit_free(parent);
+ git_tree_free(tree);
+ git_commit_free(commit);
+ bare_put_uint(writer, 15);
+ return -1;
+ }
+ if (git_diff_tree_to_tree(&diff, repo, ptree, tree, NULL) != 0) {
+ git_tree_free(ptree);
+ git_commit_free(parent);
+ git_tree_free(tree);
+ git_commit_free(commit);
+ bare_put_uint(writer, 15);
+ return -1;
+ }
+ git_tree_free(ptree);
+ git_commit_free(parent);
+ }
+
+ size_t files = git_diff_num_deltas(diff);
+ bare_put_uint(writer, (uint64_t)files);
+ for (size_t i = 0; i < files; i++) {
+ git_patch *patch = NULL;
+ if (git_patch_from_diff(&patch, diff, i) != 0) {
+ /* empty diff */
+ bare_put_uint(writer, 0);
+ bare_put_uint(writer, 0);
+ bare_put_data(writer, (const uint8_t *)"", 0);
+ bare_put_data(writer, (const uint8_t *)"", 0);
+ bare_put_uint(writer, 0);
+ continue;
+ }
+ const git_diff_delta *delta = git_patch_get_delta(patch);
+ uint32_t from_mode = delta ? delta->old_file.mode : 0;
+ uint32_t to_mode = delta ? delta->new_file.mode : 0;
+ const char *from_path = (delta && delta->old_file.path) ? delta->old_file.path : "";
+ const char *to_path = (delta && delta->new_file.path) ? delta->new_file.path : "";
+ bare_put_uint(writer, (uint64_t)from_mode);
+ bare_put_uint(writer, (uint64_t)to_mode);
+ bare_put_data(writer, (const uint8_t *)from_path, strlen(from_path));
+ bare_put_data(writer, (const uint8_t *)to_path, strlen(to_path));
+
+ size_t hunks = git_patch_num_hunks(patch);
+ uint64_t chunk_count = 0;
+ for (size_t h = 0; h < hunks; h++) {
+ const git_diff_hunk *hunk = NULL;
+ size_t lines = 0;
+ if (git_patch_get_hunk(&hunk, &lines, patch, h) != 0) continue;
+ int prev = -2;
+ for (size_t ln = 0; ln < lines; ln++) {
+ const git_diff_line *line = NULL;
+ if (git_patch_get_line_in_hunk(&line, patch, h, ln) != 0 || !line) continue;
+ int op = 0;
+ if (line->origin == '+') op = 1;
+ else if (line->origin == '-') op = 2;
+ else op = 0;
+ if (op != prev) { chunk_count++; prev = op; }
+ }
+ }
+ bare_put_uint(writer, chunk_count);
+ for (size_t h = 0; h < hunks; h++) {
+ const git_diff_hunk *hunk = NULL;
+ size_t lines = 0;
+ if (git_patch_get_hunk(&hunk, &lines, patch, h) != 0) continue;
+ int prev = -2;
+ struct {
+ char *data;
+ size_t len;
+ size_t cap;
+ } buf = {0};
+ for (size_t ln = 0; ln < lines; ln++) {
+ const git_diff_line *line = NULL;
+ if (git_patch_get_line_in_hunk(&line, patch, h, ln) != 0 || !line) continue;
+ int op = 0;
+ if (line->origin == '+') op = 1;
+ else if (line->origin == '-') op = 2;
+ else op = 0;
+ if (prev == -2) prev = op;
+ if (op != prev) {
+ bare_put_uint(writer, (uint64_t)prev);
+ bare_put_data(writer, (const uint8_t *)buf.data, buf.len);
+ free(buf.data);
+ buf.data = NULL; buf.len = 0; buf.cap = 0;
+ prev = op;
+ }
+ if (line->content && line->content_len > 0) {
+ if (append_buf(&buf.data, &buf.len, &buf.cap, line->content, line->content_len) != 0) {
+ free(buf.data);
+ git_patch_free(patch);
+ git_diff_free(diff);
+ git_tree_free(tree);
+ git_commit_free(commit);
+ bare_put_uint(writer, 15);
+ return -1;
+ }
+ }
+ }
+ if (prev != -2) {
+ bare_put_uint(writer, (uint64_t)prev);
+ bare_put_data(writer, (const uint8_t *)buf.data, buf.len);
+ free(buf.data);
+ }
+ }
+ git_patch_free(patch);
+ }
+
+ git_diff_free(diff);
+ git_tree_free(tree);
+ git_commit_free(commit);
+ return 0;
+}
diff --git a/git2d/cmd_diff.c b/git2d/cmd_diff.c
new file mode 100644
index 0000000..b32807e
--- /dev/null
+++ b/git2d/cmd_diff.c
@@ -0,0 +1,276 @@
+/*-
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+ */
+
+#include "x.h"
+
+static int diff_stats_to_string(git_diff *diff, git_buf *out)
+{
+ git_diff_stats *stats = NULL;
+ if (git_diff_get_stats(&stats, diff) != 0) {
+ return -1;
+ }
+ int rc = git_diff_stats_to_buf(out, stats, GIT_DIFF_STATS_FULL, 80);
+ git_diff_stats_free(stats);
+ return rc;
+}
+
+static void split_message(const char *message, char **title_out, char **body_out)
+{
+ *title_out = NULL;
+ *body_out = NULL;
+ if (!message)
+ return;
+ const char *nl = strchr(message, '\n');
+ if (!nl) {
+ *title_out = strdup(message);
+ *body_out = strdup("");
+ return;
+ }
+ size_t title_len = (size_t)(nl - message);
+ *title_out = (char *)malloc(title_len + 1);
+ if (*title_out) {
+ memcpy(*title_out, message, title_len);
+ (*title_out)[title_len] = '\0';
+ }
+ const char *rest = nl + 1;
+ if (*rest == '\n')
+ rest++;
+ *body_out = strdup(rest);
+}
+
+int cmd_format_patch(git_repository *repo, struct bare_reader *reader, struct bare_writer *writer)
+{
+ char hex[64] = { 0 };
+ if (bare_get_data(reader, (uint8_t *) hex, sizeof(hex) - 1) != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ return -1;
+ }
+ git_oid oid;
+ if (git_oid_fromstr(&oid, hex) != 0) {
+ bare_put_uint(writer, 14);
+ return -1;
+ }
+
+ git_commit *commit = NULL;
+ if (git_commit_lookup(&commit, repo, &oid) != 0) {
+ bare_put_uint(writer, 14);
+ return -1;
+ }
+
+ git_tree *tree = NULL;
+ if (git_commit_tree(&tree, commit) != 0) {
+ git_commit_free(commit);
+ bare_put_uint(writer, 14);
+ return -1;
+ }
+
+ git_diff *diff = NULL;
+ if (git_commit_parentcount(commit) == 0) {
+ if (git_diff_tree_to_tree(&diff, repo, NULL, tree, NULL) != 0) {
+ git_tree_free(tree);
+ git_commit_free(commit);
+ bare_put_uint(writer, 15);
+ return -1;
+ }
+ } else {
+ git_commit *parent = NULL;
+ git_tree *ptree = NULL;
+ if (git_commit_parent(&parent, commit, 0) != 0 || git_commit_tree(&ptree, parent) != 0) {
+ if (parent)
+ git_commit_free(parent);
+ git_tree_free(tree);
+ git_commit_free(commit);
+ bare_put_uint(writer, 15);
+ return -1;
+ }
+ if (git_diff_tree_to_tree(&diff, repo, ptree, tree, NULL) != 0) {
+ git_tree_free(ptree);
+ git_commit_free(parent);
+ git_tree_free(tree);
+ git_commit_free(commit);
+ bare_put_uint(writer, 15);
+ return -1;
+ }
+ git_tree_free(ptree);
+ git_commit_free(parent);
+ }
+
+ git_buf stats = { 0 };
+ if (diff_stats_to_string(diff, &stats) != 0) {
+ git_diff_free(diff);
+ git_tree_free(tree);
+ git_commit_free(commit);
+ bare_put_uint(writer, 15);
+ return -1;
+ }
+
+ git_buf patch = { 0 };
+ if (git_diff_to_buf(&patch, diff, GIT_DIFF_FORMAT_PATCH) != 0) {
+ git_buf_dispose(&stats);
+ git_diff_free(diff);
+ git_tree_free(tree);
+ git_commit_free(commit);
+ bare_put_uint(writer, 15);
+ return -1;
+ }
+
+ const git_signature *author = git_commit_author(commit);
+ char *title = NULL, *body = NULL;
+ split_message(git_commit_message(commit), &title, &body);
+
+ char header[2048];
+ char timebuf[64];
+ {
+ time_t t = git_commit_time(commit);
+ struct tm *tm = localtime(&t);
+ if (tm)
+ strftime(timebuf, sizeof(timebuf), "%a, %d %b %Y %H:%M:%S %z", tm);
+ else
+ strcpy(timebuf, "unknown");
+ }
+ snprintf(header, sizeof(header), "From %s Mon Sep 17 00:00:00 2001\nFrom: %s <%s>\nDate: %s\nSubject: [PATCH] %s\n\n", git_oid_tostr_s(&oid), author && author->name ? author->name : "", author && author->email ? author->email : "", timebuf, title ? title : "");
+
+ const char *trailer = "\n-- \n2.48.1\n";
+ size_t header_len = strlen(header);
+ size_t body_len = body ? strlen(body) : 0;
+ size_t trailer_len = strlen(trailer);
+ size_t total = header_len + body_len + (body_len ? 1 : 0) + 4 + stats.size + 1 + patch.size + trailer_len;
+
+ uint8_t *buf = (uint8_t *) malloc(total);
+ if (!buf) {
+ free(title);
+ free(body);
+ git_buf_dispose(&patch);
+ git_buf_dispose(&stats);
+ git_diff_free(diff);
+ git_tree_free(tree);
+ git_commit_free(commit);
+ bare_put_uint(writer, 15);
+ return -1;
+ }
+ size_t off = 0;
+ memcpy(buf + off, header, header_len);
+ off += header_len;
+ if (body_len) {
+ memcpy(buf + off, body, body_len);
+ off += body_len;
+ buf[off++] = '\n';
+ }
+ memcpy(buf + off, "---\n", 4);
+ off += 4;
+ memcpy(buf + off, stats.ptr, stats.size);
+ off += stats.size;
+ buf[off++] = '\n';
+ memcpy(buf + off, patch.ptr, patch.size);
+ off += patch.size;
+ memcpy(buf + off, trailer, trailer_len);
+ off += trailer_len;
+
+ bare_put_uint(writer, 0);
+ bare_put_data(writer, buf, off);
+
+ free(buf);
+ free(title);
+ free(body);
+ git_buf_dispose(&patch);
+ git_buf_dispose(&stats);
+ git_diff_free(diff);
+ git_tree_free(tree);
+ git_commit_free(commit);
+ return 0;
+}
+
+int cmd_merge_base(git_repository *repo, struct bare_reader *reader, struct bare_writer *writer)
+{
+ char hex1[64] = { 0 };
+ char hex2[64] = { 0 };
+ if (bare_get_data(reader, (uint8_t *) hex1, sizeof(hex1) - 1) != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ return -1;
+ }
+ if (bare_get_data(reader, (uint8_t *) hex2, sizeof(hex2) - 1) != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ return -1;
+ }
+ git_oid a, b, out;
+ if (git_oid_fromstr(&a, hex1) != 0 || git_oid_fromstr(&b, hex2) != 0) {
+ bare_put_uint(writer, 17);
+ return -1;
+ }
+ int rc = git_merge_base(&out, repo, &a, &b);
+ if (rc == GIT_ENOTFOUND) {
+ bare_put_uint(writer, 16);
+ return -1;
+ }
+ if (rc != 0) {
+ bare_put_uint(writer, 17);
+ return -1;
+ }
+ bare_put_uint(writer, 0);
+ bare_put_data(writer, out.id, GIT_OID_RAWSZ);
+ return 0;
+}
+
+int cmd_log(git_repository *repo, struct bare_reader *reader, struct bare_writer *writer)
+{
+ char spec[4096] = { 0 };
+ uint64_t limit = 0;
+ if (bare_get_data(reader, (uint8_t *) spec, sizeof(spec) - 1) != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ return -1;
+ }
+ if (bare_get_uint(reader, &limit) != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ return -1;
+ }
+
+ git_object *obj = NULL;
+ if (spec[0] == '\0')
+ strcpy(spec, "HEAD");
+ if (git_revparse_single(&obj, repo, spec) != 0) {
+ bare_put_uint(writer, 4);
+ return -1;
+ }
+ git_commit *start = (git_commit *) obj;
+
+ git_revwalk *walk = NULL;
+ if (git_revwalk_new(&walk, repo) != 0) {
+ git_commit_free(start);
+ bare_put_uint(writer, 9);
+ return -1;
+ }
+ git_revwalk_sorting(walk, GIT_SORT_TIME);
+ git_revwalk_push(walk, git_commit_id(start));
+ git_commit_free(start);
+
+ bare_put_uint(writer, 0);
+ git_oid oid;
+ uint64_t count = 0;
+ while ((limit == 0 || count < limit)
+ && git_revwalk_next(&oid, walk) == 0) {
+ git_commit *c = NULL;
+ if (git_commit_lookup(&c, repo, &oid) != 0)
+ break;
+ const char *msg = git_commit_summary(c);
+ const git_signature *author = git_commit_author(c);
+ time_t t = git_commit_time(c);
+ char timebuf[64];
+ struct tm *tm = localtime(&t);
+ if (tm)
+ strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", tm);
+ else
+ strcpy(timebuf, "unknown");
+
+ bare_put_data(writer, oid.id, GIT_OID_RAWSZ);
+ bare_put_data(writer, (const uint8_t *)(msg ? msg : ""), msg ? strlen(msg) : 0);
+ bare_put_data(writer, (const uint8_t *)(author && author->name ? author->name : ""), author && author->name ? strlen(author->name) : 0);
+ bare_put_data(writer, (const uint8_t *)(author && author->email ? author->email : ""), author && author->email ? strlen(author->email) : 0);
+ bare_put_data(writer, (const uint8_t *)timebuf, strlen(timebuf));
+ git_commit_free(c);
+ count++;
+ }
+ git_revwalk_free(walk);
+ return 0;
+}
diff --git a/git2d/cmd_init.c b/git2d/cmd_init.c
new file mode 100644
index 0000000..962d229
--- /dev/null
+++ b/git2d/cmd_init.c
@@ -0,0 +1,65 @@
+/*-
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+ */
+
+#include "x.h"
+
+int cmd_init_repo(const char *path, struct bare_reader *reader, struct bare_writer *writer)
+{
+ char hooks[4096] = { 0 };
+ if (bare_get_data(reader, (uint8_t *) hooks, sizeof(hooks) - 1) != BARE_ERROR_NONE) {
+ fprintf(stderr, "init_repo: protocol error reading hooks for path '%s'\n", path);
+ bare_put_uint(writer, 11);
+ return -1;
+ }
+
+ fprintf(stderr, "init_repo: starting for path='%s' hooks='%s'\n", path, hooks);
+
+ if (mkdir(path, 0700) != 0 && errno != EEXIST) {
+ fprintf(stderr, "init_repo: mkdir failed for '%s': %s\n", path, strerror(errno));
+ bare_put_uint(writer, 24);
+ return -1;
+ }
+
+ git_repository *repo = NULL;
+ git_repository_init_options opts;
+ git_repository_init_options_init(&opts, GIT_REPOSITORY_INIT_OPTIONS_VERSION);
+ opts.flags = GIT_REPOSITORY_INIT_BARE;
+ if (git_repository_init_ext(&repo, path, &opts) != 0) {
+ const git_error *ge = git_error_last();
+ fprintf(stderr, "init_repo: git_repository_init_ext failed: %s (klass=%d)\n", ge && ge->message ? ge->message : "(no message)", ge ? ge->klass : 0);
+ bare_put_uint(writer, 20);
+ return -1;
+ }
+ git_config *cfg = NULL;
+ if (git_repository_config(&cfg, repo) != 0) {
+ git_repository_free(repo);
+ const git_error *ge = git_error_last();
+ fprintf(stderr, "init_repo: open config failed: %s (klass=%d)\n", ge && ge->message ? ge->message : "(no message)", ge ? ge->klass : 0);
+ bare_put_uint(writer, 21);
+ return -1;
+ }
+ if (git_config_set_string(cfg, "core.hooksPath", hooks) != 0) {
+ git_config_free(cfg);
+ git_repository_free(repo);
+ const git_error *ge = git_error_last();
+ fprintf(stderr, "init_repo: set hooksPath failed: %s (klass=%d) hooks='%s'\n", ge && ge->message ? ge->message : "(no message)", ge ? ge->klass : 0, hooks);
+ bare_put_uint(writer, 22);
+ return -1;
+ }
+ if (git_config_set_bool(cfg, "receive.advertisePushOptions", 1) != 0) {
+ git_config_free(cfg);
+ git_repository_free(repo);
+ const git_error *ge = git_error_last();
+ fprintf(stderr, "init_repo: set advertisePushOptions failed: %s (klass=%d)\n", ge && ge->message ? ge->message : "(no message)", ge ? ge->klass : 0);
+ bare_put_uint(writer, 23);
+ return -1;
+ }
+ git_config_free(cfg);
+
+ git_repository_free(repo);
+ fprintf(stderr, "init_repo: success for path='%s'\n", path);
+ bare_put_uint(writer, 0);
+ return 0;
+}
diff --git a/git2d/cmd_ref.c b/git2d/cmd_ref.c
new file mode 100644
index 0000000..f4bae4a
--- /dev/null
+++ b/git2d/cmd_ref.c
@@ -0,0 +1,113 @@
+/*-
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+ */
+
+#include "x.h"
+
+static int write_oid(struct bare_writer *writer, const git_oid *oid)
+{
+ return bare_put_data(writer, oid->id, GIT_OID_RAWSZ) == BARE_ERROR_NONE ? 0 : -1;
+}
+
+int cmd_resolve_ref(git_repository *repo, struct bare_reader *reader, struct bare_writer *writer)
+{
+ char type[32] = { 0 };
+ char name[4096] = { 0 };
+ if (bare_get_data(reader, (uint8_t *) type, sizeof(type) - 1) != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ return -1;
+ }
+ if (bare_get_data(reader, (uint8_t *) name, sizeof(name) - 1) != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ return -1;
+ }
+
+ git_oid oid = { 0 };
+ int err = 0;
+
+ if (type[0] == '\0') {
+ git_object *obj = NULL;
+ err = git_revparse_single(&obj, repo, "HEAD^{commit}");
+ if (err != 0) {
+ bare_put_uint(writer, 12);
+ return -1;
+ }
+ git_commit *c = (git_commit *) obj;
+ git_oid_cpy(&oid, git_commit_id(c));
+ git_commit_free(c);
+ } else if (strcmp(type, "commit") == 0) {
+ err = git_oid_fromstr(&oid, name);
+ if (err != 0) {
+ bare_put_uint(writer, 12);
+ return -1;
+ }
+ } else if (strcmp(type, "branch") == 0) {
+ char fullref[4608];
+ snprintf(fullref, sizeof(fullref), "refs/heads/%s", name);
+ git_object *obj = NULL;
+ err = git_revparse_single(&obj, repo, fullref);
+ if (err != 0) {
+ bare_put_uint(writer, 12);
+ return -1;
+ }
+ git_commit *c = (git_commit *) obj;
+ git_oid_cpy(&oid, git_commit_id(c));
+ git_commit_free(c);
+ } else if (strcmp(type, "tag") == 0) {
+ char spec[4608];
+ snprintf(spec, sizeof(spec), "refs/tags/%s^{commit}", name);
+ git_object *obj = NULL;
+ err = git_revparse_single(&obj, repo, spec);
+ if (err != 0) {
+ bare_put_uint(writer, 12);
+ return -1;
+ }
+ git_commit *c = (git_commit *) obj;
+ git_oid_cpy(&oid, git_commit_id(c));
+ git_commit_free(c);
+ } else {
+ bare_put_uint(writer, 12);
+ return -1;
+ }
+
+ bare_put_uint(writer, 0);
+ return write_oid(writer, &oid);
+}
+
+int cmd_list_branches(git_repository *repo, struct bare_writer *writer)
+{
+ git_branch_iterator *it = NULL;
+ int err = git_branch_iterator_new(&it, repo, GIT_BRANCH_LOCAL);
+ if (err != 0) {
+ bare_put_uint(writer, 13);
+ return -1;
+ }
+ size_t count = 0;
+ git_reference *ref;
+ git_branch_t type;
+ while (git_branch_next(&ref, &type, it) == 0) {
+ count++;
+ git_reference_free(ref);
+ }
+ git_branch_iterator_free(it);
+
+ err = git_branch_iterator_new(&it, repo, GIT_BRANCH_LOCAL);
+ if (err != 0) {
+ bare_put_uint(writer, 13);
+ return -1;
+ }
+
+ bare_put_uint(writer, 0);
+ bare_put_uint(writer, count);
+ while (git_branch_next(&ref, &type, it) == 0) {
+ const char *name = NULL;
+ git_branch_name(&name, ref);
+ if (name == NULL)
+ name = "";
+ bare_put_data(writer, (const uint8_t *)name, strlen(name));
+ git_reference_free(ref);
+ }
+ git_branch_iterator_free(it);
+ return 0;
+}
diff --git a/git2d/cmd_tree.c b/git2d/cmd_tree.c
new file mode 100644
index 0000000..d18e817
--- /dev/null
+++ b/git2d/cmd_tree.c
@@ -0,0 +1,120 @@
+/*-
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+ */
+
+#include "x.h"
+
+int cmd_tree_list_by_oid(git_repository *repo, struct bare_reader *reader, struct bare_writer *writer)
+{
+ char hex[64] = { 0 };
+ if (bare_get_data(reader, (uint8_t *) hex, sizeof(hex) - 1) != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ return -1;
+ }
+ git_oid oid;
+ if (git_oid_fromstr(&oid, hex) != 0) {
+ bare_put_uint(writer, 4);
+ return -1;
+ }
+ git_tree *tree = NULL;
+ if (git_tree_lookup(&tree, repo, &oid) != 0) {
+ bare_put_uint(writer, 4);
+ return -1;
+ }
+ size_t count = git_tree_entrycount(tree);
+ bare_put_uint(writer, 0);
+ bare_put_uint(writer, count);
+ for (size_t i = 0; i < count; i++) {
+ const git_tree_entry *e = git_tree_entry_byindex(tree, i);
+ const char *name = git_tree_entry_name(e);
+ uint32_t mode = git_tree_entry_filemode(e);
+ const git_oid *id = git_tree_entry_id(e);
+ bare_put_uint(writer, mode);
+ bare_put_data(writer, (const uint8_t *)name, strlen(name));
+ bare_put_data(writer, id->id, GIT_OID_RAWSZ);
+ }
+ git_tree_free(tree);
+ return 0;
+}
+
+int cmd_write_tree(git_repository *repo, struct bare_reader *reader, struct bare_writer *writer)
+{
+ uint64_t count = 0;
+ if (bare_get_uint(reader, &count) != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ return -1;
+ }
+ git_treebuilder *bld = NULL;
+ if (git_treebuilder_new(&bld, repo, NULL) != 0) {
+ bare_put_uint(writer, 15);
+ return -1;
+ }
+ for (uint64_t i = 0; i < count; i++) {
+ uint64_t mode = 0;
+ if (bare_get_uint(reader, &mode) != BARE_ERROR_NONE) {
+ git_treebuilder_free(bld);
+ bare_put_uint(writer, 11);
+ return -1;
+ }
+ char name[4096] = { 0 };
+ if (bare_get_data(reader, (uint8_t *) name, sizeof(name) - 1) != BARE_ERROR_NONE) {
+ git_treebuilder_free(bld);
+ bare_put_uint(writer, 11);
+ return -1;
+ }
+ uint8_t idraw[GIT_OID_RAWSZ] = { 0 };
+ if (bare_get_fixed_data(reader, idraw, GIT_OID_RAWSZ) != BARE_ERROR_NONE) {
+ git_treebuilder_free(bld);
+ bare_put_uint(writer, 11);
+ return -1;
+ }
+ git_oid id;
+ memcpy(id.id, idraw, GIT_OID_RAWSZ);
+ git_filemode_t fm = (git_filemode_t) mode;
+ if (git_treebuilder_insert(NULL, bld, name, &id, fm) != 0) {
+ git_treebuilder_free(bld);
+ bare_put_uint(writer, 15);
+ return -1;
+ }
+ }
+ git_oid out;
+ if (git_treebuilder_write(&out, bld) != 0) {
+ git_treebuilder_free(bld);
+ bare_put_uint(writer, 15);
+ return -1;
+ }
+ git_treebuilder_free(bld);
+ bare_put_uint(writer, 0);
+ bare_put_data(writer, out.id, GIT_OID_RAWSZ);
+ return 0;
+}
+
+int cmd_blob_write(git_repository *repo, struct bare_reader *reader, struct bare_writer *writer)
+{
+ uint64_t sz = 0;
+ if (bare_get_uint(reader, &sz) != BARE_ERROR_NONE) {
+ bare_put_uint(writer, 11);
+ return -1;
+ }
+ uint8_t *data = (uint8_t *) malloc(sz);
+ if (!data) {
+ bare_put_uint(writer, 15);
+ return -1;
+ }
+ if (bare_get_fixed_data(reader, data, sz) != BARE_ERROR_NONE) {
+ free(data);
+ bare_put_uint(writer, 11);
+ return -1;
+ }
+ git_oid oid;
+ if (git_blob_create_frombuffer(&oid, repo, data, sz) != 0) {
+ free(data);
+ bare_put_uint(writer, 15);
+ return -1;
+ }
+ free(data);
+ bare_put_uint(writer, 0);
+ bare_put_data(writer, oid.id, GIT_OID_RAWSZ);
+ return 0;
+}
diff --git a/git2d/main.c b/git2d/main.c
new file mode 100644
index 0000000..8518960
--- /dev/null
+++ b/git2d/main.c
@@ -0,0 +1,81 @@
+/*-
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+ */
+
+/*
+ * TODO: Pool repositories (and take care of thread safety)
+ * libgit2 has a nice builtin per-repo cache that we could utilize this way.
+ */
+
+#include "x.h"
+
+int main(int argc, char **argv)
+{
+ if (argc != 2) {
+ errx(1, "provide one argument: the socket path");
+ }
+
+ signal(SIGPIPE, SIG_IGN);
+
+ git_libgit2_init();
+
+ int sock;
+ if ((sock = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0)) < 0)
+ err(1, "socket");
+
+ struct sockaddr_un addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ strcpy(addr.sun_path, argv[1]);
+
+ umask(0077);
+
+ if (bind(sock, (struct sockaddr *)&addr, sizeof(struct sockaddr_un))) {
+ if (errno == EADDRINUSE) {
+ unlink(argv[1]);
+ if (bind(sock, (struct sockaddr *)&addr, sizeof(struct sockaddr_un)))
+ err(1, "bind");
+ } else {
+ err(1, "bind");
+ }
+ }
+
+ listen(sock, 128);
+
+ pthread_attr_t pthread_attr;
+
+ if (pthread_attr_init(&pthread_attr) != 0)
+ err(1, "pthread_attr_init");
+
+ if (pthread_attr_setdetachstate(&pthread_attr, PTHREAD_CREATE_DETACHED)
+ != 0)
+ err(1, "pthread_attr_setdetachstate");
+
+ for (;;) {
+ int *conn = malloc(sizeof(int));
+ if (conn == NULL) {
+ warn("malloc");
+ continue;
+ }
+
+ *conn = accept(sock, 0, 0);
+ if (*conn == -1) {
+ free(conn);
+ warn("accept");
+ continue;
+ }
+
+ pthread_t thread;
+
+ if (pthread_create(&thread, &pthread_attr, session, (void *)conn) != 0) {
+ close(*conn);
+ free(conn);
+ warn("pthread_create");
+ }
+ }
+
+ close(sock);
+
+ git_libgit2_shutdown();
+}
diff --git a/git2d/rw.c b/git2d/rw.c
new file mode 100644
index 0000000..09398c2
--- /dev/null
+++ b/git2d/rw.c
@@ -0,0 +1,34 @@
+/*-
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+ */
+
+#include "x.h"
+
+bare_error conn_read(void *buffer, void *dst, uint64_t sz)
+{
+ conn_io_t *io = buffer;
+ ssize_t rsz = read(io->fd, dst, sz);
+ return (rsz == (ssize_t) sz) ? BARE_ERROR_NONE : BARE_ERROR_READ_FAILED;
+}
+
+bare_error conn_write(void *buffer, const void *src, uint64_t sz)
+{
+ conn_io_t *io = buffer;
+ const uint8_t *data = src;
+ uint64_t total = 0;
+
+ while (total < sz) {
+ ssize_t written = write(io->fd, data + total, sz - total);
+ if (written < 0) {
+ if (errno == EINTR)
+ continue;
+ return BARE_ERROR_WRITE_FAILED;
+ }
+ if (written == 0)
+ break;
+ total += written;
+ }
+
+ return (total == sz) ? BARE_ERROR_NONE : BARE_ERROR_WRITE_FAILED;
+}
diff --git a/git2d/session.c b/git2d/session.c
new file mode 100644
index 0000000..c757640
--- /dev/null
+++ b/git2d/session.c
@@ -0,0 +1,143 @@
+/*-
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+ */
+
+#include "x.h"
+
+void *session(void *_conn)
+{
+ int conn = *(int *)_conn;
+ free((int *)_conn);
+
+ int err;
+
+ conn_io_t io = {.fd = conn };
+ struct bare_reader reader = {
+ .buffer = &io,
+ .read = conn_read,
+ };
+ struct bare_writer writer = {
+ .buffer = &io,
+ .write = conn_write,
+ };
+
+ /* Repo path */
+ char path[4096] = { 0 };
+ err = bare_get_data(&reader, (uint8_t *) path, sizeof(path) - 1);
+ if (err != BARE_ERROR_NONE) {
+ goto close;
+ }
+ path[sizeof(path) - 1] = '\0';
+ fprintf(stderr, "session: path='%s'\n", path);
+
+ /* Command */
+ uint64_t cmd = 0;
+ err = bare_get_uint(&reader, &cmd);
+ if (err != BARE_ERROR_NONE) {
+ bare_put_uint(&writer, 2);
+ goto close;
+ }
+ fprintf(stderr, "session: cmd=%llu\n", (unsigned long long)cmd);
+
+ /* Repo init does not require opening an existing repo so let's just do it here */
+ if (cmd == 15) {
+ fprintf(stderr, "session: handling init for '%s'\n", path);
+ if (cmd_init_repo(path, &reader, &writer) != 0) {
+ }
+ goto close;
+ }
+
+ git_repository *repo = NULL;
+ err = git_repository_open_ext(&repo, path, GIT_REPOSITORY_OPEN_NO_SEARCH | GIT_REPOSITORY_OPEN_BARE | GIT_REPOSITORY_OPEN_NO_DOTGIT, NULL);
+ if (err != 0) {
+ bare_put_uint(&writer, 1);
+ goto close;
+ }
+ switch (cmd) {
+ case 1:
+ err = cmd_index(repo, &writer);
+ if (err != 0)
+ goto free_repo;
+ break;
+ case 2:
+ err = cmd_treeraw(repo, &reader, &writer);
+ if (err != 0)
+ goto free_repo;
+ break;
+ case 3:
+ err = cmd_resolve_ref(repo, &reader, &writer);
+ if (err != 0)
+ goto free_repo;
+ break;
+ case 4:
+ err = cmd_list_branches(repo, &writer);
+ if (err != 0)
+ goto free_repo;
+ break;
+ case 5:
+ err = cmd_format_patch(repo, &reader, &writer);
+ if (err != 0)
+ goto free_repo;
+ break;
+case 6:
+ err = cmd_commit_info(repo, &reader, &writer);
+ if (err != 0)
+ goto free_repo;
+ break;
+ case 7:
+ err = cmd_merge_base(repo, &reader, &writer);
+ if (err != 0)
+ goto free_repo;
+ break;
+ case 8:
+ err = cmd_log(repo, &reader, &writer);
+ if (err != 0)
+ goto free_repo;
+ break;
+ case 9:
+ err = cmd_tree_list_by_oid(repo, &reader, &writer);
+ if (err != 0)
+ goto free_repo;
+ break;
+ case 10:
+ err = cmd_write_tree(repo, &reader, &writer);
+ if (err != 0)
+ goto free_repo;
+ break;
+ case 11:
+ err = cmd_blob_write(repo, &reader, &writer);
+ if (err != 0)
+ goto free_repo;
+ break;
+ case 12:
+ err = cmd_commit_tree_oid(repo, &reader, &writer);
+ if (err != 0)
+ goto free_repo;
+ break;
+ case 13:
+ err = cmd_commit_create(repo, &reader, &writer);
+ if (err != 0)
+ goto free_repo;
+ break;
+ case 14:
+ err = cmd_update_ref(repo, &reader, &writer);
+ if (err != 0)
+ goto free_repo;
+ break;
+ case 0:
+ bare_put_uint(&writer, 3);
+ goto free_repo;
+ default:
+ bare_put_uint(&writer, 3);
+ goto free_repo;
+ }
+
+ free_repo:
+ git_repository_free(repo);
+
+ close:
+ close(conn);
+
+ return NULL;
+}
diff --git a/git2d/x.h b/git2d/x.h
new file mode 100644
index 0000000..972e60b
--- /dev/null
+++ b/git2d/x.h
@@ -0,0 +1,55 @@
+/*-
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+ */
+
+#ifndef X_H
+#define X_H
+
+#include <err.h>
+#include <errno.h>
+#include <git2.h>
+#include <git2/buffer.h>
+#include <pthread.h>
+#include <signal.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/un.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "bare.h"
+
+typedef struct {
+ int fd;
+} conn_io_t;
+
+bare_error conn_read(void *buffer, void *dst, uint64_t sz);
+bare_error conn_write(void *buffer, const void *src, uint64_t sz);
+
+void *session(void *_conn);
+
+int cmd_index(git_repository * repo, struct bare_writer *writer);
+int cmd_treeraw(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer);
+
+int cmd_resolve_ref(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer);
+int cmd_list_branches(git_repository * repo, struct bare_writer *writer);
+int cmd_format_patch(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer);
+int cmd_merge_base(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer);
+int cmd_log(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer);
+
+int cmd_tree_list_by_oid(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer);
+int cmd_write_tree(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer);
+int cmd_blob_write(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer);
+
+int cmd_commit_tree_oid(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer);
+int cmd_commit_create(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer);
+int cmd_update_ref(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer);
+int cmd_commit_info(git_repository * repo, struct bare_reader *reader, struct bare_writer *writer);
+
+int cmd_init_repo(const char *path, struct bare_reader *reader, struct bare_writer *writer);
+
+#endif // X_H