31 Commits

Author SHA1 Message Date
b4eafd0b41 Merge branch 'develop' 2022-06-29 23:58:31 +02:00
fa83063c61 Release v0.7.4 2022-06-29 23:58:31 +02:00
7d8fbb844e Properly handle deletion of nested worktrees 2022-06-29 23:40:23 +02:00
494c6ecb3e Cargo.lock: Updating linked-hash-map v0.5.4 -> v0.5.6 2022-06-29 23:36:50 +02:00
91a37cb12d Cargo.lock: Updating smallvec v1.8.0 -> v1.8.1 2022-06-29 23:36:46 +02:00
4e21a3daad dependencies: Update serde_json to 1.0.82 2022-06-29 23:34:55 +02:00
0e9c8d0c01 dependencies: Update clap to 3.2.7 2022-06-29 23:34:55 +02:00
512de5e187 e2e: Reduce number of tests by removing redundant ones 2022-06-29 22:47:04 +02:00
f027191896 Update worktree handling
That's a big one, see the module-level comment for details.
2022-06-23 19:21:05 +02:00
ee44fa40fd Add method to get owned commit of branch 2022-06-23 19:21:05 +02:00
e78dcf471a Print warning when giving --track and --no-track 2022-06-23 19:21:05 +02:00
056480f65a e2e: Update test for worktree adding 2022-06-23 19:21:05 +02:00
3eabc0e8f8 e2e: Update test for invalid remote name 2022-06-23 19:21:05 +02:00
d7ab3c4d6b e2e: Remove unnecessary output 2022-06-23 19:21:05 +02:00
09ce9f043e e2e: Add test case for invalid tracks 2022-06-23 19:21:05 +02:00
eac22148c5 e2e: Move invalid subdirectory test 2022-06-23 19:21:05 +02:00
92ec2e1a2d e2e: Test worktree names with whitespace 2022-06-23 19:21:05 +02:00
88961e1c6b e2e: Add caching to git repositories
It's very expensive to create new repositories from scratch. To avoid
this, a new repo & remotes are only created if necessary (depending on a
cache key given on request). If not created, they are simply copied from
a stored, clean repository / remote.
2022-06-23 19:21:05 +02:00
8c384741b3 e2e: Fix warning about default branch name 2022-06-23 19:00:22 +02:00
2053512559 e2e: Print stdout/stderr on error 2022-06-23 18:58:13 +02:00
ad7ef9277e e2e: Use pipefail for test scripts 2022-06-23 18:57:58 +02:00
95da48b5e6 e2e: Don't install recommended packages in docker 2022-06-23 18:56:35 +02:00
664cfb8965 e2e: Exit on first test error 2022-06-23 18:55:39 +02:00
ba4240720c Use static binary for e2e tests 2022-06-23 18:55:19 +02:00
ec04618a73 Use release builds for e2e tests 2022-06-23 18:54:49 +02:00
6dc298146a Cargo.lock: Updating openssl-src v111.20.0+1.1.1o -> v111.21.0+1.1.1p 2022-06-23 18:47:40 +02:00
09606cfc27 Cargo.lock: Updating crossbeam-utils v0.8.9 -> v0.8.10 2022-06-23 18:47:32 +02:00
465f877d6a Cargo.lock: Updating mio v0.8.3 -> v0.8.4 2022-06-23 18:47:26 +02:00
763e014b44 dependencies: Update clap to 3.2.6 2022-06-23 18:47:21 +02:00
474e0b60f9 Cargo.lock: Updating crossbeam-utils v0.8.8 -> v0.8.9 2022-06-17 02:25:41 +02:00
10af4d7448 Cargo.lock: Updating strum_macros v0.24.1 -> v0.24.0 2022-06-17 02:25:39 +02:00
18 changed files with 1625 additions and 543 deletions

76
Cargo.lock generated
View File

@@ -80,9 +80,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "3.2.5"
version = "3.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53da17d37dba964b9b3ecb5c5a1f193a2762c700e6829201e645b9381c99dc7"
checksum = "5b7b16274bb247b45177db843202209b12191b631a14a9d06e41b3777d6ecf14"
dependencies = [
"atty",
"bitflags",
@@ -97,9 +97,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "3.2.5"
version = "3.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c11d40217d16aee8508cc8e5fde8b4ff24639758608e5374e731b53f85749fb9"
checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902"
dependencies = [
"heck",
"proc-macro-error",
@@ -110,9 +110,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.2.2"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5538cd660450ebeb4234cfecf8f2284b844ffc4c50531e66d584ad5b91293613"
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
dependencies = [
"os_str_bytes",
]
@@ -155,12 +155,12 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.8"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38"
checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83"
dependencies = [
"cfg-if",
"lazy_static",
"once_cell",
]
[[package]]
@@ -332,7 +332,7 @@ dependencies = [
[[package]]
name = "git-repo-manager"
version = "0.7.3"
version = "0.7.4"
dependencies = [
"clap",
"comfy-table",
@@ -367,9 +367,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.11.2"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3"
[[package]]
name = "heck"
@@ -410,9 +410,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "1.8.2"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a"
checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
dependencies = [
"autocfg",
"hashbrown",
@@ -535,9 +535,9 @@ dependencies = [
[[package]]
name = "linked-hash-map"
version = "0.5.4"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "lock_api"
@@ -578,9 +578,9 @@ checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "mio"
version = "0.8.3"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799"
checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf"
dependencies = [
"libc",
"log",
@@ -602,9 +602,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
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"
checksum = "92892c4f87d56e376e469ace79f1128fdaded07646ddf73aa0be4706ff712dec"
checksum = "6d0a8313729211913936f1b95ca47a5fc7f2e04cd658c115388287f8a8361008"
dependencies = [
"cc",
]
@@ -746,18 +746,18 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.39"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f"
checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.18"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
dependencies = [
"proc-macro2",
]
@@ -847,9 +847,9 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.6"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f"
checksum = "a0a5f7c728f5d284929a1cccb5bc19884422bfe6ef4d6c409da2c41838983fcf"
[[package]]
name = "ryu"
@@ -895,9 +895,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.81"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7"
dependencies = [
"itoa",
"ryu",
@@ -974,9 +974,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.8.0"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
checksum = "cc88c725d61fc6c3132893370cac4a0200e3fedf5da8331c570664b1987f5ca2"
[[package]]
name = "socket2"
@@ -1002,9 +1002,9 @@ checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
[[package]]
name = "strum_macros"
version = "0.24.1"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9550962e7cf70d9980392878dfaf1dcc3ece024f4cf3bf3c46b978d0bad61d6c"
checksum = "4faebde00e8ff94316c01800f9054fd2ba77d30d9e922541913051d1d978918b"
dependencies = [
"heck",
"proc-macro2",
@@ -1015,9 +1015,9 @@ dependencies = [
[[package]]
name = "syn"
version = "1.0.96"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf"
checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd"
dependencies = [
"proc-macro2",
"quote",
@@ -1129,9 +1129,9 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.27"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7709595b8878a4965ce5e87ebf880a7d39c9afc6837721b21a5a816a8117d921"
checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7"
dependencies = [
"once_cell",
]
@@ -1160,9 +1160,9 @@ checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
[[package]]
name = "unicode-normalization"
version = "0.1.19"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
checksum = "81dee68f85cab8cf68dec42158baf3a79a1cdc065a8b103025965d6ccb7f6cbd"
dependencies = [
"tinyvec",
]

View File

@@ -1,6 +1,6 @@
[package]
name = "git-repo-manager"
version = "0.7.3"
version = "0.7.4"
edition = "2021"
authors = [
@@ -54,7 +54,7 @@ version = "=0.14.4"
version = "=2.1.0"
[dependencies.clap]
version = "=3.2.5"
version = "=3.2.7"
features = ["derive", "cargo"]
[dependencies.console]
@@ -70,7 +70,7 @@ version = "=6.0.0"
version = "=0.8.24"
[dependencies.serde_json]
version = "=1.0.81"
version = "=1.0.82"
[dependencies.isahc]
version = "=1.7.2"

View File

@@ -36,7 +36,7 @@ test-binary:
env \
GITHUB_API_BASEURL=http://rest:5000/github \
GITLAB_API_BASEURL=http://rest:5000/gitlab \
cargo build --profile e2e-tests
cargo build --profile e2e-tests --target {{static_target}} --features=static-build
install:
cargo install --path .
@@ -64,9 +64,9 @@ test-e2e +tests=".": test-binary
&& docker-compose build \
&& docker-compose run \
--rm \
-v $PWD/../target/e2e-tests/grm:/grm \
-v $PWD/../target/x86_64-unknown-linux-musl/e2e-tests/grm:/grm \
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 --exitfirst -p no:cacheprovider --color=yes "$@"" \
&& docker-compose rm --stop -f
update-dependencies: update-cargo-dependencies

View File

@@ -1,8 +1,14 @@
import os
from helpers import *
def pytest_configure(config):
os.environ["GIT_AUTHOR_NAME"] = "Example user"
os.environ["GIT_AUTHOR_EMAIL"] = "user@example.com"
os.environ["GIT_COMMITTER_NAME"] = "Example user"
os.environ["GIT_COMMITTER_EMAIL"] = "user@example.com"
def pytest_unconfigure(config):
pass

View File

@@ -1,7 +1,7 @@
FROM docker.io/debian:11.3
RUN apt-get update \
&& apt-get install -y \
&& apt-get install -y --no-install-recommends \
python3-pytest \
python3-toml \
python3-git \

View File

@@ -5,12 +5,26 @@ import os.path
import subprocess
import tempfile
import hashlib
import shutil
import inspect
import git
binary = os.environ["GRM_BINARY"]
def funcname():
return inspect.stack()[1][3]
def copytree(src, dest):
shutil.copytree(src, dest, dirs_exist_ok=True)
def get_temporary_directory(dir=None):
return tempfile.TemporaryDirectory(dir=dir)
def grm(args, cwd=None, is_invalid=False):
cmd = subprocess.run([binary] + args, cwd=cwd, capture_output=True, text=True)
if not is_invalid:
@@ -25,8 +39,12 @@ def grm(args, cwd=None, is_invalid=False):
def shell(script):
script = "set -o errexit\nset -o nounset\n" + script
subprocess.run(["bash"], input=script, text=True, check=True)
script = "set -o errexit\nset -o nounset\nset -o pipefail\n" + script
cmd = subprocess.run(["bash"], input=script, text=True, capture_output=True)
if cmd.returncode != 0:
print(cmd.stdout)
print(cmd.stderr)
cmd.check_returncode()
def checksum_directory(path):
@@ -112,78 +130,204 @@ def checksum_directory(path):
class TempGitRepository:
def __init__(self, dir=None):
self.dir = dir
pass
def __enter__(self):
self.tmpdir = tempfile.TemporaryDirectory(dir=self.dir)
self.remote_1_dir = tempfile.TemporaryDirectory()
self.remote_2_dir = tempfile.TemporaryDirectory()
shell(
f"""
self.tmpdir = get_temporary_directory(self.dir)
self.remote_1 = get_temporary_directory()
self.remote_2 = get_temporary_directory()
cmd = f"""
cd {self.tmpdir.name}
git init
git -c init.defaultBranch=master init
echo test > root-commit
git add root-commit
git commit -m "root-commit"
git remote add origin file://{self.remote_1_dir.name}
git remote add otherremote file://{self.remote_2_dir.name}
git remote add origin file://{self.remote_1.name}
git remote add otherremote file://{self.remote_2.name}
"""
)
shell(cmd)
return self.tmpdir.name
def __exit__(self, exc_type, exc_val, exc_tb):
del self.tmpdir
del self.remote_1_dir
del self.remote_2_dir
pass
class TempGitRemote:
obj = {}
def __init__(self, tmpdir, remoteid=None):
self.tmpdir = tmpdir
self.remoteid = remoteid
@classmethod
def get(cls, cachekey=None, initfunc=None):
if cachekey is None:
tmpdir = get_temporary_directory()
shell(
f"""
cd {tmpdir.name}
git -c init.defaultBranch=master init --bare
"""
)
newobj = cls(tmpdir)
remoteid = None
if initfunc is not None:
remoteid = newobj.init(initfunc)
newobj.remoteid = remoteid
return newobj, remoteid
else:
refresh = False
if cachekey not in cls.obj:
tmpdir = get_temporary_directory()
shell(
f"""
cd {tmpdir.name}
git -c init.defaultBranch=master init --bare
"""
)
newobj = cls(tmpdir)
remoteid = newobj.init(initfunc)
newobj.remoteid = remoteid
cls.obj[cachekey] = newobj
return cls.clone(cls.obj[cachekey])
@classmethod
def clone(cls, source):
new_remote = get_temporary_directory()
copytree(source.tmpdir.name, new_remote.name)
return cls(new_remote, source.remoteid), source.remoteid
def init(self, func):
return func(self.tmpdir.name)
def __enter__(self):
return self.tmpdir
def __exit__(self, exc_type, exc_val, exc_tb):
pass
class TempGitRepositoryWorktree:
def __init__(self):
pass
obj = {}
def __init__(self, remotes, tmpdir, commit, remote1, remote2, remote1id, remote2id):
self.remotes = remotes
self.tmpdir = tmpdir
self.commit = commit
self.remote1 = remote1
self.remote2 = remote2
self.remote1id = remote1id
self.remote2id = remote2id
@classmethod
def get(cls, cachekey, branch=None, remotes=2, basedir=None, remote_setup=None):
if cachekey not in cls.obj:
tmpdir = get_temporary_directory()
shell(
f"""
cd {tmpdir.name}
git -c init.defaultBranch=master init
echo test > root-commit-in-worktree-1
git add root-commit-in-worktree-1
git commit -m "root-commit-in-worktree-1"
echo test > root-commit-in-worktree-2
git add root-commit-in-worktree-2
git commit -m "root-commit-in-worktree-2"
git ls-files | xargs rm -rf
mv .git .git-main-working-tree
git --git-dir .git-main-working-tree config core.bare true
"""
)
repo = git.Repo(f"{tmpdir.name}/.git-main-working-tree")
commit = repo.head.commit.hexsha
if branch is not None:
repo.create_head(branch)
remote1 = None
remote2 = None
remote1id = None
remote2id = None
if remotes >= 1:
cachekeyremote, initfunc = (remote_setup or ((None, None),))[0]
remote1, remote1id = TempGitRemote.get(
cachekey=cachekeyremote, initfunc=initfunc
)
remote1 = remote1
remote1id = remote1id
shell(
f"""
cd {tmpdir.name}
git --git-dir .git-main-working-tree remote add origin file://{remote1.tmpdir.name}
"""
)
repo.remotes.origin.fetch()
repo.remotes.origin.push("master")
if remotes >= 2:
cachekeyremote, initfunc = (remote_setup or (None, (None, None)))[1]
remote2, remote2id = TempGitRemote.get(
cachekey=cachekeyremote, initfunc=initfunc
)
remote2 = remote2
remote2id = remote2id
shell(
f"""
cd {tmpdir.name}
git --git-dir .git-main-working-tree remote add otherremote file://{remote2.tmpdir.name}
"""
)
repo.remotes.otherremote.fetch()
repo.remotes.otherremote.push("master")
cls.obj[cachekey] = cls(
remotes, tmpdir, commit, remote1, remote2, remote1id, remote2id
)
return cls.clone(cls.obj[cachekey], remote_setup=remote_setup)
@classmethod
def clone(cls, source, remote_setup):
newdir = get_temporary_directory()
copytree(source.tmpdir.name, newdir.name)
remote1 = None
remote2 = None
remote1id = None
remote2id = None
repo = git.Repo(os.path.join(newdir.name, ".git-main-working-tree"))
if source.remotes >= 1:
cachekey, initfunc = (remote_setup or ((None, None),))[0]
remote1, remote1id = TempGitRemote.get(cachekey=cachekey, initfunc=initfunc)
if remote1id != source.remote1id:
repo.remotes.origin.fetch()
repo.remotes.origin.push("master")
if source.remotes >= 2:
cachekey, initfunc = (remote_setup or (None, (None, None)))[1]
remote2, remote2id = TempGitRemote.get(cachekey=cachekey, initfunc=initfunc)
if remote2id != source.remote2id:
repo.remotes.otherremote.fetch()
repo.remotes.otherremote.push("master")
return cls(
source.remotes,
newdir,
source.commit,
remote1,
remote2,
remote1id,
remote2id,
)
def __enter__(self):
self.tmpdir = tempfile.TemporaryDirectory()
self.remote_1_dir = tempfile.TemporaryDirectory()
self.remote_2_dir = tempfile.TemporaryDirectory()
shell(
f"""
cd {self.remote_1_dir.name}
git init --bare
"""
)
shell(
f"""
cd {self.remote_2_dir.name}
git init --bare
"""
)
shell(
f"""
cd {self.tmpdir.name}
git init
echo test > root-commit-in-worktree-1
git add root-commit-in-worktree-1
git commit -m "root-commit-in-worktree-1"
echo test > root-commit-in-worktree-2
git add root-commit-in-worktree-2
git commit -m "root-commit-in-worktree-2"
git remote add origin file://{self.remote_1_dir.name}
git remote add otherremote file://{self.remote_2_dir.name}
git push origin HEAD:master
git ls-files | xargs rm -rf
mv .git .git-main-working-tree
git --git-dir .git-main-working-tree config core.bare true
"""
)
commit = git.Repo(
f"{self.tmpdir.name}/.git-main-working-tree"
).head.commit.hexsha
return (self.tmpdir.name, commit)
return (self.tmpdir.name, self.commit)
def __exit__(self, exc_type, exc_val, exc_tb):
del self.tmpdir
del self.remote_1_dir
del self.remote_2_dir
pass
class RepoTree:
@@ -191,7 +335,7 @@ class RepoTree:
pass
def __enter__(self):
self.root = tempfile.TemporaryDirectory()
self.root = get_temporary_directory()
self.config = tempfile.NamedTemporaryFile()
with open(self.config.name, "w") as f:
f.write(
@@ -222,7 +366,7 @@ class EmptyDir:
pass
def __enter__(self):
self.tmpdir = tempfile.TemporaryDirectory()
self.tmpdir = get_temporary_directory()
return self.tmpdir.name
def __exit__(self, exc_type, exc_val, exc_tb):
@@ -234,7 +378,7 @@ class NonGitDir:
pass
def __enter__(self):
self.tmpdir = tempfile.TemporaryDirectory()
self.tmpdir = get_temporary_directory()
shell(
f"""
cd {self.tmpdir.name}
@@ -254,11 +398,11 @@ class TempGitFileRemote:
pass
def __enter__(self):
self.tmpdir = tempfile.TemporaryDirectory()
self.tmpdir = get_temporary_directory()
shell(
f"""
cd {self.tmpdir.name}
git init
git -c init.defaultBranch=master init
echo test > root-commit-in-remote-1
git add root-commit-in-remote-1
git commit -m "root-commit-in-remote-1"

View File

@@ -73,7 +73,7 @@ def test_repos_find(configtype, default):
mkdir repo1
(
cd ./repo1
git init
git -c init.defaultBranch=master init
echo test > test
git add test
git commit -m "commit1"
@@ -83,7 +83,7 @@ def test_repos_find(configtype, default):
mkdir repo2
(
cd ./repo2
git init
git -c init.defaultBranch=master init
git checkout -b main
echo test > test
git add test
@@ -203,7 +203,7 @@ def test_repos_find_with_invalid_repo(configtype, default):
mkdir repo1
(
cd ./repo1
git init
git -c init.defaultBranch=master init
echo test > test
git add test
git commit -m "commit1"
@@ -213,7 +213,7 @@ def test_repos_find_with_invalid_repo(configtype, default):
mkdir repo2
(
cd ./repo2
git init
git -c init.defaultBranch=master init
git checkout -b main
echo test > test
git add test

View File

@@ -6,7 +6,7 @@ from helpers import *
def test_worktree_clean():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
assert "test" in os.listdir(base_dir)
@@ -17,7 +17,7 @@ def test_worktree_clean():
def test_worktree_clean_refusal_no_tracking_branch():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
cmd = grm(["wt", "add", "test"], cwd=base_dir)
assert cmd.returncode == 0
@@ -31,7 +31,7 @@ def test_worktree_clean_refusal_no_tracking_branch():
def test_worktree_clean_refusal_uncommited_changes_new_file():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
@@ -47,7 +47,7 @@ def test_worktree_clean_refusal_uncommited_changes_new_file():
def test_worktree_clean_refusal_uncommited_changes_changed_file():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
@@ -63,7 +63,7 @@ def test_worktree_clean_refusal_uncommited_changes_changed_file():
def test_worktree_clean_refusal_uncommited_changes_cleand_file():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
@@ -81,7 +81,7 @@ def test_worktree_clean_refusal_uncommited_changes_cleand_file():
def test_worktree_clean_refusal_commited_changes():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
@@ -99,7 +99,7 @@ def test_worktree_clean_refusal_commited_changes():
def test_worktree_clean_refusal_tracking_branch_mismatch():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
@@ -117,7 +117,7 @@ def test_worktree_clean_refusal_tracking_branch_mismatch():
def test_worktree_clean_fail_from_subdir():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
cmd = grm(["wt", "add", "test"], cwd=base_dir)
assert cmd.returncode == 0
@@ -148,7 +148,7 @@ def test_worktree_clean_non_git():
def test_worktree_clean_configured_default_branch(
configure_default_branch, branch_list_empty
):
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
if configure_default_branch:
with open(os.path.join(base_dir, "grm.toml"), "w") as f:
if branch_list_empty:

View File

@@ -6,7 +6,7 @@ from helpers import *
def test_worktree_never_clean_persistent_branches():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
with open(os.path.join(base_dir, "grm.toml"), "w") as f:
f.write(
"""
@@ -33,7 +33,7 @@ def test_worktree_never_clean_persistent_branches():
def test_worktree_clean_branch_merged_into_persistent():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
with open(os.path.join(base_dir, "grm.toml"), "w") as f:
f.write(
"""
@@ -72,7 +72,7 @@ def test_worktree_clean_branch_merged_into_persistent():
def test_worktree_no_clean_unmerged_branch():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
with open(os.path.join(base_dir, "grm.toml"), "w") as f:
f.write(
"""
@@ -105,7 +105,7 @@ def test_worktree_no_clean_unmerged_branch():
def test_worktree_delete_branch_merged_into_persistent():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
with open(os.path.join(base_dir, "grm.toml"), "w") as f:
f.write(
"""

View File

@@ -23,7 +23,7 @@ def test_convert():
def test_convert_already_worktree():
with TempGitRepositoryWorktree() as (git_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (git_dir, _commit):
before = checksum_directory(git_dir)
cmd = grm(["wt", "convert"], cwd=git_dir)

View File

@@ -9,7 +9,7 @@ import git
def test_worktree_fetch():
with TempGitRepositoryWorktree() as (base_dir, root_commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, root_commit):
with TempGitFileRemote() as (remote_path, _remote_sha):
shell(
f"""
@@ -56,7 +56,7 @@ def test_worktree_fetch():
@pytest.mark.parametrize("has_changes", [True, False])
@pytest.mark.parametrize("stash", [True, False])
def test_worktree_pull(rebase, ffable, has_changes, stash):
with TempGitRepositoryWorktree() as (base_dir, root_commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, root_commit):
with TempGitFileRemote() as (remote_path, _remote_sha):
shell(
f"""

View File

@@ -14,7 +14,7 @@ import git
@pytest.mark.parametrize("has_changes", [True, False])
@pytest.mark.parametrize("stash", [True, False])
def test_worktree_rebase(pull, rebase, ffable, has_changes, stash):
with TempGitRepositoryWorktree() as (base_dir, _root_commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _root_commit):
with open(os.path.join(base_dir, "grm.toml"), "w") as f:
f.write('persistent_branches = ["mybasebranch"]')

View File

@@ -9,7 +9,7 @@ import pytest
@pytest.mark.parametrize("has_config", [True, False])
def test_worktree_status(has_config):
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
if has_config:
with open(os.path.join(base_dir, "grm.toml"), "w") as f:
f.write("")
@@ -24,7 +24,7 @@ def test_worktree_status(has_config):
def test_worktree_status_fail_from_subdir():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
cmd = grm(["wt", "add", "test"], cwd=base_dir)
assert cmd.returncode == 0
@@ -51,7 +51,7 @@ def test_worktree_status_non_git():
def test_worktree_status_warn_with_non_worktree_dir():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
cmd = grm(["wt", "add", "test"], cwd=base_dir)
assert cmd.returncode == 0

View File

@@ -4,112 +4,565 @@ from helpers import *
import git
import pytest
import datetime
import os.path
@pytest.mark.parametrize(
"config_setup",
(
(False, False, False),
(True, False, False),
(True, False, True),
(True, True, False),
(True, True, True),
),
)
@pytest.mark.parametrize("explicit_notrack", [True, False])
@pytest.mark.parametrize("explicit_track", [True, False])
@pytest.mark.parametrize(
"local_branch_setup", ((False, False), (True, False), (True, True))
)
@pytest.mark.parametrize("remote_branch_already_exists", [True, False])
@pytest.mark.parametrize("has_config", [True, False])
@pytest.mark.parametrize("has_default", [True, False])
@pytest.mark.parametrize("has_prefix", [True, False])
@pytest.mark.parametrize("remote_branch_with_prefix_already_exists", [True, False])
@pytest.mark.parametrize(
"remote_setup",
(
(0, "origin", False),
(1, "origin", False),
(2, "origin", False),
(2, "otherremote", False),
(2, "origin", True),
(2, "otherremote", True),
),
)
@pytest.mark.parametrize("track_differs_from_existing_branch_upstream", [True, False])
@pytest.mark.parametrize("worktree_with_slash", [True, False])
def test_worktree_add(
config_setup,
explicit_notrack,
explicit_track,
local_branch_setup,
remote_branch_already_exists,
has_config,
has_default,
has_prefix,
remote_branch_with_prefix_already_exists,
remote_setup,
track_differs_from_existing_branch_upstream,
worktree_with_slash,
):
(remote_count, default_remote, remotes_differ) = remote_setup
(
config_enabled,
config_has_default_remote_prefix,
config_has_default_track_enabled,
) = config_setup
(local_branch_exists, local_branch_has_tracking_branch) = local_branch_setup
has_remotes = True if remote_count > 0 else False
if worktree_with_slash:
worktree_name = "dir/test"
worktree_name = "dir/nested/test"
else:
worktree_name = "test"
with TempGitRepositoryWorktree() as (base_dir, _commit):
if has_config:
if track_differs_from_existing_branch_upstream:
explicit_track_branch_name = f"{default_remote}/somethingelse"
else:
explicit_track_branch_name = f"{default_remote}/{worktree_name}"
timestamp = datetime.datetime.now().replace(microsecond=0).isoformat()
# GitPython has some weird behaviour here. It is not possible to use kwargs
# to set the commit and author date.
#
# `committer_date=x` (which is documented) does not work, as `git commit`
# does not accept --committer-date
#
# `author_date=x` does not work, as it's now called --date in `git commit`
#
# `date=x` should work, but is refused by GitPython, as it does not know
# about the new behaviour in `git commit`
#
# Fortunately, there are env variables that control those timestamps.
os.environ["GIT_COMMITTER_DATE"] = str(timestamp)
os.environ["GIT_AUTHOR_DATE"] = str(timestamp)
def setup_remote1(directory):
if remote_branch_already_exists:
with tempfile.TemporaryDirectory() as cloned:
repo = git.Repo.clone_from(directory, cloned)
newfile = os.path.join(cloned, "change")
open(newfile, "w").close()
repo.index.add([newfile])
repo.index.commit("commit")
repo.remotes.origin.push(f"HEAD:{worktree_name}", force=True)
if remote_branch_with_prefix_already_exists:
with tempfile.TemporaryDirectory() as cloned:
repo = git.Repo.clone_from(directory, cloned)
newfile = os.path.join(cloned, "change2")
open(newfile, "w").close()
repo.index.add([newfile])
repo.index.commit("commit")
repo.remotes.origin.push(f"HEAD:myprefix/{worktree_name}", force=True)
return "_".join(
[
str(worktree_with_slash),
str(remote_branch_already_exists),
str(remote_branch_with_prefix_already_exists),
str(remotes_differ),
]
)
def setup_remote2(directory):
if remote_branch_already_exists:
with tempfile.TemporaryDirectory() as cloned:
repo = git.Repo.clone_from(directory, cloned)
newfile = os.path.join(cloned, "change")
open(newfile, "w").close()
repo.index.add([newfile])
repo.index.commit("commit")
if remotes_differ:
newfile = os.path.join(cloned, "change_on_second_remote")
open(newfile, "w").close()
repo.index.add([newfile])
repo.index.commit("commit_on_second_remote")
repo.remotes.origin.push(f"HEAD:{worktree_name}", force=True)
if remote_branch_with_prefix_already_exists:
with tempfile.TemporaryDirectory() as cloned:
repo = git.Repo.clone_from(directory, cloned)
newfile = os.path.join(cloned, "change2")
open(newfile, "w").close()
repo.index.add([newfile])
repo.index.commit("commit")
if remotes_differ:
newfile = os.path.join(cloned, "change_on_second_remote2")
open(newfile, "w").close()
repo.index.add([newfile])
repo.index.commit("commit_on_second_remote2")
repo.remotes.origin.push(f"HEAD:myprefix/{worktree_name}", force=True)
return "_".join(
[
str(worktree_with_slash),
str(remote_branch_already_exists),
str(remote_branch_with_prefix_already_exists),
str(remotes_differ),
]
)
cachefn = lambda nr: "_".join(
[
str(nr),
str(default_remote),
str(local_branch_exists),
str(remote_branch_already_exists),
str(remote_branch_with_prefix_already_exists),
str(remote_count),
str(remotes_differ),
str(worktree_name),
]
)
remote1_cache_key = cachefn(1)
remote2_cache_key = cachefn(2)
cachekey = "_".join(
[
str(local_branch_exists),
str(local_branch_has_tracking_branch),
str(remote_branch_already_exists),
str(remote_branch_with_prefix_already_exists),
str(remote_count),
str(remotes_differ),
str(worktree_name),
]
)
with TempGitRepositoryWorktree.get(
cachekey=cachekey,
branch=worktree_name if local_branch_exists else None,
remotes=remote_count,
remote_setup=[
[remote1_cache_key, setup_remote1],
[remote2_cache_key, setup_remote2],
],
) as (base_dir, initial_commit):
repo = git.Repo(os.path.join(base_dir, ".git-main-working-tree"))
if config_enabled:
with open(os.path.join(base_dir, "grm.toml"), "w") as f:
f.write(
f"""
[track]
default = {str(has_default).lower()}
default_remote = "origin"
"""
[track]
default = {str(config_has_default_track_enabled).lower()}
default_remote = "{default_remote}"
"""
)
if has_prefix:
if config_has_default_remote_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:{worktree_name}
#git reset --hard 'HEAD@{1}'
git branch -va
)
git --git-dir ./.git-main-working-tree worktree remove tmp
"""
)
cmd = grm(["wt", "add", worktree_name], cwd=base_dir)
if local_branch_exists:
if has_remotes and local_branch_has_tracking_branch:
origin = repo.remote(default_remote)
if remote_count >= 2:
otherremote = repo.remote("otherremote")
br = list(filter(lambda x: x.name == worktree_name, repo.branches))[0]
assert os.path.exists(base_dir)
if track_differs_from_existing_branch_upstream:
origin.push(
f"{worktree_name}:someothername", force=True, set_upstream=True
)
if remote_count >= 2:
otherremote.push(
f"{worktree_name}:someothername",
force=True,
set_upstream=True,
)
br.set_tracking_branch(
list(
filter(
lambda x: x.remote_head == "someothername", origin.refs
)
)[0]
)
else:
origin.push(
f"{worktree_name}:{worktree_name}",
force=True,
set_upstream=True,
)
if remote_count >= 2:
otherremote.push(
f"{worktree_name}:{worktree_name}",
force=True,
set_upstream=True,
)
br.set_tracking_branch(
list(
filter(
lambda x: x.remote_head == worktree_name, origin.refs
)
)[0]
)
args = ["wt", "add", worktree_name]
if explicit_track:
args.extend(["--track", explicit_track_branch_name])
if explicit_notrack:
args.extend(["--no-track"])
cmd = grm(args, cwd=base_dir)
if explicit_track and not explicit_notrack and not has_remotes:
assert cmd.returncode != 0
assert f'remote "{default_remote}" not found' in cmd.stderr.lower()
return
assert cmd.returncode == 0
assert len(cmd.stdout.strip().split("\n")) == 1
assert f"worktree {worktree_name} created" in cmd.stdout.lower()
def check_deviation_error(base):
if (
not local_branch_exists
and (explicit_notrack or (not explicit_notrack and not explicit_track))
and (
remote_branch_already_exists
or (
config_enabled
and config_has_default_remote_prefix
and remote_branch_with_prefix_already_exists
)
)
and remote_count >= 2
and remotes_differ
):
assert (
f"branch exists on multiple remotes, but they deviate"
in cmd.stderr.lower()
)
assert len(cmd.stderr.strip().split("\n")) == base + 1
else:
if base == 0:
assert len(cmd.stderr) == base
else:
assert len(cmd.stderr.strip().split("\n")) == base
if explicit_track and explicit_notrack:
assert "--track will be ignored" in cmd.stderr.lower()
check_deviation_error(1)
else:
check_deviation_error(0)
files = os.listdir(base_dir)
if has_config is True:
if config_enabled is True:
if worktree_with_slash:
assert set(files) == {".git-main-working-tree", "grm.toml", "dir"}
else:
assert set(files) == {".git-main-working-tree", "grm.toml", "test"}
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"}
assert set(os.listdir(os.path.join(base_dir, "dir"))) == {"nested"}
assert set(os.listdir(os.path.join(base_dir, "dir/nested"))) == {"test"}
else:
assert set(files) == {".git-main-working-tree", "grm.toml", "test"}
else:
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"}
assert set(os.listdir(os.path.join(base_dir, "dir"))) == {"nested"}
assert set(os.listdir(os.path.join(base_dir, "dir/nested"))) == {"test"}
else:
assert set(files) == {".git-main-working-tree", "test"}
repo = git.Repo(os.path.join(base_dir, worktree_name))
assert not repo.bare
assert not repo.is_dirty()
if has_config and has_default:
if has_prefix and not remote_branch_already_exists:
# assert not repo.is_dirty()
assert str(repo.head.ref) == worktree_name
local_commit = repo.head.commit.hexsha
if not has_remotes:
assert local_commit == initial_commit
elif local_branch_exists:
assert local_commit == initial_commit
elif explicit_track and not explicit_notrack:
assert local_commit == repo.commit(explicit_track_branch_name).hexsha
elif explicit_notrack:
if config_enabled and config_has_default_remote_prefix:
if remote_branch_with_prefix_already_exists:
assert (
local_commit
== repo.commit(
f"{default_remote}/myprefix/{worktree_name}"
).hexsha
)
elif remote_branch_already_exists:
assert (
local_commit
== repo.commit(f"{default_remote}/{worktree_name}").hexsha
)
else:
assert local_commit == initial_commit
elif remote_count == 1:
if config_enabled and config_has_default_remote_prefix:
if remote_branch_with_prefix_already_exists:
assert (
local_commit
== repo.commit(
f"{default_remote}/myprefix/{worktree_name}"
).hexsha
)
elif remote_branch_already_exists:
assert (
local_commit
== repo.commit(f"{default_remote}/{worktree_name}").hexsha
)
else:
assert local_commit == initial_commit
elif remote_branch_already_exists:
assert (
local_commit
== repo.commit(f"{default_remote}/{worktree_name}").hexsha
)
else:
assert local_commit == initial_commit
elif remotes_differ:
if config_enabled: # we have a default remote
if (
config_has_default_remote_prefix
and remote_branch_with_prefix_already_exists
):
assert (
local_commit
== repo.commit(
f"{default_remote}/myprefix/{worktree_name}"
).hexsha
)
elif remote_branch_already_exists:
assert (
local_commit
== repo.commit(f"{default_remote}/{worktree_name}").hexsha
)
else:
assert local_commit == initial_commit
else:
assert local_commit == initial_commit
else:
if config_enabled and config_has_default_remote_prefix:
if remote_branch_with_prefix_already_exists:
assert (
local_commit
== repo.commit(
f"{default_remote}/myprefix/{worktree_name}"
).hexsha
)
elif remote_branch_already_exists:
assert (
local_commit
== repo.commit(f"{default_remote}/{worktree_name}").hexsha
)
else:
assert local_commit == initial_commit
elif config_enabled:
if not config_has_default_remote_prefix:
if config_has_default_track_enabled:
assert (
local_commit
== repo.commit(f"{default_remote}/{worktree_name}").hexsha
)
else:
if remote_branch_already_exists:
assert (
local_commit
== repo.commit(f"{default_remote}/{worktree_name}").hexsha
)
else:
assert local_commit == initial_commit
else:
if remote_branch_with_prefix_already_exists:
assert (
local_commit
== repo.commit(
f"{default_remote}/myprefix/{worktree_name}"
).hexsha
)
elif remote_branch_already_exists:
assert (
local_commit
== repo.commit(f"{default_remote}/{worktree_name}").hexsha
)
elif config_has_default_track_enabled:
assert (
local_commit
== repo.commit(
f"{default_remote}/myprefix/{worktree_name}"
).hexsha
)
else:
assert local_commit == initial_commit
elif remote_branch_already_exists and not remotes_differ:
assert (
local_commit == repo.commit(f"{default_remote}/{worktree_name}").hexsha
)
else:
assert local_commit == initial_commit
# Check whether tracking is ok
if not has_remotes:
assert repo.active_branch.tracking_branch() is None
elif explicit_notrack:
if local_branch_exists and local_branch_has_tracking_branch:
if track_differs_from_existing_branch_upstream:
assert (
str(repo.active_branch.tracking_branch())
== f"{default_remote}/someothername"
)
else:
assert (
str(repo.active_branch.tracking_branch())
== f"{default_remote}/{worktree_name}"
)
else:
assert repo.active_branch.tracking_branch() is None
elif explicit_track:
assert (
str(repo.active_branch.tracking_branch()) == explicit_track_branch_name
)
elif config_enabled and config_has_default_track_enabled:
if config_has_default_remote_prefix:
assert (
str(repo.active_branch.tracking_branch())
== f"origin/myprefix/{worktree_name}"
== f"{default_remote}/myprefix/{worktree_name}"
)
else:
assert (
str(repo.active_branch.tracking_branch())
== f"origin/{worktree_name}"
== f"{default_remote}/{worktree_name}"
)
elif local_branch_exists and local_branch_has_tracking_branch:
if track_differs_from_existing_branch_upstream:
assert (
str(repo.active_branch.tracking_branch())
== f"{default_remote}/someothername"
)
else:
assert (
str(repo.active_branch.tracking_branch())
== f"{default_remote}/{worktree_name}"
)
else:
assert repo.active_branch.tracking_branch() is None
def test_worktree_add_invalid_name():
with TempGitRepositoryWorktree() as (base_dir, _commit):
for worktree_name in ["/absolute/path" "trailingslash/"]:
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
for worktree_name in [
"/absolute/path",
"trailingslash/",
"with spaces",
"with\t tabs",
"with\nnewline",
]:
args = ["wt", "add", worktree_name]
cmd = grm(args, cwd=base_dir)
assert cmd.returncode != 0
print(cmd.stdout)
print(cmd.stderr)
assert not os.path.exists(worktree_name)
assert not os.path.exists(os.path.join(base_dir, worktree_name))
assert "invalid worktree name" in str(cmd.stderr.lower())
def test_worktree_add_invalid_track():
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
for track in ["/absolute/path", "trailingslash/", "/"]:
args = ["wt", "add", "foo", "--track", track]
cmd = grm(args, cwd=base_dir)
assert cmd.returncode != 0
assert len(cmd.stderr.strip().split("\n")) == 1
assert not os.path.exists("foo")
assert not os.path.exists(os.path.join(base_dir, "foo"))
assert "tracking branch" in str(cmd.stderr.lower())
@pytest.mark.parametrize("use_track", [True, False])
@pytest.mark.parametrize("use_configuration", [True, False])
@pytest.mark.parametrize("use_configuration_default", [True, False])
def test_worktree_add_invalid_remote_name(
use_track, use_configuration, use_configuration_default
):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
if use_configuration:
with open(os.path.join(base_dir, "grm.toml"), "w") as f:
f.write(
f"""
[track]
default = {str(use_configuration_default).lower()}
default_remote = "thisremotedoesnotexist"
"""
)
args = ["wt", "add", "foo"]
if use_track:
args.extend(["--track", "thisremotedoesnotexist/master"])
cmd = grm(args, cwd=base_dir)
if use_track or (use_configuration and use_configuration_default):
assert cmd.returncode != 0
assert "thisremotedoesnotexist" in cmd.stderr
else:
assert cmd.returncode == 0
assert len(cmd.stderr) == 0
def test_worktree_add_into_invalid_subdirectory():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
cmd = grm(["wt", "add", "/dir/test"], cwd=base_dir)
assert cmd.returncode == 1
assert "dir" not in os.listdir(base_dir)
@@ -120,116 +573,8 @@ def test_worktree_add_into_invalid_subdirectory():
assert "dir" not in os.listdir(base_dir)
@pytest.mark.parametrize("remote_branch_already_exists", [True, False])
@pytest.mark.parametrize("has_config", [True, False])
@pytest.mark.parametrize("has_default", [True, False])
@pytest.mark.parametrize("has_prefix", [True, False])
def test_worktree_add_with_tracking(
remote_branch_already_exists, has_config, has_default, has_prefix
):
with TempGitRepositoryWorktree() as (base_dir, _commit):
if has_config:
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", "--track", "origin/test"], cwd=base_dir)
print(cmd.stderr)
assert cmd.returncode == 0
files = os.listdir(base_dir)
if has_config is True:
assert len(files) == 3
assert set(files) == {".git-main-working-tree", "grm.toml", "test"}
else:
assert len(files) == 2
assert set(files) == {".git-main-working-tree", "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"
assert str(repo.active_branch.tracking_branch()) == "origin/test"
@pytest.mark.parametrize("has_config", [True, False])
@pytest.mark.parametrize("has_default", [True, False])
@pytest.mark.parametrize("has_prefix", [True, False])
@pytest.mark.parametrize("track", [True, False])
def test_worktree_add_with_explicit_no_tracking(
has_config, has_default, has_prefix, track
):
with TempGitRepositoryWorktree() as (base_dir, _commit):
if has_config:
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 track is True:
cmd = grm(
["wt", "add", "test", "--track", "origin/test", "--no-track"],
cwd=base_dir,
)
else:
cmd = grm(["wt", "add", "test", "--no-track"], cwd=base_dir)
print(cmd.stderr)
assert cmd.returncode == 0
files = os.listdir(base_dir)
if has_config is True:
assert len(files) == 3
assert set(files) == {".git-main-working-tree", "grm.toml", "test"}
else:
assert len(files) == 2
assert set(files) == {".git-main-working-tree", "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"
assert repo.active_branch.tracking_branch() is None
def test_worktree_delete():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
assert "test" in os.listdir(base_dir)
@@ -241,12 +586,35 @@ def test_worktree_delete():
cmd = grm(["wt", "add", "check"], cwd=base_dir)
assert cmd.returncode == 0
repo = git.Repo(os.path.join(base_dir, ".git-main-working-tree"))
print(repo.branches)
assert "test" not in [str(b) for b in repo.branches]
@pytest.mark.parametrize("has_other_worktree", [True, False])
def test_worktree_delete_in_subfolder(has_other_worktree):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
cmd = grm(["wt", "add", "dir/test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
assert "dir" in os.listdir(base_dir)
if has_other_worktree is True:
cmd = grm(
["wt", "add", "dir/test2", "--track", "origin/test"], cwd=base_dir
)
assert cmd.returncode == 0
assert {"test", "test2"} == set(os.listdir(os.path.join(base_dir, "dir")))
else:
assert {"test"} == set(os.listdir(os.path.join(base_dir, "dir")))
cmd = grm(["wt", "delete", "dir/test"], cwd=base_dir)
assert cmd.returncode == 0
if has_other_worktree is True:
assert {"test2"} == set(os.listdir(os.path.join(base_dir, "dir")))
else:
assert "dir" not in os.listdir(base_dir)
def test_worktree_delete_refusal_no_tracking_branch():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
cmd = grm(["wt", "add", "test"], cwd=base_dir)
assert cmd.returncode == 0
@@ -262,7 +630,7 @@ def test_worktree_delete_refusal_no_tracking_branch():
def test_worktree_delete_refusal_uncommited_changes_new_file():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
@@ -280,7 +648,7 @@ def test_worktree_delete_refusal_uncommited_changes_new_file():
def test_worktree_delete_refusal_uncommited_changes_changed_file():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
@@ -298,7 +666,7 @@ def test_worktree_delete_refusal_uncommited_changes_changed_file():
def test_worktree_delete_refusal_uncommited_changes_deleted_file():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
@@ -318,7 +686,7 @@ def test_worktree_delete_refusal_uncommited_changes_deleted_file():
def test_worktree_delete_refusal_commited_changes():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
@@ -338,7 +706,7 @@ def test_worktree_delete_refusal_commited_changes():
def test_worktree_delete_refusal_tracking_branch_mismatch():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
@@ -358,7 +726,7 @@ def test_worktree_delete_refusal_tracking_branch_mismatch():
def test_worktree_delete_force_refusal():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
cmd = grm(["wt", "add", "test"], cwd=base_dir)
assert cmd.returncode == 0
@@ -368,7 +736,7 @@ def test_worktree_delete_force_refusal():
def test_worktree_add_delete_add():
with TempGitRepositoryWorktree() as (base_dir, _commit):
with TempGitRepositoryWorktree.get(funcname()) as (base_dir, _commit):
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
assert "test" in os.listdir(base_dir)

View File

@@ -483,6 +483,9 @@ fn main() {
match args.action {
cmd::WorktreeAction::Add(action_args) => {
if action_args.track.is_some() && action_args.no_track {
print_warning("You are using --track and --no-track at the same time. --track will be ignored");
}
let track = match &action_args.track {
Some(branch) => {
let split = branch.split_once('/');
@@ -510,7 +513,14 @@ fn main() {
track,
action_args.no_track,
) {
Ok(_) => print_success(&format!("Worktree {} created", &action_args.name)),
Ok(warnings) => {
if let Some(warnings) = warnings {
for warning in warnings {
print_warning(&warning);
}
}
print_success(&format!("Worktree {} created", &action_args.name));
}
Err(error) => {
print_error(&format!("Error creating worktree: {}", error));
process::exit(1);
@@ -518,8 +528,6 @@ fn main() {
}
}
cmd::WorktreeAction::Delete(action_args) => {
let worktree_dir = cwd.join(&action_args.name);
let worktree_config = match repo::read_worktree_root_config(&cwd) {
Ok(config) => config,
Err(error) => {
@@ -537,8 +545,9 @@ fn main() {
});
match repo.remove_worktree(
&cwd,
&action_args.name,
&worktree_dir,
Path::new(&action_args.name),
action_args.force,
&worktree_config,
) {

View File

@@ -14,8 +14,6 @@ pub mod table;
pub mod tree;
pub mod worktree;
const BRANCH_NAMESPACE_SEPARATOR: &str = "/";
/// Find all git repositories under root, recursively
///
/// The bool in the return value specifies whether there is a repository

View File

@@ -1153,18 +1153,21 @@ impl RepoHandle {
pub fn remove_worktree(
&self,
base_dir: &Path,
name: &str,
worktree_dir: &Path,
force: bool,
worktree_config: &Option<WorktreeRootConfig>,
) -> Result<(), WorktreeRemoveFailureReason> {
if !worktree_dir.exists() {
let fullpath = base_dir.join(worktree_dir);
if !fullpath.exists() {
return Err(WorktreeRemoveFailureReason::Error(format!(
"{} does not exist",
name
)));
}
let worktree_repo = RepoHandle::open(worktree_dir, false).map_err(|error| {
let worktree_repo = RepoHandle::open(&fullpath, false).map_err(|error| {
WorktreeRemoveFailureReason::Error(format!("Error opening repo: {}", error))
})?;
@@ -1176,12 +1179,11 @@ impl RepoHandle {
WorktreeRemoveFailureReason::Error(format!("Failed getting name of branch: {}", error))
})?;
if branch_name != name
&& !branch_name.ends_with(&format!("{}{}", super::BRANCH_NAMESPACE_SEPARATOR, name))
{
if branch_name != name {
return Err(WorktreeRemoveFailureReason::Error(format!(
"Branch \"{}\" is checked out in worktree, this does not look correct",
&branch_name
"Branch \"{}\" is checked out in worktree \"{}\", this does not look correct",
&branch_name,
&worktree_dir.display(),
)));
}
@@ -1251,13 +1253,47 @@ impl RepoHandle {
}
}
if let Err(e) = std::fs::remove_dir_all(&worktree_dir) {
// worktree_dir is a relative path, starting from base_dir. We walk it
// upwards (from subdirectory to parent directories) and remove each
// component, in case it is empty. Only the leaf directory can be
// removed unconditionally (as it contains the worktree itself).
if let Err(e) = std::fs::remove_dir_all(&fullpath) {
return Err(WorktreeRemoveFailureReason::Error(format!(
"Error deleting {}: {}",
&worktree_dir.display(),
e
)));
}
if let Some(current_dir) = worktree_dir.parent() {
for current_dir in current_dir.ancestors() {
let current_dir = base_dir.join(current_dir);
println!("deleting {}", current_dir.display());
if current_dir
.read_dir()
.map_err(|error| {
WorktreeRemoveFailureReason::Error(format!(
"Error reading {}: {}",
&current_dir.display(),
error
))
})?
.next()
.is_none()
{
if let Err(e) = std::fs::remove_dir_all(&current_dir) {
return Err(WorktreeRemoveFailureReason::Error(format!(
"Error deleting {}: {}",
&worktree_dir.display(),
e
)));
}
} else {
break;
}
}
}
self.prune_worktree(name)
.map_err(WorktreeRemoveFailureReason::Error)?;
branch
@@ -1310,7 +1346,13 @@ impl RepoHandle {
{
let repo_dir = &directory.join(&worktree.name());
if repo_dir.exists() {
match self.remove_worktree(worktree.name(), repo_dir, false, &config) {
match self.remove_worktree(
directory,
worktree.name(),
Path::new(worktree.name()),
false,
&config,
) {
Ok(_) => print_success(&format!("Worktree {} deleted", &worktree.name())),
Err(error) => match error {
WorktreeRemoveFailureReason::Changes(changes) => {
@@ -1435,7 +1477,7 @@ impl<'a> Branch<'a> {
}
}
impl Branch<'_> {
impl<'a> Branch<'a> {
pub fn commit(&self) -> Result<Commit, String> {
Ok(Commit(
self.0
@@ -1445,6 +1487,15 @@ impl Branch<'_> {
))
}
pub fn commit_owned(self) -> Result<Commit<'a>, String> {
Ok(Commit(
self.0
.into_reference()
.peel_to_commit()
.map_err(convert_libgit2_error)?,
))
}
pub fn set_upstream(&mut self, remote_name: &str, branch_name: &str) -> Result<(), String> {
self.0
.set_upstream(Some(&format!("{}/{}", remote_name, branch_name)))

File diff suppressed because it is too large Load Diff