github.com/driusan/dgit@v0.0.0-20221118233547-f39f0c15edbb/git/revparse.go (about) 1 package git 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "os" 7 "path/filepath" 8 "strings" 9 "time" 10 ) 11 12 type Pattern string 13 14 type ParsedRevision struct { 15 Id Sha1 16 Excluded bool 17 } 18 19 func (pr ParsedRevision) CommitID(c *Client) (CommitID, error) { 20 if pr.Id.Type(c) != "commit" { 21 return CommitID{}, fmt.Errorf("Invalid revision commit: %v", pr.Id) 22 } 23 return CommitID(pr.Id), nil 24 } 25 26 func (pr ParsedRevision) TreeID(c *Client) (TreeID, error) { 27 if pr.Id.Type(c) != "commit" { 28 return TreeID{}, fmt.Errorf("Invalid revision commit") 29 } 30 return CommitID(pr.Id).TreeID(c) 31 } 32 33 func (pr ParsedRevision) IsAncestor(c *Client, parent Commitish) bool { 34 if pr.Id.Type(c) != "commit" { 35 return false 36 } 37 com, err := pr.CommitID(c) 38 if err != nil { 39 return false 40 } 41 return com.IsAncestor(c, parent) 42 } 43 44 func (pr ParsedRevision) Ancestors(c *Client) ([]CommitID, error) { 45 comm, err := pr.CommitID(c) 46 if err != nil { 47 return nil, err 48 } 49 return comm.Ancestors(c) 50 } 51 52 // Options that may be passed to RevParse on the command line. 53 // BUG(driusan): None of the RevParse options are implemented 54 type RevParseOptions struct { 55 // Operation modes 56 ParseOpt, SQQuote bool 57 58 // Options for --parseopt 59 KeepDashDash bool 60 StopAtNonOption bool 61 StuckLong bool 62 63 // Options for Filtering 64 RevsOnly bool 65 NoRevs bool 66 Flags, NoFlags bool 67 68 // Options for output. These should probably not be here but be handled 69 // in the cmd package instead. 70 Default string 71 Prefix string 72 Verify bool 73 Quiet bool 74 SQ bool 75 Not bool 76 AbbrefRev string //strict|loose 77 Short uint // The number of characters to abbreviate to. Default should be "4" 78 Symbolic, SymbolicFullName bool 79 80 // Options for Objects 81 All bool 82 Branches, Tags, Remotes Pattern 83 Glob Pattern 84 Exclude Pattern 85 Disambiguate string // Prefix 86 87 // Options for Files 88 // BUG(driusan): These should be handled as part of "args", not in RevParseOptions. 89 // They're included here so that I don't forget about them. 90 GitCommonDir GitDir 91 ResolveGitDir File // path 92 GitPath GitDir 93 ShowCDup bool 94 SharedIndexPath bool 95 96 // Other options 97 After, Before time.Time 98 } 99 100 // RevParsePath parses a path spec such as `HEAD:README.md` into the value that 101 // it represents. The Sha1 returned may be either a tree or a blob, depending on 102 // the pathspec. 103 func RevParsePath(c *Client, opt *RevParseOptions, arg string) (Sha1, error) { 104 var tree Treeish 105 var err error 106 var treepart string 107 pathcomponent := strings.Index(arg, ":") 108 if pathcomponent < 0 { 109 treepart = arg 110 } else if pathcomponent == 0 { 111 // Nothing specified for rev refers to the 112 // index, not the head. 113 files, err := LsFiles(c, LsFilesOptions{Cached: true}, []File{File(arg[1:])}) 114 if len(files) != 1 || err != nil { 115 return Sha1{}, fmt.Errorf("%v not found", arg) 116 } 117 118 for _, entry := range files { 119 if entry.IndexEntry.PathName == IndexPath(arg[1:]) { 120 return entry.Sha1, nil 121 } 122 return Sha1{}, fmt.Errorf("%v not found", arg) 123 } 124 } else { 125 treepart = arg[0:pathcomponent] 126 } 127 if len(arg) == 40 { 128 comm, err := Sha1FromString(arg) 129 if err != nil { 130 goto notsha1 131 } 132 switch comm.Type(c) { 133 case "blob": 134 if pathcomponent >= 0 { 135 // There was a path part, but there's no way for a path 136 // to be in a blob. 137 return Sha1{}, fmt.Errorf("Could not parse %v", arg) 138 } 139 return comm, nil 140 case "tree": 141 tree = TreeID(comm) 142 goto extractpath 143 case "commit": 144 tree = CommitID(comm) 145 goto extractpath 146 default: 147 return Sha1{}, fmt.Errorf("%s is not a valid sha1", arg) 148 } 149 } 150 notsha1: 151 tree, err = RevParseTreeish(c, opt, treepart) 152 if err != nil { 153 return Sha1{}, err 154 } 155 extractpath: 156 if pathcomponent < 0 { 157 treeid, err := tree.TreeID(c) 158 if err != nil { 159 return Sha1{}, err 160 } 161 return Sha1(treeid), nil 162 } 163 path := arg[pathcomponent+1:] 164 indexes, err := expandGitTreeIntoIndexes(c, tree, true, true, false) 165 for _, entry := range indexes { 166 if entry.PathName == IndexPath(path) { 167 return entry.Sha1, nil 168 } 169 } 170 return Sha1{}, fmt.Errorf("%v not found", arg) 171 } 172 173 // RevParseTreeish will parse a single revision into a Treeish structure. 174 func RevParseTreeish(c *Client, opt *RevParseOptions, arg string) (Treeish, error) { 175 if len(arg) == 40 { 176 comm, err := Sha1FromString(arg) 177 if err != nil { 178 return nil, err 179 } 180 switch comm.Type(c) { 181 case "tree": 182 return TreeID(comm), nil 183 case "commit": 184 return CommitID(comm), nil 185 default: 186 return nil, fmt.Errorf("%s is not a tree-ish", arg) 187 } 188 } 189 190 if arg == "HEAD" { 191 return c.GetHeadCommit() 192 } 193 194 refs, err := ShowRef(c, ShowRefOptions{}, []string{arg}) 195 if err == nil && len(refs) > 0 { 196 return refs[0], nil 197 } 198 199 cid, err := RevParseCommitish(c, opt, arg) 200 if err != nil { 201 return nil, err 202 } 203 // A CommitID implements Treeish, so we just resolve the commitish to a real commit 204 return cid.CommitID(c) 205 } 206 207 // RevParse will parse a single revision into a Commitish object. 208 func RevParseCommitish(c *Client, opt *RevParseOptions, arg string) (cmt Commitish, err error) { 209 var cmtbase string 210 if pos := strings.IndexAny(arg, "@^"); pos >= 0 { 211 cmtbase = arg[:pos] 212 defer func(mod string) { 213 if err != nil { 214 // If there was already an error, then just let it be. 215 return 216 } 217 // FIXME: This should actually implement various ^ and @{} modifiers 218 switch mod { 219 case "^0": 220 basecmt, newerr := cmt.CommitID(c) 221 if newerr != nil { 222 err = newerr 223 return 224 } 225 cmt = basecmt 226 return 227 case "^": 228 basecmt, newerr := cmt.CommitID(c) 229 if newerr != nil { 230 err = newerr 231 return 232 } 233 parents, newerr := basecmt.Parents(c) 234 if newerr != nil { 235 err = newerr 236 return 237 } 238 if len(parents) != 1 { 239 err = fmt.Errorf("Can not use ^ modifier on merge commit.") 240 return 241 } 242 cmt = parents[0] 243 return 244 } 245 err = fmt.Errorf("Unhandled commit modifier: %v", mod) 246 }(arg[pos:]) 247 } else { 248 cmtbase = arg 249 } 250 if len(cmtbase) == 40 { 251 sha1, err := Sha1FromString(cmtbase) 252 return CommitID(sha1), err 253 } 254 if cmtbase == "HEAD" { 255 return c.GetHeadCommit() 256 } 257 258 // Check if it's a symbolic ref 259 var b Branch 260 r, err := SymbolicRefGet(c, SymbolicRefOptions{}, SymbolicRef(cmtbase)) 261 if err == nil { 262 // It was a symbolic ref, convert the refspec to a branch. 263 if b = Branch(r); b.Exists(c) { 264 return b, nil 265 } 266 } 267 if strings.HasPrefix(cmtbase, "refs/") { 268 if rs := c.GitDir.File(File(cmtbase)); rs.Exists() { 269 return RefSpec(cmtbase), nil 270 } 271 } 272 if rs := c.GitDir.File("refs/tags/" + File(cmtbase)); rs.Exists() { 273 return RefSpec("refs/tags/" + cmtbase), nil 274 } 275 276 // arg was not a Sha or a symbolic ref, it might still be a branch. 277 // (This will return an error if arg is an invalid branch.) 278 if b, err := GetBranch(c, cmtbase); err == nil { 279 return b, nil 280 } 281 282 // Try seeing if it's an abbreviation of a commit as a last 283 // resort. We require a length of at least 3, so that we only 284 // need to search one directory of the objects directory. 285 if len(cmtbase) > 2 && len(cmtbase) < 40 { 286 dir := cmtbase[:2] 287 var candidates []CommitID 288 289 fulldir := filepath.Join(c.GitDir.String(), "objects", dir) 290 files, err := ioutil.ReadDir(fulldir) 291 if err == nil { 292 for _, f := range files { 293 cand := dir + f.Name() 294 if strings.HasPrefix(cand, cmtbase) { 295 cid, err := Sha1FromString(cand) 296 if err != nil { 297 continue 298 } 299 candidates = append(candidates, CommitID(cid)) 300 } 301 } 302 } 303 304 // We need to check the pack file indexes even 305 // if we already found something in order to 306 // ensure that it's not an ambiguous reference. 307 packdir := filepath.Join(c.GitDir.String(), "objects", "pack") 308 packs, err := ioutil.ReadDir(packdir) 309 if err != nil { 310 // There was an error getting the packfiles, 311 // so assume there aren't any. 312 goto donecommit 313 } 314 315 for _, fi := range packs { 316 if filepath.Ext(fi.Name()) != ".idx" { 317 continue 318 } 319 packfile := filepath.Join( 320 c.GitDir.String(), 321 "objects", 322 "pack", 323 filepath.Base(fi.Name()), 324 ) 325 f, err := os.Open(packfile) 326 if err != nil { 327 fmt.Println(err) 328 continue 329 } 330 defer f.Close() 331 332 objects := v2PackObjectListFromIndex(f) 333 for _, obj := range objects { 334 cand := obj.String() 335 if strings.HasPrefix(cand, cmtbase) { 336 candidates = append(candidates, CommitID(obj)) 337 } 338 } 339 } 340 341 donecommit: 342 if len(candidates) == 1 { 343 return candidates[0], nil 344 } else if len(candidates) > 1 { 345 // Remove duplicates before declaring it ambiguous 346 m := make(map[CommitID]struct{}) 347 for _, c := range candidates { 348 m[c] = struct{}{} 349 } 350 if len(m) == 1 { 351 return candidates[0], nil 352 } 353 return nil, fmt.Errorf("Ambiguous reference: '%v', %v", arg, candidates) 354 } 355 } 356 return nil, fmt.Errorf("Could not find %v", arg) 357 } 358 359 // RevParse will parse a single revision into a Commit object. 360 func RevParseCommit(c *Client, opt *RevParseOptions, arg string) (CommitID, error) { 361 cmt, err := RevParseCommitish(c, opt, arg) 362 if err != nil { 363 return CommitID{}, fmt.Errorf("Invalid commit: %s", arg) 364 } 365 return cmt.CommitID(c) 366 } 367 368 // Implements "git rev-parse". This should be refactored in terms of RevParseCommit and cleaned up. 369 // (clean up a lot.) 370 func RevParse(c *Client, opt RevParseOptions, args []string) (commits []ParsedRevision, err2 error) { 371 if opt.Default != "" && len(args) == 0 { 372 args = []string{opt.Default} 373 } 374 if opt.Verify { 375 if len(args) != 1 { 376 return nil, fmt.Errorf("fatal: need a single revision") 377 } 378 } 379 for _, arg := range args { 380 switch arg { 381 case "--git-dir": 382 wd, err := os.Getwd() 383 if err == nil { 384 if c.GitDir.String() == wd { 385 // FIXME: It's not very clear when git uses the 386 // absolute path and when it uses the relative path, 387 // but in this case the rev-parse test suite depends 388 // on "." 389 fmt.Println(".") 390 } else { 391 fmt.Println(strings.TrimPrefix(c.GitDir.String(), wd+"/")) 392 } 393 } else { 394 fmt.Println(c.GitDir) 395 } 396 case "--is-inside-git-dir": 397 if c.IsInsideGitDir(".") { 398 fmt.Printf("true\n") 399 } else { 400 fmt.Printf("false\n") 401 } 402 case "--is-inside-work-tree": 403 if c.IsInsideWorkTree(".") { 404 fmt.Printf("true\n") 405 } else { 406 fmt.Printf("false\n") 407 } 408 case "--is-bare-repository": 409 if c.IsBare() { 410 fmt.Printf("true\n") 411 } else { 412 fmt.Printf("false\n") 413 } 414 case "--show-toplevel": 415 absgd, err := filepath.Abs(c.WorkDir.String()) 416 if err != nil { 417 fmt.Fprintln(os.Stderr, err) 418 continue 419 } 420 fmt.Println(absgd) 421 case "--show-prefix": 422 // I don't know why, but the git test suite tests that 423 // prefix prints "" when GIT_DIR is set, even when it's 424 // set to the same .git directory that would be evaluated 425 // without it. 426 if c.IsBare() || c.IsInsideGitDir(".") || os.Getenv("GIT_DIR") != "" { 427 fmt.Println("") 428 continue 429 } 430 absgd, err := filepath.Abs(c.WorkDir.String()) 431 if err != nil { 432 fmt.Fprintln(os.Stderr, err) 433 continue 434 } 435 pwd, err := os.Getwd() 436 if err != nil { 437 fmt.Fprintln(os.Stderr, err) 438 continue 439 } 440 if pwd == absgd { 441 fmt.Println("") 442 } else { 443 fmt.Println(strings.TrimPrefix(pwd, absgd+"/") + "/") 444 } 445 default: 446 if len(arg) > 0 && arg[0] == '-' { 447 fmt.Printf("%s\n", arg) 448 } else { 449 var sha string 450 var exclude bool 451 if arg[0] == '^' { 452 sha = arg[1:] 453 exclude = true 454 } else { 455 sha = arg 456 exclude = false 457 } 458 if strings.Contains(arg, ":") { 459 sha, err := RevParsePath(c, &opt, arg) 460 if err != nil { 461 err2 = err 462 } else { 463 commits = append(commits, ParsedRevision{sha, exclude}) 464 } 465 } else if strings.HasSuffix(arg, "^{tree}") { 466 tree, err := RevParseTreeish(c, &opt, strings.TrimSuffix(sha, "^{tree}")) 467 if err != nil { 468 err2 = err 469 } else { 470 treeid, err := tree.TreeID(c) 471 if err != nil { 472 err2 = err 473 } 474 commits = append(commits, ParsedRevision{Sha1(treeid), exclude}) 475 } 476 } else { 477 obj, err := RevParseCommitish(c, &opt, sha) 478 if err != nil { 479 err2 = err 480 continue 481 } 482 switch r := obj.(type) { 483 case RefSpec: 484 // If it's an annotated tag, 485 // don't dereference to a commit 486 obj, err := r.Sha1(c) 487 if err != nil { 488 err2 = err 489 } else { 490 commits = append(commits, ParsedRevision{obj, exclude}) 491 } 492 default: 493 cmt, err := obj.CommitID(c) 494 if err != nil { 495 err2 = err 496 } else { 497 commits = append(commits, ParsedRevision{Sha1(cmt), exclude}) 498 } 499 } 500 501 } 502 } 503 } 504 } 505 if opt.Verify && err2 != nil { 506 if strings.HasPrefix(err2.Error(), "Could not find") { 507 return nil, fmt.Errorf("fatal: need a single revision") 508 509 } 510 return nil, err2 511 } 512 return 513 }