1// Package config contains the abstraction of multiple config files 2package config 3 4import ( 5 "bytes" 6 "errors" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "os" 11 "path/filepath" 12 "sort" 13 "strconv" 14 15 "github.com/go-git/go-billy/v5/osfs" 16 "github.com/go-git/go-git/v5/internal/url" 17 format "github.com/go-git/go-git/v5/plumbing/format/config" 18 "github.com/mitchellh/go-homedir" 19) 20 21const ( 22 // DefaultFetchRefSpec is the default refspec used for fetch. 23 DefaultFetchRefSpec = "+refs/heads/*:refs/remotes/%s/*" 24 // DefaultPushRefSpec is the default refspec used for push. 25 DefaultPushRefSpec = "refs/heads/*:refs/heads/*" 26) 27 28// ConfigStorer generic storage of Config object 29type ConfigStorer interface { 30 Config() (*Config, error) 31 SetConfig(*Config) error 32} 33 34var ( 35 ErrInvalid = errors.New("config invalid key in remote or branch") 36 ErrRemoteConfigNotFound = errors.New("remote config not found") 37 ErrRemoteConfigEmptyURL = errors.New("remote config: empty URL") 38 ErrRemoteConfigEmptyName = errors.New("remote config: empty name") 39) 40 41// Scope defines the scope of a config file, such as local, global or system. 42type Scope int 43 44// Available ConfigScope's 45const ( 46 LocalScope Scope = iota 47 GlobalScope 48 SystemScope 49) 50 51// Config contains the repository configuration 52// https://www.kernel.org/pub/software/scm/git/docs/git-config.html#FILES 53type Config struct { 54 Core struct { 55 // IsBare if true this repository is assumed to be bare and has no 56 // working directory associated with it. 57 IsBare bool 58 // Worktree is the path to the root of the working tree. 59 Worktree string 60 // CommentChar is the character indicating the start of a 61 // comment for commands like commit and tag 62 CommentChar string 63 } 64 65 User struct { 66 // Name is the personal name of the author and the commiter of a commit. 67 Name string 68 // Email is the email of the author and the commiter of a commit. 69 Email string 70 } 71 72 Author struct { 73 // Name is the personal name of the author of a commit. 74 Name string 75 // Email is the email of the author of a commit. 76 Email string 77 } 78 79 Committer struct { 80 // Name is the personal name of the commiter of a commit. 81 Name string 82 // Email is the email of the the commiter of a commit. 83 Email string 84 } 85 86 Pack struct { 87 // Window controls the size of the sliding window for delta 88 // compression. The default is 10. A value of 0 turns off 89 // delta compression entirely. 90 Window uint 91 } 92 93 Init struct { 94 // DefaultBranch Allows overriding the default branch name 95 // e.g. when initializing a new repository or when cloning 96 // an empty repository. 97 DefaultBranch string 98 } 99 100 // Remotes list of repository remotes, the key of the map is the name 101 // of the remote, should equal to RemoteConfig.Name. 102 Remotes map[string]*RemoteConfig 103 // Submodules list of repository submodules, the key of the map is the name 104 // of the submodule, should equal to Submodule.Name. 105 Submodules map[string]*Submodule 106 // Branches list of branches, the key is the branch name and should 107 // equal Branch.Name 108 Branches map[string]*Branch 109 // URLs list of url rewrite rules, if repo url starts with URL.InsteadOf value, it will be replaced with the 110 // key instead. 111 URLs map[string]*URL 112 // Raw contains the raw information of a config file. The main goal is 113 // preserve the parsed information from the original format, to avoid 114 // dropping unsupported fields. 115 Raw *format.Config 116} 117 118// NewConfig returns a new empty Config. 119func NewConfig() *Config { 120 config := &Config{ 121 Remotes: make(map[string]*RemoteConfig), 122 Submodules: make(map[string]*Submodule), 123 Branches: make(map[string]*Branch), 124 URLs: make(map[string]*URL), 125 Raw: format.New(), 126 } 127 128 config.Pack.Window = DefaultPackWindow 129 130 return config 131} 132 133// ReadConfig reads a config file from a io.Reader. 134func ReadConfig(r io.Reader) (*Config, error) { 135 b, err := ioutil.ReadAll(r) 136 if err != nil { 137 return nil, err 138 } 139 140 cfg := NewConfig() 141 if err = cfg.Unmarshal(b); err != nil { 142 return nil, err 143 } 144 145 return cfg, nil 146} 147 148// LoadConfig loads a config file from a given scope. The returned Config, 149// contains exclusively information fom the given scope. If couldn't find a 150// config file to the given scope, a empty one is returned. 151func LoadConfig(scope Scope) (*Config, error) { 152 if scope == LocalScope { 153 return nil, fmt.Errorf("LocalScope should be read from the a ConfigStorer.") 154 } 155 156 files, err := Paths(scope) 157 if err != nil { 158 return nil, err 159 } 160 161 for _, file := range files { 162 f, err := osfs.Default.Open(file) 163 if err != nil { 164 if os.IsNotExist(err) { 165 continue 166 } 167 168 return nil, err 169 } 170 171 defer f.Close() 172 return ReadConfig(f) 173 } 174 175 return NewConfig(), nil 176} 177 178// Paths returns the config file location for a given scope. 179func Paths(scope Scope) ([]string, error) { 180 var files []string 181 switch scope { 182 case GlobalScope: 183 xdg := os.Getenv("XDG_CONFIG_HOME") 184 if xdg != "" { 185 files = append(files, filepath.Join(xdg, "git/config")) 186 } 187 188 home, err := homedir.Dir() 189 if err != nil { 190 return nil, err 191 } 192 193 files = append(files, 194 filepath.Join(home, ".gitconfig"), 195 filepath.Join(home, ".config/git/config"), 196 ) 197 case SystemScope: 198 files = append(files, "/etc/gitconfig") 199 } 200 201 return files, nil 202} 203 204// Validate validates the fields and sets the default values. 205func (c *Config) Validate() error { 206 for name, r := range c.Remotes { 207 if r.Name != name { 208 return ErrInvalid 209 } 210 211 if err := r.Validate(); err != nil { 212 return err 213 } 214 } 215 216 for name, b := range c.Branches { 217 if b.Name != name { 218 return ErrInvalid 219 } 220 221 if err := b.Validate(); err != nil { 222 return err 223 } 224 } 225 226 return nil 227} 228 229const ( 230 remoteSection = "remote" 231 submoduleSection = "submodule" 232 branchSection = "branch" 233 coreSection = "core" 234 packSection = "pack" 235 userSection = "user" 236 authorSection = "author" 237 committerSection = "committer" 238 initSection = "init" 239 urlSection = "url" 240 fetchKey = "fetch" 241 urlKey = "url" 242 bareKey = "bare" 243 worktreeKey = "worktree" 244 commentCharKey = "commentChar" 245 windowKey = "window" 246 mergeKey = "merge" 247 rebaseKey = "rebase" 248 nameKey = "name" 249 emailKey = "email" 250 defaultBranchKey = "defaultBranch" 251 252 // DefaultPackWindow holds the number of previous objects used to 253 // generate deltas. The value 10 is the same used by git command. 254 DefaultPackWindow = uint(10) 255) 256 257// Unmarshal parses a git-config file and stores it. 258func (c *Config) Unmarshal(b []byte) error { 259 r := bytes.NewBuffer(b) 260 d := format.NewDecoder(r) 261 262 c.Raw = format.New() 263 if err := d.Decode(c.Raw); err != nil { 264 return err 265 } 266 267 c.unmarshalCore() 268 c.unmarshalUser() 269 c.unmarshalInit() 270 if err := c.unmarshalPack(); err != nil { 271 return err 272 } 273 unmarshalSubmodules(c.Raw, c.Submodules) 274 275 if err := c.unmarshalBranches(); err != nil { 276 return err 277 } 278 279 if err := c.unmarshalURLs(); err != nil { 280 return err 281 } 282 283 return c.unmarshalRemotes() 284} 285 286func (c *Config) unmarshalCore() { 287 s := c.Raw.Section(coreSection) 288 if s.Options.Get(bareKey) == "true" { 289 c.Core.IsBare = true 290 } 291 292 c.Core.Worktree = s.Options.Get(worktreeKey) 293 c.Core.CommentChar = s.Options.Get(commentCharKey) 294} 295 296func (c *Config) unmarshalUser() { 297 s := c.Raw.Section(userSection) 298 c.User.Name = s.Options.Get(nameKey) 299 c.User.Email = s.Options.Get(emailKey) 300 301 s = c.Raw.Section(authorSection) 302 c.Author.Name = s.Options.Get(nameKey) 303 c.Author.Email = s.Options.Get(emailKey) 304 305 s = c.Raw.Section(committerSection) 306 c.Committer.Name = s.Options.Get(nameKey) 307 c.Committer.Email = s.Options.Get(emailKey) 308} 309 310func (c *Config) unmarshalPack() error { 311 s := c.Raw.Section(packSection) 312 window := s.Options.Get(windowKey) 313 if window == "" { 314 c.Pack.Window = DefaultPackWindow 315 } else { 316 winUint, err := strconv.ParseUint(window, 10, 32) 317 if err != nil { 318 return err 319 } 320 c.Pack.Window = uint(winUint) 321 } 322 return nil 323} 324 325func (c *Config) unmarshalRemotes() error { 326 s := c.Raw.Section(remoteSection) 327 for _, sub := range s.Subsections { 328 r := &RemoteConfig{} 329 if err := r.unmarshal(sub); err != nil { 330 return err 331 } 332 333 c.Remotes[r.Name] = r 334 } 335 336 // Apply insteadOf url rules 337 for _, r := range c.Remotes { 338 r.applyURLRules(c.URLs) 339 } 340 341 return nil 342} 343 344func (c *Config) unmarshalURLs() error { 345 s := c.Raw.Section(urlSection) 346 for _, sub := range s.Subsections { 347 r := &URL{} 348 if err := r.unmarshal(sub); err != nil { 349 return err 350 } 351 352 c.URLs[r.Name] = r 353 } 354 355 return nil 356} 357 358func unmarshalSubmodules(fc *format.Config, submodules map[string]*Submodule) { 359 s := fc.Section(submoduleSection) 360 for _, sub := range s.Subsections { 361 m := &Submodule{} 362 m.unmarshal(sub) 363 364 if m.Validate() == ErrModuleBadPath { 365 continue 366 } 367 368 submodules[m.Name] = m 369 } 370} 371 372func (c *Config) unmarshalBranches() error { 373 bs := c.Raw.Section(branchSection) 374 for _, sub := range bs.Subsections { 375 b := &Branch{} 376 377 if err := b.unmarshal(sub); err != nil { 378 return err 379 } 380 381 c.Branches[b.Name] = b 382 } 383 return nil 384} 385 386func (c *Config) unmarshalInit() { 387 s := c.Raw.Section(initSection) 388 c.Init.DefaultBranch = s.Options.Get(defaultBranchKey) 389} 390 391// Marshal returns Config encoded as a git-config file. 392func (c *Config) Marshal() ([]byte, error) { 393 c.marshalCore() 394 c.marshalUser() 395 c.marshalPack() 396 c.marshalRemotes() 397 c.marshalSubmodules() 398 c.marshalBranches() 399 c.marshalURLs() 400 c.marshalInit() 401 402 buf := bytes.NewBuffer(nil) 403 if err := format.NewEncoder(buf).Encode(c.Raw); err != nil { 404 return nil, err 405 } 406 407 return buf.Bytes(), nil 408} 409 410func (c *Config) marshalCore() { 411 s := c.Raw.Section(coreSection) 412 s.SetOption(bareKey, fmt.Sprintf("%t", c.Core.IsBare)) 413 414 if c.Core.Worktree != "" { 415 s.SetOption(worktreeKey, c.Core.Worktree) 416 } 417} 418 419func (c *Config) marshalUser() { 420 s := c.Raw.Section(userSection) 421 if c.User.Name != "" { 422 s.SetOption(nameKey, c.User.Name) 423 } 424 425 if c.User.Email != "" { 426 s.SetOption(emailKey, c.User.Email) 427 } 428 429 s = c.Raw.Section(authorSection) 430 if c.Author.Name != "" { 431 s.SetOption(nameKey, c.Author.Name) 432 } 433 434 if c.Author.Email != "" { 435 s.SetOption(emailKey, c.Author.Email) 436 } 437 438 s = c.Raw.Section(committerSection) 439 if c.Committer.Name != "" { 440 s.SetOption(nameKey, c.Committer.Name) 441 } 442 443 if c.Committer.Email != "" { 444 s.SetOption(emailKey, c.Committer.Email) 445 } 446} 447 448func (c *Config) marshalPack() { 449 s := c.Raw.Section(packSection) 450 if c.Pack.Window != DefaultPackWindow { 451 s.SetOption(windowKey, fmt.Sprintf("%d", c.Pack.Window)) 452 } 453} 454 455func (c *Config) marshalRemotes() { 456 s := c.Raw.Section(remoteSection) 457 newSubsections := make(format.Subsections, 0, len(c.Remotes)) 458 added := make(map[string]bool) 459 for _, subsection := range s.Subsections { 460 if remote, ok := c.Remotes[subsection.Name]; ok { 461 newSubsections = append(newSubsections, remote.marshal()) 462 added[subsection.Name] = true 463 } 464 } 465 466 remoteNames := make([]string, 0, len(c.Remotes)) 467 for name := range c.Remotes { 468 remoteNames = append(remoteNames, name) 469 } 470 471 sort.Strings(remoteNames) 472 473 for _, name := range remoteNames { 474 if !added[name] { 475 newSubsections = append(newSubsections, c.Remotes[name].marshal()) 476 } 477 } 478 479 s.Subsections = newSubsections 480} 481 482func (c *Config) marshalSubmodules() { 483 s := c.Raw.Section(submoduleSection) 484 s.Subsections = make(format.Subsections, len(c.Submodules)) 485 486 var i int 487 for _, r := range c.Submodules { 488 section := r.marshal() 489 // the submodule section at config is a subset of the .gitmodule file 490 // we should remove the non-valid options for the config file. 491 section.RemoveOption(pathKey) 492 s.Subsections[i] = section 493 i++ 494 } 495} 496 497func (c *Config) marshalBranches() { 498 s := c.Raw.Section(branchSection) 499 newSubsections := make(format.Subsections, 0, len(c.Branches)) 500 added := make(map[string]bool) 501 for _, subsection := range s.Subsections { 502 if branch, ok := c.Branches[subsection.Name]; ok { 503 newSubsections = append(newSubsections, branch.marshal()) 504 added[subsection.Name] = true 505 } 506 } 507 508 branchNames := make([]string, 0, len(c.Branches)) 509 for name := range c.Branches { 510 branchNames = append(branchNames, name) 511 } 512 513 sort.Strings(branchNames) 514 515 for _, name := range branchNames { 516 if !added[name] { 517 newSubsections = append(newSubsections, c.Branches[name].marshal()) 518 } 519 } 520 521 s.Subsections = newSubsections 522} 523 524func (c *Config) marshalURLs() { 525 s := c.Raw.Section(urlSection) 526 s.Subsections = make(format.Subsections, len(c.URLs)) 527 528 var i int 529 for _, r := range c.URLs { 530 section := r.marshal() 531 // the submodule section at config is a subset of the .gitmodule file 532 // we should remove the non-valid options for the config file. 533 s.Subsections[i] = section 534 i++ 535 } 536} 537 538func (c *Config) marshalInit() { 539 s := c.Raw.Section(initSection) 540 if c.Init.DefaultBranch != "" { 541 s.SetOption(defaultBranchKey, c.Init.DefaultBranch) 542 } 543} 544 545// RemoteConfig contains the configuration for a given remote repository. 546type RemoteConfig struct { 547 // Name of the remote 548 Name string 549 // URLs the URLs of a remote repository. It must be non-empty. Fetch will 550 // always use the first URL, while push will use all of them. 551 URLs []string 552 553 // insteadOfRulesApplied have urls been modified 554 insteadOfRulesApplied bool 555 // originalURLs are the urls before applying insteadOf rules 556 originalURLs []string 557 558 // Fetch the default set of "refspec" for fetch operation 559 Fetch []RefSpec 560 561 // raw representation of the subsection, filled by marshal or unmarshal are 562 // called 563 raw *format.Subsection 564} 565 566// Validate validates the fields and sets the default values. 567func (c *RemoteConfig) Validate() error { 568 if c.Name == "" { 569 return ErrRemoteConfigEmptyName 570 } 571 572 if len(c.URLs) == 0 { 573 return ErrRemoteConfigEmptyURL 574 } 575 576 for _, r := range c.Fetch { 577 if err := r.Validate(); err != nil { 578 return err 579 } 580 } 581 582 if len(c.Fetch) == 0 { 583 c.Fetch = []RefSpec{RefSpec(fmt.Sprintf(DefaultFetchRefSpec, c.Name))} 584 } 585 586 return nil 587} 588 589func (c *RemoteConfig) unmarshal(s *format.Subsection) error { 590 c.raw = s 591 592 fetch := []RefSpec{} 593 for _, f := range c.raw.Options.GetAll(fetchKey) { 594 rs := RefSpec(f) 595 if err := rs.Validate(); err != nil { 596 return err 597 } 598 599 fetch = append(fetch, rs) 600 } 601 602 c.Name = c.raw.Name 603 c.URLs = append([]string(nil), c.raw.Options.GetAll(urlKey)...) 604 c.Fetch = fetch 605 606 return nil 607} 608 609func (c *RemoteConfig) marshal() *format.Subsection { 610 if c.raw == nil { 611 c.raw = &format.Subsection{} 612 } 613 614 c.raw.Name = c.Name 615 if len(c.URLs) == 0 { 616 c.raw.RemoveOption(urlKey) 617 } else { 618 urls := c.URLs 619 if c.insteadOfRulesApplied { 620 urls = c.originalURLs 621 } 622 623 c.raw.SetOption(urlKey, urls...) 624 } 625 626 if len(c.Fetch) == 0 { 627 c.raw.RemoveOption(fetchKey) 628 } else { 629 var values []string 630 for _, rs := range c.Fetch { 631 values = append(values, rs.String()) 632 } 633 634 c.raw.SetOption(fetchKey, values...) 635 } 636 637 return c.raw 638} 639 640func (c *RemoteConfig) IsFirstURLLocal() bool { 641 return url.IsLocalEndpoint(c.URLs[0]) 642} 643 644func (c *RemoteConfig) applyURLRules(urlRules map[string]*URL) { 645 // save original urls 646 originalURLs := make([]string, len(c.URLs)) 647 copy(originalURLs, c.URLs) 648 649 for i, url := range c.URLs { 650 if matchingURLRule := findLongestInsteadOfMatch(url, urlRules); matchingURLRule != nil { 651 c.URLs[i] = matchingURLRule.ApplyInsteadOf(c.URLs[i]) 652 c.insteadOfRulesApplied = true 653 } 654 } 655 656 if c.insteadOfRulesApplied { 657 c.originalURLs = originalURLs 658 } 659} 660