1package git
2
3import (
4 "fmt"
5 "slices"
6 "strconv"
7 "strings"
8 "time"
9
10 "github.com/go-git/go-git/v5/plumbing"
11 "github.com/go-git/go-git/v5/plumbing/object"
12 "tangled.org/core/types"
13)
14
15type BranchesOptions struct {
16 Limit int
17 Offset int
18}
19
20func (g *GitRepo) Branches(opts *BranchesOptions) ([]types.Branch, error) {
21 if opts == nil {
22 opts = &BranchesOptions{}
23 }
24
25 fields := []string{
26 "refname:short",
27 "objectname",
28 "authorname",
29 "authoremail",
30 "authordate:unix",
31 "committername",
32 "committeremail",
33 "committerdate:unix",
34 "tree",
35 "parent",
36 "contents",
37 }
38
39 var outFormat strings.Builder
40 outFormat.WriteString("--format=")
41 for i, f := range fields {
42 if i != 0 {
43 outFormat.WriteString(fieldSeparator)
44 }
45 fmt.Fprintf(&outFormat, "%%(%s)", f)
46 }
47 outFormat.WriteString("")
48 outFormat.WriteString(recordSeparator)
49
50 args := []string{outFormat.String(), "--sort=-creatordate"}
51
52 // only add the count if the limit is a non-zero value,
53 // if it is zero, get as many tags as we can
54 if opts.Limit > 0 {
55 args = append(args, fmt.Sprintf("--count=%d", opts.Offset+opts.Limit))
56 }
57
58 args = append(args, "refs/heads")
59
60 output, err := g.forEachRef(args...)
61 if err != nil {
62 return nil, fmt.Errorf("failed to get branches: %w", err)
63 }
64
65 records := strings.Split(strings.TrimSpace(string(output)), recordSeparator)
66 if len(records) == 1 && records[0] == "" {
67 return nil, nil
68 }
69
70 startIdx := opts.Offset
71 if startIdx >= len(records) {
72 return nil, nil
73 }
74
75 endIdx := len(records)
76 if opts.Limit > 0 {
77 endIdx = min(startIdx+opts.Limit, len(records))
78 }
79
80 records = records[startIdx:endIdx]
81 branches := make([]types.Branch, 0, len(records))
82
83 // ignore errors here
84 defaultBranch, _ := g.FindMainBranch()
85
86 for _, line := range records {
87 parts := strings.SplitN(strings.TrimSpace(line), fieldSeparator, len(fields))
88 if len(parts) < 6 {
89 continue
90 }
91
92 branchName := parts[0]
93 commitHash := plumbing.NewHash(parts[1])
94 authorName := parts[2]
95 authorEmail := strings.TrimSuffix(strings.TrimPrefix(parts[3], "<"), ">")
96 authorDate := parts[4]
97 committerName := parts[5]
98 committerEmail := strings.TrimSuffix(strings.TrimPrefix(parts[6], "<"), ">")
99 committerDate := parts[7]
100 treeHash := plumbing.NewHash(parts[8])
101 parentHash := plumbing.NewHash(parts[9])
102 message := parts[10]
103
104 // parse creation time
105 var authoredAt, committedAt time.Time
106 if unix, err := strconv.ParseInt(authorDate, 10, 64); err == nil {
107 authoredAt = time.Unix(unix, 0)
108 }
109 if unix, err := strconv.ParseInt(committerDate, 10, 64); err == nil {
110 committedAt = time.Unix(unix, 0)
111 }
112
113 branch := types.Branch{
114 IsDefault: branchName == defaultBranch,
115 Reference: types.Reference{
116 Name: branchName,
117 Hash: commitHash.String(),
118 },
119 Commit: &object.Commit{
120 Hash: commitHash,
121 Author: object.Signature{
122 Name: authorName,
123 Email: authorEmail,
124 When: authoredAt,
125 },
126 Committer: object.Signature{
127 Name: committerName,
128 Email: committerEmail,
129 When: committedAt,
130 },
131 TreeHash: treeHash,
132 ParentHashes: []plumbing.Hash{parentHash},
133 Message: message,
134 },
135 }
136
137 branches = append(branches, branch)
138 }
139
140 slices.Reverse(branches)
141 return branches, nil
142}
143
144func (g *GitRepo) Branch(name string) (*plumbing.Reference, error) {
145 ref, err := g.r.Reference(plumbing.NewBranchReferenceName(name), false)
146 if err != nil {
147 return nil, fmt.Errorf("branch: %w", err)
148 }
149
150 if !ref.Name().IsBranch() {
151 return nil, fmt.Errorf("branch: %s is not a branch", ref.Name())
152 }
153
154 return ref, nil
155}
156
157func (g *GitRepo) DeleteBranch(branch string) error {
158 ref := plumbing.NewBranchReferenceName(branch)
159 return g.r.Storer.RemoveReference(ref)
160}