1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package gitcli
4
5import (
6 "bufio"
7 "context"
8 "fmt"
9
10 "bnema.dev/ermite/internal/domain"
11 "bnema.dev/ermite/internal/ports"
12)
13
14var _ ports.CIGit = (*Git)(nil)
15
16// emptyTree is the id of the empty tree of a SHA-1 repository, what a diff
17// from "nothing" starts at.
18const emptyTree = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
19
20// ChangedPaths runs `git diff-tree -r --name-only -z` from base to head, with
21// renames not detected (a rename lists both paths, as GitHub's push filters
22// see it), and stops reading after max paths: truncated tells there were more.
23// A zero base diffs against the empty tree.
24//
25//hexcheck:ignore suspicious-business-logic-in-adapter reads the path list git prints; the bound comes from the port
26func (g *Git) ChangedPaths(ctx context.Context, repo domain.RepoName, base, head domain.ObjectID, limit int) ([]string, bool, error) {
27 if limit < 1 || head.IsZero() {
28 return nil, false, fmt.Errorf("changed paths: %w: limit %d", domain.ErrInvalid, limit)
29 }
30 dir, err := g.repoDir(repo)
31 if err != nil {
32 return nil, false, err
33 }
34 from := emptyTree
35 if !base.IsZero() {
36 from = base.String()
37 }
38 args := readArgs(dir, "diff-tree", "-r", "--name-only", "-z", "--no-renames", "--no-commit-id", "--end-of-options", from, head.String())
39 var (
40 paths []string
41 truncated bool
42 )
43 err = g.pipe(ctx, args, func(r *bufio.Reader) error {
44 for {
45 name, err := r.ReadString(0)
46 if err != nil {
47 return nil // the end of the output; a failing exit status is reported by pipe
48 }
49 if len(paths) == limit {
50 truncated = true
51 return errStop
52 }
53 paths = append(paths, name[:len(name)-1])
54 }
55 })
56 if err != nil {
57 return nil, false, fmt.Errorf("changed paths %s..%s in %q: %w", from, head, repo, err)
58 }
59 return paths, truncated, nil
60}