github.com/stffabi/git-lfs@v2.3.5-0.20180214015214-8eeaa8d88902+incompatible/commands/commands.go (about) 1 package commands 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "log" 8 "net" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "strings" 13 "sync" 14 "time" 15 16 "github.com/git-lfs/git-lfs/config" 17 "github.com/git-lfs/git-lfs/errors" 18 "github.com/git-lfs/git-lfs/filepathfilter" 19 "github.com/git-lfs/git-lfs/git" 20 "github.com/git-lfs/git-lfs/lfs" 21 "github.com/git-lfs/git-lfs/lfsapi" 22 "github.com/git-lfs/git-lfs/locking" 23 "github.com/git-lfs/git-lfs/tools" 24 "github.com/git-lfs/git-lfs/tq" 25 ) 26 27 // Populate man pages 28 //go:generate go run ../docs/man/mangen.go 29 30 var ( 31 Debugging = false 32 ErrorBuffer = &bytes.Buffer{} 33 ErrorWriter = io.MultiWriter(os.Stderr, ErrorBuffer) 34 OutputWriter = io.MultiWriter(os.Stdout, ErrorBuffer) 35 ManPages = make(map[string]string, 20) 36 tqManifest = make(map[string]*tq.Manifest) 37 38 cfg *config.Configuration 39 apiClient *lfsapi.Client 40 global sync.Mutex 41 42 includeArg string 43 excludeArg string 44 ) 45 46 // getTransferManifest builds a tq.Manifest from the global os and git 47 // environments. 48 func getTransferManifest() *tq.Manifest { 49 return getTransferManifestOperationRemote("", "") 50 } 51 52 // getTransferManifestOperationRemote builds a tq.Manifest from the global os 53 // and git environments and operation-specific and remote-specific settings. 54 // Operation must be "download", "upload", or the empty string. 55 func getTransferManifestOperationRemote(operation, remote string) *tq.Manifest { 56 c := getAPIClient() 57 58 global.Lock() 59 defer global.Unlock() 60 61 k := fmt.Sprintf("%s.%s", operation, remote) 62 if tqManifest[k] == nil { 63 tqManifest[k] = tq.NewManifest(cfg.Filesystem(), c, operation, remote) 64 } 65 66 return tqManifest[k] 67 } 68 69 func getAPIClient() *lfsapi.Client { 70 global.Lock() 71 defer global.Unlock() 72 73 if apiClient == nil { 74 c, err := lfsapi.NewClient(cfg) 75 if err != nil { 76 ExitWithError(err) 77 } 78 apiClient = c 79 } 80 return apiClient 81 } 82 83 func closeAPIClient() error { 84 global.Lock() 85 defer global.Unlock() 86 if apiClient == nil { 87 return nil 88 } 89 return apiClient.Close() 90 } 91 92 func newLockClient() *locking.Client { 93 lockClient, err := locking.NewClient(cfg.PushRemote(), getAPIClient()) 94 if err == nil { 95 os.MkdirAll(cfg.LFSStorageDir(), 0755) 96 err = lockClient.SetupFileCache(cfg.LFSStorageDir()) 97 } 98 99 if err != nil { 100 Exit("Unable to create lock system: %v", err.Error()) 101 } 102 103 // Configure dirs 104 lockClient.LocalWorkingDir = cfg.LocalWorkingDir() 105 lockClient.LocalGitDir = cfg.LocalGitDir() 106 lockClient.SetLockableFilesReadOnly = cfg.SetLockableFilesReadOnly() 107 108 return lockClient 109 } 110 111 // newDownloadCheckQueue builds a checking queue, checks that objects are there but doesn't download 112 func newDownloadCheckQueue(manifest *tq.Manifest, remote string, options ...tq.Option) *tq.TransferQueue { 113 return newDownloadQueue(manifest, remote, append(options, 114 tq.DryRun(true), 115 )...) 116 } 117 118 // newDownloadQueue builds a DownloadQueue, allowing concurrent downloads. 119 func newDownloadQueue(manifest *tq.Manifest, remote string, options ...tq.Option) *tq.TransferQueue { 120 return tq.NewTransferQueue(tq.Download, manifest, remote, append(options, 121 tq.RemoteRef(currentRemoteRef()), 122 )...) 123 } 124 125 func currentRemoteRef() *git.Ref { 126 return git.NewRefUpdate(cfg.Git, cfg.PushRemote(), cfg.CurrentRef(), nil).Right() 127 } 128 129 func buildFilepathFilter(config *config.Configuration, includeArg, excludeArg *string) *filepathfilter.Filter { 130 inc, exc := determineIncludeExcludePaths(config, includeArg, excludeArg) 131 return filepathfilter.New(inc, exc) 132 } 133 134 func downloadTransfer(p *lfs.WrappedPointer) (name, path, oid string, size int64) { 135 path, _ = cfg.Filesystem().ObjectPath(p.Oid) 136 return p.Name, path, p.Oid, p.Size 137 } 138 139 // Get user-readable manual install steps for hooks 140 func getHookInstallSteps() string { 141 hooks := lfs.LoadHooks(cfg.HookDir()) 142 steps := make([]string, 0, len(hooks)) 143 for _, h := range hooks { 144 steps = append(steps, fmt.Sprintf( 145 "Add the following to .git/hooks/%s:\n\n%s", 146 h.Type, tools.Indent(h.Contents))) 147 } 148 149 return strings.Join(steps, "\n\n") 150 } 151 152 func installHooks(force bool) error { 153 hooks := lfs.LoadHooks(cfg.HookDir()) 154 for _, h := range hooks { 155 if err := h.Install(force); err != nil { 156 return err 157 } 158 } 159 160 return nil 161 } 162 163 // uninstallHooks removes all hooks in range of the `hooks` var. 164 func uninstallHooks() error { 165 if !cfg.InRepo() { 166 return errors.New("Not in a git repository") 167 } 168 169 hooks := lfs.LoadHooks(cfg.HookDir()) 170 for _, h := range hooks { 171 if err := h.Uninstall(); err != nil { 172 return err 173 } 174 } 175 176 return nil 177 } 178 179 // Error prints a formatted message to Stderr. It also gets printed to the 180 // panic log if one is created for this command. 181 func Error(format string, args ...interface{}) { 182 if len(args) == 0 { 183 fmt.Fprintln(ErrorWriter, format) 184 return 185 } 186 fmt.Fprintf(ErrorWriter, format+"\n", args...) 187 } 188 189 // Print prints a formatted message to Stdout. It also gets printed to the 190 // panic log if one is created for this command. 191 func Print(format string, args ...interface{}) { 192 if len(args) == 0 { 193 fmt.Fprintln(OutputWriter, format) 194 return 195 } 196 fmt.Fprintf(OutputWriter, format+"\n", args...) 197 } 198 199 // Exit prints a formatted message and exits. 200 func Exit(format string, args ...interface{}) { 201 Error(format, args...) 202 os.Exit(2) 203 } 204 205 // ExitWithError either panics with a full stack trace for fatal errors, or 206 // simply prints the error message and exits immediately. 207 func ExitWithError(err error) { 208 errorWith(err, Panic, Exit) 209 } 210 211 // FullError prints either a full stack trace for fatal errors, or just the 212 // error message. 213 func FullError(err error) { 214 errorWith(err, LoggedError, Error) 215 } 216 217 func errorWith(err error, fatalErrFn func(error, string, ...interface{}), errFn func(string, ...interface{})) { 218 if Debugging || errors.IsFatalError(err) { 219 fatalErrFn(err, "%s", err) 220 return 221 } 222 223 errFn("%s", err) 224 } 225 226 // Debug prints a formatted message if debugging is enabled. The formatted 227 // message also shows up in the panic log, if created. 228 func Debug(format string, args ...interface{}) { 229 if !Debugging { 230 return 231 } 232 log.Printf(format, args...) 233 } 234 235 // LoggedError prints the given message formatted with its arguments (if any) to 236 // Stderr. If an empty string is passed as the "format" argument, only the 237 // standard error logging message will be printed, and the error's body will be 238 // omitted. 239 // 240 // It also writes a stack trace for the error to a log file without exiting. 241 func LoggedError(err error, format string, args ...interface{}) { 242 if len(format) > 0 { 243 Error(format, args...) 244 } 245 file := handlePanic(err) 246 247 if len(file) > 0 { 248 fmt.Fprintf(os.Stderr, "\nErrors logged to %s\nUse `git lfs logs last` to view the log.\n", file) 249 } 250 } 251 252 // Panic prints a formatted message, and writes a stack trace for the error to 253 // a log file before exiting. 254 func Panic(err error, format string, args ...interface{}) { 255 LoggedError(err, format, args...) 256 os.Exit(2) 257 } 258 259 func Cleanup() { 260 if err := cfg.Cleanup(); err != nil { 261 fmt.Fprintf(os.Stderr, "Error clearing old temp files: %s\n", err) 262 } 263 } 264 265 func PipeMediaCommand(name string, args ...string) error { 266 return PipeCommand("bin/"+name, args...) 267 } 268 269 func PipeCommand(name string, args ...string) error { 270 cmd := exec.Command(name, args...) 271 cmd.Stdin = os.Stdin 272 cmd.Stderr = os.Stderr 273 cmd.Stdout = os.Stdout 274 return cmd.Run() 275 } 276 277 func requireStdin(msg string) { 278 var out string 279 280 stat, err := os.Stdin.Stat() 281 if err != nil { 282 out = fmt.Sprintf("Cannot read from STDIN. %s (%s)", msg, err) 283 } else if (stat.Mode() & os.ModeCharDevice) != 0 { 284 out = fmt.Sprintf("Cannot read from STDIN. %s", msg) 285 } 286 287 if len(out) > 0 { 288 Error(out) 289 os.Exit(1) 290 } 291 } 292 293 func requireInRepo() { 294 if !cfg.InRepo() { 295 Print("Not in a git repository.") 296 os.Exit(128) 297 } 298 } 299 300 func handlePanic(err error) string { 301 if err == nil { 302 return "" 303 } 304 305 return logPanic(err) 306 } 307 308 func logPanic(loggedError error) string { 309 var ( 310 fmtWriter io.Writer = os.Stderr 311 lineEnding string = "\n" 312 ) 313 314 now := time.Now() 315 name := now.Format("20060102T150405.999999999") 316 full := filepath.Join(cfg.LocalLogDir(), name+".log") 317 318 if err := os.MkdirAll(cfg.LocalLogDir(), 0755); err != nil { 319 full = "" 320 fmt.Fprintf(fmtWriter, "Unable to log panic to %s: %s\n\n", cfg.LocalLogDir(), err.Error()) 321 } else if file, err := os.Create(full); err != nil { 322 filename := full 323 full = "" 324 defer func() { 325 fmt.Fprintf(fmtWriter, "Unable to log panic to %s\n\n", filename) 326 logPanicToWriter(fmtWriter, err, lineEnding) 327 }() 328 } else { 329 fmtWriter = file 330 lineEnding = gitLineEnding(cfg.Git) 331 defer file.Close() 332 } 333 334 logPanicToWriter(fmtWriter, loggedError, lineEnding) 335 336 return full 337 } 338 339 func ipAddresses() []string { 340 ips := make([]string, 0, 1) 341 ifaces, err := net.Interfaces() 342 if err != nil { 343 ips = append(ips, "Error getting network interface: "+err.Error()) 344 return ips 345 } 346 for _, i := range ifaces { 347 if i.Flags&net.FlagUp == 0 { 348 continue // interface down 349 } 350 if i.Flags&net.FlagLoopback != 0 { 351 continue // loopback interface 352 } 353 addrs, _ := i.Addrs() 354 l := make([]string, 0, 1) 355 if err != nil { 356 ips = append(ips, "Error getting IP address: "+err.Error()) 357 continue 358 } 359 for _, addr := range addrs { 360 var ip net.IP 361 switch v := addr.(type) { 362 case *net.IPNet: 363 ip = v.IP 364 case *net.IPAddr: 365 ip = v.IP 366 } 367 if ip == nil || ip.IsLoopback() { 368 continue 369 } 370 l = append(l, ip.String()) 371 } 372 if len(l) > 0 { 373 ips = append(ips, strings.Join(l, " ")) 374 } 375 } 376 return ips 377 } 378 379 func logPanicToWriter(w io.Writer, loggedError error, le string) { 380 // log the version 381 gitV, err := git.Version() 382 if err != nil { 383 gitV = "Error getting git version: " + err.Error() 384 } 385 386 fmt.Fprint(w, config.VersionDesc+le) 387 fmt.Fprint(w, gitV+le) 388 389 // log the command that was run 390 fmt.Fprint(w, le) 391 fmt.Fprintf(w, "$ %s", filepath.Base(os.Args[0])) 392 if len(os.Args) > 0 { 393 fmt.Fprintf(w, " %s", strings.Join(os.Args[1:], " ")) 394 } 395 fmt.Fprint(w, le) 396 397 // log the error message and stack trace 398 w.Write(ErrorBuffer.Bytes()) 399 fmt.Fprint(w, le) 400 401 fmt.Fprintf(w, "%+v"+le, loggedError) 402 403 for key, val := range errors.Context(err) { 404 fmt.Fprintf(w, "%s=%v"+le, key, val) 405 } 406 407 fmt.Fprint(w, le+"Current time in UTC: "+le) 408 fmt.Fprint(w, time.Now().UTC().Format("2006-01-02 15:04:05")+le) 409 410 fmt.Fprint(w, le+"ENV:"+le) 411 412 // log the environment 413 for _, env := range lfs.Environ(cfg, getTransferManifest()) { 414 fmt.Fprint(w, env+le) 415 } 416 417 fmt.Fprint(w, le+"Client IP addresses:"+le) 418 419 for _, ip := range ipAddresses() { 420 fmt.Fprint(w, ip+le) 421 } 422 } 423 424 func determineIncludeExcludePaths(config *config.Configuration, includeArg, excludeArg *string) (include, exclude []string) { 425 if includeArg == nil { 426 include = config.FetchIncludePaths() 427 } else { 428 include = tools.CleanPaths(*includeArg, ",") 429 } 430 if excludeArg == nil { 431 exclude = config.FetchExcludePaths() 432 } else { 433 exclude = tools.CleanPaths(*excludeArg, ",") 434 } 435 return 436 } 437 438 func buildProgressMeter(dryRun bool, d tq.Direction) *tq.Meter { 439 m := tq.NewMeter() 440 m.Logger = m.LoggerFromEnv(cfg.Os) 441 m.DryRun = dryRun 442 m.Direction = d 443 return m 444 } 445 446 func requireGitVersion() { 447 minimumGit := "1.8.2" 448 449 if !git.IsGitVersionAtLeast(minimumGit) { 450 gitver, err := git.Version() 451 if err != nil { 452 Exit("Error getting git version: %s", err) 453 } 454 Exit("git version >= %s is required for Git LFS, your version: %s", minimumGit, gitver) 455 } 456 }