github.com/atlassian/git-lob@v0.0.0-20150806085256-2386a5ed291a/util/util.go (about) 1 package util 2 3 import ( 4 "errors" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "os/exec" 9 "path/filepath" 10 "regexp" 11 "runtime" 12 "sort" 13 "strconv" 14 "strings" 15 ) 16 17 var ( 18 parseSizeRegex *regexp.Regexp 19 ) 20 21 var cachedRepoRoot string 22 var cachedRepoRootIsSeparate bool 23 var cachedRepoRootWorkingDir string 24 25 // Gets the root folder of this git repository (the one containing .git) 26 func GetRepoRoot() (path string, isSeparateGitDir bool, reterr error) { 27 // We could call 'git rev-parse --git-dir' but this requires shelling out = slow, especially on Windows 28 // We should try to avoid that whenever we can 29 // So let's just find it ourselves; first containing folder with a .git folder/file 30 curDir, err := os.Getwd() 31 if err != nil { 32 return "", false, err 33 } 34 origCurDir := curDir 35 // Use the cached value if known 36 if cachedRepoRootWorkingDir == curDir && cachedRepoRoot != "" { 37 return cachedRepoRoot, cachedRepoRootIsSeparate, nil 38 } 39 40 for { 41 exists, isDir := FileOrDirExists(filepath.Join(curDir, ".git")) 42 if exists { 43 // Store in cache to speed up 44 cachedRepoRoot = curDir 45 cachedRepoRootWorkingDir = origCurDir 46 cachedRepoRootIsSeparate = !isDir 47 return curDir, !isDir, nil 48 } 49 curDir = filepath.Dir(curDir) 50 if len(curDir) == 0 || curDir[len(curDir)-1] == filepath.Separator || curDir == "." { 51 // Not a repo 52 return "", false, errors.New("Couldn't find repo root, not a git folder") 53 } 54 } 55 } 56 57 // Gets the git data dir of git repository (the .git dir, or where .git file points) 58 func GetGitDir() string { 59 root, isSeparate, err := GetRepoRoot() 60 if err != nil { 61 return "" 62 } 63 git := filepath.Join(root, ".git") 64 if isSeparate { 65 // Git repo folder is separate, read location from file 66 filebytes, err := ioutil.ReadFile(git) 67 if err != nil { 68 LogErrorf("Can't read .git file %v: %v\n", git, err) 69 return "" 70 } 71 filestr := string(filebytes) 72 match := regexp.MustCompile("gitdir:[\\s]+([^\\r\\n]+)").FindStringSubmatch(filestr) 73 if match == nil { 74 LogErrorf("Unexpected contents of .git file %v: %v\n", git, filestr) 75 return "" 76 } 77 // The text in the git dir will use cygwin-style separators, so normalise 78 return filepath.Clean(match[1]) 79 } else { 80 // Regular git dir 81 return git 82 } 83 84 } 85 86 // Utility method to determine if a file/dir exists 87 func FileOrDirExists(path string) (exists bool, isDir bool) { 88 fi, err := os.Stat(path) 89 if err != nil { 90 return false, false 91 } else { 92 return true, fi.IsDir() 93 } 94 } 95 96 // Utility method to determine if a file (NOT dir) exists 97 func FileExists(path string) bool { 98 ret, isDir := FileOrDirExists(path) 99 return ret && !isDir 100 } 101 102 // Utility method to determine if a dir (NOT file) exists 103 func DirExists(path string) bool { 104 ret, isDir := FileOrDirExists(path) 105 return ret && isDir 106 } 107 108 // Utility method to determine if a file/dir exists and is of a specific size 109 func FileExistsAndIsOfSize(path string, sz int64) bool { 110 fi, err := os.Stat(path) 111 112 if err != nil && os.IsNotExist(err) { 113 return false 114 } 115 116 return fi.Size() == sz 117 } 118 119 // Parse a string representing a size into a number of bytes 120 // supports m/mb = megabytes, g/gb = gigabytes etc (case insensitive) 121 func ParseSize(str string) (int64, error) { 122 if parseSizeRegex == nil { 123 parseSizeRegex = regexp.MustCompile(`(?i)^\s*([\d\.]+)\s*([KMGTP]?B?)\s*$`) 124 } 125 126 if match := parseSizeRegex.FindStringSubmatch(str); match != nil { 127 value, err := strconv.ParseFloat(match[1], 32) 128 if err != nil { 129 return 0, err 130 } 131 strUnits := strings.ToUpper(match[2]) 132 switch strUnits { 133 case "KB", "K": 134 return int64(value * (1 << 10)), nil 135 case "MB", "M": 136 return int64(value * (1 << 20)), nil 137 case "GB", "G": 138 return int64(value * (1 << 30)), nil 139 case "TB", "T": 140 return int64(value * (1 << 40)), nil 141 case "PB", "P": 142 return int64(value * (1 << 50)), nil 143 default: 144 return int64(value), nil 145 146 } 147 148 } else { 149 return 0, errors.New(fmt.Sprintf("Invalid size: %v", str)) 150 } 151 152 } 153 154 func FormatBytes(sz int64) (suffix string, scaled float32) { 155 switch { 156 case sz >= (1 << 50): 157 return "PB", float32(sz) / float32(1<<50) 158 case sz >= (1 << 40): 159 return "TB", float32(sz) / float32(1<<40) 160 case sz >= (1 << 30): 161 return "GB", float32(sz) / float32(1<<30) 162 case sz >= (1 << 20): 163 return "MB", float32(sz) / float32(1<<20) 164 case sz >= (1 << 10): 165 return "KB", float32(sz) / float32(1<<10) 166 default: 167 return "B", float32(sz) 168 } 169 170 } 171 172 func FormatFloat(f float32) string { 173 // Just adjust width & precision based on scale to be friendly 174 switch { 175 case f < 1000: 176 // Need %g to make after decimal place optional 177 return fmt.Sprintf("%.3g", f) 178 default: 179 // Need %f here to kill exponent 180 return fmt.Sprintf("%4.0f", f) 181 } 182 } 183 184 // Format a number of bytes into a display format 185 func FormatSize(sz int64) string { 186 187 suffix, num := FormatBytes(sz) 188 return FormatFloat(num) + suffix 189 } 190 191 // Format a bytes per second transfer rate into a display format 192 func FormatTransferRate(bytesPerSecond int64) string { 193 194 suffix, num := FormatBytes(bytesPerSecond) 195 return fmt.Sprintf("%v%v/s", FormatFloat(num), suffix) 196 } 197 198 // Calculates transfer rates by averaging over n samples 199 type TransferRateCalculator struct { 200 numSamples int 201 samples []int64 // bytesPerSecond samples 202 sampleInsertIdx int 203 } 204 205 func NewTransferRateCalculator(numSamples int) *TransferRateCalculator { 206 return &TransferRateCalculator{numSamples, make([]int64, numSamples), 0} 207 } 208 func (t *TransferRateCalculator) AddSample(bytesPerSecond int64) { 209 t.samples[t.sampleInsertIdx] = bytesPerSecond 210 t.sampleInsertIdx = (t.sampleInsertIdx + 1) % t.numSamples 211 } 212 func (t *TransferRateCalculator) Average() int64 { 213 var sum int64 214 for _, s := range t.samples { 215 sum += s 216 } 217 return sum / int64(t.numSamples) 218 } 219 220 // Search a sorted slice of strings for a specific string 221 // Returns boolean for if found, and either location or insertion point 222 func StringBinarySearch(sortedSlice []string, searchTerm string) (bool, int) { 223 // Convenience method to easily provide boolean of whether to insert or not 224 idx := sort.SearchStrings(sortedSlice, searchTerm) 225 found := idx < len(sortedSlice) && sortedSlice[idx] == searchTerm 226 return found, idx 227 } 228 229 // Remove duplicates from a slice of strings (in place) 230 // Linear to logarithmic time, doesn't change the ordering of the slice 231 // allocates/frees a new map of up to the size of the slice though 232 func StringRemoveDuplicates(s *[]string) { 233 if s == nil || *s == nil { 234 return 235 } 236 uniques := NewStringSet() 237 insertidx := 0 238 for _, x := range *s { 239 if !uniques.Contains(x) { 240 uniques.Add(x) 241 (*s)[insertidx] = x // could do this only when x != insertidx but prob wasteful compare 242 insertidx++ 243 } 244 } 245 // If any were eliminated it will now be shorter 246 *s = (*s)[:insertidx] 247 } 248 249 // Return whether a given filename passes the include / exclude path filters 250 // Only paths that are in includePaths and outside excludePaths are passed 251 // If includePaths is empty that filter always passes and the same with excludePaths 252 // Both path lists support wildcard matches 253 func FilenamePassesIncludeExcludeFilter(filename string, includePaths, excludePaths []string) bool { 254 if len(includePaths) == 0 && len(excludePaths) == 0 { 255 return true 256 } 257 258 // For Win32, becuase git reports files with / separators 259 cleanfilename := filepath.Clean(filename) 260 if len(includePaths) > 0 { 261 matched := false 262 for _, inc := range includePaths { 263 matched, _ = filepath.Match(inc, filename) 264 if !matched && IsWindows() { 265 // Also Win32 match 266 matched, _ = filepath.Match(inc, cleanfilename) 267 } 268 if !matched { 269 // Also support matching a parent directory without a wildcard 270 if strings.HasPrefix(cleanfilename, inc+string(filepath.Separator)) { 271 matched = true 272 } 273 } 274 if matched { 275 break 276 } 277 278 } 279 if !matched { 280 return false 281 } 282 } 283 284 if len(excludePaths) > 0 { 285 for _, ex := range excludePaths { 286 matched, _ := filepath.Match(ex, filename) 287 if !matched && IsWindows() { 288 // Also Win32 match 289 matched, _ = filepath.Match(ex, cleanfilename) 290 } 291 if matched { 292 return false 293 } 294 // Also support matching a parent directory without a wildcard 295 if strings.HasPrefix(cleanfilename, ex+string(filepath.Separator)) { 296 return false 297 } 298 299 } 300 } 301 302 return true 303 304 } 305 306 // Execute 1:n os.exec.Command instances for a list of files, splitting where the command line might 307 // get too long. name is the command name as per exec.Command 308 // Files are appended to the end of the argument list 309 // errorCallback is called for any errors so caller can decide whether to abort 310 func ExecForManyFilesSplitIfRequired(files []string, 311 errorCallback func(args []string, output string, err error) (abort bool), 312 name string, baseargs ...string) { 313 314 // How many characters have we used in base args? 315 baseLen := len(name) 316 for _, arg := range baseargs { 317 // +1 for separator (in practice might be +3 with quoting but we'll allow a little legroom) 318 baseLen += len(arg) + 1 319 } 320 321 lenLeft := GetMaxCommandLineLength() - baseLen - 1 322 argsLeft := GetMaxCommandLineArguments() - len(baseargs) - 1 323 324 if lenLeft <= 0 || argsLeft <= 0 { 325 errorCallback(baseargs, "", 326 fmt.Errorf("Base arguments were too long to include anything else in ExecForManyFilesSplitIfRequired: %v %v", name, baseargs)) 327 return 328 } 329 330 for filesLeft := files; len(filesLeft) > 0; { 331 newargs := baseargs 332 var filesUsed int 333 for _, file := range filesLeft { 334 lenadded := len(file) 335 if strings.Contains(file, " \t") { 336 // 2 for quoting 337 lenadded += 2 338 } 339 if lenadded > lenLeft || argsLeft == 0 { 340 break 341 } 342 argsLeft-- 343 lenLeft -= (lenadded + 1) // +1 for space separator 344 newargs = append(newargs, file) 345 filesUsed++ 346 } 347 // Issue this command 348 cmd := exec.Command(name, newargs...) 349 outp, err := cmd.CombinedOutput() 350 if err != nil { 351 abort := errorCallback(newargs, string(outp), err) 352 if abort { 353 return 354 } 355 } 356 357 if filesUsed == len(filesLeft) { 358 break 359 } else { 360 filesLeft = filesLeft[filesUsed:] 361 } 362 363 } 364 365 } 366 367 // Make a list of filenames expressed relative to the root of the repo relative to the 368 // current working dir. This is useful when needing to call out to git, but the user 369 // may be in a subdir of their repo 370 func MakeRepoFileListRelativeToCwd(repofiles []string) []string { 371 root, _, err := GetRepoRoot() 372 if err != nil { 373 LogError("Unable to get repo root: ", err.Error()) 374 return repofiles 375 } 376 wd, err := os.Getwd() 377 if err != nil { 378 LogError("Unable to get working dir: ", err.Error()) 379 return repofiles 380 } 381 382 // Early-out if working dir is root dir, same result 383 if root == wd { 384 return repofiles 385 } 386 387 var ret []string 388 for _, f := range repofiles { 389 abs := filepath.Join(root, f) 390 rel, err := filepath.Rel(wd, abs) 391 if err != nil { 392 LogErrorf("Unable to convert %v to path relative to working dir %v: %v\n", abs, wd, err.Error()) 393 // Use absolute file instead (longer) 394 ret = append(ret, abs) 395 } else { 396 ret = append(ret, rel) 397 } 398 } 399 400 return ret 401 402 } 403 404 // Are we running on Windows? Need to handle some extra path shenanigans 405 func IsWindows() bool { 406 return runtime.GOOS == "windows" 407 }