github.com/atlassian/git-lob@v0.0.0-20150806085256-2386a5ed291a/util/config.go (about)

     1  package util
     2  
     3  import (
     4  	"bufio"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path"
     9  	"path/filepath"
    10  	"regexp"
    11  	"sort"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"github.com/atlassian/git-lob/Godeps/_workspace/src/github.com/mitchellh/go-homedir"
    16  )
    17  
    18  // Options (command line or config file)
    19  // Only general options, command-specific ones dealt with in commands
    20  type Options struct {
    21  	// Help option was requested
    22  	HelpRequested bool
    23  	// Output verbosely (to console & log)
    24  	Verbose bool
    25  	// Output quietly (to console)
    26  	Quiet bool
    27  	// Don't actually perform any tasks
    28  	DryRun bool
    29  	// Never prompt for user input, rely on command line options only
    30  	NonInteractive bool
    31  	// The command to run
    32  	Command string
    33  	// Other value options not converted
    34  	StringOpts map[string]string
    35  	// Other boolean options not converted
    36  	BoolOpts StringSet
    37  	// Other arguments to the command
    38  	Args []string
    39  	// Whether to write output to a log
    40  	LogEnabled bool
    41  	// Log file (optional, defaults to ~/git-lob.log if not specified)
    42  	LogFile string
    43  	// Log verbosely even if main Verbose option is disabled for console
    44  	VerboseLog bool
    45  	// Shared folder in which to store binary files for all repos
    46  	SharedStore string
    47  	// Auto fetch (download) on checkout?
    48  	AutoFetchEnabled bool
    49  	// 'Recent' window in days for fetching all refs (branches/tags) compared to current date
    50  	FetchRefsPeriodDays int
    51  	// 'Recent' window in days for fetching commits on HEAD compared to latest commit date
    52  	FetchCommitsPeriodHEAD int
    53  	// 'Recent' window in days for fetching commits on other branches/tags compared to latest commit date
    54  	FetchCommitsPeriodOther int
    55  	// Retention window in days for refs compared to current date
    56  	RetentionRefsPeriod int
    57  	// Retention window in days for commits on HEAD compared to latest commit date
    58  	RetentionCommitsPeriodHEAD int
    59  	// Retention window in days for commits on other branches/tags compared to latest commit date
    60  	RetentionCommitsPeriodOther int
    61  	// The remote to check for unpushed commits before pruning ('*' means 'any')
    62  	PruneRemote string
    63  	// Whether to always operate prune old in safe mode
    64  	PruneSafeMode bool
    65  	// List of paths to include when fetching
    66  	FetchIncludePaths []string
    67  	// List of paths to exclude when fetching
    68  	FetchExcludePaths []string
    69  	// Size above which we'll try to download deltas on fetch (smart servers only)
    70  	FetchDeltasAboveSize int64
    71  	// Size above which we'll try to upload deltas on push (smart servers only)
    72  	PushDeltasAboveSize int64
    73  	// The command to run over SSH on a remote smart server to push/pull (default "git-lob-server")
    74  	SSHServerCommand string
    75  	// Combination of root .gitconfig and repository config as map
    76  	GitConfig map[string]string
    77  }
    78  
    79  func NewOptions() *Options {
    80  	return &Options{
    81  		StringOpts:                  make(map[string]string),
    82  		BoolOpts:                    NewStringSet(),
    83  		Args:                        make([]string, 0, 5),
    84  		GitConfig:                   make(map[string]string),
    85  		FetchRefsPeriodDays:         30,
    86  		FetchCommitsPeriodHEAD:      7,
    87  		FetchCommitsPeriodOther:     0,
    88  		FetchIncludePaths:           []string{},
    89  		FetchExcludePaths:           []string{},
    90  		FetchDeltasAboveSize:        1024 * 1024,
    91  		PushDeltasAboveSize:         1024 * 1024,
    92  		RetentionRefsPeriod:         30,
    93  		RetentionCommitsPeriodHEAD:  7,
    94  		RetentionCommitsPeriodOther: 0,
    95  		PruneRemote:                 "origin",
    96  		SSHServerCommand:            "git-lob-serve",
    97  	}
    98  }
    99  
   100  // Load config from gitconfig and populate opts
   101  func LoadConfig(opts *Options) {
   102  	configmap := ReadConfig()
   103  	parseConfig(configmap, opts)
   104  }
   105  
   106  // Parse a loaded config map and populate opts
   107  func parseConfig(configmap map[string]string, opts *Options) {
   108  	opts.GitConfig = configmap
   109  
   110  	// Translate our settings to config
   111  	if strings.ToLower(configmap["git-lob.verbose"]) == "true" {
   112  		opts.Verbose = true
   113  	}
   114  	if strings.ToLower(configmap["git-lob.quiet"]) == "true" {
   115  		opts.Quiet = true
   116  	}
   117  	if strings.ToLower(configmap["git-lob.logenabled"]) == "true" {
   118  		opts.LogEnabled = true
   119  	}
   120  	logfile := configmap["git-lob.logfile"]
   121  	if logfile != "" {
   122  		opts.LogFile = logfile
   123  	}
   124  	if strings.ToLower(configmap["git-lob.logverbose"]) == "true" {
   125  		opts.VerboseLog = true
   126  	}
   127  	if sharedStore := configmap["git-lob.sharedstore"]; sharedStore != "" {
   128  		sharedStore = filepath.Clean(sharedStore)
   129  		exists, isDir := FileOrDirExists(sharedStore)
   130  		if exists && !isDir {
   131  			LogErrorf("Invalid path for git-lob.sharedstore: %v\n", sharedStore)
   132  		} else {
   133  			if !exists {
   134  				err := os.MkdirAll(sharedStore, 0755)
   135  				if err != nil {
   136  					LogErrorf("Unable to create path for git-lob.sharedstore: %v\n", sharedStore)
   137  				} else {
   138  					exists = true
   139  					isDir = true
   140  				}
   141  			}
   142  
   143  			if exists && isDir {
   144  				opts.SharedStore = sharedStore
   145  			}
   146  		}
   147  	}
   148  	if strings.ToLower(configmap["git-lob.autofetch"]) == "true" {
   149  		opts.AutoFetchEnabled = true
   150  	}
   151  
   152  	//git-lob.fetch-refs
   153  	//git-lob.fetch-commits-head
   154  	//git-lob.fetch-commits-other default
   155  	if recentrefs := configmap["git-lob.fetch-refs"]; recentrefs != "" {
   156  		n, err := strconv.ParseInt(recentrefs, 10, 0)
   157  		if err == nil {
   158  			opts.FetchRefsPeriodDays = int(n)
   159  		}
   160  	}
   161  	if recent := configmap["git-lob.fetch-commits-head"]; recent != "" {
   162  		n, err := strconv.ParseInt(recent, 10, 0)
   163  		if err == nil {
   164  			opts.FetchCommitsPeriodHEAD = int(n)
   165  		}
   166  	}
   167  	if recent := configmap["git-lob.fetch-commits-other"]; recent != "" {
   168  		n, err := strconv.ParseInt(recent, 10, 0)
   169  		if err == nil {
   170  			opts.FetchCommitsPeriodOther = int(n)
   171  		}
   172  	}
   173  	//git-lob.retention-period-refs
   174  	//git-lob.retention-period-head
   175  	//git-lob.retention-period-other
   176  	if recentrefs := configmap["git-lob.retention-period-refs"]; recentrefs != "" {
   177  		n, err := strconv.ParseInt(recentrefs, 10, 0)
   178  		if err == nil {
   179  			opts.RetentionRefsPeriod = int(n)
   180  		}
   181  	}
   182  	if recent := configmap["git-lob.retention-period-head"]; recent != "" {
   183  		n, err := strconv.ParseInt(recent, 10, 0)
   184  		if err == nil {
   185  			opts.RetentionCommitsPeriodHEAD = int(n)
   186  		}
   187  	}
   188  	if recent := configmap["git-lob.retention-period-other"]; recent != "" {
   189  		n, err := strconv.ParseInt(recent, 10, 0)
   190  		if err == nil {
   191  			opts.RetentionCommitsPeriodOther = int(n)
   192  		}
   193  	}
   194  	if fetchincludes := configmap["git-lob.fetch-include"]; fetchincludes != "" {
   195  		// Split on comma
   196  		for _, inc := range strings.Split(fetchincludes, ",") {
   197  			inc = strings.TrimSpace(inc)
   198  			opts.FetchIncludePaths = append(opts.FetchIncludePaths, inc)
   199  		}
   200  	}
   201  	if fetchexcludes := configmap["git-lob.fetch-exclude"]; fetchexcludes != "" {
   202  		// Split on comma
   203  		for _, ex := range strings.Split(fetchexcludes, ",") {
   204  			ex = strings.TrimSpace(ex)
   205  			opts.FetchExcludePaths = append(opts.FetchExcludePaths, ex)
   206  		}
   207  	}
   208  	if pruneremote := strings.TrimSpace(configmap["git-lob.prune-check-remote"]); pruneremote != "" {
   209  		opts.PruneRemote = pruneremote
   210  	}
   211  	if strings.ToLower(configmap["git-lob.prune-safe"]) == "true" {
   212  		opts.PruneSafeMode = true
   213  	}
   214  	if sshserver := configmap["git-lob.ssh-server"]; sshserver != "" {
   215  		opts.SSHServerCommand = sshserver
   216  	}
   217  
   218  	if recent := configmap["git-lob.fetch-delta-size"]; recent != "" {
   219  		n, err := strconv.ParseInt(recent, 10, 64)
   220  		if err == nil {
   221  			opts.FetchDeltasAboveSize = int64(n)
   222  		}
   223  	}
   224  	if recent := configmap["git-lob.push-delta-size"]; recent != "" {
   225  		n, err := strconv.ParseInt(recent, 10, 64)
   226  		if err == nil {
   227  			opts.PushDeltasAboveSize = int64(n)
   228  		}
   229  	}
   230  
   231  }
   232  
   233  // Read .gitconfig / .git/config for specific options to override
   234  // Returns a map of setting=value, where group levels are indicated by dot-notation
   235  // e.g. git-lob.logfile=blah
   236  // all keys are converted to lower case for easier matching
   237  func ReadConfig() map[string]string {
   238  	// Don't call out to 'git config' to read file, that's slower and forces a dependency on git
   239  	// which we may not want to have (e.g. support for libgit clients)
   240  	// Read files directly, it's a simple format anyway
   241  
   242  	// TODO system git config?
   243  
   244  	var ret map[string]string = nil
   245  
   246  	// User config
   247  	home, err := homedir.Dir()
   248  	if err != nil {
   249  		LogError("Unable to access user home directory: ", err.Error())
   250  		// continue anyway
   251  	} else {
   252  		userConfigFile := path.Join(home, ".gitconfig")
   253  		userConfig, err := ReadConfigFile(userConfigFile)
   254  		if err == nil {
   255  			if ret == nil {
   256  				ret = userConfig
   257  			} else {
   258  				for key, val := range userConfig {
   259  					ret[key] = val
   260  				}
   261  			}
   262  		}
   263  	}
   264  
   265  	// repo config
   266  	gitDir := GetGitDir()
   267  	repoConfigFile := path.Join(gitDir, "config")
   268  	repoConfig, err := ReadConfigFile(repoConfigFile)
   269  	if err == nil {
   270  		if ret == nil {
   271  			ret = repoConfig
   272  		} else {
   273  			for key, val := range repoConfig {
   274  				ret[key] = val
   275  			}
   276  		}
   277  	}
   278  
   279  	if ret == nil {
   280  		ret = make(map[string]string)
   281  	}
   282  
   283  	return ret
   284  
   285  }
   286  
   287  // Read a specific .gitconfig-formatted config file
   288  // Returns a map of setting=value, where group levels are indicated by dot-notation
   289  // e.g. git-lob.logfile=blah
   290  // all keys are converted to lower case for easier matching
   291  func ReadConfigFile(filepath string) (map[string]string, error) {
   292  	f, err := os.OpenFile(filepath, os.O_RDONLY, 0644)
   293  	if err != nil {
   294  		return make(map[string]string), err
   295  	}
   296  	defer f.Close()
   297  
   298  	// Need the directory for relative path includes
   299  	dir := path.Dir(filepath)
   300  	return ReadConfigStream(f, dir)
   301  
   302  }
   303  func ReadConfigStream(in io.Reader, dir string) (map[string]string, error) {
   304  	ret := make(map[string]string, 10)
   305  	sectionRegex := regexp.MustCompile(`^\[(.*)\]$`)                    // simple section regex ie [section]
   306  	namedSectionRegex := regexp.MustCompile(`^\[(.*)\s+\"(.*)\"\s*\]$`) // named section regex ie [section "name"]
   307  
   308  	scanner := bufio.NewScanner(in)
   309  	var currentSection string
   310  	var currentSectionName string
   311  	for scanner.Scan() {
   312  		// Reads lines by default, \n is already stripped
   313  		line := strings.TrimSpace(scanner.Text())
   314  		// Detect comments - discard any of the line after the comment but keep anything before
   315  		commentPos := strings.IndexAny(line, "#;")
   316  		if commentPos != -1 {
   317  			// skip comments
   318  			if commentPos == 0 {
   319  				continue
   320  			} else {
   321  				// just strip rest of line after the comment
   322  				line = strings.TrimSpace(line[0:commentPos])
   323  				if len(line) == 0 {
   324  					continue
   325  				}
   326  			}
   327  		}
   328  
   329  		// Check for sections
   330  		if secmatch := sectionRegex.FindStringSubmatch(line); secmatch != nil {
   331  			// named section? [section "name"]
   332  			if namedsecmatch := namedSectionRegex.FindStringSubmatch(line); namedsecmatch != nil {
   333  				// Named section
   334  				currentSection = namedsecmatch[1]
   335  				currentSectionName = namedsecmatch[2]
   336  
   337  			} else {
   338  				// Normal section
   339  				currentSection = secmatch[1]
   340  				currentSectionName = ""
   341  			}
   342  			continue
   343  		}
   344  
   345  		// Otherwise, probably a standard setting
   346  		equalPos := strings.Index(line, "=")
   347  		if equalPos != -1 {
   348  			name := strings.TrimSpace(line[0:equalPos])
   349  			value := strings.TrimSpace(line[equalPos+1:])
   350  			if currentSection != "" {
   351  				if currentSectionName != "" {
   352  					name = fmt.Sprintf("%v.%v.%v", currentSection, currentSectionName, name)
   353  				} else {
   354  					name = fmt.Sprintf("%v.%v", currentSection, name)
   355  				}
   356  			}
   357  			// convert key to lower case for easier matching
   358  			name = strings.ToLower(name)
   359  
   360  			// Check for includes and expand immediately
   361  			if name == "include.path" {
   362  				// if this is a relative, prepend containing dir context
   363  				includeFile := value
   364  				if !path.IsAbs(includeFile) {
   365  					includeFile = path.Join(dir, includeFile)
   366  				}
   367  				includemap, err := ReadConfigFile(includeFile)
   368  				if err == nil {
   369  					for key, value := range includemap {
   370  						ret[key] = value
   371  					}
   372  				}
   373  			} else {
   374  				ret[name] = value
   375  			}
   376  		}
   377  
   378  	}
   379  	if scanner.Err() != nil {
   380  		// Problem (other than io.EOF)
   381  		// return content we read up to here anyway
   382  		return ret, scanner.Err()
   383  	}
   384  
   385  	return ret, nil
   386  
   387  }
   388  
   389  // Write a .gitconfig-style config file
   390  // Takes a map of setting=value, where group levels are indicated by dot-notation
   391  // e.g. git-lob.logfile=blah
   392  // Note: overwrites whole file & loses comments if you ReadConfigFile then WriteConfigFile
   393  // only intended for internal use, don't use on user-edited files
   394  // This is NOT a merge-on-write user-friendly config updater (like SourceTree has)
   395  func WriteConfigFile(filepath string, contents map[string]string) error {
   396  	f, err := os.OpenFile(filepath, os.O_TRUNC|os.O_WRONLY, 0644)
   397  	if err != nil {
   398  		return err
   399  	}
   400  	defer f.Close()
   401  
   402  	return WriteConfigStream(f, contents)
   403  
   404  }
   405  func WriteConfigStream(out io.Writer, contents map[string]string) error {
   406  	// We need to iterate over content IN ORDER so that we can group correctly
   407  	// golang map iteration is not ordered though so we need to sort keys & iterate on those
   408  	keys := make([]string, 0, len(contents))
   409  	for key, _ := range contents {
   410  		keys = append(keys, key)
   411  	}
   412  	sort.Strings(keys)
   413  	lastGroup := ""
   414  	for _, key := range keys {
   415  		val := contents[key]
   416  		splitkey := strings.SplitN(key, ".", 1)
   417  		var group string
   418  		if len(splitkey) > 1 {
   419  			group = splitkey[0]
   420  			if strings.ContainsAny(group, " \t") {
   421  				group = fmt.Sprintf("\"%v\"", group)
   422  			}
   423  			key = splitkey[1]
   424  		}
   425  		if group != lastGroup {
   426  			_, err := out.Write([]byte(fmt.Sprintf("[%v]\n", group)))
   427  			if err != nil {
   428  				return err
   429  			}
   430  			lastGroup = group
   431  		}
   432  
   433  		if group != "" {
   434  			// Indent values in group
   435  			out.Write([]byte{'\t'})
   436  		}
   437  		_, err := out.Write([]byte(fmt.Sprintf("%v = %v\n", key, val)))
   438  		if err != nil {
   439  			return err
   440  		}
   441  
   442  	}
   443  	return nil
   444  
   445  }