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