Lindenii Project Forge
git2d: Factor commands out into their own files
#include "x.h" int cmd1(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 */ 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; }
/*- * 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'; /* Open repo */ 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; } /* Command */ uint64_t cmd = 0; err = bare_get_uint(&reader, &cmd); if (err != BARE_ERROR_NONE) { bare_put_uint(&writer, 2); goto free_repo; } switch (cmd) { case 1:
err = cmd1(repo, &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; }
/* HEAD tree */ git_object *obj = NULL; err = git_revparse_single(&obj, repo, "HEAD^{tree}"); if (err != 0) { bare_put_uint(&writer, 4); goto free_repo; } 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); goto free_tree; } git_otype objtype = git_tree_entry_type(entry); if (objtype != GIT_OBJECT_BLOB) { bare_put_uint(&writer, 6); goto free_tree_entry; } git_object *obj2 = NULL; err = git_tree_entry_to_object(&obj2, repo, entry); if (err != 0) { bare_put_uint(&writer, 7); goto free_tree_entry; } git_blob *blob = (git_blob *) obj2; const void *content = git_blob_rawcontent(blob); if (content == NULL) { bare_put_uint(&writer, 8); goto free_blob; } bare_put_uint(&writer, 0); bare_put_data(&writer, content, git_blob_rawsize(blob)); /* Commits */ git_revwalk *walker = NULL; if (git_revwalk_new(&walker, repo) != 0) { bare_put_uint(&writer, 9); goto free_blob; } if (git_revwalk_push_head(walker) != 0) { bare_put_uint(&writer, 9); goto free_blob; } 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); free_blob: git_blob_free(blob); free_tree_entry: git_tree_entry_free(entry); free_tree: git_tree_free(tree);
free_repo: git_repository_free(repo); close: close(conn); return NULL; }
/*- * SPDX-License-Identifier: AGPL-3.0-only * SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> */ #include <err.h> #include <errno.h> #include <git2.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" #ifndef X_H #define X_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 cmd1(git_repository *repo, struct bare_writer *writer);
#endif // X_H
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package main import ( "errors" "flag"
"log"
"net" "net/http" "os/exec" "syscall" "time" "go.lindenii.runxiyu.org/lindenii-common/clog" ) func main() { configPath := flag.String( "config", "/etc/lindenii/forge.scfg", "path to configuration file", ) flag.Parse() if err := loadConfig(*configPath); err != nil { clog.Fatal(1, "Loading configuration: "+err.Error()) } if err := deployHooks(); err != nil { clog.Fatal(1, "Deploying hooks to filesystem: "+err.Error()) } if err := loadTemplates(); err != nil { clog.Fatal(1, "Loading templates: "+err.Error()) } if err := deployGit2D(); err != nil { clog.Fatal(1, "Deploying git2d: "+err.Error()) } // Launch Git2D go func() { cmd := exec.Command(config.Git.DaemonPath, config.Git.Socket) //#nosec G204
cmd.Stderr = log.Writer() cmd.Stdout = log.Writer()
if err := cmd.Run(); err != nil { panic(err) } }() // UNIX socket listener for hooks { hooksListener, err := net.Listen("unix", config.Hooks.Socket) if errors.Is(err, syscall.EADDRINUSE) { clog.Warn("Removing existing socket " + config.Hooks.Socket) if err = syscall.Unlink(config.Hooks.Socket); err != nil { clog.Fatal(1, "Removing existing socket: "+err.Error()) } if hooksListener, err = net.Listen("unix", config.Hooks.Socket); err != nil { clog.Fatal(1, "Listening hooks: "+err.Error()) } } else if err != nil { clog.Fatal(1, "Listening hooks: "+err.Error()) } clog.Info("Listening hooks on unix " + config.Hooks.Socket) go func() { if err = serveGitHooks(hooksListener); err != nil { clog.Fatal(1, "Serving hooks: "+err.Error()) } }() } // UNIX socket listener for LMTP { lmtpListener, err := net.Listen("unix", config.LMTP.Socket) if errors.Is(err, syscall.EADDRINUSE) { clog.Warn("Removing existing socket " + config.LMTP.Socket) if err = syscall.Unlink(config.LMTP.Socket); err != nil { clog.Fatal(1, "Removing existing socket: "+err.Error()) } if lmtpListener, err = net.Listen("unix", config.LMTP.Socket); err != nil { clog.Fatal(1, "Listening LMTP: "+err.Error()) } } else if err != nil { clog.Fatal(1, "Listening LMTP: "+err.Error()) } clog.Info("Listening LMTP on unix " + config.LMTP.Socket) go func() { if err = serveLMTP(lmtpListener); err != nil { clog.Fatal(1, "Serving LMTP: "+err.Error()) } }() } // SSH listener { sshListener, err := net.Listen(config.SSH.Net, config.SSH.Addr) if errors.Is(err, syscall.EADDRINUSE) && config.SSH.Net == "unix" { clog.Warn("Removing existing socket " + config.SSH.Addr) if err = syscall.Unlink(config.SSH.Addr); err != nil { clog.Fatal(1, "Removing existing socket: "+err.Error()) } if sshListener, err = net.Listen(config.SSH.Net, config.SSH.Addr); err != nil { clog.Fatal(1, "Listening SSH: "+err.Error()) } } else if err != nil { clog.Fatal(1, "Listening SSH: "+err.Error()) } clog.Info("Listening SSH on " + config.SSH.Net + " " + config.SSH.Addr) go func() { if err = serveSSH(sshListener); err != nil { clog.Fatal(1, "Serving SSH: "+err.Error()) } }() } // HTTP listener { httpListener, err := net.Listen(config.HTTP.Net, config.HTTP.Addr) if errors.Is(err, syscall.EADDRINUSE) && config.HTTP.Net == "unix" { clog.Warn("Removing existing socket " + config.HTTP.Addr) if err = syscall.Unlink(config.HTTP.Addr); err != nil { clog.Fatal(1, "Removing existing socket: "+err.Error()) } if httpListener, err = net.Listen(config.HTTP.Net, config.HTTP.Addr); err != nil { clog.Fatal(1, "Listening HTTP: "+err.Error()) } } else if err != nil { clog.Fatal(1, "Listening HTTP: "+err.Error()) } server := http.Server{ Handler: &forgeHTTPRouter{}, ReadTimeout: time.Duration(config.HTTP.ReadTimeout) * time.Second, WriteTimeout: time.Duration(config.HTTP.ReadTimeout) * time.Second, IdleTimeout: time.Duration(config.HTTP.ReadTimeout) * time.Second, } //exhaustruct:ignore clog.Info("Listening HTTP on " + config.HTTP.Net + " " + config.HTTP.Addr) go func() { if err = server.Serve(httpListener); err != nil && !errors.Is(err, http.ErrServerClosed) { clog.Fatal(1, "Serving HTTP: "+err.Error()) } }() } // IRC bot go ircBotLoop() select {} }