Imported Upstream version 2.5.0
[scm/test.git] / commands / command_status.go
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 }