From 2d0f50177548a8b6ef05268b42d1b8e6248f1556 Mon Sep 17 00:00:00 2001 From: Paulo Gomes Date: Thu, 27 Nov 2025 23:43:56 +0000 Subject: [PATCH 1/3] Add .gitattributes Signed-off-by: Paulo Gomes --- .gitattributes | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b27bec4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +* text=auto eol=lf +*.go text eol=lf +go.mod text eol=lf +go.sum text eol=lf From 2ef42cc3c759b4ed9b32036a89ed1d6d7226b7ea Mon Sep 17 00:00:00 2001 From: Paulo Gomes Date: Thu, 27 Nov 2025 23:35:22 +0000 Subject: [PATCH 2/3] build: Add initial golangci-lint configuration Signed-off-by: Paulo Gomes --- .github/workflows/build.yml | 3 ++ .golangci.yaml | 41 +++++++++++++++++++ Makefile | 23 +++++++++++ cmd/gogit-http-server/logging.go | 4 +- cmd/gogit-http-server/main.go | 4 +- cmd/gogit/clone.go | 3 +- cmd/gogit/daemon.go | 12 ++++-- cmd/gogit/fetch.go | 2 +- cmd/gogit/main.go | 6 ++- cmd/gogit/pull.go | 1 + cmd/gogit/push.go | 2 + cmd/gogit/update-server-info.go | 12 ++++-- cmd/gogit/verify-pack.go | 20 +++++++-- server/git/logging.go | 4 +- server/git/server.go | 69 +++++++++++++++++++++++--------- 15 files changed, 171 insertions(+), 35 deletions(-) create mode 100644 .golangci.yaml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fbcfa62..c17ba46 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,3 +31,6 @@ jobs: go-version: ${{ matrix.go-version }} - run: make build + + - name: Validate + run: make validate diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..820b7e4 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,41 @@ +version: "2" +run: + concurrency: 3 +linters: + default: all + disable: + - depguard + - gochecknoglobals + - exhaustruct + - err113 + - gochecknoinits + - ireturn + - forbidigo + - mnd + - varnamelen + - godoclint + - noctx + - noinlineerr + - nestif + - gosec + - funlen + - cyclop + - gocritic + - revive + - wrapcheck + - wsl + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling +formatters: + enable: + - gci + - gofmt + - gofumpt + - goimports + exclusions: + generated: lax diff --git a/Makefile b/Makefile index 8896c6f..e7bbe57 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,26 @@ +# renovate: datasource=github-tags depName=golangci/golangci-lint +GOLANGCI_VERSION ?= v2.6.1 +TOOLS_BIN := $(shell mkdir -p build/tools && realpath build/tools) + +GOLANGCI = $(TOOLS_BIN)/golangci-lint-$(GOLANGCI_VERSION) +$(GOLANGCI): + rm -f $(TOOLS_BIN)/golangci-lint* + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/$(GOLANGCI_VERSION)/install.sh | sh -s -- -b $(TOOLS_BIN) $(GOLANGCI_VERSION) + mv $(TOOLS_BIN)/golangci-lint $(TOOLS_BIN)/golangci-lint-$(GOLANGCI_VERSION) + .PHONY: build build: go build -o build/ ./... + +validate: validate-lint validate-dirty + +validate-lint: $(GOLANGCI) + $(GOLANGCI) run + +validate-dirty: +ifneq ($(shell git status --porcelain --untracked-files=no),) + @echo worktree is dirty + @git --no-pager status + @git --no-pager diff + @exit 1 +endif diff --git a/cmd/gogit-http-server/logging.go b/cmd/gogit-http-server/logging.go index 87a8af0..ea8ca78 100644 --- a/cmd/gogit-http-server/logging.go +++ b/cmd/gogit-http-server/logging.go @@ -9,17 +9,19 @@ import ( type logWriter struct { http.ResponseWriter + code, bytes int } func (r *logWriter) Write(p []byte) (int, error) { written, err := r.ResponseWriter.Write(p) r.bytes += written + return written, err } // Note this is generally only called when sending an HTTP error, so it's -// important to set the `code` value to 200 as a default +// important to set the `code` value to 200 as a default. func (r *logWriter) WriteHeader(code int) { r.code = code r.ResponseWriter.WriteHeader(code) diff --git a/cmd/gogit-http-server/main.go b/cmd/gogit-http-server/main.go index eb087b9..6db156c 100644 --- a/cmd/gogit-http-server/main.go +++ b/cmd/gogit-http-server/main.go @@ -45,12 +45,14 @@ var rootCmd = &cobra.Command{ if err := http.ListenAndServe(addr, handler); !errors.Is(err, http.ErrServerClosed) { return err } + return nil }, } func main() { - if err := rootCmd.Execute(); err != nil { + err := rootCmd.Execute() + if err != nil { log.Fatal(err) } } diff --git a/cmd/gogit/clone.go b/cmd/gogit/clone.go index 4024b06..bb9673b 100644 --- a/cmd/gogit/clone.go +++ b/cmd/gogit/clone.go @@ -60,9 +60,10 @@ var cloneCmd = &cobra.Command{ opts.Progress = cmd.OutOrStdout() } - fmt.Fprintf(cmd.ErrOrStderr(), "Cloning into '%s'...\n", dir) //nolint:errcheck + fmt.Fprintf(cmd.ErrOrStderr(), "Cloning into '%s'...\n", dir) _, err = git.PlainClone(dir, &opts) + return err }, DisableFlagsInUseLine: true, diff --git a/cmd/gogit/daemon.go b/cmd/gogit/daemon.go index c42317d..e7c8e0e 100644 --- a/cmd/gogit/daemon.go +++ b/cmd/gogit/daemon.go @@ -48,6 +48,7 @@ var daemonCmd = &cobra.Command{ } log.Printf("Starting Git daemon on %q", addr) + return srv.ListenAndServe() }, } @@ -62,17 +63,20 @@ var _ transport.Loader = (*dirsLoader)(nil) // NewDirsLoader creates a new dirsLoader with the given directories. func NewDirsLoader(dirs []string, strict, exportAll bool) *dirsLoader { - var loaders []transport.Loader - var fss []billy.Filesystem + loaders := make([]transport.Loader, 0, len(dirs)) + fss := make([]billy.Filesystem, 0, len(dirs)) + for _, dir := range dirs { abs, err := filepath.Abs(dir) if err != nil { continue } + fs := osfs.New(abs, osfs.WithBoundOS()) fss = append(fss, fs) loaders = append(loaders, transport.NewFilesystemLoader(fs, strict)) } + return &dirsLoader{loaders: loaders, fss: fss, exportAll: exportAll} } @@ -87,16 +91,18 @@ func (d *dirsLoader) Load(ep *transport.Endpoint) (storage.Storer, error) { // repository. dfs := d.fss[i] okFile := filepath.Join(ep.Path, "git-daemon-export-ok") + stat, err := dfs.Lstat(okFile) if err != nil || (stat != nil && !stat.Mode().IsRegular()) { // If the file does not exist or is a directory, // we skip this repository. continue } - } + return storer, nil } } + return nil, transport.ErrRepositoryNotFound } diff --git a/cmd/gogit/fetch.go b/cmd/gogit/fetch.go index 7297272..af84475 100644 --- a/cmd/gogit/fetch.go +++ b/cmd/gogit/fetch.go @@ -27,7 +27,7 @@ func init() { var fetchCmd = &cobra.Command{ Use: "fetch [] [--] [ [...]]", Short: "Download objects and refs from another repository", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { r, err := git.PlainOpen(".") if err != nil { return err diff --git a/cmd/gogit/main.go b/cmd/gogit/main.go index 335ffff..b7b3175 100644 --- a/cmd/gogit/main.go +++ b/cmd/gogit/main.go @@ -34,22 +34,26 @@ var envToTarget = map[string]trace.Target{ func init() { // Set up tracing var target trace.Target + for k, v := range envToTarget { if ok, _ := strconv.ParseBool(os.Getenv(k)); ok { target |= v } } + trace.SetTarget(target) } func main() { - if err := rootCmd.Execute(); err != nil { + err := rootCmd.Execute() + if err != nil { var rerr *transport.RemoteError if errors.As(err, &rerr) { fmt.Fprintln(os.Stderr, rerr) } else { fmt.Fprintln(os.Stderr, err) } + os.Exit(1) } } diff --git a/cmd/gogit/pull.go b/cmd/gogit/pull.go index ee05161..4734c2e 100644 --- a/cmd/gogit/pull.go +++ b/cmd/gogit/pull.go @@ -50,6 +50,7 @@ var pullCmd = &cobra.Command{ switch { case errors.Is(err, git.NoErrAlreadyUpToDate): cmd.Println("Already up-to-date.") + return nil default: return err diff --git a/cmd/gogit/push.go b/cmd/gogit/push.go index c6914c0..5346829 100644 --- a/cmd/gogit/push.go +++ b/cmd/gogit/push.go @@ -51,6 +51,7 @@ var pushCmd = &cobra.Command{ if args[0] == remote { remoteName = remote isRemote = true + break } } @@ -114,6 +115,7 @@ var pushCmd = &cobra.Command{ err = remote.Push(&opts) if errors.Is(err, git.NoErrAlreadyUpToDate) { cmd.PrintErr("Everything up-to-date") + return nil } diff --git a/cmd/gogit/update-server-info.go b/cmd/gogit/update-server-info.go index 41dda47..005f22c 100644 --- a/cmd/gogit/update-server-info.go +++ b/cmd/gogit/update-server-info.go @@ -1,6 +1,8 @@ package main import ( + "errors" + "github.com/go-git/go-git/v6" "github.com/go-git/go-git/v6/plumbing/transport" "github.com/go-git/go-git/v6/storage/filesystem" @@ -14,13 +16,17 @@ func init() { var updateServerInfoCmd = &cobra.Command{ Use: "update-server-info", Short: "Update the server info file", - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, _ []string) error { r, err := git.PlainOpen(".") if err != nil { return err } - fs := r.Storer.(*filesystem.Storage).Filesystem() - return transport.UpdateServerInfo(r.Storer, fs) + store, ok := r.Storer.(*filesystem.Storage) + if !ok { + return errors.New("storer does not implement filesystem.Storage") + } + + return transport.UpdateServerInfo(r.Storer, store.Filesystem()) }, } diff --git a/cmd/gogit/verify-pack.go b/cmd/gogit/verify-pack.go index b097d10..3cdc282 100644 --- a/cmd/gogit/verify-pack.go +++ b/cmd/gogit/verify-pack.go @@ -99,7 +99,7 @@ func verifyPack(path string, verbose bool) error { } }() - scanner, err := pf.Scanner() + scanner, err := pf.Scanner() //nolint:staticcheck if err != nil { return fmt.Errorf("failed to get scanner: %w", err) } @@ -131,7 +131,10 @@ func verifyPack(path string, verbose bool) error { return fmt.Errorf("failed to scan object at offset %d", entry.Offset) } - header := scanner.Data().Value().(packfile.ObjectHeader) + header, ok := scanner.Data().Value().(packfile.ObjectHeader) + if !ok { + return errors.New("failed to scan pack header") + } // For delta objects, Size is the delta size. // For regular objects, Size is the inflated size. @@ -195,13 +198,17 @@ func verifyPack(path string, verbose bool) error { return fmt.Errorf("failed to scan object at offset %d", objects[i].offset) } - header := scanner.Data().Value().(packfile.ObjectHeader) + header, ok := scanner.Data().Value().(packfile.ObjectHeader) + if !ok { + return errors.New("failed to scan pack header") + } // Calculate delta chain depth. depth := 1 var baseHash plumbing.Hash + //exhaustive:ignore only delta types needs handling. switch header.Type { case plumbing.REFDeltaObject: baseHash = header.Reference @@ -236,7 +243,10 @@ func verifyPack(path string, verbose bool) error { break } - baseHeader := scanner.Data().Value().(packfile.ObjectHeader) + baseHeader, ok := scanner.Data().Value().(packfile.ObjectHeader) + if !ok { + break + } depth++ @@ -326,6 +336,7 @@ func openPack(path string) (billy.File, billy.File, error) { if verifyPackFixtureUrl { f = fixtures.ByURL(path) } + if verifyPackFixtureTag { f = fixtures.ByTag(path) } @@ -335,6 +346,7 @@ func openPack(path string) (billy.File, billy.File, error) { } fixture := f.One() + return fixture.Idx(), fixture.Packfile(), nil } diff --git a/server/git/logging.go b/server/git/logging.go index 4ee5ce0..8c44bfb 100644 --- a/server/git/logging.go +++ b/server/git/logging.go @@ -9,13 +9,15 @@ import ( ) type Logger interface { - Printf(format string, v ...interface{}) + Printf(format string, v ...any) } func LoggingMiddleware(logger Logger, next Handler) HandlerFunc { return func(ctx context.Context, c io.ReadWriteCloser, r *packp.GitProtoRequest) { now := time.Now() + next.ServeTCP(ctx, c, r) + elapsedTime := time.Since(now) if logger != nil { logger.Printf("%s %s %s %v %v", r.Host, r.RequestCommand, r.Pathname, r.ExtraParams, elapsedTime) diff --git a/server/git/server.go b/server/git/server.go index 8da794c..b540124 100644 --- a/server/git/server.go +++ b/server/git/server.go @@ -99,14 +99,17 @@ func (s *Server) Shutdown(ctx context.Context) error { if pollIntervalBase > shutdownPollIntervalMax { pollIntervalBase = shutdownPollIntervalMax } + return interval } timer := time.NewTimer(nextPollInterval()) + for { if s.closeIdleConns() { return lnerr } + select { case <-ctx.Done(): return ctx.Err() @@ -123,6 +126,7 @@ func (s *Server) Close() error { s.mu.Lock() defer s.mu.Unlock() + err := s.closeListenersLocked() // We need to unlock the mutex while waiting for listenersGroup. @@ -131,9 +135,10 @@ func (s *Server) Close() error { s.mu.Lock() for c := range s.activeConn { - c.Close() //nolint:errcheck + c.Close() delete(s.activeConn, c) } + return err } @@ -143,14 +148,17 @@ func (s *Server) ListenAndServe() error { if s.shuttingDown() { return ErrServerClosed } + addr := s.Addr if addr == "" { addr = DefaultAddr // Default Git protocol port } + ln, err := net.Listen("tcp", addr) if err != nil { return err } + return s.Serve(ln) } @@ -158,8 +166,9 @@ func (s *Server) ListenAndServe() error { // listener. func (s *Server) Serve(ln net.Listener) error { origLn := ln + l := &onceCloseListener{Listener: ln} - defer l.Close() //nolint:errcheck + defer l.Close() if !s.trackListener(&l.Listener, true) { return ErrServerClosed @@ -175,28 +184,35 @@ func (s *Server) Serve(ln net.Listener) error { } var tempDelay time.Duration // how long to sleep on accept failure + ctx := context.WithValue(baseCtx, ServerContextKey, s) + for { rw, err := l.Accept() if err != nil { if s.shuttingDown() { return ErrServerClosed } - if ne, ok := err.(net.Error); ok && ne.Temporary() { + + var ne net.Error + if errors.As(err, &ne) { if tempDelay == 0 { tempDelay = 5 * time.Millisecond } else { tempDelay *= 2 } - if max := 1 * time.Second; tempDelay > max { - tempDelay = max - } + + tempDelay = min(tempDelay, 1*time.Second) + s.logf("git: Accept error: %v; retrying in %v", err, tempDelay) time.Sleep(tempDelay) + continue } + return err } + connCtx := ctx if cc := s.ConnContext; cc != nil { connCtx = cc(ctx, rw) @@ -204,10 +220,12 @@ func (s *Server) Serve(ln net.Listener) error { panic("git: ConnContext returned nil context") } } + tempDelay = 0 c := s.newConn(rw) s.trackConn(c, true) - go c.serve(connCtx) //nolint:errcheck + + go c.serve(connCtx) } } @@ -222,6 +240,7 @@ func (s *Server) closeListenersLocked() error { err = cerr } } + return err } @@ -247,19 +266,23 @@ func (s *Server) handler(ctx context.Context, c net.Conn, req *packp.GitProtoReq func (s *Server) trackListener(ln *net.Listener, add bool) bool { s.mu.Lock() defer s.mu.Unlock() + if s.listeners == nil { s.listeners = make(map[*net.Listener]struct{}) } + if add { if s.shuttingDown() { return false } + s.listeners[ln] = struct{}{} s.listenerGroup.Add(1) } else { delete(s.listeners, ln) s.listenerGroup.Done() } + return true } @@ -267,20 +290,24 @@ func (s *Server) trackListener(ln *net.Listener, add bool) bool { // connection was found. func (s *Server) closeIdleConns() bool { idle := true + for c := range s.activeConn { unixSec := c.unixSec.Load() if unixSec == 0 { // New connection, skip it. idle = false + continue } - c.Close() //nolint:errcheck + + c.Close() delete(s.activeConn, c) } + return idle } -func (s *Server) logf(format string, args ...interface{}) { +func (s *Server) logf(format string, args ...any) { if s.ErrorLog != nil { s.ErrorLog.Printf(format, args...) } @@ -289,10 +316,13 @@ func (s *Server) logf(format string, args ...interface{}) { func (s *Server) trackConn(c *conn, add bool) { s.mu.Lock() defer s.mu.Unlock() + c.unixSec.Store(uint64(time.Now().Unix())) + if s.activeConn == nil { s.activeConn = make(map[*conn]struct{}) } + if add { s.activeConn[c] = struct{}{} } else { @@ -305,6 +335,7 @@ type conn struct { // Conn is the underlying net.Conn that is being used to read and write Git // protocol messages. net.Conn + // unix timestamp in seconds when the connection was established unixSec atomic.Uint64 // s the server that is handling this connection. @@ -319,19 +350,13 @@ func (s *Server) newConn(rwc net.Conn) *conn { } } -// logf logs a message using the server's ErrorLog, if set. -func (c *conn) logf(format string, args ...interface{}) { - if c.s.ErrorLog != nil { - c.s.logf(format, args...) - } -} - // serve serves a new connection. func (c *conn) serve(ctx context.Context) { defer func() { if err := recover(); err != nil { c.s.logf("git: panic serving connection: %v", err) - if cerr := c.Conn.Close(); cerr != nil { + + if cerr := c.Close(); cerr != nil { c.s.logf("git: error closing connection: %v", cerr) } } @@ -342,9 +367,11 @@ func (c *conn) serve(ctx context.Context) { var req packp.GitProtoRequest if err := req.Decode(r); err != nil { c.s.logf("git: error decoding request: %v", err) - if rErr := renderError(c, fmt.Errorf("error decoding request: %s", transport.ErrInvalidRequest)); rErr != nil { + + if rErr := renderError(c, fmt.Errorf("error decoding request: %w", transport.ErrInvalidRequest)); rErr != nil { c.s.logf("git: error writing error response: %v", rErr) } + return } @@ -355,12 +382,14 @@ func (c *conn) serve(ctx context.Context) { // multiple Close calls. type onceCloseListener struct { net.Listener + once sync.Once closeErr error } func (oc *onceCloseListener) Close() error { oc.once.Do(oc.close) + return oc.closeErr } @@ -374,8 +403,10 @@ type contextKey struct { func renderError(rw io.WriteCloser, err error) error { if _, err := pktline.WriteError(rw, err); err != nil { - rw.Close() //nolint:errcheck + rw.Close() + return err } + return rw.Close() } From 643f56bdd635415ec491f649608336009a858a89 Mon Sep 17 00:00:00 2001 From: Paulo Gomes Date: Tue, 2 Dec 2025 08:25:31 +0000 Subject: [PATCH 3/3] verify-pack: Split verifyPack into smaller funcs Signed-off-by: Paulo Gomes --- cmd/gogit/verify-pack.go | 285 +++++++++++++++++++++------------------ 1 file changed, 157 insertions(+), 128 deletions(-) diff --git a/cmd/gogit/verify-pack.go b/cmd/gogit/verify-pack.go index 3cdc282..913ece8 100644 --- a/cmd/gogit/verify-pack.go +++ b/cmd/gogit/verify-pack.go @@ -109,71 +109,151 @@ func verifyPack(path string, verbose bool) error { return fmt.Errorf("failed to get entries: %w", err) } - var objects []objectInfo + objects, err := collectObjectInfo(entries, scanner) + if err != nil { + return err + } - for { - entry, err := entries.Next() - if errors.Is(err, io.EOF) { - break + // Calculate the packed size of the last object. + if len(objects) > 0 { + stat, err := packFile.Stat() + if err != nil { + return fmt.Errorf("failed to stat pack file: %w", err) } + // Pack file ends with a checksum (20-byte SHA-1 or 32-byte SHA-256). + objects[len(objects)-1].packedSize = stat.Size() - objects[len(objects)-1].offset - int64(ch.Size()) + } + // Resolve actual types for all objects (after delta application). + for i := range objects { + obj, err := pf.GetByOffset(objects[i].offset) if err != nil { - return fmt.Errorf("failed to read entry: %w", err) + return fmt.Errorf("failed to get object at offset %d: %w", objects[i].offset, err) } - // Read raw object header to get delta information. - err = scanner.SeekFromStart(int64(entry.Offset)) - if err != nil { - return fmt.Errorf("failed to seek to offset %d: %w", entry.Offset, err) + objects[i].typ = obj.Type() + } + + deltaChains, err := calculateDeltaChains(objects, scanner) + if err != nil { + return err + } + + if verbose { + printPackStatistics(objects, deltaChains) + } + + fmt.Printf("%s: ok\n", path) + + return nil +} + +func openPack(path string) (billy.File, billy.File, error) { + if verifyPackFixtureUrl || verifyPackFixtureTag { + var f fixtures.Fixtures + if verifyPackFixtureUrl { + f = fixtures.ByURL(path) } - if !scanner.Scan() { - return fmt.Errorf("failed to scan object at offset %d", entry.Offset) + if verifyPackFixtureTag { + f = fixtures.ByTag(path) } - header, ok := scanner.Data().Value().(packfile.ObjectHeader) - if !ok { - return errors.New("failed to scan pack header") + if len(f) == 0 { + return nil, nil, fmt.Errorf("no fixture found for %q", path) } - // For delta objects, Size is the delta size. - // For regular objects, Size is the inflated size. - info := objectInfo{ - hash: entry.Hash, - diskType: header.Type, - size: header.Size, - offset: int64(entry.Offset), + fixture := f.One() + + return fixture.Idx(), fixture.Packfile(), nil + } + + idxPath := path + packPath := path + + if before, ok := strings.CutSuffix(path, ".idx"); ok { + packPath = before + ".pack" + } else if before, ok := strings.CutSuffix(path, ".pack"); ok { + idxPath = before + ".idx" + } else { + return nil, nil, errors.New("file must have .idx or .pack extension") + } + + idxFile, err := os.Open(idxPath) + if err != nil { + return nil, nil, fmt.Errorf("failed to open index file: %w", err) + } + + packFile, err := os.Open(packPath) + if err != nil { + return nil, nil, fmt.Errorf("failed to open pack file: %w", err) + } + + return idxFile, packFile, nil +} + +// printPackStatistics outputs detailed object information and delta chain +// statistics to stdout. +func printPackStatistics(objects []objectInfo, deltaChains map[plumbing.Hash]int) { + for _, obj := range objects { + // Format type with padding to match git's output. + typeStr := obj.typ.String() + if len(typeStr) == 4 { + typeStr = typeStr + " " + } else { + typeStr = typeStr + " " } - // Calculate packed size (distance to next header or end of file). - if len(objects) > 0 { - objects[len(objects)-1].packedSize = info.offset - objects[len(objects)-1].offset + fmt.Printf("%s %s%d %d %d", + obj.hash.String(), + typeStr, + obj.size, + obj.packedSize, + obj.offset, + ) + + if obj.diskType.IsDelta() && !obj.base.IsZero() { + fmt.Printf(" %d %s", obj.depth, obj.base.String()) } - objects = append(objects, info) + fmt.Println() } - // Calculate the packed size of the last object. - if len(objects) > 0 { - stat, err := packFile.Stat() - if err != nil { - return fmt.Errorf("failed to stat pack file: %w", err) - } - // Pack file ends with a checksum (20-byte SHA-1 or 32-byte SHA-256). - objects[len(objects)-1].packedSize = stat.Size() - objects[len(objects)-1].offset - int64(ch.Size()) + // Print statistics. + nonDelta := len(objects) - len(deltaChains) + fmt.Printf("non delta: %d objects\n", nonDelta) + + // Count chain lengths. + chainLengths := make(map[int]int) + for _, depth := range deltaChains { + chainLengths[depth]++ } - // Resolve actual types for all objects (after delta application). - for i := range objects { - obj, err := pf.GetByOffset(objects[i].offset) - if err != nil { - return fmt.Errorf("failed to get object at offset %d: %w", objects[i].offset, err) + // Sort chain lengths for consistent output. + lengths := make([]int, 0, len(chainLengths)) + for length := range chainLengths { + lengths = append(lengths, length) + } + + sort.Ints(lengths) + + for _, length := range lengths { + count := chainLengths[length] + + objWord := "objects" + if count == 1 { + objWord = "object" } - objects[i].typ = obj.Type() + fmt.Printf("chain length = %d: %d %s\n", length, count, objWord) } +} - // Build delta chain information. +// calculateDeltaChains computes delta chain depth and base object references +// for all delta objects in the pack file. +// +//nolint:gocognit +func calculateDeltaChains(objects []objectInfo, scanner *packfile.Scanner) (map[plumbing.Hash]int, error) { deltaChains := make(map[plumbing.Hash]int) objectByHash := make(map[plumbing.Hash]*objectInfo) objectByOffset := make(map[int64]*objectInfo) @@ -191,16 +271,16 @@ func verifyPack(path string, verbose bool) error { err := scanner.SeekFromStart(objects[i].offset) if err != nil { - return fmt.Errorf("failed to seek to offset %d: %w", objects[i].offset, err) + return nil, fmt.Errorf("failed to seek to offset %d: %w", objects[i].offset, err) } if !scanner.Scan() { - return fmt.Errorf("failed to scan object at offset %d", objects[i].offset) + return nil, fmt.Errorf("failed to scan object at offset %d", objects[i].offset) } header, ok := scanner.Data().Value().(packfile.ObjectHeader) if !ok { - return errors.New("failed to scan pack header") + return nil, errors.New("failed to scan pack header") } // Calculate delta chain depth. @@ -270,106 +350,55 @@ func verifyPack(path string, verbose bool) error { deltaChains[objects[i].hash] = depth } - if verbose { - for _, obj := range objects { - // Format type with padding to match git's output. - typeStr := obj.typ.String() - if len(typeStr) == 4 { - typeStr = typeStr + " " - } else { - typeStr = typeStr + " " - } - - fmt.Printf("%s %s%d %d %d", - obj.hash.String(), - typeStr, - obj.size, - obj.packedSize, - obj.offset, - ) + return deltaChains, nil +} - if obj.diskType.IsDelta() && !obj.base.IsZero() { - fmt.Printf(" %d %s", obj.depth, obj.base.String()) - } +// collectObjectInfo reads all objects from the pack file and builds a list +// of object metadata including offsets and packed sizes. +func collectObjectInfo(entries idxfile.EntryIter, scanner *packfile.Scanner) ([]objectInfo, error) { + var objects []objectInfo - fmt.Println() + for { + entry, err := entries.Next() + if errors.Is(err, io.EOF) { + break } - // Print statistics. - nonDelta := len(objects) - len(deltaChains) - fmt.Printf("non delta: %d objects\n", nonDelta) - - // Count chain lengths. - chainLengths := make(map[int]int) - for _, depth := range deltaChains { - chainLengths[depth]++ + if err != nil { + return nil, fmt.Errorf("failed to read entry: %w", err) } - // Sort chain lengths for consistent output. - var lengths []int - for length := range chainLengths { - lengths = append(lengths, length) + // Read raw object header to get delta information. + err = scanner.SeekFromStart(int64(entry.Offset)) + if err != nil { + return nil, fmt.Errorf("failed to seek to offset %d: %w", entry.Offset, err) } - sort.Ints(lengths) - - for _, length := range lengths { - count := chainLengths[length] - - objWord := "objects" - if count == 1 { - objWord = "object" - } - - fmt.Printf("chain length = %d: %d %s\n", length, count, objWord) + if !scanner.Scan() { + return nil, fmt.Errorf("failed to scan object at offset %d", entry.Offset) } - } - fmt.Printf("%s: ok\n", path) - - return nil -} - -func openPack(path string) (billy.File, billy.File, error) { - if verifyPackFixtureUrl || verifyPackFixtureTag { - var f fixtures.Fixtures - if verifyPackFixtureUrl { - f = fixtures.ByURL(path) + header, ok := scanner.Data().Value().(packfile.ObjectHeader) + if !ok { + return nil, errors.New("failed to scan pack header") } - if verifyPackFixtureTag { - f = fixtures.ByTag(path) + // For delta objects, Size is the delta size. + // For regular objects, Size is the inflated size. + info := objectInfo{ + hash: entry.Hash, + diskType: header.Type, + size: header.Size, + offset: int64(entry.Offset), } - if len(f) == 0 { - return nil, nil, fmt.Errorf("no fixture found for %q", path) + // Calculate packed size (distance to next header or end of file). + if len(objects) > 0 { + objects[len(objects)-1].packedSize = info.offset - objects[len(objects)-1].offset } - fixture := f.One() - - return fixture.Idx(), fixture.Packfile(), nil - } - - idxPath := path - packPath := path - - if before, ok := strings.CutSuffix(path, ".idx"); ok { - packPath = before + ".pack" - } else if before, ok := strings.CutSuffix(path, ".pack"); ok { - idxPath = before + ".idx" - } else { - return nil, nil, errors.New("file must have .idx or .pack extension") - } - - idxFile, err := os.Open(idxPath) - if err != nil { - return nil, nil, fmt.Errorf("failed to open index file: %w", err) - } - - packFile, err := os.Open(packPath) - if err != nil { - return nil, nil, fmt.Errorf("failed to open pack file: %w", err) + objects = append(objects, info) } - return idxFile, packFile, nil + return objects, nil }