[ops] Modify jiri project move behavior
Previously, `jiri` would attempt to move a directory atomically with
os.Rename(), falling back to exec'ing `mv` directly if the move crossed
a mount point.
This behavior does not handle a move of a directory to a child of
itself, in either the primary or fallback path. Instead we opt to route
all moves of directories thru a temporary swap directory in `.jiri_root`
to ensure we do not fall into any child path pitfalls and to avoid the
mount point complexities if we were to swap thru tmp.
Bug: b/195346750
Change-Id: Id99e3127c942f08e4da93d6a5ecf21189b3c8de5
Reviewed-on: https://fuchsia-review.googlesource.com/c/jiri/+/574151
Reviewed-by: Petr Hosek <phosek@google.com>
Commit-Queue: Nathan Mulcahey <nmulcahey@google.com>
diff --git a/project/operations.go b/project/operations.go
index e6ecabd..8ff9427 100644
--- a/project/operations.go
+++ b/project/operations.go
@@ -6,6 +6,7 @@
import (
"fmt"
+ "hash/fnv"
"io"
"io/ioutil"
"os"
@@ -371,11 +372,7 @@
}
// If it was nested project it might have been moved with its parent project
if op.source != op.destination {
- path, perm := filepath.Dir(op.destination), os.FileMode(0755)
- if err := os.MkdirAll(path, perm); err != nil {
- return fmtError(err)
- }
- if err := osutil.Rename(op.source, op.destination); err != nil {
+ if err := renameDir(jirix, op.source, op.destination); err != nil {
return fmtError(err)
}
}
@@ -930,3 +927,41 @@
}
return nil
}
+
+func renameDir(jirix *jiri.X, src, dst string) error {
+ // Parent directory permissions
+ perm := os.FileMode(0755)
+ swapDir := jirix.SwapDir()
+
+ // Hash src path as swap dir name
+ h := fnv.New32a()
+ h.Write([]byte(src))
+ tmp := filepath.Join(swapDir, fmt.Sprintf("%d", h.Sum32()))
+
+ // Ensure .jiri_root/swap exists
+ if err := os.MkdirAll(swapDir, perm); err != nil {
+ return err
+ }
+
+ // Move src -> tmp
+ if err := osutil.Rename(src, tmp); err != nil {
+ return err
+ }
+
+ // Ensure the dst's parent exists, it may have
+ // been within src
+ parentDir := filepath.Dir(dst)
+ if err := os.MkdirAll(parentDir, perm); err != nil {
+ return err
+ }
+
+ // Move tmp -> dst
+ if err := osutil.Rename(tmp, dst); err != nil {
+ if err := osutil.Rename(tmp, src); err != nil {
+ jirix.Logger.Errorf("Could not move %s to %s, original contents are in %s. Please complete the move manually", src, dst, tmp)
+ }
+ return err
+ }
+
+ return nil
+}
\ No newline at end of file
diff --git a/project/project_test.go b/project/project_test.go
index 08b17e4..bb439c4 100644
--- a/project/project_test.go
+++ b/project/project_test.go
@@ -1275,6 +1275,43 @@
checkReadme(t, fake.X, localProjects[1], "initial readme")
}
+// TestUpdateUniverseMovedProjectSelf checks that UpdateUniverse can move a
+// project to a path currently nested within itself.
+func TestUpdateUniverseMovedProjectSelf(t *testing.T) {
+ localProjects, fake, cleanup := setupUniverse(t)
+ defer cleanup()
+ if err := fake.UpdateUniverse(false); err != nil {
+ t.Fatal(err)
+ }
+
+ // Update the local path at which project 1 is located.
+ m, err := fake.ReadRemoteManifest()
+ if err != nil {
+ t.Fatal(err)
+ }
+ oldProjectPath := localProjects[1].Path
+ localProjects[1].Path = filepath.Join(oldProjectPath, "new-project-path")
+ projects := []project.Project{}
+ for _, p := range m.Projects {
+ if p.Name == localProjects[1].Name {
+ p.Path = localProjects[1].Path
+ }
+ projects = append(projects, p)
+ }
+ m.Projects = projects
+ if err := fake.WriteRemoteManifest(m); err != nil {
+ t.Fatal(err)
+ }
+ // Check that UpdateUniverse() moves the local copy of the project 1.
+ if err := fake.UpdateUniverse(false); err != nil {
+ t.Fatal(err)
+ }
+ if err := dirExists(localProjects[2].Path); err != nil {
+ t.Fatalf("expected project %q at path %q to exist but it did not", localProjects[1].Name, localProjects[1].Path)
+ }
+ checkReadme(t, fake.X, localProjects[1], "initial readme")
+}
+
// TestUpdateUniverseChangeRemote checks that UpdateUniverse can change remote
// of a project.
func TestUpdateUniverseChangeRemote(t *testing.T) {
diff --git a/x.go b/x.go
index fcd3a4f..0362e2f 100644
--- a/x.go
+++ b/x.go
@@ -458,6 +458,16 @@
return filepath.Join(x.RootMetaDir(), "scripts")
}
+// SwapDir returns the path to the swap directory. This is used
+// to stage moves of direcories that may be moving into subdirectories
+// of themselves or to handle unnesting of repositories.
+//
+// We use a directory in `.jiri_root` to prevent the complexities of an
+// arbitrary $TMPDIR which can cross mountpoints/filesystems.
+func (x *X) SwapDir() string {
+ return filepath.Join(x.RootMetaDir(), "swap")
+}
+
// UpdateHistoryDir returns the path to the update history directory.
func (x *X) UpdateHistoryDir() string {
return filepath.Join(x.RootMetaDir(), "update_history")