A CLI for tangled.sh
at main 159 lines 3.0 kB view raw
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}