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}
61
62// CommitsOffBranches lists the commits reachable from head and from no branch
63// (refs/heads/*), newest first, at most limit. It runs
64// `git log --not --branches --not <head>`: tags do not count, so the commit of
65// another tag that no branch has is listed.
66//
67//hexcheck:ignore suspicious-business-logic-in-adapter parses git output into domain values; the limit comes from the port
68func (g *Git) CommitsOffBranches(ctx context.Context, repo domain.RepoName, head domain.ObjectID, limit int) ([]domain.Commit, bool, error) {
69 if head.IsZero() {
70 return nil, false, fmt.Errorf("commits off branches: %w: no head", domain.ErrInvalid)
71 }
72 // "--not --branches" excludes every branch, the second "--not" turns the
73 // negation off again for the head that follows.
74 return g.logCommits(ctx, repo, head, limit, []string{"--not", "--branches", "--not"}, nil)
75}