返回 DeepSeek-Reasonix
drivers.go
根目录 / internal / gitcmd / drivers.go
1 package gitcmd
2
3 import (
4 "bytes"
5 "context"
6 "errors"
7 "fmt"
8 "os/exec"
9 "slices"
10 "strings"
11 )
12
13 // ErrRepositoryDrivers reports that an invocation was not started because the
14 // repository's driver configuration could not be listed or neutralized.
15 var ErrRepositoryDrivers = errors.New("gitcmd: repository-defined git drivers cannot be neutralized")
16
17 // repositoryScopes are the config scopes a repository's author controls;
18 // includes are reported under the scope of the file that includes them.
19 var repositoryScopes = []string{"local", "worktree"}
20
21 // driverOverrides returns -c entries that empty every filter and merge driver
22 // defined at repository scope for the repository args will operate on, and
23 // turn off a repository-set submodule.recurse. The
24 // listing runs with the same leading global options, so -C and --git-dir
25 // resolve the same repository the invocation will.
26 func driverOverrides(ctx context.Context, dir string, repoEnv, args []string) ([]string, error) {
27 sub := subcommandIndex(args)
28 if sub < 0 || slices.Contains(noContentConversion, args[sub]) {
29 return nil, nil
30 }
31 query := []string{"config", "--name-only", "-z", "--get-regexp", `^(filter|merge)\.|^submodule\.recurse$`}
32 scoped := slices.Insert(slices.Clone(query), 1, "--show-scope")
33 out, err := listConfig(ctx, dir, repoEnv, args[:sub], scoped)
34 if exitCode(err) == 129 {
35 // git before 2.26 has no --show-scope; every definition is then
36 // treated as repository-authored.
37 out, err = listConfig(ctx, dir, repoEnv, args[:sub], query)
38 if err == nil {
39 return overridesFor(out, false)
40 }
41 }
42 if exitCode(err) == 1 {
43 return nil, nil
44 }
45 if err != nil {
46 return nil, fmt.Errorf("%w: %w", ErrRepositoryDrivers, err)
47 }
48 return overridesFor(out, true)
49 }
50
51 // noContentConversion are subcommands that never pass content through a
52 // filter or merge driver and never honour submodule.recurse, so the listing
53 // (a second git process per call) could only return overrides they ignore.
54 var noContentConversion = []string{
55 "version", "rev-parse", "symbolic-ref", "check-ref-format", "merge-base", "rev-list",
56 "show-ref", "for-each-ref", "update-ref", "write-tree", "commit-tree",
57 }
58
59 func listConfig(ctx context.Context, dir string, repoEnv, globals, query []string) ([]byte, error) {
60 cmd := newCommand(ctx, Args(dir, nil, append(slices.Clone(globals), query...)...), repoEnv)
61 var stderr bytes.Buffer
62 cmd.Stderr = &stderr
63 out, err := cmd.Output()
64 if err != nil && exitCode(err) != 1 {
65 return nil, fmt.Errorf("%w: %s", err, strings.TrimSpace(stderr.String()))
66 }
67 return out, err
68 }
69
70 func exitCode(err error) int {
71 var exit *exec.ExitError
72 if errors.As(err, &exit) {
73 return exit.ExitCode()
74 }
75 return 0
76 }
77
78 // overridesFor turns NUL-separated config key names — each preceded by its
79 // scope when scoped — into -c entries. Keys without a subsection (merge.ff)
80 // name no driver.
81 func overridesFor(out []byte, scoped bool) ([]string, error) {
82 fields := strings.Split(strings.TrimSuffix(string(out), "\x00"), "\x00")
83 var overrides []string
84 seen := map[string]bool{}
85 step := 1
86 if scoped {
87 step = 2
88 }
89 for i := 0; i+step-1 < len(fields); i += step {
90 key := fields[i+step-1]
91 if scoped && !slices.Contains(repositoryScopes, fields[i]) {
92 continue
93 }
94 if key == "submodule.recurse" {
95 // Only a repository-authored value is overridden, so a user's
96 // own global choice stays in force outside such repositories.
97 if !seen[key] {
98 seen[key] = true
99 overrides = append(overrides, "submodule.recurse=false")
100 }
101 continue
102 }
103 section, rest, _ := strings.Cut(key, ".")
104 dot := strings.LastIndexByte(rest, '.')
105 if dot <= 0 {
106 continue
107 }
108 name := rest[:dot]
109 if seen[section+"."+name] {
110 continue
111 }
112 seen[section+"."+name] = true
113 // -c splits at the first '=', so such a name cannot be addressed.
114 if strings.ContainsRune(name, '=') {
115 return nil, fmt.Errorf("%w: driver %q", ErrRepositoryDrivers, section+"."+name)
116 }
117 prefix := section + "." + name + "."
118 switch section {
119 case "filter":
120 // An empty command is no filter; required=false keeps the
121 // now-absent filter from failing the invocation.
122 overrides = append(overrides, prefix+"clean=", prefix+"smudge=", prefix+"process=", prefix+"required=false")
123 case "merge":
124 // An empty driver fails the merge of that path as a conflict.
125 overrides = append(overrides, prefix+"driver=")
126 }
127 }
128 return overrides, nil
129 }
130
130 lines GO