github.com/catandhorse/git-lfs@v2.5.2+incompatible/commands/command_status.go (about)

     1  package commands
     2  
     3  import (
     4  	"crypto/sha256"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"strings"
    12  
    13  	"github.com/git-lfs/git-lfs/git"
    14  	"github.com/git-lfs/git-lfs/lfs"
    15  	"github.com/spf13/cobra"
    16  )
    17  
    18  var (
    19  	porcelain  = false
    20  	statusJson = false
    21  )
    22  
    23  func statusCommand(cmd *cobra.Command, args []string) {
    24  	requireInRepo()
    25  
    26  	// tolerate errors getting ref so this works before first commit
    27  	ref, _ := git.CurrentRef()
    28  
    29  	scanIndexAt := "HEAD"
    30  	if ref == nil {
    31  		scanIndexAt = git.RefBeforeFirstCommit
    32  	}
    33  
    34  	scanner, err := lfs.NewPointerScanner()
    35  	if err != nil {
    36  		scanner.Close()
    37  
    38  		ExitWithError(err)
    39  	}
    40  
    41  	if porcelain {
    42  		porcelainStagedPointers(scanIndexAt)
    43  		return
    44  	} else if statusJson {
    45  		jsonStagedPointers(scanner, scanIndexAt)
    46  		return
    47  	}
    48  
    49  	statusScanRefRange(ref)
    50  
    51  	staged, unstaged, err := scanIndex(scanIndexAt)
    52  	if err != nil {
    53  		ExitWithError(err)
    54  	}
    55  
    56  	wd, _ := os.Getwd()
    57  	repo := cfg.LocalWorkingDir()
    58  
    59  	Print("\nGit LFS objects to be committed:\n")
    60  	for _, entry := range staged {
    61  		// Find a path from the current working directory to the
    62  		// absolute path of each side of the entry.
    63  		src := relativize(wd, filepath.Join(repo, entry.SrcName))
    64  		dst := relativize(wd, filepath.Join(repo, entry.DstName))
    65  
    66  		switch entry.Status {
    67  		case lfs.StatusRename, lfs.StatusCopy:
    68  			Print("\t%s -> %s (%s)", src, dst, formatBlobInfo(scanner, entry))
    69  		default:
    70  			Print("\t%s (%s)", src, formatBlobInfo(scanner, entry))
    71  		}
    72  	}
    73  
    74  	Print("\nGit LFS objects not staged for commit:\n")
    75  	for _, entry := range unstaged {
    76  		src := relativize(wd, filepath.Join(repo, entry.SrcName))
    77  
    78  		Print("\t%s (%s)", src, formatBlobInfo(scanner, entry))
    79  	}
    80  
    81  	Print("")
    82  
    83  	if err = scanner.Close(); err != nil {
    84  		ExitWithError(err)
    85  	}
    86  }
    87  
    88  var z40 = regexp.MustCompile(`\^?0{40}`)
    89  
    90  func formatBlobInfo(s *lfs.PointerScanner, entry *lfs.DiffIndexEntry) string {
    91  	fromSha, fromSrc, err := blobInfoFrom(s, entry)
    92  	if err != nil {
    93  		ExitWithError(err)
    94  	}
    95  
    96  	from := fmt.Sprintf("%s: %s", fromSrc, fromSha)
    97  	if entry.Status == lfs.StatusAddition {
    98  		return from
    99  	}
   100  
   101  	toSha, toSrc, err := blobInfoTo(s, entry)
   102  	if err != nil {
   103  		ExitWithError(err)
   104  	}
   105  	to := fmt.Sprintf("%s: %s", toSrc, toSha)
   106  
   107  	return fmt.Sprintf("%s -> %s", from, to)
   108  }
   109  
   110  func blobInfoFrom(s *lfs.PointerScanner, entry *lfs.DiffIndexEntry) (sha, from string, err error) {
   111  	var blobSha string = entry.SrcSha
   112  	if z40.MatchString(blobSha) {
   113  		blobSha = entry.DstSha
   114  	}
   115  
   116  	return blobInfo(s, blobSha, entry.SrcName)
   117  }
   118  
   119  func blobInfoTo(s *lfs.PointerScanner, entry *lfs.DiffIndexEntry) (sha, from string, err error) {
   120  	var name string = entry.DstName
   121  	if len(name) == 0 {
   122  		name = entry.SrcName
   123  	}
   124  
   125  	return blobInfo(s, entry.DstSha, name)
   126  }
   127  
   128  func blobInfo(s *lfs.PointerScanner, blobSha, name string) (sha, from string, err error) {
   129  	if !z40.MatchString(blobSha) {
   130  		s.Scan(blobSha)
   131  		if err := s.Err(); err != nil {
   132  			if git.IsMissingObject(err) {
   133  				return "<missing>", "?", nil
   134  			}
   135  			return "", "", err
   136  		}
   137  
   138  		var from string
   139  		if s.Pointer() != nil {
   140  			from = "LFS"
   141  		} else {
   142  			from = "Git"
   143  		}
   144  
   145  		return s.ContentsSha()[:7], from, nil
   146  	}
   147  
   148  	f, err := os.Open(filepath.Join(cfg.LocalWorkingDir(), name))
   149  	if err != nil {
   150  		return "", "", err
   151  	}
   152  	defer f.Close()
   153  
   154  	shasum := sha256.New()
   155  	if _, err = io.Copy(shasum, f); err != nil {
   156  		return "", "", err
   157  	}
   158  
   159  	return fmt.Sprintf("%x", shasum.Sum(nil))[:7], "File", nil
   160  }
   161  
   162  func scanIndex(ref string) (staged, unstaged []*lfs.DiffIndexEntry, err error) {
   163  	uncached, err := lfs.NewDiffIndexScanner(ref, false)
   164  	if err != nil {
   165  		return nil, nil, err
   166  	}
   167  
   168  	cached, err := lfs.NewDiffIndexScanner(ref, true)
   169  	if err != nil {
   170  		return nil, nil, err
   171  	}
   172  
   173  	seenNames := make(map[string]struct{}, 0)
   174  
   175  	staged, err = drainScanner(seenNames, cached)
   176  	if err != nil {
   177  		return nil, nil, err
   178  	}
   179  
   180  	unstaged, err = drainScanner(seenNames, uncached)
   181  	if err != nil {
   182  		return nil, nil, err
   183  	}
   184  
   185  	return
   186  }
   187  
   188  func drainScanner(cache map[string]struct{}, scanner *lfs.DiffIndexScanner) ([]*lfs.DiffIndexEntry, error) {
   189  	var to []*lfs.DiffIndexEntry
   190  
   191  	for scanner.Scan() {
   192  		entry := scanner.Entry()
   193  
   194  		key := keyFromEntry(entry)
   195  		if _, seen := cache[key]; !seen {
   196  			to = append(to, entry)
   197  
   198  			cache[key] = struct{}{}
   199  		}
   200  	}
   201  
   202  	if err := scanner.Err(); err != nil {
   203  		return nil, err
   204  	}
   205  	return to, nil
   206  }
   207  
   208  func keyFromEntry(e *lfs.DiffIndexEntry) string {
   209  	var name string = e.DstName
   210  	if len(name) == 0 {
   211  		name = e.SrcName
   212  	}
   213  
   214  	return strings.Join([]string{e.SrcSha, e.DstSha, name}, ":")
   215  }
   216  
   217  func statusScanRefRange(ref *git.Ref) {
   218  	if ref == nil {
   219  		return
   220  	}
   221  
   222  	Print("On branch %s", ref.Name)
   223  
   224  	remoteRef, err := cfg.GitConfig().CurrentRemoteRef()
   225  	if err != nil {
   226  		return
   227  	}
   228  
   229  	gitscanner := lfs.NewGitScanner(func(p *lfs.WrappedPointer, err error) {
   230  		if err != nil {
   231  			Panic(err, "Could not scan for Git LFS objects")
   232  			return
   233  		}
   234  
   235  		Print("\t%s (%s)", p.Name, p.Oid)
   236  	})
   237  	defer gitscanner.Close()
   238  
   239  	Print("Git LFS objects to be pushed to %s:\n", remoteRef.Name)
   240  	if err := gitscanner.ScanRefRange(ref.Sha, "^"+remoteRef.Sha, nil); err != nil {
   241  		Panic(err, "Could not scan for Git LFS objects")
   242  	}
   243  
   244  }
   245  
   246  type JSONStatusEntry struct {
   247  	Status string `json:"status"`
   248  	From   string `json:"from,omitempty"`
   249  }
   250  
   251  type JSONStatus struct {
   252  	Files map[string]JSONStatusEntry `json:"files"`
   253  }
   254  
   255  func jsonStagedPointers(scanner *lfs.PointerScanner, ref string) {
   256  	staged, unstaged, err := scanIndex(ref)
   257  	if err != nil {
   258  		ExitWithError(err)
   259  	}
   260  
   261  	status := JSONStatus{Files: make(map[string]JSONStatusEntry)}
   262  
   263  	for _, entry := range append(unstaged, staged...) {
   264  		_, fromSrc, err := blobInfoFrom(scanner, entry)
   265  		if err != nil {
   266  			ExitWithError(err)
   267  		}
   268  
   269  		if fromSrc != "LFS" {
   270  			continue
   271  		}
   272  
   273  		switch entry.Status {
   274  		case lfs.StatusRename, lfs.StatusCopy:
   275  			status.Files[entry.DstName] = JSONStatusEntry{
   276  				Status: string(entry.Status), From: entry.SrcName,
   277  			}
   278  		default:
   279  			status.Files[entry.SrcName] = JSONStatusEntry{
   280  				Status: string(entry.Status),
   281  			}
   282  		}
   283  	}
   284  
   285  	ret, err := json.Marshal(status)
   286  	if err != nil {
   287  		ExitWithError(err)
   288  	}
   289  	Print(string(ret))
   290  }
   291  
   292  func porcelainStagedPointers(ref string) {
   293  	staged, unstaged, err := scanIndex(ref)
   294  	if err != nil {
   295  		ExitWithError(err)
   296  	}
   297  
   298  	seenNames := make(map[string]struct{})
   299  
   300  	for _, entry := range append(unstaged, staged...) {
   301  		name := entry.DstName
   302  		if len(name) == 0 {
   303  			name = entry.SrcName
   304  		}
   305  
   306  		if _, seen := seenNames[name]; !seen {
   307  			Print(porcelainStatusLine(entry))
   308  
   309  			seenNames[name] = struct{}{}
   310  		}
   311  	}
   312  }
   313  
   314  func porcelainStatusLine(entry *lfs.DiffIndexEntry) string {
   315  	switch entry.Status {
   316  	case lfs.StatusRename, lfs.StatusCopy:
   317  		return fmt.Sprintf("%s  %s -> %s", entry.Status, entry.SrcName, entry.DstName)
   318  	case lfs.StatusModification:
   319  		return fmt.Sprintf(" %s %s", entry.Status, entry.SrcName)
   320  	}
   321  
   322  	return fmt.Sprintf("%s  %s", entry.Status, entry.SrcName)
   323  }
   324  
   325  // relativize relatives a path from "from" to "to". For instance, note that, for
   326  // any paths "from" and "to", that:
   327  //
   328  //   to == filepath.Clean(filepath.Join(from, relativize(from, to)))
   329  func relativize(from, to string) string {
   330  	if len(from) == 0 {
   331  		return to
   332  	}
   333  
   334  	flist := strings.Split(filepath.ToSlash(from), "/")
   335  	tlist := strings.Split(filepath.ToSlash(to), "/")
   336  
   337  	var (
   338  		divergence int
   339  		min        int
   340  	)
   341  
   342  	if lf, lt := len(flist), len(tlist); lf < lt {
   343  		min = lf
   344  	} else {
   345  		min = lt
   346  	}
   347  
   348  	for ; divergence < min; divergence++ {
   349  		if flist[divergence] != tlist[divergence] {
   350  			break
   351  		}
   352  	}
   353  
   354  	return strings.Repeat("../", len(flist)-divergence) +
   355  		strings.Join(tlist[divergence:], "/")
   356  }
   357  
   358  func init() {
   359  	RegisterCommand("status", statusCommand, func(cmd *cobra.Command) {
   360  		cmd.Flags().BoolVarP(&porcelain, "porcelain", "p", false, "Give the output in an easy-to-parse format for scripts.")
   361  		cmd.Flags().BoolVarP(&statusJson, "json", "j", false, "Give the output in a stable json format for scripts.")
   362  	})
   363  }