Skip to content

Commit cbb78db

Browse files
Add initial implementation of git-sync
1 parent 24bdc27 commit cbb78db

File tree

5 files changed

+343
-46
lines changed

5 files changed

+343
-46
lines changed

LICENSE

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
MIT License
22

33
Copyright (c) 2021 Jacob Gillespie <jacobwgillespie@gmail.com>
4+
Copyright (c) 2009 Chris Wanstrath
45

56
Permission is hereby granted, free of charge, to any person obtaining a copy
67
of this software and associated documentation files (the "Software"), to deal

git/git.go

+239
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
package git
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
"regexp"
9+
"strings"
10+
)
11+
12+
var (
13+
originNamesInLookupOrder = []string{"upstream", "github", "origin"}
14+
remotesRegexp = regexp.MustCompile(`(.+)\s+(.+)\s+\((push|fetch)\)`)
15+
)
16+
17+
func BranchShortName(ref string) string {
18+
reg := regexp.MustCompile("^refs/(remotes/)?.+?/")
19+
return reg.ReplaceAllString(ref, "")
20+
}
21+
22+
func ConfigAll(name string) ([]string, error) {
23+
mode := "--get-all"
24+
if strings.Contains(name, "*") {
25+
mode = "--get-regexp"
26+
}
27+
28+
output, err := execGit("config", mode, name)
29+
if err != nil {
30+
return nil, fmt.Errorf("unknown config %s", name)
31+
}
32+
return splitLines(output), nil
33+
}
34+
35+
func CurrentBranch() (string, error) {
36+
head, err := Head()
37+
if err != nil {
38+
return "", fmt.Errorf("aborted: not currently on any branch")
39+
}
40+
return head, nil
41+
}
42+
43+
var cachedDir string
44+
45+
func Dir() (string, error) {
46+
if cachedDir != "" {
47+
return cachedDir, nil
48+
}
49+
50+
output, err := execGitQuiet("rev-parse", "-q", "--git-dir")
51+
if err != nil {
52+
return "", fmt.Errorf("not a git repository (or any of the parent directories): .git")
53+
}
54+
55+
var chdir string
56+
// for i, flag := range GlobalFlags {
57+
// if flag == "-C" {
58+
// dir := GlobalFlags[i+1]
59+
// if filepath.IsAbs(dir) {
60+
// chdir = dir
61+
// } else {
62+
// chdir = filepath.Join(chdir, dir)
63+
// }
64+
// }
65+
// }
66+
67+
gitDir := firstLine(output)
68+
69+
if !filepath.IsAbs(gitDir) {
70+
if chdir != "" {
71+
gitDir = filepath.Join(chdir, gitDir)
72+
}
73+
74+
gitDir, err = filepath.Abs(gitDir)
75+
if err != nil {
76+
return "", err
77+
}
78+
79+
gitDir = filepath.Clean(gitDir)
80+
}
81+
82+
cachedDir = gitDir
83+
return gitDir, nil
84+
}
85+
86+
func DefaultBranch(remote string) string {
87+
if name, err := SymbolicRef(fmt.Sprintf("refs/remotes/%s/HEAD", remote)); err != nil {
88+
return name
89+
}
90+
return "refs/heads/main"
91+
}
92+
93+
func HasFile(segments ...string) bool {
94+
// For Git >= 2.5.0
95+
if output, err := execGitQuiet("rev-parse", "-q", "--git-path", filepath.Join(segments...)); err == nil {
96+
if lines := splitLines(output); len(lines) == 1 {
97+
if _, err := os.Stat(lines[0]); err == nil {
98+
return true
99+
}
100+
}
101+
}
102+
103+
return false
104+
}
105+
106+
func Head() (string, error) {
107+
return SymbolicRef("HEAD")
108+
}
109+
110+
func LocalBranches() ([]string, error) {
111+
output, err := execGit("branch", "--list")
112+
if err != nil {
113+
return nil, err
114+
}
115+
branches := []string{}
116+
for _, branch := range splitLines(output) {
117+
branches = append(branches, branch[2:])
118+
}
119+
return branches, nil
120+
}
121+
122+
func MainRemote() (string, error) {
123+
remotes, err := Remotes()
124+
if err != nil || len(remotes) == 0 {
125+
return "", fmt.Errorf("aborted: no git remotes found")
126+
}
127+
return remotes[0], nil
128+
}
129+
130+
func NewRange(a, b string) (*Range, error) {
131+
output, err := execGitQuiet("rev-parse", "-q", a, b)
132+
if err != nil {
133+
return nil, err
134+
}
135+
lines := splitLines(output)
136+
if len(lines) != 2 {
137+
return nil, fmt.Errorf("can't parse range %s..%s", a, b)
138+
}
139+
return &Range{lines[0], lines[1]}, nil
140+
}
141+
142+
func Remotes() ([]string, error) {
143+
output, err := execGit("remote", "-v")
144+
if err != nil {
145+
return nil, fmt.Errorf("aborted: can't load git remotes")
146+
}
147+
148+
remoteLines := splitLines(output)
149+
150+
remotesMap := make(map[string]map[string]string)
151+
for _, r := range remoteLines {
152+
if remotesRegexp.MatchString(r) {
153+
match := remotesRegexp.FindStringSubmatch(r)
154+
name := strings.TrimSpace(match[1])
155+
url := strings.TrimSpace(match[2])
156+
urlType := strings.TrimSpace(match[3])
157+
utm, ok := remotesMap[name]
158+
if !ok {
159+
utm = make(map[string]string)
160+
remotesMap[name] = utm
161+
}
162+
utm[urlType] = url
163+
}
164+
}
165+
166+
remotes := []string{}
167+
168+
for _, name := range originNamesInLookupOrder {
169+
if _, ok := remotesMap[name]; ok {
170+
remotes = append(remotes, name)
171+
delete(remotesMap, name)
172+
}
173+
}
174+
175+
for name := range remotesMap {
176+
remotes = append(remotes, name)
177+
}
178+
179+
return remotes, nil
180+
}
181+
182+
func Spawn(args ...string) error {
183+
cmd := exec.Command("git", args...)
184+
cmd.Stdin = os.Stdin
185+
cmd.Stdout = os.Stdout
186+
cmd.Stderr = os.Stderr
187+
return cmd.Run()
188+
}
189+
190+
func Quiet(args ...string) bool {
191+
fmt.Printf("%v\n", args)
192+
cmd := exec.Command("git", args...)
193+
cmd.Stderr = os.Stderr
194+
return cmd.Run() == nil
195+
}
196+
197+
func SymbolicFullName(name string) (string, error) {
198+
output, err := execGitQuiet("rev-parse", "--symbolic-full-name", name)
199+
if err != nil {
200+
return "", fmt.Errorf("unknown revision or path not in the working tree: %s", name)
201+
}
202+
return firstLine(output), nil
203+
}
204+
205+
func SymbolicRef(ref string) (string, error) {
206+
output, err := execGit("symbolic-ref", ref)
207+
if err != nil {
208+
return "", err
209+
}
210+
return firstLine(output), err
211+
}
212+
213+
func execGit(args ...string) (string, error) {
214+
cmd := exec.Command("git", args...)
215+
cmd.Stderr = os.Stderr
216+
output, err := cmd.Output()
217+
return string(output), err
218+
}
219+
220+
func execGitQuiet(args ...string) (string, error) {
221+
cmd := exec.Command("git", args...)
222+
output, err := cmd.Output()
223+
return string(output), err
224+
}
225+
226+
func splitLines(output string) []string {
227+
output = strings.TrimSuffix(output, "\n")
228+
if output == "" {
229+
return []string{}
230+
}
231+
return strings.Split(output, "\n")
232+
}
233+
234+
func firstLine(output string) string {
235+
if i := strings.Index(output, "\n"); i >= 0 {
236+
return output[0:i]
237+
}
238+
return output
239+
}

git/range.go

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package git
2+
3+
import "strings"
4+
5+
type Range struct {
6+
A string
7+
B string
8+
}
9+
10+
func (r *Range) IsIdentical() bool {
11+
return strings.EqualFold(r.A, r.B)
12+
}
13+
14+
func (r *Range) IsAncestor() bool {
15+
return Quiet("merge-base", "--is-ancestor", r.A, r.B)
16+
}

main.go

+87-12
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,101 @@ package main
33
import (
44
"fmt"
55
"os"
6+
"regexp"
7+
"strings"
68

7-
"github.com/go-git/go-git/v5"
9+
"github.com/jacobwgillespie/git-sync/git"
10+
)
11+
12+
var (
13+
green = "\033[32m"
14+
lightGreen = "\033[32;1m"
15+
red = "\033[31m"
16+
lightRed = "\033[31;1m"
17+
resetColor = "\033[0m"
818
)
919

1020
func main() {
11-
path, err := os.Getwd()
12-
if err != nil {
13-
panic(err)
21+
remote, err := git.MainRemote()
22+
check(err)
23+
24+
defaultBranch := git.BranchShortName(git.DefaultBranch(remote))
25+
fullDefaultBranch := fmt.Sprintf("refs/remotes/%s/%s", remote, defaultBranch)
26+
currentBranch := ""
27+
if current, err := git.CurrentBranch(); err == nil {
28+
currentBranch = git.BranchShortName(current)
1429
}
1530

16-
repo, err := git.PlainOpen(path)
17-
if err != nil {
18-
panic(err)
31+
err = git.Spawn("fetch", "--prune", "--quiet", "--progress", remote)
32+
check(err)
33+
34+
branchToRemote := map[string]string{}
35+
if lines, err := git.ConfigAll("branch.*.remote"); err == nil {
36+
configRe := regexp.MustCompile(`^branch\.(.+?)\.remote (.+)`)
37+
38+
for _, line := range lines {
39+
if matches := configRe.FindStringSubmatch(line); len(matches) > 0 {
40+
branchToRemote[matches[1]] = matches[2]
41+
}
42+
}
1943
}
2044

21-
branches, err := localBranches()
22-
if err != nil {
23-
panic(err)
45+
branches, err := git.LocalBranches()
46+
check(err)
47+
48+
for _, branch := range branches {
49+
fullBranch := fmt.Sprintf("refs/heads/%s", branch)
50+
remoteBranch := fmt.Sprintf("refs/remotes/%s/%s", remote, branch)
51+
gone := false
52+
53+
if branchToRemote[branch] == remote {
54+
if upstream, err := git.SymbolicFullName(fmt.Sprintf("%s@{upstream}", branch)); err == nil {
55+
remoteBranch = upstream
56+
} else {
57+
remoteBranch = ""
58+
gone = true
59+
}
60+
} else if !git.HasFile(strings.Split(remoteBranch, "/")...) {
61+
remoteBranch = ""
62+
}
63+
64+
if remoteBranch != "" {
65+
diff, err := git.NewRange(fullBranch, remoteBranch)
66+
check(err)
67+
68+
if diff.IsIdentical() {
69+
continue
70+
} else if diff.IsAncestor() {
71+
if branch == currentBranch {
72+
git.Quiet("merge", "--ff-only", "--quiet", remoteBranch)
73+
} else {
74+
git.Quiet("update-ref", fullBranch, remoteBranch)
75+
}
76+
fmt.Printf("%sUpdated branch %s%s%s (was %s).\n", green, lightGreen, branch, resetColor, diff.A[0:7])
77+
} else {
78+
fmt.Fprintf(os.Stderr, "warning: '%s' seems to contain unpushed commits\n", branch)
79+
}
80+
} else if gone {
81+
diff, err := git.NewRange(fullBranch, fullDefaultBranch)
82+
check(err)
83+
84+
if diff.IsAncestor() {
85+
if branch == currentBranch {
86+
git.Quiet("checkout", "--quiet", defaultBranch)
87+
currentBranch = defaultBranch
88+
}
89+
git.Quiet("branch", "-D", branch)
90+
fmt.Printf("%sDeleted branch %s%s%s (was %s).\n", red, lightRed, branch, resetColor, diff.A[0:7])
91+
} else {
92+
fmt.Fprintf(os.Stderr, "warning: '%s' was deleted on %s, but appears not merged into '%s'\n", branch, remote, defaultBranch)
93+
}
94+
}
2495
}
96+
}
2597

26-
fmt.Printf("%v\n", repo)
27-
fmt.Printf("%v\n", branches)
98+
func check(err error) {
99+
if err != nil {
100+
fmt.Fprintf(os.Stderr, "%s\n", err)
101+
os.Exit(1)
102+
}
28103
}

0 commit comments

Comments
 (0)