-
Notifications
You must be signed in to change notification settings - Fork 78
Add atomic stack storage and migration primitives #527
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,8 @@ import ( | |
| "os" | ||
| "path/filepath" | ||
| "time" | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New line needed? |
||
| "github.com/github/gh-stack/internal/stack" | ||
| ) | ||
|
|
||
| const stateFileName = "gh-stack-modify-state" | ||
|
|
@@ -75,7 +77,7 @@ func StatePath(gitDir string) string { | |
| // LoadState reads the modify state file from the git directory. | ||
| // Returns nil, nil if the file does not exist. | ||
| func LoadState(gitDir string) (*StateFile, error) { | ||
| data, err := os.ReadFile(StatePath(gitDir)) | ||
| data, err := stack.ReadStateFile(StatePath(gitDir)) | ||
| if err != nil { | ||
| if errors.Is(err, os.ErrNotExist) { | ||
| return nil, nil | ||
|
|
@@ -96,18 +98,9 @@ func SaveState(gitDir string, state *StateFile) error { | |
| if err != nil { | ||
| return fmt.Errorf("marshaling modify state: %w", err) | ||
| } | ||
| target := StatePath(gitDir) | ||
| tmp := target + ".tmp" | ||
| if err := os.WriteFile(tmp, data, 0644); err != nil { | ||
| if err := stack.WriteAtomic(StatePath(gitDir), data); err != nil { | ||
|
skarim marked this conversation as resolved.
|
||
| return fmt.Errorf("writing modify state: %w", err) | ||
| } | ||
| // Remove existing target before rename for Windows compatibility | ||
| // (os.Rename fails on Windows if the target already exists). | ||
| _ = os.Remove(target) | ||
| if err := os.Rename(tmp, target); err != nil { | ||
| _ = os.Remove(tmp) | ||
| return fmt.Errorf("committing modify state: %w", err) | ||
| } | ||
|
Comment on lines
-99
to
-110
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems like the biggest core change in this PR right? If I am understanding this correctly we are now writing to a temp file before we replace the pre-existing one (in stack/atomic.go) instead of what appears to be deleting and then replacing the file in the deleted code? |
||
| return nil | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| package stack | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "os" | ||
| "path/filepath" | ||
| ) | ||
|
|
||
| // ReadStateFile reads a complete state file, allowing WriteAtomic to replace it | ||
| // while the read is in progress. On Windows, its handle permits delete sharing. | ||
| func ReadStateFile(path string) ([]byte, error) { | ||
| return readStateFile(path) | ||
| } | ||
|
|
||
| // WriteAtomic publishes data at path using a fully written temporary file in | ||
| // the same directory. It preserves an existing regular file's permissions and | ||
| // uses 0644 for a new file. The parent directory must already exist. | ||
| // It does not acquire locks; callers must serialize mutations. | ||
| func WriteAtomic(path string, data []byte) error { | ||
| return writeFileAtomic(path, data, 0644, true) | ||
| } | ||
|
|
||
| // writeFileAtomic publishes a fully written sibling of path. When replace is | ||
| // false, an existing destination is never overwritten, even on a racing create. | ||
| func writeFileAtomic(path string, data []byte, mode os.FileMode, replace bool) (err error) { | ||
| if replace { | ||
| info, statErr := os.Lstat(path) | ||
| switch { | ||
| case statErr == nil: | ||
| if !info.Mode().IsRegular() { | ||
| return fmt.Errorf("cannot replace non-regular file %q", path) | ||
| } | ||
| mode = info.Mode().Perm() | ||
| case !errors.Is(statErr, os.ErrNotExist): | ||
| return statErr | ||
| } | ||
| } | ||
|
|
||
| f, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+"-*") | ||
| if err != nil { | ||
| return err | ||
| } | ||
| temp := f.Name() | ||
| closed := false | ||
| defer func() { | ||
| if !closed { | ||
| err = errors.Join(err, f.Close()) | ||
| } | ||
| if removeErr := os.Remove(temp); removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) { | ||
| err = errors.Join(err, fmt.Errorf("removing temporary file: %w", removeErr)) | ||
| } | ||
| }() | ||
|
|
||
| if err := f.Chmod(mode); err != nil { | ||
| return err | ||
| } | ||
| if _, err := f.Write(data); err != nil { | ||
| return err | ||
| } | ||
| if err := f.Sync(); err != nil { | ||
| return err | ||
| } | ||
| closed = true | ||
| if err := f.Close(); err != nil { | ||
| return err | ||
| } | ||
| if err := publishFile(temp, path, replace); err != nil { | ||
| return err | ||
| } | ||
| return syncDirectory(filepath.Dir(path)) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| //go:build !windows | ||
|
|
||
| package stack | ||
|
|
||
| import ( | ||
| "errors" | ||
| "os" | ||
| ) | ||
|
|
||
| func readStateFile(path string) ([]byte, error) { | ||
| return os.ReadFile(path) | ||
| } | ||
|
|
||
| func publishFile(temp, path string, replace bool) error { | ||
| if replace { | ||
| return os.Rename(temp, path) | ||
| } | ||
| // Linking publishes a complete file without replacing an existing backup. | ||
| // The temporary name is removed by writeFileAtomic. | ||
| return os.Link(temp, path) | ||
| } | ||
|
|
||
| func syncDirectory(path string) error { | ||
| f, err := os.Open(path) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| return errors.Join(f.Sync(), f.Close()) | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.