13 Commits

9 changed files with 158 additions and 143 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: hakoerber

54
Cargo.lock generated
View File

@@ -80,9 +80,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "clap" name = "clap"
version = "3.2.5" version = "3.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53da17d37dba964b9b3ecb5c5a1f193a2762c700e6829201e645b9381c99dc7" checksum = "9f1fe12880bae935d142c8702d500c63a4e8634b6c3c57ad72bf978fc7b6249a"
dependencies = [ dependencies = [
"atty", "atty",
"bitflags", "bitflags",
@@ -97,9 +97,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "3.2.5" version = "3.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c11d40217d16aee8508cc8e5fde8b4ff24639758608e5374e731b53f85749fb9" checksum = "ed6db9e867166a43a53f7199b5e4d1f522a1e5bd626654be263c999ce59df39a"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro-error", "proc-macro-error",
@@ -110,9 +110,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_lex" name = "clap_lex"
version = "0.2.2" version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5538cd660450ebeb4234cfecf8f2284b844ffc4c50531e66d584ad5b91293613" checksum = "87eba3c8c7f42ef17f6c659fc7416d0f4758cd3e58861ee63c5fa4a4dde649e4"
dependencies = [ dependencies = [
"os_str_bytes", "os_str_bytes",
] ]
@@ -155,12 +155,12 @@ dependencies = [
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.8" version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"lazy_static", "once_cell",
] ]
[[package]] [[package]]
@@ -367,9 +367,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.11.2" version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3"
[[package]] [[package]]
name = "heck" name = "heck"
@@ -410,9 +410,9 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.8.2" version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown", "hashbrown",
@@ -578,9 +578,9 @@ checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.8.3" version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
dependencies = [ dependencies = [
"libc", "libc",
"log", "log",
@@ -602,9 +602,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]] [[package]]
name = "openssl-src" name = "openssl-src"
version = "111.20.0+1.1.1o" version = "111.21.0+1.1.1p"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92892c4f87d56e376e469ace79f1128fdaded07646ddf73aa0be4706ff712dec" checksum = "6d0a8313729211913936f1b95ca47a5fc7f2e04cd658c115388287f8a8361008"
dependencies = [ dependencies = [
"cc", "cc",
] ]
@@ -746,18 +746,18 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.39" version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.18" version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@@ -847,9 +847,9 @@ dependencies = [
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.6" version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" checksum = "a0a5f7c728f5d284929a1cccb5bc19884422bfe6ef4d6c409da2c41838983fcf"
[[package]] [[package]]
name = "ryu" name = "ryu"
@@ -1002,9 +1002,9 @@ checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
[[package]] [[package]]
name = "strum_macros" name = "strum_macros"
version = "0.24.1" version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9550962e7cf70d9980392878dfaf1dcc3ece024f4cf3bf3c46b978d0bad61d6c" checksum = "6878079b17446e4d3eba6192bb0a2950d5b14f0ed8424b852310e5a94345d0ef"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@@ -1015,9 +1015,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.96" version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View File

@@ -28,7 +28,7 @@ rust-version = "1.57"
license = "GPL-3.0-only" license = "GPL-3.0-only"
[profile.e2e-tests] [profile.e2e-tests]
inherits = "release" inherits = "dev"
[lib] [lib]
name = "grm" name = "grm"
@@ -54,7 +54,7 @@ version = "=0.14.4"
version = "=2.1.0" version = "=2.1.0"
[dependencies.clap] [dependencies.clap]
version = "=3.2.5" version = "=3.2.6"
features = ["derive", "cargo"] features = ["derive", "cargo"]
[dependencies.console] [dependencies.console]

View File

@@ -36,7 +36,7 @@ test-binary:
env \ env \
GITHUB_API_BASEURL=http://rest:5000/github \ GITHUB_API_BASEURL=http://rest:5000/github \
GITLAB_API_BASEURL=http://rest:5000/gitlab \ GITLAB_API_BASEURL=http://rest:5000/gitlab \
cargo build --target {{static_target}} --profile e2e-tests --features=static-build cargo build --profile e2e-tests
install: install:
cargo install --path . cargo install --path .
@@ -64,7 +64,7 @@ test-e2e +tests=".": test-binary
&& docker-compose build \ && docker-compose build \
&& docker-compose run \ && docker-compose run \
--rm \ --rm \
-v $PWD/../target/{{static_target}}/e2e-tests/grm:/grm \ -v $PWD/../target/e2e-tests/grm:/grm \
pytest \ pytest \
"GRM_BINARY=/grm ALTERNATE_DOMAIN=alternate-rest python3 -m pytest -p no:cacheprovider --color=yes "$@"" \ "GRM_BINARY=/grm ALTERNATE_DOMAIN=alternate-rest python3 -m pytest -p no:cacheprovider --color=yes "$@"" \
&& docker-compose rm --stop -f && docker-compose rm --stop -f

View File

@@ -12,9 +12,18 @@ import os.path
@pytest.mark.parametrize("has_config", [True, False]) @pytest.mark.parametrize("has_config", [True, False])
@pytest.mark.parametrize("has_default", [True, False]) @pytest.mark.parametrize("has_default", [True, False])
@pytest.mark.parametrize("has_prefix", [True, False]) @pytest.mark.parametrize("has_prefix", [True, False])
def test_worktree_add_simple( @pytest.mark.parametrize("worktree_with_slash", [True, False])
remote_branch_already_exists, has_config, has_default, has_prefix def test_worktree_add(
remote_branch_already_exists,
has_config,
has_default,
has_prefix,
worktree_with_slash,
): ):
if worktree_with_slash:
worktree_name = "dir/test"
else:
worktree_name = "test"
with TempGitRepositoryWorktree() as (base_dir, _commit): with TempGitRepositoryWorktree() as (base_dir, _commit):
if has_config: if has_config:
with open(os.path.join(base_dir, "grm.toml"), "w") as f: with open(os.path.join(base_dir, "grm.toml"), "w") as f:
@@ -42,54 +51,61 @@ def test_worktree_add_simple(
touch change touch change
git add change git add change
git commit -m commit git commit -m commit
git push origin HEAD:test git push origin HEAD:{worktree_name}
#git reset --hard 'HEAD@{1}' #git reset --hard 'HEAD@{1}'
git branch -va git branch -va
) )
git --git-dir ./.git-main-working-tree worktree remove tmp git --git-dir ./.git-main-working-tree worktree remove tmp
""" """
) )
cmd = grm(["wt", "add", "test"], cwd=base_dir) cmd = grm(["wt", "add", worktree_name], cwd=base_dir)
assert cmd.returncode == 0 assert cmd.returncode == 0
files = os.listdir(base_dir) files = os.listdir(base_dir)
if has_config is True: if has_config is True:
assert len(files) == 3 assert len(files) == 3
assert set(files) == {".git-main-working-tree", "grm.toml", "test"} if worktree_with_slash:
assert set(files) == {".git-main-working-tree", "grm.toml", "dir"}
assert set(os.listdir(os.path.join(base_dir, "dir"))) == {"test"}
else:
assert set(files) == {".git-main-working-tree", "grm.toml", "test"}
else: else:
assert len(files) == 2 assert len(files) == 2
assert set(files) == {".git-main-working-tree", "test"} if worktree_with_slash:
assert set(files) == {".git-main-working-tree", "dir"}
assert set(os.listdir(os.path.join(base_dir, "dir"))) == {"test"}
else:
assert set(files) == {".git-main-working-tree", "test"}
repo = git.Repo(os.path.join(base_dir, "test")) repo = git.Repo(os.path.join(base_dir, worktree_name))
assert not repo.bare assert not repo.bare
assert not repo.is_dirty() assert not repo.is_dirty()
if has_config and has_default: if has_config and has_default:
if has_prefix and not remote_branch_already_exists: if has_prefix and not remote_branch_already_exists:
assert ( assert (
str(repo.active_branch.tracking_branch()) == "origin/myprefix/test" str(repo.active_branch.tracking_branch())
== f"origin/myprefix/{worktree_name}"
) )
else: else:
assert str(repo.active_branch.tracking_branch()) == "origin/test" assert (
str(repo.active_branch.tracking_branch())
== f"origin/{worktree_name}"
)
else: else:
assert repo.active_branch.tracking_branch() is None assert repo.active_branch.tracking_branch() is None
def test_worktree_add_into_subdirectory(): def test_worktree_add_invalid_name():
with TempGitRepositoryWorktree() as (base_dir, _commit): with TempGitRepositoryWorktree() as (base_dir, _commit):
cmd = grm(["wt", "add", "dir/test"], cwd=base_dir) for worktree_name in ["/absolute/path" "trailingslash/"]:
assert cmd.returncode == 0 args = ["wt", "add", worktree_name]
cmd = grm(args, cwd=base_dir)
files = os.listdir(base_dir) assert cmd.returncode != 0
assert len(files) == 2 print(cmd.stdout)
assert set(files) == {".git-main-working-tree", "dir"} print(cmd.stderr)
assert not os.path.exists(worktree_name)
files = os.listdir(os.path.join(base_dir, "dir")) assert not os.path.exists(os.path.join(base_dir, worktree_name))
assert set(files) == {"test"} assert "invalid worktree name" in str(cmd.stderr.lower())
repo = git.Repo(os.path.join(base_dir, "dir", "test"))
assert not repo.bare
assert not repo.is_dirty()
assert repo.active_branch.tracking_branch() is None
def test_worktree_add_into_invalid_subdirectory(): def test_worktree_add_into_invalid_subdirectory():
@@ -212,67 +228,6 @@ def test_worktree_add_with_explicit_no_tracking(
assert repo.active_branch.tracking_branch() is None assert repo.active_branch.tracking_branch() is None
@pytest.mark.parametrize("remote_branch_already_exists", [True, False])
@pytest.mark.parametrize("has_default", [True, False])
@pytest.mark.parametrize("has_prefix", [True, False])
def test_worktree_add_with_config(
remote_branch_already_exists, has_default, has_prefix
):
with TempGitRepositoryWorktree() as (base_dir, _commit):
with open(os.path.join(base_dir, "grm.toml"), "w") as f:
f.write(
f"""
[track]
default = {str(has_default).lower()}
default_remote = "origin"
"""
)
if has_prefix:
f.write(
"""
default_remote_prefix = "myprefix"
"""
)
if remote_branch_already_exists:
shell(
f"""
cd {base_dir}
git --git-dir ./.git-main-working-tree worktree add tmp
(
cd tmp
touch change
git add change
git commit -m commit
git push origin HEAD:test
#git reset --hard 'HEAD@{1}'
git branch -va
)
git --git-dir ./.git-main-working-tree worktree remove tmp
"""
)
cmd = grm(["wt", "add", "test"], cwd=base_dir)
print(cmd.stderr)
assert cmd.returncode == 0
files = os.listdir(base_dir)
assert len(files) == 3
assert set(files) == {".git-main-working-tree", "grm.toml", "test"}
repo = git.Repo(os.path.join(base_dir, "test"))
assert not repo.bare
assert not repo.is_dirty()
assert str(repo.active_branch) == "test"
if has_default:
if has_prefix and not remote_branch_already_exists:
assert (
str(repo.active_branch.tracking_branch()) == "origin/myprefix/test"
)
else:
assert str(repo.active_branch.tracking_branch()) == "origin/test"
else:
assert repo.active_branch.tracking_branch() is None
def test_worktree_delete(): def test_worktree_delete():
with TempGitRepositoryWorktree() as (base_dir, _commit): with TempGitRepositoryWorktree() as (base_dir, _commit):
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir) cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)

View File

@@ -1,3 +1,5 @@
#![forbid(unsafe_code)]
use std::path::Path; use std::path::Path;
use std::process; use std::process;
@@ -502,25 +504,9 @@ fn main() {
None => None, None => None,
}; };
let mut name: &str = &action_args.name;
let subdirectory;
let split = name.split_once('/');
match split {
None => subdirectory = None,
Some(split) => {
if split.0.is_empty() || split.1.is_empty() {
print_error("Worktree name cannot start or end with a slash");
process::exit(1);
} else {
(subdirectory, name) = (Some(Path::new(split.0)), split.1);
}
}
}
match worktree::add_worktree( match worktree::add_worktree(
&cwd, &cwd,
name, &action_args.name,
subdirectory,
track, track,
action_args.no_track, action_args.no_track,
) { ) {

View File

@@ -1,5 +1,6 @@
#![feature(io_error_more)] #![feature(io_error_more)]
#![feature(const_option_ext)] #![feature(const_option_ext)]
#![forbid(unsafe_code)]
use std::path::Path; use std::path::Path;

View File

@@ -222,7 +222,7 @@ fn sync_repo(root_path: &Path, repo: &repo::Repo, init_worktree: bool) -> Result
if newly_created && repo.worktree_setup && init_worktree { if newly_created && repo.worktree_setup && init_worktree {
match repo_handle.default_branch() { match repo_handle.default_branch() {
Ok(branch) => { Ok(branch) => {
worktree::add_worktree(&repo_path, &branch.name()?, None, None, false)?; worktree::add_worktree(&repo_path, &branch.name()?, None, false)?;
} }
Err(_error) => print_repo_error( Err(_error) => print_repo_error(
&repo.name, &repo.name,

View File

@@ -21,10 +21,17 @@ pub const GIT_MAIN_WORKTREE_DIRECTORY: &str = ".git-main-working-tree";
pub fn add_worktree( pub fn add_worktree(
directory: &Path, directory: &Path,
name: &str, name: &str,
subdirectory: Option<&Path>,
track: Option<(&str, &str)>, track: Option<(&str, &str)>,
no_track: bool, no_track: bool,
) -> Result<(), String> { ) -> Result<(), String> {
// A branch name must never start or end with a slash. Everything else is ok.
if name.starts_with('/') || name.ends_with('/') {
return Err(format!(
"Invalid worktree name: {}. It cannot start or end with a slash",
name
));
}
let repo = repo::RepoHandle::open(directory, true).map_err(|error| match error.kind { let repo = repo::RepoHandle::open(directory, true).map_err(|error| match error.kind {
repo::RepoErrorKind::NotFound => { repo::RepoErrorKind::NotFound => {
String::from("Current directory does not contain a worktree setup") String::from("Current directory does not contain a worktree setup")
@@ -38,11 +45,6 @@ pub fn add_worktree(
return Err(format!("Worktree {} already exists", &name)); return Err(format!("Worktree {} already exists", &name));
} }
let path = match subdirectory {
Some(dir) => directory.join(dir).join(name),
None => directory.join(Path::new(name)),
};
let mut remote_branch_exists = false; let mut remote_branch_exists = false;
let mut target_branch = match repo.find_local_branch(name) { let mut target_branch = match repo.find_local_branch(name) {
@@ -193,10 +195,80 @@ pub fn add_worktree(
} }
} }
if let Some(subdirectory) = subdirectory { // We have to create subdirectories first, otherwise adding the worktree
std::fs::create_dir_all(subdirectory).map_err(|error| error.to_string())?; // will fail
if name.contains('/') {
let path = Path::new(&name);
if let Some(base) = path.parent() {
// This is a workaround of a bug in libgit2 (?)
//
// When *not* doing this, we will receive an error from the `Repository::worktree()`
// like this:
//
// > failed to make directory '/{repo}/.git-main-working-tree/worktrees/dir/test
//
// This is a discrepancy between the behaviour of libgit2 and the
// git CLI when creating worktrees with slashes:
//
// The git CLI will create the worktree's configuration directory
// inside {git_dir}/worktrees/{last_path_component}. Look at this:
//
// ```
// $ git worktree add 1/2/3 -b 1/2/3
// $ ls .git/worktrees
// 3
// ```
//
// Interesting: When adding a worktree with a different name but the
// same final path component, git starts adding a counter suffix to
// the worktree directories:
//
// ```
// $ git worktree add 1/3/3 -b 1/3/3
// $ git worktree add 1/4/3 -b 1/4/3
// $ ls .git/worktrees
// 3
// 31
// 32
// ```
//
// I *guess* that the mapping back from the worktree directory under .git to the actual
// worktree directory is done via the `gitdir` file inside `.git/worktrees/{worktree}.
// This means that the actual directory would not matter. You can verify this by
// just renaming it:
//
// ```
// $ mv .git/worktrees/3 .git/worktrees/foobar
// $ git worktree list
// /tmp/ fcc8a2a7 [master]
// /tmp/1/2/3 fcc8a2a7 [1/2/3]
// /tmp/1/3/3 fcc8a2a7 [1/3/3]
// /tmp/1/4/3 fcc8a2a7 [1/4/3]
// ```
//
// => Still works
//
// Anyway, libgit2 does not do this: It tries to create the worktree
// directory inside .git with the exact name of the worktree, including
// any slashes. It should be this code:
//
// https://github.com/libgit2/libgit2/blob/f98dd5438f8d7bfd557b612fdf1605b1c3fb8eaf/src/libgit2/worktree.c#L346
//
// As a workaround, we can create the base directory manually for now.
//
// Tracking upstream issue: https://github.com/libgit2/libgit2/issues/6327
std::fs::create_dir_all(
directory
.join(GIT_MAIN_WORKTREE_DIRECTORY)
.join("worktrees")
.join(base),
)
.map_err(|error| error.to_string())?;
std::fs::create_dir_all(base).map_err(|error| error.to_string())?;
}
} }
repo.new_worktree(name, &path, &target_branch)?;
repo.new_worktree(name, &directory.join(&name), &target_branch)?;
Ok(()) Ok(())
} }