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 }