Skip to content

Commit 45034ca

Browse files
committed
chore(cli): harden .gitignore
1 parent d8bf2b8 commit 45034ca

2 files changed

Lines changed: 103 additions & 24 deletions

File tree

.gitignore

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
1+
# Build / local binaries
12
bin/
2-
coverage.out
3-
coverage.html
4-
dist/
53
gitant
4+
git-remote-gitant
5+
dist/
6+
7+
# OS junk
8+
.DS_Store
9+
Thumbs.db
10+
11+
# Test coverage
12+
coverage.html
13+
coverage.out
14+
coverage/
15+
16+
# Env
17+
.env*
18+
!.env.example

cmd/git-remote-gitant/main.go

Lines changed: 87 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,19 @@
44
//
55
// Git automatically invokes this binary when it encounters a gitant:// URL.
66
// The helper communicates with the gitant daemon over HTTP to fetch/push objects.
7+
//
8+
// Fetch uses packfile-based transfer: all requested objects are fetched in a
9+
// single request to the daemon's git-upload-pack endpoint, which returns a
10+
// packfile that git can ingest directly. This is much faster than object-by-object
11+
// fetching for large repositories.
712
package main
813

914
import (
1015
"bufio"
16+
"bytes"
1117
"fmt"
18+
"io"
19+
"net/http"
1220
"net/url"
1321
"os"
1422
"strings"
@@ -38,6 +46,10 @@ func main() {
3846
scanner := bufio.NewScanner(os.Stdin)
3947
writer := bufio.NewWriter(os.Stdout)
4048

49+
// Batch buffer for fetch requests — collected until blank line, then
50+
// sent as a single packfile request for efficiency.
51+
var pendingFetches []string
52+
4153
for scanner.Scan() {
4254
line := strings.TrimSpace(scanner.Text())
4355

@@ -49,6 +61,12 @@ func main() {
4961
writer.Flush()
5062

5163
case line == "":
64+
// End of command batch — flush any pending fetches as a single
65+
// packfile request, then exit.
66+
if len(pendingFetches) > 0 {
67+
flushFetches(daemonURL, repoID, pendingFetches, writer)
68+
pendingFetches = nil
69+
}
5270
writer.Flush()
5371
return
5472

@@ -72,31 +90,19 @@ func main() {
7290
writer.Flush()
7391

7492
case strings.HasPrefix(line, "fetch "):
75-
// fetch <sha1> <ref>
93+
// Buffer the fetch request for batch processing
7694
parts := strings.Fields(line)
77-
if len(parts) < 3 {
78-
continue
79-
}
80-
sha1 := parts[1]
81-
ref := parts[2]
82-
83-
// Fetch the object from daemon
84-
var obj struct {
85-
Hash string `json:"hash"`
86-
Type string `json:"type"`
87-
Content []byte `json:"content"`
88-
}
89-
if err := client.Get(fmt.Sprintf("/api/v1/repos/%s/objects/%s", url.PathEscape(repoID), url.PathEscape(sha1)), &obj); err != nil {
90-
fmt.Fprintf(os.Stderr, "error fetching object %s: %v\n", sha1, err)
91-
} else {
92-
// Write the object data to stdout so git can consume it
93-
writer.Write(obj.Content)
94-
fmt.Fprintf(os.Stderr, "fetch %s %s (%d bytes)\n", sha1, ref, len(obj.Content))
95+
if len(parts) >= 3 {
96+
pendingFetches = append(pendingFetches, parts[1])
9597
}
96-
fmt.Fprintln(writer)
97-
writer.Flush()
9898

9999
case strings.HasPrefix(line, "push "):
100+
// Flush any pending fetches before pushing
101+
if len(pendingFetches) > 0 {
102+
flushFetches(daemonURL, repoID, pendingFetches, writer)
103+
pendingFetches = nil
104+
}
105+
100106
// push <refspec>
101107
refspec := strings.TrimPrefix(line, "push ")
102108

@@ -128,3 +134,63 @@ func main() {
128134
}
129135
}
130136
}
137+
138+
// flushFetches sends a single packfile request for all pending want hashes.
139+
// This is much faster than fetching objects one-by-one for large repos.
140+
func flushFetches(daemonURL, repoID string, wants []string, writer *bufio.Writer) {
141+
if len(wants) == 0 {
142+
return
143+
}
144+
145+
// Build pkt-line want request
146+
var pktBuf bytes.Buffer
147+
for _, want := range wants {
148+
fmt.Fprintf(&pktBuf, "%04xwant %s\n", len("want ")+len(want)+1, want)
149+
}
150+
pktBuf.WriteString("0000") // flush packet
151+
fmt.Fprintf(&pktBuf, "%04xdone\n", len("done\n")+4)
152+
153+
endpoint := fmt.Sprintf("%s/api/v1/repos/%s/git-upload-pack", daemonURL, url.PathEscape(repoID))
154+
resp, err := http.Post(endpoint, "application/x-git-upload-pack-request", &pktBuf)
155+
if err != nil {
156+
fmt.Fprintf(os.Stderr, "packfile fetch error: %v\n", err)
157+
// Fall back to object-by-object fetch
158+
fetchObjectsFallback(daemonURL, repoID, wants, writer)
159+
return
160+
}
161+
defer resp.Body.Close()
162+
163+
if resp.StatusCode != http.StatusOK {
164+
fmt.Fprintf(os.Stderr, "packfile fetch failed (HTTP %d), falling back to object fetch\n", resp.StatusCode)
165+
fetchObjectsFallback(daemonURL, repoID, wants, writer)
166+
return
167+
}
168+
169+
// Read the entire packfile response and write to stdout
170+
packData, err := io.ReadAll(resp.Body)
171+
if err != nil {
172+
fmt.Fprintf(os.Stderr, "error reading packfile response: %v\n", err)
173+
return
174+
}
175+
176+
fmt.Fprintf(os.Stderr, "fetch: packfile %d bytes for %d objects\n", len(packData), len(wants))
177+
writer.Write(packData)
178+
}
179+
180+
// fetchObjectsFallback fetches objects one-by-one when packfile transfer fails.
181+
func fetchObjectsFallback(daemonURL, repoID string, wants []string, writer *bufio.Writer) {
182+
client := cli.NewClient(daemonURL)
183+
for _, sha1 := range wants {
184+
var obj struct {
185+
Hash string `json:"hash"`
186+
Type string `json:"type"`
187+
Content []byte `json:"content"`
188+
}
189+
if err := client.Get(fmt.Sprintf("/api/v1/repos/%s/objects/%s", url.PathEscape(repoID), url.PathEscape(sha1)), &obj); err != nil {
190+
fmt.Fprintf(os.Stderr, "error fetching object %s: %v\n", sha1, err)
191+
} else {
192+
writer.Write(obj.Content)
193+
fmt.Fprintf(os.Stderr, "fetch %s (%d bytes)\n", sha1, len(obj.Content))
194+
}
195+
}
196+
}

0 commit comments

Comments
 (0)