Merge branch 'develop'
This commit is contained in:
42
Cargo.lock
generated
42
Cargo.lock
generated
@@ -195,6 +195,7 @@ dependencies = [
|
||||
"git2",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
"shellexpand",
|
||||
"tempdir",
|
||||
"toml",
|
||||
@@ -336,6 +337,12 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.5"
|
||||
@@ -592,6 +599,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
@@ -600,24 +613,36 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.134"
|
||||
version = "1.0.135"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96b3c34c1690edf8174f5b289a336ab03f568a4460d8c6df75f2f3a692b3bc6a"
|
||||
checksum = "2cf9235533494ea2ddcdb794665461814781c53f19d87b76e571a1c35acbad2b"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.134"
|
||||
version = "1.0.135"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "784ed1fbfa13fe191077537b0d70ec8ad1e903cfe04831da608aa36457cb653d"
|
||||
checksum = "8dcde03d87d4c973c04be249e7d8f0b35db1c848c487bd43032808e59dd8328d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.8.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"ryu",
|
||||
"serde",
|
||||
"yaml-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellexpand"
|
||||
version = "2.1.0"
|
||||
@@ -850,3 +875,12 @@ name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
|
||||
dependencies = [
|
||||
"linked-hash-map",
|
||||
]
|
||||
|
||||
@@ -40,7 +40,7 @@ path = "src/grm/main.rs"
|
||||
version = "=0.5.8"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "=1.0.134"
|
||||
version = "=1.0.135"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.git2]
|
||||
@@ -62,5 +62,8 @@ version = "=1.5.4"
|
||||
[dependencies.comfy-table]
|
||||
version = "=5.0.0"
|
||||
|
||||
[dependencies.serde_yaml]
|
||||
version = "=0.8.23"
|
||||
|
||||
[dev-dependencies.tempdir]
|
||||
version = "=0.3.7"
|
||||
|
||||
3
Justfile
3
Justfile
@@ -52,3 +52,6 @@ check-pip-requirements: e2e-venv
|
||||
@cd ./e2e_tests \
|
||||
&& . ./venv/bin/activate \
|
||||
&& pip list --outdated | grep -q '.' && exit 1 || exit 0
|
||||
|
||||
clean:
|
||||
cargo clean
|
||||
|
||||
@@ -14,7 +14,13 @@ AUTOUPDATE_DISABLED = []
|
||||
|
||||
if os.path.exists(INDEX_DIR):
|
||||
subprocess.run(
|
||||
["git", "pull", "--depth=1", "origin"],
|
||||
["git", "fetch", "--depth=1", "origin"],
|
||||
cwd=INDEX_DIR,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
subprocess.run(
|
||||
["git", "reset", "--hard", "origin/master"],
|
||||
cwd=INDEX_DIR,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
@@ -33,7 +39,7 @@ update_necessary = False
|
||||
|
||||
# This updates the crates.io index, see https://github.com/rust-lang/cargo/issues/3377
|
||||
subprocess.run(
|
||||
["cargo", "search", "--limit", "0"],
|
||||
["cargo", "update", "--dry-run"],
|
||||
check=True,
|
||||
capture_output=False, # to get some git output
|
||||
)
|
||||
|
||||
@@ -5,7 +5,8 @@ Manager](https://github.com/hakoerber/git-repo-manager/) (GRM for short), a
|
||||
tool that helps you manage git repositories.
|
||||
|
||||
GRM helps you manage git repositories in a declarative way. Configure your
|
||||
repositories in a TOML file, GRM does the rest. Take a look at [the example
|
||||
repositories in a TOML or YAML file, GRM does the rest. Take a look at [the
|
||||
example
|
||||
configuration](https://github.com/hakoerber/git-repo-manager/blob/master/example.config.toml)
|
||||
to get a feel for the way you configure your repositories. See the [repository
|
||||
tree chapter](./repos.md) for details.
|
||||
|
||||
@@ -74,3 +74,9 @@ $ grm repos status
|
||||
╰──────────┴──────────┴────────┴──────────┴───────┴─────────╯
|
||||
```
|
||||
|
||||
## YAML
|
||||
|
||||
By default, the repo configuration uses TOML. If you prefer YAML, just give it
|
||||
a YAML file instead (file ending does not matter, `grm` will figure out the format
|
||||
itself). For generating a configuration, pass `--format yaml` to `grm repo find`
|
||||
to generate YAML instead of TOML.
|
||||
|
||||
@@ -309,6 +309,10 @@ grm wt pull --rebase
|
||||
[✔] my-cool-branch: Done
|
||||
```
|
||||
|
||||
As noted, this will fail if there are any local changes in your worktree. If you
|
||||
want to stash these changes automatically before the pull (and unstash them
|
||||
afterwards), use the `--stash` option.
|
||||
|
||||
This will rebase your changes onto the upstream branch. This is mainly helpful
|
||||
for persistent branches that change on the remote side.
|
||||
|
||||
@@ -346,6 +350,10 @@ run two commands.
|
||||
I understand that the UX is not the most intuitive. If you can think of an
|
||||
improvement, please let me know (e.g. via an GitHub issue)!
|
||||
|
||||
As with `pull`, `rebase` will also refuse to run when there are changes in your
|
||||
worktree. And you can also use the `--stash` option to stash/unstash changes
|
||||
automatically.
|
||||
|
||||
### Manual access
|
||||
|
||||
GRM isn't doing any magic, it's just git under the hood. If you need to have access
|
||||
|
||||
@@ -7,6 +7,7 @@ pluggy==1.0.0
|
||||
py==1.11.0
|
||||
pyparsing==3.0.7
|
||||
pytest==6.2.5
|
||||
PyYAML==6.0
|
||||
smmap==5.0.0
|
||||
toml==0.10.2
|
||||
typing_extensions==4.0.1
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import tempfile
|
||||
|
||||
import toml
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from helpers import *
|
||||
|
||||
@@ -30,6 +32,16 @@ def test_repos_find_empty():
|
||||
assert len(cmd.stderr) != 0
|
||||
|
||||
|
||||
def test_repos_find_invalid_format():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
cmd = grm(
|
||||
["repos", "find", tmpdir, "--format", "invalidformat"], is_invalid=True
|
||||
)
|
||||
assert cmd.returncode != 0
|
||||
assert len(cmd.stdout) == 0
|
||||
assert "isn't a valid value" in cmd.stderr
|
||||
|
||||
|
||||
def test_repos_find_non_git_repos():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
shell(
|
||||
@@ -50,7 +62,9 @@ def test_repos_find_non_git_repos():
|
||||
assert len(cmd.stderr) != 0
|
||||
|
||||
|
||||
def test_repos_find():
|
||||
@pytest.mark.parametrize("default", [True, False])
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
def test_repos_find(configtype, default):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
shell(
|
||||
f"""
|
||||
@@ -83,11 +97,19 @@ def test_repos_find():
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "find", tmpdir])
|
||||
args = ["repos", "find", tmpdir]
|
||||
if not default:
|
||||
args += ["--format", configtype]
|
||||
cmd = grm(args)
|
||||
assert cmd.returncode == 0
|
||||
assert len(cmd.stderr) == 0
|
||||
|
||||
output = toml.loads(cmd.stdout)
|
||||
if default or configtype == "toml":
|
||||
output = toml.loads(cmd.stdout)
|
||||
elif configtype == "yaml":
|
||||
output = yaml.safe_load(cmd.stdout)
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
assert isinstance(output, dict)
|
||||
assert set(output.keys()) == {"trees"}
|
||||
@@ -125,14 +147,24 @@ def test_repos_find():
|
||||
assert origin["url"] == "https://example.com/repo2.git"
|
||||
|
||||
|
||||
def test_repos_find_in_root():
|
||||
@pytest.mark.parametrize("default", [True, False])
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
def test_repos_find_in_root(configtype, default):
|
||||
with TempGitRepository() as repo_dir:
|
||||
|
||||
cmd = grm(["repos", "find", repo_dir])
|
||||
args = ["repos", "find", repo_dir]
|
||||
if not default:
|
||||
args += ["--format", configtype]
|
||||
cmd = grm(args)
|
||||
assert cmd.returncode == 0
|
||||
assert len(cmd.stderr) == 0
|
||||
|
||||
output = toml.loads(cmd.stdout)
|
||||
if default or configtype == "toml":
|
||||
output = toml.loads(cmd.stdout)
|
||||
elif configtype == "yaml":
|
||||
output = yaml.safe_load(cmd.stdout)
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
assert isinstance(output, dict)
|
||||
assert set(output.keys()) == {"trees"}
|
||||
@@ -160,7 +192,9 @@ def test_repos_find_in_root():
|
||||
assert someremote["type"] == "file"
|
||||
|
||||
|
||||
def test_repos_find_with_invalid_repo():
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
@pytest.mark.parametrize("default", [True, False])
|
||||
def test_repos_find_with_invalid_repo(configtype, default):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
shell(
|
||||
f"""
|
||||
@@ -193,11 +227,19 @@ def test_repos_find_with_invalid_repo():
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "find", tmpdir])
|
||||
args = ["repos", "find", tmpdir]
|
||||
if not default:
|
||||
args += ["--format", configtype]
|
||||
cmd = grm(args)
|
||||
assert cmd.returncode == 0
|
||||
assert "broken" in cmd.stderr
|
||||
|
||||
output = toml.loads(cmd.stdout)
|
||||
if default or configtype == "toml":
|
||||
output = toml.loads(cmd.stdout)
|
||||
elif configtype == "yaml":
|
||||
output = yaml.safe_load(cmd.stdout)
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
assert isinstance(output, dict)
|
||||
assert set(output.keys()) == {"trees"}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import tempfile
|
||||
import re
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
import toml
|
||||
@@ -9,8 +10,134 @@ import git
|
||||
|
||||
from helpers import *
|
||||
|
||||
templates = {
|
||||
"repo_simple": {
|
||||
"toml": """
|
||||
[[trees]]
|
||||
root = "{root}"
|
||||
|
||||
def test_repos_sync_config_is_valid_symlink():
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
""",
|
||||
"yaml": """
|
||||
trees:
|
||||
- root: "{root}"
|
||||
repos:
|
||||
- name: "test"
|
||||
""",
|
||||
},
|
||||
"repo_with_remote": {
|
||||
"toml": """
|
||||
[[trees]]
|
||||
root = "{root}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "{remotename}"
|
||||
url = "file://{remote}"
|
||||
type = "file"
|
||||
""",
|
||||
"yaml": textwrap.dedent(
|
||||
"""
|
||||
trees:
|
||||
- root: "{root}"
|
||||
repos:
|
||||
- name: test
|
||||
remotes:
|
||||
- name: "{remotename}"
|
||||
url: "file://{remote}"
|
||||
type: "file"
|
||||
"""
|
||||
),
|
||||
},
|
||||
"repo_with_two_remotes": {
|
||||
"toml": """
|
||||
[[trees]]
|
||||
root = "{root}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin"
|
||||
url = "file://{remote1}"
|
||||
type = "file"
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin2"
|
||||
url = "file://{remote2}"
|
||||
type = "file"
|
||||
""",
|
||||
"yaml": textwrap.dedent(
|
||||
"""
|
||||
trees:
|
||||
- root: "{root}"
|
||||
repos:
|
||||
- name: "test"
|
||||
remotes:
|
||||
- name: "origin"
|
||||
url: "file://{remote1}"
|
||||
type: "file"
|
||||
- name: "origin2"
|
||||
url: "file://{remote2}"
|
||||
type: "file"
|
||||
"""
|
||||
),
|
||||
},
|
||||
"worktree_repo_simple": {
|
||||
"toml": """
|
||||
[[trees]]
|
||||
root = "{root}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
worktree_setup = true
|
||||
""",
|
||||
"yaml": textwrap.dedent(
|
||||
"""
|
||||
trees:
|
||||
- root: "{root}"
|
||||
repos:
|
||||
- name: test
|
||||
worktree_setup: true
|
||||
"""
|
||||
),
|
||||
},
|
||||
"worktree_repo_with_remote": {
|
||||
"toml": """
|
||||
[[trees]]
|
||||
root = "{root}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
worktree_setup = true
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin"
|
||||
url = "file://{remote}"
|
||||
type = "file"
|
||||
""",
|
||||
"yaml": textwrap.dedent(
|
||||
"""
|
||||
trees:
|
||||
- root: "{root}"
|
||||
repos:
|
||||
- name: test
|
||||
worktree_setup: true
|
||||
remotes:
|
||||
- name: origin
|
||||
url: "file://{remote}"
|
||||
type: "file"
|
||||
"""
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
def test_repos_sync_config_is_valid_symlink(configtype):
|
||||
with tempfile.TemporaryDirectory() as target:
|
||||
with TempGitFileRemote() as (remote, head_commit_sha):
|
||||
with tempfile.NamedTemporaryFile() as config:
|
||||
@@ -20,20 +147,13 @@ def test_repos_sync_config_is_valid_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"
|
||||
"""
|
||||
templates["repo_with_remote"][configtype].format(
|
||||
root=target, remote=remote, remotename="origin"
|
||||
)
|
||||
)
|
||||
|
||||
subprocess.run(["cat", config.name])
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config_symlink])
|
||||
assert cmd.returncode == 0
|
||||
|
||||
@@ -85,20 +205,13 @@ def test_repos_sync_config_is_unreadable():
|
||||
assert "permission denied" in cmd.stderr.lower()
|
||||
|
||||
|
||||
def test_repos_sync_unmanaged_repos():
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
def test_repos_sync_unmanaged_repos(configtype):
|
||||
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"
|
||||
"""
|
||||
)
|
||||
f.write(templates["repo_simple"][configtype].format(root=root))
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode == 0
|
||||
@@ -112,19 +225,12 @@ def test_repos_sync_unmanaged_repos():
|
||||
assert any([re.match(regex, l) for l in cmd.stderr.lower().split("\n")])
|
||||
|
||||
|
||||
def test_repos_sync_root_is_file():
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
def test_repos_sync_root_is_file(configtype):
|
||||
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"
|
||||
"""
|
||||
)
|
||||
f.write(templates["repo_simple"][configtype].format(root=target.name))
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode != 0
|
||||
@@ -132,30 +238,17 @@ def test_repos_sync_root_is_file():
|
||||
assert "not a directory" in cmd.stderr.lower()
|
||||
|
||||
|
||||
def test_repos_sync_normal_clone():
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
def test_repos_sync_normal_clone(configtype):
|
||||
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"
|
||||
"""
|
||||
templates["repo_with_two_remotes"][configtype].format(
|
||||
root=target, remote1=remote1, remote2=remote2
|
||||
)
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
@@ -183,19 +276,12 @@ def test_repos_sync_normal_clone():
|
||||
assert urls[0] == f"file://{remote2}"
|
||||
|
||||
|
||||
def test_repos_sync_normal_init():
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
def test_repos_sync_normal_init(configtype):
|
||||
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"
|
||||
"""
|
||||
)
|
||||
f.write(templates["repo_simple"][configtype].format(root=target))
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode == 0
|
||||
@@ -210,25 +296,17 @@ def test_repos_sync_normal_init():
|
||||
assert not repo.head.is_valid()
|
||||
|
||||
|
||||
def test_repos_sync_normal_add_remote():
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
def test_repos_sync_normal_add_remote(configtype):
|
||||
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"
|
||||
"""
|
||||
templates["repo_with_remote"][configtype].format(
|
||||
root=target, remote=remote1, remotename="origin"
|
||||
)
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
@@ -246,23 +324,9 @@ def test_repos_sync_normal_add_remote():
|
||||
|
||||
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"
|
||||
"""
|
||||
templates["repo_with_two_remotes"][configtype].format(
|
||||
root=target, remote1=remote1, remote2=remote2
|
||||
)
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
@@ -282,30 +346,17 @@ def test_repos_sync_normal_add_remote():
|
||||
assert urls[0] == f"file://{remote2}"
|
||||
|
||||
|
||||
def test_repos_sync_normal_remove_remote():
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
def test_repos_sync_normal_remove_remote(configtype):
|
||||
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"
|
||||
"""
|
||||
templates["repo_with_two_remotes"][configtype].format(
|
||||
root=target, remote1=remote1, remote2=remote2
|
||||
)
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
@@ -326,18 +377,9 @@ def test_repos_sync_normal_remove_remote():
|
||||
|
||||
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"
|
||||
"""
|
||||
templates["repo_with_remote"][configtype].format(
|
||||
root=target, remote=remote2, remotename="origin2"
|
||||
)
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
@@ -369,25 +411,17 @@ def test_repos_sync_normal_remove_remote():
|
||||
assert urls[0] == f"file://{remote2}"
|
||||
|
||||
|
||||
def test_repos_sync_normal_change_remote_url():
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
def test_repos_sync_normal_change_remote_url(configtype):
|
||||
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"
|
||||
"""
|
||||
templates["repo_with_remote"][configtype].format(
|
||||
root=target, remote=remote1, remotename="origin"
|
||||
)
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
@@ -405,18 +439,9 @@ def test_repos_sync_normal_change_remote_url():
|
||||
|
||||
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"
|
||||
"""
|
||||
templates["repo_with_remote"][configtype].format(
|
||||
root=target, remote=remote2, remotename="origin"
|
||||
)
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
@@ -429,25 +454,17 @@ def test_repos_sync_normal_change_remote_url():
|
||||
assert urls[0] == f"file://{remote2}"
|
||||
|
||||
|
||||
def test_repos_sync_normal_change_remote_name():
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
def test_repos_sync_normal_change_remote_name(configtype):
|
||||
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"
|
||||
"""
|
||||
templates["repo_with_remote"][configtype].format(
|
||||
root=target, remote=remote1, remotename="origin"
|
||||
)
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
@@ -465,18 +482,9 @@ def test_repos_sync_normal_change_remote_name():
|
||||
|
||||
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"
|
||||
"""
|
||||
templates["repo_with_remote"][configtype].format(
|
||||
root=target, remote=remote1, remotename="origin2"
|
||||
)
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
@@ -492,25 +500,16 @@ def test_repos_sync_normal_change_remote_name():
|
||||
assert urls[0] == f"file://{remote1}"
|
||||
|
||||
|
||||
def test_repos_sync_worktree_clone():
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
def test_repos_sync_worktree_clone(configtype):
|
||||
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"
|
||||
"""
|
||||
templates["worktree_repo_with_remote"][configtype].format(
|
||||
root=target, remote=remote, remotename="origin"
|
||||
)
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
@@ -530,19 +529,13 @@ def test_repos_sync_worktree_clone():
|
||||
assert str(repo.head.commit) == head_commit_sha
|
||||
|
||||
|
||||
def test_repos_sync_worktree_init():
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
def test_repos_sync_worktree_init(configtype):
|
||||
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
|
||||
"""
|
||||
templates["worktree_repo_simple"][configtype].format(root=target)
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
@@ -559,43 +552,42 @@ def test_repos_sync_worktree_init():
|
||||
assert not repo.head.is_valid()
|
||||
|
||||
|
||||
def test_repos_sync_invalid_toml():
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
def test_repos_sync_invalid_syntax(configtype):
|
||||
with tempfile.NamedTemporaryFile() as config:
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = invalid as there are no quotes ;)
|
||||
"""
|
||||
)
|
||||
if configtype == "toml":
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = invalid as there are no quotes ;)
|
||||
"""
|
||||
)
|
||||
elif configtype == "yaml":
|
||||
f.write(
|
||||
f"""
|
||||
trees:
|
||||
wrong:
|
||||
indentation:
|
||||
"""
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode != 0
|
||||
|
||||
|
||||
def test_repos_sync_unchanged():
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
def test_repos_sync_unchanged(configtype):
|
||||
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"
|
||||
"""
|
||||
templates["repo_with_two_remotes"][configtype].format(
|
||||
root=target, remote1=remote1, remote2=remote2
|
||||
)
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
@@ -609,25 +601,17 @@ def test_repos_sync_unchanged():
|
||||
assert before == after
|
||||
|
||||
|
||||
def test_repos_sync_normal_change_to_worktree():
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
def test_repos_sync_normal_change_to_worktree(configtype):
|
||||
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"
|
||||
"""
|
||||
templates["repo_with_remote"][configtype].format(
|
||||
root=target, remote=remote1, remotename="origin"
|
||||
)
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
@@ -637,19 +621,9 @@ def test_repos_sync_normal_change_to_worktree():
|
||||
|
||||
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"
|
||||
"""
|
||||
templates["worktree_repo_with_remote"][configtype].format(
|
||||
root=target, remote=remote1, remotename="origin"
|
||||
)
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
@@ -658,26 +632,17 @@ def test_repos_sync_normal_change_to_worktree():
|
||||
assert "not using a worktree setup" in cmd.stderr
|
||||
|
||||
|
||||
def test_repos_sync_worktree_change_to_normal():
|
||||
@pytest.mark.parametrize("configtype", ["toml", "yaml"])
|
||||
def test_repos_sync_worktree_change_to_normal(configtype):
|
||||
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"
|
||||
"""
|
||||
templates["worktree_repo_with_remote"][configtype].format(
|
||||
root=target, remote=remote1, remotename="origin"
|
||||
)
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
@@ -687,18 +652,9 @@ def test_repos_sync_worktree_change_to_normal():
|
||||
|
||||
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"
|
||||
"""
|
||||
templates["repo_with_remote"][configtype].format(
|
||||
root=target, remote=remote1, remotename="origin"
|
||||
)
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from helpers import *
|
||||
|
||||
import re
|
||||
|
||||
import pytest
|
||||
import git
|
||||
|
||||
@@ -51,7 +53,9 @@ def test_worktree_fetch():
|
||||
|
||||
@pytest.mark.parametrize("rebase", [True, False])
|
||||
@pytest.mark.parametrize("ffable", [True, False])
|
||||
def test_worktree_pull(rebase, ffable):
|
||||
@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 TempGitFileRemote() as (remote_path, _remote_sha):
|
||||
shell(
|
||||
@@ -94,51 +98,79 @@ def test_worktree_pull(rebase, ffable):
|
||||
"""
|
||||
)
|
||||
|
||||
if has_changes:
|
||||
shell(
|
||||
f"""
|
||||
cd {base_dir}/master
|
||||
echo change >> root-commit-in-worktree-1
|
||||
echo uncommitedchange > uncommitedchange
|
||||
"""
|
||||
)
|
||||
|
||||
args = ["wt", "pull"]
|
||||
if rebase:
|
||||
args += ["--rebase"]
|
||||
if stash:
|
||||
args += ["--stash"]
|
||||
cmd = grm(args, cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
|
||||
assert repo.commit("upstream/master").hexsha == remote_commit
|
||||
assert repo.commit("origin/master").hexsha == root_commit
|
||||
assert (
|
||||
repo.commit("master").hexsha != repo.commit("origin/master").hexsha
|
||||
)
|
||||
|
||||
if not rebase:
|
||||
if ffable:
|
||||
assert (
|
||||
repo.commit("master").hexsha
|
||||
!= repo.commit("origin/master").hexsha
|
||||
)
|
||||
assert (
|
||||
repo.commit("master").hexsha
|
||||
== repo.commit("upstream/master").hexsha
|
||||
)
|
||||
assert repo.commit("upstream/master").hexsha == remote_commit
|
||||
else:
|
||||
assert "cannot be fast forwarded" in cmd.stderr
|
||||
assert (
|
||||
repo.commit("master").hexsha
|
||||
!= repo.commit("origin/master").hexsha
|
||||
)
|
||||
assert repo.commit("master").hexsha != remote_commit
|
||||
assert repo.commit("upstream/master").hexsha == remote_commit
|
||||
if has_changes and not stash:
|
||||
assert cmd.returncode != 0
|
||||
assert re.match(r".*master.*contains changes.*", cmd.stderr)
|
||||
else:
|
||||
if ffable:
|
||||
assert (
|
||||
repo.commit("master").hexsha
|
||||
!= repo.commit("origin/master").hexsha
|
||||
)
|
||||
assert (
|
||||
repo.commit("master").hexsha
|
||||
== repo.commit("upstream/master").hexsha
|
||||
)
|
||||
assert repo.commit("upstream/master").hexsha == remote_commit
|
||||
assert repo.commit("upstream/master").hexsha == remote_commit
|
||||
assert repo.commit("origin/master").hexsha == root_commit
|
||||
assert (
|
||||
repo.commit("master").hexsha
|
||||
!= repo.commit("origin/master").hexsha
|
||||
)
|
||||
if has_changes:
|
||||
assert ["uncommitedchange"] == repo.untracked_files
|
||||
assert repo.is_dirty()
|
||||
else:
|
||||
assert (
|
||||
repo.commit("master").message.strip()
|
||||
== "local-commit-in-master"
|
||||
)
|
||||
assert repo.commit("master~1").hexsha == remote_commit
|
||||
assert not repo.is_dirty()
|
||||
|
||||
if not rebase:
|
||||
if ffable:
|
||||
assert cmd.returncode == 0
|
||||
assert (
|
||||
repo.commit("master").hexsha
|
||||
!= repo.commit("origin/master").hexsha
|
||||
)
|
||||
assert (
|
||||
repo.commit("master").hexsha
|
||||
== repo.commit("upstream/master").hexsha
|
||||
)
|
||||
assert (
|
||||
repo.commit("upstream/master").hexsha == remote_commit
|
||||
)
|
||||
else:
|
||||
assert cmd.returncode != 0
|
||||
assert "cannot be fast forwarded" in cmd.stderr
|
||||
assert (
|
||||
repo.commit("master").hexsha
|
||||
!= repo.commit("origin/master").hexsha
|
||||
)
|
||||
assert repo.commit("master").hexsha != remote_commit
|
||||
assert (
|
||||
repo.commit("upstream/master").hexsha == remote_commit
|
||||
)
|
||||
else:
|
||||
assert cmd.returncode == 0
|
||||
if ffable:
|
||||
assert (
|
||||
repo.commit("master").hexsha
|
||||
!= repo.commit("origin/master").hexsha
|
||||
)
|
||||
assert (
|
||||
repo.commit("master").hexsha
|
||||
== repo.commit("upstream/master").hexsha
|
||||
)
|
||||
assert (
|
||||
repo.commit("upstream/master").hexsha == remote_commit
|
||||
)
|
||||
else:
|
||||
assert (
|
||||
repo.commit("master").message.strip()
|
||||
== "local-commit-in-master"
|
||||
)
|
||||
assert repo.commit("master~1").hexsha == remote_commit
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
|
||||
from helpers import *
|
||||
|
||||
import pytest
|
||||
import re
|
||||
|
||||
import pytest
|
||||
import git
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pull", [True, False])
|
||||
@pytest.mark.parametrize("rebase", [True, False])
|
||||
@pytest.mark.parametrize("ffable", [True, False])
|
||||
def test_worktree_rebase(pull, rebase, ffable):
|
||||
@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 open(os.path.join(base_dir, "grm.toml"), "w") as f:
|
||||
f.write('persistent_branches = ["mybasebranch"]')
|
||||
@@ -83,6 +86,14 @@ def test_worktree_rebase(pull, rebase, ffable):
|
||||
"""
|
||||
)
|
||||
|
||||
if has_changes:
|
||||
shell(
|
||||
f"""
|
||||
cd {base_dir}/myfeatbranch
|
||||
echo uncommitedchange > uncommitedchange
|
||||
"""
|
||||
)
|
||||
|
||||
grm(["wt", "delete", "--force", "tmp"], cwd=base_dir)
|
||||
|
||||
repo = git.Repo(f"{base_dir}/.git-main-working-tree")
|
||||
@@ -133,17 +144,23 @@ def test_worktree_rebase(pull, rebase, ffable):
|
||||
args += ["--pull"]
|
||||
if rebase:
|
||||
args += ["--rebase"]
|
||||
if stash:
|
||||
args += ["--stash"]
|
||||
cmd = grm(args, cwd=base_dir)
|
||||
|
||||
print(args)
|
||||
if rebase and not pull:
|
||||
assert cmd.returncode != 0
|
||||
assert len(cmd.stderr) != 0
|
||||
elif has_changes and not stash:
|
||||
assert cmd.returncode != 0
|
||||
assert re.match(r".*myfeatbranch.*contains changes.*", cmd.stderr)
|
||||
else:
|
||||
assert cmd.returncode == 0
|
||||
repo = git.Repo(f"{base_dir}/myfeatbranch")
|
||||
if has_changes:
|
||||
assert ["uncommitedchange"] == repo.untracked_files
|
||||
if pull:
|
||||
if rebase:
|
||||
assert cmd.returncode == 0
|
||||
if ffable:
|
||||
assert (
|
||||
repo.commit("HEAD").message.strip()
|
||||
@@ -190,6 +207,7 @@ def test_worktree_rebase(pull, rebase, ffable):
|
||||
assert repo.commit("HEAD~6").message.strip() == "commit-root"
|
||||
else:
|
||||
if ffable:
|
||||
assert cmd.returncode == 0
|
||||
assert (
|
||||
repo.commit("HEAD").message.strip()
|
||||
== "commit-in-feat-remote"
|
||||
@@ -208,6 +226,7 @@ def test_worktree_rebase(pull, rebase, ffable):
|
||||
)
|
||||
assert repo.commit("HEAD~4").message.strip() == "commit-root"
|
||||
else:
|
||||
assert cmd.returncode != 0
|
||||
assert (
|
||||
repo.commit("HEAD").message.strip()
|
||||
== "commit-in-feat-local-no-ff"
|
||||
@@ -226,6 +245,7 @@ def test_worktree_rebase(pull, rebase, ffable):
|
||||
)
|
||||
assert repo.commit("HEAD~4").message.strip() == "commit-root"
|
||||
else:
|
||||
assert cmd.returncode == 0
|
||||
if ffable:
|
||||
assert repo.commit("HEAD").message.strip() == "commit-in-feat-local"
|
||||
assert (
|
||||
|
||||
@@ -9,6 +9,8 @@ source ./venv/bin/activate
|
||||
pip --disable-pip-version-check install -r ./requirements.txt
|
||||
|
||||
pip3 list --outdated --format=freeze | grep -v '^\-e' | cut -d = -f 1 | while read -r package ; do
|
||||
[[ package == "pip" ]] && continue
|
||||
[[ package == "setuptools" ]] && continue
|
||||
pip install --upgrade "${package}"
|
||||
version="$(pip show "${package}" | grep '^Version' | cut -d ' ' -f 2)"
|
||||
message="e2e_tests/pip: Update ${package} to ${version}"
|
||||
|
||||
16
example.config.yaml
Normal file
16
example.config.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
trees:
|
||||
- root: "~/example-projects/"
|
||||
repos:
|
||||
- name: "git-repo-manager"
|
||||
remotes:
|
||||
- name: "origin"
|
||||
url: "https://code.hkoerber.de/hannes/git-repo-manager.git"
|
||||
type: "https"
|
||||
- name: "github"
|
||||
url: "https://github.com/hakoerber/git-repo-manager.git"
|
||||
type: "https"
|
||||
- name: "dotfiles"
|
||||
remotes:
|
||||
- name: "origin"
|
||||
url: "https://github.com/hakoerber/dotfiles.git"
|
||||
type: "https"
|
||||
@@ -36,6 +36,10 @@ impl Config {
|
||||
Err(error) => Err(error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_yaml(&self) -> Result<String, String> {
|
||||
serde_yaml::to_string(self).map_err(|e| e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -62,12 +66,15 @@ pub fn read_config(path: &str) -> Result<Config, String> {
|
||||
|
||||
let config: Config = match toml::from_str(&content) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
return Err(format!(
|
||||
"Error parsing configuration file \"{}\": {}",
|
||||
path, e
|
||||
))
|
||||
}
|
||||
Err(_) => match serde_yaml::from_str(&content) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
return Err(format!(
|
||||
"Error parsing configuration file \"{}\": {}",
|
||||
path, e
|
||||
))
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Ok(config)
|
||||
|
||||
@@ -61,10 +61,25 @@ pub struct OptionalConfig {
|
||||
pub config: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(clap::ArgEnum, Clone)]
|
||||
pub enum ConfigFormat {
|
||||
Yaml,
|
||||
Toml,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct Find {
|
||||
#[clap(help = "The path to search through")]
|
||||
pub path: String,
|
||||
|
||||
#[clap(
|
||||
arg_enum,
|
||||
short,
|
||||
long,
|
||||
help = "Format to produce",
|
||||
default_value_t = ConfigFormat::Toml,
|
||||
)]
|
||||
pub format: ConfigFormat,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
@@ -132,6 +147,8 @@ pub struct WorktreeFetchArgs {}
|
||||
pub struct WorktreePullArgs {
|
||||
#[clap(long = "--rebase", help = "Perform a rebase instead of a fast-forward")]
|
||||
pub rebase: bool,
|
||||
#[clap(long = "--stash", help = "Stash & unstash changes before & after pull")]
|
||||
pub stash: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
@@ -140,6 +157,11 @@ pub struct WorktreeRebaseArgs {
|
||||
pub pull: bool,
|
||||
#[clap(long = "--rebase", help = "Perform a rebase when doing a pull")]
|
||||
pub rebase: bool,
|
||||
#[clap(
|
||||
long = "--stash",
|
||||
help = "Stash & unstash changes before & after rebase"
|
||||
)]
|
||||
pub stash: bool,
|
||||
}
|
||||
|
||||
pub fn parse() -> Opts {
|
||||
|
||||
@@ -119,15 +119,34 @@ fn main() {
|
||||
} else {
|
||||
let config = trees.to_config();
|
||||
|
||||
let toml = match config.as_toml() {
|
||||
Ok(toml) => toml,
|
||||
Err(error) => {
|
||||
print_error(&format!("Failed converting config to TOML: {}", &error));
|
||||
process::exit(1);
|
||||
match find.format {
|
||||
cmd::ConfigFormat::Toml => {
|
||||
let toml = match config.as_toml() {
|
||||
Ok(toml) => toml,
|
||||
Err(error) => {
|
||||
print_error(&format!(
|
||||
"Failed converting config to TOML: {}",
|
||||
&error
|
||||
));
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
print!("{}", toml);
|
||||
}
|
||||
};
|
||||
|
||||
print!("{}", toml);
|
||||
cmd::ConfigFormat::Yaml => {
|
||||
let yaml = match config.as_yaml() {
|
||||
Ok(yaml) => yaml,
|
||||
Err(error) => {
|
||||
print_error(&format!(
|
||||
"Failed converting config to YAML: {}",
|
||||
&error
|
||||
));
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
print!("{}", yaml);
|
||||
}
|
||||
}
|
||||
}
|
||||
for warning in warnings {
|
||||
print_warning(&warning);
|
||||
@@ -350,26 +369,27 @@ fn main() {
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
let mut failures = false;
|
||||
for worktree in repo.get_worktrees().unwrap_or_else(|error| {
|
||||
print_error(&format!("Error getting worktrees: {}", error));
|
||||
process::exit(1);
|
||||
}) {
|
||||
if let Some(warning) =
|
||||
worktree
|
||||
.forward_branch(args.rebase)
|
||||
.unwrap_or_else(|error| {
|
||||
print_error(&format!(
|
||||
"Error updating worktree branch: {}",
|
||||
error
|
||||
));
|
||||
process::exit(1);
|
||||
})
|
||||
if let Some(warning) = worktree
|
||||
.forward_branch(args.rebase, args.stash)
|
||||
.unwrap_or_else(|error| {
|
||||
print_error(&format!("Error updating worktree branch: {}", error));
|
||||
process::exit(1);
|
||||
})
|
||||
{
|
||||
print_warning(&format!("{}: {}", worktree.name(), warning));
|
||||
failures = true;
|
||||
} else {
|
||||
print_success(&format!("{}: Done", worktree.name()));
|
||||
}
|
||||
}
|
||||
if failures {
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
cmd::WorktreeAction::Rebase(args) => {
|
||||
if args.rebase && !args.pull {
|
||||
@@ -406,10 +426,12 @@ fn main() {
|
||||
process::exit(1);
|
||||
});
|
||||
|
||||
let mut failures = false;
|
||||
|
||||
for worktree in &worktrees {
|
||||
if args.pull {
|
||||
if let Some(warning) = worktree
|
||||
.forward_branch(args.rebase)
|
||||
.forward_branch(args.rebase, args.stash)
|
||||
.unwrap_or_else(|error| {
|
||||
print_error(&format!(
|
||||
"Error updating worktree branch: {}",
|
||||
@@ -418,28 +440,29 @@ fn main() {
|
||||
process::exit(1);
|
||||
})
|
||||
{
|
||||
failures = true;
|
||||
print_warning(&format!("{}: {}", worktree.name(), warning));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for worktree in &worktrees {
|
||||
if let Some(warning) =
|
||||
worktree
|
||||
.rebase_onto_default(&config)
|
||||
.unwrap_or_else(|error| {
|
||||
print_error(&format!(
|
||||
"Error rebasing worktree branch: {}",
|
||||
error
|
||||
));
|
||||
process::exit(1);
|
||||
})
|
||||
if let Some(warning) = worktree
|
||||
.rebase_onto_default(&config, args.stash)
|
||||
.unwrap_or_else(|error| {
|
||||
print_error(&format!("Error rebasing worktree branch: {}", error));
|
||||
process::exit(1);
|
||||
})
|
||||
{
|
||||
failures = true;
|
||||
print_warning(&format!("{}: {}", worktree.name(), warning));
|
||||
} else {
|
||||
print_success(&format!("{}: Done", worktree.name()));
|
||||
}
|
||||
}
|
||||
if failures {
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
81
src/repo.rs
81
src/repo.rs
@@ -181,17 +181,30 @@ impl Worktree {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn forward_branch(&self, rebase: bool) -> Result<Option<String>, String> {
|
||||
pub fn forward_branch(&self, rebase: bool, stash: bool) -> Result<Option<String>, String> {
|
||||
let repo = Repo::open(Path::new(&self.name), false)
|
||||
.map_err(|error| format!("Error opening worktree: {}", error))?;
|
||||
|
||||
if let Ok(remote_branch) = repo.find_local_branch(&self.name)?.upstream() {
|
||||
let status = repo.status(false)?;
|
||||
let mut stashed_changes = false;
|
||||
|
||||
if !status.clean() {
|
||||
return Ok(Some(String::from("Worktree contains changes")));
|
||||
if stash {
|
||||
repo.stash()?;
|
||||
stashed_changes = true;
|
||||
} else {
|
||||
return Ok(Some(String::from("Worktree contains changes")));
|
||||
}
|
||||
}
|
||||
|
||||
let unstash = || -> Result<(), String> {
|
||||
if stashed_changes {
|
||||
repo.stash_pop()?;
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let remote_annotated_commit = repo
|
||||
.0
|
||||
.find_annotated_commit(remote_branch.commit()?.id().0)
|
||||
@@ -231,6 +244,7 @@ impl Worktree {
|
||||
continue;
|
||||
}
|
||||
rebase.abort().map_err(convert_libgit2_error)?;
|
||||
unstash()?;
|
||||
return Err(convert_libgit2_error(error));
|
||||
}
|
||||
}
|
||||
@@ -243,9 +257,11 @@ impl Worktree {
|
||||
.map_err(convert_libgit2_error)?;
|
||||
|
||||
if analysis.is_up_to_date() {
|
||||
unstash()?;
|
||||
return Ok(None);
|
||||
}
|
||||
if !analysis.is_fast_forward() {
|
||||
unstash()?;
|
||||
return Ok(Some(String::from("Worktree cannot be fast forwarded")));
|
||||
}
|
||||
|
||||
@@ -257,15 +273,18 @@ impl Worktree {
|
||||
)
|
||||
.map_err(convert_libgit2_error)?;
|
||||
}
|
||||
unstash()?;
|
||||
} else {
|
||||
return Ok(Some(String::from("No remote branch to rebase onto")));
|
||||
};
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub fn rebase_onto_default(
|
||||
&self,
|
||||
config: &Option<WorktreeRootConfig>,
|
||||
stash: bool,
|
||||
) -> Result<Option<String>, String> {
|
||||
let repo = Repo::open(Path::new(&self.name), false)
|
||||
.map_err(|error| format!("Error opening worktree: {}", error))?;
|
||||
@@ -291,6 +310,25 @@ impl Worktree {
|
||||
},
|
||||
};
|
||||
|
||||
let status = repo.status(false)?;
|
||||
let mut stashed_changes = false;
|
||||
|
||||
if !status.clean() {
|
||||
if stash {
|
||||
repo.stash()?;
|
||||
stashed_changes = true;
|
||||
} else {
|
||||
return Ok(Some(String::from("Worktree contains changes")));
|
||||
}
|
||||
}
|
||||
|
||||
let unstash = || -> Result<(), String> {
|
||||
if stashed_changes {
|
||||
repo.stash_pop()?;
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let base_branch = repo.find_local_branch(&default_branch_name)?;
|
||||
let base_annotated_commit = repo
|
||||
.0
|
||||
@@ -330,11 +368,13 @@ impl Worktree {
|
||||
continue;
|
||||
}
|
||||
rebase.abort().map_err(convert_libgit2_error)?;
|
||||
unstash()?;
|
||||
return Err(convert_libgit2_error(error));
|
||||
}
|
||||
}
|
||||
|
||||
rebase.finish(None).map_err(convert_libgit2_error)?;
|
||||
unstash()?;
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
@@ -456,6 +496,35 @@ impl Repo {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stash(&self) -> Result<(), String> {
|
||||
let head_branch = self.head_branch()?;
|
||||
let head = head_branch.commit()?;
|
||||
let author = head.author();
|
||||
|
||||
// This is honestly quite horrible. The problem is that all stash operations expect a
|
||||
// mutable reference (as they, well, mutate the repo after all). But we are heavily using
|
||||
// immutable references a lot with this struct. I'm really not sure how to best solve this.
|
||||
// Right now, we just open the repo AGAIN. It is safe, as we are only accessing the stash
|
||||
// with the second reference, so there are no cross effects. But it just smells. Also,
|
||||
// using `unwrap()` here as we are already sure that the repo is openable(?).
|
||||
let mut repo = Repo::open(self.0.path(), false).unwrap();
|
||||
repo.0
|
||||
.stash_save2(&author, None, Some(git2::StashFlags::INCLUDE_UNTRACKED))
|
||||
.map_err(convert_libgit2_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stash_pop(&self) -> Result<(), String> {
|
||||
let mut repo = Repo::open(self.0.path(), false).unwrap();
|
||||
repo.0
|
||||
.stash_pop(
|
||||
0,
|
||||
Some(git2::StashApplyOptions::new().reinstantiate_index()),
|
||||
)
|
||||
.map_err(convert_libgit2_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn rename_remote(&self, remote: &RemoteHandle, new_name: &str) -> Result<(), String> {
|
||||
let failed_refspecs = self
|
||||
.0
|
||||
@@ -1253,6 +1322,10 @@ impl Commit<'_> {
|
||||
pub fn id(&self) -> Oid {
|
||||
Oid(self.0.id())
|
||||
}
|
||||
|
||||
pub(self) fn author(&self) -> git2::Signature {
|
||||
self.0.author()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Branch<'a> {
|
||||
@@ -1323,9 +1396,7 @@ fn get_remote_callbacks() -> git2::RemoteCallbacks<'static> {
|
||||
Some(username) => username,
|
||||
None => panic!("Could not get username. This is a bug"),
|
||||
};
|
||||
git2::Cred::ssh_key_from_agent(username).or_else(|_| {
|
||||
git2::Cred::ssh_key(username, None, &crate::env_home().join(".ssh/id_rsa"), None)
|
||||
})
|
||||
git2::Cred::ssh_key_from_agent(username)
|
||||
});
|
||||
|
||||
callbacks
|
||||
|
||||
Reference in New Issue
Block a user