forked from go-git/go-git
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauto.go
More file actions
231 lines (203 loc) · 6.11 KB
/
Copy pathauto.go
File metadata and controls
231 lines (203 loc) · 6.11 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
package config
import (
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/go-git/go-billy/v6"
"github.com/go-git/go-billy/v6/osfs"
"github.com/go-git/go-git/v6/config"
)
// Git environment variables that override config file paths.
const (
envGitConfigGlobal = "GIT_CONFIG_GLOBAL"
envGitConfigSystem = "GIT_CONFIG_SYSTEM"
envGitConfigNoSystem = "GIT_CONFIG_NOSYSTEM"
envXDGConfigHome = "XDG_CONFIG_HOME"
)
// maxConfigFileSize is the largest config file readAndClose will consume
// before returning an error. Git config files are normally a few KB;
// the limit guards against accidental or malicious memory exhaustion.
const maxConfigFileSize = 10 << 20 // 10 MiB
// Option configures an [auto] ConfigSource.
type Option func(*auto)
// WithFilesystem sets the filesystem used to read configuration files.
// When not provided, the host OS filesystem is used.
func WithFilesystem(fs billy.Basic) Option {
return func(a *auto) {
a.fs = fs
}
}
// NewAuto returns a ConfigSource that mimics default Git behaviour.
//
// For each scope it applies Git's precedence rules:
// - GIT_CONFIG_GLOBAL, when set to a non-empty path, reads only that
// file. When set to "", global config is disabled entirely.
// When unset, ~/.gitconfig is used if it exists; otherwise the XDG
// config path is used as a fallback (they are alternatives, not merged).
// - GIT_CONFIG_NOSYSTEM, when truthy, skips system config entirely.
// - GIT_CONFIG_SYSTEM, when set to a non-empty path, reads only that
// file. When set to "", system config is disabled entirely.
func NewAuto(opts ...Option) *auto { //nolint:revive
a := &auto{fs: osfs.Default}
for _, o := range opts {
o(a)
}
return a
}
type auto struct {
fs billy.Basic
}
func (a *auto) Load(scope config.Scope) (config.ConfigStorer, error) {
var cfg *config.Config
var err error
switch scope {
case config.GlobalScope:
cfg, err = a.loadGlobal()
case config.SystemScope:
cfg, err = a.loadSystem()
default:
return nil, fmt.Errorf("unsupported scope: %d", scope)
}
if err != nil {
return nil, err
}
return &readOnlyStorer{cfg: *cfg}, nil
}
// loadGlobal resolves global config following git's precedence rules.
// GIT_CONFIG_GLOBAL replaces all standard paths when set; an empty value
// explicitly disables global config. When unset, ~/.gitconfig is used
// if it exists, otherwise the XDG config path is used as a fallback.
func (a *auto) loadGlobal() (*config.Config, error) {
if path, ok := os.LookupEnv(envGitConfigGlobal); ok {
if path == "" {
return config.NewConfig(), nil
}
return a.loadAndMerge([]string{path})
}
return a.loadAndMerge(a.globalPaths())
}
// loadSystem resolves system config following git's precedence rules.
// GIT_CONFIG_NOSYSTEM, when truthy, skips system config entirely.
// GIT_CONFIG_SYSTEM overrides the default path when set; an empty value
// explicitly disables system config.
func (a *auto) loadSystem() (*config.Config, error) {
if isNoSystem() {
return config.NewConfig(), nil
}
if path, ok := os.LookupEnv(envGitConfigSystem); ok {
if path == "" {
return config.NewConfig(), nil
}
return a.loadAndMerge([]string{path})
}
return a.loadAndMerge(systemPaths())
}
// globalPaths returns the config file path for the global scope.
//
// Git treats ~/.gitconfig and the XDG config as mutually exclusive
// alternatives: when ~/.gitconfig exists the XDG location is ignored.
func (a *auto) globalPaths() []string {
home, err := os.UserHomeDir()
if err != nil {
return nil
}
gitconfig := filepath.Join(home, ".gitconfig")
if _, sErr := a.fs.Stat(gitconfig); sErr == nil {
return []string{gitconfig}
}
if p := xdgConfigPath(home); p != "" {
return []string{p}
}
return nil
}
// xdgConfigPath returns the XDG git config file path, consulting
// XDG_CONFIG_HOME with platform-specific fallbacks.
func xdgConfigPath(home string) string {
if xdg := os.Getenv(envXDGConfigHome); xdg != "" {
return filepath.Join(xdg, "git", "config")
}
if runtime.GOOS == "windows" {
if appData := os.Getenv("APPDATA"); appData != "" {
return filepath.Join(appData, "git", "config")
}
return ""
}
return filepath.Join(home, ".config", "git", "config")
}
// systemPaths returns the candidate file paths for system-level Git
// configuration.
func systemPaths() []string {
if runtime.GOOS == "windows" {
if pf := os.Getenv("PROGRAMFILES"); pf != "" {
return []string{filepath.Join(pf, "Git", "etc", "gitconfig")}
}
return nil
}
return []string{"/etc/gitconfig"}
}
// loadAndMerge reads every existing config file in paths and merges them
// in order so that later files take precedence (last value wins).
// If no file is found, an empty config is returned.
func (a *auto) loadAndMerge(paths []string) (*config.Config, error) {
configs := make([]*config.Config, 0, len(paths))
for _, p := range paths {
f, err := a.fs.Open(p)
if err != nil {
if os.IsNotExist(err) {
continue
}
return nil, err
}
cfg, err := readAndClose(f)
if err != nil {
return nil, err
}
configs = append(configs, cfg)
}
if len(configs) == 0 {
return config.NewConfig(), nil
}
if len(configs) == 1 {
return configs[0], nil
}
merged := config.Merge(configs...)
return &merged, nil
}
// readAndClose reads a Git config from r and closes it.
// Files larger than [maxConfigFileSize] are rejected.
func readAndClose(r io.ReadCloser) (cfg *config.Config, err error) {
defer func() {
if cErr := r.Close(); cErr != nil && err == nil {
err = cErr
}
}()
b, err := io.ReadAll(io.LimitReader(r, maxConfigFileSize+1))
if err != nil {
return nil, err
}
if int64(len(b)) > maxConfigFileSize {
return nil, fmt.Errorf("config file exceeds maximum size (%d bytes)", maxConfigFileSize)
}
cfg = config.NewConfig()
if err = cfg.Unmarshal(b); err != nil {
return nil, err
}
return cfg, nil
}
// isNoSystem reports whether GIT_CONFIG_NOSYSTEM is set to a truthy value,
// matching git's case-insensitive boolean parsing.
func isNoSystem() bool {
v := os.Getenv(envGitConfigNoSystem)
if v == "" {
return false
}
switch strings.ToLower(v) {
case "0", "false", "no", "off":
return false
default:
return true
}
}