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 }