From 33d7af2229cc4c27e1831f7d44e434e6a91bf6f9 Mon Sep 17 00:00:00 2001 From: itchyny Date: Tue, 7 Jan 2020 23:39:39 +0900 Subject: [PATCH] implement cycle renames --- mmv.go | 60 ++++++++++++++++++++++++++++++++++++-- mmv_test.go | 83 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 2 deletions(-) diff --git a/mmv.go b/mmv.go index 4b9f62f..58f2ab4 100644 --- a/mmv.go +++ b/mmv.go @@ -1,6 +1,11 @@ package mmv -import "os" +import ( + "fmt" + "math/rand" + "os" + "path/filepath" +) // Move multiple files. func Move(files map[string]string) error { @@ -22,8 +27,59 @@ type rename struct { func buildRenames(files map[string]string) ([]rename, error) { rs := make([]rename, 0, 2*len(files)) + vs := make(map[string]int, len(files)) + revs := make(map[string]string, len(files)) for src, dst := range files { - rs = append(rs, rename{src, dst}) + revs[dst] = src + } + var i int + for _, dst := range files { + if vs[dst] > 0 { + continue + } + i++ + var cycle bool + for { + vs[dst] = i + if x, ok := files[dst]; ok { + dst = x + if vs[x] > 0 { + cycle = vs[x] == i + break + } + } else { + break + } + } + var tmp string + if cycle { + tmp = randomPath(filepath.Dir(dst)) + rs = append(rs, rename{dst, tmp}) + vs[dst]-- + } + for { + if src, ok := revs[dst]; ok && (!cycle || vs[src] == i) { + rs = append(rs, rename{src, dst}) + if !cycle { + vs[dst] = i + } + dst = src + } else { + break + } + } + if cycle { + rs = append(rs, rename{tmp, dst}) + } } return rs, nil } + +func randomPath(dir string) string { + for { + path := filepath.Join(dir, fmt.Sprint(rand.Uint64())) + if _, err := os.Stat(path); err != nil && os.IsNotExist(err) { + return path + } + } +} diff --git a/mmv_test.go b/mmv_test.go index c06ac22..1128996 100644 --- a/mmv_test.go +++ b/mmv_test.go @@ -16,6 +16,7 @@ func TestMove(t *testing.T) { files map[string]string contents map[string]string expected map[string]string + cnt int err error }{ { @@ -27,6 +28,7 @@ func TestMove(t *testing.T) { files: map[string]string{ "foo": "bar", }, + cnt: 1, contents: map[string]string{ "foo": "0", }, @@ -40,6 +42,7 @@ func TestMove(t *testing.T) { "foo": "qux", "bar": "quxx", }, + cnt: 2, contents: map[string]string{ "foo": "0", "bar": "1", @@ -51,6 +54,84 @@ func TestMove(t *testing.T) { "baz": "2", }, }, + { + name: "swap two files", + files: map[string]string{ + "foo": "bar", + "bar": "foo", + }, + cnt: 3, + contents: map[string]string{ + "foo": "0", + "bar": "1", + "baz": "2", + }, + expected: map[string]string{ + "bar": "0", + "foo": "1", + "baz": "2", + }, + }, + { + name: "two swaps", + files: map[string]string{ + "foo": "bar", + "bar": "foo", + "baz": "qux", + "qux": "baz", + }, + cnt: 6, + contents: map[string]string{ + "foo": "0", + "bar": "1", + "baz": "2", + "qux": "3", + }, + expected: map[string]string{ + "bar": "0", + "foo": "1", + "qux": "2", + "baz": "3", + }, + }, + { + name: "three files", + files: map[string]string{ + "foo": "bar", + "bar": "baz", + "baz": "qux", + }, + cnt: 3, + contents: map[string]string{ + "foo": "0", + "bar": "1", + "baz": "2", + }, + expected: map[string]string{ + "bar": "0", + "baz": "1", + "qux": "2", + }, + }, + { + name: "cycle three files", + files: map[string]string{ + "foo": "bar", + "bar": "baz", + "baz": "foo", + }, + cnt: 4, + contents: map[string]string{ + "foo": "0", + "bar": "1", + "baz": "2", + }, + expected: map[string]string{ + "bar": "0", + "baz": "1", + "foo": "2", + }, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { @@ -59,6 +140,8 @@ func TestMove(t *testing.T) { require.NoError(t, os.Chdir(dir)) require.NoError(t, err) require.NoError(t, setupFiles(tc.contents)) + rs, _ := buildRenames(tc.files) + assert.Equal(t, tc.cnt, len(rs)) require.NoError(t, Move(tc.files)) assert.Equal(t, tc.expected, fileContents(".")) })