diff options
Diffstat (limited to 'git2d')
-rw-r--r-- | git2d/bare.c | 300 | ||||
-rw-r--r-- | git2d/bare.h | 72 | ||||
-rw-r--r-- | git2d/cmd1.c | 124 | ||||
-rw-r--r-- | git2d/cmd2.c | 121 | ||||
-rw-r--r-- | git2d/cmd_commit.c | 403 | ||||
-rw-r--r-- | git2d/cmd_diff.c | 276 | ||||
-rw-r--r-- | git2d/cmd_init.c | 65 | ||||
-rw-r--r-- | git2d/cmd_ref.c | 113 | ||||
-rw-r--r-- | git2d/cmd_tree.c | 120 | ||||
-rw-r--r-- | git2d/main.c | 81 | ||||
-rw-r--r-- | git2d/rw.c | 34 | ||||
-rw-r--r-- | git2d/session.c | 143 | ||||
-rw-r--r-- | git2d/x.h | 55 |
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 |