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.
712package main
813
914import (
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