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  }