A CLI for tangled.sh
1package git
2
3import (
4 "bufio"
5 "fmt"
6 "net/url"
7 "os"
8 "os/exec"
9 "path"
10 "sort"
11 "strings"
12)
13
14type Remote = struct {
15 Name string
16 Host string
17 Path string
18}
19
20// Remotes gets the configured git remotes for the current working directory
21func Remotes() ([]*Remote, error) {
22 cmd := exec.Command("git", "remote", "--verbose")
23
24 output, err := cmd.Output()
25 if err != nil {
26 return nil, err
27 }
28
29 remotes := []*Remote{}
30
31 scanner := bufio.NewScanner(strings.NewReader(string(output)))
32outer:
33 for scanner.Scan() {
34 line := scanner.Text()
35 fields := strings.Fields(line)
36 if len(fields) != 3 {
37 continue
38 }
39 if fields[2] != "(fetch)" {
40 continue
41 }
42
43 u, err := normalizeGitURL(fields[1])
44 if err != nil {
45 continue
46 }
47 u.Name = fields[0]
48
49 for _, remote := range remotes {
50 if remote.Host == u.Host && remote.Path == u.Path {
51 continue outer
52 }
53 }
54
55 remotes = append(remotes, u)
56 }
57
58 return remotes, nil
59}
60
61func normalizeGitURL(raw string) (*Remote, error) {
62 // Handle SSH-style: git@github.com:user/repo.git
63 if at := strings.Index(raw, "@"); at != -1 {
64 colon := strings.Index(raw, ":")
65 if colon == -1 || colon < at {
66 return nil, fmt.Errorf("invalid SSH Git URL: %s", raw)
67 }
68
69 host := raw[at+1 : colon]
70 path := raw[colon+1:]
71 return &Remote{
72 Host: host,
73 Path: path,
74 }, nil
75 }
76
77 // Handle HTTPS-style
78 if strings.HasPrefix(raw, "http://") || strings.HasPrefix(raw, "https://") {
79 u, err := url.Parse(raw)
80 if err != nil {
81 return nil, fmt.Errorf("invalid URL: %w", err)
82 }
83
84 return &Remote{
85 Host: u.Host,
86 Path: strings.TrimPrefix(u.Path, "/"),
87 }, nil
88 }
89
90 return nil, fmt.Errorf("unsupported Git URL format: %s", raw)
91}
92
93func FormatPatch(revRange string) (string, error) {
94 cmd := exec.Command("git", "format-patch", "--stdout", revRange)
95
96 output, err := cmd.Output()
97 if err != nil {
98 return "", err
99 }
100
101 return string(output), nil
102}
103
104func FormatPatchToTmp(revRange string) ([]string, error) {
105 tmpDir, err := os.MkdirTemp("", "knit-patches-")
106 if err != nil {
107 return nil, err
108 }
109
110 cmd := exec.Command(
111 "git",
112 "format-patch",
113 "--output-directory",
114 tmpDir,
115 revRange,
116 )
117 if err := cmd.Run(); err != nil {
118 return nil, err
119 }
120
121 entries, err := os.ReadDir(tmpDir)
122 if err != nil {
123 return nil, err
124 }
125
126 var paths []string
127 for _, entry := range entries {
128 if entry.IsDir() {
129 continue
130 }
131 paths = append(paths, path.Join(tmpDir, entry.Name()))
132 }
133 sort.Strings(paths)
134 return paths, nil
135}
136
137func RemoteBranches(remote string) ([]string, error) {
138 cmd := exec.Command("git", "branch", "--remotes", "--list", remote+"*")
139
140 output, err := cmd.Output()
141 if err != nil {
142 return nil, err
143 }
144
145 branches := []string{}
146 scanner := bufio.NewScanner(strings.NewReader(string(output)))
147
148 for scanner.Scan() {
149 line := strings.TrimSpace(scanner.Text())
150 branch := strings.TrimPrefix(line, remote+"/")
151 if strings.HasPrefix(branch, "HEAD ->") {
152 continue
153 }
154
155 branches = append(branches, branch)
156
157 }
158 return branches, nil
159}