Compare commits

..

No commits in common. "main" and "v0.1.2" have entirely different histories.
main ... v0.1.2

11 changed files with 136 additions and 320 deletions

View file

@ -3,12 +3,9 @@ name: CI
on: on:
push: push:
branches: branches:
- main - master
pull_request: pull_request:
permissions:
contents: read
jobs: jobs:
test: test:
name: Test name: Test
@ -18,9 +15,9 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@main
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v4 uses: actions/setup-go@v2
with: with:
go-version: 1.x go-version: 1.x
- name: Test - name: Test

View file

@ -5,24 +5,28 @@ on:
tags: tags:
- 'v*' - 'v*'
permissions:
contents: write
jobs: jobs:
release: release:
name: Release name: Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@main
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v4 uses: actions/setup-go@v2
with: with:
go-version: 1.x go-version: 1.x
- name: Cross build - name: Cross build
run: make cross run: make cross
- name: Create Release - name: Create Release
uses: ncipollo/release-action@v1 id: create_release
uses: actions/create-release@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
name: Release ${{ github.ref_name }} tag_name: ${{ github.ref }}
artifacts: 'goxz/*' release_name: Release ${{ github.ref }}
- name: Upload
run: make upload
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -1,20 +1,8 @@
# Changelog # Changelog
## [v0.1.6](https://github.com/itchyny/mmv/compare/v0.1.5..v0.1.6) (2023-04-26) ## [v0.1.2](https://github.com/itchyny/gojq/compare/v0.1.1..v0.1.2) (2020-09-19)
* Remove dependency on shell for splitting EDITOR with spaces.
## [v0.1.5](https://github.com/itchyny/mmv/compare/v0.1.4..v0.1.5) (2023-04-09)
* Support EDITOR with spaces in the editor path.
## [v0.1.4](https://github.com/itchyny/mmv/compare/v0.1.3..v0.1.4) (2021-09-18)
* Release `arm64` artifacts.
## [v0.1.3](https://github.com/itchyny/mmv/compare/v0.1.2..v0.1.3) (2021-01-10)
* Support renaming when one of the paths is a parent directory of another.
## [v0.1.2](https://github.com/itchyny/mmv/compare/v0.1.1..v0.1.2) (2020-09-19)
* Fix for EDITOR configured with arguments. * Fix for EDITOR configured with arguments.
## [v0.1.1](https://github.com/itchyny/mmv/compare/v0.1.0..v0.1.1) (2020-01-09) ## [v0.1.1](https://github.com/itchyny/gojq/compare/v0.1.0..v0.1.1) (2020-01-09)
* Undo the processed renames on error not to leave the temporary file. * Undo the processed renames on error not to leave the temporary file.
* Return the stat source error on failure. * Return the stat source error on failure.

View file

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2020-2023 itchyny Copyright (c) 2020 itchyny
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -1,9 +1,10 @@
BIN := mmv BIN := mmv
VERSION := $$(make -s show-version) VERSION := $$(make -s show-version)
VERSION_PATH := cmd/$(BIN) VERSION_PATH := cmd/$(BIN)
CURRENT_REVISION = $(shell git rev-parse --short HEAD) CURRENT_REVISION := $(shell git rev-parse --short HEAD)
BUILD_LDFLAGS = "-s -w -X main.revision=$(CURRENT_REVISION)" BUILD_LDFLAGS := "-s -w -X main.revision=$(CURRENT_REVISION)"
GOBIN ?= $(shell go env GOPATH)/bin GOBIN ?= $(shell go env GOPATH)/bin
export GO111MODULE=on
.PHONY: all .PHONY: all
all: build all: build
@ -14,40 +15,40 @@ build:
.PHONY: install .PHONY: install
install: install:
go install -ldflags=$(BUILD_LDFLAGS) ./cmd/$(BIN) go install -ldflags=$(BUILD_LDFLAGS) ./...
.PHONY: show-version .PHONY: show-version
show-version: $(GOBIN)/gobump show-version: $(GOBIN)/gobump
@gobump show -r "$(VERSION_PATH)" @gobump show -r $(VERSION_PATH)
$(GOBIN)/gobump: $(GOBIN)/gobump:
@go install github.com/x-motemen/gobump/cmd/gobump@latest @cd && go get github.com/x-motemen/gobump/cmd/gobump
.PHONY: cross .PHONY: cross
cross: $(GOBIN)/goxz CREDITS cross: $(GOBIN)/goxz CREDITS
goxz -n $(BIN) -pv=v$(VERSION) -build-ldflags=$(BUILD_LDFLAGS) ./cmd/$(BIN) goxz -n $(BIN) -pv=v$(VERSION) -build-ldflags=$(BUILD_LDFLAGS) ./cmd/$(BIN)
$(GOBIN)/goxz: $(GOBIN)/goxz:
go install github.com/Songmu/goxz/cmd/goxz@latest cd && go get github.com/Songmu/goxz/cmd/goxz
CREDITS: $(GOBIN)/gocredits go.sum CREDITS: $(GOBIN)/gocredits go.sum
go mod tidy go mod tidy
gocredits -w . gocredits -w .
$(GOBIN)/gocredits: $(GOBIN)/gocredits:
go install github.com/Songmu/gocredits/cmd/gocredits@latest cd && go get github.com/Songmu/gocredits/cmd/gocredits
.PHONY: test .PHONY: test
test: build test: build
go test -v -race ./... go test -v ./...
.PHONY: lint .PHONY: lint
lint: $(GOBIN)/staticcheck lint: $(GOBIN)/golint
go vet ./... go vet ./...
staticcheck -checks all ./... golint -set_exit_status ./...
$(GOBIN)/staticcheck: $(GOBIN)/golint:
go install honnef.co/go/tools/cmd/staticcheck@latest cd && go get golang.org/x/lint/golint
.PHONY: clean .PHONY: clean
clean: clean:
@ -56,9 +57,21 @@ clean:
.PHONY: bump .PHONY: bump
bump: $(GOBIN)/gobump bump: $(GOBIN)/gobump
test -z "$$(git status --porcelain || echo .)" ifneq ($(shell git status --porcelain),)
test "$$(git branch --show-current)" = "main" $(error git workspace is dirty)
endif
ifneq ($(shell git rev-parse --abbrev-ref HEAD),master)
$(error current branch is not master)
endif
@gobump up -w "$(VERSION_PATH)" @gobump up -w "$(VERSION_PATH)"
git commit -am "bump up version to $(VERSION)" git commit -am "bump up version to $(VERSION)"
git tag "v$(VERSION)" git tag "v$(VERSION)"
git push --atomic origin main tag "v$(VERSION)" git push origin master
git push origin "refs/tags/v$(VERSION)"
.PHONY: upload
upload: $(GOBIN)/ghr
ghr "v$(VERSION)" goxz
$(GOBIN)/ghr:
cd && go get github.com/tcnksm/ghr

View file

@ -1,7 +1,7 @@
# mmv # mmv
[![CI Status](https://github.com/itchyny/mmv/workflows/CI/badge.svg)](https://github.com/itchyny/mmv/actions) [![CI Status](https://github.com/itchyny/mmv/workflows/CI/badge.svg)](https://github.com/itchyny/mmv/actions)
[![Go Report Card](https://goreportcard.com/badge/github.com/itchyny/mmv)](https://goreportcard.com/report/github.com/itchyny/mmv) [![Go Report Card](https://goreportcard.com/badge/github.com/itchyny/mmv)](https://goreportcard.com/report/github.com/itchyny/mmv)
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/itchyny/mmv/blob/main/LICENSE) [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/itchyny/mmv/blob/master/LICENSE)
[![release](https://img.shields.io/github/release/itchyny/mmv/all.svg)](https://github.com/itchyny/mmv/releases) [![release](https://img.shields.io/github/release/itchyny/mmv/all.svg)](https://github.com/itchyny/mmv/releases)
[![pkg.go.dev](https://pkg.go.dev/badge/github.com/itchyny/mmv)](https://pkg.go.dev/github.com/itchyny/mmv) [![pkg.go.dev](https://pkg.go.dev/badge/github.com/itchyny/mmv)](https://pkg.go.dev/github.com/itchyny/mmv)
@ -24,7 +24,7 @@ brew install itchyny/tap/mmv
### Build from source ### Build from source
```bash ```bash
go install github.com/itchyny/mmv/cmd/mmv@latest go get github.com/itchyny/mmv/cmd/mmv
``` ```
## Features ## Features

View file

@ -4,12 +4,12 @@ import (
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"os/exec" "os/exec"
"runtime" "runtime"
"strings" "strings"
"github.com/kballard/go-shellquote"
_ "github.com/mattn/getwild" _ "github.com/mattn/getwild"
"github.com/mattn/go-tty" "github.com/mattn/go-tty"
@ -18,7 +18,7 @@ import (
const name = "mmv" const name = "mmv"
const version = "0.1.6" const version = "0.1.2"
var revision = "HEAD" var revision = "HEAD"
@ -79,45 +79,42 @@ func rename(args []string) error {
} }
xs[src] = true xs[src] = true
} }
f, err := ioutil.TempFile("", name+"-")
f, err := os.CreateTemp("", name+"-")
if err != nil { if err != nil {
return err return err
} }
defer os.Remove(f.Name()) defer func() {
f.Close()
os.Remove(f.Name())
}()
for _, arg := range args { for _, arg := range args {
f.WriteString(arg) f.WriteString(arg)
f.WriteString("\n") f.WriteString("\n")
} }
if err = f.Close(); err != nil { editor := os.Getenv("EDITOR")
return err if editor == "" {
editor = "vi"
} }
tty, err := tty.Open() tty, err := tty.Open()
if err != nil { if err != nil {
return err return err
} }
defer tty.Close() defer tty.Close()
editor := os.Getenv("EDITOR") editorWithArgs := strings.Fields(editor)
if editor == "" {
editor = "vi"
}
editorWithArgs, err := shellquote.Split(editor)
if err != nil {
return fmt.Errorf("%s: %s", err, editor)
}
editorWithArgs = append(editorWithArgs, f.Name()) editorWithArgs = append(editorWithArgs, f.Name())
cmd := exec.Command(editorWithArgs[0], editorWithArgs[1:]...) cmd := exec.Command(editorWithArgs[0], editorWithArgs[1:]...)
cmd.Stdin = tty.Input() cmd.Stdin = tty.Input()
cmd.Stdout = tty.Output() cmd.Stdout = tty.Output()
cmd.Stderr = tty.Output() cmd.Stderr = tty.Output()
if err = cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return fmt.Errorf("abort renames: %s", err) return fmt.Errorf("abort renames: %s", err)
} }
if err := f.Close(); err != nil {
cnt, err := os.ReadFile(f.Name()) return err
}
cnt, err := ioutil.ReadFile(f.Name())
if err != nil { if err != nil {
return err return err
} }
@ -129,6 +126,5 @@ func rename(args []string) error {
for i, src := range args { for i, src := range args {
files[src] = got[i] files[src] = got[i]
} }
return mmv.Rename(files) return mmv.Rename(files)
} }

12
go.mod
View file

@ -1,14 +1,10 @@
module github.com/itchyny/mmv module github.com/itchyny/mmv
go 1.20 go 1.15
require ( require (
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/mattn/getwild v0.0.2-0.20200919000855-c2e221927ad6 github.com/mattn/getwild v0.0.2-0.20200919000855-c2e221927ad6
github.com/mattn/go-tty v0.0.4 github.com/mattn/go-isatty v0.0.12 // indirect
) github.com/mattn/go-tty v0.0.3
golang.org/x/sys v0.0.0-20200918174421-af09f7315aff // indirect
require (
github.com/mattn/go-isatty v0.0.18 // indirect
golang.org/x/sys v0.7.0 // indirect
) )

19
go.sum
View file

@ -1,18 +1,17 @@
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/mattn/getwild v0.0.2-0.20200919000855-c2e221927ad6 h1:uWR+2CTTaHQzDS/DApbJ2H8UEPQl90atrKtczXj2xcs= github.com/mattn/getwild v0.0.2-0.20200919000855-c2e221927ad6 h1:uWR+2CTTaHQzDS/DApbJ2H8UEPQl90atrKtczXj2xcs=
github.com/mattn/getwild v0.0.2-0.20200919000855-c2e221927ad6/go.mod h1:AG+GKQydHp7iLJn+VV+D7y8LeYs5bQ0Xz4fmKd5o1Sg= github.com/mattn/getwild v0.0.2-0.20200919000855-c2e221927ad6/go.mod h1:AG+GKQydHp7iLJn+VV+D7y8LeYs5bQ0Xz4fmKd5o1Sg=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-tty v0.0.4 h1:NVikla9X8MN0SQAqCYzpGyXv0jY7MNl3HOWD2dkle7E= github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI=
github.com/mattn/go-tty v0.0.4/go.mod h1:u5GGXBtZU6RQoKV8gY5W6UhMudbR5vXnUe7j3pxse28= github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= golang.org/x/sys v0.0.0-20200918174421-af09f7315aff h1:1CPUrky56AcgSpxz/KfgzQWzfG09u5YOL8MvPYBlrL8=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

192
mmv.go
View file

@ -1,12 +1,10 @@
// Package mmv provides a method to rename multiple files.
package mmv package mmv
import ( import (
"crypto/rand" "fmt"
"encoding/base64" "math/rand"
"os" "os"
"path/filepath" "path/filepath"
"strings"
) )
// Rename multiple files. // Rename multiple files.
@ -65,7 +63,7 @@ type sameSourceError struct {
} }
func (err *sameSourceError) Error() string { func (err *sameSourceError) Error() string {
return "duplicate source: " + err.path return fmt.Sprintf("duplicate source: %s", err.path)
} }
type sameDestinationError struct { type sameDestinationError struct {
@ -73,23 +71,7 @@ type sameDestinationError struct {
} }
func (err *sameDestinationError) Error() string { func (err *sameDestinationError) Error() string {
return "duplicate destination: " + err.path return fmt.Sprintf("duplicate destination: %s", err.path)
}
type invalidRenameError struct {
src, dst string
}
func (err *invalidRenameError) Error() string {
return "invalid rename: " + err.src + ", " + err.dst
}
type temporaryPathError struct {
dir string
}
func (err *temporaryPathError) Error() string {
return "failed to create a temporary path: " + err.dir
} }
func buildRenames(files map[string]string) ([]rename, error) { func buildRenames(files map[string]string) ([]rename, error) {
@ -122,144 +104,76 @@ func buildRenames(files map[string]string) ([]rename, error) {
if _, ok := revs[dst]; ok { if _, ok := revs[dst]; ok {
return nil, &sameDestinationError{dst} return nil, &sameDestinationError{dst}
} }
if k, l := len(src), len(dst); k > l && src[l] == filepath.Separator && src[:l] == dst ||
k < l && dst[k] == filepath.Separator && dst[:k] == src {
return nil, &invalidRenameError{src, dst}
}
revs[dst] = src revs[dst] = src
} }
// group paths by directory depth // remove source == destination
srcdepths := make([][]string, 1)
dstdepths := make([][]string, 1)
for src, dst := range files { for src, dst := range files {
// group source paths by directory depth if src == dst {
i := strings.Count(src, string(filepath.Separator)) delete(files, src)
if len(srcdepths) <= i { delete(revs, dst)
xs := make([][]string, i*2)
copy(xs, srcdepths)
srcdepths = xs
}
srcdepths[i] = append(srcdepths[i], src)
// group destination paths by directory depth
i = strings.Count(dst, string(filepath.Separator))
if len(dstdepths) <= i {
xs := make([][]string, i*2)
copy(xs, dstdepths)
dstdepths = xs
}
dstdepths[i] = append(dstdepths[i], dst)
}
// result renames
count := len(files)
rs := make([]rename, 0, 2*count)
// check if any parent directory will be moved
for i := len(srcdepths) - 1; i >= 0; i-- {
L:
for _, src := range srcdepths[i] {
for j := 0; j < i; j++ {
for _, s := range srcdepths[j] {
if k := len(s); len(src) > k && src[k] == filepath.Separator && src[:k] == s {
if d := files[s]; s != d {
if dst, l := files[src], len(d); i == j+1 && len(dst) > l && dst[:l] == d && dst[l:] == src[k:] {
// skip moving a file when it moves along with the closest parent directory
delete(files, src)
delete(revs, dst)
} else {
// move to a temporary path before any parent directory is moved
tmp, err := temporaryPath(filepath.Dir(s))
if err != nil {
return nil, err
}
rs = append(rs, rename{src, tmp})
files[tmp] = files[dst]
delete(files, src)
revs[dst] = tmp
}
continue L
}
}
}
}
// remove if source path is equal to destination path
if dst := files[src]; src == dst {
delete(files, src)
delete(revs, dst)
}
} }
} }
// list renames in increasing destination directory depth order // list the renames
i, vs := 0, make(map[string]int, count) var i int
for _, dsts := range dstdepths { rs := make([]rename, 0, 2*len(files))
for _, dst := range dsts { vs := make(map[string]int, len(files))
if vs[dst] > 0 { for _, dst := range files {
continue if vs[dst] > 0 {
} continue
i++ // connected component identifier }
i++ // connected component identifier
// mark the nodes in the connected component and check cycle // mark the nodes in the connected component and check cycle
var cycle bool var cycle bool
for { for {
vs[dst] = i vs[dst] = i
if x, ok := files[dst]; ok { if x, ok := files[dst]; ok {
dst = x dst = x
if vs[x] > 0 { if vs[x] > 0 {
cycle = vs[x] == i cycle = vs[x] == i
break
}
} else {
break break
} }
} else {
break
} }
}
// if there is a cycle, rename to a temporary file // if there is a cycle, rename to a temporary file
var tmp string var tmp string
if cycle { if cycle {
var err error tmp = randomPath(filepath.Dir(dst))
tmp, err = temporaryPath(filepath.Dir(dst)) rs = append(rs, rename{dst, tmp})
if err != nil { vs[dst]--
return nil, err }
// rename from the leaf node
for {
if src, ok := revs[dst]; ok && (!cycle || vs[src] == i) {
rs = append(rs, rename{src, dst})
if !cycle {
vs[dst] = i
} }
rs = append(rs, rename{dst, tmp}) dst = src
vs[dst]-- } else {
break
} }
}
// rename from the leaf node // if there is a cycle, rename the temporary file
for { if cycle {
if src, ok := revs[dst]; ok && (!cycle || vs[src] == i) { rs = append(rs, rename{tmp, dst})
rs = append(rs, rename{src, dst})
if !cycle {
vs[dst] = i
}
dst = src
} else {
break
}
}
// if there is a cycle, rename the temporary file
if cycle {
rs = append(rs, rename{tmp, dst})
}
} }
} }
return rs, nil return rs, nil
} }
// create a temporary path where there is no file currently func randomPath(dir string) string {
func temporaryPath(dir string) (string, error) { for {
bs := make([]byte, 16) path := filepath.Join(dir, fmt.Sprint(rand.Uint64()))
for i := 0; i < 256; i++ {
if _, err := rand.Read(bs); err != nil {
return "", err
}
path := filepath.Join(dir, base64.RawURLEncoding.EncodeToString(bs))
if _, err := os.Stat(path); err != nil && os.IsNotExist(err) { if _, err := os.Stat(path); err != nil && os.IsNotExist(err) {
return path, nil return path
} }
} }
return "", &temporaryPathError{dir}
} }

View file

@ -1,6 +1,7 @@
package mmv package mmv
import ( import (
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@ -295,98 +296,12 @@ func TestRename(t *testing.T) {
"a/b/c/baz": "2", "a/b/c/baz": "2",
}, },
}, },
{
name: "invalid rename error",
files: map[string]string{
"x/y": "x",
},
contents: map[string]string{
"x/y": "0",
},
expected: map[string]string{
"x/y": "0",
},
err: "invalid rename: x",
},
{
name: "invalid rename error",
files: map[string]string{
"x/y": "x/y/z",
},
contents: map[string]string{
"x/y": "0",
},
expected: map[string]string{
"x/y": "0",
},
err: "invalid rename: x",
},
{
name: "directory renames",
files: map[string]string{
"x/foo": "y/bar",
"x/bar": "z/baz",
"x/qux": "z/qux",
"x/quy": "z/baz/qux",
"x/": "z/",
"y/bar": "x/foo",
"y/qux": "x/qux",
"y/": "w/",
"w/": "y/",
"w/x/": "y/y/",
"w/x/x/": "x/z/",
"w/x/y": "y/x/y",
"w/x/z": "w/x/z",
"w/x/w": "x/x/w",
"v/": "v/",
"v/x": "v/y",
"v/x/x": "v/y/x",
"xxxxx": "yyyyy",
},
count: 26,
contents: map[string]string{
"x/foo": "0",
"x/bar/a": "1",
"x/qux": "2",
"x/quy": "3",
"x/quz": "4",
"y/bar": "5",
"y/baz": "6",
"y/qux": "7",
"w/a": "8",
"w/x/a": "9",
"w/x/x/a": "10",
"w/x/y": "11",
"w/x/z": "12",
"w/x/w": "13",
"v/x/x": "14",
"xxxxx": "15",
},
expected: map[string]string{
"y/bar": "0",
"z/baz/a": "1",
"z/qux": "2",
"z/baz/qux": "3",
"z/quz": "4",
"x/foo": "5",
"w/baz": "6",
"x/qux": "7",
"y/a": "8",
"y/y/a": "9",
"x/z/a": "10",
"y/x/y": "11",
"w/x/z": "12",
"x/x/w": "13",
"v/y/x": "14",
"yyyyy": "15",
},
},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
dir, err := os.MkdirTemp("", "mmv-") dir, err := ioutil.TempDir("", "mmv-")
if err != nil { if err != nil {
t.Fatalf("os.MkdirTemp returned an error: %s", err) t.Fatalf("ioutil.TempDir returned an error: %s", err)
} }
t.Cleanup(func() { os.RemoveAll(dir) }) t.Cleanup(func() { os.RemoveAll(dir) })
if err := os.Chdir(dir); err != nil { if err := os.Chdir(dir); err != nil {
@ -416,13 +331,7 @@ func TestRename(t *testing.T) {
func setupFiles(contents map[string]string) error { func setupFiles(contents map[string]string) error {
for f, cnt := range contents { for f, cnt := range contents {
dir := filepath.Dir(f) if err := ioutil.WriteFile(f, []byte(cnt), 0o600); err != nil {
if dir != "." {
if err := os.MkdirAll(dir, 0o700); err != nil {
return err
}
}
if err := os.WriteFile(f, []byte(cnt), 0o600); err != nil {
return err return err
} }
} }
@ -431,7 +340,7 @@ func setupFiles(contents map[string]string) error {
func fileContents(dir string) map[string]string { func fileContents(dir string) map[string]string {
m := make(map[string]string) m := make(map[string]string)
fis, _ := os.ReadDir(dir) fis, _ := ioutil.ReadDir(dir)
for _, fi := range fis { for _, fi := range fis {
if fi.IsDir() { if fi.IsDir() {
for k, v := range fileContents(filepath.Join(dir, fi.Name())) { for k, v := range fileContents(filepath.Join(dir, fi.Name())) {
@ -439,7 +348,7 @@ func fileContents(dir string) map[string]string {
} }
} else { } else {
path := filepath.Join(dir, fi.Name()) path := filepath.Join(dir, fi.Name())
cnt, _ := os.ReadFile(path) cnt, _ := ioutil.ReadFile(path)
m[filepath.ToSlash(path)] = string(cnt) m[filepath.ToSlash(path)] = string(cnt)
} }
} }