1
2
3 package git
4
5 import (
6 "errors"
7 "fmt"
8 "io/fs"
9 "net/url"
10 "os"
11 "path/filepath"
12 "strconv"
13 "strings"
14 "time"
15
16 "edge-infra.dev/pkg/lib/cli/sh"
17 )
18
19
20 type Git struct {
21 sh *sh.Shell
22 }
23
24
25 func New() *Git {
26 return &Git{sh.New()}
27 }
28
29
30 func NewInDir(path string) *Git {
31 return &Git{sh.NewInDir(path)}
32 }
33
34
35
36 func (g *Git) Path() (string, error) {
37 out, err := g.sh.Run("git rev-parse --show-toplevel")
38 if err != nil {
39 return "", fmt.Errorf("failed to get repository path: %w", err)
40 }
41 return strings.TrimSpace(out), nil
42 }
43
44
45 func (g *Git) Remote() (string, error) {
46 out, err := g.sh.Run("git config --get remote.origin.url")
47 if err != nil {
48 return "", fmt.Errorf("failed to get git remote origin URL: %w", err)
49 }
50
51
52 urlStr := strings.TrimSuffix(strings.TrimSpace(out), ".git")
53
54
55 url, err := url.Parse(urlStr)
56 if err != nil {
57
58
59 return urlStr, nil
60 }
61 url.User = nil
62
63 return url.String(), nil
64 }
65
66
67
68
69
70
71
72 func (g *Git) Timestamp(args ...string) (time.Time, error) {
73 cmd := "git log -1 --format=%ct"
74 if len(args) > 0 {
75 cmd = cmd + " " + strings.Join(args, " ")
76 }
77
78 out, err := g.sh.Run(cmd)
79 if err != nil {
80 return time.Time{}, fmt.Errorf("failed to get timestamp of commit from git: %w", err)
81 }
82
83 timestamp, err := strconv.ParseInt(strings.TrimSpace(out), 10, 64)
84 if err != nil {
85 return time.Time{}, fmt.Errorf("failed to convert timestamp of commit to int: %w", err)
86 }
87
88 return time.Unix(timestamp, 0).UTC(), nil
89 }
90
91
92 func (g *Git) Commit() (string, error) {
93 out, err := g.sh.Run("git rev-parse HEAD")
94 if err != nil {
95 return "", fmt.Errorf("failed to get HEAD commit from git: %w", err)
96 }
97 return strings.TrimSpace(out), nil
98 }
99
100
101 func (g *Git) Branch() (string, error) {
102 out, err := g.sh.Run("git rev-parse --abbrev-ref HEAD")
103 if err != nil {
104 return "", fmt.Errorf("failed to get current branch from git: %w", err)
105 }
106 return strings.TrimSpace(out), nil
107 }
108
109
110
111 func (g *Git) CommitForFile(file string) (string, error) {
112 out, err := g.sh.Run("git log -n 1 --pretty=format:%H -- " + file)
113 if err != nil {
114 return "", fmt.Errorf("failed to get HEAD commit from git for %s: %w", file, err)
115 }
116 return strings.TrimSpace(out), nil
117 }
118
119 func (g *Git) UpdateRemotes(remotes ...string) error {
120 _, err := g.sh.Run(fmt.Sprintf("git remote update %s", strings.Join(remotes, " ")))
121 if err != nil {
122 return fmt.Errorf("failed to update remotes %s: %w", remotes, err)
123 }
124 return nil
125 }
126
127
128
129 func (g *Git) IsDirty() (bool, error) {
130 out, err := g.sh.Run("git status --porcelain --untracked=no")
131 if err != nil {
132 return false, fmt.Errorf("failed to check if current branch is dirty: %w", err)
133 }
134 return strings.TrimSpace(out) != "", nil
135 }
136
137
138 func (g *Git) IsRebaseInProgress() (bool, error) {
139 present, err := g.checkForPath("rebase-merge")
140 if err != nil {
141 return false, err
142 }
143 if present {
144 return true, nil
145 }
146
147 present, err = g.checkForPath("rebase-apply")
148 if err != nil {
149 return false, err
150 }
151 if present {
152 return true, nil
153 }
154
155 return false, nil
156 }
157
158 func (g *Git) checkForPath(p string) (bool, error) {
159 path, err := g.sh.Run(fmt.Sprintf("git rev-parse --git-path %s", p))
160 if err != nil {
161 return false, fmt.Errorf("failed to resolve git path %s: %w", p, err)
162 }
163 path = strings.TrimSpace(path)
164 _, err = os.Stat(filepath.Join(g.sh.Getwd(), path))
165 switch {
166 case err == nil:
167 return true, nil
168 case errors.Is(err, fs.ErrNotExist):
169 return false, nil
170 default:
171 return false, fmt.Errorf("failed to check for existence of '%s': %w", path, err)
172 }
173 }
174
View as plain text