-
Notifications
You must be signed in to change notification settings - Fork 172
Add directory tracking to sync #425
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
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
1322aa9
Use filer in sync command
pietern 91a3d4d
Pass file handle instead of buffer
pietern 952d5ec
Ignore ErrNotExist on deletes
pietern ccb6980
Delete directories when they become empty
pietern 5e57dc9
Simplify dir diff logic
pietern b3a5727
Track both directories to create and directories to remove
pietern e63f2a2
Merge remote-tracking branch 'origin/main' into sync-delete-empty-dir…
pietern 811c523
Update sync integration test
pietern 59b75a1
Merge remote-tracking branch 'origin/main' into sync-delete-empty-dir…
pietern 5a82294
Failures deleting a directory can be ignored
pietern 5cbae81
Assert file exists
pietern e8b3473
Factor out directory processing code into dedicated type
pietern d9fb6c4
Add assert
pietern 4a5de17
break -> return
pietern 06073df
Merge branch 'main' into sync-delete-empty-directories
pietern File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,100 @@ | ||
| package sync | ||
|
|
||
| import ( | ||
| "path" | ||
| ) | ||
|
|
||
| type diff struct { | ||
| put []string | ||
| delete []string | ||
| rmdir []string | ||
| mkdir []string | ||
| put []string | ||
| } | ||
|
|
||
| func (d diff) IsEmpty() bool { | ||
| return len(d.put) == 0 && len(d.delete) == 0 | ||
| } | ||
|
|
||
| // groupedMkdir returns a slice of slices of paths to create. | ||
| // Because the underlying mkdir calls create intermediate directories, | ||
| // we can group them together to reduce the total number of calls. | ||
| // This returns a slice of a slice for parity with [groupedRmdir]. | ||
| func (d diff) groupedMkdir() [][]string { | ||
| // Compute the set of prefixes of all paths to create. | ||
| prefixes := make(map[string]bool) | ||
| for _, name := range d.mkdir { | ||
| dir := path.Dir(name) | ||
| for dir != "." && dir != "/" { | ||
| prefixes[dir] = true | ||
| dir = path.Dir(dir) | ||
| } | ||
| } | ||
|
|
||
| var out []string | ||
|
|
||
| // Collect all paths that are not a prefix of another path. | ||
| for _, name := range d.mkdir { | ||
| if !prefixes[name] { | ||
| out = append(out, name) | ||
| } | ||
| } | ||
|
|
||
| return [][]string{out} | ||
| } | ||
|
|
||
| // groupedRmdir returns a slice of slices of paths to delete. | ||
| // The outer slice is ordered such that each inner slice can be | ||
| // deleted in parallel, as long as it is processed in order. | ||
| // The first entry will contain leaf directories, the second entry | ||
| // will contain intermediate directories, and so on. | ||
| func (d diff) groupedRmdir() [][]string { | ||
| // Compute the number of times each directory is a prefix of another directory. | ||
| prefixes := make(map[string]int) | ||
| for _, dir := range d.rmdir { | ||
| prefixes[dir] = 0 | ||
| } | ||
| for _, dir := range d.rmdir { | ||
| dir = path.Dir(dir) | ||
| for dir != "." && dir != "/" { | ||
| // Increment the prefix count for this directory, only if it | ||
| // it one of the directories we are deleting. | ||
| if _, ok := prefixes[dir]; ok { | ||
| prefixes[dir]++ | ||
| } | ||
| dir = path.Dir(dir) | ||
| } | ||
| } | ||
|
|
||
| var out [][]string | ||
|
|
||
| for len(prefixes) > 0 { | ||
| var toDelete []string | ||
|
|
||
| // Find directories which are not a prefix of another directory. | ||
| // These are the directories we can delete. | ||
| for dir, count := range prefixes { | ||
| if count == 0 { | ||
| toDelete = append(toDelete, dir) | ||
| delete(prefixes, dir) | ||
| } | ||
| } | ||
|
|
||
| // Remove these directories from the prefixes map. | ||
| for _, dir := range toDelete { | ||
| dir = path.Dir(dir) | ||
| for dir != "." && dir != "/" { | ||
| // Decrement the prefix count for this directory, only if it | ||
| // it one of the directories we are deleting. | ||
| if _, ok := prefixes[dir]; ok { | ||
| prefixes[dir]-- | ||
| } | ||
| dir = path.Dir(dir) | ||
| } | ||
| } | ||
|
|
||
| // Add these directories to the output. | ||
| out = append(out, toDelete) | ||
| } | ||
|
|
||
| return out | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| package sync | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| func TestDiffGroupedMkdir(t *testing.T) { | ||
| d := diff{ | ||
| mkdir: []string{ | ||
| "foo", | ||
| "foo/bar", | ||
| "foo/bar/baz1", | ||
| "foo/bar/baz2", | ||
| "foo1", | ||
| "a/b", | ||
| "a/b/c/d/e/f", | ||
| }, | ||
| } | ||
|
|
||
| // Expect only leaf directories to be included. | ||
| out := d.groupedMkdir() | ||
| assert.Len(t, out, 1) | ||
| assert.ElementsMatch(t, []string{ | ||
| "foo/bar/baz1", | ||
| "foo/bar/baz2", | ||
| "foo1", | ||
| "a/b/c/d/e/f", | ||
| }, out[0]) | ||
|
pietern marked this conversation as resolved.
|
||
| } | ||
|
|
||
| func TestDiffGroupedRmdir(t *testing.T) { | ||
| d := diff{ | ||
| rmdir: []string{ | ||
| "a/b/c/d/e/f", | ||
| "a/b/c/d/e", | ||
| "a/b/c/d", | ||
| "a/b/c", | ||
| "a/b/e/f/g/h", | ||
| "a/b/e/f/g", | ||
| "a/b/e/f", | ||
| "a/b/e", | ||
| "a/b", | ||
| }, | ||
| } | ||
|
|
||
| out := d.groupedRmdir() | ||
| assert.Len(t, out, 5) | ||
| assert.ElementsMatch(t, []string{"a/b/c/d/e/f", "a/b/e/f/g/h"}, out[0]) | ||
| assert.ElementsMatch(t, []string{"a/b/c/d/e", "a/b/e/f/g"}, out[1]) | ||
| assert.ElementsMatch(t, []string{"a/b/c/d", "a/b/e/f"}, out[2]) | ||
| assert.ElementsMatch(t, []string{"a/b/c", "a/b/e"}, out[3]) | ||
| assert.ElementsMatch(t, []string{"a/b"}, out[4]) | ||
| } | ||
|
|
||
| func TestDiffGroupedRmdirWithLeafsOnly(t *testing.T) { | ||
| d := diff{ | ||
| rmdir: []string{ | ||
| "foo/bar/baz1", | ||
| "foo/bar1", | ||
| "foo/bar/baz2", | ||
| "foo/bar2", | ||
| "foo1", | ||
| "foo2", | ||
| }, | ||
| } | ||
|
|
||
| // Expect all directories to be included. | ||
| out := d.groupedRmdir() | ||
| assert.Len(t, out, 1) | ||
| assert.ElementsMatch(t, d.rmdir, out[0]) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| package sync | ||
|
|
||
| import ( | ||
| "path" | ||
| "path/filepath" | ||
| "sort" | ||
| ) | ||
|
|
||
| // DirSet is a set of directories. | ||
| type DirSet map[string]struct{} | ||
|
|
||
| // MakeDirSet turns a list of file paths into the complete set of directories | ||
| // that is needed to store them (including parent directories). | ||
| func MakeDirSet(files []string) DirSet { | ||
| out := map[string]struct{}{} | ||
|
|
||
| // Iterate over all files. | ||
| for _, f := range files { | ||
| // Get the directory of the file in /-separated form. | ||
| dir := filepath.ToSlash(filepath.Dir(f)) | ||
|
|
||
| // Add this directory and its parents until it is either "." or already in the set. | ||
| for dir != "." { | ||
| if _, ok := out[dir]; ok { | ||
| break | ||
| } | ||
| out[dir] = struct{}{} | ||
| dir = path.Dir(dir) | ||
| } | ||
| } | ||
|
|
||
| return out | ||
| } | ||
|
|
||
| // Slice returns a sorted copy of the dirset elements as a slice. | ||
| func (dirset DirSet) Slice() []string { | ||
| out := make([]string, 0, len(dirset)) | ||
| for dir := range dirset { | ||
| out = append(out, dir) | ||
| } | ||
| sort.Strings(out) | ||
| return out | ||
| } | ||
|
|
||
| // Remove returns the set difference of two DirSets. | ||
| func (dirset DirSet) Remove(other DirSet) DirSet { | ||
| out := map[string]struct{}{} | ||
| for dir := range dirset { | ||
| if _, ok := other[dir]; !ok { | ||
| out[dir] = struct{}{} | ||
| } | ||
| } | ||
| return out | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.