github.com/purpleclay/gitz@v0.8.2-0.20240515052600-43f80eea2fe1/config.go (about)

     1  package git
     2  
     3  import (
     4  	"fmt"
     5  	"strings"
     6  	"unicode"
     7  )
     8  
     9  // ErrInvalidConfigPath is raised when a config setting is to be accessed
    10  // with an invalid config path
    11  type ErrInvalidConfigPath struct {
    12  	// Path to the config setting
    13  	Path string
    14  
    15  	// Position of the first offending character within the path
    16  	Position int
    17  
    18  	// Reason why the path is invalid
    19  	Reason string
    20  }
    21  
    22  // Error returns a friendly formatted message of the current error
    23  func (e ErrInvalidConfigPath) Error() string {
    24  	var buf strings.Builder
    25  	if e.Position == -1 {
    26  		buf.WriteString(e.Path)
    27  	} else {
    28  		buf.WriteString(e.Path[:e.Position])
    29  		buf.WriteString(fmt.Sprintf("|%c|", e.Path[e.Position]))
    30  		if e.Position != len(e.Path)-1 {
    31  			buf.WriteString(e.Path[e.Position+1:])
    32  		}
    33  	}
    34  
    35  	return fmt.Sprintf("path: %s invalid as %s", buf.String(), e.Reason)
    36  }
    37  
    38  // ErrMissingConfigValue is raised when a git config path does not
    39  // have a corresponding value
    40  type ErrMissingConfigValue struct {
    41  	// Path to the config setting
    42  	Path string
    43  }
    44  
    45  // Error returns a friendly formatted message of the current error
    46  func (e ErrMissingConfigValue) Error() string {
    47  	return fmt.Sprintf("config paths mismatch. path: %s is missing a corresponding value", e.Path)
    48  }
    49  
    50  // Config attempts to retrieve all git config for the current repository.
    51  // A map is returned containing each config item and its corresponding
    52  // latest value. Values are resolved from local, system and global config
    53  func (c *Client) Config() (map[string]string, error) {
    54  	cfg, err := c.exec("git config --list")
    55  	if err != nil {
    56  		return nil, err
    57  	}
    58  
    59  	values := map[string]string{}
    60  
    61  	lines := strings.Split(cfg, "\n")
    62  	for _, line := range lines {
    63  		pos := strings.Index(line, "=")
    64  		values[line[:pos]] = line[pos+1:]
    65  	}
    66  
    67  	return values, nil
    68  }
    69  
    70  // ConfigL attempts to query a batch of local git config settings for
    71  // their values. If multiple values have been set for any config item,
    72  // all are returned, ordered by most recent value first. A partial batch
    73  // is never returned, all config settings must exist
    74  func (c *Client) ConfigL(paths ...string) (map[string][]string, error) {
    75  	return c.configQuery("local", paths...)
    76  }
    77  
    78  func (c *Client) configQuery(location string, paths ...string) (map[string][]string, error) {
    79  	if len(paths) == 0 {
    80  		return nil, nil
    81  	}
    82  
    83  	values := map[string][]string{}
    84  
    85  	var cmd strings.Builder
    86  	for _, path := range paths {
    87  		cmd.WriteString("git config ")
    88  		cmd.WriteString("--" + location)
    89  		cmd.WriteString(" --get-all ")
    90  		cmd.WriteString(path)
    91  
    92  		cfg, err := c.exec(cmd.String())
    93  		if err != nil {
    94  			return nil, err
    95  		}
    96  		cmd.Reset()
    97  
    98  		v := reverse(strings.Split(cfg, "\n")...)
    99  		values[path] = v
   100  	}
   101  
   102  	return values, nil
   103  }
   104  
   105  // ConfigG attempts to query a batch of global git config settings for
   106  // their values. If multiple values have been set for any config item,
   107  // all are returned, ordered by most recent value first. A partial batch
   108  // is never returned, all config settings must exist
   109  func (c *Client) ConfigG(paths ...string) (map[string][]string, error) {
   110  	return c.configQuery("global", paths...)
   111  }
   112  
   113  // ConfigS attempts to query a batch of system git config settings for
   114  // their values. If multiple values have been set for any config item,
   115  // all are returned, ordered by most recent value first. A partial batch
   116  // is never returned, all config settings must exist
   117  func (c *Client) ConfigS(paths ...string) (map[string][]string, error) {
   118  	return c.configQuery("system", paths...)
   119  }
   120  
   121  // ConfigSetL attempts to batch assign values to a group of local git
   122  // config settings. If any setting exists, a new line is added to the
   123  // local git config, effectively assigning multiple values to the same
   124  // setting. Basic validation is performed to minimize the possibility
   125  // of a partial batch update
   126  func (c *Client) ConfigSetL(pairs ...string) error {
   127  	return c.configSet("local", pairs...)
   128  }
   129  
   130  func (c *Client) configSet(location string, pairs ...string) error {
   131  	if len(pairs) == 0 {
   132  		return nil
   133  	}
   134  
   135  	if err := checkConfig(pairs); err != nil {
   136  		return err
   137  	}
   138  
   139  	var cmd strings.Builder
   140  	for i := 0; i < len(pairs); i += 2 {
   141  		cmd.WriteString("git config ")
   142  		cmd.WriteString("--" + location)
   143  		cmd.WriteString(" --add ")
   144  		cmd.WriteString(fmt.Sprintf("%s '%s'", pairs[i], pairs[i+1]))
   145  
   146  		if _, err := c.exec(cmd.String()); err != nil {
   147  			return err
   148  		}
   149  		cmd.Reset()
   150  	}
   151  
   152  	return nil
   153  }
   154  
   155  // ConfigSetG attempts to batch assign values to a group of global git
   156  // config settings. If any setting exists, a new line is added to the
   157  // local git config, effectively assigning multiple values to the same
   158  // setting. Basic validation is performed to minimize the possibility
   159  // of a partial batch update
   160  func (c *Client) ConfigSetG(pairs ...string) error {
   161  	return c.configSet("global", pairs...)
   162  }
   163  
   164  // ConfigSetS attempts to batch assign values to a group of system git
   165  // config settings. If any setting exists, a new line is added to the
   166  // local git config, effectively assigning multiple values to the same
   167  // setting. Basic validation is performed to minimize the possibility
   168  // of a partial batch update
   169  func (c *Client) ConfigSetS(pairs ...string) error {
   170  	return c.configSet("system", pairs...)
   171  }
   172  
   173  // CheckConfigPath performs rudimentary checks to ensure the config path
   174  // conforms to the git config specification. A config path is invalid if:
   175  //
   176  //   - No dot separator exists, or the last character is a dot separator
   177  //   - First character after the last dot separator is not a letter
   178  //   - Path contains non-alphanumeric characters
   179  func CheckConfigPath(path string) error {
   180  	lastDot := strings.LastIndex(path, ".")
   181  	if lastDot == -1 || lastDot == len(path)-1 {
   182  		return ErrInvalidConfigPath{
   183  			Path:     path,
   184  			Position: lastDot,
   185  			Reason:   "dot separator is missing or is the last character",
   186  		}
   187  	}
   188  
   189  	for i, c := range path {
   190  		if i == lastDot+1 && !unicode.IsLetter(c) {
   191  			return ErrInvalidConfigPath{
   192  				Path:     path,
   193  				Position: i,
   194  				Reason:   "first character after final dot must be a letter [a-zA-Z]",
   195  			}
   196  		}
   197  
   198  		if unicode.IsDigit(c) || unicode.IsLetter(c) || c == '.' {
   199  			continue
   200  		}
   201  
   202  		return ErrInvalidConfigPath{
   203  			Path:     path,
   204  			Position: i,
   205  			Reason:   "non alphanumeric character detected [a-zA-Z0-9]",
   206  		}
   207  	}
   208  
   209  	return nil
   210  }
   211  
   212  func checkConfig(pairs []string) error {
   213  	if len(pairs)%2 != 0 {
   214  		return ErrMissingConfigValue{Path: pairs[len(pairs)-1]}
   215  	}
   216  
   217  	for i := 0; i < len(pairs); i += 2 {
   218  		if err := CheckConfigPath(pairs[i]); err != nil {
   219  			return err
   220  		}
   221  	}
   222  
   223  	return nil
   224  }
   225  
   226  // ToInlineConfig converts a series of config settings from path value notation
   227  // into the corresponding inline config notation compatible with git commands
   228  //
   229  //	"user.name", "penguin" => []string{"-c user.name='penguin'"}
   230  func ToInlineConfig(pairs ...string) ([]string, error) {
   231  	if len(pairs) == 0 {
   232  		return nil, nil
   233  	}
   234  
   235  	if err := checkConfig(pairs); err != nil {
   236  		return nil, err
   237  	}
   238  
   239  	cfg := make([]string, 0, len(pairs)%2)
   240  	for i := 0; i < len(pairs); i += 2 {
   241  		cfg = append(cfg, fmt.Sprintf("-c %s='%s'", pairs[i], pairs[i+1]))
   242  	}
   243  
   244  	return cfg, nil
   245  }