Skip to content

Commit 32fd36c

Browse files
authored
Merge pull request #7 from github/skarim/state-file-locking
lock state file writes
2 parents 0f67464 + fc4528b commit 32fd36c

16 files changed

Lines changed: 519 additions & 16 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,3 +467,4 @@ Compared to the typical workflow, there's no need to name branches, run `git add
467467
| 5 | Invalid arguments or flags |
468468
| 6 | Disambiguation required (branch belongs to multiple stacks) |
469469
| 7 | Rebase already in progress |
470+
| 8 | Stack is locked by another process |

cmd/add.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,8 +200,7 @@ func runAdd(cfg *config.Config, opts *addOptions, args []string) error {
200200
}
201201

202202
if err := stack.Save(gitDir, sf); err != nil {
203-
cfg.Errorf("failed to save stack state: %s", err)
204-
return ErrSilent
203+
return handleSaveError(cfg, err)
205204
}
206205

207206
// Print summary

cmd/init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ func runInit(cfg *config.Config, opts *initOptions) error {
327327
syncStackPRs(cfg, &sf.Stacks[len(sf.Stacks)-1])
328328

329329
if err := stack.Save(gitDir, sf); err != nil {
330-
return err
330+
return handleSaveError(cfg, err)
331331
}
332332

333333
// Print result

cmd/merge.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func runMerge(cfg *config.Config, target string) error {
4444
syncStackPRs(cfg, s)
4545

4646
// Persist the refreshed PR state.
47-
_ = stack.Save(result.GitDir, result.StackFile)
47+
stack.SaveNonBlocking(result.GitDir, result.StackFile)
4848

4949
// Resolve which branch to operate on.
5050
var br *stack.BranchRef

cmd/push.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,7 @@ func runPush(cfg *config.Config, opts *pushOptions) error {
180180
syncStackPRs(cfg, s)
181181

182182
if err := stack.Save(gitDir, sf); err != nil {
183-
cfg.Errorf("failed to save stack state: %s", err)
184-
return ErrSilent
183+
return handleSaveError(cfg, err)
185184
}
186185

187186
cfg.Successf("Pushed and synced %d branches", len(s.ActiveBranches()))

cmd/rebase.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ func runRebase(cfg *config.Config, opts *rebaseOptions) error {
305305

306306
syncStackPRs(cfg, s)
307307

308-
_ = stack.Save(gitDir, sf)
308+
stack.SaveNonBlocking(gitDir, sf)
309309

310310
merged := s.MergedBranches()
311311
if len(merged) > 0 {
@@ -496,7 +496,7 @@ func continueRebase(cfg *config.Config, gitDir string) error {
496496

497497
syncStackPRs(cfg, s)
498498

499-
_ = stack.Save(gitDir, sf)
499+
stack.SaveNonBlocking(gitDir, sf)
500500

501501
cfg.Printf("All branches in stack rebased locally with %s", s.Trunk.Branch)
502502
cfg.Printf("To push up your changes and open/update the stack of PRs, run `%s`",

cmd/sync.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ func runSync(cfg *config.Config, opts *syncOptions) error {
221221
} else {
222222
// Persist refreshed PR state even on conflict, then bail out
223223
// before pushing or reporting success.
224-
_ = stack.Save(gitDir, sf)
224+
stack.SaveNonBlocking(gitDir, sf)
225225
return ErrConflict
226226
}
227227
}
@@ -288,8 +288,7 @@ func runSync(cfg *config.Config, opts *syncOptions) error {
288288
updateBaseSHAs(s)
289289

290290
if err := stack.Save(gitDir, sf); err != nil {
291-
cfg.Errorf("failed to save stack state: %s", err)
292-
return ErrSilent
291+
return handleSaveError(cfg, err)
293292
}
294293

295294
cfg.Printf("")

cmd/unstack.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ func runUnstack(cfg *config.Config, opts *unstackOptions) error {
5050
// Remove from local tracking
5151
sf.RemoveStackForBranch(target)
5252
if err := stack.Save(gitDir, sf); err != nil {
53-
cfg.Errorf("failed to save stack state: %s", err)
54-
return ErrSilent
53+
return handleSaveError(cfg, err)
5554
}
5655
cfg.Successf("Stack removed from local tracking")
5756

cmd/utils.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ var (
2626
ErrInvalidArgs = &ExitError{Code: 5} // invalid arguments or flags
2727
ErrDisambiguate = &ExitError{Code: 6} // multiple stacks/remotes, can't auto-select
2828
ErrRebaseActive = &ExitError{Code: 7} // rebase already in progress
29+
ErrLockFailed = &ExitError{Code: 8} // could not acquire stack file lock
2930
)
3031

3132
// ExitError is returned by commands to indicate a specific exit code.
@@ -77,6 +78,9 @@ type loadStackResult struct {
7778
// branch, calls resolveStack (which may prompt for disambiguation), checks for
7879
// a nil stack, and re-reads the current branch (in case disambiguation caused
7980
// a checkout). Errors are printed via cfg and returned.
81+
//
82+
// loadStack does NOT acquire the stack file lock. The lock is acquired
83+
// automatically by stack.Save() when writing.
8084
func loadStack(cfg *config.Config, branch string) (*loadStackResult, error) {
8185
gitDir, err := git.GitDir()
8286
if err != nil {
@@ -133,6 +137,24 @@ func loadStack(cfg *config.Config, branch string) (*loadStackResult, error) {
133137
}, nil
134138
}
135139

140+
// handleSaveError translates a stack.Save error into the appropriate user
141+
// message and exit error. Lock contention and stale-file detection both
142+
// return ErrLockFailed (exit 8); other write failures return ErrSilent (exit 1).
143+
func handleSaveError(cfg *config.Config, err error) error {
144+
var lockErr *stack.LockError
145+
if errors.As(err, &lockErr) {
146+
cfg.Errorf("another process is currently editing the stack — try again later")
147+
return ErrLockFailed
148+
}
149+
var staleErr *stack.StaleError
150+
if errors.As(err, &staleErr) {
151+
cfg.Errorf("stack file was modified by another process — please re-run the command")
152+
return ErrLockFailed
153+
}
154+
cfg.Errorf("failed to save stack state: %s", err)
155+
return ErrSilent
156+
}
157+
136158
// resolveStack finds the stack for the given branch, handling ambiguity when
137159
// a branch (typically a trunk) belongs to multiple stacks. If exactly one
138160
// stack matches, it is returned directly. If multiple stacks match, the user

cmd/view.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ func runView(cfg *config.Config, opts *viewOptions) error {
5050
s := result.Stack
5151
currentBranch := result.CurrentBranch
5252

53-
// Sync PR state
53+
// Sync PR state and save (best-effort).
5454
syncStackPRs(cfg, s)
55-
_ = stack.Save(gitDir, sf)
55+
stack.SaveNonBlocking(gitDir, sf)
5656

5757
if opts.asJSON {
5858
return viewJSON(cfg, s, currentBranch)

0 commit comments

Comments
 (0)