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  }