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 }