aboutsummaryrefslogtreecommitdiff
path: root/forged/internal/ipc/git2c
diff options
context:
space:
mode:
authorRunxi Yu <me@runxiyu.org>2025-08-12 11:01:07 +0800
committerRunxi Yu <me@runxiyu.org>2025-09-16 08:58:16 +0800
commitc12fe030fe5935882047e75ac8a3792faea27574 (patch)
treee2b6f795410348596a7965694bed7e85511d0874 /forged/internal/ipc/git2c
parentRemove forge-specific functions from misc (diff)
downloadforge-c12fe030fe5935882047e75ac8a3792faea27574.tar.gz
forge-c12fe030fe5935882047e75ac8a3792faea27574.tar.zst
forge-c12fe030fe5935882047e75ac8a3792faea27574.zip
RefactorHEADmaster
Diffstat (limited to 'forged/internal/ipc/git2c')
-rw-r--r--forged/internal/ipc/git2c/build.go119
-rw-r--r--forged/internal/ipc/git2c/client.go47
-rw-r--r--forged/internal/ipc/git2c/cmd_index.go65
-rw-r--r--forged/internal/ipc/git2c/cmd_init_repo.go26
-rw-r--r--forged/internal/ipc/git2c/cmd_treeraw.go95
-rw-r--r--forged/internal/ipc/git2c/doc.go2
-rw-r--r--forged/internal/ipc/git2c/extra.go413
-rw-r--r--forged/internal/ipc/git2c/git_types.go25
-rw-r--r--forged/internal/ipc/git2c/perror.go87
-rw-r--r--forged/internal/ipc/git2c/tree.go118
10 files changed, 997 insertions, 0 deletions
diff --git a/forged/internal/ipc/git2c/build.go b/forged/internal/ipc/git2c/build.go
new file mode 100644
index 0000000..3d1b7a0
--- /dev/null
+++ b/forged/internal/ipc/git2c/build.go
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+
+package git2c
+
+import (
+ "encoding/hex"
+ "fmt"
+ "path"
+ "sort"
+ "strings"
+)
+
+func (c *Client) BuildTreeRecursive(repoPath, baseTreeHex string, updates map[string]string) (string, error) {
+ treeCache := make(map[string][]TreeEntryRaw)
+ var walk func(prefix, hexid string) error
+ walk = func(prefix, hexid string) error {
+ ents, err := c.TreeListByOID(repoPath, hexid)
+ if err != nil {
+ return err
+ }
+ treeCache[prefix] = ents
+ for _, e := range ents {
+ if e.Mode == 40000 {
+ sub := path.Join(prefix, e.Name)
+ if err := walk(sub, e.OID); err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+ }
+ if err := walk("", baseTreeHex); err != nil {
+ return "", err
+ }
+
+ for p, blob := range updates {
+ parts := strings.Split(p, "/")
+ dir := strings.Join(parts[:len(parts)-1], "/")
+ name := parts[len(parts)-1]
+ entries := treeCache[dir]
+ found := false
+ for i := range entries {
+ if entries[i].Name == name {
+ if blob == "" {
+ entries = append(entries[:i], entries[i+1:]...)
+ } else {
+ entries[i].Mode = 0o100644
+ entries[i].OID = blob
+ }
+ found = true
+ break
+ }
+ }
+ if !found && blob != "" {
+ entries = append(entries, TreeEntryRaw{Mode: 0o100644, Name: name, OID: blob})
+ }
+ treeCache[dir] = entries
+ }
+
+ built := make(map[string]string)
+ var build func(prefix string) (string, error)
+ build = func(prefix string) (string, error) {
+ entries := treeCache[prefix]
+ for i := range entries {
+ if entries[i].Mode == 0o40000 || entries[i].Mode == 40000 {
+ sub := path.Join(prefix, entries[i].Name)
+ var ok bool
+ var oid string
+ if oid, ok = built[sub]; !ok {
+ var err error
+ oid, err = build(sub)
+ if err != nil {
+ return "", err
+ }
+ }
+ entries[i].Mode = 0o40000
+ entries[i].OID = oid
+ }
+ }
+ sort.Slice(entries, func(i, j int) bool {
+ ni, nj := entries[i].Name, entries[j].Name
+ if ni == nj {
+ return entries[i].Mode != 0o40000 && entries[j].Mode == 0o40000
+ }
+ if strings.HasPrefix(nj, ni) && len(ni) < len(nj) {
+ return entries[i].Mode != 0o40000
+ }
+ if strings.HasPrefix(ni, nj) && len(nj) < len(ni) {
+ return entries[j].Mode == 0o40000
+ }
+ return ni < nj
+ })
+ wr := make([]TreeEntryRaw, 0, len(entries))
+ for _, e := range entries {
+ if e.OID == "" {
+ continue
+ }
+ if e.Mode == 40000 {
+ e.Mode = 0o40000
+ }
+ if _, err := hex.DecodeString(e.OID); err != nil {
+ return "", fmt.Errorf("invalid OID hex for %s/%s: %w", prefix, e.Name, err)
+ }
+ wr = append(wr, TreeEntryRaw{Mode: e.Mode, Name: e.Name, OID: e.OID})
+ }
+ id, err := c.WriteTree(repoPath, wr)
+ if err != nil {
+ return "", err
+ }
+ built[prefix] = id
+ return id, nil
+ }
+ root, err := build("")
+ if err != nil {
+ return "", err
+ }
+ return root, nil
+}
diff --git a/forged/internal/ipc/git2c/client.go b/forged/internal/ipc/git2c/client.go
new file mode 100644
index 0000000..79c2024
--- /dev/null
+++ b/forged/internal/ipc/git2c/client.go
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+
+package git2c
+
+import (
+ "context"
+ "fmt"
+ "net"
+
+ "go.lindenii.runxiyu.org/forge/forged/internal/common/bare"
+)
+
+type Client struct {
+ socketPath string
+ conn net.Conn
+ writer *bare.Writer
+ reader *bare.Reader
+}
+
+func NewClient(ctx context.Context, socketPath string) (*Client, error) {
+ dialer := &net.Dialer{} //exhaustruct:ignore
+ conn, err := dialer.DialContext(ctx, "unix", socketPath)
+ if err != nil {
+ return nil, fmt.Errorf("git2d connection failed: %w", err)
+ }
+
+ writer := bare.NewWriter(conn)
+ reader := bare.NewReader(conn)
+
+ return &Client{
+ socketPath: socketPath,
+ conn: conn,
+ writer: writer,
+ reader: reader,
+ }, nil
+}
+
+func (c *Client) Close() (err error) {
+ if c.conn != nil {
+ err = c.conn.Close()
+ if err != nil {
+ return fmt.Errorf("close underlying socket: %w", err)
+ }
+ }
+ return nil
+}
diff --git a/forged/internal/ipc/git2c/cmd_index.go b/forged/internal/ipc/git2c/cmd_index.go
new file mode 100644
index 0000000..44a0845
--- /dev/null
+++ b/forged/internal/ipc/git2c/cmd_index.go
@@ -0,0 +1,65 @@
+// 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) CmdIndex(repoPath string) ([]Commit, *FilenameContents, error) {
+ err := c.writer.WriteData([]byte(repoPath))
+ if err != nil {
+ return nil, nil, fmt.Errorf("sending repo path failed: %w", err)
+ }
+ err = c.writer.WriteUint(1)
+ if 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
+}
diff --git a/forged/internal/ipc/git2c/cmd_init_repo.go b/forged/internal/ipc/git2c/cmd_init_repo.go
new file mode 100644
index 0000000..ae1e92a
--- /dev/null
+++ b/forged/internal/ipc/git2c/cmd_init_repo.go
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+
+package git2c
+
+import "fmt"
+
+func (c *Client) InitRepo(repoPath, hooksPath string) error {
+ if err := c.writer.WriteData([]byte(repoPath)); err != nil {
+ return fmt.Errorf("sending repo path failed: %w", err)
+ }
+ if err := c.writer.WriteUint(15); err != nil {
+ return fmt.Errorf("sending command failed: %w", err)
+ }
+ if err := c.writer.WriteData([]byte(hooksPath)); err != nil {
+ return fmt.Errorf("sending hooks path failed: %w", err)
+ }
+ status, err := c.reader.ReadUint()
+ if err != nil {
+ return fmt.Errorf("reading status failed: %w", err)
+ }
+ if status != 0 {
+ return Perror(status)
+ }
+ return nil
+}
diff --git a/forged/internal/ipc/git2c/cmd_treeraw.go b/forged/internal/ipc/git2c/cmd_treeraw.go
new file mode 100644
index 0000000..d2d5ac2
--- /dev/null
+++ b/forged/internal/ipc/git2c/cmd_treeraw.go
@@ -0,0 +1,95 @@
+// 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) CmdTreeRaw(repoPath, pathSpec string) ([]TreeEntry, string, error) {
+ err := c.writer.WriteData([]byte(repoPath))
+ if err != nil {
+ return nil, "", fmt.Errorf("sending repo path failed: %w", err)
+ }
+ err = c.writer.WriteUint(2)
+ if err != nil {
+ return nil, "", fmt.Errorf("sending command failed: %w", err)
+ }
+ err = c.writer.WriteData([]byte(pathSpec))
+ if 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)
+ }
+}
diff --git a/forged/internal/ipc/git2c/doc.go b/forged/internal/ipc/git2c/doc.go
new file mode 100644
index 0000000..e14dae0
--- /dev/null
+++ b/forged/internal/ipc/git2c/doc.go
@@ -0,0 +1,2 @@
+// Package git2c provides routines to interact with the git2d backend daemon.
+package git2c
diff --git a/forged/internal/ipc/git2c/extra.go b/forged/internal/ipc/git2c/extra.go
new file mode 100644
index 0000000..1a3e3a6
--- /dev/null
+++ b/forged/internal/ipc/git2c/extra.go
@@ -0,0 +1,413 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+
+package git2c
+
+import (
+ "encoding/hex"
+ "fmt"
+ "time"
+)
+
+type DiffChunk struct {
+ Op uint64
+ Content string
+}
+
+type FileDiff struct {
+ FromMode uint64
+ ToMode uint64
+ FromPath string
+ ToPath string
+ Chunks []DiffChunk
+}
+
+type CommitInfo struct {
+ Hash string
+ AuthorName string
+ AuthorEmail string
+ AuthorWhen int64 // unix secs
+ AuthorTZMin int64 // minutes ofs
+ CommitterName string
+ CommitterEmail string
+ CommitterWhen int64
+ CommitterTZMin int64
+ Message string
+ Parents []string // hex
+ Files []FileDiff
+}
+
+func (c *Client) ResolveRef(repoPath, refType, refName string) (string, error) {
+ if err := c.writer.WriteData([]byte(repoPath)); err != nil {
+ return "", fmt.Errorf("sending repo path failed: %w", err)
+ }
+ if err := c.writer.WriteUint(3); err != nil {
+ return "", fmt.Errorf("sending command failed: %w", err)
+ }
+ if err := c.writer.WriteData([]byte(refType)); err != nil {
+ return "", fmt.Errorf("sending ref type failed: %w", err)
+ }
+ if err := c.writer.WriteData([]byte(refName)); err != nil {
+ return "", fmt.Errorf("sending ref name failed: %w", err)
+ }
+
+ status, err := c.reader.ReadUint()
+ if err != nil {
+ return "", fmt.Errorf("reading status failed: %w", err)
+ }
+ if status != 0 {
+ return "", Perror(status)
+ }
+ id, err := c.reader.ReadData()
+ if err != nil {
+ return "", fmt.Errorf("reading oid failed: %w", err)
+ }
+ return hex.EncodeToString(id), nil
+}
+
+func (c *Client) ListBranches(repoPath string) ([]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(4); err != nil {
+ return nil, fmt.Errorf("sending command failed: %w", err)
+ }
+ status, err := c.reader.ReadUint()
+ if err != nil {
+ return nil, fmt.Errorf("reading status failed: %w", err)
+ }
+ if status != 0 {
+ return nil, Perror(status)
+ }
+ count, err := c.reader.ReadUint()
+ if err != nil {
+ return nil, fmt.Errorf("reading count failed: %w", err)
+ }
+ branches := make([]string, 0, count)
+ for range count {
+ name, err := c.reader.ReadData()
+ if err != nil {
+ return nil, fmt.Errorf("reading branch name failed: %w", err)
+ }
+ branches = append(branches, string(name))
+ }
+ return branches, nil
+}
+
+func (c *Client) FormatPatch(repoPath, commitHex string) (string, error) {
+ if err := c.writer.WriteData([]byte(repoPath)); err != nil {
+ return "", fmt.Errorf("sending repo path failed: %w", err)
+ }
+ if err := c.writer.WriteUint(5); err != nil {
+ return "", fmt.Errorf("sending command failed: %w", err)
+ }
+ if err := c.writer.WriteData([]byte(commitHex)); err != nil {
+ return "", fmt.Errorf("sending commit failed: %w", err)
+ }
+ status, err := c.reader.ReadUint()
+ if err != nil {
+ return "", fmt.Errorf("reading status failed: %w", err)
+ }
+ if status != 0 {
+ return "", Perror(status)
+ }
+ buf, err := c.reader.ReadData()
+ if err != nil {
+ return "", fmt.Errorf("reading patch failed: %w", err)
+ }
+ return string(buf), nil
+}
+
+func (c *Client) MergeBase(repoPath, hexA, hexB string) (string, error) {
+ if err := c.writer.WriteData([]byte(repoPath)); err != nil {
+ return "", fmt.Errorf("sending repo path failed: %w", err)
+ }
+ if err := c.writer.WriteUint(7); err != nil {
+ return "", fmt.Errorf("sending command failed: %w", err)
+ }
+ if err := c.writer.WriteData([]byte(hexA)); err != nil {
+ return "", fmt.Errorf("sending oid A failed: %w", err)
+ }
+ if err := c.writer.WriteData([]byte(hexB)); err != nil {
+ return "", fmt.Errorf("sending oid B failed: %w", err)
+ }
+ status, err := c.reader.ReadUint()
+ if err != nil {
+ return "", fmt.Errorf("reading status failed: %w", err)
+ }
+ if status != 0 {
+ return "", Perror(status)
+ }
+ base, err := c.reader.ReadData()
+ if err != nil {
+ return "", fmt.Errorf("reading base oid failed: %w", err)
+ }
+ return hex.EncodeToString(base), nil
+}
+
+func (c *Client) Log(repoPath, refSpec string, n uint) ([]Commit, 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(8); err != nil {
+ return nil, fmt.Errorf("sending command failed: %w", err)
+ }
+ if err := c.writer.WriteData([]byte(refSpec)); err != nil {
+ return nil, fmt.Errorf("sending refspec failed: %w", err)
+ }
+ if err := c.writer.WriteUint(uint64(n)); err != nil {
+ return nil, fmt.Errorf("sending limit failed: %w", err)
+ }
+ status, err := c.reader.ReadUint()
+ if err != nil {
+ return nil, fmt.Errorf("reading status failed: %w", err)
+ }
+ if status != 0 {
+ return nil, Perror(status)
+ }
+ var out []Commit
+ for {
+ id, err := c.reader.ReadData()
+ if err != nil {
+ break
+ }
+ title, _ := c.reader.ReadData()
+ authorName, _ := c.reader.ReadData()
+ authorEmail, _ := c.reader.ReadData()
+ date, _ := c.reader.ReadData()
+ out = append(out, Commit{
+ Hash: hex.EncodeToString(id),
+ Author: string(authorName),
+ Email: string(authorEmail),
+ Date: string(date),
+ Message: string(title),
+ })
+ }
+ return out, nil
+}
+
+func (c *Client) CommitTreeOID(repoPath, commitHex string) (string, error) {
+ if err := c.writer.WriteData([]byte(repoPath)); err != nil {
+ return "", fmt.Errorf("sending repo path failed: %w", err)
+ }
+ if err := c.writer.WriteUint(12); err != nil {
+ return "", fmt.Errorf("sending command failed: %w", err)
+ }
+ if err := c.writer.WriteData([]byte(commitHex)); err != nil {
+ return "", fmt.Errorf("sending oid failed: %w", err)
+ }
+ status, err := c.reader.ReadUint()
+ if err != nil {
+ return "", fmt.Errorf("reading status failed: %w", err)
+ }
+ if status != 0 {
+ return "", Perror(status)
+ }
+ id, err := c.reader.ReadData()
+ if err != nil {
+ return "", fmt.Errorf("reading tree oid failed: %w", err)
+ }
+ return hex.EncodeToString(id), nil
+}
+
+func (c *Client) CommitCreate(repoPath, treeHex string, parents []string, authorName, authorEmail string, when time.Time, message string) (string, error) {
+ if err := c.writer.WriteData([]byte(repoPath)); err != nil {
+ return "", fmt.Errorf("sending repo path failed: %w", err)
+ }
+ if err := c.writer.WriteUint(13); err != nil {
+ return "", fmt.Errorf("sending command failed: %w", err)
+ }
+ if err := c.writer.WriteData([]byte(treeHex)); err != nil {
+ return "", fmt.Errorf("sending tree oid failed: %w", err)
+ }
+ if err := c.writer.WriteUint(uint64(len(parents))); err != nil {
+ return "", fmt.Errorf("sending parents count failed: %w", err)
+ }
+ for _, p := range parents {
+ if err := c.writer.WriteData([]byte(p)); err != nil {
+ return "", fmt.Errorf("sending parent oid failed: %w", err)
+ }
+ }
+ if err := c.writer.WriteData([]byte(authorName)); err != nil {
+ return "", fmt.Errorf("sending author name failed: %w", err)
+ }
+ if err := c.writer.WriteData([]byte(authorEmail)); err != nil {
+ return "", fmt.Errorf("sending author email failed: %w", err)
+ }
+ if err := c.writer.WriteInt(when.Unix()); err != nil {
+ return "", fmt.Errorf("sending when failed: %w", err)
+ }
+ _, offset := when.Zone()
+ if err := c.writer.WriteInt(int64(offset / 60)); err != nil {
+ return "", fmt.Errorf("sending tz offset failed: %w", err)
+ }
+ if err := c.writer.WriteData([]byte(message)); err != nil {
+ return "", fmt.Errorf("sending message failed: %w", err)
+ }
+ status, err := c.reader.ReadUint()
+ if err != nil {
+ return "", fmt.Errorf("reading status failed: %w", err)
+ }
+ if status != 0 {
+ return "", Perror(status)
+ }
+ id, err := c.reader.ReadData()
+ if err != nil {
+ return "", fmt.Errorf("reading commit oid failed: %w", err)
+ }
+ return hex.EncodeToString(id), nil
+}
+
+func (c *Client) UpdateRef(repoPath, refName, commitHex string) error {
+ if err := c.writer.WriteData([]byte(repoPath)); err != nil {
+ return fmt.Errorf("sending repo path failed: %w", err)
+ }
+ if err := c.writer.WriteUint(14); err != nil {
+ return fmt.Errorf("sending command failed: %w", err)
+ }
+ if err := c.writer.WriteData([]byte(refName)); err != nil {
+ return fmt.Errorf("sending ref name failed: %w", err)
+ }
+ if err := c.writer.WriteData([]byte(commitHex)); err != nil {
+ return fmt.Errorf("sending commit oid failed: %w", err)
+ }
+ status, err := c.reader.ReadUint()
+ if err != nil {
+ return fmt.Errorf("reading status failed: %w", err)
+ }
+ if status != 0 {
+ return Perror(status)
+ }
+ return nil
+}
+
+func (c *Client) CommitInfo(repoPath, commitHex string) (*CommitInfo, 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(6); err != nil {
+ return nil, fmt.Errorf("sending command failed: %w", err)
+ }
+ if err := c.writer.WriteData([]byte(commitHex)); err != nil {
+ return nil, fmt.Errorf("sending commit failed: %w", err)
+ }
+ status, err := c.reader.ReadUint()
+ if err != nil {
+ return nil, fmt.Errorf("reading status failed: %w", err)
+ }
+ if status != 0 {
+ return nil, Perror(status)
+ }
+ id, err := c.reader.ReadData()
+ if err != nil {
+ return nil, fmt.Errorf("reading id failed: %w", err)
+ }
+ aname, err := c.reader.ReadData()
+ if err != nil {
+ return nil, fmt.Errorf("reading author name failed: %w", err)
+ }
+ aemail, err := c.reader.ReadData()
+ if err != nil {
+ return nil, fmt.Errorf("reading author email failed: %w", err)
+ }
+ awhen, err := c.reader.ReadI64()
+ if err != nil {
+ return nil, fmt.Errorf("reading author time failed: %w", err)
+ }
+ aoff, err := c.reader.ReadI64()
+ if err != nil {
+ return nil, fmt.Errorf("reading author tz failed: %w", err)
+ }
+ cname, err := c.reader.ReadData()
+ if err != nil {
+ return nil, fmt.Errorf("reading committer name failed: %w", err)
+ }
+ cemail, err := c.reader.ReadData()
+ if err != nil {
+ return nil, fmt.Errorf("reading committer email failed: %w", err)
+ }
+ cwhen, err := c.reader.ReadI64()
+ if err != nil {
+ return nil, fmt.Errorf("reading committer time failed: %w", err)
+ }
+ coff, err := c.reader.ReadI64()
+ if err != nil {
+ return nil, fmt.Errorf("reading committer tz failed: %w", err)
+ }
+ msg, err := c.reader.ReadData()
+ if err != nil {
+ return nil, fmt.Errorf("reading message failed: %w", err)
+ }
+ pcnt, err := c.reader.ReadUint()
+ if err != nil {
+ return nil, fmt.Errorf("reading parents count failed: %w", err)
+ }
+ parents := make([]string, 0, pcnt)
+ for i := uint64(0); i < pcnt; i++ {
+ praw, perr := c.reader.ReadData()
+ if perr != nil {
+ return nil, fmt.Errorf("reading parent failed: %w", perr)
+ }
+ parents = append(parents, hex.EncodeToString(praw))
+ }
+ fcnt, err := c.reader.ReadUint()
+ if err != nil {
+ return nil, fmt.Errorf("reading file count failed: %w", err)
+ }
+ files := make([]FileDiff, 0, fcnt)
+ for i := uint64(0); i < fcnt; i++ {
+ fromMode, err := c.reader.ReadUint()
+ if err != nil {
+ return nil, fmt.Errorf("reading from mode failed: %w", err)
+ }
+ toMode, err := c.reader.ReadUint()
+ if err != nil {
+ return nil, fmt.Errorf("reading to mode failed: %w", err)
+ }
+ fromPath, err := c.reader.ReadData()
+ if err != nil {
+ return nil, fmt.Errorf("reading from path failed: %w", err)
+ }
+ toPath, err := c.reader.ReadData()
+ if err != nil {
+ return nil, fmt.Errorf("reading to path failed: %w", err)
+ }
+ ccnt, err := c.reader.ReadUint()
+ if err != nil {
+ return nil, fmt.Errorf("reading chunk count failed: %w", err)
+ }
+ chunks := make([]DiffChunk, 0, ccnt)
+ for j := uint64(0); j < ccnt; j++ {
+ op, err := c.reader.ReadUint()
+ if err != nil {
+ return nil, fmt.Errorf("reading chunk op failed: %w", err)
+ }
+ content, err := c.reader.ReadData()
+ if err != nil {
+ return nil, fmt.Errorf("reading chunk content failed: %w", err)
+ }
+ chunks = append(chunks, DiffChunk{Op: op, Content: string(content)})
+ }
+ files = append(files, FileDiff{
+ FromMode: fromMode,
+ ToMode: toMode,
+ FromPath: string(fromPath),
+ ToPath: string(toPath),
+ Chunks: chunks,
+ })
+ }
+ return &CommitInfo{
+ Hash: hex.EncodeToString(id),
+ AuthorName: string(aname),
+ AuthorEmail: string(aemail),
+ AuthorWhen: awhen,
+ AuthorTZMin: aoff,
+ CommitterName: string(cname),
+ CommitterEmail: string(cemail),
+ CommitterWhen: cwhen,
+ CommitterTZMin: coff,
+ Message: string(msg),
+ Parents: parents,
+ Files: files,
+ }, nil
+}
diff --git a/forged/internal/ipc/git2c/git_types.go b/forged/internal/ipc/git2c/git_types.go
new file mode 100644
index 0000000..da685bf
--- /dev/null
+++ b/forged/internal/ipc/git2c/git_types.go
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+
+package git2c
+
+type Commit struct {
+ Hash string
+ Author string
+ Email string
+ Date string
+ Message string
+}
+
+type FilenameContents struct {
+ Filename string
+ Content []byte
+}
+
+type TreeEntry struct {
+ Name string
+ Mode string
+ Size uint64
+ IsFile bool
+ IsSubtree bool
+}
diff --git a/forged/internal/ipc/git2c/perror.go b/forged/internal/ipc/git2c/perror.go
new file mode 100644
index 0000000..4be2a07
--- /dev/null
+++ b/forged/internal/ipc/git2c/perror.go
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+
+// TODO: Make the C part report detailed error messages too
+
+package git2c
+
+import "errors"
+
+var (
+ ErrUnknown = errors.New("git2c: unknown error")
+ ErrPath = errors.New("git2c: get tree entry by path failed")
+ ErrRevparse = errors.New("git2c: revparse failed")
+ ErrReadme = errors.New("git2c: no readme")
+ ErrBlobExpected = errors.New("git2c: blob expected")
+ ErrEntryToObject = errors.New("git2c: tree entry to object conversion failed")
+ ErrBlobRawContent = errors.New("git2c: get blob raw content failed")
+ ErrRevwalk = errors.New("git2c: revwalk failed")
+ ErrRevwalkPushHead = errors.New("git2c: revwalk push head failed")
+ ErrBareProto = errors.New("git2c: bare protocol error")
+ ErrRefResolve = errors.New("git2c: ref resolve failed")
+ ErrBranches = errors.New("git2c: list branches failed")
+ ErrCommitLookup = errors.New("git2c: commit lookup failed")
+ ErrDiff = errors.New("git2c: diff failed")
+ ErrMergeBaseNone = errors.New("git2c: no merge base found")
+ ErrMergeBase = errors.New("git2c: merge base failed")
+ ErrCommitCreate = errors.New("git2c: commit create failed")
+ ErrUpdateRef = errors.New("git2c: update ref failed")
+ ErrCommitTree = errors.New("git2c: commit tree lookup failed")
+ ErrInitRepoCreate = errors.New("git2c: init repo: create failed")
+ ErrInitRepoConfig = errors.New("git2c: init repo: open config failed")
+ ErrInitRepoSetHooksPath = errors.New("git2c: init repo: set core.hooksPath failed")
+ ErrInitRepoSetAdvertisePushOptions = errors.New("git2c: init repo: set receive.advertisePushOptions failed")
+ ErrInitRepoMkdir = errors.New("git2c: init repo: create directory failed")
+)
+
+func Perror(errno uint64) error {
+ switch errno {
+ case 0:
+ return nil
+ case 3:
+ return ErrPath
+ case 4:
+ return ErrRevparse
+ case 5:
+ return ErrReadme
+ case 6:
+ return ErrBlobExpected
+ case 7:
+ return ErrEntryToObject
+ case 8:
+ return ErrBlobRawContent
+ case 9:
+ return ErrRevwalk
+ case 10:
+ return ErrRevwalkPushHead
+ case 11:
+ return ErrBareProto
+ case 12:
+ return ErrRefResolve
+ case 13:
+ return ErrBranches
+ case 14:
+ return ErrCommitLookup
+ case 15:
+ return ErrDiff
+ case 16:
+ return ErrMergeBaseNone
+ case 17:
+ return ErrMergeBase
+ case 18:
+ return ErrUpdateRef
+ case 19:
+ return ErrCommitCreate
+ case 20:
+ return ErrInitRepoCreate
+ case 21:
+ return ErrInitRepoConfig
+ case 22:
+ return ErrInitRepoSetHooksPath
+ case 23:
+ return ErrInitRepoSetAdvertisePushOptions
+ case 24:
+ return ErrInitRepoMkdir
+ }
+ return ErrUnknown
+}
diff --git a/forged/internal/ipc/git2c/tree.go b/forged/internal/ipc/git2c/tree.go
new file mode 100644
index 0000000..f598e14
--- /dev/null
+++ b/forged/internal/ipc/git2c/tree.go
@@ -0,0 +1,118 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
+
+package git2c
+
+import (
+ "encoding/hex"
+ "fmt"
+)
+
+type TreeEntryRaw struct {
+ Mode uint64
+ Name string
+ OID string // hex
+}
+
+func (c *Client) TreeListByOID(repoPath, treeHex string) ([]TreeEntryRaw, 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(9); err != nil {
+ return nil, fmt.Errorf("sending command failed: %w", err)
+ }
+ if err := c.writer.WriteData([]byte(treeHex)); err != nil {
+ return nil, fmt.Errorf("sending tree oid failed: %w", err)
+ }
+ status, err := c.reader.ReadUint()
+ if err != nil {
+ return nil, fmt.Errorf("reading status failed: %w", err)
+ }
+ if status != 0 {
+ return nil, Perror(status)
+ }
+ count, err := c.reader.ReadUint()
+ if err != nil {
+ return nil, fmt.Errorf("reading count failed: %w", err)
+ }
+ entries := make([]TreeEntryRaw, 0, count)
+ for range count {
+ mode, err := c.reader.ReadUint()
+ if err != nil {
+ return nil, fmt.Errorf("reading mode failed: %w", err)
+ }
+ name, err := c.reader.ReadData()
+ if err != nil {
+ return nil, fmt.Errorf("reading name failed: %w", err)
+ }
+ id, err := c.reader.ReadData()
+ if err != nil {
+ return nil, fmt.Errorf("reading oid failed: %w", err)
+ }
+ entries = append(entries, TreeEntryRaw{Mode: mode, Name: string(name), OID: hex.EncodeToString(id)})
+ }
+ return entries, nil
+}
+
+func (c *Client) WriteTree(repoPath string, entries []TreeEntryRaw) (string, error) {
+ if err := c.writer.WriteData([]byte(repoPath)); err != nil {
+ return "", fmt.Errorf("sending repo path failed: %w", err)
+ }
+ if err := c.writer.WriteUint(10); err != nil {
+ return "", fmt.Errorf("sending command failed: %w", err)
+ }
+ if err := c.writer.WriteUint(uint64(len(entries))); err != nil {
+ return "", fmt.Errorf("sending count failed: %w", err)
+ }
+ for _, e := range entries {
+ if err := c.writer.WriteUint(e.Mode); err != nil {
+ return "", fmt.Errorf("sending mode failed: %w", err)
+ }
+ if err := c.writer.WriteData([]byte(e.Name)); err != nil {
+ return "", fmt.Errorf("sending name failed: %w", err)
+ }
+ raw, err := hex.DecodeString(e.OID)
+ if err != nil {
+ return "", fmt.Errorf("decode oid hex: %w", err)
+ }
+ if err := c.writer.WriteDataFixed(raw); err != nil {
+ return "", fmt.Errorf("sending oid failed: %w", err)
+ }
+ }
+ status, err := c.reader.ReadUint()
+ if err != nil {
+ return "", fmt.Errorf("reading status failed: %w", err)
+ }
+ if status != 0 {
+ return "", Perror(status)
+ }
+ id, err := c.reader.ReadData()
+ if err != nil {
+ return "", fmt.Errorf("reading oid failed: %w", err)
+ }
+ return hex.EncodeToString(id), nil
+}
+
+func (c *Client) WriteBlob(repoPath string, content []byte) (string, error) {
+ if err := c.writer.WriteData([]byte(repoPath)); err != nil {
+ return "", fmt.Errorf("sending repo path failed: %w", err)
+ }
+ if err := c.writer.WriteUint(11); err != nil {
+ return "", fmt.Errorf("sending command failed: %w", err)
+ }
+ if err := c.writer.WriteData(content); err != nil {
+ return "", fmt.Errorf("sending blob content failed: %w", err)
+ }
+ status, err := c.reader.ReadUint()
+ if err != nil {
+ return "", fmt.Errorf("reading status failed: %w", err)
+ }
+ if status != 0 {
+ return "", Perror(status)
+ }
+ id, err := c.reader.ReadData()
+ if err != nil {
+ return "", fmt.Errorf("reading oid failed: %w", err)
+ }
+ return hex.EncodeToString(id), nil
+}