// Package gitconfigenv composes Git's indexed environment configuration. package gitconfigenv import ( "fmt" "strconv" "strings" ) const ( countKey = "GIT_CONFIG_COUNT" keyPrefix = "GIT_CONFIG_KEY_" valuePrefix = "GIT_CONFIG_VALUE_" maxEntries = 256 ) // Entry is one Git configuration key/value pair supplied through the // GIT_CONFIG_COUNT environment protocol. type Entry struct { Key string Value string } // Merge combines ordinary environment variables with Git's indexed configuration block. // Overlay values take precedence for ordinary variables; indexed Git entries retain source order. func Merge(base, overlay map[string]string) (map[string]string, error) { baseEntries, err := entriesFrom(base) if err != nil { return nil, fmt.Errorf("base Git config: %w", err) } overlayEntries, err := entriesFrom(overlay) if err != nil { return nil, fmt.Errorf("overlay Git config: %w", err) } result := make(map[string]string) for key, value := range base { if !isIndexedKey(key) { result[key] = value } } for key, value := range overlay { if !isIndexedKey(key) { result[key] = value } } entries := removeBoundaryOverlap(baseEntries, overlayEntries) writeEntries(result, entries) return result, nil } // CopyIndexed replaces target's indexed Git configuration with source's validated block. func CopyIndexed(target, source map[string]string) { for key := range target { if isIndexedKey(key) { delete(target, key) } } for key, value := range source { if isIndexedKey(key) { target[key] = value } } } // EnvironmentEntries serializes a validated indexed Git configuration block. func EnvironmentEntries(env map[string]string) ([]string, error) { entries, err := entriesFrom(env) if err != nil { return nil, err } if len(entries) == 0 { return nil, nil } result := make([]string, 0, len(entries)*2+1) result = append(result, fmt.Sprintf("%s=%d", countKey, len(entries))) for index, entry := range entries { result = append(result, fmt.Sprintf("%s%d=%s", keyPrefix, index, entry.Key), fmt.Sprintf("%s%d=%s", valuePrefix, index, entry.Value), ) } return result, nil } // Filter returns a copy of env with indexed Git configuration entries that do // not satisfy keep removed. The complete ordered block is provided so callers // can remove a related pair without disturbing unrelated host configuration. // Ordinary environment variables are preserved. func Filter(env map[string]string, keep func(index int, entries []Entry) bool) (map[string]string, error) { entries, err := entriesFrom(env) if err != nil { return nil, err } result := make(map[string]string, len(env)) for key, value := range env { if !isIndexedKey(key) { result[key] = value } } filtered := make([]Entry, 0, len(entries)) for index, entry := range entries { if keep(index, entries) { filtered = append(filtered, entry) } } writeEntries(result, filtered) return result, nil } func isIndexedKey(key string) bool { return key == countKey || strings.HasPrefix(key, keyPrefix) || strings.HasPrefix(key, valuePrefix) } // IsIndexedKey reports whether key belongs to Git's indexed environment // configuration block. func IsIndexedKey(key string) bool { return isIndexedKey(key) } func entriesFrom(env map[string]string) ([]Entry, error) { if len(env) == 0 { return nil, nil } countValue, hasCount := env[countKey] if !hasCount { // Git reads indexed entries only when GIT_CONFIG_COUNT is set, so any // leftover key/value pair configures nothing. Ignore them the way Git // does rather than rejecting an inherited environment we do not own. return nil, nil } count, err := strconv.Atoi(countValue) if err != nil || count < 0 || count > maxEntries { return nil, fmt.Errorf("%s must be between 0 and %d", countKey, maxEntries) } entries := make([]Entry, 0, count) for index := 0; index < count; index++ { key, keyOK := env[fmt.Sprintf("%s%d", keyPrefix, index)] value, valueOK := env[fmt.Sprintf("%s%d", valuePrefix, index)] if !keyOK || !valueOK || key == "" { return nil, fmt.Errorf("entry %d must include a key and value", index) } entries = append(entries, Entry{Key: key, Value: value}) } // Entries at or past the count are invisible to Git — a parent process that // lowered GIT_CONFIG_COUNT leaves its higher indexes behind — so they are // dropped here instead of failing the whole block. return entries, nil } func removeBoundaryOverlap(base, overlay []Entry) []Entry { maxOverlap := min(len(base), len(overlay)) for overlap := maxOverlap; overlap > 0; overlap-- { if entriesEqual(base[len(base)-overlap:], overlay[:overlap]) { return append(append([]Entry{}, base...), overlay[overlap:]...) } } return append(append([]Entry{}, base...), overlay...) } func entriesEqual(left, right []Entry) bool { if len(left) != len(right) { return false } for index := range left { if left[index] != right[index] { return false } } return true } func writeEntries(env map[string]string, entries []Entry) { if len(entries) == 0 { return } env[countKey] = strconv.Itoa(len(entries)) for index, entry := range entries { env[fmt.Sprintf("%s%d", keyPrefix, index)] = entry.Key env[fmt.Sprintf("%s%d", valuePrefix, index)] = entry.Value } }