git2c, git2d: Rename cmd1 and cmd2 descriptively
/*- * SPDX-License-Identifier: AGPL-3.0-only * SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> */ #include "x.h" int
cmd1(git_repository *repo, struct bare_writer *writer)
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 */ 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" int
cmd2(git_repository *repo, struct bare_reader *reader, struct bare_writer *writer)
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, 10); 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, 11); 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, 3); 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, 3); goto cleanup; } git_blob *blob = (git_blob *)blob_obj; const void *content = git_blob_rawcontent(blob); if (content == NULL) { bare_put_uint(writer, 3); 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, 3); } cleanup: if (entry != NULL) 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);
err = cmd_index(repo, &writer);
if (err != 0) goto free_repo; break; case 2:
err = cmd2(repo, &reader, &writer);
err = cmd_treeraw(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; }
/*- * 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); int cmd2(git_repository *repo, struct bare_reader *reader, struct bare_writer *writer);
int cmd_index(git_repository *repo, struct bare_writer *writer); int cmd_treeraw(git_repository *repo, struct bare_reader *reader, 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 git2c import ( "encoding/hex" "errors" "fmt" "io" )
func (c *Client) Cmd1(repoPath string) ([]Commit, *FilenameContents, error) {
func (c *Client) CmdIndex(repoPath string) ([]Commit, *FilenameContents, error) {
if err := c.writer.WriteData([]byte(repoPath)); err != nil { return nil, nil, fmt.Errorf("sending repo path failed: %w", err) } if err := c.writer.WriteUint(1); err != nil { return nil, nil, fmt.Errorf("sending command failed: %w", err) } status, err := c.reader.ReadUint() if err != nil { return nil, nil, fmt.Errorf("reading status failed: %w", err) } if status != 0 { return nil, nil, fmt.Errorf("git2d error: %d", status) } // README readmeRaw, err := c.reader.ReadData() if err != nil { readmeRaw = nil } readmeFilename := "README.md" // TODO readme := &FilenameContents{Filename: readmeFilename, Content: readmeRaw} // Commits var commits []Commit for { id, err := c.reader.ReadData() if err != nil { if errors.Is(err, io.EOF) { break } return nil, nil, fmt.Errorf("reading commit ID failed: %w", err) } title, _ := c.reader.ReadData() authorName, _ := c.reader.ReadData() authorEmail, _ := c.reader.ReadData() authorDate, _ := c.reader.ReadData() commits = append(commits, Commit{ Hash: hex.EncodeToString(id), Author: string(authorName), Email: string(authorEmail), Date: string(authorDate), Message: string(title), }) } return commits, readme, nil }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package git2c import ( "errors" "fmt" "io" )
func (c *Client) Cmd2(repoPath, pathSpec string) ([]TreeEntry, string, error) {
func (c *Client) CmdTreeRaw(repoPath, pathSpec string) ([]TreeEntry, string, error) {
if err := c.writer.WriteData([]byte(repoPath)); err != nil { return nil, "", fmt.Errorf("sending repo path failed: %w", err) } if err := c.writer.WriteUint(2); err != nil { return nil, "", fmt.Errorf("sending command failed: %w", err) } if err := c.writer.WriteData([]byte(pathSpec)); err != nil { return nil, "", fmt.Errorf("sending path failed: %w", err) } status, err := c.reader.ReadUint() if err != nil { return nil, "", fmt.Errorf("reading status failed: %w", err) } switch status { case 0: kind, err := c.reader.ReadUint() if err != nil { return nil, "", fmt.Errorf("reading object kind failed: %w", err) } switch kind { case 1: // Tree count, err := c.reader.ReadUint() if err != nil { return nil, "", fmt.Errorf("reading entry count failed: %w", err) } var files []TreeEntry for range count { typeCode, err := c.reader.ReadUint() if err != nil { return nil, "", fmt.Errorf("error reading entry type: %w", err) } mode, err := c.reader.ReadUint() if err != nil { return nil, "", fmt.Errorf("error reading entry mode: %w", err) } size, err := c.reader.ReadUint() if err != nil { return nil, "", fmt.Errorf("error reading entry size: %w", err) } name, err := c.reader.ReadData() if err != nil { return nil, "", fmt.Errorf("error reading entry name: %w", err) } files = append(files, TreeEntry{ Name: string(name), Mode: fmt.Sprintf("%06o", mode), Size: size, IsFile: typeCode == 2, IsSubtree: typeCode == 1, }) } return files, "", nil case 2: // Blob content, err := c.reader.ReadData() if err != nil && !errors.Is(err, io.EOF) { return nil, "", fmt.Errorf("error reading file content: %w", err) } return nil, string(content), nil default: return nil, "", fmt.Errorf("unknown kind: %d", kind) } case 3: return nil, "", fmt.Errorf("path not found: %s", pathSpec) default: return nil, "", fmt.Errorf("unknown status code: %d", status) } }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package unsorted import ( "net/http" "go.lindenii.runxiyu.org/forge/internal/git2c" "go.lindenii.runxiyu.org/forge/internal/render" "go.lindenii.runxiyu.org/forge/internal/web" ) // httpHandleRepoIndex provides the front page of a repo using git2d. func (s *Server) httpHandleRepoIndex(w http.ResponseWriter, req *http.Request, params map[string]any) { repoName := params["repo_name"].(string) groupPath := params["group_path"].([]string) _, repoPath, _, _, _, _, _ := s.getRepoInfo(req.Context(), groupPath, repoName, "") // TODO: Don't use getRepoInfo client, err := git2c.NewClient(s.config.Git.Socket) if err != nil { web.ErrorPage500(s.templates, w, params, err.Error()) return } defer client.Close()
commits, readme, err := client.Cmd1(repoPath)
commits, readme, err := client.CmdIndex(repoPath)
if err != nil { web.ErrorPage500(s.templates, w, params, err.Error()) return } params["commits"] = commits params["readme_filename"] = readme.Filename _, params["readme"] = render.Readme(readme.Content, readme.Filename) s.renderTemplate(w, "repo_index", params) // TODO: Caching }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package unsorted import ( "fmt" "html/template" "net/http" "strings" "go.lindenii.runxiyu.org/forge/internal/git2c" "go.lindenii.runxiyu.org/forge/internal/misc" "go.lindenii.runxiyu.org/forge/internal/web" ) // httpHandleRepoRaw serves raw files, or directory listings that point to raw // files. func (s *Server) httpHandleRepoRaw(writer http.ResponseWriter, request *http.Request, params map[string]any) { repoName := params["repo_name"].(string) groupPath := params["group_path"].([]string) rawPathSpec := params["rest"].(string) pathSpec := strings.TrimSuffix(rawPathSpec, "/") params["path_spec"] = pathSpec _, repoPath, _, _, _, _, _ := s.getRepoInfo(request.Context(), groupPath, repoName, "") client, err := git2c.NewClient(s.config.Git.Socket) if err != nil { web.ErrorPage500(s.templates, writer, params, err.Error()) return } defer client.Close()
files, content, err := client.Cmd2(repoPath, pathSpec)
files, content, err := client.CmdTreeRaw(repoPath, pathSpec)
if err != nil { web.ErrorPage500(s.templates, writer, params, err.Error()) return } switch { case files != nil: params["files"] = files params["readme_filename"] = "README.md" params["readme"] = template.HTML("<p>README rendering here is WIP again</p>") // TODO s.renderTemplate(writer, "repo_raw_dir", params) case content != "": if misc.RedirectNoDir(writer, request) { return } writer.Header().Set("Content-Type", "application/octet-stream") fmt.Fprint(writer, content) default: web.ErrorPage500(s.templates, writer, params, "Unknown error fetching repo raw data") } }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package unsorted import ( "html/template" "net/http" "strings" "go.lindenii.runxiyu.org/forge/internal/git2c" "go.lindenii.runxiyu.org/forge/internal/render" "go.lindenii.runxiyu.org/forge/internal/web" ) // httpHandleRepoTree provides a friendly, syntax-highlighted view of // individual files, and provides directory views that link to these files. // // TODO: Do not highlight files that are too large. func (s *Server) httpHandleRepoTree(writer http.ResponseWriter, request *http.Request, params map[string]any) { repoName := params["repo_name"].(string) groupPath := params["group_path"].([]string) rawPathSpec := params["rest"].(string) pathSpec := strings.TrimSuffix(rawPathSpec, "/") params["path_spec"] = pathSpec _, repoPath, _, _, _, _, _ := s.getRepoInfo(request.Context(), groupPath, repoName, "") client, err := git2c.NewClient(s.config.Git.Socket) if err != nil { web.ErrorPage500(s.templates, writer, params, err.Error()) return } defer client.Close()
files, content, err := client.Cmd2(repoPath, pathSpec)
files, content, err := client.CmdTreeRaw(repoPath, pathSpec)
if err != nil { web.ErrorPage500(s.templates, writer, params, err.Error()) return } switch { case files != nil: params["files"] = files params["readme_filename"] = "README.md" params["readme"] = template.HTML("<p>README rendering here is WIP again</p>") // TODO s.renderTemplate(writer, "repo_tree_dir", params) case content != "": rendered := render.Highlight(pathSpec, content) params["file_contents"] = rendered s.renderTemplate(writer, "repo_tree_file", params) default: web.ErrorPage500(s.templates, writer, params, "Unknown object type, something is seriously wrong") } }