github.com/benchkram/bob@v0.0.0-20240314204020-b7a57f2f9be9/bobgit/status/status.go (about) 1 package status 2 3 import ( 4 "bytes" 5 "fmt" 6 "path/filepath" 7 "sort" 8 "strings" 9 10 git "github.com/go-git/go-git/v5" 11 "github.com/logrusorgru/aurora" 12 ) 13 14 type MultiRepoStatus map[string]git.Status 15 16 type S struct { 17 Staging MultiRepoStatus 18 Unstaged MultiRepoStatus 19 Untracked MultiRepoStatus 20 Conflicts MultiRepoStatus 21 22 Repos []string 23 } 24 25 func New() *S { 26 s := &S{ 27 Staging: make(MultiRepoStatus), 28 Unstaged: make(MultiRepoStatus), 29 Untracked: make(MultiRepoStatus), 30 Conflicts: make(MultiRepoStatus), 31 } 32 return s 33 } 34 35 // AddRepo iniitializes the state maps with a new repo 36 func (s *S) AddRepo(repoName string) { 37 s.Staging[repoName] = make(git.Status) 38 s.Unstaged[repoName] = make(git.Status) 39 s.Untracked[repoName] = make(git.Status) 40 s.Conflicts[repoName] = make(git.Status) 41 } 42 43 func (s *S) String() string { 44 const spacing = " " 45 46 buf := bytes.NewBuffer(nil) 47 48 { 49 b := bytes.NewBuffer(nil) 50 keys := sortedKeys(s.Conflicts) 51 var conflictingRepos []string 52 for _, repoName := range keys { 53 repoStatus := s.Conflicts[repoName] 54 repoStatusKeys := sortedKeysStatus(repoStatus) 55 for _, path := range repoStatusKeys { 56 fmt.Fprint(b, spacing) 57 fprintChanges(b, repoName, path, repoStatus[path], aurora.Red) 58 conflictingRepos = append(conflictingRepos, repoName) 59 } 60 } 61 62 if len(conflictingRepos) > 0 { 63 fmt.Fprintln(buf, "On at least one repository") 64 fmt.Fprintln(buf, "You have unmerged paths.") 65 fmt.Fprintln(buf, " (fix conflicts using plain git)") 66 fmt.Fprintln(buf, "\nUnmerged paths:") 67 fmt.Fprint(buf, b) 68 } 69 } 70 71 { 72 fmt.Fprintln(buf, "Changes to be committed:") 73 74 // keys := sortedKeys(s.Staging) 75 // for _, repoName := range keys { 76 // repoStatus := s.Staging[repoName] 77 // for path, status := range repoStatus { 78 // fmt.Fprint(buf, spacing) 79 // //fmt.Fprintf(buf, "s%cw%c", status.Staging, status.Worktree) // debugging 80 // //fmt.Fprintf(buf, " (%s) ", aurora.Green(repoName)) 81 // fprintChanges(buf, repoName, path, status, aurora.Green) 82 // } 83 // } 84 FPrintMultirepoStatus(buf, spacing, s.Staging, aurora.Green) 85 fmt.Fprintln(buf) 86 } 87 88 { 89 fmt.Fprintln(buf, "Changes not staged for commit:") 90 91 // keys := sortedKeys(s.Unstaged) 92 // for _, repoName := range keys { 93 // repoStatus := s.Unstaged[repoName] 94 // for path, status := range repoStatus { 95 // fmt.Fprint(buf, spacing) 96 // //fmt.Fprintf(buf, "s%cw%c", status.Staging, status.Worktree) // debugging 97 // //fmt.Fprintf(buf, " (%s) ", aurora.Red(repoName)) 98 // fprintChanges(buf, repoName, path, status, aurora.Red) 99 // } 100 // } 101 FPrintMultirepoStatus(buf, spacing, s.Unstaged, aurora.Red) 102 fmt.Fprintln(buf) 103 } 104 105 { 106 fmt.Fprintln(buf, "Untracked files:") 107 108 keys := sortedKeys(s.Untracked) 109 for _, repoName := range keys { 110 repoStatus := s.Untracked[repoName] 111 repoStatusKeys := sortedKeysStatus(repoStatus) 112 113 for _, path := range repoStatusKeys { 114 fmt.Fprint(buf, spacing) 115 fprintChanges(buf, repoName, path, &git.FileStatus{}, aurora.Red) 116 } 117 } 118 } 119 120 return buf.String() 121 } 122 123 func FPrintMultirepoStatus(buf *bytes.Buffer, spacing string, repos MultiRepoStatus, color func(interface{}) aurora.Value) { 124 keys := sortedKeys(repos) 125 for _, repoName := range keys { 126 repoStatus := repos[repoName] 127 repoStatusKeys := sortedKeysStatus(repoStatus) 128 for _, path := range repoStatusKeys { 129 fmt.Fprint(buf, spacing) 130 fprintChanges(buf, repoName, path, repoStatus[path], color) 131 } 132 } 133 } 134 135 // fprintChanges helper to color & highlight the output based on status 136 func fprintChanges( 137 buf *bytes.Buffer, 138 repoPath, localPath string, 139 status *git.FileStatus, 140 color func(interface{}) aurora.Value, 141 ) { 142 if repoPath == "." { 143 repoPath = "" 144 } 145 146 dir, basename := splitDirAndBasename(repoPath) 147 // fmt.Printf("prefix: [%s] path: [%s]\n", dir, basename) 148 149 // fmt.Println(string(status.Staging) + " " + string(status.Worktree)) 150 151 // check for the conflicts first 152 if (status.Staging == git.UpdatedButUnmerged || status.Worktree == git.UpdatedButUnmerged) || 153 (status.Staging == git.Deleted && status.Worktree == git.Deleted) || 154 (status.Staging == git.Added && status.Worktree == git.Added) { 155 conflictText := getConflictText(status) 156 fmt.Fprint(buf, color(conflictText+dir+aurora.Bold(withSlash(basename)).String()).String()) 157 fmt.Fprintln(buf, color(localPath).String()) 158 } else if status.Staging == git.Renamed { 159 //fmt.Fprint(buf, color("renamed: "+aurora.Bold(withSlash(repoName)).String()).String()) 160 fmt.Fprint(buf, color("renamed: ")) 161 fmt.Fprint(buf, color(dir).String()) 162 fmt.Fprint(buf, color(aurora.Bold(withSlash(basename))).String()) 163 fmt.Fprint(buf, color(status.Extra).String()) 164 fmt.Fprint(buf, color(" -> ").String()) 165 fmt.Fprint(buf, color(dir).String()) 166 fmt.Fprint(buf, color(aurora.Bold(withSlash(basename))).String()) 167 fmt.Fprintln(buf, color(localPath).String()) 168 } else if status.Staging == git.Modified || status.Worktree == git.Modified { 169 fmt.Fprint(buf, color("modified: "+dir+aurora.Bold(withSlash(basename)).String()).String()) 170 fmt.Fprintln(buf, color(localPath).String()) 171 } else if status.Staging == git.Added || status.Worktree == git.Added { 172 fmt.Fprint(buf, color("new file: "+dir+aurora.Bold(withSlash(basename)).String()).String()) 173 fmt.Fprintln(buf, color(localPath).String()) 174 } else if status.Staging == git.Deleted || status.Worktree == git.Deleted { 175 fmt.Fprint(buf, color("deleted: "+dir+aurora.Bold(withSlash(basename)).String()).String()) 176 fmt.Fprintln(buf, color(localPath).String()) 177 } else { 178 fmt.Fprint(buf, color(dir).String()) 179 fmt.Fprint(buf, color(aurora.Bold(withSlash(basename)).String()).String()) 180 fmt.Fprintln(buf, color(localPath).String()) 181 } 182 } 183 184 // sortedKeys returns the keys of `MultiRepoStatus` sorted by `sort.Strings` 185 func sortedKeys(m MultiRepoStatus) (keys []string) { 186 keys = make([]string, 0, len(m)) 187 for key := range m { 188 keys = append(keys, key) 189 } 190 sort.Strings(keys) 191 return keys 192 } 193 194 func sortedKeysStatus(m git.Status) (keys []string) { 195 keys = make([]string, 0, len(m)) 196 for key := range m { 197 keys = append(keys, key) 198 } 199 sort.Strings(keys) 200 return keys 201 } 202 203 func withSlash(s string) string { 204 if strings.HasSuffix(s, "/") || s == "" { 205 return s 206 } 207 return s + "/" 208 } 209 210 // split repoPath in dir and basename 211 // with some additional sanitising. 212 func splitDirAndBasename(path string) (prefix, name string) { 213 if path != "" { 214 prefix = filepath.Dir(path) + "/" 215 if prefix == "./" { 216 prefix = "" 217 } 218 219 name = filepath.Base(path) 220 if name == ".." { 221 name = "" 222 } 223 } 224 return prefix, name 225 } 226 227 // return the merge conflict text for the file 228 // depending on the conflicting status 229 // on merge, delete, etc 230 func getConflictText(status *git.FileStatus) string { 231 conflictText := "both modified:\t" 232 233 if status.Worktree == git.UpdatedButUnmerged && status.Staging == git.Deleted { 234 conflictText = "deleted by us:\t" 235 } 236 if status.Staging == git.UpdatedButUnmerged && status.Worktree == git.Deleted { 237 conflictText = "deleted by them:\t" 238 } 239 if status.Staging == git.Deleted && status.Worktree == git.Deleted { 240 conflictText = "both deleted:\t" 241 } 242 if status.Worktree == git.UpdatedButUnmerged && status.Staging == git.Added { 243 conflictText = "added by us:\t" 244 } 245 if status.Staging == git.UpdatedButUnmerged && status.Worktree == git.Added { 246 conflictText = "added by them:\t" 247 } 248 if status.Staging == git.Added && status.Worktree == git.Added { 249 conflictText = "both added:\t" 250 } 251 252 return conflictText 253 }