A CLI for tangled.sh

Compare changes

Choose any two refs to compare.

Changed files
+124 -11
cmd
knit
git
pr
+3
.gitignore
··· 1 1 knit 2 + knit.bash 3 + knit.fish 4 + knit.zsh
+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
··· 6 6 7 7 Using go: 8 8 9 - `go install tangled.sh/rockorager.dev/knit` 9 + ``` 10 + go install tangled.sh/rockorager.dev/knit/cmd/knit@latest 11 + ``` 10 12 11 13 Otherwise, 12 14
+1
cmd/knit/main.go
··· 17 17 Use: "knit", 18 18 Short: "knit is a CLI for tangled.sh", 19 19 } 20 + root.CompletionOptions.HiddenDefaultCmd = true 20 21 21 22 root.AddCommand(auth.Command()) 22 23 root.AddCommand(pr.Command())
+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
··· 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
··· 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, "@") {