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  }