1package lfs 2 3import ( 4 "fmt" 5 "io" 6 "os" 7 "path/filepath" 8 "runtime" 9 "strings" 10 11 "github.com/git-lfs/git-lfs/v3/config" 12 "github.com/git-lfs/git-lfs/v3/tools" 13) 14 15type Platform int 16 17const ( 18 PlatformWindows = Platform(iota) 19 PlatformLinux = Platform(iota) 20 PlatformOSX = Platform(iota) 21 PlatformOther = Platform(iota) // most likely a *nix variant e.g. freebsd 22 PlatformUndetermined = Platform(iota) 23) 24 25var currentPlatform = PlatformUndetermined 26 27func join(parts ...string) string { 28 return strings.Join(parts, "/") 29} 30 31func (f *GitFilter) CopyCallbackFile(event, filename string, index, totalFiles int) (tools.CopyCallback, *os.File, error) { 32 logPath, _ := f.cfg.Os.Get("GIT_LFS_PROGRESS") 33 if len(logPath) == 0 || len(filename) == 0 || len(event) == 0 { 34 return nil, nil, nil 35 } 36 37 if !filepath.IsAbs(logPath) { 38 return nil, nil, fmt.Errorf("GIT_LFS_PROGRESS must be an absolute path") 39 } 40 41 cbDir := filepath.Dir(logPath) 42 if err := tools.MkdirAll(cbDir, f.cfg); err != nil { 43 return nil, nil, wrapProgressError(err, event, logPath) 44 } 45 46 file, err := os.OpenFile(logPath, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) 47 if err != nil { 48 return nil, file, wrapProgressError(err, event, logPath) 49 } 50 51 var prevWritten int64 52 53 cb := tools.CopyCallback(func(total int64, written int64, current int) error { 54 if written != prevWritten { 55 _, err := file.Write([]byte(fmt.Sprintf("%s %d/%d %d/%d %s\n", event, index, totalFiles, written, total, filename))) 56 file.Sync() 57 prevWritten = written 58 return wrapProgressError(err, event, logPath) 59 } 60 61 return nil 62 }) 63 64 return cb, file, nil 65} 66 67func wrapProgressError(err error, event, filename string) error { 68 if err != nil { 69 return fmt.Errorf("error writing Git LFS %s progress to %s: %s", event, filename, err.Error()) 70 } 71 72 return nil 73} 74 75var localDirSet = tools.NewStringSetFromSlice([]string{".", "./", ".\\"}) 76 77func GetPlatform() Platform { 78 if currentPlatform == PlatformUndetermined { 79 switch runtime.GOOS { 80 case "windows": 81 currentPlatform = PlatformWindows 82 case "linux": 83 currentPlatform = PlatformLinux 84 case "darwin": 85 currentPlatform = PlatformOSX 86 default: 87 currentPlatform = PlatformOther 88 } 89 } 90 return currentPlatform 91} 92 93type PathConverter interface { 94 Convert(string) string 95} 96 97// Convert filenames expressed relative to the root of the repo relative to the 98// current working dir. Useful when needing to calling git with results from a rooted command, 99// but the user is in a subdir of their repo 100func NewRepoToCurrentPathConverter(cfg *config.Configuration) (PathConverter, error) { 101 r, c, p, err := pathConverterArgs(cfg) 102 if err != nil { 103 return nil, err 104 } 105 106 return &repoToCurrentPathConverter{ 107 repoDir: r, 108 currDir: c, 109 passthrough: p, 110 }, nil 111} 112 113type repoToCurrentPathConverter struct { 114 repoDir string 115 currDir string 116 passthrough bool 117} 118 119func (p *repoToCurrentPathConverter) Convert(filename string) string { 120 if p.passthrough { 121 return filename 122 } 123 124 abs := join(p.repoDir, filename) 125 rel, err := filepath.Rel(p.currDir, abs) 126 if err != nil { 127 // Use absolute file instead 128 return abs 129 } 130 return filepath.ToSlash(rel) 131} 132 133// Convert filenames expressed relative to the current directory to be 134// relative to the repo root. Useful when calling git with arguments that requires them 135// to be rooted but the user is in a subdir of their repo & expects to use relative args 136func NewCurrentToRepoPathConverter(cfg *config.Configuration) (PathConverter, error) { 137 r, c, p, err := pathConverterArgs(cfg) 138 if err != nil { 139 return nil, err 140 } 141 142 return ¤tToRepoPathConverter{ 143 repoDir: r, 144 currDir: c, 145 passthrough: p, 146 }, nil 147} 148 149type currentToRepoPathConverter struct { 150 repoDir string 151 currDir string 152 passthrough bool 153} 154 155func (p *currentToRepoPathConverter) Convert(filename string) string { 156 if p.passthrough { 157 return filename 158 } 159 160 var abs string 161 if filepath.IsAbs(filename) { 162 abs = tools.ResolveSymlinks(filename) 163 } else { 164 abs = join(p.currDir, filename) 165 } 166 reltoroot, err := filepath.Rel(p.repoDir, abs) 167 if err != nil { 168 // Can't do this, use absolute as best fallback 169 return abs 170 } 171 return filepath.ToSlash(reltoroot) 172} 173 174// Convert filenames expressed relative to the current directory to be relative 175// to the repo root and convert them into wildmatch patterns. 176func NewCurrentToRepoPatternConverter(cfg *config.Configuration) (PathConverter, error) { 177 r, c, p, err := pathConverterArgs(cfg) 178 if err != nil { 179 return nil, err 180 } 181 182 return ¤tToRepoPatternConverter{ 183 c: ¤tToRepoPathConverter{ 184 repoDir: r, 185 currDir: c, 186 passthrough: p, 187 }, 188 }, nil 189} 190 191type currentToRepoPatternConverter struct { 192 c *currentToRepoPathConverter 193} 194 195func (p *currentToRepoPatternConverter) Convert(filename string) string { 196 pattern := p.c.Convert(filename) 197 if st, err := os.Stat(filename); err == nil && st.IsDir() { 198 pattern += "/" 199 } 200 if strings.HasPrefix(pattern, "./") { 201 pattern = pattern[2:] 202 if len(pattern) == 0 { 203 pattern = "**" 204 } 205 } 206 return pattern 207} 208 209func pathConverterArgs(cfg *config.Configuration) (string, string, bool, error) { 210 currDir, err := os.Getwd() 211 if err != nil { 212 return "", "", false, fmt.Errorf("unable to get working dir: %v", err) 213 } 214 currDir = tools.ResolveSymlinks(currDir) 215 return cfg.LocalWorkingDir(), currDir, cfg.LocalWorkingDir() == currDir, nil 216} 217 218// Are we running on Windows? Need to handle some extra path shenanigans 219func IsWindows() bool { 220 return GetPlatform() == PlatformWindows 221} 222 223func CopyFileContents(cfg *config.Configuration, src string, dst string) error { 224 tmp, err := TempFile(cfg, filepath.Base(dst)) 225 if err != nil { 226 return err 227 } 228 defer func() { 229 tmp.Close() 230 os.Remove(tmp.Name()) 231 }() 232 in, err := os.Open(src) 233 if err != nil { 234 return err 235 } 236 defer in.Close() 237 _, err = io.Copy(tmp, in) 238 if err != nil { 239 return err 240 } 241 err = tmp.Close() 242 if err != nil { 243 return err 244 } 245 return os.Rename(tmp.Name(), dst) 246} 247 248func LinkOrCopy(cfg *config.Configuration, src string, dst string) error { 249 if src == dst { 250 return nil 251 } 252 err := os.Link(src, dst) 253 if err == nil { 254 return err 255 } 256 return CopyFileContents(cfg, src, dst) 257} 258 259// TempFile creates a temporary file in the temporary directory specified by the 260// configuration that has the proper permissions for the repository. On 261// success, it returns an open, non-nil *os.File, and the caller is responsible 262// for closing and/or removing it. On failure, the temporary file is 263// automatically cleaned up and an error returned. 264// 265// This function is designed to handle only temporary files that will be renamed 266// into place later somewhere within the Git repository. 267func TempFile(cfg *config.Configuration, pattern string) (*os.File, error) { 268 return tools.TempFile(cfg.TempDir(), pattern, cfg) 269} 270