@@ -5,14 +5,14 @@ import (
55 "errors"
66 "fmt"
77 "os"
8- "os/exec "
8+ "path/filepath "
99 "strings"
1010 "time"
1111
1212 cligit "github.com/cli/cli/v2/git"
1313)
1414
15- // client is a shared git client used by all package-level functions .
15+ // client is used by unscoped operations. Scoped operations keep their own copy .
1616var client = & cligit.Client {}
1717
1818// ErrMultipleRemotes is returned by ResolveRemote when multiple remotes
@@ -33,22 +33,58 @@ type CommitInfo struct {
3333 Time time.Time
3434}
3535
36- // run executes an arbitrary git command via the client and returns trimmed stdout.
37- func run (args ... string ) (string , error ) {
38- cmd , err := client .Command (context .Background (), args ... )
36+ func (d * defaultOps ) command (args ... string ) (* cligit.Command , error ) {
37+ c , err := d .gitClient ()
3938 if err != nil {
40- return "" , err
39+ return nil , err
4140 }
42- out , err := cmd .Output ()
41+ cmd , err := c .Command (context .Background (), args ... )
42+ if err != nil {
43+ return nil , err
44+ }
45+ d .configureCommand (cmd )
46+ return cmd , nil
47+ }
48+
49+ func (d * defaultOps ) configureCommand (cmd * cligit.Command ) {
50+ if ! d .scoped {
51+ return
52+ }
53+ env := cmd .Environ ()
54+ cmd .Env = make ([]string , 0 , len (env ))
55+ for _ , entry := range env {
56+ key , _ , _ := strings .Cut (entry , "=" )
57+ switch strings .ToUpper (key ) {
58+ case "GIT_DIR" , "GIT_COMMON_DIR" , "GIT_WORK_TREE" , "GIT_INDEX_FILE" ,
59+ "GIT_OBJECT_DIRECTORY" , "GIT_ALTERNATE_OBJECT_DIRECTORIES" ,
60+ "GIT_PREFIX" , "GIT_GRAFT_FILE" , "GIT_SHALLOW_FILE" ,
61+ "GIT_IMPLICIT_WORK_TREE" , "GIT_NAMESPACE" , "GIT_CONFIG" :
62+ // An explicit target must not inherit another checkout's index,
63+ // object database, or working directory from the caller.
64+ continue
65+ }
66+ cmd .Env = append (cmd .Env , entry )
67+ }
68+ }
69+
70+ // runRaw preserves path whitespace, NUL delimiters, and output on nonzero exits.
71+ func (d * defaultOps ) runRaw (args ... string ) (string , error ) {
72+ cmd , err := d .command (args ... )
4373 if err != nil {
4474 return "" , err
4575 }
46- return strings .TrimSpace (string (out )), nil
76+ out , err := cmd .Output ()
77+ return string (out ), err
78+ }
79+
80+ func (d * defaultOps ) run (args ... string ) (string , error ) {
81+ out , err := d .runRaw (args ... )
82+ return strings .TrimSpace (out ), err
4783}
4884
4985// runSilent executes a git command via the client and only returns an error.
50- func runSilent (args ... string ) error {
51- cmd , err := client . Command ( context . Background (), args ... )
86+ func ( d * defaultOps ) runSilent (args ... string ) error {
87+ cmd , err := d . command ( args ... )
5288 if err != nil {
5389 return err
5490 }
@@ -57,8 +93,11 @@ func runSilent(args ...string) error {
5793
5894// runInteractive runs a git command with stdin/stdout/stderr connected to
5995// the terminal, allowing interactive programs like editors to work.
60- func runInteractive (args ... string ) error {
61- cmd := exec .Command ("git" , args ... )
96+ func (d * defaultOps ) runInteractive (args ... string ) error {
97+ cmd , err := d .command (args ... )
98+ if err != nil {
99+ return err
100+ }
62101 cmd .Stdin = os .Stdin
63102 cmd .Stdout = os .Stdout
64103 cmd .Stderr = os .Stderr
@@ -84,69 +123,143 @@ func IsRebaseStartError(err error) bool {
84123 return errors .As (err , & startErr )
85124}
86125
87- func runRebaseCommand (args []string , opts RebaseOpts ) error {
88- if IsRebaseInProgress () {
126+ func (d * defaultOps ) runRebaseCommand (args []string , opts RebaseOpts ) error {
127+ inProgress , err := d .rebaseInProgress ()
128+ if err != nil {
129+ return & RebaseStartError {Err : err }
130+ }
131+ if inProgress {
89132 return & RebaseStartError {Err : errors .New ("a rebase is already in progress" )}
90133 }
91- err := runSilent (args ... )
134+ err = d . runSilent (args ... )
92135 if err == nil {
93136 return nil
94137 }
95- err = tryAutoResolveRebase (err , opts )
96- if err != nil && ! IsRebaseInProgress () {
138+ err = d .tryAutoResolveRebase (err , opts )
139+ if err == nil {
140+ return nil
141+ }
142+ inProgress , stateErr := d .rebaseInProgress ()
143+ if stateErr != nil {
144+ return errors .Join (err , stateErr )
145+ }
146+ if ! inProgress {
97147 return & RebaseStartError {Err : err }
98148 }
99149 return err
100150}
101151
102- // rebaseContinueOnce runs a single git rebase --continue without auto-resolve.
103- func rebaseContinueOnce (opts RebaseOpts ) error {
104- args := []string {"rebase" }
152+ func rebaseArgs (opts RebaseOpts ) []string {
153+ // The cascade owns its ref range and must never stash another worktree.
154+ // Use configuration overrides rather than flags unavailable in Git 2.36.
155+ args := []string {"-c" , "rebase.updateRefs=false" , "-c" , "rebase.autoStash=false" , "rebase" }
105156 if opts .CommitterDateIsAuthorDate {
106- args = append (args , "--committer-date-is-author-date" )
157+ // The apply backend loses this option after a conflict. The merge
158+ // backend persists it for continuation and rerere auto-continuation.
159+ args = append (args , "--merge" , "--committer-date-is-author-date" )
107160 }
108- args = append (args , "--continue" )
109- cmd := exec .Command ("git" , args ... )
110- cmd .Env = append (os .Environ (), "GIT_EDITOR=true" )
161+ return args
162+ }
163+
164+ // rebaseContinueOnce runs a single git rebase --continue without auto-resolve.
165+ func (d * defaultOps ) rebaseContinueOnce (_ RebaseOpts ) error {
166+ // The merge backend persists date options at rebase start. Repeating
167+ // start-only options with --continue does not change that saved state.
168+ cmd , err := d .command (append (rebaseArgs (RebaseOpts {}), "--continue" )... )
169+ if err != nil {
170+ return err
171+ }
172+ cmd .Env = append (cmd .Environ (), "GIT_EDITOR=true" )
111173 return cmd .Run ()
112174}
113175
114176// tryAutoResolveRebase checks whether rerere has resolved all conflicts
115177// from a failed rebase. If so, it auto-continues the rebase (potentially
116178// multiple times for multi-commit rebases). Returns originalErr if any
117179// conflicts remain that need manual resolution.
118- func tryAutoResolveRebase (originalErr error , opts RebaseOpts ) error {
180+ func (d * defaultOps ) tryAutoResolveRebase (originalErr error , opts RebaseOpts ) error {
181+ previousStep := ""
119182 for i := 0 ; i < 1000 ; i ++ {
120- if ! IsRebaseInProgress () {
121- if i == 0 {
122- return originalErr
123- }
124- return nil
125- }
126- conflicts , err := ConflictedFiles ()
183+ inProgress , err := d .rebaseInProgress ()
127184 if err != nil {
185+ return errors .Join (originalErr , err )
186+ }
187+ if ! inProgress {
128188 return originalErr
129189 }
190+ conflicts , err := d .ConflictedFiles ()
191+ if err != nil {
192+ return errors .Join (originalErr , err )
193+ }
130194 if len (conflicts ) > 0 {
131195 return originalErr
132196 }
197+ step , err := d .rebaseStep ()
198+ if err != nil {
199+ return errors .Join (originalErr , err )
200+ }
201+ if step == previousStep {
202+ return originalErr
203+ }
204+ previousStep = step
133205 // Rerere resolved all conflicts — auto-continue.
134- if rebaseContinueOnce (opts ) == nil {
206+ if err := d . rebaseContinueOnce (opts ); err == nil {
135207 return nil
208+ } else {
209+ originalErr = err
136210 }
137211 // Continue hit another conflicting commit; loop to check
138212 // if rerere resolved that one too.
139213 }
140214 return originalErr
141215}
142216
217+ func (d * defaultOps ) rebaseStep () (string , error ) {
218+ gitDir , err := d .GitDir ()
219+ if err != nil {
220+ return "" , err
221+ }
222+ for _ , file := range []string {"rebase-merge/msgnum" , "rebase-apply/next" } {
223+ data , err := os .ReadFile (filepath .Join (gitDir , filepath .FromSlash (file )))
224+ if errors .Is (err , os .ErrNotExist ) {
225+ continue
226+ }
227+ if err != nil {
228+ return "" , err
229+ }
230+ return file + ":" + string (data ), nil
231+ }
232+ return "" , fmt .Errorf ("cannot find rebase progress in %q" , gitDir )
233+ }
234+
143235// --- Public functions delegate through the ops interface ---
144236
145- // GitDir returns the path to the .git directory.
237+ // GitDir returns the absolute path to the current worktree's Git directory.
146238func GitDir () (string , error ) {
147239 return ops .GitDir ()
148240}
149241
242+ // CommonDir returns the absolute Git directory shared by all linked worktrees.
243+ func CommonDir () (string , error ) {
244+ return ops .CommonDir ()
245+ }
246+
247+ // Worktrees lists all registered worktrees, including the main worktree.
248+ func Worktrees () ([]Worktree , error ) {
249+ return ops .Worktrees ()
250+ }
251+
252+ // ForWorktree returns operations scoped to a worktree in the same repository.
253+ // Invalid contexts return errors from the resulting operations.
254+ func ForWorktree (path string ) Ops {
255+ return ops .ForWorktree (path )
256+ }
257+
258+ // CheckVersion requires Git 2.36 or newer.
259+ func CheckVersion () error {
260+ return ops .CheckVersion ()
261+ }
262+
150263// RootDir returns the repository's root directory.
151264func RootDir () (string , error ) {
152265 return ops .RootDir ()
0 commit comments