+20
-3
Makefile
+20
-3
Makefile
···
2
2
3
3
PREFIX ?= /usr/local
4
4
BINDIR = $(PREFIX)/bin
5
+
BASHDIR = $(PREFIX)/share/bash-completions/completions
6
+
FISHDIR = $(PREFIX)/share/fish/vendor_completions.d
7
+
ZSHDIR = $(PREFIX)/share/zsh/vendor_completions
5
8
6
9
.PHONY: all build install clean
7
10
8
-
all: build
11
+
all: build completions
9
12
10
13
build:
11
14
go build -o $(BINARY_NAME) ./cmd/knit
12
15
16
+
completions: build
17
+
./$(BINARY_NAME) completion bash > knit.bash
18
+
./$(BINARY_NAME) completion fish > knit.fish
19
+
./$(BINARY_NAME) completion zsh > knit.zsh
20
+
13
21
install: build
14
-
mkdir -p $(BINDIR)
15
-
cp $(BINARY_NAME) $(BINDIR)/$(BINARY_NAME)
22
+
install -d $(BINDIR)
23
+
install -m 755 $(BINARY_NAME) $(BINDIR)/$(BINARY_NAME)
24
+
install -d $(BASHDIR)
25
+
install -m 755 knit.bash $(BASHDIR)/knit
26
+
install -d $(FISHDIR)
27
+
install -m 755 knit.fish $(FISHDIR)/knit.fish
28
+
install -d $(ZSHDIR)
29
+
install -m 755 knit.zsh $(ZSHDIR)/_knit
16
30
17
31
clean:
18
32
rm -f $(BINARY_NAME)
33
+
rm -f knit.bash
34
+
rm -f knit.fish
35
+
rm -f knit.zsh
+3
-1
README.md
+3
-1
README.md
+1
cmd/knit/main.go
+1
cmd/knit/main.go
+36
git/git.go
+36
git/git.go
···
4
4
"bufio"
5
5
"fmt"
6
6
"net/url"
7
+
"os"
7
8
"os/exec"
9
+
"path"
10
+
"sort"
8
11
"strings"
9
12
)
10
13
···
96
99
}
97
100
98
101
return string(output), nil
102
+
}
103
+
104
+
func 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
99
135
}
100
136
101
137
func RemoteBranches(remote string) ([]string, error) {
+23
-1
knit.go
+23
-1
knit.go
···
1
1
package knit
2
2
3
-
import "errors"
3
+
import (
4
+
"errors"
5
+
"os"
6
+
"os/exec"
7
+
)
4
8
5
9
// DefaultHost is the default host for tangled.sh services
6
10
const DefaultHost = "tangled.sh"
···
11
15
// ErrRequiresAuth is returned when a command requires authentication but the
12
16
// user has not authenticated with the host
13
17
var ErrRequiresAuth = errors.New("authentication required")
18
+
19
+
func Editor() string {
20
+
e := os.Getenv("EDITOR")
21
+
if e != "" {
22
+
return e
23
+
}
24
+
25
+
options := []string{"nvim", "vim", "nano"}
26
+
for _, opt := range options {
27
+
_, err := exec.LookPath(opt)
28
+
if err != nil {
29
+
continue
30
+
}
31
+
return opt
32
+
}
33
+
34
+
return "vi"
35
+
}
+38
-6
pr/create.go
+38
-6
pr/create.go
···
6
6
"fmt"
7
7
"net/http"
8
8
"net/url"
9
+
"os"
10
+
"os/exec"
9
11
"path"
10
12
"strings"
11
13
"sync"
···
36
38
}
37
39
_ = client
38
40
41
+
paths, err := git.FormatPatchToTmp(args[0])
42
+
if err != nil {
43
+
return err
44
+
}
45
+
46
+
editor := exec.Command(knit.Editor(), paths...)
47
+
editor.Stdout = os.Stdout
48
+
editor.Stderr = os.Stderr
49
+
editor.Stdin = os.Stdin
50
+
if err := editor.Run(); err != nil {
51
+
return err
52
+
}
53
+
54
+
// Combine the result into a single slice
55
+
var patch []byte
56
+
for _, p := range paths {
57
+
b, err := os.ReadFile(p)
58
+
if err != nil {
59
+
return err
60
+
}
61
+
patch = append(patch, b...)
62
+
}
63
+
39
64
// Get available remotes
40
65
remotes, err := git.Remotes()
41
66
if err != nil {
···
58
83
Value(&remote).
59
84
Options(options...).
60
85
Run()
61
-
}
62
-
63
-
patch, err := git.FormatPatch(args[0])
64
-
if err != nil {
65
-
return err
66
86
}
67
87
68
88
branches, err := git.RemoteBranches(remote.Name)
···
102
122
return err
103
123
}
104
124
125
+
var submit bool
126
+
if err := huh.NewConfirm().
127
+
Title(fmt.Sprintf("Submit PR to %s/%s?", remote.Host, remote.Path)).
128
+
Value(&submit).
129
+
Run(); err != nil {
130
+
return err
131
+
}
132
+
133
+
if !submit {
134
+
return fmt.Errorf("PR submission canceled")
135
+
}
136
+
105
137
form := url.Values{}
106
138
form.Add("title", title)
107
139
form.Add("body", description)
108
140
form.Add("targetBranch", targetBranch)
109
-
form.Add("patch", patch)
141
+
form.Add("patch", string(patch))
110
142
111
143
p := remote.Path
112
144
if !strings.HasPrefix(p, "@") {