Skip to content

Commit a93d340

Browse files
authored
Extract git package with Client struct to encapsulate global state
1 parent 679e434 commit a93d340

8 files changed

Lines changed: 836 additions & 284 deletions

File tree

exit/exit.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package exit
2+
3+
import "os"
4+
5+
var Exit = os.Exit

git/git.go

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
package git
2+
3+
import (
4+
"bufio"
5+
"os/exec"
6+
"regexp"
7+
"strconv"
8+
"strings"
9+
10+
config "github.com/remotemobprogramming/mob/v5/configuration"
11+
"github.com/remotemobprogramming/mob/v5/exit"
12+
"github.com/remotemobprogramming/mob/v5/say"
13+
"github.com/remotemobprogramming/mob/v5/workdir"
14+
)
15+
16+
type Client struct {
17+
PassthroughStderrStdout bool
18+
}
19+
20+
func (g *Client) runCommandSilent(name string, args ...string) (string, string, error) {
21+
command := exec.Command(name, args...)
22+
if len(workdir.Path) > 0 {
23+
command.Dir = workdir.Path
24+
}
25+
commandString := strings.Join(command.Args, " ")
26+
say.Debug("Running command <" + commandString + "> in silent mode, capturing combined output")
27+
outputBytes, err := command.CombinedOutput()
28+
output := string(outputBytes)
29+
say.Debug(output)
30+
return commandString, output, err
31+
}
32+
33+
func (g *Client) runCommand(name string, args ...string) (string, string, error) {
34+
command := exec.Command(name, args...)
35+
if len(workdir.Path) > 0 {
36+
command.Dir = workdir.Path
37+
}
38+
commandString := strings.Join(command.Args, " ")
39+
say.Debug("Running command <" + commandString + "> passing output through")
40+
41+
stdout, _ := command.StdoutPipe()
42+
command.Stderr = command.Stdout
43+
errStart := command.Start()
44+
if errStart != nil {
45+
return commandString, "", errStart
46+
}
47+
48+
output := ""
49+
50+
stdoutscanner := bufio.NewScanner(stdout)
51+
lineEnded := true
52+
stdoutscanner.Split(bufio.ScanBytes)
53+
for stdoutscanner.Scan() {
54+
character := stdoutscanner.Text()
55+
if character == "\n" {
56+
lineEnded = true
57+
} else {
58+
if lineEnded {
59+
say.PrintToConsole(" ")
60+
lineEnded = false
61+
}
62+
}
63+
say.PrintToConsole(character)
64+
output += character
65+
}
66+
67+
errWait := command.Wait()
68+
if errWait != nil {
69+
say.Debug(output)
70+
return commandString, output, errWait
71+
}
72+
73+
say.Debug(output)
74+
return commandString, output, nil
75+
}
76+
77+
func (g *Client) Run(args ...string) {
78+
say.Indented("git " + strings.Join(args, " "))
79+
commandString, output, err := "", "", error(nil)
80+
if g.PassthroughStderrStdout {
81+
commandString, output, err = g.runCommand("git", args...)
82+
} else {
83+
commandString, output, err = g.runCommandSilent("git", args...)
84+
}
85+
86+
if err != nil {
87+
if !g.IsRepo() {
88+
say.Error("expecting the current working directory to be a git repository.")
89+
} else {
90+
if strings.Contains(output, "does not support push options") {
91+
say.Error("The receiving end does not support push options")
92+
say.Fix("Disable the push option ci.skip in your .mob file or set the expected environment variable", "export MOB_SKIP_CI_PUSH_OPTION_ENABLED=false")
93+
} else {
94+
say.Error(commandString)
95+
say.Error(output)
96+
say.Error(err.Error())
97+
}
98+
}
99+
exit.Exit(1)
100+
}
101+
}
102+
103+
func (g *Client) Silent(args ...string) string {
104+
commandString, output, err := g.runCommandSilent("git", args...)
105+
106+
if err != nil {
107+
if !g.IsRepo() {
108+
say.Error("expecting the current working directory to be a git repository.")
109+
} else {
110+
say.Error(commandString)
111+
say.Error(output)
112+
say.Error(err.Error())
113+
}
114+
exit.Exit(1)
115+
}
116+
return strings.TrimSpace(output)
117+
}
118+
119+
func (g *Client) SilentIgnoreFailure(args ...string) (string, error) {
120+
_, output, err := g.runCommandSilent("git", args...)
121+
122+
if err != nil {
123+
return "", err
124+
}
125+
return strings.TrimSpace(output), nil
126+
}
127+
128+
func (g *Client) RunWithoutEmptyStrings(args ...string) {
129+
argsWithoutEmptyStrings := deleteEmptyStrings(args)
130+
g.Run(argsWithoutEmptyStrings...)
131+
}
132+
133+
func (g *Client) RunIgnoreFailure(args ...string) error {
134+
commandString, output, err := "", "", error(nil)
135+
if g.PassthroughStderrStdout {
136+
commandString, output, err = g.runCommand("git", args...)
137+
} else {
138+
commandString, output, err = g.runCommandSilent("git", args...)
139+
}
140+
141+
if err != nil {
142+
if !g.IsRepo() {
143+
say.Error("expecting the current working directory to be a git repository.")
144+
exit.Exit(1)
145+
} else {
146+
say.Warning(commandString)
147+
say.Warning(output)
148+
say.Warning(err.Error())
149+
return err
150+
}
151+
}
152+
153+
say.Indented(commandString)
154+
return nil
155+
}
156+
157+
func HooksOption(c config.Configuration) string {
158+
if c.GitHooksEnabled {
159+
return ""
160+
} else {
161+
return "--no-verify"
162+
}
163+
}
164+
165+
func (g *Client) CurrentBranch() string {
166+
// upgrade to branch --show-current when git v2.21 is more widely spread
167+
return g.Silent("rev-parse", "--abbrev-ref", "HEAD")
168+
}
169+
170+
func (g *Client) Branches() []string {
171+
return strings.Split(g.Silent("branch", "--format=%(refname:short)"), "\n")
172+
}
173+
174+
func (g *Client) RemoteBranches() []string {
175+
return strings.Split(g.Silent("branch", "--remotes", "--format=%(refname:short)"), "\n")
176+
}
177+
178+
func (g *Client) UserName() string {
179+
output, _ := g.SilentIgnoreFailure("config", "--get", "user.name")
180+
return output
181+
}
182+
183+
func (g *Client) UserEmail() string {
184+
return g.Silent("config", "--get", "user.email")
185+
}
186+
187+
func (g *Client) IsRepo() bool {
188+
_, _, err := g.runCommandSilent("git", "rev-parse")
189+
return err == nil
190+
}
191+
192+
func (g *Client) RootDir() string {
193+
return g.Silent("rev-parse", "--show-toplevel")
194+
}
195+
196+
func (g *Client) Dir() string {
197+
return g.Silent("rev-parse", "--absolute-git-dir")
198+
}
199+
200+
func (g *Client) HasCommits() bool {
201+
commitCount := g.Silent("rev-list", "--all", "--count")
202+
return commitCount != "0"
203+
}
204+
205+
func (g *Client) DoBranchesDiverge(ancestor string, successor string) bool {
206+
_, _, err := g.runCommandSilent("git", "merge-base", "--is-ancestor", ancestor, successor)
207+
if err == nil {
208+
return false
209+
}
210+
return true
211+
}
212+
213+
func (g *Client) Version() string {
214+
_, output, err := g.runCommandSilent("git", "--version")
215+
if err != nil {
216+
say.Debug("gitVersion encountered an error: " + err.Error())
217+
return ""
218+
}
219+
return strings.TrimSpace(output)
220+
}
221+
222+
func (g *Client) IsNothingToCommit() bool {
223+
output := g.Silent("status", "--porcelain")
224+
return len(output) == 0
225+
}
226+
227+
func (g *Client) HasUncommittedChanges() bool {
228+
return !g.IsNothingToCommit()
229+
}
230+
231+
func (g *Client) CommitHash() string {
232+
output, _ := g.SilentIgnoreFailure("rev-parse", "HEAD")
233+
return output
234+
}
235+
236+
type GitVersion struct {
237+
Major int
238+
Minor int
239+
Patch int
240+
}
241+
242+
func ParseVersion(version string) GitVersion {
243+
// The git version string can be customized, so we need a more complex regex, for example: git version 2.38.1.windows.1
244+
// "git" and "version" are optional, and the version number can be x, x.y or x.y.z
245+
r := regexp.MustCompile(`(?:git)?(?: version )?(?P<major>\d+)(?:\.(?P<minor>\d+)(?:\.(?P<patch>\d+))?)?`)
246+
matches := r.FindStringSubmatch(version)
247+
var v GitVersion
248+
var err error
249+
if len(matches) > r.SubexpIndex("major") {
250+
v.Major, err = strconv.Atoi(matches[r.SubexpIndex("major")])
251+
if err != nil {
252+
v.Major = 0
253+
return v
254+
}
255+
}
256+
if len(matches) > r.SubexpIndex("minor") {
257+
v.Minor, err = strconv.Atoi(matches[r.SubexpIndex("minor")])
258+
if err != nil {
259+
v.Minor = 0
260+
return v
261+
}
262+
}
263+
if len(matches) > r.SubexpIndex("patch") {
264+
v.Patch, err = strconv.Atoi(matches[r.SubexpIndex("patch")])
265+
if err != nil {
266+
v.Patch = 0
267+
}
268+
}
269+
return v
270+
}
271+
272+
func (v GitVersion) Less(rhs GitVersion) bool {
273+
return v.Major < rhs.Major ||
274+
(v.Major == rhs.Major && v.Minor < rhs.Minor) ||
275+
(v.Major == rhs.Major && v.Minor == rhs.Minor && v.Patch < rhs.Patch)
276+
}
277+
278+
func deleteEmptyStrings(s []string) []string {
279+
var r []string
280+
for _, str := range s {
281+
if str != "" {
282+
r = append(r, str)
283+
}
284+
}
285+
return r
286+
}

0 commit comments

Comments
 (0)