Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7aa45c7768 | |||
| a065de5b2d | |||
| d976ccefc2 | |||
| de186901d0 | |||
| c9f4d41780 | |||
| b3906c646a | |||
| 43c47bdca6 | |||
| df0b5728fc | |||
| d0b78686e2 | |||
| f02a0fc17a | |||
| 4e83aba672 | |||
| e2e55b8e79 | |||
| 655379cd61 | |||
| 340085abf8 | |||
| f5f8dfa188 | |||
| e43d4bf3cd | |||
| 48f3bc0199 | |||
| 0973ae36b8 | |||
| 09c67d4908 | |||
| 47841dadfb | |||
| 102758c25c | |||
| 6aa385b044 | |||
| e44b63edbb | |||
| 1e6c9407b6 | |||
| b967b6dca3 | |||
| 83973f8a1a | |||
| ff32759058 | |||
| b6c06e29a4 | |||
| 6bec0eda69 | |||
| 7541a74fa4 | |||
| 4ad4a55631 | |||
| e2db935c74 | |||
| 99c4f33e28 | |||
| f50fc9aee2 | |||
| 1cf5585e2c | |||
| 0c6a4a72ef | |||
| e516a652f5 | |||
| ddce614009 | |||
| d677c2d41b | |||
| 3aecee3549 | |||
| 667ea87c39 | |||
| 3e18caf719 | |||
| 711d9131da | |||
| 8ba214d6cf | |||
| 77c00cee5f | |||
| 12cb18c528 | |||
| 6b80a0f2d5 | |||
| 06e7d68089 | |||
| db0f91f32f | |||
| fa40f4d6aa | |||
| 78a957268d | |||
| ca1f649ecf | |||
| bbedc9d8a8 | |||
| 09f22edf49 | |||
| b0746c95b5 | |||
| 153d09f3ef | |||
| 74a7772a29 | |||
| 5df6dcb053 |
29
.github/workflows/gh-pages.yml
vendored
Normal file
29
.github/workflows/gh-pages.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: github pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-20.04
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup mdBook
|
||||
uses: peaceiris/actions-mdbook@v1
|
||||
with:
|
||||
mdbook-version: 'latest'
|
||||
|
||||
- run: cd ./docs && mdbook build
|
||||
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
if: ${{ github.ref == 'refs/heads/master' }}
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./docs/book
|
||||
81
Cargo.lock
generated
81
Cargo.lock
generated
@@ -169,6 +169,12 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-cprng"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.3"
|
||||
@@ -182,7 +188,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "git-repo-manager"
|
||||
version = "0.1.0"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"comfy-table",
|
||||
@@ -191,14 +197,15 @@ dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
"shellexpand",
|
||||
"tempdir",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "git2"
|
||||
version = "0.13.23"
|
||||
version = "0.13.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a8057932925d3a9d9e4434ea016570d37420ddb1ceed45a174d577f24ed6700"
|
||||
checksum = "845e007a28f1fcac035715988a234e8ec5458fd825b20a20c7dec74237ef341f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
@@ -280,15 +287,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.107"
|
||||
version = "0.2.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219"
|
||||
checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119"
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
version = "0.12.24+1.3.0"
|
||||
version = "0.12.25+1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddbd6021eef06fb289a8f54b3c2acfdd85ff2a585dfbb24b8576325373d2152c"
|
||||
checksum = "8f68169ef08d6519b2fe133ecc637408d933c0174b23b80bb2f79828966fbaab"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -399,9 +406,9 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.70"
|
||||
version = "0.9.71"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6517987b3f8226b5da3661dad65ff7f300cc59fb5ea8333ca191fc65fde3edf"
|
||||
checksum = "7df13d165e607909b363a4757a6f133f8a818a74e9d3a98d09c6128e15fa4c73"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cc",
|
||||
@@ -498,6 +505,43 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293"
|
||||
dependencies = [
|
||||
"fuchsia-cprng",
|
||||
"libc",
|
||||
"rand_core 0.3.1",
|
||||
"rdrand",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
|
||||
dependencies = [
|
||||
"rand_core 0.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
|
||||
|
||||
[[package]]
|
||||
name = "rdrand"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
|
||||
dependencies = [
|
||||
"rand_core 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.2.10"
|
||||
@@ -534,6 +578,15 @@ version = "0.6.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||
|
||||
[[package]]
|
||||
name = "remove_dir_all"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.1.0"
|
||||
@@ -640,6 +693,16 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempdir"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8"
|
||||
dependencies = [
|
||||
"rand",
|
||||
"remove_dir_all",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.1.2"
|
||||
|
||||
17
Cargo.toml
17
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "git-repo-manager"
|
||||
version = "0.1.0"
|
||||
version = "0.4.0"
|
||||
edition = "2021"
|
||||
authors = [
|
||||
"Hannes Körber <hannes@hkoerber.de>",
|
||||
@@ -37,17 +37,17 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
|
||||
[dependencies.toml]
|
||||
version = "0.5"
|
||||
version = "0.5.8"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0"
|
||||
version = "1.0.130"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.git2]
|
||||
version = "0.13"
|
||||
version = "0.13.24"
|
||||
|
||||
[dependencies.shellexpand]
|
||||
version = "2.1"
|
||||
version = "2.1.0"
|
||||
|
||||
[dependencies.clap]
|
||||
version = "3.0.0-beta.5"
|
||||
@@ -56,7 +56,10 @@ version = "3.0.0-beta.5"
|
||||
version = "0.15.0"
|
||||
|
||||
[dependencies.regex]
|
||||
version = "1.5"
|
||||
version = "1.5.4"
|
||||
|
||||
[dependencies.comfy-table]
|
||||
version = "5.0"
|
||||
version = "5.0.0"
|
||||
|
||||
[dev-dependencies.tempdir]
|
||||
version = "0.3.7"
|
||||
|
||||
36
Justfile
36
Justfile
@@ -1,8 +1,40 @@
|
||||
lint:
|
||||
cargo clippy --no-deps
|
||||
check: test
|
||||
cargo check
|
||||
cargo fmt --check
|
||||
cargo clippy --no-deps -- -Dwarnings
|
||||
|
||||
lint-fix:
|
||||
cargo clippy --no-deps --fix
|
||||
|
||||
release:
|
||||
cargo build --release
|
||||
|
||||
install:
|
||||
cargo install --path .
|
||||
|
||||
test: test-unit test-integration test-e2e
|
||||
|
||||
test-unit:
|
||||
cargo test --lib --bins
|
||||
|
||||
test-integration:
|
||||
cargo test --test "*"
|
||||
|
||||
e2e-venv:
|
||||
cd ./e2e_tests \
|
||||
&& python3 -m venv venv \
|
||||
&& . ./venv/bin/activate \
|
||||
&& pip --disable-pip-version-check install -r ./requirements.txt >/dev/null
|
||||
|
||||
|
||||
test-e2e: e2e-venv release
|
||||
cd ./e2e_tests \
|
||||
&& . ./venv/bin/activate \
|
||||
&& python -m pytest .
|
||||
|
||||
update-dependencies:
|
||||
@cd ./depcheck \
|
||||
&& python3 -m venv ./venv \
|
||||
&& . ./venv/bin/activate \
|
||||
&& pip --disable-pip-version-check install -r ./requirements.txt > /dev/null \
|
||||
&& ./update-cargo-dependencies.py
|
||||
|
||||
81
README.md
81
README.md
@@ -3,81 +3,8 @@
|
||||
GRM helps you manage git repositories in a declarative way. Configure your
|
||||
repositories in a [TOML](https://toml.io/) file, GRM does the rest.
|
||||
|
||||
## Quickstart
|
||||
|
||||
See [the example configuration](example.config.toml) to get a feel for the way
|
||||
you configure your repositories.
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
$ cargo install --git https://github.com/hakoerber/git-repo-manager.git --branch master
|
||||
```
|
||||
|
||||
### Get the example configuration
|
||||
|
||||
```bash
|
||||
$ curl --proto '=https' --tlsv1.2 -sSfO https://raw.githubusercontent.com/hakoerber/git-repo-manager/master/example.config.toml
|
||||
```
|
||||
|
||||
### Run it!
|
||||
|
||||
```bash
|
||||
$ grm sync --config example.config.toml
|
||||
[⚙] Cloning into "/home/me/projects/git-repo-manager" from "https://code.hkoerber.de/hannes/git-repo-manager.git"
|
||||
[✔] git-repo-manager: Repository successfully cloned
|
||||
[⚙] git-repo-manager: Setting up new remote "github" to "https://github.com/hakoerber/git-repo-manager.git"
|
||||
[✔] git-repo-manager: OK
|
||||
[⚙] Cloning into "/home/me/projects/dotfiles" from "https://github.com/hakoerber/dotfiles.git"
|
||||
[✔] dotfiles: Repository successfully cloned
|
||||
[✔] dotfiles: OK
|
||||
```
|
||||
|
||||
If you run it again, it will report no changes:
|
||||
|
||||
```
|
||||
$ grm sync --config example.config.toml
|
||||
[✔] git-repo-manager: OK
|
||||
[✔] dotfiles: OK
|
||||
```
|
||||
|
||||
### Generate your own configuration
|
||||
|
||||
Now, if you already have a few repositories, it would be quite laborious to write
|
||||
a configuration from scratch. Luckily, GRM has a way to generate a configuration
|
||||
from an existing file tree:
|
||||
|
||||
```bash
|
||||
$ grm find ~/your/project/root > config.toml
|
||||
```
|
||||
|
||||
This will detect all repositories and remotes and write them to `config.toml`.
|
||||
|
||||
### Show the state of your projects
|
||||
|
||||
```bash
|
||||
$ grm status --config example.config.toml
|
||||
+------------------+------------+----------------------------------+--------+---------+
|
||||
| Repo | Status | Branches | HEAD | Remotes |
|
||||
+=====================================================================================+
|
||||
| git-repo-manager | | branch: master <origin/master> ✔ | master | github |
|
||||
| | | | | origin |
|
||||
|------------------+------------+----------------------------------+--------+---------|
|
||||
| dotfiles | No changes | branch: master <origin/master> ✔ | master | origin |
|
||||
+------------------+------------+----------------------------------+--------+---------+
|
||||
```
|
||||
|
||||
You can also use `status` without `--config` to check the current directory:
|
||||
|
||||
```
|
||||
$ cd ./dotfiles
|
||||
$ grm status
|
||||
+----------+------------+----------------------------------+--------+---------+
|
||||
| Repo | Status | Branches | HEAD | Remotes |
|
||||
+=============================================================================+
|
||||
| dotfiles | No changes | branch: master <origin/master> ✔ | master | origin |
|
||||
+----------+------------+----------------------------------+--------+---------+
|
||||
```
|
||||
**Take a look at the [official documentation](https://hakoerber.github.io/git-repo-manager/)
|
||||
for installation & quickstart.**
|
||||
|
||||
# Why?
|
||||
|
||||
@@ -117,10 +44,6 @@ repositories itself.
|
||||
* Support multiple file formats (YAML, JSON).
|
||||
* Add systemd timer unit to run regular syncs
|
||||
|
||||
# Dev Notes
|
||||
|
||||
It requires nightly features due to the usage of [`std::path::Path::is_symlink()`](https://doc.rust-lang.org/std/fs/struct.FileType.html#method.is_symlink). See the [tracking issue](https://github.com/rust-lang/rust/issues/85748).
|
||||
|
||||
# Crates
|
||||
|
||||
* [`toml`](https://docs.rs/toml/) for the configuration file
|
||||
|
||||
2
depcheck/.gitignore
vendored
Normal file
2
depcheck/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/crates.io-index/
|
||||
/venv/
|
||||
2
depcheck/requirements.txt
Normal file
2
depcheck/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
semver==2.13.0
|
||||
tomlkit==0.7.2
|
||||
74
depcheck/update-cargo-dependencies.py
Executable file
74
depcheck/update-cargo-dependencies.py
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
|
||||
import semver
|
||||
import tomlkit
|
||||
|
||||
INDEX_DIR = "crates.io-index"
|
||||
|
||||
if os.path.exists(INDEX_DIR):
|
||||
subprocess.run(
|
||||
["git", "pull", "--depth=1", "origin"],
|
||||
cwd=INDEX_DIR,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
else:
|
||||
subprocess.run(
|
||||
["git", "clone", "--depth=1", "https://github.com/rust-lang/crates.io-index"],
|
||||
check=True,
|
||||
capture_output=False, # to get some git output
|
||||
)
|
||||
|
||||
with open("../Cargo.toml", "r") as cargo_config:
|
||||
cargo = tomlkit.parse(cargo_config.read())
|
||||
|
||||
update_necessary = False
|
||||
|
||||
for tier in ["dependencies", "dev-dependencies"]:
|
||||
for name, dependency in cargo[tier].items():
|
||||
version = dependency["version"]
|
||||
if len(name) >= 4:
|
||||
info_file = f"{INDEX_DIR}/{name[0:2]}/{name[2:4]}/{name}"
|
||||
elif len(name) == 3:
|
||||
info_file = f"{INDEX_DIR}/3/{name[0]}/{name}"
|
||||
elif len(name) == 2:
|
||||
info_file = f"{INDEX_DIR}/2/{name}"
|
||||
elif len(name) == 1:
|
||||
info_file = f"{INDEX_DIR}/1/{name}"
|
||||
|
||||
current_version = semver.VersionInfo.parse(version)
|
||||
|
||||
latest_version = None
|
||||
for version_entry in open(info_file, "r").readlines():
|
||||
version = semver.VersionInfo.parse(json.loads(version_entry)["vers"])
|
||||
if current_version.prerelease == "" and version.prerelease != "":
|
||||
# skip prereleases, except when we are on a prerelease already
|
||||
continue
|
||||
if latest_version is None or version > latest_version:
|
||||
latest_version = version
|
||||
|
||||
if latest_version != current_version:
|
||||
update_necessary = True
|
||||
if latest_version < current_version:
|
||||
print(
|
||||
f"{name}: Your current version is newer than the newest version on crates.io, the hell?"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"{name}: New version found: {latest_version} (current {current_version})"
|
||||
)
|
||||
cargo[tier][name]["version"] = str(latest_version)
|
||||
|
||||
|
||||
if update_necessary is True:
|
||||
with open("../Cargo.toml", "w") as cargo_config:
|
||||
cargo_config.write(tomlkit.dumps(cargo))
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("Everything up to date")
|
||||
sys.exit(0)
|
||||
1
docs/.gitignore
vendored
Normal file
1
docs/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
book
|
||||
9
docs/book.toml
Normal file
9
docs/book.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[book]
|
||||
authors = ["Hannes Körber"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "Git Repo Manager"
|
||||
|
||||
[output.html]
|
||||
mathjax-support = true
|
||||
7
docs/src/SUMMARY.md
Normal file
7
docs/src/SUMMARY.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Summary
|
||||
|
||||
- [Overview](./overview.md)
|
||||
- [Getting started](./getting_started.md)
|
||||
- [Repository trees](./repos.md)
|
||||
- [Git Worktrees](./worktrees.md)
|
||||
- [FAQ](./faq.md)
|
||||
10
docs/src/faq.md
Normal file
10
docs/src/faq.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# FAQ
|
||||
|
||||
## Why is the nightly toolchain required?
|
||||
|
||||
Building GRM currently requires nightly features due to the usage of
|
||||
[`std::path::Path::is_symlink()`](https://doc.rust-lang.org/std/fs/struct.FileType.html#method.is_symlink).
|
||||
See the [tracking issue](https://github.com/rust-lang/rust/issues/85748).
|
||||
|
||||
`is_symlink()` is actually available in rustc 1.57, so it will be on stable in
|
||||
the near future. This would mean that GRM can be built using the stable toolchain!
|
||||
22
docs/src/getting_started.md
Normal file
22
docs/src/getting_started.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Quickstart
|
||||
|
||||
## Installation
|
||||
|
||||
Building GRM currently requires the nightly Rust toolchain. The easiest way
|
||||
is using [`rustup`](https://rustup.rs/). Make sure that rustup is properly installed.
|
||||
|
||||
Make sure that the nightly toolchain is installed:
|
||||
|
||||
```
|
||||
$ rustup toolchain install nightly
|
||||
```
|
||||
|
||||
```bash
|
||||
$ cargo +nightly install --git https://github.com/hakoerber/git-repo-manager.git --branch master
|
||||
```
|
||||
|
||||
If you're brave, you can also run the development build:
|
||||
|
||||
```bash
|
||||
$ cargo +nightly install --git https://github.com/hakoerber/git-repo-manager.git --branch develop
|
||||
```
|
||||
33
docs/src/overview.md
Normal file
33
docs/src/overview.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Overview
|
||||
|
||||
Welcome! This is the documentation for [Git Repo
|
||||
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
|
||||
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.
|
||||
|
||||
GRM also provides some tooling to work with single git repositories using
|
||||
`git-worktree`. See [the worktree chapter](./worktree.md) for more details.
|
||||
|
||||
## Why use GRM?
|
||||
|
||||
If you're working with a lot of git repositories, GRM can help you to manage them
|
||||
in an easy way:
|
||||
|
||||
* You want to easily clone many repositories to a new machine.
|
||||
* You want to change remotes for multiple repositories (e.g. because your GitLab
|
||||
domain changed).
|
||||
* You want to get an overview over all repositories you have, and check whether
|
||||
you forgot to commit or push something.
|
||||
|
||||
If you want to work with [git worktrees](https://git-scm.com/docs/git-worktree)
|
||||
in a streamlined, easy way, GRM provides you with an opinionated workflow. It's
|
||||
especially helpful when the following describes you:
|
||||
|
||||
* You're juggling a lot of git branches, switching between them a lot.
|
||||
* When switching branches, you'd like to just leave your work as-is, without
|
||||
using the stash or temporary commits.
|
||||
76
docs/src/repos.md
Normal file
76
docs/src/repos.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Managing tree of git repositories
|
||||
|
||||
When managing multiple git repositories with GRM, you'll generally have a
|
||||
configuration file containing information about all the repos you have. GRM then
|
||||
makes sure that you repositories match that config. If they don't exist yet, it
|
||||
will clone them. It will also make sure that all remotes are configured properly.
|
||||
|
||||
Let's try it out:
|
||||
|
||||
## Get the example configuration
|
||||
|
||||
```bash
|
||||
$ curl --proto '=https' --tlsv1.2 -sSfO https://raw.githubusercontent.com/hakoerber/git-repo-manager/master/example.config.toml
|
||||
```
|
||||
|
||||
Then, you're ready to run the first sync. This will clone all configured repositories
|
||||
and set up the remotes.
|
||||
|
||||
```bash
|
||||
$ grm repos sync --config example.config.toml
|
||||
[⚙] Cloning into "/home/me/projects/git-repo-manager" from "https://code.hkoerber.de/hannes/git-repo-manager.git"
|
||||
[✔] git-repo-manager: Repository successfully cloned
|
||||
[⚙] git-repo-manager: Setting up new remote "github" to "https://github.com/hakoerber/git-repo-manager.git"
|
||||
[✔] git-repo-manager: OK
|
||||
[⚙] Cloning into "/home/me/projects/dotfiles" from "https://github.com/hakoerber/dotfiles.git"
|
||||
[✔] dotfiles: Repository successfully cloned
|
||||
[✔] dotfiles: OK
|
||||
```
|
||||
|
||||
If you run it again, it will report no changes:
|
||||
|
||||
```
|
||||
$ grm repos sync --config example.config.toml
|
||||
[✔] git-repo-manager: OK
|
||||
[✔] dotfiles: OK
|
||||
```
|
||||
|
||||
### Generate your own configuration
|
||||
|
||||
Now, if you already have a few repositories, it would be quite laborious to write
|
||||
a configuration from scratch. Luckily, GRM has a way to generate a configuration
|
||||
from an existing file tree:
|
||||
|
||||
```bash
|
||||
$ grm repos find ~/your/project/root > config.toml
|
||||
```
|
||||
|
||||
This will detect all repositories and remotes and write them to `config.toml`.
|
||||
|
||||
### Show the state of your projects
|
||||
|
||||
```bash
|
||||
$ grm repos status --config example.config.toml
|
||||
╭──────────────────┬──────────┬────────┬───────────────────┬────────┬─────────╮
|
||||
│ Repo ┆ Worktree ┆ Status ┆ Branches ┆ HEAD ┆ Remotes │
|
||||
╞══════════════════╪══════════╪════════╪═══════════════════╪════════╪═════════╡
|
||||
│ git-repo-manager ┆ ┆ ✔ ┆ branch: master ┆ master ┆ github │
|
||||
│ ┆ ┆ ┆ <origin/master> ✔ ┆ ┆ origin │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
|
||||
│ dotfiles ┆ ┆ ✔ ┆ ┆ Empty ┆ origin │
|
||||
╰──────────────────┴──────────┴────────┴───────────────────┴────────┴─────────╯
|
||||
```
|
||||
|
||||
You can also use `status` without `--config` to check the repository you're currently
|
||||
in:
|
||||
|
||||
```
|
||||
$ cd ~/example-projects/dotfiles
|
||||
$ grm repos status
|
||||
╭──────────┬──────────┬────────┬──────────┬───────┬─────────╮
|
||||
│ Repo ┆ Worktree ┆ Status ┆ Branches ┆ HEAD ┆ Remotes │
|
||||
╞══════════╪══════════╪════════╪══════════╪═══════╪═════════╡
|
||||
│ dotfiles ┆ ┆ ✔ ┆ ┆ Empty ┆ origin │
|
||||
╰──────────┴──────────┴────────┴──────────┴───────┴─────────╯
|
||||
```
|
||||
|
||||
227
docs/src/worktrees.md
Normal file
227
docs/src/worktrees.md
Normal file
@@ -0,0 +1,227 @@
|
||||
# Git Worktrees
|
||||
|
||||
## Why?
|
||||
|
||||
The default workflow when using git is having your repository in a single directory.
|
||||
Then, you can check out a certain reference (usually a branch), which will update
|
||||
the files in the directory to match the state of that reference. Most of the time,
|
||||
this is exactly what you need and works perfectly. But especially when you're using
|
||||
with branches a lot, you may notice that there is a lot of work required to make
|
||||
everything run smootly.
|
||||
|
||||
Maybe you experienced the following: You're working on a feature branch. Then,
|
||||
for some reason, you have to change branches (maybe to investigate some issue).
|
||||
But you get the following:
|
||||
|
||||
```
|
||||
error: Your local changes to the following files would be overwritten by checkout
|
||||
```
|
||||
|
||||
Now you can create a temporary commit or stash your changes. In any case, you have
|
||||
some mental overhead before you can work on something else. Especially with stashes,
|
||||
you'll have to remember to do a `git stash pop` before resuming your work (I
|
||||
cannot count the number of times where is "rediscovered" some code hidden in some
|
||||
old stash I forgot about.
|
||||
|
||||
And even worse: If you're currently in the process of resolving merge conflicts or an
|
||||
interactive rebase, there is just no way to "pause" this work to check out a
|
||||
different branch.
|
||||
|
||||
Sometimes, it's crucial to have an unchanging state of your repository until some
|
||||
long-running process finishes. I'm thinking of Ansible and Terraform runs. I'd
|
||||
rather not change to a different branch while ansible or Terraform are running as
|
||||
I have no idea how those tools would behave (and I'm not too eager to find out).
|
||||
|
||||
In any case, Git Worktrees are here for the rescue:
|
||||
|
||||
## What are git worktrees?
|
||||
|
||||
[Git Worktrees](https://git-scm.com/docs/git-worktree) allow you to have multiple
|
||||
independent checkouts of your repository on different directories. You can have
|
||||
multiple directories that correspond to different references in your repository.
|
||||
Each worktree has it's independent working tree (duh) and index, so there is no
|
||||
to run into conflicts. Changing to a different branch is just a `cd` away (if
|
||||
the worktree is already set up).
|
||||
|
||||
## Worktrees in GRM
|
||||
|
||||
GRM exposes an opinionated way to use worktrees in your repositories. Opinionated,
|
||||
because there is a single invariant that makes reasoning about your worktree
|
||||
setup quite easy:
|
||||
|
||||
**The branch inside the worktree is always the same as the directory name of the worktree.**
|
||||
|
||||
In other words: If you're checking out branch `mybranch` into a new worktree, the
|
||||
worktree directory will be named `mybranch`.
|
||||
|
||||
GRM can be used with both "normal" and worktree-enabled repositories. But note
|
||||
that a single repository can be either the former or the latter. You'll have to
|
||||
decide during the initial setup which way you want to go for that repository.
|
||||
|
||||
If you want to clone your repository in a worktree-enabled way, specify
|
||||
`worktree_setup = true` for the repository in your `config.toml`:
|
||||
|
||||
```toml
|
||||
[[trees.repos]]
|
||||
name = "git-repo-manager"
|
||||
worktree_setup = true
|
||||
```
|
||||
|
||||
Now, when you run a `grm sync`, you'll notice that the directory of the repository
|
||||
is empty! Well, not totally, there is a hidden directory called `.git-main-working-tree`.
|
||||
This is where the repository actually "lives" (it's a bare checkout).
|
||||
|
||||
### Creating a new worktree
|
||||
|
||||
To actually work, you'll first have to create a new worktree checkout. All
|
||||
worktree-related commands are available as subcommands of `grm worktree` (or
|
||||
`grm wt` for short):
|
||||
|
||||
```
|
||||
$ grm wt add mybranch
|
||||
[✔] Worktree mybranch created
|
||||
```
|
||||
|
||||
You'll see that there is now a directory called `mybranch` that contains a checkout
|
||||
of your repository, using the branch `mybranch`
|
||||
|
||||
```bash
|
||||
$ cd ./mybranch && git status
|
||||
On branch mybranch
|
||||
nothing to commit, working tree clean
|
||||
```
|
||||
|
||||
You can work in this repository as usual. Make changes, commit them, revert them,
|
||||
whatever you're up to :)
|
||||
|
||||
Just note that you *should* not change the branch inside the worktree
|
||||
directory. There is nothing preventing you from doing so, but you will notice
|
||||
that you'll run into problems when trying to remove a worktree (more on that
|
||||
later). It may also lead to confusing behaviour, as there can be no two
|
||||
worktrees that have the same branch checked out. So if you decide to use the
|
||||
worktree setup, go all in, let `grm` manage your branches and bury `git branch`
|
||||
(and `git checkout -b`).
|
||||
|
||||
You will notice that there is no tracking branch set up for the new branch. You
|
||||
can of course set up one manually after creating the worktree, but there is an
|
||||
easier way, using the `--track` flag during creation. Let's create another
|
||||
worktree. Go back to the root of the repository, and run:
|
||||
|
||||
```bash
|
||||
$ grm wt add mybranch2 --track origin/mybranch2
|
||||
[✔] Worktree mybranch2 created
|
||||
```
|
||||
|
||||
You'll see that this branch is now tracking `mybranch` on the `origin` remote:
|
||||
|
||||
```bash
|
||||
$ cd ./mybranch2 && git status
|
||||
On branch mybranch
|
||||
|
||||
Your branch is up to date with 'origin/mybranch2'.
|
||||
nothing to commit, working tree clean
|
||||
```
|
||||
|
||||
The behaviour of `--track` differs depending on the existence of the remote branch:
|
||||
|
||||
* If the remote branch already exists, `grm` uses it as the base of the new
|
||||
local branch.
|
||||
* If the remote branch does not exist (as in our example), `grm` will create a
|
||||
new remote tracking branch, using the default branch (either `main` or `master`)
|
||||
as the base
|
||||
|
||||
### Showing the status of your worktrees
|
||||
|
||||
There is a handy little command that will show your an overview over all worktrees
|
||||
in a repository, including their status (i.e. changes files). Just run the following
|
||||
in the root of your repository:
|
||||
|
||||
```
|
||||
$ grm wt status
|
||||
╭───────────┬────────┬──────────┬──────────────────╮
|
||||
│ Worktree ┆ Status ┆ Branch ┆ Remote branch │
|
||||
╞═══════════╪════════╪══════════╪══════════════════╡
|
||||
│ mybranch ┆ ✔ ┆ mybranch ┆ │
|
||||
│ mybranch2 ┆ ✔ ┆ mybranch ┆ origin/mybranch2 │
|
||||
╰───────────┴────────┴──────────┴──────────────────╯
|
||||
```
|
||||
|
||||
The "Status" column would show any uncommitted changes (new / modified / deleted
|
||||
files) and the "Remote branch" would show differences to the remote branch (e.g.
|
||||
if there are new pushes to the remote branch that are not yet incorporated into
|
||||
your local branch).
|
||||
|
||||
|
||||
### Deleting worktrees
|
||||
|
||||
If you're done with your worktrees, use `grm wt delete` to delete them. Let's
|
||||
start with `mybranch2`:
|
||||
|
||||
```
|
||||
$ grm wt delete mybranch2
|
||||
[✔] Worktree mybranch2 deleted
|
||||
```
|
||||
|
||||
Easy. On to `mybranch`:
|
||||
|
||||
```
|
||||
$ grm wt delete mybranch
|
||||
[!] Changes in worktree: No remote tracking branch for branch mybranch found. Refusing to delete
|
||||
```
|
||||
|
||||
Hmmm. `grm` tells you:
|
||||
|
||||
"Hey, there is no remote branch that you could have pushed
|
||||
your changes to. I'd rather not delete work that you cannot recover."
|
||||
|
||||
Note that `grm` is very cautious here. As your repository will not be deleted,
|
||||
you could still recover the commits via [`git-reflog`](https://git-scm.com/docs/git-reflog).
|
||||
But better safe then sorry! Note that you'd get a similar error message if your
|
||||
worktree had any uncommitted files, for the same reason. Now you can either
|
||||
commit & push your changes, or your tell `grm` that you know what you're doing:
|
||||
|
||||
```
|
||||
$ grm wt delete mybranch --force
|
||||
[✔] Worktree mybranch deleted
|
||||
```
|
||||
|
||||
If you just want to delete all worktrees that do not contain any changes, you
|
||||
can also use the following:
|
||||
|
||||
```
|
||||
$ grm wt clean
|
||||
```
|
||||
|
||||
Note that this will not delete the default branch of the repository. It can of
|
||||
course still be delete with `grm wt delete` if neccessary.
|
||||
|
||||
### Converting an existing repository
|
||||
|
||||
It is possible to convert an existing directory to a worktree setup, using `grm
|
||||
wt convert`. This command has to be run in the root of the repository you want
|
||||
to convert:
|
||||
|
||||
```
|
||||
grm wt convert
|
||||
[✔] Conversion successful
|
||||
```
|
||||
|
||||
This command will refuse to run if you have any changes in your repository.
|
||||
Commit them and try again!
|
||||
|
||||
Afterwards, the directory is empty, as there are no worktrees checked out yet.
|
||||
Now you can use the usual commands to set up worktrees.
|
||||
|
||||
### Manual access
|
||||
|
||||
GRM isn't doing any magic, it's just git under the hood. If you need to have access
|
||||
to the underlying git repository, you can always do this:
|
||||
|
||||
```
|
||||
$ git --git-dir ./.git-main-working-tree [...]
|
||||
```
|
||||
|
||||
This should never be required (whenever you have to do this, you can consider
|
||||
this a bug in GRM and open an [issue](https://github.com/hakoerber/git-repo-manager/issues/new),
|
||||
but it may help in a pinch.
|
||||
|
||||
2
e2e_tests/.gitignore
vendored
Normal file
2
e2e_tests/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/venv/
|
||||
/__pycache__/
|
||||
229
e2e_tests/helpers.py
Normal file
229
e2e_tests/helpers.py
Normal file
@@ -0,0 +1,229 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import subprocess
|
||||
import tempfile
|
||||
import hashlib
|
||||
|
||||
import git
|
||||
|
||||
binary = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "target/release/grm"
|
||||
)
|
||||
|
||||
|
||||
def grm(args, cwd=None, is_invalid=False):
|
||||
cmd = subprocess.run([binary] + args, cwd=cwd, capture_output=True, text=True)
|
||||
if not is_invalid:
|
||||
assert "USAGE" not in cmd.stderr
|
||||
print(f"grmcmd: {args}")
|
||||
print(f"stdout:\n{cmd.stdout}")
|
||||
print(f"stderr:\n{cmd.stderr}")
|
||||
assert "panicked" not in cmd.stderr
|
||||
return cmd
|
||||
|
||||
|
||||
def shell(script):
|
||||
script = "set -o errexit\nset -o nounset\n" + script
|
||||
subprocess.run(["bash"], input=script, text=True, check=True)
|
||||
|
||||
|
||||
def checksum_directory(path):
|
||||
"""
|
||||
Gives a "checksum" of a directory that includes all files & directories
|
||||
recursively, including owner/group/permissions. Useful to compare that a
|
||||
directory did not change after a command was run.
|
||||
|
||||
The following makes it a bit complicated:
|
||||
|
||||
> Whether or not the lists are sorted depends on the file system.
|
||||
|
||||
- https://docs.python.org/3/library/os.html#os.walk
|
||||
|
||||
This means we have to first get a list of all hashes of files and
|
||||
directories, then sort the hashes and then create the hash for the whole
|
||||
directory.
|
||||
"""
|
||||
path = os.path.realpath(path)
|
||||
|
||||
hashes = []
|
||||
|
||||
if not os.path.exists(path):
|
||||
raise f"{path} not found"
|
||||
|
||||
def get_stat_hash(path):
|
||||
stat = bytes(str(os.stat(path).__hash__()), "ascii")
|
||||
return stat
|
||||
|
||||
for root, dirs, files in os.walk(path):
|
||||
for file in files:
|
||||
checksum = hashlib.md5()
|
||||
filepath = os.path.join(root, file)
|
||||
checksum.update(str.encode(filepath))
|
||||
checksum.update(get_stat_hash(filepath))
|
||||
with open(filepath, "rb") as f:
|
||||
while True:
|
||||
data = f.read(8192)
|
||||
if not data:
|
||||
break
|
||||
checksum.update(data)
|
||||
hashes.append(checksum.digest())
|
||||
|
||||
for d in dirs:
|
||||
checksum = hashlib.md5()
|
||||
dirpath = os.path.join(root, d)
|
||||
checksum.update(get_stat_hash(dirpath))
|
||||
hashes.append(checksum.digest())
|
||||
|
||||
checksum = hashlib.md5()
|
||||
for c in sorted(hashes):
|
||||
checksum.update(c)
|
||||
return checksum.hexdigest()
|
||||
|
||||
|
||||
class TempGitRepository:
|
||||
def __init__(self, dir=None):
|
||||
self.dir = dir
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
self.tmpdir = tempfile.TemporaryDirectory(dir=self.dir)
|
||||
self.remote_1_dir = tempfile.TemporaryDirectory()
|
||||
self.remote_2_dir = tempfile.TemporaryDirectory()
|
||||
shell(
|
||||
f"""
|
||||
cd {self.tmpdir.name}
|
||||
git init
|
||||
echo test > test
|
||||
git add test
|
||||
git commit -m "commit1"
|
||||
git remote add origin file://{self.remote_1_dir.name}
|
||||
git remote add otherremote file://{self.remote_2_dir.name}
|
||||
"""
|
||||
)
|
||||
return self.tmpdir.name
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
del self.tmpdir
|
||||
del self.remote_1_dir
|
||||
del self.remote_2_dir
|
||||
|
||||
|
||||
class TempGitRepositoryWorktree:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
self.tmpdir = tempfile.TemporaryDirectory()
|
||||
self.remote_1_dir = tempfile.TemporaryDirectory()
|
||||
self.remote_2_dir = tempfile.TemporaryDirectory()
|
||||
shell(
|
||||
f"""
|
||||
cd {self.remote_1_dir.name}
|
||||
git init --bare
|
||||
"""
|
||||
)
|
||||
shell(
|
||||
f"""
|
||||
cd {self.remote_2_dir.name}
|
||||
git init --bare
|
||||
"""
|
||||
)
|
||||
shell(
|
||||
f"""
|
||||
cd {self.tmpdir.name}
|
||||
git init
|
||||
echo test > test
|
||||
git add test
|
||||
git commit -m "commit1"
|
||||
echo test > test2
|
||||
git add test2
|
||||
git commit -m "commit2"
|
||||
git remote add origin file://{self.remote_1_dir.name}
|
||||
git remote add otherremote file://{self.remote_2_dir.name}
|
||||
git ls-files | xargs rm -rf
|
||||
mv .git .git-main-working-tree
|
||||
git --git-dir .git-main-working-tree config core.bare true
|
||||
"""
|
||||
)
|
||||
return self.tmpdir.name
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
del self.tmpdir
|
||||
del self.remote_1_dir
|
||||
del self.remote_2_dir
|
||||
|
||||
|
||||
class EmptyDir:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
self.tmpdir = tempfile.TemporaryDirectory()
|
||||
return self.tmpdir.name
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
del self.tmpdir
|
||||
|
||||
|
||||
class NonGitDir:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
self.tmpdir = tempfile.TemporaryDirectory()
|
||||
shell(
|
||||
f"""
|
||||
cd {self.tmpdir.name}
|
||||
mkdir testdir
|
||||
touch testdir/test
|
||||
touch test2
|
||||
"""
|
||||
)
|
||||
return self.tmpdir.name
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
del self.tmpdir
|
||||
|
||||
|
||||
class TempGitFileRemote:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
self.tmpdir = tempfile.TemporaryDirectory()
|
||||
shell(
|
||||
f"""
|
||||
cd {self.tmpdir.name}
|
||||
git init
|
||||
echo test > test
|
||||
git add test
|
||||
git commit -m "commit1"
|
||||
echo test > test2
|
||||
git add test2
|
||||
git commit -m "commit2"
|
||||
git ls-files | xargs rm -rf
|
||||
mv .git/* .
|
||||
git config core.bare true
|
||||
"""
|
||||
)
|
||||
head_commit_sha = git.Repo(self.tmpdir.name).head.commit.hexsha
|
||||
return (self.tmpdir.name, head_commit_sha)
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
del self.tmpdir
|
||||
|
||||
|
||||
class NonExistentPath:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
self.dir = "/doesnotexist"
|
||||
if os.path.exists(self.dir):
|
||||
raise f"{self.dir} exists for some reason"
|
||||
return self.dir
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
pass
|
||||
12
e2e_tests/requirements.txt
Normal file
12
e2e_tests/requirements.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
attrs==21.2.0
|
||||
gitdb==4.0.9
|
||||
GitPython==3.1.24
|
||||
iniconfig==1.1.1
|
||||
packaging==21.3
|
||||
pluggy==1.0.0
|
||||
py==1.11.0
|
||||
pyparsing==3.0.6
|
||||
pytest==6.2.5
|
||||
smmap==5.0.0
|
||||
toml==0.10.2
|
||||
typing-extensions==4.0.0
|
||||
13
e2e_tests/test_basic.py
Normal file
13
e2e_tests/test_basic.py
Normal file
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from helpers import *
|
||||
|
||||
|
||||
def test_invalid_command():
|
||||
cmd = grm(["whatever"], is_invalid=True)
|
||||
assert "USAGE" in cmd.stderr
|
||||
|
||||
|
||||
def test_help():
|
||||
cmd = grm(["--help"])
|
||||
assert "USAGE" in cmd.stdout
|
||||
160
e2e_tests/test_repos_find.py
Normal file
160
e2e_tests/test_repos_find.py
Normal file
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import tempfile
|
||||
|
||||
import toml
|
||||
|
||||
from helpers import *
|
||||
|
||||
|
||||
def test_repos_find_nonexistent():
|
||||
with NonExistentPath() as nonexistent_dir:
|
||||
cmd = grm(["repos", "find", nonexistent_dir])
|
||||
assert "does not exist" in cmd.stderr.lower()
|
||||
assert cmd.returncode != 0
|
||||
assert not os.path.exists(nonexistent_dir)
|
||||
|
||||
|
||||
def test_repos_find_file():
|
||||
with tempfile.NamedTemporaryFile() as tmpfile:
|
||||
cmd = grm(["repos", "find", tmpfile.name])
|
||||
assert "not a directory" in cmd.stderr.lower()
|
||||
assert cmd.returncode != 0
|
||||
|
||||
|
||||
def test_repos_find_empty():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
cmd = grm(["repos", "find", tmpdir])
|
||||
assert cmd.returncode == 0
|
||||
assert len(cmd.stdout) == 0
|
||||
assert len(cmd.stderr) != 0
|
||||
|
||||
|
||||
def test_repos_find_non_git_repos():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
shell(
|
||||
f"""
|
||||
cd {tmpdir}
|
||||
mkdir non_git
|
||||
(
|
||||
cd ./non_git
|
||||
echo test > test
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "find", tmpdir])
|
||||
|
||||
assert cmd.returncode == 0
|
||||
assert len(cmd.stdout) == 0
|
||||
assert len(cmd.stderr) != 0
|
||||
|
||||
|
||||
def test_repos_find():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
shell(
|
||||
f"""
|
||||
cd {tmpdir}
|
||||
mkdir repo1
|
||||
(
|
||||
cd ./repo1
|
||||
git init
|
||||
echo test > test
|
||||
git add test
|
||||
git commit -m "commit1"
|
||||
git remote add origin https://example.com/repo2.git
|
||||
git remote add someremote ssh://example.com/repo2.git
|
||||
)
|
||||
mkdir repo2
|
||||
(
|
||||
cd ./repo2
|
||||
git init
|
||||
git co -b main
|
||||
echo test > test
|
||||
git add test
|
||||
git commit -m "commit1"
|
||||
git remote add origin https://example.com/repo2.git
|
||||
)
|
||||
mkdir non_git
|
||||
(
|
||||
cd non_git
|
||||
echo test > test
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "find", tmpdir])
|
||||
assert cmd.returncode == 0
|
||||
assert len(cmd.stderr) == 0
|
||||
|
||||
output = toml.loads(cmd.stdout)
|
||||
|
||||
assert isinstance(output, dict)
|
||||
assert set(output.keys()) == {"trees"}
|
||||
assert isinstance(output["trees"], list)
|
||||
assert len(output["trees"]) == 1
|
||||
for tree in output["trees"]:
|
||||
assert set(tree.keys()) == {"root", "repos"}
|
||||
assert tree["root"] == tmpdir
|
||||
assert isinstance(tree["repos"], list)
|
||||
assert len(tree["repos"]) == 2
|
||||
|
||||
repo1 = [r for r in tree["repos"] if r["name"] == "repo1"][0]
|
||||
assert repo1["worktree_setup"] is False
|
||||
assert isinstance(repo1["remotes"], list)
|
||||
assert len(repo1["remotes"]) == 2
|
||||
|
||||
origin = [r for r in repo1["remotes"] if r["name"] == "origin"][0]
|
||||
assert set(origin.keys()) == {"name", "type", "url"}
|
||||
assert origin["type"] == "https"
|
||||
assert origin["url"] == "https://example.com/repo2.git"
|
||||
|
||||
someremote = [r for r in repo1["remotes"] if r["name"] == "someremote"][0]
|
||||
assert set(origin.keys()) == {"name", "type", "url"}
|
||||
assert someremote["type"] == "ssh"
|
||||
assert someremote["url"] == "ssh://example.com/repo2.git"
|
||||
|
||||
repo2 = [r for r in tree["repos"] if r["name"] == "repo2"][0]
|
||||
assert repo2["worktree_setup"] is False
|
||||
assert isinstance(repo1["remotes"], list)
|
||||
assert len(repo2["remotes"]) == 1
|
||||
|
||||
origin = [r for r in repo2["remotes"] if r["name"] == "origin"][0]
|
||||
assert set(origin.keys()) == {"name", "type", "url"}
|
||||
assert origin["type"] == "https"
|
||||
assert origin["url"] == "https://example.com/repo2.git"
|
||||
|
||||
|
||||
def test_repos_find_in_root():
|
||||
with TempGitRepository() as repo_dir:
|
||||
|
||||
cmd = grm(["repos", "find", repo_dir])
|
||||
assert cmd.returncode == 0
|
||||
assert len(cmd.stderr) == 0
|
||||
|
||||
output = toml.loads(cmd.stdout)
|
||||
|
||||
assert isinstance(output, dict)
|
||||
assert set(output.keys()) == {"trees"}
|
||||
assert isinstance(output["trees"], list)
|
||||
assert len(output["trees"]) == 1
|
||||
for tree in output["trees"]:
|
||||
assert set(tree.keys()) == {"root", "repos"}
|
||||
assert tree["root"] == os.path.dirname(repo_dir)
|
||||
assert isinstance(tree["repos"], list)
|
||||
assert len(tree["repos"]) == 1
|
||||
|
||||
repo1 = [
|
||||
r for r in tree["repos"] if r["name"] == os.path.basename(repo_dir)
|
||||
][0]
|
||||
assert repo1["worktree_setup"] is False
|
||||
assert isinstance(repo1["remotes"], list)
|
||||
assert len(repo1["remotes"]) == 2
|
||||
|
||||
origin = [r for r in repo1["remotes"] if r["name"] == "origin"][0]
|
||||
assert set(origin.keys()) == {"name", "type", "url"}
|
||||
assert origin["type"] == "file"
|
||||
|
||||
someremote = [r for r in repo1["remotes"] if r["name"] == "otherremote"][0]
|
||||
assert set(origin.keys()) == {"name", "type", "url"}
|
||||
assert someremote["type"] == "file"
|
||||
707
e2e_tests/test_repos_sync.py
Normal file
707
e2e_tests/test_repos_sync.py
Normal file
@@ -0,0 +1,707 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import tempfile
|
||||
import re
|
||||
|
||||
import pytest
|
||||
import toml
|
||||
import git
|
||||
|
||||
from helpers import *
|
||||
|
||||
|
||||
def test_repos_sync_config_is_valid_symlink():
|
||||
with tempfile.TemporaryDirectory() as target:
|
||||
with TempGitFileRemote() as (remote, head_commit_sha):
|
||||
with tempfile.NamedTemporaryFile() as config:
|
||||
with tempfile.TemporaryDirectory() as config_dir:
|
||||
config_symlink = os.path.join(config_dir, "cfglink")
|
||||
os.symlink(config.name, config_symlink)
|
||||
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = "{target}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin"
|
||||
url = "file://{remote}"
|
||||
type = "file"
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config_symlink])
|
||||
assert cmd.returncode == 0
|
||||
|
||||
git_dir = os.path.join(target, "test")
|
||||
assert os.path.exists(git_dir)
|
||||
with git.Repo(git_dir) as repo:
|
||||
assert not repo.bare
|
||||
assert not repo.is_dirty()
|
||||
assert set([str(r) for r in repo.remotes]) == {"origin"}
|
||||
assert str(repo.active_branch) == "master"
|
||||
assert str(repo.head.commit) == head_commit_sha
|
||||
|
||||
|
||||
def test_repos_sync_config_is_invalid_symlink():
|
||||
with tempfile.TemporaryDirectory() as target:
|
||||
with tempfile.TemporaryDirectory() as config_dir:
|
||||
with NonExistentPath() as nonexistent_dir:
|
||||
config_symlink = os.path.join(config_dir, "cfglink")
|
||||
os.symlink(nonexistent_dir, config_symlink)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config_symlink])
|
||||
|
||||
assert cmd.returncode != 0
|
||||
assert len(cmd.stdout) == 0
|
||||
assert "not found" in cmd.stderr.lower()
|
||||
assert not os.path.exists(os.path.join(target, "test"))
|
||||
assert not os.path.exists(os.path.join(target, "test"))
|
||||
|
||||
|
||||
def test_repos_sync_config_is_directory():
|
||||
with tempfile.TemporaryDirectory() as config:
|
||||
cmd = grm(["repos", "sync", "--config", config])
|
||||
|
||||
assert cmd.returncode != 0
|
||||
assert len(cmd.stdout) == 0
|
||||
assert "is a directory" in cmd.stderr.lower()
|
||||
|
||||
|
||||
def test_repos_sync_config_is_unreadable():
|
||||
with tempfile.TemporaryDirectory() as config_dir:
|
||||
config_path = os.path.join(config_dir, "cfg")
|
||||
open(config_path, "w")
|
||||
os.chmod(config_path, 0o0000)
|
||||
cmd = grm(["repos", "sync", "--config", config_path])
|
||||
|
||||
assert os.path.exists(config_path)
|
||||
assert cmd.returncode != 0
|
||||
assert len(cmd.stdout) == 0
|
||||
assert "permission denied" in cmd.stderr.lower()
|
||||
|
||||
|
||||
def test_repos_sync_unmanaged_repos():
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
with TempGitRepository(dir=root) as unmanaged_repo:
|
||||
with tempfile.NamedTemporaryFile() as config:
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = "{root}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode == 0
|
||||
|
||||
git_dir = os.path.join(root, "test")
|
||||
assert os.path.exists(git_dir)
|
||||
|
||||
# this removes the prefix (root) from the path (unmanaged_repo)
|
||||
unmanaged_repo_name = os.path.relpath(unmanaged_repo, root)
|
||||
regex = f".*unmanaged.*{unmanaged_repo_name}"
|
||||
assert any([re.match(regex, l) for l in cmd.stderr.lower().split("\n")])
|
||||
|
||||
|
||||
def test_repos_sync_root_is_file():
|
||||
with tempfile.NamedTemporaryFile() as target:
|
||||
with tempfile.NamedTemporaryFile() as config:
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = "{target.name}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode != 0
|
||||
assert len(cmd.stdout) == 0
|
||||
assert "not a directory" in cmd.stderr.lower()
|
||||
|
||||
|
||||
def test_repos_sync_normal_clone():
|
||||
with tempfile.TemporaryDirectory() as target:
|
||||
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
|
||||
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
|
||||
with tempfile.NamedTemporaryFile() as config:
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = "{target}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin"
|
||||
url = "file://{remote1}"
|
||||
type = "file"
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin2"
|
||||
url = "file://{remote2}"
|
||||
type = "file"
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode == 0
|
||||
|
||||
git_dir = os.path.join(target, "test")
|
||||
assert os.path.exists(git_dir)
|
||||
with git.Repo(git_dir) as repo:
|
||||
assert not repo.bare
|
||||
assert not repo.is_dirty()
|
||||
assert set([str(r) for r in repo.remotes]) == {
|
||||
"origin",
|
||||
"origin2",
|
||||
}
|
||||
assert str(repo.active_branch) == "master"
|
||||
assert str(repo.head.commit) == remote1_head_commit_sha
|
||||
|
||||
assert len(repo.remotes) == 2
|
||||
urls = list(repo.remote("origin").urls)
|
||||
assert len(urls) == 1
|
||||
assert urls[0] == f"file://{remote1}"
|
||||
|
||||
urls = list(repo.remote("origin2").urls)
|
||||
assert len(urls) == 1
|
||||
assert urls[0] == f"file://{remote2}"
|
||||
|
||||
|
||||
def test_repos_sync_normal_init():
|
||||
with tempfile.TemporaryDirectory() as target:
|
||||
with tempfile.NamedTemporaryFile() as config:
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = "{target}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode == 0
|
||||
|
||||
git_dir = os.path.join(target, "test")
|
||||
assert os.path.exists(git_dir)
|
||||
with git.Repo(git_dir) as repo:
|
||||
assert not repo.bare
|
||||
assert not repo.is_dirty()
|
||||
# as there are no commits yet, HEAD does not point to anything
|
||||
# valid
|
||||
assert not repo.head.is_valid()
|
||||
|
||||
|
||||
def test_repos_sync_normal_add_remote():
|
||||
with tempfile.TemporaryDirectory() as target:
|
||||
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
|
||||
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
|
||||
with tempfile.NamedTemporaryFile() as config:
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = "{target}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin"
|
||||
url = "file://{remote1}"
|
||||
type = "file"
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode == 0
|
||||
|
||||
git_dir = os.path.join(target, "test")
|
||||
|
||||
assert os.path.exists(git_dir)
|
||||
with git.Repo(git_dir) as repo:
|
||||
assert not repo.bare
|
||||
assert not repo.is_dirty()
|
||||
assert set([str(r) for r in repo.remotes]) == {"origin"}
|
||||
assert str(repo.active_branch) == "master"
|
||||
assert str(repo.head.commit) == remote1_head_commit_sha
|
||||
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = "{target}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin"
|
||||
url = "file://{remote1}"
|
||||
type = "file"
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin2"
|
||||
url = "file://{remote2}"
|
||||
type = "file"
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode == 0
|
||||
with git.Repo(git_dir) as repo:
|
||||
assert set([str(r) for r in repo.remotes]) == {
|
||||
"origin",
|
||||
"origin2",
|
||||
}
|
||||
|
||||
urls = list(repo.remote("origin").urls)
|
||||
assert len(urls) == 1
|
||||
assert urls[0] == f"file://{remote1}"
|
||||
|
||||
urls = list(repo.remote("origin2").urls)
|
||||
assert len(urls) == 1
|
||||
assert urls[0] == f"file://{remote2}"
|
||||
|
||||
|
||||
def test_repos_sync_normal_remove_remote():
|
||||
with tempfile.TemporaryDirectory() as target:
|
||||
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
|
||||
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
|
||||
with tempfile.NamedTemporaryFile() as config:
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = "{target}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin"
|
||||
url = "file://{remote1}"
|
||||
type = "file"
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin2"
|
||||
url = "file://{remote2}"
|
||||
type = "file"
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode == 0
|
||||
|
||||
git_dir = os.path.join(target, "test")
|
||||
|
||||
assert os.path.exists(git_dir)
|
||||
with git.Repo(git_dir) as repo:
|
||||
assert not repo.bare
|
||||
assert not repo.is_dirty()
|
||||
assert set([str(r) for r in repo.remotes]) == {
|
||||
"origin",
|
||||
"origin2",
|
||||
}
|
||||
assert str(repo.active_branch) == "master"
|
||||
assert str(repo.head.commit) == remote1_head_commit_sha
|
||||
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = "{target}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin2"
|
||||
url = "file://{remote2}"
|
||||
type = "file"
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode == 0
|
||||
shell(f"cd {git_dir} && git remote -v")
|
||||
with git.Repo(git_dir) as repo:
|
||||
"""
|
||||
There is some bug(?) in GitPython. It does not properly
|
||||
detect removed remotes. It will still report the old
|
||||
remove in repo.remotes.
|
||||
|
||||
So instead, we make sure that we get an Exception when
|
||||
we try to access the old remove via repo.remote().
|
||||
|
||||
Note that repo.remote() checks the actual repo lazily.
|
||||
Even `exists()` seems to just check against repo.remotes
|
||||
and will return True even if the remote is not actually
|
||||
configured. So we have to force GitPython to hit the filesystem.
|
||||
calling Remotes.urls does. But it returns an iterator
|
||||
that first has to be unwrapped via list(). Only THEN
|
||||
do we actually get an exception of the remotes does not
|
||||
exist.
|
||||
"""
|
||||
with pytest.raises(git.exc.GitCommandError):
|
||||
list(repo.remote("origin").urls)
|
||||
|
||||
urls = list(repo.remote("origin2").urls)
|
||||
assert len(urls) == 1
|
||||
assert urls[0] == f"file://{remote2}"
|
||||
|
||||
|
||||
def test_repos_sync_normal_change_remote_url():
|
||||
with tempfile.TemporaryDirectory() as target:
|
||||
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
|
||||
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
|
||||
with tempfile.NamedTemporaryFile() as config:
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = "{target}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin"
|
||||
url = "file://{remote1}"
|
||||
type = "file"
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode == 0
|
||||
|
||||
git_dir = os.path.join(target, "test")
|
||||
|
||||
assert os.path.exists(git_dir)
|
||||
with git.Repo(git_dir) as repo:
|
||||
assert not repo.bare
|
||||
assert not repo.is_dirty()
|
||||
assert set([str(r) for r in repo.remotes]) == {"origin"}
|
||||
assert str(repo.active_branch) == "master"
|
||||
assert str(repo.head.commit) == remote1_head_commit_sha
|
||||
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = "{target}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin"
|
||||
url = "file://{remote2}"
|
||||
type = "file"
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode == 0
|
||||
with git.Repo(git_dir) as repo:
|
||||
assert set([str(r) for r in repo.remotes]) == {"origin"}
|
||||
|
||||
urls = list(repo.remote("origin").urls)
|
||||
assert len(urls) == 1
|
||||
assert urls[0] == f"file://{remote2}"
|
||||
|
||||
|
||||
def test_repos_sync_normal_change_remote_name():
|
||||
with tempfile.TemporaryDirectory() as target:
|
||||
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
|
||||
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
|
||||
with tempfile.NamedTemporaryFile() as config:
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = "{target}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin"
|
||||
url = "file://{remote1}"
|
||||
type = "file"
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode == 0
|
||||
|
||||
git_dir = os.path.join(target, "test")
|
||||
|
||||
assert os.path.exists(git_dir)
|
||||
with git.Repo(git_dir) as repo:
|
||||
assert not repo.bare
|
||||
assert not repo.is_dirty()
|
||||
assert set([str(r) for r in repo.remotes]) == {"origin"}
|
||||
assert str(repo.active_branch) == "master"
|
||||
assert str(repo.head.commit) == remote1_head_commit_sha
|
||||
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = "{target}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin2"
|
||||
url = "file://{remote1}"
|
||||
type = "file"
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode == 0
|
||||
with git.Repo(git_dir) as repo:
|
||||
# See the note in `test_repos_sync_normal_remove_remote()`
|
||||
# about repo.remotes
|
||||
with pytest.raises(git.exc.GitCommandError):
|
||||
list(repo.remote("origin").urls)
|
||||
|
||||
urls = list(repo.remote("origin2").urls)
|
||||
assert len(urls) == 1
|
||||
assert urls[0] == f"file://{remote1}"
|
||||
|
||||
|
||||
def test_repos_sync_worktree_clone():
|
||||
with tempfile.TemporaryDirectory() as target:
|
||||
with TempGitFileRemote() as (remote, head_commit_sha):
|
||||
with tempfile.NamedTemporaryFile() as config:
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = "{target}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
worktree_setup = true
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin"
|
||||
url = "file://{remote}"
|
||||
type = "file"
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode == 0
|
||||
|
||||
worktree_dir = f"{target}/test"
|
||||
assert os.path.exists(worktree_dir)
|
||||
|
||||
assert set(os.listdir(worktree_dir)) == {".git-main-working-tree"}
|
||||
|
||||
with git.Repo(
|
||||
os.path.join(worktree_dir, ".git-main-working-tree")
|
||||
) as repo:
|
||||
assert repo.bare
|
||||
assert set([str(r) for r in repo.remotes]) == {"origin"}
|
||||
assert str(repo.active_branch) == "master"
|
||||
assert str(repo.head.commit) == head_commit_sha
|
||||
|
||||
|
||||
def test_repos_sync_worktree_init():
|
||||
with tempfile.TemporaryDirectory() as target:
|
||||
with tempfile.NamedTemporaryFile() as config:
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = "{target}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
worktree_setup = true
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode == 0
|
||||
|
||||
worktree_dir = f"{target}/test"
|
||||
assert os.path.exists(worktree_dir)
|
||||
|
||||
assert set(os.listdir(worktree_dir)) == {".git-main-working-tree"}
|
||||
with git.Repo(os.path.join(worktree_dir, ".git-main-working-tree")) as repo:
|
||||
assert repo.bare
|
||||
# as there are no commits yet, HEAD does not point to anything
|
||||
# valid
|
||||
assert not repo.head.is_valid()
|
||||
|
||||
|
||||
def test_repos_sync_invalid_toml():
|
||||
with tempfile.NamedTemporaryFile() as config:
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = invalid as there are no quotes ;)
|
||||
"""
|
||||
)
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode != 0
|
||||
|
||||
|
||||
def test_repos_sync_unchanged():
|
||||
with tempfile.TemporaryDirectory() as target:
|
||||
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
|
||||
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
|
||||
with tempfile.NamedTemporaryFile() as config:
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = "{target}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin"
|
||||
url = "file://{remote1}"
|
||||
type = "file"
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin2"
|
||||
url = "file://{remote2}"
|
||||
type = "file"
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode == 0
|
||||
|
||||
before = checksum_directory(target)
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
after = checksum_directory(target)
|
||||
assert cmd.returncode == 0
|
||||
|
||||
assert before == after
|
||||
|
||||
|
||||
def test_repos_sync_normal_change_to_worktree():
|
||||
with tempfile.TemporaryDirectory() as target:
|
||||
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
|
||||
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
|
||||
with tempfile.NamedTemporaryFile() as config:
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = "{target}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin"
|
||||
url = "file://{remote1}"
|
||||
type = "file"
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode == 0
|
||||
|
||||
git_dir = os.path.join(target, "test")
|
||||
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = "{target}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
worktree_setup = true
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin"
|
||||
url = "file://{remote1}"
|
||||
type = "file"
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode != 0
|
||||
assert "already exists" in cmd.stderr
|
||||
assert "not using a worktree setup" in cmd.stderr
|
||||
|
||||
|
||||
def test_repos_sync_worktree_change_to_normal():
|
||||
with tempfile.TemporaryDirectory() as target:
|
||||
with TempGitFileRemote() as (remote1, remote1_head_commit_sha):
|
||||
with TempGitFileRemote() as (remote2, remote2_head_commit_sha):
|
||||
with tempfile.NamedTemporaryFile() as config:
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = "{target}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
worktree_setup = true
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin"
|
||||
url = "file://{remote1}"
|
||||
type = "file"
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode == 0
|
||||
|
||||
git_dir = os.path.join(target, "test")
|
||||
|
||||
with open(config.name, "w") as f:
|
||||
f.write(
|
||||
f"""
|
||||
[[trees]]
|
||||
root = "{target}"
|
||||
|
||||
[[trees.repos]]
|
||||
name = "test"
|
||||
|
||||
[[trees.repos.remotes]]
|
||||
name = "origin"
|
||||
url = "file://{remote1}"
|
||||
type = "file"
|
||||
"""
|
||||
)
|
||||
|
||||
cmd = grm(["repos", "sync", "--config", config.name])
|
||||
assert cmd.returncode != 0
|
||||
assert "already exists" in cmd.stderr
|
||||
assert "using a worktree setup" in cmd.stderr
|
||||
153
e2e_tests/test_worktree_clean.py
Normal file
153
e2e_tests/test_worktree_clean.py
Normal file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from helpers import *
|
||||
|
||||
|
||||
def test_worktree_clean():
|
||||
with TempGitRepositoryWorktree() as base_dir:
|
||||
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
assert "test" in os.listdir(base_dir)
|
||||
|
||||
cmd = grm(["wt", "clean"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
assert "test" not in os.listdir(base_dir)
|
||||
|
||||
|
||||
def test_worktree_clean_refusal_no_tracking_branch():
|
||||
with TempGitRepositoryWorktree() as base_dir:
|
||||
before = checksum_directory(base_dir)
|
||||
|
||||
cmd = grm(["wt", "add", "test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
|
||||
before = checksum_directory(f"{base_dir}/test")
|
||||
cmd = grm(["wt", "clean"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
assert "test" in os.listdir(base_dir)
|
||||
|
||||
after = checksum_directory(f"{base_dir}/test")
|
||||
assert before == after
|
||||
|
||||
|
||||
def test_worktree_clean_refusal_uncommited_changes_new_file():
|
||||
with TempGitRepositoryWorktree() as base_dir:
|
||||
before = checksum_directory(base_dir)
|
||||
|
||||
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
|
||||
shell(f"cd {base_dir}/test && touch changed_file")
|
||||
|
||||
before = checksum_directory(f"{base_dir}/test")
|
||||
cmd = grm(["wt", "clean"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
assert "test" in os.listdir(base_dir)
|
||||
|
||||
after = checksum_directory(f"{base_dir}/test")
|
||||
assert before == after
|
||||
|
||||
|
||||
def test_worktree_clean_refusal_uncommited_changes_changed_file():
|
||||
with TempGitRepositoryWorktree() as base_dir:
|
||||
before = checksum_directory(base_dir)
|
||||
|
||||
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
|
||||
shell(f"cd {base_dir}/test && git ls-files | shuf | head | xargs rm -rf")
|
||||
|
||||
before = checksum_directory(f"{base_dir}/test")
|
||||
cmd = grm(["wt", "clean"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
assert "test" in os.listdir(base_dir)
|
||||
|
||||
after = checksum_directory(f"{base_dir}/test")
|
||||
assert before == after
|
||||
|
||||
|
||||
def test_worktree_clean_refusal_uncommited_changes_cleand_file():
|
||||
with TempGitRepositoryWorktree() as base_dir:
|
||||
before = checksum_directory(base_dir)
|
||||
|
||||
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
|
||||
shell(
|
||||
f"cd {base_dir}/test && git ls-files | shuf | head | while read f ; do echo $RANDOM > $f ; done"
|
||||
)
|
||||
|
||||
before = checksum_directory(f"{base_dir}/test")
|
||||
cmd = grm(["wt", "clean"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
assert "test" in os.listdir(base_dir)
|
||||
|
||||
after = checksum_directory(f"{base_dir}/test")
|
||||
assert before == after
|
||||
|
||||
|
||||
def test_worktree_clean_refusal_commited_changes():
|
||||
with TempGitRepositoryWorktree() as base_dir:
|
||||
before = checksum_directory(base_dir)
|
||||
|
||||
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
|
||||
shell(
|
||||
f'cd {base_dir}/test && touch changed_file && git add changed_file && git commit -m "commitmsg"'
|
||||
)
|
||||
|
||||
before = checksum_directory(f"{base_dir}/test")
|
||||
cmd = grm(["wt", "clean"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
assert "test" in os.listdir(base_dir)
|
||||
|
||||
after = checksum_directory(f"{base_dir}/test")
|
||||
assert before == after
|
||||
|
||||
|
||||
def test_worktree_clean_refusal_tracking_branch_mismatch():
|
||||
with TempGitRepositoryWorktree() as base_dir:
|
||||
before = checksum_directory(base_dir)
|
||||
|
||||
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
|
||||
shell(
|
||||
f"cd {base_dir}/test && git push origin test && git reset --hard origin/test^"
|
||||
)
|
||||
|
||||
before = checksum_directory(f"{base_dir}/test")
|
||||
cmd = grm(["wt", "clean"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
assert "test" in os.listdir(base_dir)
|
||||
|
||||
after = checksum_directory(f"{base_dir}/test")
|
||||
assert before == after
|
||||
|
||||
|
||||
def test_worktree_clean_fail_from_subdir():
|
||||
with TempGitRepositoryWorktree() as base_dir:
|
||||
cmd = grm(["wt", "add", "test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
|
||||
cmd = grm(["wt", "clean"], cwd=f"{base_dir}/test")
|
||||
assert cmd.returncode != 0
|
||||
assert len(cmd.stdout) == 0
|
||||
assert len(cmd.stderr) != 0
|
||||
|
||||
|
||||
def test_worktree_clean_non_worktree():
|
||||
with TempGitRepository() as git_dir:
|
||||
cmd = grm(["wt", "clean"], cwd=git_dir)
|
||||
assert cmd.returncode != 0
|
||||
assert len(cmd.stdout) == 0
|
||||
assert len(cmd.stderr) != 0
|
||||
|
||||
|
||||
def test_worktree_clean_non_git():
|
||||
with NonGitDir() as base_dir:
|
||||
cmd = grm(["wt", "clean"], cwd=base_dir)
|
||||
assert cmd.returncode != 0
|
||||
assert len(cmd.stdout) == 0
|
||||
assert len(cmd.stderr) != 0
|
||||
55
e2e_tests/test_worktree_conversion.py
Normal file
55
e2e_tests/test_worktree_conversion.py
Normal file
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import tempfile
|
||||
|
||||
from helpers import *
|
||||
|
||||
|
||||
def test_convert():
|
||||
with TempGitRepository() as git_dir:
|
||||
cmd = grm(["wt", "convert"], cwd=git_dir)
|
||||
assert cmd.returncode == 0
|
||||
|
||||
files = os.listdir(git_dir)
|
||||
assert len(files) == 1
|
||||
assert files[0] == ".git-main-working-tree"
|
||||
|
||||
cmd = grm(["wt", "add", "test"], cwd=git_dir)
|
||||
assert cmd.returncode == 0
|
||||
|
||||
files = os.listdir(git_dir)
|
||||
assert len(files) == 2
|
||||
assert set(files) == {".git-main-working-tree", "test"}
|
||||
|
||||
|
||||
def test_convert_already_worktree():
|
||||
with TempGitRepositoryWorktree() as git_dir:
|
||||
before = checksum_directory(git_dir)
|
||||
|
||||
cmd = grm(["wt", "convert"], cwd=git_dir)
|
||||
assert cmd.returncode != 0
|
||||
|
||||
after = checksum_directory(git_dir)
|
||||
assert before == after
|
||||
|
||||
|
||||
def test_convert_non_git():
|
||||
with NonGitDir() as dir:
|
||||
before = checksum_directory(dir)
|
||||
|
||||
cmd = grm(["wt", "convert"], cwd=dir)
|
||||
assert cmd.returncode != 0
|
||||
|
||||
after = checksum_directory(dir)
|
||||
assert before == after
|
||||
|
||||
|
||||
def test_convert_empty():
|
||||
with EmptyDir() as dir:
|
||||
before = checksum_directory(dir)
|
||||
|
||||
cmd = grm(["wt", "convert"], cwd=dir)
|
||||
assert cmd.returncode != 0
|
||||
|
||||
after = checksum_directory(dir)
|
||||
assert before == after
|
||||
42
e2e_tests/test_worktree_status.py
Normal file
42
e2e_tests/test_worktree_status.py
Normal file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from helpers import *
|
||||
|
||||
|
||||
def test_worktree_status():
|
||||
with TempGitRepositoryWorktree() as base_dir:
|
||||
cmd = grm(["wt", "add", "test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
|
||||
cmd = grm(["wt", "status"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
assert len(cmd.stderr) == 0
|
||||
stdout = cmd.stdout.lower()
|
||||
assert "test" in stdout
|
||||
|
||||
|
||||
def test_worktree_status_fail_from_subdir():
|
||||
with TempGitRepositoryWorktree() as base_dir:
|
||||
cmd = grm(["wt", "add", "test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
|
||||
cmd = grm(["wt", "status"], cwd=f"{base_dir}/test")
|
||||
assert cmd.returncode != 0
|
||||
assert len(cmd.stdout) == 0
|
||||
assert len(cmd.stderr) != 0
|
||||
|
||||
|
||||
def test_worktree_status_non_worktree():
|
||||
with TempGitRepository() as git_dir:
|
||||
cmd = grm(["wt", "status"], cwd=git_dir)
|
||||
assert cmd.returncode != 0
|
||||
assert len(cmd.stdout) == 0
|
||||
assert len(cmd.stderr) != 0
|
||||
|
||||
|
||||
def test_worktree_status_non_git():
|
||||
with NonGitDir() as base_dir:
|
||||
cmd = grm(["wt", "status"], cwd=base_dir)
|
||||
assert cmd.returncode != 0
|
||||
assert len(cmd.stdout) == 0
|
||||
assert len(cmd.stderr) != 0
|
||||
210
e2e_tests/test_worktrees.py
Normal file
210
e2e_tests/test_worktrees.py
Normal file
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from helpers import *
|
||||
|
||||
import git
|
||||
|
||||
|
||||
def test_worktree_add_simple():
|
||||
with TempGitRepositoryWorktree() as base_dir:
|
||||
before = checksum_directory(base_dir)
|
||||
|
||||
cmd = grm(["wt", "add", "test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
|
||||
files = os.listdir(base_dir)
|
||||
assert len(files) == 2
|
||||
assert set(files) == {".git-main-working-tree", "test"}
|
||||
|
||||
repo = git.Repo(os.path.join(base_dir, "test"))
|
||||
assert not repo.bare
|
||||
assert not repo.is_dirty()
|
||||
assert str(repo.active_branch) == "test"
|
||||
assert repo.active_branch.tracking_branch() is None
|
||||
|
||||
|
||||
def test_worktree_add_with_tracking():
|
||||
with TempGitRepositoryWorktree() as base_dir:
|
||||
before = checksum_directory(base_dir)
|
||||
|
||||
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
|
||||
print(cmd.stderr)
|
||||
assert cmd.returncode == 0
|
||||
|
||||
files = os.listdir(base_dir)
|
||||
assert len(files) == 2
|
||||
assert set(files) == {".git-main-working-tree", "test"}
|
||||
|
||||
repo = git.Repo(os.path.join(base_dir, "test"))
|
||||
assert not repo.bare
|
||||
assert not repo.is_dirty()
|
||||
assert str(repo.active_branch) == "test"
|
||||
assert str(repo.active_branch.tracking_branch()) == "origin/test"
|
||||
|
||||
|
||||
def test_worktree_delete():
|
||||
with TempGitRepositoryWorktree() as base_dir:
|
||||
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
assert "test" in os.listdir(base_dir)
|
||||
|
||||
cmd = grm(["wt", "delete", "test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
assert "test" not in os.listdir(base_dir)
|
||||
|
||||
cmd = grm(["wt", "add", "check"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
repo = git.Repo(os.path.join(base_dir, ".git-main-working-tree"))
|
||||
print(repo.branches)
|
||||
assert "test" not in [str(b) for b in repo.branches]
|
||||
|
||||
|
||||
def test_worktree_delete_refusal_no_tracking_branch():
|
||||
with TempGitRepositoryWorktree() as base_dir:
|
||||
before = checksum_directory(base_dir)
|
||||
|
||||
cmd = grm(["wt", "add", "test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
|
||||
before = checksum_directory(f"{base_dir}/test")
|
||||
cmd = grm(["wt", "delete", "test"], cwd=base_dir)
|
||||
assert cmd.returncode != 0
|
||||
stderr = cmd.stderr.lower()
|
||||
assert "refuse" in stderr or "refusing" in stderr
|
||||
assert "test" in os.listdir(base_dir)
|
||||
|
||||
after = checksum_directory(f"{base_dir}/test")
|
||||
assert before == after
|
||||
|
||||
|
||||
def test_worktree_delete_refusal_uncommited_changes_new_file():
|
||||
with TempGitRepositoryWorktree() as base_dir:
|
||||
before = checksum_directory(base_dir)
|
||||
|
||||
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
|
||||
shell(f"cd {base_dir}/test && touch changed_file")
|
||||
|
||||
before = checksum_directory(f"{base_dir}/test")
|
||||
cmd = grm(["wt", "delete", "test"], cwd=base_dir)
|
||||
assert cmd.returncode != 0
|
||||
stderr = cmd.stderr.lower()
|
||||
assert "refuse" in stderr or "refusing" in stderr
|
||||
assert "test" in os.listdir(base_dir)
|
||||
|
||||
after = checksum_directory(f"{base_dir}/test")
|
||||
assert before == after
|
||||
|
||||
|
||||
def test_worktree_delete_refusal_uncommited_changes_changed_file():
|
||||
with TempGitRepositoryWorktree() as base_dir:
|
||||
before = checksum_directory(base_dir)
|
||||
|
||||
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
|
||||
shell(f"cd {base_dir}/test && git ls-files | shuf | head | xargs rm -rf")
|
||||
|
||||
before = checksum_directory(f"{base_dir}/test")
|
||||
cmd = grm(["wt", "delete", "test"], cwd=base_dir)
|
||||
assert cmd.returncode != 0
|
||||
stderr = cmd.stderr.lower()
|
||||
assert "refuse" in stderr or "refusing" in stderr
|
||||
assert "test" in os.listdir(base_dir)
|
||||
|
||||
after = checksum_directory(f"{base_dir}/test")
|
||||
assert before == after
|
||||
|
||||
|
||||
def test_worktree_delete_refusal_uncommited_changes_deleted_file():
|
||||
with TempGitRepositoryWorktree() as base_dir:
|
||||
before = checksum_directory(base_dir)
|
||||
|
||||
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
|
||||
shell(
|
||||
f"cd {base_dir}/test && git ls-files | shuf | head | while read f ; do echo $RANDOM > $f ; done"
|
||||
)
|
||||
|
||||
before = checksum_directory(f"{base_dir}/test")
|
||||
cmd = grm(["wt", "delete", "test"], cwd=base_dir)
|
||||
assert cmd.returncode != 0
|
||||
stderr = cmd.stderr.lower()
|
||||
assert "refuse" in stderr or "refusing" in stderr
|
||||
assert "test" in os.listdir(base_dir)
|
||||
|
||||
after = checksum_directory(f"{base_dir}/test")
|
||||
assert before == after
|
||||
|
||||
|
||||
def test_worktree_delete_refusal_commited_changes():
|
||||
with TempGitRepositoryWorktree() as base_dir:
|
||||
before = checksum_directory(base_dir)
|
||||
|
||||
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
|
||||
shell(
|
||||
f'cd {base_dir}/test && touch changed_file && git add changed_file && git commit -m "commitmsg"'
|
||||
)
|
||||
|
||||
before = checksum_directory(f"{base_dir}/test")
|
||||
cmd = grm(["wt", "delete", "test"], cwd=base_dir)
|
||||
assert cmd.returncode != 0
|
||||
stderr = cmd.stderr.lower()
|
||||
assert "refuse" in stderr or "refusing" in stderr
|
||||
assert "test" in os.listdir(base_dir)
|
||||
|
||||
after = checksum_directory(f"{base_dir}/test")
|
||||
assert before == after
|
||||
|
||||
|
||||
def test_worktree_delete_refusal_tracking_branch_mismatch():
|
||||
with TempGitRepositoryWorktree() as base_dir:
|
||||
before = checksum_directory(base_dir)
|
||||
|
||||
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
|
||||
shell(
|
||||
f"cd {base_dir}/test && git push origin test && git reset --hard origin/test^"
|
||||
)
|
||||
|
||||
before = checksum_directory(f"{base_dir}/test")
|
||||
cmd = grm(["wt", "delete", "test"], cwd=base_dir)
|
||||
assert cmd.returncode != 0
|
||||
stderr = cmd.stderr.lower()
|
||||
assert "refuse" in stderr or "refusing" in stderr
|
||||
assert "test" in os.listdir(base_dir)
|
||||
|
||||
after = checksum_directory(f"{base_dir}/test")
|
||||
assert before == after
|
||||
|
||||
|
||||
def test_worktree_delete_force_refusal():
|
||||
with TempGitRepositoryWorktree() as base_dir:
|
||||
before = checksum_directory(base_dir)
|
||||
|
||||
cmd = grm(["wt", "add", "test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
|
||||
cmd = grm(["wt", "delete", "test", "--force"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
assert "test" not in os.listdir(base_dir)
|
||||
|
||||
|
||||
def test_worktree_add_delete_add():
|
||||
with TempGitRepositoryWorktree() as base_dir:
|
||||
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
assert "test" in os.listdir(base_dir)
|
||||
|
||||
cmd = grm(["wt", "delete", "test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
assert "test" not in os.listdir(base_dir)
|
||||
|
||||
cmd = grm(["wt", "add", "test", "--track", "origin/test"], cwd=base_dir)
|
||||
assert cmd.returncode == 0
|
||||
assert "test" in os.listdir(base_dir)
|
||||
75
src/cmd.rs
75
src/cmd.rs
@@ -19,6 +19,20 @@ pub struct Opts {
|
||||
|
||||
#[derive(Parser)]
|
||||
pub enum SubCommand {
|
||||
#[clap(about = "Manage repositories")]
|
||||
Repos(Repos),
|
||||
#[clap(visible_alias = "wt", about = "Manage worktrees")]
|
||||
Worktree(Worktree),
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct Repos {
|
||||
#[clap(subcommand, name = "action")]
|
||||
pub action: ReposAction,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub enum ReposAction {
|
||||
#[clap(
|
||||
visible_alias = "run",
|
||||
about = "Synchronize the repositories to the configured values"
|
||||
@@ -45,11 +59,7 @@ pub struct Sync {
|
||||
#[derive(Parser)]
|
||||
#[clap()]
|
||||
pub struct OptionalConfig {
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
about = "Path to the configuration file"
|
||||
)]
|
||||
#[clap(short, long, about = "Path to the configuration file")]
|
||||
pub config: Option<String>,
|
||||
}
|
||||
|
||||
@@ -59,6 +69,61 @@ pub struct Find {
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct Worktree {
|
||||
#[clap(subcommand, name = "action")]
|
||||
pub action: WorktreeAction,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub enum WorktreeAction {
|
||||
#[clap(about = "Add a new worktree")]
|
||||
Add(WorktreeAddArgs),
|
||||
#[clap(about = "Add an existing worktree")]
|
||||
Delete(WorktreeDeleteArgs),
|
||||
#[clap(about = "Show state of existing worktrees")]
|
||||
Status(WorktreeStatusArgs),
|
||||
#[clap(about = "Convert a normal repository to a worktree setup")]
|
||||
Convert(WorktreeConvertArgs),
|
||||
#[clap(about = "Clean all worktrees that do not contain uncommited/unpushed changes")]
|
||||
Clean(WorktreeCleanArgs),
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct WorktreeAddArgs {
|
||||
#[clap(about = "Name of the worktree")]
|
||||
pub name: String,
|
||||
|
||||
#[clap(
|
||||
short = 'n',
|
||||
long = "branch-namespace",
|
||||
about = "Namespace of the branch"
|
||||
)]
|
||||
pub branch_namespace: Option<String>,
|
||||
#[clap(short = 't', long = "track", about = "Remote branch to track")]
|
||||
pub track: Option<String>,
|
||||
}
|
||||
#[derive(Parser)]
|
||||
pub struct WorktreeDeleteArgs {
|
||||
#[clap(about = "Name of the worktree")]
|
||||
pub name: String,
|
||||
|
||||
#[clap(
|
||||
long = "force",
|
||||
about = "Force deletion, even when there are uncommitted/unpushed changes"
|
||||
)]
|
||||
pub force: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct WorktreeStatusArgs {}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct WorktreeConvertArgs {}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct WorktreeCleanArgs {}
|
||||
|
||||
pub fn parse() -> Opts {
|
||||
Opts::parse()
|
||||
}
|
||||
|
||||
@@ -21,8 +21,12 @@ pub fn read_config(path: &str) -> Result<Config, String> {
|
||||
Err(e) => {
|
||||
return Err(format!(
|
||||
"Error reading configuration file \"{}\": {}",
|
||||
path, e
|
||||
))
|
||||
path,
|
||||
match e.kind() {
|
||||
std::io::ErrorKind::NotFound => String::from("not found"),
|
||||
_ => e.to_string(),
|
||||
}
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
1032
src/lib.rs
1032
src/lib.rs
File diff suppressed because it is too large
Load Diff
257
src/repo.rs
257
src/repo.rs
@@ -10,6 +10,7 @@ use crate::output::*;
|
||||
pub enum RemoteType {
|
||||
Ssh,
|
||||
Https,
|
||||
File,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@@ -46,10 +47,18 @@ pub struct Remote {
|
||||
pub remote_type: RemoteType,
|
||||
}
|
||||
|
||||
fn worktree_setup_default() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Repo {
|
||||
pub name: String,
|
||||
|
||||
#[serde(default = "worktree_setup_default")]
|
||||
pub worktree_setup: bool,
|
||||
|
||||
pub remotes: Option<Vec<Remote>>,
|
||||
}
|
||||
|
||||
@@ -82,11 +91,14 @@ pub struct RepoStatus {
|
||||
|
||||
pub head: Option<String>,
|
||||
|
||||
pub changes: Option<RepoChanges>,
|
||||
// None(_) => Could not get changes (e.g. because it's a worktree setup)
|
||||
// Some(None) => No changes
|
||||
// Some(Some(_)) => Changes
|
||||
pub changes: Option<Option<RepoChanges>>,
|
||||
|
||||
pub worktrees: usize,
|
||||
|
||||
pub submodules: Vec<(String, SubmoduleStatus)>,
|
||||
pub submodules: Option<Vec<(String, SubmoduleStatus)>>,
|
||||
|
||||
pub branches: Vec<(String, Option<(String, RemoteTrackingStatus)>)>,
|
||||
}
|
||||
@@ -116,6 +128,14 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_file_remote() {
|
||||
assert_eq!(
|
||||
detect_remote_type("file:///somedir"),
|
||||
Some(RemoteType::File)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_invalid_remotes() {
|
||||
assert_eq!(detect_remote_type("https//example.com"), None);
|
||||
@@ -136,12 +156,6 @@ mod tests {
|
||||
fn check_unsupported_protocol_git() {
|
||||
detect_remote_type("git://example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn check_unsupported_protocol_file() {
|
||||
detect_remote_type("file:///");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn detect_remote_type(remote_url: &str) -> Option<RemoteType> {
|
||||
@@ -155,20 +169,28 @@ pub fn detect_remote_type(remote_url: &str) -> Option<RemoteType> {
|
||||
if remote_url.starts_with("https://") {
|
||||
return Some(RemoteType::Https);
|
||||
}
|
||||
if remote_url.starts_with("file://") {
|
||||
return Some(RemoteType::File);
|
||||
}
|
||||
if remote_url.starts_with("http://") {
|
||||
unimplemented!("Remotes using HTTP protocol are not supported");
|
||||
}
|
||||
if remote_url.starts_with("git://") {
|
||||
unimplemented!("Remotes using git protocol are not supported");
|
||||
}
|
||||
if remote_url.starts_with("file://") || remote_url.starts_with('/') {
|
||||
unimplemented!("Remotes using local protocol are not supported");
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn open_repo(path: &Path) -> Result<Repository, RepoError> {
|
||||
match Repository::open(path) {
|
||||
pub fn open_repo(path: &Path, is_worktree: bool) -> Result<Repository, RepoError> {
|
||||
let open_func = match is_worktree {
|
||||
true => Repository::open_bare,
|
||||
false => Repository::open,
|
||||
};
|
||||
let path = match is_worktree {
|
||||
true => path.join(super::GIT_MAIN_WORKTREE_DIRECTORY),
|
||||
false => path.to_path_buf(),
|
||||
};
|
||||
match open_func(path) {
|
||||
Ok(r) => Ok(r),
|
||||
Err(e) => match e.code() {
|
||||
git2::ErrorCode::NotFound => Err(RepoError::new(RepoErrorKind::NotFound)),
|
||||
@@ -179,24 +201,67 @@ pub fn open_repo(path: &Path) -> Result<Repository, RepoError> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_repo(path: &Path) -> Result<Repository, Box<dyn std::error::Error>> {
|
||||
match Repository::init(path) {
|
||||
Ok(r) => Ok(r),
|
||||
Err(e) => Err(Box::new(e)),
|
||||
pub fn init_repo(path: &Path, is_worktree: bool) -> Result<Repository, Box<dyn std::error::Error>> {
|
||||
let repo = match is_worktree {
|
||||
false => Repository::init(path)?,
|
||||
true => Repository::init_bare(path.join(super::GIT_MAIN_WORKTREE_DIRECTORY))?,
|
||||
};
|
||||
|
||||
if is_worktree {
|
||||
repo_set_config_push(&repo, "upstream")?;
|
||||
}
|
||||
|
||||
Ok(repo)
|
||||
}
|
||||
|
||||
pub fn clone_repo(remote: &Remote, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn get_repo_config(repo: &git2::Repository) -> Result<git2::Config, String> {
|
||||
repo.config()
|
||||
.map_err(|error| format!("Failed getting repository configuration: {}", error))
|
||||
}
|
||||
|
||||
pub fn repo_make_bare(repo: &git2::Repository, value: bool) -> Result<(), String> {
|
||||
let mut config = get_repo_config(repo)?;
|
||||
|
||||
config
|
||||
.set_bool(super::GIT_CONFIG_BARE_KEY, value)
|
||||
.map_err(|error| format!("Could not set {}: {}", super::GIT_CONFIG_BARE_KEY, error))
|
||||
}
|
||||
|
||||
pub fn repo_set_config_push(repo: &git2::Repository, value: &str) -> Result<(), String> {
|
||||
let mut config = get_repo_config(repo)?;
|
||||
|
||||
config
|
||||
.set_str(super::GIT_CONFIG_PUSH_DEFAULT, value)
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"Could not set {}: {}",
|
||||
super::GIT_CONFIG_PUSH_DEFAULT,
|
||||
error
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clone_repo(
|
||||
remote: &Remote,
|
||||
path: &Path,
|
||||
is_worktree: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let clone_target = match is_worktree {
|
||||
false => path.to_path_buf(),
|
||||
true => path.join(super::GIT_MAIN_WORKTREE_DIRECTORY),
|
||||
};
|
||||
|
||||
print_action(&format!(
|
||||
"Cloning into \"{}\" from \"{}\"",
|
||||
&path.display(),
|
||||
&clone_target.display(),
|
||||
&remote.url
|
||||
));
|
||||
match remote.remote_type {
|
||||
RemoteType::Https => match Repository::clone(&remote.url, &path) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(Box::new(e)),
|
||||
},
|
||||
RemoteType::Https | RemoteType::File => {
|
||||
let mut builder = git2::build::RepoBuilder::new();
|
||||
builder.bare(is_worktree);
|
||||
builder.clone(&remote.url, &clone_target)?;
|
||||
}
|
||||
RemoteType::Ssh => {
|
||||
let mut callbacks = RemoteCallbacks::new();
|
||||
callbacks.credentials(|_url, username_from_url, _allowed_types| {
|
||||
@@ -207,17 +272,22 @@ pub fn clone_repo(remote: &Remote, path: &Path) -> Result<(), Box<dyn std::error
|
||||
fo.remote_callbacks(callbacks);
|
||||
|
||||
let mut builder = git2::build::RepoBuilder::new();
|
||||
builder.bare(is_worktree);
|
||||
builder.fetch_options(fo);
|
||||
|
||||
match builder.clone(&remote.url, path) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(Box::new(e)),
|
||||
}
|
||||
builder.clone(&remote.url, &clone_target)?;
|
||||
}
|
||||
}
|
||||
|
||||
if is_worktree {
|
||||
let repo = open_repo(&clone_target, false)?;
|
||||
repo_set_config_push(&repo, "upstream")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_repo_status(repo: &git2::Repository) -> RepoStatus {
|
||||
pub fn get_repo_status(repo: &git2::Repository, is_worktree: bool) -> RepoStatus {
|
||||
let operation = match repo.state() {
|
||||
git2::RepositoryState::Clean => None,
|
||||
state => Some(state),
|
||||
@@ -232,73 +302,100 @@ pub fn get_repo_status(repo: &git2::Repository) -> RepoStatus {
|
||||
.map(|repo_name| repo_name.unwrap().to_string())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let head = match empty {
|
||||
let head = match is_worktree {
|
||||
true => None,
|
||||
false => Some(repo.head().unwrap().shorthand().unwrap().to_string()),
|
||||
false => match empty {
|
||||
true => None,
|
||||
false => Some(repo.head().unwrap().shorthand().unwrap().to_string()),
|
||||
},
|
||||
};
|
||||
|
||||
let statuses = repo.statuses(None).unwrap();
|
||||
|
||||
let changes = match statuses.is_empty() {
|
||||
let changes = match is_worktree {
|
||||
true => None,
|
||||
false => {
|
||||
let mut files_new = 0;
|
||||
let mut files_modified = 0;
|
||||
let mut files_deleted = 0;
|
||||
for status in statuses.iter() {
|
||||
let status_bits = status.status();
|
||||
if status_bits.intersects(
|
||||
git2::Status::INDEX_MODIFIED
|
||||
| git2::Status::INDEX_RENAMED
|
||||
| git2::Status::INDEX_TYPECHANGE
|
||||
| git2::Status::WT_MODIFIED
|
||||
| git2::Status::WT_RENAMED
|
||||
| git2::Status::WT_TYPECHANGE,
|
||||
) {
|
||||
files_modified += 1;
|
||||
} else if status_bits.intersects(git2::Status::INDEX_NEW | git2::Status::WT_NEW) {
|
||||
files_new += 1;
|
||||
} else if status_bits
|
||||
.intersects(git2::Status::INDEX_DELETED | git2::Status::WT_DELETED)
|
||||
{
|
||||
files_deleted += 1;
|
||||
let statuses = repo
|
||||
.statuses(Some(
|
||||
git2::StatusOptions::new()
|
||||
.include_ignored(false)
|
||||
.include_untracked(true),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
match statuses.is_empty() {
|
||||
true => Some(None),
|
||||
false => {
|
||||
let mut files_new = 0;
|
||||
let mut files_modified = 0;
|
||||
let mut files_deleted = 0;
|
||||
for status in statuses.iter() {
|
||||
let status_bits = status.status();
|
||||
if status_bits.intersects(
|
||||
git2::Status::INDEX_MODIFIED
|
||||
| git2::Status::INDEX_RENAMED
|
||||
| git2::Status::INDEX_TYPECHANGE
|
||||
| git2::Status::WT_MODIFIED
|
||||
| git2::Status::WT_RENAMED
|
||||
| git2::Status::WT_TYPECHANGE,
|
||||
) {
|
||||
files_modified += 1;
|
||||
} else if status_bits
|
||||
.intersects(git2::Status::INDEX_NEW | git2::Status::WT_NEW)
|
||||
{
|
||||
files_new += 1;
|
||||
} else if status_bits
|
||||
.intersects(git2::Status::INDEX_DELETED | git2::Status::WT_DELETED)
|
||||
{
|
||||
files_deleted += 1;
|
||||
}
|
||||
}
|
||||
if (files_new, files_modified, files_deleted) == (0, 0, 0) {
|
||||
panic!(
|
||||
"is_empty() returned true, but no file changes were detected. This is a bug!"
|
||||
);
|
||||
}
|
||||
Some(Some(RepoChanges {
|
||||
files_new,
|
||||
files_modified,
|
||||
files_deleted,
|
||||
}))
|
||||
}
|
||||
}
|
||||
Some(RepoChanges {
|
||||
files_new,
|
||||
files_modified,
|
||||
files_deleted,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let worktrees = repo.worktrees().unwrap().len();
|
||||
|
||||
let mut submodules = Vec::new();
|
||||
for submodule in repo.submodules().unwrap() {
|
||||
let submodule_name = submodule.name().unwrap().to_string();
|
||||
let submodules = match is_worktree {
|
||||
true => None,
|
||||
false => {
|
||||
let mut submodules = Vec::new();
|
||||
for submodule in repo.submodules().unwrap() {
|
||||
let submodule_name = submodule.name().unwrap().to_string();
|
||||
|
||||
let submodule_status;
|
||||
let status = repo
|
||||
.submodule_status(submodule.name().unwrap(), git2::SubmoduleIgnore::None)
|
||||
.unwrap();
|
||||
let submodule_status;
|
||||
let status = repo
|
||||
.submodule_status(submodule.name().unwrap(), git2::SubmoduleIgnore::None)
|
||||
.unwrap();
|
||||
|
||||
if status.intersects(
|
||||
git2::SubmoduleStatus::WD_INDEX_MODIFIED
|
||||
| git2::SubmoduleStatus::WD_WD_MODIFIED
|
||||
| git2::SubmoduleStatus::WD_UNTRACKED,
|
||||
) {
|
||||
submodule_status = SubmoduleStatus::Changed;
|
||||
} else if status.is_wd_uninitialized() {
|
||||
submodule_status = SubmoduleStatus::Uninitialized;
|
||||
} else if status.is_wd_modified() {
|
||||
submodule_status = SubmoduleStatus::OutOfDate;
|
||||
} else {
|
||||
submodule_status = SubmoduleStatus::Clean;
|
||||
if status.intersects(
|
||||
git2::SubmoduleStatus::WD_INDEX_MODIFIED
|
||||
| git2::SubmoduleStatus::WD_WD_MODIFIED
|
||||
| git2::SubmoduleStatus::WD_UNTRACKED,
|
||||
) {
|
||||
submodule_status = SubmoduleStatus::Changed;
|
||||
} else if status.is_wd_uninitialized() {
|
||||
submodule_status = SubmoduleStatus::Uninitialized;
|
||||
} else if status.is_wd_modified() {
|
||||
submodule_status = SubmoduleStatus::OutOfDate;
|
||||
} else {
|
||||
submodule_status = SubmoduleStatus::Clean;
|
||||
}
|
||||
|
||||
submodules.push((submodule_name, submodule_status));
|
||||
}
|
||||
Some(submodules)
|
||||
}
|
||||
|
||||
submodules.push((submodule_name, submodule_status));
|
||||
}
|
||||
};
|
||||
|
||||
let mut branches = Vec::new();
|
||||
for (local_branch, _) in repo
|
||||
|
||||
11
tests/helpers.rs
Normal file
11
tests/helpers.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use tempdir::TempDir;
|
||||
|
||||
pub fn init_tmpdir() -> TempDir {
|
||||
let tmp_dir = TempDir::new("grm-test").unwrap();
|
||||
println!("Temporary directory: {}", tmp_dir.path().display());
|
||||
tmp_dir
|
||||
}
|
||||
|
||||
pub fn cleanup_tmpdir(tempdir: TempDir) {
|
||||
tempdir.close().unwrap();
|
||||
}
|
||||
43
tests/repo.rs
Normal file
43
tests/repo.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use grm::repo::*;
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::*;
|
||||
|
||||
#[test]
|
||||
fn open_empty_repo() {
|
||||
let tmpdir = init_tmpdir();
|
||||
assert!(matches!(
|
||||
open_repo(tmpdir.path(), true),
|
||||
Err(RepoError {
|
||||
kind: RepoErrorKind::NotFound
|
||||
})
|
||||
));
|
||||
assert!(matches!(
|
||||
open_repo(tmpdir.path(), false),
|
||||
Err(RepoError {
|
||||
kind: RepoErrorKind::NotFound
|
||||
})
|
||||
));
|
||||
cleanup_tmpdir(tmpdir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_repo() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let tmpdir = init_tmpdir();
|
||||
let repo = init_repo(tmpdir.path(), false)?;
|
||||
assert!(!repo.is_bare());
|
||||
assert!(repo.is_empty()?);
|
||||
cleanup_tmpdir(tmpdir);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_repo_with_worktree() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let tmpdir = init_tmpdir();
|
||||
let repo = init_repo(tmpdir.path(), true)?;
|
||||
assert!(repo.is_bare());
|
||||
assert!(repo.is_empty()?);
|
||||
cleanup_tmpdir(tmpdir);
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user