1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package gitcli
4
5import (
6 "context"
7 "fmt"
8 "strconv"
9 "strings"
10 "time"
11
12 "bnema.dev/ermite/internal/domain"
13)
14
15// CountCommits runs `git rev-list --count`, from the commit graph when the
16// repository has one. A bound uses --since-as-filter, which looks at every
17// reachable commit: --since stops the walk at the first older one, and
18// misses newer commits behind it when dates are out of order.
19//
20//hexcheck:ignore suspicious-business-logic-in-adapter reads the count git prints
21func (g *Git) CountCommits(ctx context.Context, repo domain.RepoName, head domain.ObjectID, since time.Time) (int, error) {
22 if head.IsZero() {
23 return 0, fmt.Errorf("count commits: %w: no head", domain.ErrInvalid)
24 }
25 dir, err := g.repoDir(repo)
26 if err != nil {
27 return 0, err
28 }
29 args := []string{"rev-list", "--count"}
30 if !since.IsZero() {
31 args = append(args, "--since-as-filter=@"+strconv.FormatInt(since.Unix(), 10))
32 }
33 out, err := g.run(ctx, readArgs(dir, append(args, "--end-of-options", head.String())...)...)
34 if err != nil {
35 if exitCode(err) == 128 {
36 return 0, fmt.Errorf("count commits of %q: %w", repo, domain.ErrNotFound)
37 }
38 return 0, fmt.Errorf("count commits of %q: %w", repo, err)
39 }
40 n, err := strconv.Atoi(strings.TrimSpace(out))
41 if err != nil {
42 return 0, fmt.Errorf("count commits of %q: git printed %q", repo, strings.TrimSpace(out))
43 }
44 return n, nil
45}
46
47// LatestTag runs `git for-each-ref --sort=-creatordate --count=1` on the tags.
48//
49//hexcheck:ignore suspicious-business-logic-in-adapter reads the line git prints
50func (g *Git) LatestTag(ctx context.Context, repo domain.RepoName) (domain.TagInfo, bool, error) {
51 dir, err := g.repoDir(repo)
52 if err != nil {
53 return domain.TagInfo{}, false, err
54 }
55 out, err := g.run(ctx, readArgs(dir, "for-each-ref", "--sort=-creatordate", "--count=1",
56 "--format=%(refname:strip=2)%00%(creatordate:unix)%00%(objecttype)%00%(objectname)%00%(*objecttype)%00%(*objectname)", tagsRefPrefix)...)
57 if err != nil {
58 return domain.TagInfo{}, false, fmt.Errorf("latest tag of %q: %w", repo, err)
59 }
60 line := strings.TrimSuffix(out, "\n")
61 if line == "" {
62 return domain.TagInfo{}, false, nil
63 }
64 f := strings.Split(line, "\x00")
65 if len(f) != 6 || f[0] == "" {
66 return domain.TagInfo{}, false, fmt.Errorf("latest tag of %q: unexpected line %q", repo, truncateText(line))
67 }
68 sec, err := strconv.ParseInt(f[1], 10, 64)
69 if err != nil {
70 return domain.TagInfo{}, false, fmt.Errorf("latest tag of %q: unexpected line %q", repo, truncateText(line))
71 }
72 tag := domain.TagInfo{Name: domain.ValidGitText(f[0]), At: time.Unix(sec, 0).UTC()}
73 // A lightweight tag names its commit; an annotated one is peeled to the
74 // end of its chain of tags.
75 commit := ""
76 switch {
77 case f[2] == "commit":
78 commit = f[3]
79 case f[4] == "commit":
80 commit = f[5]
81 }
82 if commit != "" {
83 if tag.CommitID, err = domain.ParseObjectID(commit); err != nil {
84 return domain.TagInfo{}, false, fmt.Errorf("latest tag of %q: %w", repo, err)
85 }
86 }
87 return tag, true, nil
88}