Compare commits
7 Commits
v0.7.3
...
94bfe971b3
| Author | SHA1 | Date | |
|---|---|---|---|
| 94bfe971b3 | |||
| b77c442f56 | |||
| a3f9c9fda1 | |||
| 2a0a591194 | |||
| 23526ae62b | |||
| addff12c17 | |||
| c56765ce26 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
github: hakoerber
|
||||||
@@ -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"
|
||||||
|
|||||||
4
Justfile
4
Justfile
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
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"}
|
assert set(files) == {".git-main-working-tree", "grm.toml", "test"}
|
||||||
else:
|
else:
|
||||||
assert len(files) == 2
|
assert len(files) == 2
|
||||||
|
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"}
|
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)
|
||||||
|
|||||||
@@ -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,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user