// SPDX-License-Identifier: AGPL-3.0-or-later package gitcli import ( "bufio" "context" "fmt" "bnema.dev/ermite/internal/domain" "bnema.dev/ermite/internal/ports" ) var _ ports.CIGit = (*Git)(nil) // emptyTree is the id of the empty tree of a SHA-1 repository, what a diff // from "nothing" starts at. const emptyTree = "4b825dc642cb6eb9a060e54bf8d69288fbee4904" // ChangedPaths runs `git diff-tree -r --name-only -z` from base to head, with // renames not detected (a rename lists both paths, as GitHub's push filters // see it), and stops reading after max paths: truncated tells there were more. // A zero base diffs against the empty tree. // //hexcheck:ignore suspicious-business-logic-in-adapter reads the path list git prints; the bound comes from the port func (g *Git) ChangedPaths(ctx context.Context, repo domain.RepoName, base, head domain.ObjectID, limit int) ([]string, bool, error) { if limit < 1 || head.IsZero() { return nil, false, fmt.Errorf("changed paths: %w: limit %d", domain.ErrInvalid, limit) } dir, err := g.repoDir(repo) if err != nil { return nil, false, err } from := emptyTree if !base.IsZero() { from = base.String() } args := readArgs(dir, "diff-tree", "-r", "--name-only", "-z", "--no-renames", "--no-commit-id", "--end-of-options", from, head.String()) var ( paths []string truncated bool ) err = g.pipe(ctx, args, func(r *bufio.Reader) error { for { name, err := r.ReadString(0) if err != nil { return nil // the end of the output; a failing exit status is reported by pipe } if len(paths) == limit { truncated = true return errStop } paths = append(paths, name[:len(name)-1]) } }) if err != nil { return nil, false, fmt.Errorf("changed paths %s..%s in %q: %w", from, head, repo, err) } return paths, truncated, nil }