20 Commits
v0.3 ... v0.4

Author SHA1 Message Date
7aa45c7768 Merge branch 'develop' 2021-11-29 01:06:09 +01:00
a065de5b2d Release v0.4 2021-11-29 01:05:47 +01:00
d976ccefc2 Merge branch 'e2e' into develop 2021-11-29 00:54:25 +01:00
de186901d0 Support file remotes 2021-11-29 00:53:11 +01:00
c9f4d41780 Properly report push errors 2021-11-29 00:53:11 +01:00
b3906c646a Remove wrong error message about remote branch 2021-11-29 00:53:11 +01:00
43c47bdca6 Fail with non-zero exit code on clippy warnings 2021-11-29 00:45:41 +01:00
df0b5728fc Add test to "check" Justfile target 2021-11-29 00:42:36 +01:00
d0b78686e2 Add an E2E test suite 2021-11-29 00:42:36 +01:00
f02a0fc17a Format cargo update script with black 2021-11-29 00:42:36 +01:00
4e83aba672 Fix formatting of push error message 2021-11-29 00:42:36 +01:00
e2e55b8e79 Properly handle error during repo open 2021-11-29 00:42:36 +01:00
655379cd61 Return failures during sync 2021-11-29 00:42:36 +01:00
340085abf8 Detect change from worktree to non-worktree during sync 2021-11-29 00:42:36 +01:00
f5f8dfa188 Better error message when config not found 2021-11-29 00:42:36 +01:00
e43d4bf3cd Split unit and integ tests in Justfile 2021-11-29 00:23:42 +01:00
48f3bc0199 Support file remotes 2021-11-28 16:23:30 +01:00
0973ae36b8 Properly report push errors 2021-11-28 16:22:22 +01:00
09c67d4908 Remove wrong error message about remote branch 2021-11-28 16:22:22 +01:00
47841dadfb Release v0.3 2021-11-26 17:27:39 +01:00
17 changed files with 1696 additions and 45 deletions

2
Cargo.lock generated
View File

@@ -188,7 +188,7 @@ dependencies = [
[[package]] [[package]]
name = "git-repo-manager" name = "git-repo-manager"
version = "0.3.0" version = "0.4.0"
dependencies = [ dependencies = [
"clap", "clap",
"comfy-table", "comfy-table",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "git-repo-manager" name = "git-repo-manager"
version = "0.3.0" version = "0.4.0"
edition = "2021" edition = "2021"
authors = [ authors = [
"Hannes Körber <hannes@hkoerber.de>", "Hannes Körber <hannes@hkoerber.de>",

View File

@@ -1,7 +1,7 @@
check: check: test
cargo check cargo check
cargo fmt --check cargo fmt --check
cargo clippy --no-deps cargo clippy --no-deps -- -Dwarnings
lint-fix: lint-fix:
cargo clippy --no-deps --fix cargo clippy --no-deps --fix
@@ -12,9 +12,26 @@ release:
install: install:
cargo install --path . cargo install --path .
test: test: test-unit test-integration test-e2e
test-unit:
cargo test --lib --bins cargo test --lib --bins
test-integration:
cargo test --test "*"
e2e-venv:
cd ./e2e_tests \
&& python3 -m venv venv \
&& . ./venv/bin/activate \
&& pip --disable-pip-version-check install -r ./requirements.txt >/dev/null
test-e2e: e2e-venv release
cd ./e2e_tests \
&& . ./venv/bin/activate \
&& python -m pytest .
update-dependencies: update-dependencies:
@cd ./depcheck \ @cd ./depcheck \
&& python3 -m venv ./venv \ && python3 -m venv ./venv \

2
e2e_tests/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/venv/
/__pycache__/

229
e2e_tests/helpers.py Normal file
View File

@@ -0,0 +1,229 @@
#!/usr/bin/env python3
import os
import os.path
import subprocess
import tempfile
import hashlib
import git
binary = os.path.join(
os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "target/release/grm"
)
def grm(args, cwd=None, is_invalid=False):
cmd = subprocess.run([binary] + args, cwd=cwd, capture_output=True, text=True)
if not is_invalid:
assert "USAGE" not in cmd.stderr
print(f"grmcmd: {args}")
print(f"stdout:\n{cmd.stdout}")
print(f"stderr:\n{cmd.stderr}")
assert "panicked" not in cmd.stderr
return cmd
def shell(script):
script = "set -o errexit\nset -o nounset\n" + script
subprocess.run(["bash"], input=script, text=True, check=True)
def checksum_directory(path):
"""
Gives a "checksum" of a directory that includes all files & directories
recursively, including owner/group/permissions. Useful to compare that a
directory did not change after a command was run.
The following makes it a bit complicated:
> Whether or not the lists are sorted depends on the file system.
- https://docs.python.org/3/library/os.html#os.walk
This means we have to first get a list of all hashes of files and
directories, then sort the hashes and then create the hash for the whole
directory.
"""
path = os.path.realpath(path)
hashes = []
if not os.path.exists(path):
raise f"{path} not found"
def get_stat_hash(path):
stat = bytes(str(os.stat(path).__hash__()), "ascii")
return stat
for root, dirs, files in os.walk(path):
for file in files:
checksum = hashlib.md5()
filepath = os.path.join(root, file)
checksum.update(str.encode(filepath))
checksum.update(get_stat_hash(filepath))
with open(filepath, "rb") as f:
while True:
data = f.read(8192)
if not data:
break
checksum.update(data)
hashes.append(checksum.digest())
for d in dirs:
checksum = hashlib.md5()
dirpath = os.path.join(root, d)
checksum.update(get_stat_hash(dirpath))
hashes.append(checksum.digest())
checksum = hashlib.md5()
for c in sorted(hashes):
checksum.update(c)
return checksum.hexdigest()
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"""
cd {self.tmpdir.name}
git init
echo test > test
git add test
git commit -m "commit1"
git remote add origin file://{self.remote_1_dir.name}
git remote add otherremote file://{self.remote_2_dir.name}
"""
)
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
class TempGitRepositoryWorktree:
def __init__(self):
pass
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 > test
git add test
git commit -m "commit1"
echo test > test2
git add test2
git commit -m "commit2"
git remote add origin file://{self.remote_1_dir.name}
git remote add otherremote file://{self.remote_2_dir.name}
git ls-files | xargs rm -rf
mv .git .git-main-working-tree
git --git-dir .git-main-working-tree config core.bare true
"""
)
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
class EmptyDir:
def __init__(self):
pass
def __enter__(self):
self.tmpdir = tempfile.TemporaryDirectory()
return self.tmpdir.name
def __exit__(self, exc_type, exc_val, exc_tb):
del self.tmpdir
class NonGitDir:
def __init__(self):
pass
def __enter__(self):
self.tmpdir = tempfile.TemporaryDirectory()
shell(
f"""
cd {self.tmpdir.name}
mkdir testdir
touch testdir/test
touch test2
"""
)
return self.tmpdir.name
def __exit__(self, exc_type, exc_val, exc_tb):
del self.tmpdir
class TempGitFileRemote:
def __init__(self):
pass
def __enter__(self):
self.tmpdir = tempfile.TemporaryDirectory()
shell(
f"""
cd {self.tmpdir.name}
git init
echo test > test
git add test
git commit -m "commit1"
echo test > test2
git add test2
git commit -m "commit2"
git ls-files | xargs rm -rf
mv .git/* .
git config core.bare true
"""
)
head_commit_sha = git.Repo(self.tmpdir.name).head.commit.hexsha
return (self.tmpdir.name, head_commit_sha)
def __exit__(self, exc_type, exc_val, exc_tb):
del self.tmpdir
class NonExistentPath:
def __init__(self):
pass
def __enter__(self):
self.dir = "/doesnotexist"
if os.path.exists(self.dir):
raise f"{self.dir} exists for some reason"
return self.dir
def __exit__(self, exc_type, exc_val, exc_tb):
pass

View File

@@ -0,0 +1,12 @@
attrs==21.2.0
gitdb==4.0.9
GitPython==3.1.24
iniconfig==1.1.1
packaging==21.3
pluggy==1.0.0
py==1.11.0
pyparsing==3.0.6
pytest==6.2.5
smmap==5.0.0
toml==0.10.2
typing-extensions==4.0.0

13
e2e_tests/test_basic.py Normal file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/env python3
from helpers import *
def test_invalid_command():
cmd = grm(["whatever"], is_invalid=True)
assert "USAGE" in cmd.stderr
def test_help():
cmd = grm(["--help"])
assert "USAGE" in cmd.stdout

View File

@@ -0,0 +1,160 @@
#!/usr/bin/env python3
import tempfile
import toml
from helpers import *
def test_repos_find_nonexistent():
with NonExistentPath() as nonexistent_dir:
cmd = grm(["repos", "find", nonexistent_dir])
assert "does not exist" in cmd.stderr.lower()
assert cmd.returncode != 0
assert not os.path.exists(nonexistent_dir)
def test_repos_find_file():
with tempfile.NamedTemporaryFile() as tmpfile:
cmd = grm(["repos", "find", tmpfile.name])
assert "not a directory" in cmd.stderr.lower()
assert cmd.returncode != 0
def test_repos_find_empty():
with tempfile.TemporaryDirectory() as tmpdir:
cmd = grm(["repos", "find", tmpdir])
assert cmd.returncode == 0
assert len(cmd.stdout) == 0
assert len(cmd.stderr) != 0
def test_repos_find_non_git_repos():
with tempfile.TemporaryDirectory() as tmpdir:
shell(
f"""
cd {tmpdir}
mkdir non_git
(
cd ./non_git
echo test > test
)
"""
)
cmd = grm(["repos", "find", tmpdir])
assert cmd.returncode == 0
assert len(cmd.stdout) == 0
assert len(cmd.stderr) != 0
def test_repos_find():
with tempfile.TemporaryDirectory() as tmpdir:
shell(
f"""
cd {tmpdir}
mkdir repo1
(
cd ./repo1
git init
echo test > test
git add test
git commit -m "commit1"
git remote add origin https://example.com/repo2.git
git remote add someremote ssh://example.com/repo2.git
)
mkdir repo2
(
cd ./repo2
git init
git co -b main
echo test > test
git add test
git commit -m "commit1"
git remote add origin https://example.com/repo2.git
)
mkdir non_git
(
cd non_git
echo test > test
)
"""
)
cmd = grm(["repos", "find", tmpdir])
assert cmd.returncode == 0
assert len(cmd.stderr) == 0
output = toml.loads(cmd.stdout)
assert isinstance(output, dict)
assert set(output.keys()) == {"trees"}
assert isinstance(output["trees"], list)
assert len(output["trees"]) == 1
for tree in output["trees"]:
assert set(tree.keys()) == {"root", "repos"}
assert tree["root"] == tmpdir
assert isinstance(tree["repos"], list)
assert len(tree["repos"]) == 2
repo1 = [r for r in tree["repos"] if r["name"] == "repo1"][0]
assert repo1["worktree_setup"] is False
assert isinstance(repo1["remotes"], list)
assert len(repo1["remotes"]) == 2
origin = [r for r in repo1["remotes"] if r["name"] == "origin"][0]
assert set(origin.keys()) == {"name", "type", "url"}
assert origin["type"] == "https"
assert origin["url"] == "https://example.com/repo2.git"
someremote = [r for r in repo1["remotes"] if r["name"] == "someremote"][0]
assert set(origin.keys()) == {"name", "type", "url"}
assert someremote["type"] == "ssh"
assert someremote["url"] == "ssh://example.com/repo2.git"
repo2 = [r for r in tree["repos"] if r["name"] == "repo2"][0]
assert repo2["worktree_setup"] is False
assert isinstance(repo1["remotes"], list)
assert len(repo2["remotes"]) == 1
origin = [r for r in repo2["remotes"] if r["name"] == "origin"][0]
assert set(origin.keys()) == {"name", "type", "url"}
assert origin["type"] == "https"
assert origin["url"] == "https://example.com/repo2.git"
def test_repos_find_in_root():
with TempGitRepository() as repo_dir:
cmd = grm(["repos", "find", repo_dir])
assert cmd.returncode == 0
assert len(cmd.stderr) == 0
output = toml.loads(cmd.stdout)
assert isinstance(output, dict)
assert set(output.keys()) == {"trees"}
assert isinstance(output["trees"], list)
assert len(output["trees"]) == 1
for tree in output["trees"]:
assert set(tree.keys()) == {"root", "repos"}
assert tree["root"] == os.path.dirname(repo_dir)
assert isinstance(tree["repos"], list)
assert len(tree["repos"]) == 1
repo1 = [
r for r in tree["repos"] if r["name"] == os.path.basename(repo_dir)
][0]
assert repo1["worktree_setup"] is False
assert isinstance(repo1["remotes"], list)
assert len(repo1["remotes"]) == 2
origin = [r for r in repo1["remotes"] if r["name"] == "origin"][0]
assert set(origin.keys()) == {"name", "type", "url"}
assert origin["type"] == "file"
someremote = [r for r in repo1["remotes"] if r["name"] == "otherremote"][0]
assert set(origin.keys()) == {"name", "type", "url"}
assert someremote["type"] == "file"

View File

@@ -0,0 +1,707 @@
#!/usr/bin/env python3
import tempfile
import re
import pytest
import toml
import git
from helpers import *
def test_repos_sync_config_is_valid_symlink():
with tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote, head_commit_sha):
with tempfile.NamedTemporaryFile() as config:
with tempfile.TemporaryDirectory() as config_dir:
config_symlink = os.path.join(config_dir, "cfglink")
os.symlink(config.name, config_symlink)
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote}"
type = "file"
"""
)
cmd = grm(["repos", "sync", "--config", config_symlink])
assert cmd.returncode == 0
git_dir = os.path.join(target, "test")
assert os.path.exists(git_dir)
with git.Repo(git_dir) as repo:
assert not repo.bare
assert not repo.is_dirty()
assert set([str(r) for r in repo.remotes]) == {"origin"}
assert str(repo.active_branch) == "master"
assert str(repo.head.commit) == head_commit_sha
def test_repos_sync_config_is_invalid_symlink():
with tempfile.TemporaryDirectory() as target:
with tempfile.TemporaryDirectory() as config_dir:
with NonExistentPath() as nonexistent_dir:
config_symlink = os.path.join(config_dir, "cfglink")
os.symlink(nonexistent_dir, config_symlink)
cmd = grm(["repos", "sync", "--config", config_symlink])
assert cmd.returncode != 0
assert len(cmd.stdout) == 0
assert "not found" in cmd.stderr.lower()
assert not os.path.exists(os.path.join(target, "test"))
assert not os.path.exists(os.path.join(target, "test"))
def test_repos_sync_config_is_directory():
with tempfile.TemporaryDirectory() as config:
cmd = grm(["repos", "sync", "--config", config])
assert cmd.returncode != 0
assert len(cmd.stdout) == 0
assert "is a directory" in cmd.stderr.lower()
def test_repos_sync_config_is_unreadable():
with tempfile.TemporaryDirectory() as config_dir:
config_path = os.path.join(config_dir, "cfg")
open(config_path, "w")
os.chmod(config_path, 0o0000)
cmd = grm(["repos", "sync", "--config", config_path])
assert os.path.exists(config_path)
assert cmd.returncode != 0
assert len(cmd.stdout) == 0
assert "permission denied" in cmd.stderr.lower()
def test_repos_sync_unmanaged_repos():
with tempfile.TemporaryDirectory() as root:
with TempGitRepository(dir=root) as unmanaged_repo:
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{root}"
[[trees.repos]]
name = "test"
"""
)
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode == 0
git_dir = os.path.join(root, "test")
assert os.path.exists(git_dir)
# this removes the prefix (root) from the path (unmanaged_repo)
unmanaged_repo_name = os.path.relpath(unmanaged_repo, root)
regex = f".*unmanaged.*{unmanaged_repo_name}"
assert any([re.match(regex, l) for l in cmd.stderr.lower().split("\n")])
def test_repos_sync_root_is_file():
with tempfile.NamedTemporaryFile() as target:
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target.name}"
[[trees.repos]]
name = "test"
"""
)
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode != 0
assert len(cmd.stdout) == 0
assert "not a directory" in cmd.stderr.lower()
def test_repos_sync_normal_clone():
with tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
[[trees.repos.remotes]]
name = "origin2"
url = "file://{remote2}"
type = "file"
"""
)
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode == 0
git_dir = os.path.join(target, "test")
assert os.path.exists(git_dir)
with git.Repo(git_dir) as repo:
assert not repo.bare
assert not repo.is_dirty()
assert set([str(r) for r in repo.remotes]) == {
"origin",
"origin2",
}
assert str(repo.active_branch) == "master"
assert str(repo.head.commit) == remote1_head_commit_sha
assert len(repo.remotes) == 2
urls = list(repo.remote("origin").urls)
assert len(urls) == 1
assert urls[0] == f"file://{remote1}"
urls = list(repo.remote("origin2").urls)
assert len(urls) == 1
assert urls[0] == f"file://{remote2}"
def test_repos_sync_normal_init():
with tempfile.TemporaryDirectory() as target:
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
"""
)
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode == 0
git_dir = os.path.join(target, "test")
assert os.path.exists(git_dir)
with git.Repo(git_dir) as repo:
assert not repo.bare
assert not repo.is_dirty()
# as there are no commits yet, HEAD does not point to anything
# valid
assert not repo.head.is_valid()
def test_repos_sync_normal_add_remote():
with tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
"""
)
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode == 0
git_dir = os.path.join(target, "test")
assert os.path.exists(git_dir)
with git.Repo(git_dir) as repo:
assert not repo.bare
assert not repo.is_dirty()
assert set([str(r) for r in repo.remotes]) == {"origin"}
assert str(repo.active_branch) == "master"
assert str(repo.head.commit) == remote1_head_commit_sha
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
[[trees.repos.remotes]]
name = "origin2"
url = "file://{remote2}"
type = "file"
"""
)
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode == 0
with git.Repo(git_dir) as repo:
assert set([str(r) for r in repo.remotes]) == {
"origin",
"origin2",
}
urls = list(repo.remote("origin").urls)
assert len(urls) == 1
assert urls[0] == f"file://{remote1}"
urls = list(repo.remote("origin2").urls)
assert len(urls) == 1
assert urls[0] == f"file://{remote2}"
def test_repos_sync_normal_remove_remote():
with tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
[[trees.repos.remotes]]
name = "origin2"
url = "file://{remote2}"
type = "file"
"""
)
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode == 0
git_dir = os.path.join(target, "test")
assert os.path.exists(git_dir)
with git.Repo(git_dir) as repo:
assert not repo.bare
assert not repo.is_dirty()
assert set([str(r) for r in repo.remotes]) == {
"origin",
"origin2",
}
assert str(repo.active_branch) == "master"
assert str(repo.head.commit) == remote1_head_commit_sha
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin2"
url = "file://{remote2}"
type = "file"
"""
)
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode == 0
shell(f"cd {git_dir} && git remote -v")
with git.Repo(git_dir) as repo:
"""
There is some bug(?) in GitPython. It does not properly
detect removed remotes. It will still report the old
remove in repo.remotes.
So instead, we make sure that we get an Exception when
we try to access the old remove via repo.remote().
Note that repo.remote() checks the actual repo lazily.
Even `exists()` seems to just check against repo.remotes
and will return True even if the remote is not actually
configured. So we have to force GitPython to hit the filesystem.
calling Remotes.urls does. But it returns an iterator
that first has to be unwrapped via list(). Only THEN
do we actually get an exception of the remotes does not
exist.
"""
with pytest.raises(git.exc.GitCommandError):
list(repo.remote("origin").urls)
urls = list(repo.remote("origin2").urls)
assert len(urls) == 1
assert urls[0] == f"file://{remote2}"
def test_repos_sync_normal_change_remote_url():
with tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
"""
)
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode == 0
git_dir = os.path.join(target, "test")
assert os.path.exists(git_dir)
with git.Repo(git_dir) as repo:
assert not repo.bare
assert not repo.is_dirty()
assert set([str(r) for r in repo.remotes]) == {"origin"}
assert str(repo.active_branch) == "master"
assert str(repo.head.commit) == remote1_head_commit_sha
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote2}"
type = "file"
"""
)
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode == 0
with git.Repo(git_dir) as repo:
assert set([str(r) for r in repo.remotes]) == {"origin"}
urls = list(repo.remote("origin").urls)
assert len(urls) == 1
assert urls[0] == f"file://{remote2}"
def test_repos_sync_normal_change_remote_name():
with tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
"""
)
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode == 0
git_dir = os.path.join(target, "test")
assert os.path.exists(git_dir)
with git.Repo(git_dir) as repo:
assert not repo.bare
assert not repo.is_dirty()
assert set([str(r) for r in repo.remotes]) == {"origin"}
assert str(repo.active_branch) == "master"
assert str(repo.head.commit) == remote1_head_commit_sha
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin2"
url = "file://{remote1}"
type = "file"
"""
)
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode == 0
with git.Repo(git_dir) as repo:
# See the note in `test_repos_sync_normal_remove_remote()`
# about repo.remotes
with pytest.raises(git.exc.GitCommandError):
list(repo.remote("origin").urls)
urls = list(repo.remote("origin2").urls)
assert len(urls) == 1
assert urls[0] == f"file://{remote1}"
def test_repos_sync_worktree_clone():
with tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote, head_commit_sha):
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
worktree_setup = true
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote}"
type = "file"
"""
)
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode == 0
worktree_dir = f"{target}/test"
assert os.path.exists(worktree_dir)
assert set(os.listdir(worktree_dir)) == {".git-main-working-tree"}
with git.Repo(
os.path.join(worktree_dir, ".git-main-working-tree")
) as repo:
assert repo.bare
assert set([str(r) for r in repo.remotes]) == {"origin"}
assert str(repo.active_branch) == "master"
assert str(repo.head.commit) == head_commit_sha
def test_repos_sync_worktree_init():
with tempfile.TemporaryDirectory() as target:
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
worktree_setup = true
"""
)
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode == 0
worktree_dir = f"{target}/test"
assert os.path.exists(worktree_dir)
assert set(os.listdir(worktree_dir)) == {".git-main-working-tree"}
with git.Repo(os.path.join(worktree_dir, ".git-main-working-tree")) as repo:
assert repo.bare
# as there are no commits yet, HEAD does not point to anything
# valid
assert not repo.head.is_valid()
def test_repos_sync_invalid_toml():
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = invalid as there are no quotes ;)
"""
)
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode != 0
def test_repos_sync_unchanged():
with tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
[[trees.repos.remotes]]
name = "origin2"
url = "file://{remote2}"
type = "file"
"""
)
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode == 0
before = checksum_directory(target)
cmd = grm(["repos", "sync", "--config", config.name])
after = checksum_directory(target)
assert cmd.returncode == 0
assert before == after
def test_repos_sync_normal_change_to_worktree():
with tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
"""
)
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode == 0
git_dir = os.path.join(target, "test")
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
worktree_setup = true
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
"""
)
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode != 0
assert "already exists" in cmd.stderr
assert "not using a worktree setup" in cmd.stderr
def test_repos_sync_worktree_change_to_normal():
with tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
worktree_setup = true
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
"""
)
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode == 0
git_dir = os.path.join(target, "test")
with open(config.name, "w") as f:
f.write(
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
"""
)
cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode != 0
assert "already exists" in cmd.stderr
assert "using a worktree setup" in cmd.stderr

View File

@@ -0,0 +1,153 @@
#!/usr/bin/env python3
from helpers import *
def test_worktree_clean():
with TempGitRepositoryWorktree() as base_dir:
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
assert "test" in os.listdir(base_dir)
cmd = grm(["wt", "clean"], cwd=base_dir)
assert cmd.returncode == 0
assert "test" not in os.listdir(base_dir)
def test_worktree_clean_refusal_no_tracking_branch():
with TempGitRepositoryWorktree() as base_dir:
before = checksum_directory(base_dir)
cmd = grm(["wt", "add", "test"], cwd=base_dir)
assert cmd.returncode == 0
before = checksum_directory(f"{base_dir}/test")
cmd = grm(["wt", "clean"], cwd=base_dir)
assert cmd.returncode == 0
assert "test" in os.listdir(base_dir)
after = checksum_directory(f"{base_dir}/test")
assert before == after
def test_worktree_clean_refusal_uncommited_changes_new_file():
with TempGitRepositoryWorktree() as base_dir:
before = checksum_directory(base_dir)
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
shell(f"cd {base_dir}/test && touch changed_file")
before = checksum_directory(f"{base_dir}/test")
cmd = grm(["wt", "clean"], cwd=base_dir)
assert cmd.returncode == 0
assert "test" in os.listdir(base_dir)
after = checksum_directory(f"{base_dir}/test")
assert before == after
def test_worktree_clean_refusal_uncommited_changes_changed_file():
with TempGitRepositoryWorktree() as base_dir:
before = checksum_directory(base_dir)
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
shell(f"cd {base_dir}/test && git ls-files | shuf | head | xargs rm -rf")
before = checksum_directory(f"{base_dir}/test")
cmd = grm(["wt", "clean"], cwd=base_dir)
assert cmd.returncode == 0
assert "test" in os.listdir(base_dir)
after = checksum_directory(f"{base_dir}/test")
assert before == after
def test_worktree_clean_refusal_uncommited_changes_cleand_file():
with TempGitRepositoryWorktree() as base_dir:
before = checksum_directory(base_dir)
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
shell(
f"cd {base_dir}/test && git ls-files | shuf | head | while read f ; do echo $RANDOM > $f ; done"
)
before = checksum_directory(f"{base_dir}/test")
cmd = grm(["wt", "clean"], cwd=base_dir)
assert cmd.returncode == 0
assert "test" in os.listdir(base_dir)
after = checksum_directory(f"{base_dir}/test")
assert before == after
def test_worktree_clean_refusal_commited_changes():
with TempGitRepositoryWorktree() as base_dir:
before = checksum_directory(base_dir)
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
shell(
f'cd {base_dir}/test && touch changed_file && git add changed_file && git commit -m "commitmsg"'
)
before = checksum_directory(f"{base_dir}/test")
cmd = grm(["wt", "clean"], cwd=base_dir)
assert cmd.returncode == 0
assert "test" in os.listdir(base_dir)
after = checksum_directory(f"{base_dir}/test")
assert before == after
def test_worktree_clean_refusal_tracking_branch_mismatch():
with TempGitRepositoryWorktree() as base_dir:
before = checksum_directory(base_dir)
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
shell(
f"cd {base_dir}/test && git push origin test && git reset --hard origin/test^"
)
before = checksum_directory(f"{base_dir}/test")
cmd = grm(["wt", "clean"], cwd=base_dir)
assert cmd.returncode == 0
assert "test" in os.listdir(base_dir)
after = checksum_directory(f"{base_dir}/test")
assert before == after
def test_worktree_clean_fail_from_subdir():
with TempGitRepositoryWorktree() as base_dir:
cmd = grm(["wt", "add", "test"], cwd=base_dir)
assert cmd.returncode == 0
cmd = grm(["wt", "clean"], cwd=f"{base_dir}/test")
assert cmd.returncode != 0
assert len(cmd.stdout) == 0
assert len(cmd.stderr) != 0
def test_worktree_clean_non_worktree():
with TempGitRepository() as git_dir:
cmd = grm(["wt", "clean"], cwd=git_dir)
assert cmd.returncode != 0
assert len(cmd.stdout) == 0
assert len(cmd.stderr) != 0
def test_worktree_clean_non_git():
with NonGitDir() as base_dir:
cmd = grm(["wt", "clean"], cwd=base_dir)
assert cmd.returncode != 0
assert len(cmd.stdout) == 0
assert len(cmd.stderr) != 0

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env python3
import tempfile
from helpers import *
def test_convert():
with TempGitRepository() as git_dir:
cmd = grm(["wt", "convert"], cwd=git_dir)
assert cmd.returncode == 0
files = os.listdir(git_dir)
assert len(files) == 1
assert files[0] == ".git-main-working-tree"
cmd = grm(["wt", "add", "test"], cwd=git_dir)
assert cmd.returncode == 0
files = os.listdir(git_dir)
assert len(files) == 2
assert set(files) == {".git-main-working-tree", "test"}
def test_convert_already_worktree():
with TempGitRepositoryWorktree() as git_dir:
before = checksum_directory(git_dir)
cmd = grm(["wt", "convert"], cwd=git_dir)
assert cmd.returncode != 0
after = checksum_directory(git_dir)
assert before == after
def test_convert_non_git():
with NonGitDir() as dir:
before = checksum_directory(dir)
cmd = grm(["wt", "convert"], cwd=dir)
assert cmd.returncode != 0
after = checksum_directory(dir)
assert before == after
def test_convert_empty():
with EmptyDir() as dir:
before = checksum_directory(dir)
cmd = grm(["wt", "convert"], cwd=dir)
assert cmd.returncode != 0
after = checksum_directory(dir)
assert before == after

View File

@@ -0,0 +1,42 @@
#!/usr/bin/env python3
from helpers import *
def test_worktree_status():
with TempGitRepositoryWorktree() as base_dir:
cmd = grm(["wt", "add", "test"], cwd=base_dir)
assert cmd.returncode == 0
cmd = grm(["wt", "status"], cwd=base_dir)
assert cmd.returncode == 0
assert len(cmd.stderr) == 0
stdout = cmd.stdout.lower()
assert "test" in stdout
def test_worktree_status_fail_from_subdir():
with TempGitRepositoryWorktree() as base_dir:
cmd = grm(["wt", "add", "test"], cwd=base_dir)
assert cmd.returncode == 0
cmd = grm(["wt", "status"], cwd=f"{base_dir}/test")
assert cmd.returncode != 0
assert len(cmd.stdout) == 0
assert len(cmd.stderr) != 0
def test_worktree_status_non_worktree():
with TempGitRepository() as git_dir:
cmd = grm(["wt", "status"], cwd=git_dir)
assert cmd.returncode != 0
assert len(cmd.stdout) == 0
assert len(cmd.stderr) != 0
def test_worktree_status_non_git():
with NonGitDir() as base_dir:
cmd = grm(["wt", "status"], cwd=base_dir)
assert cmd.returncode != 0
assert len(cmd.stdout) == 0
assert len(cmd.stderr) != 0

210
e2e_tests/test_worktrees.py Normal file
View File

@@ -0,0 +1,210 @@
#!/usr/bin/env python3
from helpers import *
import git
def test_worktree_add_simple():
with TempGitRepositoryWorktree() as base_dir:
before = checksum_directory(base_dir)
cmd = grm(["wt", "add", "test"], cwd=base_dir)
assert cmd.returncode == 0
files = os.listdir(base_dir)
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_add_with_tracking():
with TempGitRepositoryWorktree() as base_dir:
before = checksum_directory(base_dir)
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
print(cmd.stderr)
assert cmd.returncode == 0
files = os.listdir(base_dir)
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"
def test_worktree_delete():
with TempGitRepositoryWorktree() as base_dir:
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
assert "test" in os.listdir(base_dir)
cmd = grm(["wt", "delete", "test"], cwd=base_dir)
assert cmd.returncode == 0
assert "test" not in os.listdir(base_dir)
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]
def test_worktree_delete_refusal_no_tracking_branch():
with TempGitRepositoryWorktree() as base_dir:
before = checksum_directory(base_dir)
cmd = grm(["wt", "add", "test"], cwd=base_dir)
assert cmd.returncode == 0
before = checksum_directory(f"{base_dir}/test")
cmd = grm(["wt", "delete", "test"], cwd=base_dir)
assert cmd.returncode != 0
stderr = cmd.stderr.lower()
assert "refuse" in stderr or "refusing" in stderr
assert "test" in os.listdir(base_dir)
after = checksum_directory(f"{base_dir}/test")
assert before == after
def test_worktree_delete_refusal_uncommited_changes_new_file():
with TempGitRepositoryWorktree() as base_dir:
before = checksum_directory(base_dir)
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
shell(f"cd {base_dir}/test && touch changed_file")
before = checksum_directory(f"{base_dir}/test")
cmd = grm(["wt", "delete", "test"], cwd=base_dir)
assert cmd.returncode != 0
stderr = cmd.stderr.lower()
assert "refuse" in stderr or "refusing" in stderr
assert "test" in os.listdir(base_dir)
after = checksum_directory(f"{base_dir}/test")
assert before == after
def test_worktree_delete_refusal_uncommited_changes_changed_file():
with TempGitRepositoryWorktree() as base_dir:
before = checksum_directory(base_dir)
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
shell(f"cd {base_dir}/test && git ls-files | shuf | head | xargs rm -rf")
before = checksum_directory(f"{base_dir}/test")
cmd = grm(["wt", "delete", "test"], cwd=base_dir)
assert cmd.returncode != 0
stderr = cmd.stderr.lower()
assert "refuse" in stderr or "refusing" in stderr
assert "test" in os.listdir(base_dir)
after = checksum_directory(f"{base_dir}/test")
assert before == after
def test_worktree_delete_refusal_uncommited_changes_deleted_file():
with TempGitRepositoryWorktree() as base_dir:
before = checksum_directory(base_dir)
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
shell(
f"cd {base_dir}/test && git ls-files | shuf | head | while read f ; do echo $RANDOM > $f ; done"
)
before = checksum_directory(f"{base_dir}/test")
cmd = grm(["wt", "delete", "test"], cwd=base_dir)
assert cmd.returncode != 0
stderr = cmd.stderr.lower()
assert "refuse" in stderr or "refusing" in stderr
assert "test" in os.listdir(base_dir)
after = checksum_directory(f"{base_dir}/test")
assert before == after
def test_worktree_delete_refusal_commited_changes():
with TempGitRepositoryWorktree() as base_dir:
before = checksum_directory(base_dir)
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
shell(
f'cd {base_dir}/test && touch changed_file && git add changed_file && git commit -m "commitmsg"'
)
before = checksum_directory(f"{base_dir}/test")
cmd = grm(["wt", "delete", "test"], cwd=base_dir)
assert cmd.returncode != 0
stderr = cmd.stderr.lower()
assert "refuse" in stderr or "refusing" in stderr
assert "test" in os.listdir(base_dir)
after = checksum_directory(f"{base_dir}/test")
assert before == after
def test_worktree_delete_refusal_tracking_branch_mismatch():
with TempGitRepositoryWorktree() as base_dir:
before = checksum_directory(base_dir)
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
shell(
f"cd {base_dir}/test && git push origin test && git reset --hard origin/test^"
)
before = checksum_directory(f"{base_dir}/test")
cmd = grm(["wt", "delete", "test"], cwd=base_dir)
assert cmd.returncode != 0
stderr = cmd.stderr.lower()
assert "refuse" in stderr or "refusing" in stderr
assert "test" in os.listdir(base_dir)
after = checksum_directory(f"{base_dir}/test")
assert before == after
def test_worktree_delete_force_refusal():
with TempGitRepositoryWorktree() as base_dir:
before = checksum_directory(base_dir)
cmd = grm(["wt", "add", "test"], cwd=base_dir)
assert cmd.returncode == 0
cmd = grm(["wt", "delete", "test", "--force"], cwd=base_dir)
assert cmd.returncode == 0
assert "test" not in os.listdir(base_dir)
def test_worktree_add_delete_add():
with TempGitRepositoryWorktree() as base_dir:
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
assert "test" in os.listdir(base_dir)
cmd = grm(["wt", "delete", "test"], cwd=base_dir)
assert cmd.returncode == 0
assert "test" not in os.listdir(base_dir)
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
assert cmd.returncode == 0
assert "test" in os.listdir(base_dir)

View File

@@ -21,8 +21,12 @@ pub fn read_config(path: &str) -> Result<Config, String> {
Err(e) => { Err(e) => {
return Err(format!( return Err(format!(
"Error reading configuration file \"{}\": {}", "Error reading configuration file \"{}\": {}",
path, e path,
)) match e.kind() {
std::io::ErrorKind::NotFound => String::from("not found"),
_ => e.to_string(),
}
));
} }
}; };

View File

@@ -1,3 +1,5 @@
#![feature(io_error_more)]
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process; use std::process;
@@ -113,7 +115,8 @@ fn get_default_branch(repo: &git2::Repository) -> Result<git2::Branch, String> {
} }
} }
fn sync_trees(config: Config) { fn sync_trees(config: Config) -> bool {
let mut failures = false;
for tree in config.trees { for tree in config.trees {
let repos = tree.repos.unwrap_or_default(); let repos = tree.repos.unwrap_or_default();
@@ -131,17 +134,29 @@ fn sync_trees(config: Config) {
&repo.name, &repo.name,
"Repo already exists, but is not using a worktree setup", "Repo already exists, but is not using a worktree setup",
); );
process::exit(1); failures = true;
continue;
} }
repo_handle = Some(open_repo(&repo_path, repo.worktree_setup).unwrap_or_else( repo_handle = match open_repo(&repo_path, repo.worktree_setup) {
|error| { Ok(repo) => Some(repo),
Err(error) => {
if !repo.worktree_setup {
if open_repo(&repo_path, true).is_ok() {
print_repo_error(
&repo.name,
"Repo already exists, but is using a worktree setup",
);
}
} else {
print_repo_error( print_repo_error(
&repo.name, &repo.name,
&format!("Opening repository failed: {}", error), &format!("Opening repository failed: {}", error),
); );
process::exit(1); }
}, failures = true;
)); continue;
}
};
} else if matches!(&repo.remotes, None) || repo.remotes.as_ref().unwrap().is_empty() { } else if matches!(&repo.remotes, None) || repo.remotes.as_ref().unwrap().is_empty() {
print_repo_action( print_repo_action(
&repo.name, &repo.name,
@@ -188,6 +203,7 @@ fn sync_trees(config: Config) {
&repo.name, &repo.name,
&format!("Repository failed during getting the remotes: {}", e), &format!("Repository failed during getting the remotes: {}", e),
); );
failures = true;
continue; continue;
} }
} }
@@ -210,6 +226,7 @@ fn sync_trees(config: Config) {
&repo.name, &repo.name,
&format!("Repository failed during setting the remotes: {}", e), &format!("Repository failed during setting the remotes: {}", e),
); );
failures = true;
continue; continue;
} }
} else { } else {
@@ -218,6 +235,7 @@ fn sync_trees(config: Config) {
Some(url) => url, Some(url) => url,
None => { None => {
print_repo_error(&repo.name, &format!("Repository failed during getting of the remote URL for remote \"{}\". This is most likely caused by a non-utf8 remote name", remote.name)); print_repo_error(&repo.name, &format!("Repository failed during getting of the remote URL for remote \"{}\". This is most likely caused by a non-utf8 remote name", remote.name));
failures = true;
continue; continue;
} }
}; };
@@ -228,6 +246,7 @@ fn sync_trees(config: Config) {
); );
if let Err(e) = repo_handle.remote_set_url(&remote.name, &remote.url) { if let Err(e) = repo_handle.remote_set_url(&remote.name, &remote.url) {
print_repo_error(&repo.name, &format!("Repository failed during setting of the remote URL for remote \"{}\": {}", &remote.name, e)); print_repo_error(&repo.name, &format!("Repository failed during setting of the remote URL for remote \"{}\": {}", &remote.name, e));
failures = true;
continue; continue;
}; };
} }
@@ -248,6 +267,7 @@ fn sync_trees(config: Config) {
&current_remote, e &current_remote, e
), ),
); );
failures = true;
continue; continue;
} }
} }
@@ -257,7 +277,15 @@ fn sync_trees(config: Config) {
print_repo_success(&repo.name, "OK"); print_repo_success(&repo.name, "OK");
} }
let current_repos = find_repos_without_details(&root_path).unwrap(); let current_repos = match find_repos_without_details(&root_path) {
Ok(repos) => repos,
Err(error) => {
print_error(&error.to_string());
failures = true;
continue;
}
};
for (repo, _) in current_repos { for (repo, _) in current_repos {
let name = path_as_string(repo.strip_prefix(&root_path).unwrap()); let name = path_as_string(repo.strip_prefix(&root_path).unwrap());
if !repos.iter().any(|r| r.name == name) { if !repos.iter().any(|r| r.name == name) {
@@ -265,9 +293,11 @@ fn sync_trees(config: Config) {
} }
} }
} }
!failures
} }
fn find_repos_without_details(path: &Path) -> Option<Vec<(PathBuf, bool)>> { fn find_repos_without_details(path: &Path) -> Result<Vec<(PathBuf, bool)>, String> {
let mut repos: Vec<(PathBuf, bool)> = Vec::new(); let mut repos: Vec<(PathBuf, bool)> = Vec::new();
let git_dir = path.join(".git"); let git_dir = path.join(".git");
@@ -288,26 +318,34 @@ fn find_repos_without_details(path: &Path) -> Option<Vec<(PathBuf, bool)>> {
continue; continue;
} }
if path.is_dir() { if path.is_dir() {
if let Some(mut r) = find_repos_without_details(&path) { match find_repos_without_details(&path) {
repos.append(&mut r); Ok(ref mut r) => repos.append(r),
}; Err(error) => return Err(error),
}
} }
} }
Err(e) => { Err(e) => {
print_error(&format!("Error accessing directory: {}", e)); return Err(format!("Error accessing directory: {}", e));
continue;
} }
}; };
} }
} }
Err(e) => { Err(e) => {
print_error(&format!("Failed to open \"{}\": {}", &path.display(), &e)); return Err(format!(
return None; "Failed to open \"{}\": {}",
&path.display(),
match e.kind() {
std::io::ErrorKind::NotADirectory =>
String::from("directory expected, but path is not a directory"),
std::io::ErrorKind::NotFound => String::from("not found"),
_ => format!("{:?}", e.kind()),
}
));
} }
}; };
} }
Some(repos) Ok(repos)
} }
fn get_actual_git_directory(path: &Path, is_worktree: bool) -> PathBuf { fn get_actual_git_directory(path: &Path, is_worktree: bool) -> PathBuf {
@@ -809,7 +847,9 @@ pub fn run() {
process::exit(1); process::exit(1);
} }
}; };
sync_trees(config); if !sync_trees(config) {
process::exit(1);
}
} }
cmd::ReposAction::Status(args) => match &args.config { cmd::ReposAction::Status(args) => match &args.config {
Some(config_path) => { Some(config_path) => {
@@ -949,10 +989,6 @@ pub fn run() {
.set_upstream(Some(&upstream_branch_name)) .set_upstream(Some(&upstream_branch_name))
.unwrap(); .unwrap();
} else { } else {
print_error(&format!(
"Remote branch {} not found",
&upstream_branch_name
));
let split_at = upstream_branch_name.find('/').unwrap_or(0); let split_at = upstream_branch_name.find('/').unwrap_or(0);
if split_at == 0 || split_at >= upstream_branch_name.len() - 1 { if split_at == 0 || split_at >= upstream_branch_name.len() - 1 {
print_error("Tracking branch needs to match the pattern <remote>/<branch_name>"); print_error("Tracking branch needs to match the pattern <remote>/<branch_name>");
@@ -997,7 +1033,15 @@ pub fn run() {
); );
remote remote
.push(&[push_refspec], Some(&mut push_options)) .push(&[push_refspec], Some(&mut push_options))
.unwrap(); .unwrap_or_else(|error| {
print_error(&format!(
"Pushing to {} ({}) failed: {}",
remote_name,
remote.url().unwrap(),
error
));
process::exit(1);
});
target_branch target_branch
.set_upstream(Some(&upstream_branch_name)) .set_upstream(Some(&upstream_branch_name))

View File

@@ -10,6 +10,7 @@ use crate::output::*;
pub enum RemoteType { pub enum RemoteType {
Ssh, Ssh,
Https, Https,
File,
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@@ -127,6 +128,14 @@ mod tests {
); );
} }
#[test]
fn check_file_remote() {
assert_eq!(
detect_remote_type("file:///somedir"),
Some(RemoteType::File)
);
}
#[test] #[test]
fn check_invalid_remotes() { fn check_invalid_remotes() {
assert_eq!(detect_remote_type("https//example.com"), None); assert_eq!(detect_remote_type("https//example.com"), None);
@@ -147,12 +156,6 @@ mod tests {
fn check_unsupported_protocol_git() { fn check_unsupported_protocol_git() {
detect_remote_type("git://example.com"); detect_remote_type("git://example.com");
} }
#[test]
#[should_panic]
fn check_unsupported_protocol_file() {
detect_remote_type("file:///");
}
} }
pub fn detect_remote_type(remote_url: &str) -> Option<RemoteType> { pub fn detect_remote_type(remote_url: &str) -> Option<RemoteType> {
@@ -166,15 +169,15 @@ pub fn detect_remote_type(remote_url: &str) -> Option<RemoteType> {
if remote_url.starts_with("https://") { if remote_url.starts_with("https://") {
return Some(RemoteType::Https); return Some(RemoteType::Https);
} }
if remote_url.starts_with("file://") {
return Some(RemoteType::File);
}
if remote_url.starts_with("http://") { if remote_url.starts_with("http://") {
unimplemented!("Remotes using HTTP protocol are not supported"); unimplemented!("Remotes using HTTP protocol are not supported");
} }
if remote_url.starts_with("git://") { if remote_url.starts_with("git://") {
unimplemented!("Remotes using git protocol are not supported"); unimplemented!("Remotes using git protocol are not supported");
} }
if remote_url.starts_with("file://") || remote_url.starts_with('/') {
unimplemented!("Remotes using local protocol are not supported");
}
None None
} }
@@ -254,7 +257,7 @@ pub fn clone_repo(
&remote.url &remote.url
)); ));
match remote.remote_type { match remote.remote_type {
RemoteType::Https => { RemoteType::Https | RemoteType::File => {
let mut builder = git2::build::RepoBuilder::new(); let mut builder = git2::build::RepoBuilder::new();
builder.bare(is_worktree); builder.bare(is_worktree);
builder.clone(&remote.url, &clone_target)?; builder.clone(&remote.url, &clone_target)?;