aboutsummaryrefslogblamecommitdiffstats
path: root/repository/git_config.go
blob: b46cc69b2b00bf9feef9ba750326da3b7f6d2fc6 (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 {
	cli          gitCli
	localityFlag string
}

func newGitConfig(cli gitCli, global bool) *gitConfig {
	localityFlag := "--local"
	if global {
		localityFlag = "--global"
	}
	return &gitConfig{
		cli:          cli,
		localityFlag: localityFlag,
	}
}

// StoreString store a single key/value pair in the config of the repo
func (gc *gitConfig) StoreString(key string, value string) error {
	_, err := gc.cli.runGitCommand("config", gc.localityFlag, "--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())))
}

// ReadAll read all key/value pair matching the key prefix
func (gc *gitConfig) ReadAll(keyPrefix string) (map[string]string, error) {
	stdout, err := gc.cli.runGitCommand("config", gc.localityFlag, "--includes", "--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.SplitN(line, " ", 2)
		result[parts[0]] = parts[1]
	}

	return result, nil
}

func (gc *gitConfig) ReadString(key string) (string, error) {
	stdout, err := gc.cli.runGitCommand("config", gc.localityFlag, "--includes", "--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 time.Time{}, err
	}
	return ParseTimestamp(value)
}

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

func (gc *gitConfig) unsetAll(keyPrefix string) error {
	_, err := gc.cli.runGitCommand("config", gc.localityFlag, "--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.cli.runGitCommand("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
}