21 Commits
v0.5 ... v0.6.0

Author SHA1 Message Date
44a716248e Release v0.6.0 2022-01-23 12:28:45 +01:00
d20006a325 Merge branch 'develop' 2022-01-23 12:28:15 +01:00
f8adec1413 e2e: Ignore pip and setuptools for autoupdate 2022-01-23 12:27:59 +01:00
868269359c dependencies: Update serde to 1.0.135 2022-01-23 12:25:20 +01:00
61d4a4a0d8 e2e: Add PyYAML for YAML parsing 2022-01-22 11:23:53 +01:00
4e4de95a07 depcheck: Fix command to update crates.io index 2022-01-22 11:23:53 +01:00
9b64de7991 Add YAML as a config format option
@mustafa89 ;)
2022-01-22 11:23:53 +01:00
e45de3b498 depcheck: Fix crates.io cache update 2022-01-22 11:23:53 +01:00
6e4c388195 Add --stash options to pull and rebase 2022-01-22 11:23:53 +01:00
6436a8194e Disable "raw" SSH key usage
There is no sane way to get that fallback working with libgit2. Plus,
it's not a good practice anyway to have a non-password protected SSH
key.
2022-01-22 11:23:53 +01:00
f10ae25b2a Justfile: Add target to clean up 2022-01-22 11:23:53 +01:00
fd6b3b7438 Release v0.5.1 2022-01-22 11:19:37 +01:00
d68ff012f2 e2e_tests/pip: Update pyparsing to 3.0.7 2022-01-22 10:58:59 +01:00
9aad65edac dependencies: Update clap to 3.0.10 2022-01-22 10:58:59 +01:00
c370ef5815 dependencies: Update serde to 1.0.134 2022-01-22 10:58:59 +01:00
8f5b743ea4 Cargo.lock: Updating openssl-probe v0.1.4 -> v0.1.5 2022-01-22 10:58:47 +01:00
c0e981dbd4 Cargo.lock: Updating getrandom v0.2.3 -> v0.2.4 2022-01-22 10:58:47 +01:00
4303621b30 Cargo.lock: Updating smallvec v1.7.0 -> v1.8.0 2022-01-22 10:58:47 +01:00
63e04a9dcf dependencies: Update clap to 3.0.7 2022-01-22 10:58:47 +01:00
08ee946f2e dependencies: Update clap to 3.0.6 2022-01-22 10:58:47 +01:00
81de5a2d70 e2e_tests/pip: Update GitPython to 3.1.26 2022-01-22 10:58:47 +01:00
18 changed files with 649 additions and 396 deletions

76
Cargo.lock generated
View File

@@ -51,9 +51,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "clap" name = "clap"
version = "3.0.5" version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f34b09b9ee8c7c7b400fe2f8df39cafc9538b03d6ba7f4ae13e4cb90bfbb7d" checksum = "7a30c3bf9ff12dfe5dae53f0a96e0febcd18420d1c0e7fad77796d9d5c4b5375"
dependencies = [ dependencies = [
"atty", "atty",
"bitflags", "bitflags",
@@ -68,9 +68,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "3.0.5" version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41a0645a430ec9136d2d701e54a95d557de12649a9dd7109ced3187e648ac824" checksum = "517358c28fcef6607bf6f76108e02afad7e82297d132a6b846dcc1fc3efcd153"
dependencies = [ dependencies = [
"heck 0.4.0", "heck 0.4.0",
"proc-macro-error", "proc-macro-error",
@@ -176,9 +176,9 @@ checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.3" version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
@@ -187,7 +187,7 @@ dependencies = [
[[package]] [[package]]
name = "git-repo-manager" name = "git-repo-manager"
version = "0.5.0" version = "0.6.0"
dependencies = [ dependencies = [
"clap", "clap",
"comfy-table", "comfy-table",
@@ -195,6 +195,7 @@ dependencies = [
"git2", "git2",
"regex", "regex",
"serde", "serde",
"serde_yaml",
"shellexpand", "shellexpand",
"tempdir", "tempdir",
"toml", "toml",
@@ -292,9 +293,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.112" version = "0.2.113"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b03d17f364a3a042d5e5d46b053bbbf82c92c9430c592dd4c064dc6ee997125" checksum = "eef78b64d87775463c549fbd80e19249ef436ea3bf1de2a1eb7e717ec7fab1e9"
[[package]] [[package]]
name = "libgit2-sys" name = "libgit2-sys"
@@ -336,6 +337,12 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "linked-hash-map"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.5" version = "0.4.5"
@@ -405,9 +412,9 @@ checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
[[package]] [[package]]
name = "openssl-probe" name = "openssl-probe"
version = "0.1.4" version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
@@ -503,9 +510,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.14" version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47aa80447ce4daf1717500037052af176af5d38cc3e571d9ec1c7353fc10c87d" checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@@ -592,6 +599,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "ryu"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.1.0" version = "1.1.0"
@@ -600,24 +613,36 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.133" version = "1.0.135"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97565067517b60e2d1ea8b268e59ce036de907ac523ad83a0475da04e818989a" checksum = "2cf9235533494ea2ddcdb794665461814781c53f19d87b76e571a1c35acbad2b"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.133" version = "1.0.135"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed201699328568d8d08208fdd080e3ff594e6c422e438b6705905da01005d537" checksum = "8dcde03d87d4c973c04be249e7d8f0b35db1c848c487bd43032808e59dd8328d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "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]] [[package]]
name = "shellexpand" name = "shellexpand"
version = "2.1.0" version = "2.1.0"
@@ -659,9 +684,9 @@ dependencies = [
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.7.0" version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
[[package]] [[package]]
name = "strsim" name = "strsim"
@@ -689,9 +714,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.85" version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a684ac3dcd8913827e18cd09a68384ee66c1de24157e3c556c9ab16d85695fb7" checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -850,3 +875,12 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 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",
]

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "git-repo-manager" name = "git-repo-manager"
version = "0.5.0" version = "0.6.0"
edition = "2021" edition = "2021"
authors = [ authors = [
"Hannes Körber <hannes@hkoerber.de>", "Hannes Körber <hannes@hkoerber.de>",
@@ -40,7 +40,7 @@ path = "src/grm/main.rs"
version = "=0.5.8" version = "=0.5.8"
[dependencies.serde] [dependencies.serde]
version = "=1.0.133" version = "=1.0.135"
features = ["derive"] features = ["derive"]
[dependencies.git2] [dependencies.git2]
@@ -50,7 +50,7 @@ version = "=0.13.25"
version = "=2.1.0" version = "=2.1.0"
[dependencies.clap] [dependencies.clap]
version = "=3.0.5" version = "=3.0.10"
features = ["derive", "cargo"] features = ["derive", "cargo"]
[dependencies.console] [dependencies.console]
@@ -62,5 +62,8 @@ version = "=1.5.4"
[dependencies.comfy-table] [dependencies.comfy-table]
version = "=5.0.0" version = "=5.0.0"
[dependencies.serde_yaml]
version = "=0.8.23"
[dev-dependencies.tempdir] [dev-dependencies.tempdir]
version = "=0.3.7" version = "=0.3.7"

View File

@@ -52,3 +52,6 @@ check-pip-requirements: e2e-venv
@cd ./e2e_tests \ @cd ./e2e_tests \
&& . ./venv/bin/activate \ && . ./venv/bin/activate \
&& pip list --outdated | grep -q '.' && exit 1 || exit 0 && pip list --outdated | grep -q '.' && exit 1 || exit 0
clean:
cargo clean

View File

@@ -14,7 +14,13 @@ AUTOUPDATE_DISABLED = []
if os.path.exists(INDEX_DIR): if os.path.exists(INDEX_DIR):
subprocess.run( 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, cwd=INDEX_DIR,
check=True, check=True,
capture_output=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 # This updates the crates.io index, see https://github.com/rust-lang/cargo/issues/3377
subprocess.run( subprocess.run(
["cargo", "search", "--limit", "0"], ["cargo", "update", "--dry-run"],
check=True, check=True,
capture_output=False, # to get some git output capture_output=False, # to get some git output
) )

View File

@@ -5,7 +5,8 @@ Manager](https://github.com/hakoerber/git-repo-manager/) (GRM for short), a
tool that helps you manage git repositories. tool that helps you manage git repositories.
GRM helps you manage git repositories in a declarative way. Configure your 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) 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 to get a feel for the way you configure your repositories. See the [repository
tree chapter](./repos.md) for details. tree chapter](./repos.md) for details.

View File

@@ -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.

View File

@@ -309,6 +309,10 @@ grm wt pull --rebase
[✔] my-cool-branch: Done [✔] 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 This will rebase your changes onto the upstream branch. This is mainly helpful
for persistent branches that change on the remote side. 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 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)! 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 ### Manual access
GRM isn't doing any magic, it's just git under the hood. If you need to have access GRM isn't doing any magic, it's just git under the hood. If you need to have access

View File

@@ -1,12 +1,13 @@
attrs==21.4.0 attrs==21.4.0
gitdb==4.0.9 gitdb==4.0.9
GitPython==3.1.25 GitPython==3.1.26
iniconfig==1.1.1 iniconfig==1.1.1
packaging==21.3 packaging==21.3
pluggy==1.0.0 pluggy==1.0.0
py==1.11.0 py==1.11.0
pyparsing==3.0.6 pyparsing==3.0.7
pytest==6.2.5 pytest==6.2.5
PyYAML==6.0
smmap==5.0.0 smmap==5.0.0
toml==0.10.2 toml==0.10.2
typing_extensions==4.0.1 typing_extensions==4.0.1

View File

@@ -3,6 +3,8 @@
import tempfile import tempfile
import toml import toml
import pytest
import yaml
from helpers import * from helpers import *
@@ -30,6 +32,16 @@ def test_repos_find_empty():
assert len(cmd.stderr) != 0 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(): def test_repos_find_non_git_repos():
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
shell( shell(
@@ -50,7 +62,9 @@ def test_repos_find_non_git_repos():
assert len(cmd.stderr) != 0 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: with tempfile.TemporaryDirectory() as tmpdir:
shell( shell(
f""" 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 cmd.returncode == 0
assert len(cmd.stderr) == 0 assert len(cmd.stderr) == 0
if default or configtype == "toml":
output = toml.loads(cmd.stdout) output = toml.loads(cmd.stdout)
elif configtype == "yaml":
output = yaml.safe_load(cmd.stdout)
else:
raise NotImplementedError()
assert isinstance(output, dict) assert isinstance(output, dict)
assert set(output.keys()) == {"trees"} assert set(output.keys()) == {"trees"}
@@ -125,14 +147,24 @@ def test_repos_find():
assert origin["url"] == "https://example.com/repo2.git" 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: 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 cmd.returncode == 0
assert len(cmd.stderr) == 0 assert len(cmd.stderr) == 0
if default or configtype == "toml":
output = toml.loads(cmd.stdout) output = toml.loads(cmd.stdout)
elif configtype == "yaml":
output = yaml.safe_load(cmd.stdout)
else:
raise NotImplementedError()
assert isinstance(output, dict) assert isinstance(output, dict)
assert set(output.keys()) == {"trees"} assert set(output.keys()) == {"trees"}
@@ -160,7 +192,9 @@ def test_repos_find_in_root():
assert someremote["type"] == "file" 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: with tempfile.TemporaryDirectory() as tmpdir:
shell( shell(
f""" 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 cmd.returncode == 0
assert "broken" in cmd.stderr assert "broken" in cmd.stderr
if default or configtype == "toml":
output = toml.loads(cmd.stdout) output = toml.loads(cmd.stdout)
elif configtype == "yaml":
output = yaml.safe_load(cmd.stdout)
else:
raise NotImplementedError()
assert isinstance(output, dict) assert isinstance(output, dict)
assert set(output.keys()) == {"trees"} assert set(output.keys()) == {"trees"}

View File

@@ -2,6 +2,7 @@
import tempfile import tempfile
import re import re
import textwrap
import pytest import pytest
import toml import toml
@@ -9,8 +10,134 @@ import git
from helpers import * 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 tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote, head_commit_sha): with TempGitFileRemote() as (remote, head_commit_sha):
with tempfile.NamedTemporaryFile() as config: with tempfile.NamedTemporaryFile() as config:
@@ -20,19 +147,12 @@ def test_repos_sync_config_is_valid_symlink():
with open(config.name, "w") as f: with open(config.name, "w") as f:
f.write( f.write(
f""" templates["repo_with_remote"][configtype].format(
[[trees]] root=target, remote=remote, remotename="origin"
root = "{target}"
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote}"
type = "file"
"""
) )
)
subprocess.run(["cat", config.name])
cmd = grm(["repos", "sync", "--config", config_symlink]) cmd = grm(["repos", "sync", "--config", config_symlink])
assert cmd.returncode == 0 assert cmd.returncode == 0
@@ -85,20 +205,13 @@ def test_repos_sync_config_is_unreadable():
assert "permission denied" in cmd.stderr.lower() 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 tempfile.TemporaryDirectory() as root:
with TempGitRepository(dir=root) as unmanaged_repo: with TempGitRepository(dir=root) as unmanaged_repo:
with tempfile.NamedTemporaryFile() as config: with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f: with open(config.name, "w") as f:
f.write( f.write(templates["repo_simple"][configtype].format(root=root))
f"""
[[trees]]
root = "{root}"
[[trees.repos]]
name = "test"
"""
)
cmd = grm(["repos", "sync", "--config", config.name]) cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode == 0 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")]) 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 target:
with tempfile.NamedTemporaryFile() as config: with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f: with open(config.name, "w") as f:
f.write( f.write(templates["repo_simple"][configtype].format(root=target.name))
f"""
[[trees]]
root = "{target.name}"
[[trees.repos]]
name = "test"
"""
)
cmd = grm(["repos", "sync", "--config", config.name]) cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode != 0 assert cmd.returncode != 0
@@ -132,30 +238,17 @@ def test_repos_sync_root_is_file():
assert "not a directory" in cmd.stderr.lower() 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 tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha): with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha): with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config: with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f: with open(config.name, "w") as f:
f.write( f.write(
f""" templates["repo_with_two_remotes"][configtype].format(
[[trees]] root=target, remote1=remote1, remote2=remote2
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]) cmd = grm(["repos", "sync", "--config", config.name])
@@ -183,19 +276,12 @@ def test_repos_sync_normal_clone():
assert urls[0] == f"file://{remote2}" 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.TemporaryDirectory() as target:
with tempfile.NamedTemporaryFile() as config: with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f: with open(config.name, "w") as f:
f.write( f.write(templates["repo_simple"][configtype].format(root=target))
f"""
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
"""
)
cmd = grm(["repos", "sync", "--config", config.name]) cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode == 0 assert cmd.returncode == 0
@@ -210,25 +296,17 @@ def test_repos_sync_normal_init():
assert not repo.head.is_valid() 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 tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha): with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha): with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config: with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f: with open(config.name, "w") as f:
f.write( f.write(
f""" templates["repo_with_remote"][configtype].format(
[[trees]] root=target, remote=remote1, remotename="origin"
root = "{target}" )
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
"""
) )
cmd = grm(["repos", "sync", "--config", config.name]) 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: with open(config.name, "w") as f:
f.write( f.write(
f""" templates["repo_with_two_remotes"][configtype].format(
[[trees]] root=target, remote1=remote1, remote2=remote2
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]) cmd = grm(["repos", "sync", "--config", config.name])
@@ -282,30 +346,17 @@ def test_repos_sync_normal_add_remote():
assert urls[0] == f"file://{remote2}" 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 tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha): with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha): with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config: with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f: with open(config.name, "w") as f:
f.write( f.write(
f""" templates["repo_with_two_remotes"][configtype].format(
[[trees]] root=target, remote1=remote1, remote2=remote2
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]) 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: with open(config.name, "w") as f:
f.write( f.write(
f""" templates["repo_with_remote"][configtype].format(
[[trees]] root=target, remote=remote2, remotename="origin2"
root = "{target}" )
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin2"
url = "file://{remote2}"
type = "file"
"""
) )
cmd = grm(["repos", "sync", "--config", config.name]) cmd = grm(["repos", "sync", "--config", config.name])
@@ -369,25 +411,17 @@ def test_repos_sync_normal_remove_remote():
assert urls[0] == f"file://{remote2}" 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 tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha): with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha): with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config: with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f: with open(config.name, "w") as f:
f.write( f.write(
f""" templates["repo_with_remote"][configtype].format(
[[trees]] root=target, remote=remote1, remotename="origin"
root = "{target}" )
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
"""
) )
cmd = grm(["repos", "sync", "--config", config.name]) 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: with open(config.name, "w") as f:
f.write( f.write(
f""" templates["repo_with_remote"][configtype].format(
[[trees]] root=target, remote=remote2, remotename="origin"
root = "{target}" )
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote2}"
type = "file"
"""
) )
cmd = grm(["repos", "sync", "--config", config.name]) 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}" 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 tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha): with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha): with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config: with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f: with open(config.name, "w") as f:
f.write( f.write(
f""" templates["repo_with_remote"][configtype].format(
[[trees]] root=target, remote=remote1, remotename="origin"
root = "{target}" )
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
"""
) )
cmd = grm(["repos", "sync", "--config", config.name]) 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: with open(config.name, "w") as f:
f.write( f.write(
f""" templates["repo_with_remote"][configtype].format(
[[trees]] root=target, remote=remote1, remotename="origin2"
root = "{target}" )
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin2"
url = "file://{remote1}"
type = "file"
"""
) )
cmd = grm(["repos", "sync", "--config", config.name]) 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}" 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 tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote, head_commit_sha): with TempGitFileRemote() as (remote, head_commit_sha):
with tempfile.NamedTemporaryFile() as config: with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f: with open(config.name, "w") as f:
f.write( f.write(
f""" templates["worktree_repo_with_remote"][configtype].format(
[[trees]] root=target, remote=remote, remotename="origin"
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]) 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 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.TemporaryDirectory() as target:
with tempfile.NamedTemporaryFile() as config: with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f: with open(config.name, "w") as f:
f.write( f.write(
f""" templates["worktree_repo_simple"][configtype].format(root=target)
[[trees]]
root = "{target}"
[[trees.repos]]
name = "test"
worktree_setup = true
"""
) )
cmd = grm(["repos", "sync", "--config", config.name]) cmd = grm(["repos", "sync", "--config", config.name])
@@ -559,43 +552,42 @@ def test_repos_sync_worktree_init():
assert not repo.head.is_valid() 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 tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f: with open(config.name, "w") as f:
if configtype == "toml":
f.write( f.write(
f""" f"""
[[trees]] [[trees]]
root = invalid as there are no quotes ;) 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]) cmd = grm(["repos", "sync", "--config", config.name])
assert cmd.returncode != 0 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 tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha): with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha): with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config: with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f: with open(config.name, "w") as f:
f.write( f.write(
f""" templates["repo_with_two_remotes"][configtype].format(
[[trees]] root=target, remote1=remote1, remote2=remote2
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]) cmd = grm(["repos", "sync", "--config", config.name])
@@ -609,25 +601,17 @@ def test_repos_sync_unchanged():
assert before == after 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 tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha): with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha): with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config: with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f: with open(config.name, "w") as f:
f.write( f.write(
f""" templates["repo_with_remote"][configtype].format(
[[trees]] root=target, remote=remote1, remotename="origin"
root = "{target}" )
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
"""
) )
cmd = grm(["repos", "sync", "--config", config.name]) 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: with open(config.name, "w") as f:
f.write( f.write(
f""" templates["worktree_repo_with_remote"][configtype].format(
[[trees]] root=target, remote=remote1, remotename="origin"
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]) 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 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 tempfile.TemporaryDirectory() as target:
with TempGitFileRemote() as (remote1, remote1_head_commit_sha): with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
with TempGitFileRemote() as (remote2, remote2_head_commit_sha): with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
with tempfile.NamedTemporaryFile() as config: with tempfile.NamedTemporaryFile() as config:
with open(config.name, "w") as f: with open(config.name, "w") as f:
f.write( f.write(
f""" templates["worktree_repo_with_remote"][configtype].format(
[[trees]] root=target, remote=remote1, remotename="origin"
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]) 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: with open(config.name, "w") as f:
f.write( f.write(
f""" templates["repo_with_remote"][configtype].format(
[[trees]] root=target, remote=remote1, remotename="origin"
root = "{target}" )
[[trees.repos]]
name = "test"
[[trees.repos.remotes]]
name = "origin"
url = "file://{remote1}"
type = "file"
"""
) )
cmd = grm(["repos", "sync", "--config", config.name]) cmd = grm(["repos", "sync", "--config", config.name])

View File

@@ -2,6 +2,8 @@
from helpers import * from helpers import *
import re
import pytest import pytest
import git import git
@@ -51,7 +53,9 @@ def test_worktree_fetch():
@pytest.mark.parametrize("rebase", [True, False]) @pytest.mark.parametrize("rebase", [True, False])
@pytest.mark.parametrize("ffable", [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 TempGitRepositoryWorktree() as (base_dir, root_commit):
with TempGitFileRemote() as (remote_path, _remote_sha): with TempGitFileRemote() as (remote_path, _remote_sha):
shell( shell(
@@ -94,20 +98,40 @@ 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"] args = ["wt", "pull"]
if rebase: if rebase:
args += ["--rebase"] args += ["--rebase"]
if stash:
args += ["--stash"]
cmd = grm(args, cwd=base_dir) cmd = grm(args, cwd=base_dir)
assert cmd.returncode == 0 if has_changes and not stash:
assert cmd.returncode != 0
assert re.match(r".*master.*contains changes.*", cmd.stderr)
else:
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("origin/master").hexsha == root_commit
assert ( assert (
repo.commit("master").hexsha != repo.commit("origin/master").hexsha repo.commit("master").hexsha
!= repo.commit("origin/master").hexsha
) )
if has_changes:
assert ["uncommitedchange"] == repo.untracked_files
assert repo.is_dirty()
else:
assert not repo.is_dirty()
if not rebase: if not rebase:
if ffable: if ffable:
assert cmd.returncode == 0
assert ( assert (
repo.commit("master").hexsha repo.commit("master").hexsha
!= repo.commit("origin/master").hexsha != repo.commit("origin/master").hexsha
@@ -116,16 +140,22 @@ def test_worktree_pull(rebase, ffable):
repo.commit("master").hexsha repo.commit("master").hexsha
== repo.commit("upstream/master").hexsha == repo.commit("upstream/master").hexsha
) )
assert repo.commit("upstream/master").hexsha == remote_commit assert (
repo.commit("upstream/master").hexsha == remote_commit
)
else: else:
assert cmd.returncode != 0
assert "cannot be fast forwarded" in cmd.stderr assert "cannot be fast forwarded" in cmd.stderr
assert ( assert (
repo.commit("master").hexsha repo.commit("master").hexsha
!= repo.commit("origin/master").hexsha != repo.commit("origin/master").hexsha
) )
assert repo.commit("master").hexsha != remote_commit assert repo.commit("master").hexsha != remote_commit
assert repo.commit("upstream/master").hexsha == remote_commit assert (
repo.commit("upstream/master").hexsha == remote_commit
)
else: else:
assert cmd.returncode == 0
if ffable: if ffable:
assert ( assert (
repo.commit("master").hexsha repo.commit("master").hexsha
@@ -135,7 +165,9 @@ def test_worktree_pull(rebase, ffable):
repo.commit("master").hexsha repo.commit("master").hexsha
== repo.commit("upstream/master").hexsha == repo.commit("upstream/master").hexsha
) )
assert repo.commit("upstream/master").hexsha == remote_commit assert (
repo.commit("upstream/master").hexsha == remote_commit
)
else: else:
assert ( assert (
repo.commit("master").message.strip() repo.commit("master").message.strip()

View File

@@ -2,15 +2,18 @@
from helpers import * from helpers import *
import pytest import re
import pytest
import git import git
@pytest.mark.parametrize("pull", [True, False]) @pytest.mark.parametrize("pull", [True, False])
@pytest.mark.parametrize("rebase", [True, False]) @pytest.mark.parametrize("rebase", [True, False])
@pytest.mark.parametrize("ffable", [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 TempGitRepositoryWorktree() as (base_dir, _root_commit):
with open(os.path.join(base_dir, "grm.toml"), "w") as f: with open(os.path.join(base_dir, "grm.toml"), "w") as f:
f.write('persistent_branches = ["mybasebranch"]') 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) grm(["wt", "delete", "--force", "tmp"], cwd=base_dir)
repo = git.Repo(f"{base_dir}/.git-main-working-tree") repo = git.Repo(f"{base_dir}/.git-main-working-tree")
@@ -133,17 +144,23 @@ def test_worktree_rebase(pull, rebase, ffable):
args += ["--pull"] args += ["--pull"]
if rebase: if rebase:
args += ["--rebase"] args += ["--rebase"]
if stash:
args += ["--stash"]
cmd = grm(args, cwd=base_dir) cmd = grm(args, cwd=base_dir)
print(args)
if rebase and not pull: if rebase and not pull:
assert cmd.returncode != 0 assert cmd.returncode != 0
assert len(cmd.stderr) != 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: else:
assert cmd.returncode == 0
repo = git.Repo(f"{base_dir}/myfeatbranch") repo = git.Repo(f"{base_dir}/myfeatbranch")
if has_changes:
assert ["uncommitedchange"] == repo.untracked_files
if pull: if pull:
if rebase: if rebase:
assert cmd.returncode == 0
if ffable: if ffable:
assert ( assert (
repo.commit("HEAD").message.strip() 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" assert repo.commit("HEAD~6").message.strip() == "commit-root"
else: else:
if ffable: if ffable:
assert cmd.returncode == 0
assert ( assert (
repo.commit("HEAD").message.strip() repo.commit("HEAD").message.strip()
== "commit-in-feat-remote" == "commit-in-feat-remote"
@@ -208,6 +226,7 @@ def test_worktree_rebase(pull, rebase, ffable):
) )
assert repo.commit("HEAD~4").message.strip() == "commit-root" assert repo.commit("HEAD~4").message.strip() == "commit-root"
else: else:
assert cmd.returncode != 0
assert ( assert (
repo.commit("HEAD").message.strip() repo.commit("HEAD").message.strip()
== "commit-in-feat-local-no-ff" == "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" assert repo.commit("HEAD~4").message.strip() == "commit-root"
else: else:
assert cmd.returncode == 0
if ffable: if ffable:
assert repo.commit("HEAD").message.strip() == "commit-in-feat-local" assert repo.commit("HEAD").message.strip() == "commit-in-feat-local"
assert ( assert (

View File

@@ -9,6 +9,8 @@ source ./venv/bin/activate
pip --disable-pip-version-check install -r ./requirements.txt 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 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}" pip install --upgrade "${package}"
version="$(pip show "${package}" | grep '^Version' | cut -d ' ' -f 2)" version="$(pip show "${package}" | grep '^Version' | cut -d ' ' -f 2)"
message="e2e_tests/pip: Update ${package} to ${version}" message="e2e_tests/pip: Update ${package} to ${version}"

16
example.config.yaml Normal file
View 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"

View File

@@ -36,6 +36,10 @@ impl Config {
Err(error) => Err(error.to_string()), 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)] #[derive(Debug, Serialize, Deserialize)]
@@ -61,6 +65,8 @@ pub fn read_config(path: &str) -> Result<Config, String> {
}; };
let config: Config = match toml::from_str(&content) { let config: Config = match toml::from_str(&content) {
Ok(c) => c,
Err(_) => match serde_yaml::from_str(&content) {
Ok(c) => c, Ok(c) => c,
Err(e) => { Err(e) => {
return Err(format!( return Err(format!(
@@ -68,6 +74,7 @@ pub fn read_config(path: &str) -> Result<Config, String> {
path, e path, e
)) ))
} }
},
}; };
Ok(config) Ok(config)

View File

@@ -61,10 +61,25 @@ pub struct OptionalConfig {
pub config: Option<String>, pub config: Option<String>,
} }
#[derive(clap::ArgEnum, Clone)]
pub enum ConfigFormat {
Yaml,
Toml,
}
#[derive(Parser)] #[derive(Parser)]
pub struct Find { pub struct Find {
#[clap(help = "The path to search through")] #[clap(help = "The path to search through")]
pub path: String, pub path: String,
#[clap(
arg_enum,
short,
long,
help = "Format to produce",
default_value_t = ConfigFormat::Toml,
)]
pub format: ConfigFormat,
} }
#[derive(Parser)] #[derive(Parser)]
@@ -132,6 +147,8 @@ pub struct WorktreeFetchArgs {}
pub struct WorktreePullArgs { pub struct WorktreePullArgs {
#[clap(long = "--rebase", help = "Perform a rebase instead of a fast-forward")] #[clap(long = "--rebase", help = "Perform a rebase instead of a fast-forward")]
pub rebase: bool, pub rebase: bool,
#[clap(long = "--stash", help = "Stash & unstash changes before & after pull")]
pub stash: bool,
} }
#[derive(Parser)] #[derive(Parser)]
@@ -140,6 +157,11 @@ pub struct WorktreeRebaseArgs {
pub pull: bool, pub pull: bool,
#[clap(long = "--rebase", help = "Perform a rebase when doing a pull")] #[clap(long = "--rebase", help = "Perform a rebase when doing a pull")]
pub rebase: bool, pub rebase: bool,
#[clap(
long = "--stash",
help = "Stash & unstash changes before & after rebase"
)]
pub stash: bool,
} }
pub fn parse() -> Opts { pub fn parse() -> Opts {

View File

@@ -119,16 +119,35 @@ fn main() {
} else { } else {
let config = trees.to_config(); let config = trees.to_config();
match find.format {
cmd::ConfigFormat::Toml => {
let toml = match config.as_toml() { let toml = match config.as_toml() {
Ok(toml) => toml, Ok(toml) => toml,
Err(error) => { Err(error) => {
print_error(&format!("Failed converting config to TOML: {}", &error)); print_error(&format!(
"Failed converting config to TOML: {}",
&error
));
process::exit(1); 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 { for warning in warnings {
print_warning(&warning); print_warning(&warning);
} }
@@ -350,26 +369,27 @@ fn main() {
process::exit(1); process::exit(1);
}); });
let mut failures = false;
for worktree in repo.get_worktrees().unwrap_or_else(|error| { for worktree in repo.get_worktrees().unwrap_or_else(|error| {
print_error(&format!("Error getting worktrees: {}", error)); print_error(&format!("Error getting worktrees: {}", error));
process::exit(1); process::exit(1);
}) { }) {
if let Some(warning) = if let Some(warning) = worktree
worktree .forward_branch(args.rebase, args.stash)
.forward_branch(args.rebase)
.unwrap_or_else(|error| { .unwrap_or_else(|error| {
print_error(&format!( print_error(&format!("Error updating worktree branch: {}", error));
"Error updating worktree branch: {}",
error
));
process::exit(1); process::exit(1);
}) })
{ {
print_warning(&format!("{}: {}", worktree.name(), warning)); print_warning(&format!("{}: {}", worktree.name(), warning));
failures = true;
} else { } else {
print_success(&format!("{}: Done", worktree.name())); print_success(&format!("{}: Done", worktree.name()));
} }
} }
if failures {
process::exit(1);
}
} }
cmd::WorktreeAction::Rebase(args) => { cmd::WorktreeAction::Rebase(args) => {
if args.rebase && !args.pull { if args.rebase && !args.pull {
@@ -406,10 +426,12 @@ fn main() {
process::exit(1); process::exit(1);
}); });
let mut failures = false;
for worktree in &worktrees { for worktree in &worktrees {
if args.pull { if args.pull {
if let Some(warning) = worktree if let Some(warning) = worktree
.forward_branch(args.rebase) .forward_branch(args.rebase, args.stash)
.unwrap_or_else(|error| { .unwrap_or_else(|error| {
print_error(&format!( print_error(&format!(
"Error updating worktree branch: {}", "Error updating worktree branch: {}",
@@ -418,28 +440,29 @@ fn main() {
process::exit(1); process::exit(1);
}) })
{ {
failures = true;
print_warning(&format!("{}: {}", worktree.name(), warning)); print_warning(&format!("{}: {}", worktree.name(), warning));
} }
} }
} }
for worktree in &worktrees { for worktree in &worktrees {
if let Some(warning) = if let Some(warning) = worktree
worktree .rebase_onto_default(&config, args.stash)
.rebase_onto_default(&config)
.unwrap_or_else(|error| { .unwrap_or_else(|error| {
print_error(&format!( print_error(&format!("Error rebasing worktree branch: {}", error));
"Error rebasing worktree branch: {}",
error
));
process::exit(1); process::exit(1);
}) })
{ {
failures = true;
print_warning(&format!("{}: {}", worktree.name(), warning)); print_warning(&format!("{}: {}", worktree.name(), warning));
} else { } else {
print_success(&format!("{}: Done", worktree.name())); print_success(&format!("{}: Done", worktree.name()));
} }
} }
if failures {
process::exit(1);
}
} }
} }
} }

View File

@@ -181,16 +181,29 @@ impl Worktree {
&self.name &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) let repo = Repo::open(Path::new(&self.name), false)
.map_err(|error| format!("Error opening worktree: {}", error))?; .map_err(|error| format!("Error opening worktree: {}", error))?;
if let Ok(remote_branch) = repo.find_local_branch(&self.name)?.upstream() { if let Ok(remote_branch) = repo.find_local_branch(&self.name)?.upstream() {
let status = repo.status(false)?; let status = repo.status(false)?;
let mut stashed_changes = false;
if !status.clean() { if !status.clean() {
if stash {
repo.stash()?;
stashed_changes = true;
} else {
return Ok(Some(String::from("Worktree contains changes"))); return Ok(Some(String::from("Worktree contains changes")));
} }
}
let unstash = || -> Result<(), String> {
if stashed_changes {
repo.stash_pop()?;
}
Ok(())
};
let remote_annotated_commit = repo let remote_annotated_commit = repo
.0 .0
@@ -231,6 +244,7 @@ impl Worktree {
continue; continue;
} }
rebase.abort().map_err(convert_libgit2_error)?; rebase.abort().map_err(convert_libgit2_error)?;
unstash()?;
return Err(convert_libgit2_error(error)); return Err(convert_libgit2_error(error));
} }
} }
@@ -243,9 +257,11 @@ impl Worktree {
.map_err(convert_libgit2_error)?; .map_err(convert_libgit2_error)?;
if analysis.is_up_to_date() { if analysis.is_up_to_date() {
unstash()?;
return Ok(None); return Ok(None);
} }
if !analysis.is_fast_forward() { if !analysis.is_fast_forward() {
unstash()?;
return Ok(Some(String::from("Worktree cannot be fast forwarded"))); return Ok(Some(String::from("Worktree cannot be fast forwarded")));
} }
@@ -257,15 +273,18 @@ impl Worktree {
) )
.map_err(convert_libgit2_error)?; .map_err(convert_libgit2_error)?;
} }
unstash()?;
} else { } else {
return Ok(Some(String::from("No remote branch to rebase onto"))); return Ok(Some(String::from("No remote branch to rebase onto")));
}; };
Ok(None) Ok(None)
} }
pub fn rebase_onto_default( pub fn rebase_onto_default(
&self, &self,
config: &Option<WorktreeRootConfig>, config: &Option<WorktreeRootConfig>,
stash: bool,
) -> Result<Option<String>, String> { ) -> Result<Option<String>, String> {
let repo = Repo::open(Path::new(&self.name), false) let repo = Repo::open(Path::new(&self.name), false)
.map_err(|error| format!("Error opening worktree: {}", error))?; .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_branch = repo.find_local_branch(&default_branch_name)?;
let base_annotated_commit = repo let base_annotated_commit = repo
.0 .0
@@ -330,11 +368,13 @@ impl Worktree {
continue; continue;
} }
rebase.abort().map_err(convert_libgit2_error)?; rebase.abort().map_err(convert_libgit2_error)?;
unstash()?;
return Err(convert_libgit2_error(error)); return Err(convert_libgit2_error(error));
} }
} }
rebase.finish(None).map_err(convert_libgit2_error)?; rebase.finish(None).map_err(convert_libgit2_error)?;
unstash()?;
Ok(None) 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> { pub fn rename_remote(&self, remote: &RemoteHandle, new_name: &str) -> Result<(), String> {
let failed_refspecs = self let failed_refspecs = self
.0 .0
@@ -1253,6 +1322,10 @@ impl Commit<'_> {
pub fn id(&self) -> Oid { pub fn id(&self) -> Oid {
Oid(self.0.id()) Oid(self.0.id())
} }
pub(self) fn author(&self) -> git2::Signature {
self.0.author()
}
} }
impl<'a> Branch<'a> { impl<'a> Branch<'a> {
@@ -1323,9 +1396,7 @@ fn get_remote_callbacks() -> git2::RemoteCallbacks<'static> {
Some(username) => username, Some(username) => username,
None => panic!("Could not get username. This is a bug"), None => panic!("Could not get username. This is a bug"),
}; };
git2::Cred::ssh_key_from_agent(username).or_else(|_| { git2::Cred::ssh_key_from_agent(username)
git2::Cred::ssh_key(username, None, &crate::env_home().join(".ssh/id_rsa"), None)
})
}); });
callbacks callbacks