go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/submodule_update/gitutil/git.go (about)

     1  // Copyright 2023 The Fuchsia Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  // Package gitutil is a wrapper layer for handing calls to the git tool.
     6  package gitutil
     7  
     8  import (
     9  	"bytes"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"os/exec"
    14  	"sort"
    15  	"strconv"
    16  	"strings"
    17  
    18  	"github.com/golang/glog"
    19  )
    20  
    21  // GitError structure for returning git process state.
    22  type GitError struct {
    23  	Root        string
    24  	Args        []string
    25  	Output      string
    26  	ErrorOutput string
    27  	err         error
    28  }
    29  
    30  func gitError(output, errorOutput string, err error, root string, args ...string) GitError {
    31  	return GitError{
    32  		Root:        root,
    33  		Args:        args,
    34  		Output:      output,
    35  		ErrorOutput: errorOutput,
    36  		err:         err,
    37  	}
    38  }
    39  
    40  // Error outputs GitError struct as a string.
    41  func (ge GitError) Error() string {
    42  	lines := []string{
    43  		fmt.Sprintf("(%s) 'git %s' failed:", ge.Root, strings.Join(ge.Args, " ")),
    44  		"stdout:",
    45  		ge.Output,
    46  		"stderr:",
    47  		ge.ErrorOutput,
    48  		"command fail error: " + ge.err.Error(),
    49  	}
    50  	return strings.Join(lines, "\n")
    51  }
    52  
    53  // Git structure for environmental options passed to git tool.
    54  type Git struct {
    55  	opts         map[string]string
    56  	rootDir      string
    57  	submoduleDir string
    58  	userName     string
    59  	userEmail    string
    60  }
    61  
    62  type gitOpt interface {
    63  	gitOpt()
    64  }
    65  
    66  // AuthorDateOpt is the git author date.
    67  type AuthorDateOpt string
    68  
    69  // CommitterDateOpt is the git committer date.
    70  type CommitterDateOpt string
    71  
    72  // RootDirOpt is the git root directory.
    73  type RootDirOpt string
    74  
    75  // SubmoduleDirOpt is the relative path (from git root) to a submodule.
    76  type SubmoduleDirOpt string
    77  
    78  // UserNameOpt is the git username.
    79  type UserNameOpt string
    80  
    81  // UserEmailOpt is the git user email.
    82  type UserEmailOpt string
    83  
    84  func (AuthorDateOpt) gitOpt()    {}
    85  func (CommitterDateOpt) gitOpt() {}
    86  func (RootDirOpt) gitOpt()       {}
    87  func (SubmoduleDirOpt) gitOpt()  {}
    88  func (UserNameOpt) gitOpt()      {}
    89  func (UserEmailOpt) gitOpt()     {}
    90  
    91  // Reference structure is a branch reference information.
    92  type Reference struct {
    93  	Name     string
    94  	Revision string
    95  	IsHead   bool
    96  }
    97  
    98  // Branch structure tracks the state of a branch.
    99  type Branch struct {
   100  	*Reference
   101  	Tracking *Reference
   102  }
   103  
   104  // Revision is a git revision.
   105  type Revision string
   106  
   107  // BranchName is a git branch name.
   108  type BranchName string
   109  
   110  // New is the Git factory.
   111  func New(opts ...gitOpt) *Git {
   112  	rootDir := ""
   113  	submoduleDir := ""
   114  	userName := ""
   115  	userEmail := ""
   116  	env := map[string]string{}
   117  	for _, opt := range opts {
   118  		switch typedOpt := opt.(type) {
   119  		case AuthorDateOpt:
   120  			env["GIT_AUTHOR_DATE"] = string(typedOpt)
   121  		case CommitterDateOpt:
   122  			env["GIT_COMMITTER_DATE"] = string(typedOpt)
   123  		case RootDirOpt:
   124  			rootDir = string(typedOpt)
   125  		case SubmoduleDirOpt:
   126  			submoduleDir = string(typedOpt)
   127  		case UserNameOpt:
   128  			userName = string(typedOpt)
   129  		case UserEmailOpt:
   130  			userEmail = string(typedOpt)
   131  		}
   132  	}
   133  	return &Git{
   134  		opts:         env,
   135  		rootDir:      rootDir,
   136  		submoduleDir: submoduleDir,
   137  		userName:     userName,
   138  		userEmail:    userEmail,
   139  	}
   140  }
   141  
   142  // RootDir returns the root directory of the Git object.
   143  func (g *Git) RootDir() string {
   144  	return g.rootDir
   145  }
   146  
   147  // Update allows updating of an existing Git object.
   148  func (g *Git) Update(opts ...gitOpt) {
   149  	for _, opt := range opts {
   150  		switch typedOpt := opt.(type) {
   151  		case AuthorDateOpt:
   152  			g.opts["GIT_AUTHOR_DATE"] = string(typedOpt)
   153  		case CommitterDateOpt:
   154  			g.opts["GIT_COMMITTER_DATE"] = string(typedOpt)
   155  		case RootDirOpt:
   156  			g.rootDir = string(typedOpt)
   157  		case SubmoduleDirOpt:
   158  			g.submoduleDir = string(typedOpt)
   159  		case UserNameOpt:
   160  			g.userName = string(typedOpt)
   161  		case UserEmailOpt:
   162  			g.userEmail = string(typedOpt)
   163  		}
   164  	}
   165  }
   166  
   167  // AddAllFiles adds/updates all file in working tree to staging.
   168  func (g *Git) AddAllFiles() error {
   169  	return g.run("add", "-A")
   170  }
   171  
   172  // CheckoutBranch checks out the given branch.
   173  func (g *Git) CheckoutBranch(branch string, gitSubmodules bool, opts ...CheckoutOpt) error {
   174  	args := []string{"checkout"}
   175  	var force ForceOpt = false
   176  	var detach DetachOpt = false
   177  	for _, opt := range opts {
   178  		switch typedOpt := opt.(type) {
   179  		case ForceOpt:
   180  			force = typedOpt
   181  		case DetachOpt:
   182  			detach = typedOpt
   183  		}
   184  	}
   185  	if force {
   186  		args = append(args, "-f")
   187  	}
   188  	if detach {
   189  		args = append(args, "--detach")
   190  	}
   191  
   192  	if gitSubmodules {
   193  		args = append(args, "--recurse-submodules")
   194  	}
   195  
   196  	args = append(args, branch)
   197  	if err := g.run(args...); err != nil {
   198  		return err
   199  	}
   200  	// After checkout with submodules update/checkout submodules.
   201  	if gitSubmodules {
   202  		return g.SubmoduleUpdate(nil, InitOpt(true))
   203  	}
   204  	return nil
   205  }
   206  
   207  // SubmoduleAdd adds submodule to current branch.
   208  func (g *Git) SubmoduleAdd(remote string, path string) error {
   209  	// Use -f to add submodules even if in .gitignore.
   210  	return g.run("submodule", "add", "-f", remote, path)
   211  }
   212  
   213  // SubmoduleStatus returns current current status of submodules in a superproject.
   214  func (g *Git) SubmoduleStatus(opts ...SubmoduleStatusOpt) (string, error) {
   215  	args := []string{"submodule", "status"}
   216  	for _, opt := range opts {
   217  		switch typedOpt := opt.(type) {
   218  		case CachedOpt:
   219  			if typedOpt {
   220  				args = append(args, "--cached")
   221  			}
   222  		}
   223  	}
   224  	out, err := g.runOutput(args...)
   225  	if err != nil {
   226  		return "", err
   227  	}
   228  	return strings.Join(out, "\n"), nil
   229  }
   230  
   231  // SubmoduleUpdate updates submodules for current branch.
   232  func (g *Git) SubmoduleUpdate(paths []string, opts ...SubmoduleUpdateOpt) error {
   233  	args := []string{"submodule", "update"}
   234  	for _, opt := range opts {
   235  		switch typedOpt := opt.(type) {
   236  		case InitOpt:
   237  			if typedOpt {
   238  				args = append(args, "--init")
   239  			}
   240  		}
   241  	}
   242  	args = append(args, "--jobs=50")
   243  	args = append(args, paths...)
   244  	return g.run(args...)
   245  
   246  }
   247  
   248  // Clone clones the given repository to the given local path.  If reference is
   249  // not empty it uses the given path as a reference/shared repo.
   250  func (g *Git) Clone(repo, path string, opts ...CloneOpt) error {
   251  	args := []string{"clone"}
   252  	for _, opt := range opts {
   253  		switch typedOpt := opt.(type) {
   254  		case BareOpt:
   255  			if typedOpt {
   256  				args = append(args, "--bare")
   257  			}
   258  		case ReferenceOpt:
   259  			reference := string(typedOpt)
   260  			if reference != "" {
   261  				args = append(args, []string{"--reference-if-able", reference}...)
   262  			}
   263  		case SharedOpt:
   264  			if typedOpt {
   265  				args = append(args, []string{"--shared", "--local"}...)
   266  			}
   267  		case NoCheckoutOpt:
   268  			if typedOpt {
   269  				args = append(args, "--no-checkout")
   270  			}
   271  		case DepthOpt:
   272  			if typedOpt > 0 {
   273  				args = append(args, []string{"--depth", strconv.Itoa(int(typedOpt))}...)
   274  			}
   275  		case OmitBlobsOpt:
   276  			if typedOpt {
   277  				args = append(args, "--filter=blob:none")
   278  			}
   279  		case OffloadPackfilesOpt:
   280  			if typedOpt {
   281  				args = append([]string{"-c", "fetch.uriprotocols=https"}, args...)
   282  			}
   283  		case RecurseSubmodulesOpt:
   284  			// TODO(iankaz): Add setting submodule.fetchJobs in git config to jiri init
   285  			if typedOpt {
   286  				args = append(args, []string{"--recurse-submodules", "--jobs=16"}...)
   287  			}
   288  		}
   289  	}
   290  	args = append(args, repo)
   291  	args = append(args, path)
   292  	return g.run(args...)
   293  }
   294  
   295  // CommitWithMessage commits all files in staging with the given
   296  // message.
   297  func (g *Git) CommitWithMessage(message string) error {
   298  	return g.run("commit", "--allow-empty", "--allow-empty-message", "-m", message)
   299  }
   300  
   301  // GetSymbolicRef returns which branch working tree (HEAD) is on.
   302  func (g *Git) GetSymbolicRef() (string, error) {
   303  	out, err := g.runOutput("symbolic-ref", "-q", "HEAD")
   304  	if err != nil {
   305  		return "", err
   306  	}
   307  	if got, want := len(out), 1; got != want {
   308  		return "", fmt.Errorf("unexpected length of %v: got %v, want %v", out, got, want)
   309  	}
   310  	return out[0], nil
   311  }
   312  
   313  // Fetch fetches refs and tags from the given remote.
   314  func (g *Git) Fetch(remote string, opts ...FetchOpt) error {
   315  	return g.FetchRefspec(remote, "", opts...)
   316  }
   317  
   318  // FetchRefspec fetches refs and tags from the given remote for a particular refspec.
   319  func (g *Git) FetchRefspec(remote, refspec string, opts ...FetchOpt) error {
   320  	tags := false
   321  	all := false
   322  	prune := false
   323  	updateShallow := false
   324  	depth := 0
   325  	fetchTag := ""
   326  	updateHeadOk := false
   327  	recurseSubmodules := false
   328  	jobs := uint(0)
   329  	for _, opt := range opts {
   330  		switch typedOpt := opt.(type) {
   331  		case TagsOpt:
   332  			tags = bool(typedOpt)
   333  		case AllOpt:
   334  			all = bool(typedOpt)
   335  		case PruneOpt:
   336  			prune = bool(typedOpt)
   337  		case DepthOpt:
   338  			depth = int(typedOpt)
   339  		case UpdateShallowOpt:
   340  			updateShallow = bool(typedOpt)
   341  		case FetchTagOpt:
   342  			fetchTag = string(typedOpt)
   343  		case UpdateHeadOkOpt:
   344  			updateHeadOk = bool(typedOpt)
   345  		case RecurseSubmodulesOpt:
   346  			recurseSubmodules = bool(typedOpt)
   347  		case JobsOpt:
   348  			jobs = uint(typedOpt)
   349  		}
   350  	}
   351  	args := []string{}
   352  	args = append(args, "fetch")
   353  	if prune {
   354  		args = append(args, "-p")
   355  	}
   356  	if tags {
   357  		args = append(args, "--tags")
   358  	}
   359  	if depth > 0 {
   360  		args = append(args, "--depth", strconv.Itoa(depth))
   361  	}
   362  	if updateShallow {
   363  		args = append(args, "--update-shallow")
   364  	}
   365  	if all {
   366  		args = append(args, "--all")
   367  	}
   368  	if updateHeadOk {
   369  		args = append(args, "--update-head-ok")
   370  	}
   371  	if recurseSubmodules {
   372  		args = append(args, "--recurse-submodules")
   373  	}
   374  	if jobs > 0 {
   375  		args = append(args, "--jobs="+strconv.FormatUint(uint64(jobs), 10))
   376  	}
   377  	if remote != "" {
   378  		args = append(args, remote)
   379  	}
   380  	if fetchTag != "" {
   381  		args = append(args, "tag", fetchTag)
   382  	}
   383  	if refspec != "" {
   384  		args = append(args, refspec)
   385  	}
   386  
   387  	return g.run(args...)
   388  }
   389  
   390  // FilesWithUncommittedChanges returns the list of files that have
   391  // uncommitted changes.
   392  func (g *Git) FilesWithUncommittedChanges() ([]string, error) {
   393  	out, err := g.runOutput("diff", "--name-only", "--no-ext-diff")
   394  	if err != nil {
   395  		return nil, err
   396  	}
   397  	out2, err := g.runOutput("diff", "--cached", "--name-only", "--no-ext-diff")
   398  	if err != nil {
   399  		return nil, err
   400  	}
   401  	return append(out, out2...), nil
   402  }
   403  
   404  // Remove removes the given files.
   405  func (g *Git) Remove(fileNames ...string) error {
   406  	args := []string{"rm"}
   407  	args = append(args, fileNames...)
   408  	return g.run(args...)
   409  }
   410  
   411  // ConfigGetKey gets current git configuration value for the given key.
   412  func (g *Git) ConfigGetKey(key string) (string, error) {
   413  	out, err := g.runOutput("config", "--get", key)
   414  	if err != nil {
   415  		return "", err
   416  	}
   417  	if got, want := len(out), 1; got != want {
   418  		glog.Warning("wanted one line log, got %d line log: %q", got, out)
   419  	}
   420  	return out[0], nil
   421  }
   422  
   423  // ConfigGetKeyFromFile gets current git configuration value for the given key from file.
   424  //
   425  // Returns an empty string if the configuration value is not found.
   426  func (g *Git) ConfigGetKeyFromFile(key string, file string) (string, error) {
   427  	out, err := g.runOutput("config", "--file", file, "--default", "", "--get", key)
   428  	if err != nil {
   429  		return "", err
   430  	}
   431  	if len(out) == 0 {
   432  		return "", nil
   433  	}
   434  	if len(out) > 1 {
   435  		glog.Warning("wanted one line log, got %d line log: %q", len(out), out)
   436  	}
   437  	return out[0], nil
   438  }
   439  
   440  // ConfigAddKeyToFile adds additional git configuration value to file.
   441  func (g *Git) ConfigAddKeyToFile(key string, file string, value string) error {
   442  	return g.run("config", "--file", file, key, value)
   443  }
   444  
   445  func (g *Git) run(args ...string) error {
   446  	var stdout, stderr bytes.Buffer
   447  	if err := g.runGit(&stdout, &stderr, args...); err != nil {
   448  		return gitError(stdout.String(), stderr.String(), err, g.rootDir, args...)
   449  	}
   450  	return nil
   451  }
   452  
   453  func trimOutput(o string) []string {
   454  	output := strings.TrimSpace(o)
   455  	if len(output) == 0 {
   456  		return nil
   457  	}
   458  	return strings.Split(output, "\n")
   459  }
   460  
   461  func (g *Git) runOutput(args ...string) ([]string, error) {
   462  	var stdout, stderr bytes.Buffer
   463  	if err := g.runGit(&stdout, &stderr, args...); err != nil {
   464  		return nil, gitError(stdout.String(), stderr.String(), err, g.rootDir, args...)
   465  	}
   466  	return trimOutput(stdout.String()), nil
   467  }
   468  
   469  func (g *Git) runGit(stdout, stderr io.Writer, args ...string) error {
   470  	if g.submoduleDir != "" {
   471  		args = append([]string{"-C", g.submoduleDir}, args...)
   472  	}
   473  	if g.userName != "" {
   474  		args = append([]string{"-c", fmt.Sprintf("user.name=%s", g.userName)}, args...)
   475  	}
   476  	if g.userEmail != "" {
   477  		args = append([]string{"-c", fmt.Sprintf("user.email=%s", g.userEmail)}, args...)
   478  	}
   479  	var outbuf bytes.Buffer
   480  	var errbuf bytes.Buffer
   481  	command := exec.Command("git", args...)
   482  	command.Stdin = os.Stdin
   483  	command.Stdout = io.MultiWriter(stdout, &outbuf)
   484  	command.Stderr = io.MultiWriter(stderr, &errbuf)
   485  	env := sliceToMap(os.Environ())
   486  	env = mergeMaps(g.opts, env)
   487  	command.Env = mapToSlice(env)
   488  	dir := g.rootDir
   489  	if dir == "" {
   490  		// Use working directory
   491  		if cwd, err := os.Getwd(); err == nil {
   492  			dir = cwd
   493  		}
   494  	}
   495  	command.Dir = dir
   496  	err := command.Run()
   497  	return err
   498  }
   499  
   500  // splitKeyValue splits kv into its key and value components.  The format of kv
   501  // is "key=value"; the split is performed on the first '=' character.
   502  func splitKeyValue(kv string) (string, string) {
   503  	split := strings.SplitN(kv, "=", 2)
   504  	if len(split) == 2 {
   505  		return split[0], split[1]
   506  	}
   507  	return split[0], ""
   508  }
   509  
   510  type keySorter []string
   511  
   512  func (s keySorter) Len() int      { return len(s) }
   513  func (s keySorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
   514  func (s keySorter) Less(i, j int) bool {
   515  	ikey, _ := splitKeyValue(s[i])
   516  	jkey, _ := splitKeyValue(s[j])
   517  	return ikey < jkey
   518  }
   519  
   520  // sortByKey sorts vars into ascending key order, where vars is expected to be
   521  // in the []"key=value" slice representation.
   522  func sortByKey(vars []string) {
   523  	sort.Sort(keySorter(vars))
   524  }
   525  
   526  // joinKeyValue joins key and value into a single string "key=value".
   527  func joinKeyValue(key, value string) string {
   528  	return key + "=" + value
   529  }
   530  
   531  // mapToSlice converts from the map to the slice representation.  The returned
   532  // slice is in sorted order.
   533  func mapToSlice(from map[string]string) []string {
   534  	to := make([]string, 0, len(from))
   535  	for key, value := range from {
   536  		if key != "" {
   537  			to = append(to, joinKeyValue(key, value))
   538  		}
   539  	}
   540  	sortByKey(to)
   541  	return to
   542  }
   543  
   544  // mergeMaps merges together maps, and returns a new map with the merged result.
   545  //
   546  // As a result of its semantics, mergeMaps called with a single map returns a
   547  // copy of the map, with empty keys dropped.
   548  func mergeMaps(maps ...map[string]string) map[string]string {
   549  	merged := make(map[string]string)
   550  	for _, m := range maps {
   551  		for key, value := range m {
   552  			if key != "" {
   553  				merged[key] = value
   554  			}
   555  		}
   556  	}
   557  	return merged
   558  }
   559  
   560  // SliceToMap converts from the slice to the map representation.  If the same
   561  // key appears more than once, the last one "wins"; the value is set based on
   562  // the last slice element containing that key.
   563  func sliceToMap(from []string) map[string]string {
   564  	to := make(map[string]string, len(from))
   565  	for _, kv := range from {
   566  		if key, value := splitKeyValue(kv); key != "" {
   567  			to[key] = value
   568  		}
   569  	}
   570  	return to
   571  }