返回 DeepSeek-Reasonix
project_approval.go
根目录 / internal / hook / project_approval.go
1 package hook
2
3 import (
4 "path/filepath"
5 "strings"
6
7 "mvdan.cc/sh/v3/syntax"
8
9 "reasonix/internal/config"
10 "reasonix/internal/shellparse"
11 )
12
13 // IssueAwaitingApproval marks a project hook held back until the user approves
14 // the project's hooks as they stand now.
15 const IssueAwaitingApproval = "awaiting_approval"
16
17 // ProjectHooksProgram is the project's hooks file as one program the user
18 // approves: the declarations plus every workspace file a command names.
19 func ProjectHooksProgram(projectRoot string) (config.ProjectProgram, bool) {
20 path := ProjectSettingsPath(projectRoot)
21 s := readSettings(path)
22 if s == nil {
23 return config.ProjectProgram{}, false
24 }
25 return projectHooksProgram(projectRoot, path, s)
26 }
27
28 func projectHooksProgram(root, path string, s *Settings) (config.ProjectProgram, bool) {
29 var hooks []ResolvedHook
30 appendResolved(&hooks, s, ScopeProject, path)
31 if len(hooks) == 0 {
32 return config.ProjectProgram{}, false
33 }
34 ws := root
35 if real, err := filepath.EvalSymlinks(root); err == nil {
36 ws = real
37 }
38 var words, commands []string
39 for _, h := range hooks {
40 base := ws
41 if cwd := strings.TrimSpace(h.Cwd); cwd != "" {
42 base = cwd
43 if !filepath.IsAbs(cwd) {
44 base = filepath.Join(ws, cwd)
45 }
46 }
47 for _, word := range commandWords(h.Command) {
48 // Joined, not cleaned: the OS follows a link before "..".
49 if !filepath.IsAbs(word) {
50 word = base + string(filepath.Separator) + word
51 }
52 words = append(words, word)
53 }
54 line := string(h.Event) + ": " + h.Command
55 if h.Cwd != "" {
56 line += " (cwd " + h.Cwd + ")"
57 }
58 commands = append(commands, line+config.EnvSummary(h.Env))
59 }
60 detail := strings.Join(commands, "; ")
61 return config.NewProjectProgram(config.ProjectProgramHooks, SettingsDirname+"/"+SettingsFilename, detail, ws, ws, s.Hooks, words), true
62 }
63
64 // commandWords lists the static words of every command in a hook line. A line
65 // the Bash parser cannot read (a cmd.exe one) falls back to its fields.
66 func commandWords(command string) []string {
67 file, err := shellparse.ParseBash(command)
68 if err != nil || file == nil {
69 return strings.Fields(command)
70 }
71 var out []string
72 syntax.Walk(file, func(node syntax.Node) bool {
73 if word, ok := node.(*syntax.Word); ok {
74 if value, ok := shellparse.StaticWord(word); ok && strings.ContainsAny(value, `/\.`) {
75 out = append(out, value)
76 }
77 return false
78 }
79 return true
80 })
81 return out
82 }
83
84 // PendingProjectHooks reports the project's hooks when they are held back.
85 func PendingProjectHooks(opts LoadOptions) (config.ProjectProgram, bool) {
86 if opts.ProjectRoot == "" {
87 return config.ProjectProgram{}, false
88 }
89 program, ok := ProjectHooksProgram(opts.ProjectRoot)
90 if !ok || projectProgramApproved(opts, program) {
91 return config.ProjectProgram{}, false
92 }
93 return program, true
94 }
95
96 // ApproveProjectHooksAs approves settings as the hooks block just written, not
97 // whatever the file holds afterwards: a write that lands in between is then a
98 // different declaration, and stays unapproved.
99 func ApproveProjectHooksAs(opts LoadOptions, settings Settings) error {
100 program, ok := projectHooksProgram(opts.ProjectRoot, ProjectSettingsPath(opts.ProjectRoot), &settings)
101 if !ok {
102 return nil
103 }
104 return config.NewProjectProgramStore(reasonixHomeForOptions(opts)).Approve(opts.ProjectRoot, program)
105 }
106
107 // ApproveProjectHooks records the project's hooks as they stand now.
108 func ApproveProjectHooks(opts LoadOptions) error {
109 program, ok := ProjectHooksProgram(opts.ProjectRoot)
110 if !ok {
111 return nil
112 }
113 return config.NewProjectProgramStore(reasonixHomeForOptions(opts)).Approve(opts.ProjectRoot, program)
114 }
115
116 // appendApprovedProjectHooks adds the project's hooks only when approved as
117 // they stand, each carrying the approval so it is checked again before it runs.
118 func appendApprovedProjectHooks(out *[]ResolvedHook, opts LoadOptions, path string, s *Settings) {
119 program, ok := projectHooksProgram(opts.ProjectRoot, path, s)
120 if ok && !projectProgramApproved(opts, program) {
121 return
122 }
123 start := len(*out)
124 appendResolved(out, s, ScopeProject, path)
125 for i := start; ok && i < len(*out); i++ {
126 (*out)[i].approval = &program
127 }
128 }
129
130 func projectProgramApproved(opts LoadOptions, program config.ProjectProgram) bool {
131 ok, err := config.NewProjectProgramStore(reasonixHomeForOptions(opts)).Approved(opts.ProjectRoot, program)
132 return err == nil && ok
133 }
134
135 // refuseChangedHook records, instead of running, a project hook whose approval
136 // no longer holds because a file it names changed after loading.
137 func refuseChangedHook(report *Report, h ResolvedHook) bool {
138 if h.approval == nil {
139 return false
140 }
141 err := h.approval.Verify()
142 if err == nil {
143 return false
144 }
145 report.Outcomes = append(report.Outcomes, Outcome{Hook: h, Decision: DecisionError, ExitCode: -1, Stderr: err.Error(), Refusal: err})
146 return true
147 }
148
148 lines GO