github.com/driusan/dgit@v0.0.0-20221118233547-f39f0c15edbb/git/status.go (about) 1 package git 2 3 import ( 4 "fmt" 5 "os" 6 ) 7 8 type StatusUntrackedMode uint8 9 10 const ( 11 StatusUntrackedNo = StatusUntrackedMode(iota) 12 StatusUntrackedNormal 13 StatusUntrackedAll 14 ) 15 16 type StatusIgnoreSubmodules uint8 17 18 const ( 19 StatusIgnoreSubmodulesNone = StatusIgnoreSubmodules(iota) 20 StatusIgnoreSubmodulesUntracked 21 StatisIgnoreSubmodulesDirty 22 StatusIgnoreSubmodulesAll 23 ) 24 25 type StatusColumnOptions string 26 27 type StatusOptions struct { 28 Short bool 29 Branch bool 30 ShowStash bool 31 Porcelain uint8 32 Long bool 33 Verbose bool 34 Ignored bool 35 36 NullTerminate bool 37 38 UntrackedMode StatusUntrackedMode 39 40 IgnoreSubmodules StatusIgnoreSubmodules 41 Column StatusColumnOptions 42 } 43 44 // Helper to run update-index --refresh 45 func refreshIndex(c *Client) error { 46 idx, err := c.GitDir.ReadIndex() 47 if err != nil { 48 return err 49 } 50 nidx, err := UpdateIndex(c, idx, UpdateIndexOptions{Refresh: true}, nil) 51 if err != nil { 52 return err 53 } 54 f, err := c.GitDir.Create("index") 55 if err != nil { 56 return err 57 } 58 if err := nidx.WriteIndex(f); err != nil { 59 return err 60 } 61 return nil 62 } 63 func Status(c *Client, opts StatusOptions, files []File) (string, error) { 64 // This doesn't feel right, but seems to be required to match the behaviour 65 // of git. Start status by refreshing the stat info on disk to limit spurious 66 // empty diffs 67 if err := refreshIndex(c); err != nil { 68 return "", err 69 } 70 if opts.Porcelain > 1 || opts.ShowStash || opts.Verbose || opts.Ignored || (opts.Column != "default" && opts.Column != "") { 71 return "", fmt.Errorf("Unsupported option for Status") 72 } 73 if opts.Column == "" { 74 opts.Column = "column" 75 } 76 var ret string 77 if opts.Branch || opts.Long { 78 branch, err := StatusBranch(c, opts, "") 79 if err != nil { 80 return "", err 81 } 82 if branch != "" { 83 ret += branch + "\n" 84 } 85 } 86 if opts.Short || opts.Porcelain == 1 || opts.NullTerminate { 87 opts.Short = true // If porcelain=1, ensure short=true too.. 88 lineending := "\n" 89 if opts.NullTerminate { 90 lineending = "\000" 91 } 92 status, err := StatusShort(c, files, opts.UntrackedMode, "", lineending) 93 if err != nil { 94 return "", err 95 } 96 ret += status 97 } else if opts.Long { 98 status, err := StatusLong(c, files, opts.UntrackedMode, "") 99 if err != nil { 100 return "", err 101 } 102 ret += status 103 } 104 return ret, nil 105 } 106 107 func StatusBranch(c *Client, opts StatusOptions, lineprefix string) (string, error) { 108 var ret string 109 if opts.Short || opts.Porcelain > 0 { 110 if !opts.Branch { 111 return "", nil 112 } 113 } 114 h, herr := c.GetHeadCommit() 115 116 switch branch, err := SymbolicRefGet(c, SymbolicRefOptions{Short: true}, "HEAD"); err { 117 case nil: 118 if opts.Short { 119 if herr != nil { 120 return "## No commits yet on " + branch.String(), nil 121 } 122 return "## " + branch.String(), nil 123 } 124 ret = fmt.Sprintf("On branch %v", branch) 125 case DetachedHead: 126 if opts.Short { 127 return "## HEAD (no branch)", nil 128 } 129 ret = fmt.Sprintf("HEAD detached at %v", h.String()) 130 default: 131 return "", err 132 } 133 134 if herr != nil { 135 ret += lineprefix + "\n\nNo commits yet\n" 136 } 137 return ret, nil 138 139 } 140 141 // Return a string of the status 142 func StatusLong(c *Client, files []File, untracked StatusUntrackedMode, lineprefix string) (string, error) { 143 // If no head commit: "no changes yet", else branch info 144 // Changes to be committed: dgit diff-index --cached HEAD 145 // Unmerged: git ls-files -u 146 // Changes not staged: dgit diff-files 147 // Untracked: dgit ls-files -o 148 var ret string 149 index, _ := c.GitDir.ReadIndex() 150 hasStaged := false 151 152 var lsfiles []File 153 if len(files) == 0 { 154 lsfiles = []File{File(c.WorkDir)} 155 } else { 156 lsfiles = files 157 } 158 // Start by getting a list of unmerged and keeping them in a map, so 159 // that we can exclude them from the non-"unmerged" 160 unmergedMap := make(map[File]bool) 161 unmerged, err := LsFiles(c, LsFilesOptions{Unmerged: true}, lsfiles) 162 if err != nil { 163 return "", err 164 } 165 for _, f := range unmerged { 166 fname, err := f.PathName.FilePath(c) 167 if err != nil { 168 return "", err 169 } 170 unmergedMap[fname] = true 171 } 172 173 var staged []HashDiff 174 hasCommit := false 175 if head, err := c.GetHeadCommit(); err != nil { 176 // There is no head commit to compare against, so just say 177 // everything in the cache (which isn't unmerged) is new 178 staged, err := LsFiles(c, LsFilesOptions{Cached: true}, lsfiles) 179 if err != nil { 180 return "", err 181 } 182 var stagedMsg string 183 if len(staged) > 0 { 184 hasStaged = true 185 for _, f := range staged { 186 fname, err := f.PathName.FilePath(c) 187 if err != nil { 188 return "", err 189 } 190 191 if _, ok := unmergedMap[fname]; ok { 192 // There's a merge conflict, it'l show up in "Unmerged" 193 continue 194 } 195 stagedMsg += fmt.Sprintf("%v\tnew file:\t%v\n", lineprefix, fname) 196 } 197 } 198 199 if stagedMsg != "" { 200 ret += fmt.Sprintf("%vChanges to be committed:\n", lineprefix) 201 ret += fmt.Sprintf("%v (use \"git rm --cached <file>...\" to unstage)\n", lineprefix) 202 ret += fmt.Sprintf("%v\n", lineprefix) 203 ret += stagedMsg 204 ret += fmt.Sprintf("%v\n", lineprefix) 205 } 206 } else { 207 hasCommit = true 208 staged, err = DiffIndex(c, DiffIndexOptions{Cached: true}, index, head, files) 209 if err != nil { 210 return "", err 211 } 212 } 213 214 // Staged 215 if len(staged) > 0 { 216 hasStaged = true 217 218 stagedMsg := "" 219 for _, f := range staged { 220 fname, err := f.Name.FilePath(c) 221 if err != nil { 222 return "", err 223 } 224 225 if _, ok := unmergedMap[fname]; ok { 226 // There's a merge conflict, it'l show up in "Unmerged" 227 continue 228 } 229 230 if f.Src == (TreeEntry{}) { 231 stagedMsg += fmt.Sprintf("%v\tnew file:\t%v\n", lineprefix, fname) 232 } else if f.Dst == (TreeEntry{}) { 233 stagedMsg += fmt.Sprintf("%v\tdeleted:\t%v\n", lineprefix, fname) 234 } else { 235 stagedMsg += fmt.Sprintf("%v\tmodified:\t%v\n", lineprefix, fname) 236 } 237 } 238 if stagedMsg != "" { 239 ret += fmt.Sprintf("%vChanges to be committed:\n", lineprefix) 240 ret += fmt.Sprintf("%v (use \"git reset HEAD <file>...\" to unstage)\n", lineprefix) 241 ret += fmt.Sprintf("%v\n", lineprefix) 242 ret += stagedMsg 243 ret += fmt.Sprintf("%v\n", lineprefix) 244 } 245 } 246 247 // We already did the LsFiles for the unmerged, so just iterate over 248 // them. 249 if len(unmerged) > 0 { 250 ret += fmt.Sprintf("%vUnmerged paths:\n", lineprefix) 251 ret += fmt.Sprintf("%v (use \"git reset HEAD <file>...\" to unstage)\n", lineprefix) 252 ret += fmt.Sprintf("%v (use \"git add <file>...\" to mark resolution)\n", lineprefix) 253 ret += fmt.Sprintf("%v\n", lineprefix) 254 255 for i, f := range unmerged { 256 fname, err := f.PathName.FilePath(c) 257 if err != nil { 258 return "", err 259 } 260 switch f.Stage() { 261 case Stage1: 262 switch unmerged[i+1].Stage() { 263 case Stage2: 264 if i >= len(unmerged)-2 { 265 // Stage3 is missing, we've reached the end of the index. 266 ret += fmt.Sprintf("%v\tdeleted by them:\t%v\n", lineprefix, fname) 267 continue 268 } 269 switch unmerged[i+2].Stage() { 270 case Stage3: 271 // There's a stage1, stage2, and stage3. If they weren't all different, read-tree would 272 // have resolved it as a trivial stage0 merge. 273 ret += fmt.Sprintf("%v\tboth modified:\t%v\n", lineprefix, fname) 274 default: 275 // Stage3 is missing, but we haven't reached the end of the index. 276 ret += fmt.Sprintf("%v\tdeleted by them:\t%v\n", lineprefix, fname) 277 } 278 continue 279 case Stage3: 280 // Stage2 is missing 281 ret += fmt.Sprintf("%v\tdeleted by us:\t%v\n", lineprefix, fname) 282 continue 283 default: 284 panic("Unhandled index") 285 } 286 case Stage2: 287 if i == 0 || unmerged[i-1].Stage() != Stage1 { 288 // If this is a Stage2, and the previous wasn't Stage1, 289 // then we know the next one must be Stage3 or read-tree 290 // would have handled it as a trivial merge. 291 ret += fmt.Sprintf("%v\tboth added:\t%v\n", lineprefix, fname) 292 } 293 // If the previous was Stage1, it was handled by the previous 294 // loop iteration. 295 continue 296 case Stage3: 297 // There can't be just a Stage3 or read-tree would 298 // have resolved it as Stage0. All cases were handled 299 // by Stage1 or Stage2 300 continue 301 default: 302 // If ls-files -u returned something other than 303 // Stage1-3, there's an unrelated bug somewhere. 304 panic("Invalid unmerged stage") 305 } 306 } 307 ret += fmt.Sprintf("%v\n", lineprefix) 308 } 309 // Not staged changes 310 notstaged, err := DiffFiles(c, DiffFilesOptions{}, lsfiles) 311 if err != nil { 312 return "", err 313 } 314 315 hasUnstaged := false 316 if len(notstaged) > 0 { 317 hasUnstaged = true 318 notStagedMsg := "" 319 for _, f := range notstaged { 320 fname, err := f.Name.FilePath(c) 321 if err != nil { 322 return "", err 323 } 324 325 if _, ok := unmergedMap[fname]; ok { 326 // There's a merge conflict, it'l show up in "Unmerged" 327 continue 328 } 329 330 if f.Src == (TreeEntry{}) { 331 notStagedMsg += fmt.Sprintf("%v\tnew file:\t%v\n", lineprefix, fname) 332 } else if f.Dst == (TreeEntry{}) { 333 notStagedMsg += fmt.Sprintf("%v\tdeleted:\t%v\n", lineprefix, fname) 334 } else { 335 notStagedMsg += fmt.Sprintf("%v\tmodified:\t%v\n", lineprefix, fname) 336 } 337 } 338 if notStagedMsg != "" { 339 ret += fmt.Sprintf("%vChanges not staged for commit:\n", lineprefix) 340 ret += fmt.Sprintf("%v (use \"git add <file>...\" to update what will be committed)\n", lineprefix) 341 ret += fmt.Sprintf("%v (use \"git checkout -- <file>...\" to discard changes in working directory)\n", lineprefix) 342 ret += fmt.Sprintf("%v\n", lineprefix) 343 ret += notStagedMsg 344 ret += fmt.Sprintf("%v\n", lineprefix) 345 } 346 } 347 348 hasUntracked := false 349 if untracked != StatusUntrackedNo { 350 lsfilesopts := LsFilesOptions{ 351 Others: true, 352 ExcludeStandard: true, // Configurable some day 353 } 354 if untracked == StatusUntrackedNormal { 355 lsfilesopts.Directory = true 356 } 357 358 untracked, err := LsFiles(c, lsfilesopts, lsfiles) 359 if len(untracked) > 0 { 360 hasUntracked = true 361 } 362 if err != nil { 363 return "", err 364 } 365 if len(untracked) > 0 { 366 ret += fmt.Sprintf("%vUntracked files:\n", lineprefix) 367 ret += fmt.Sprintf("%v (use \"git add <file>...\" to include in what will be committed)\n", lineprefix) 368 ret += fmt.Sprintf("%v\n", lineprefix) 369 370 for _, f := range untracked { 371 fname, err := f.PathName.FilePath(c) 372 if err != nil { 373 return "", err 374 } 375 if fname.IsDir() { 376 ret += fmt.Sprintf("%v\t%v/\n", lineprefix, fname) 377 } else { 378 ret += fmt.Sprintf("%v\t%v\n", lineprefix, fname) 379 } 380 } 381 ret += fmt.Sprintf("%v\n", lineprefix) 382 } 383 } else { 384 if hasUnstaged { 385 ret += fmt.Sprintf("%vUntracked files not listed (use -u option to show untracked files)\n", lineprefix) 386 } 387 } 388 var summary string 389 switch { 390 case hasStaged && hasUntracked && hasCommit: 391 case hasStaged && hasUntracked && !hasCommit: 392 case hasStaged && !hasUntracked && hasCommit && !hasUnstaged: 393 case hasStaged && !hasUntracked && hasCommit && hasUnstaged: 394 if untracked != StatusUntrackedNo { 395 summary = `no changes added to commit (use "git add" and/or "git commit -a")` 396 } 397 case hasStaged && !hasUntracked && !hasCommit: 398 case !hasStaged && hasUntracked && hasCommit: 399 fallthrough 400 case !hasStaged && hasUntracked && !hasCommit: 401 summary = `nothing added to commit but untracked files present (use "git add" to track)` 402 case !hasStaged && !hasUntracked && hasCommit && !hasUnstaged: 403 summary = "nothing to commit, working tree clean" 404 case !hasStaged && !hasUntracked && hasCommit && hasUnstaged: 405 summary = `no changes added to commit (use "git add" and/or "git commit -a")` 406 case !hasStaged && !hasUntracked && !hasCommit: 407 summary = `nothing to commit (create/copy files and use "git add" to track)` 408 default: 409 } 410 if summary != "" { 411 ret += lineprefix + summary + "\n" 412 } 413 return ret, nil 414 } 415 416 // Implements git status --short 417 func StatusShort(c *Client, files []File, untracked StatusUntrackedMode, lineprefix, lineending string) (string, error) { 418 var lsfiles []File 419 if len(files) == 0 { 420 lsfiles = []File{File(c.WorkDir)} 421 } else { 422 lsfiles = files 423 } 424 425 cfiles, err := LsFiles(c, LsFilesOptions{Cached: true}, lsfiles) 426 if err != nil { 427 return "", err 428 } 429 tree := make(map[IndexPath]*IndexEntry) 430 // It's not an error to use "git status" before the first commit, 431 // so discard the error 432 if head, err := c.GetHeadCommit(); err == nil { 433 i, err := LsTree(c, LsTreeOptions{FullTree: true, Recurse: true}, head, files) 434 if err != nil { 435 return "", err 436 } 437 438 // this should probably be an LsTreeMap library function, it would be 439 // useful other places.. 440 for _, e := range i { 441 tree[e.PathName] = e 442 } 443 } 444 var ret string 445 var wtst, ist rune 446 for i, f := range cfiles { 447 wtst = ' ' 448 ist = ' ' 449 fname, err := f.PathName.FilePath(c) 450 if err != nil { 451 return "", err 452 } 453 switch f.Stage() { 454 case Stage0: 455 if head, ok := tree[f.PathName]; !ok { 456 ist = 'A' 457 } else { 458 if head.Sha1 == f.Sha1 { 459 ist = ' ' 460 } else { 461 ist = 'M' 462 } 463 } 464 465 stat, err := fname.Stat() 466 if os.IsNotExist(err) { 467 wtst = 'D' 468 } else { 469 mtime, err := fname.MTime() 470 if err != nil { 471 return "", err 472 } 473 if mtime != f.Mtime || stat.Size() != int64(f.Fsize) { 474 wtst = 'M' 475 } else { 476 wtst = ' ' 477 } 478 } 479 if ist != ' ' || wtst != ' ' { 480 ret += fmt.Sprintf("%c%c %v%v", ist, wtst, fname, lineending) 481 } 482 case Stage1: 483 switch cfiles[i+1].Stage() { 484 case Stage2: 485 if i >= len(cfiles)-2 { 486 // Stage3 is missing, we've reached the end of the index. 487 ret += fmt.Sprintf("MD %v%v", fname, lineending) 488 continue 489 } 490 switch cfiles[i+2].Stage() { 491 case Stage3: 492 // There's a stage1, stage2, and stage3. If they weren't all different, read-tree would 493 // have resolved it as a trivial stage0 merge. 494 ret += fmt.Sprintf("UU %v%v", fname, lineending) 495 default: 496 // Stage3 is missing, but we haven't reached the end of the index. 497 ret += fmt.Sprintf("MD%v%v", fname, lineending) 498 } 499 continue 500 case Stage3: 501 // Stage2 is missing 502 ret += fmt.Sprintf("DM %v%v", fname, lineending) 503 continue 504 default: 505 panic("Unhandled index") 506 } 507 case Stage2: 508 if i == 0 || cfiles[i-1].Stage() != Stage1 { 509 // If this is a Stage2, and the previous wasn't Stage1, 510 // then we know the next one must be Stage3 or read-tree 511 // would have handled it as a trivial merge. 512 ret += fmt.Sprintf("AA %v%v", fname, lineending) 513 } 514 // If the previous was Stage1, it was handled by the previous 515 // loop iteration. 516 continue 517 case Stage3: 518 // There can't be just a Stage3 or read-tree would 519 // have resolved it as Stage0. All cases were handled 520 // by Stage1 or Stage2 521 continue 522 } 523 } 524 if untracked != StatusUntrackedNo { 525 lsfilesopts := LsFilesOptions{ 526 Others: true, 527 } 528 if untracked == StatusUntrackedNormal { 529 lsfilesopts.Directory = true 530 } 531 532 untracked, err := LsFiles(c, lsfilesopts, lsfiles) 533 if err != nil { 534 return "", err 535 } 536 for _, f := range untracked { 537 fname, err := f.PathName.FilePath(c) 538 if err != nil { 539 return "", err 540 } 541 if name := fname.String(); name == "." { 542 ret += "?? ./" + lineending 543 } else { 544 ret += "?? " + name + lineending 545 } 546 } 547 } 548 return ret, nil 549 550 }