aboutsummaryrefslogblamecommitdiffstats
path: root/repository/config_git.go
blob: 67ca34363b1743a6a3b10692a6f71fd5f2518474 (plain) (tree)
1
2
3
4
5
6
7
8



                  
                

                 
              




                                 

                           
                       
                                                   

 
                                                          
                                  
                   
                                          
         

                                                              


                                                                                            

                                                          



                                                                      

                                                                  


                  







                                                                        

                                                                           
                                                                     































                                                                                 
                                                            































                                                                                 




                                                                    
                                    

 
                                                        
                                                                    



                                                       
                                                               
























































                                                                                             

























                                                                               
                                                      






                                                           



                                 
                                             
 
package repository

import (
	"fmt"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/blang/semver"
	"github.com/pkg/errors"
)

var _ Config = &gitConfig{}

type gitConfig struct {
	execFn func(args ...string) (string, error)
}

func newGitConfig(repo *GitRepo, global bool) *gitConfig {
	configCmdFlag := "--local"
	if global {
		configCmdFlag = "--global"
	}
	return &gitConfig{
		execFn: func(args ...string) (string, error) {
			if len(args) > 0 && args[0] == "config" {
				args = append([]string{args[0], configCmdFlag}, args[1:]...)
			}
			return repo.runGitCommand(args...)
		},
	}
}

// StoreConfig store a single key/value pair in the config of the repo
func (gc *gitConfig) StoreString(key string, value string) error {
	_, err := gc.execFn("config", "--replace-all", key, value)
	return err
}

func (gc *gitConfig) StoreBool(key string, value bool) error {
	return gc.StoreString(key, strconv.FormatBool(value))
}

func (gc *gitConfig) StoreTimestamp(key string, value time.Time) error {
	return gc.StoreString(key, strconv.Itoa(int(value.Unix())))
}

// ReadConfigs read all key/value pair matching the key prefix
func (gc *gitConfig) ReadAll(keyPrefix string) (map[string]string, error) {
	stdout, err := gc.execFn("config", "--get-regexp", keyPrefix)

	//   / \
	//  / ! \
	// -------
	//
	// There can be a legitimate error here, but I see no portable way to
	// distinguish them from the git error that say "no matching value exist"
	if err != nil {
		return nil, nil
	}

	lines := strings.Split(stdout, "\n")

	result := make(map[string]string, len(lines))

	for _, line := range lines {
		if strings.TrimSpace(line) == "" {
			continue
		}

		parts := strings.Fields(line)
		if len(parts) != 2 {
			return nil, fmt.Errorf("bad git config: %s", line)
		}

		result[parts[0]] = parts[1]
	}

	return result, nil
}

func (gc *gitConfig) ReadString(key string) (string, error) {
	stdout, err := gc.execFn("config", "--get-all", key)

	//   / \
	//  / ! \
	// -------
	//
	// There can be a legitimate error here, but I see no portable way to
	// distinguish them from the git error that say "no matching value exist"
	if err != nil {
		return "", ErrNoConfigEntry
	}

	lines := strings.Split(stdout, "\n")

	if len(lines) == 0 {
		return "", ErrNoConfigEntry
	}
	if len(lines) > 1 {
		return "", ErrMultipleConfigEntry
	}

	return lines[0], nil
}

func (gc *gitConfig) ReadBool(key string) (bool, error) {
	val, err := gc.ReadString(key)
	if err != nil {
		return false, err
	}

	return strconv.ParseBool(val)
}

func (gc *gitConfig) ReadTimestamp(key string) (*time.Time, error) {
	value, err := gc.ReadString(key)
	if err != nil {
		return nil, err
	}
	return parseTimestamp(value)
}

func (gc *gitConfig) rmSection(keyPrefix string) error {
	_, err := gc.execFn("config", "--remove-section", keyPrefix)
	return err
}

func (gc *gitConfig) unsetAll(keyPrefix string) error {
	_, err := gc.execFn("config", "--unset-all", keyPrefix)
	return err
}

// return keyPrefix section
// example: sectionFromKey(a.b.c.d) return a.b.c
func sectionFromKey(keyPrefix string) string {
	s := strings.Split(keyPrefix, ".")
	if len(s) == 1 {
		return keyPrefix
	}

	return strings.Join(s[:len(s)-1], ".")
}

// rmConfigs with git version lesser than 2.18
func (gc *gitConfig) rmConfigsGitVersionLT218(keyPrefix string) error {
	// try to remove key/value pair by key
	err := gc.unsetAll(keyPrefix)
	if err != nil {
		return gc.rmSection(keyPrefix)
	}

	m, err := gc.ReadAll(sectionFromKey(keyPrefix))
	if err != nil {
		return err
	}

	// if section doesn't have any left key/value remove the section
	if len(m) == 0 {
		return gc.rmSection(sectionFromKey(keyPrefix))
	}

	return nil
}

// RmConfigs remove all key/value pair matching the key prefix
func (gc *gitConfig) RemoveAll(keyPrefix string) error {
	// starting from git 2.18.0 sections are automatically deleted when the last existing
	// key/value is removed. Before 2.18.0 we should remove the section
	// see https://github.com/git/git/blob/master/Documentation/RelNotes/2.18.0.txt#L379
	lt218, err := gc.gitVersionLT218()
	if err != nil {
		return errors.Wrap(err, "getting git version")
	}

	if lt218 {
		return gc.rmConfigsGitVersionLT218(keyPrefix)
	}

	err = gc.unsetAll(keyPrefix)
	if err != nil {
		return gc.rmSection(keyPrefix)
	}

	return nil
}

func (gc *gitConfig) gitVersion() (*semver.Version, error) {
	versionOut, err := gc.execFn("version")
	if err != nil {
		return nil, err
	}
	return parseGitVersion(versionOut)
}

func parseGitVersion(versionOut string) (*semver.Version, error) {
	// extract the version and truncate potential bad parts
	// ex: 2.23.0.rc1 instead of 2.23.0-rc1
	r := regexp.MustCompile(`(\d+\.){1,2}\d+`)

	extracted := r.FindString(versionOut)
	if extracted == "" {
		return nil, fmt.Errorf("unreadable git version %s", versionOut)
	}

	version, err := semver.Make(extracted)
	if err != nil {
		return nil, err
	}

	return &version, nil
}

func (gc *gitConfig) gitVersionLT218() (bool, error) {
	version, err := gc.gitVersion()
	if err != nil {
		return false, err
	}

	version218string := "2.18.0"
	gitVersion218, err := semver.Make(version218string)
	if err != nil {
		return false, err
	}

	return version.LT(gitVersion218), nil
}