11package git
22
33import (
4+ "bufio"
45 "bytes"
56 "errors"
67 "fmt"
8+ "io"
79 "io/fs"
810 "os"
911 "os/exec"
1012 "path/filepath"
13+ "strings"
1114)
1215
1316// ObjectType represents the type of a Git object ("blob", "tree",
@@ -157,7 +160,7 @@ func (repo *Repository) GitDir() (string, error) {
157160 return repo .gitDir , nil
158161}
159162
160- // GitPath returns that path of a file within the git repository, by
163+ // GitPath returns the path of a file within the git repository, by
161164// calling `git rev-parse --git-path $relPath`. The returned path is
162165// relative to the current directory.
163166func (repo * Repository ) GitPath (relPath string ) (string , error ) {
@@ -173,3 +176,95 @@ func (repo *Repository) GitPath(relPath string) (string, error) {
173176 // current directory, we can use it as-is:
174177 return string (bytes .TrimSpace (out )), nil
175178}
179+
180+ // UnreachableStats holds the count and size of unreachable objects.
181+ type UnreachableStats struct {
182+ Count int64
183+ Size int64
184+ }
185+
186+ // GetUnreachableStats runs 'git fsck --unreachable --no-reflogs --full'
187+ // and returns the count and total size of unreachable objects.
188+ // This implementation collects all OIDs from fsck output and then uses
189+ // batch mode to efficiently retrieve their sizes.
190+ func (repo * Repository ) GetUnreachableStats () (UnreachableStats , error ) {
191+ // Run git fsck. Using CombinedOutput captures both stdout and stderr.
192+ cmd := exec .Command (repo .gitBin , "-C" , repo .gitDir , "fsck" , "--unreachable" , "--no-reflogs" , "--full" )
193+ cmd .Env = os .Environ ()
194+ output , err := cmd .CombinedOutput ()
195+ if err != nil {
196+ fmt .Fprintln (os .Stderr )
197+ fmt .Fprintln (os .Stderr , "An error occurred trying to process unreachable objects." )
198+ os .Stderr .Write (output )
199+ fmt .Fprintln (os .Stderr )
200+ return UnreachableStats {Count : 0 , Size : 0 }, err
201+ }
202+
203+ var oids []string
204+ count := int64 (0 )
205+ for _ , line := range bytes .Split (output , []byte {'\n' }) {
206+ fields := bytes .Fields (line )
207+ // Expected line format: "unreachable <type> <oid> ..."
208+ if len (fields ) >= 3 && string (fields [0 ]) == "unreachable" {
209+ count ++
210+ oid := string (fields [2 ])
211+ oids = append (oids , oid )
212+ }
213+ }
214+
215+ // Retrieve the total size using batch mode.
216+ totalSize , err := repo .getTotalSizeFromOids (oids )
217+ if err != nil {
218+ return UnreachableStats {}, fmt .Errorf ("failed to get sizes via batch mode: %w" , err )
219+ }
220+
221+ return UnreachableStats {Count : count , Size : totalSize }, nil
222+ }
223+
224+ // getTotalSizeFromOids uses 'git cat-file --batch-check' to retrieve sizes for
225+ // the provided OIDs. It writes each OID to stdin and reads back lines in the
226+ // format: "<oid> <type> <size>".
227+ func (repo * Repository ) getTotalSizeFromOids (oids []string ) (int64 , error ) {
228+ cmd := exec .Command (repo .gitBin , "-C" , repo .gitDir , "cat-file" , "--batch-check" )
229+ stdinPipe , err := cmd .StdinPipe ()
230+ if err != nil {
231+ return 0 , fmt .Errorf ("failed to get stdin pipe: %w" , err )
232+ }
233+ stdoutPipe , err := cmd .StdoutPipe ()
234+ if err != nil {
235+ return 0 , fmt .Errorf ("failed to get stdout pipe: %w" , err )
236+ }
237+
238+ if err := cmd .Start (); err != nil {
239+ return 0 , fmt .Errorf ("failed to start git cat-file batch: %w" , err )
240+ }
241+
242+ // Write all OIDs to the batch process.
243+ go func () {
244+ defer stdinPipe .Close ()
245+ for _ , oid := range oids {
246+ io .WriteString (stdinPipe , oid + "\n " )
247+ }
248+ }()
249+
250+ var totalSize int64
251+ scanner := bufio .NewScanner (stdoutPipe )
252+ // Each line is expected to be: "<oid> <type> <size>"
253+ for scanner .Scan () {
254+ parts := strings .Fields (scanner .Text ())
255+ if len (parts ) == 3 {
256+ var size int64
257+ fmt .Sscanf (parts [2 ], "%d" , & size )
258+ totalSize += size
259+ } else {
260+ return 0 , fmt .Errorf ("unexpected output format: %s" , scanner .Text ())
261+ }
262+ }
263+ if err := scanner .Err (); err != nil {
264+ return 0 , fmt .Errorf ("error reading git cat-file output: %w" , err )
265+ }
266+ if err := cmd .Wait (); err != nil {
267+ return 0 , fmt .Errorf ("git cat-file batch process error: %w" , err )
268+ }
269+ return totalSize , nil
270+ }
0 commit comments