13 "github.com/git-lfs/git-lfs/git"
14 "github.com/git-lfs/git-lfs/lfs"
15 "github.com/spf13/cobra"
23 func statusCommand(cmd *cobra.Command, args []string) {
26 // tolerate errors getting ref so this works before first commit
27 ref, _ := git.CurrentRef()
31 scanIndexAt = git.RefBeforeFirstCommit
34 scanner, err := lfs.NewPointerScanner()
42 porcelainStagedPointers(scanIndexAt)
44 } else if statusJson {
45 jsonStagedPointers(scanner, scanIndexAt)
49 statusScanRefRange(ref)
51 staged, unstaged, err := scanIndex(scanIndexAt)
57 repo := cfg.LocalWorkingDir()
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))
67 case lfs.StatusRename, lfs.StatusCopy:
68 Print("\t%s -> %s (%s)", src, dst, formatBlobInfo(scanner, entry))
70 Print("\t%s (%s)", src, formatBlobInfo(scanner, entry))
74 Print("\nGit LFS objects not staged for commit:\n")
75 for _, entry := range unstaged {
76 src := relativize(wd, filepath.Join(repo, entry.SrcName))
78 Print("\t%s (%s)", src, formatBlobInfo(scanner, entry))
83 if err = scanner.Close(); err != nil {
88 var z40 = regexp.MustCompile(`\^?0{40}`)
90 func formatBlobInfo(s *lfs.PointerScanner, entry *lfs.DiffIndexEntry) string {
91 fromSha, fromSrc, err := blobInfoFrom(s, entry)
96 from := fmt.Sprintf("%s: %s", fromSrc, fromSha)
97 if entry.Status == lfs.StatusAddition {
101 toSha, toSrc, err := blobInfoTo(s, entry)
105 to := fmt.Sprintf("%s: %s", toSrc, toSha)
107 return fmt.Sprintf("%s -> %s", from, to)
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
116 return blobInfo(s, blobSha, entry.SrcName)
119 func blobInfoTo(s *lfs.PointerScanner, entry *lfs.DiffIndexEntry) (sha, from string, err error) {
120 var name string = entry.DstName
125 return blobInfo(s, entry.DstSha, name)
128 func blobInfo(s *lfs.PointerScanner, blobSha, name string) (sha, from string, err error) {
129 if !z40.MatchString(blobSha) {
131 if err := s.Err(); err != nil {
132 if git.IsMissingObject(err) {
133 return "<missing>", "?", nil
139 if s.Pointer() != nil {
145 return s.ContentsSha()[:7], from, nil
148 f, err := os.Open(filepath.Join(cfg.LocalWorkingDir(), name))
154 shasum := sha256.New()
155 if _, err = io.Copy(shasum, f); err != nil {
159 return fmt.Sprintf("%x", shasum.Sum(nil))[:7], "File", nil
162 func scanIndex(ref string) (staged, unstaged []*lfs.DiffIndexEntry, err error) {
163 uncached, err := lfs.NewDiffIndexScanner(ref, false)
168 cached, err := lfs.NewDiffIndexScanner(ref, true)
173 seenNames := make(map[string]struct{}, 0)
175 staged, err = drainScanner(seenNames, cached)
180 unstaged, err = drainScanner(seenNames, uncached)
188 func drainScanner(cache map[string]struct{}, scanner *lfs.DiffIndexScanner) ([]*lfs.DiffIndexEntry, error) {
189 var to []*lfs.DiffIndexEntry
192 entry := scanner.Entry()
194 key := keyFromEntry(entry)
195 if _, seen := cache[key]; !seen {
196 to = append(to, entry)
198 cache[key] = struct{}{}
202 if err := scanner.Err(); err != nil {
208 func keyFromEntry(e *lfs.DiffIndexEntry) string {
209 var name string = e.DstName
214 return strings.Join([]string{e.SrcSha, e.DstSha, name}, ":")
217 func statusScanRefRange(ref *git.Ref) {
222 Print("On branch %s", ref.Name)
224 remoteRef, err := cfg.GitConfig().CurrentRemoteRef()
229 gitscanner := lfs.NewGitScanner(func(p *lfs.WrappedPointer, err error) {
231 Panic(err, "Could not scan for Git LFS objects")
235 Print("\t%s (%s)", p.Name, p.Oid)
237 defer gitscanner.Close()
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")
246 type JSONStatusEntry struct {
247 Status string `json:"status"`
248 From string `json:"from,omitempty"`
251 type JSONStatus struct {
252 Files map[string]JSONStatusEntry `json:"files"`
255 func jsonStagedPointers(scanner *lfs.PointerScanner, ref string) {
256 staged, unstaged, err := scanIndex(ref)
261 status := JSONStatus{Files: make(map[string]JSONStatusEntry)}
263 for _, entry := range append(unstaged, staged...) {
264 _, fromSrc, err := blobInfoFrom(scanner, entry)
269 if fromSrc != "LFS" {
273 switch entry.Status {
274 case lfs.StatusRename, lfs.StatusCopy:
275 status.Files[entry.DstName] = JSONStatusEntry{
276 Status: string(entry.Status), From: entry.SrcName,
279 status.Files[entry.SrcName] = JSONStatusEntry{
280 Status: string(entry.Status),
285 ret, err := json.Marshal(status)
292 func porcelainStagedPointers(ref string) {
293 staged, unstaged, err := scanIndex(ref)
298 seenNames := make(map[string]struct{})
300 for _, entry := range append(unstaged, staged...) {
301 name := entry.DstName
306 if _, seen := seenNames[name]; !seen {
307 Print(porcelainStatusLine(entry))
309 seenNames[name] = struct{}{}
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)
322 return fmt.Sprintf("%s %s", entry.Status, entry.SrcName)
325 // relativize relatives a path from "from" to "to". For instance, note that, for
326 // any paths "from" and "to", that:
328 // to == filepath.Clean(filepath.Join(from, relativize(from, to)))
329 func relativize(from, to string) string {
334 flist := strings.Split(filepath.ToSlash(from), "/")
335 tlist := strings.Split(filepath.ToSlash(to), "/")
342 if lf, lt := len(flist), len(tlist); lf < lt {
348 for ; divergence < min; divergence++ {
349 if flist[divergence] != tlist[divergence] {
354 return strings.Repeat("../", len(flist)-divergence) +
355 strings.Join(tlist[divergence:], "/")
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.")