Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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
|
||||||
79
Cargo.lock
generated
79
Cargo.lock
generated
@@ -169,6 +169,12 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fuchsia-cprng"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
@@ -191,14 +197,15 @@ dependencies = [
|
|||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"shellexpand",
|
"shellexpand",
|
||||||
|
"tempdir",
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "git2"
|
name = "git2"
|
||||||
version = "0.13.23"
|
version = "0.13.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a8057932925d3a9d9e4434ea016570d37420ddb1ceed45a174d577f24ed6700"
|
checksum = "845e007a28f1fcac035715988a234e8ec5458fd825b20a20c7dec74237ef341f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -280,15 +287,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.107"
|
version = "0.2.108"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fbe5e23404da5b4f555ef85ebed98fb4083e55a00c317800bc2a50ede9f3d219"
|
checksum = "8521a1b57e76b1ec69af7599e75e38e7b7fad6610f037db8c79b127201b5d119"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libgit2-sys"
|
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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ddbd6021eef06fb289a8f54b3c2acfdd85ff2a585dfbb24b8576325373d2152c"
|
checksum = "8f68169ef08d6519b2fe133ecc637408d933c0174b23b80bb2f79828966fbaab"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -399,9 +406,9 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.70"
|
version = "0.9.71"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6517987b3f8226b5da3661dad65ff7f300cc59fb5ea8333ca191fc65fde3edf"
|
checksum = "7df13d165e607909b363a4757a6f133f8a818a74e9d3a98d09c6128e15fa4c73"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"cc",
|
"cc",
|
||||||
@@ -498,6 +505,43 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"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]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.2.10"
|
version = "0.2.10"
|
||||||
@@ -534,6 +578,15 @@ version = "0.6.25"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
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]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -640,6 +693,16 @@ dependencies = [
|
|||||||
"unicode-xid",
|
"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]]
|
[[package]]
|
||||||
name = "termcolor"
|
name = "termcolor"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
|
|||||||
17
Cargo.toml
17
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "git-repo-manager"
|
name = "git-repo-manager"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = [
|
authors = [
|
||||||
"Hannes Körber <hannes@hkoerber.de>",
|
"Hannes Körber <hannes@hkoerber.de>",
|
||||||
@@ -37,17 +37,17 @@ path = "src/main.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
[dependencies.toml]
|
[dependencies.toml]
|
||||||
version = "0.5"
|
version = "0.5.8"
|
||||||
|
|
||||||
[dependencies.serde]
|
[dependencies.serde]
|
||||||
version = "1.0"
|
version = "1.0.130"
|
||||||
features = ["derive"]
|
features = ["derive"]
|
||||||
|
|
||||||
[dependencies.git2]
|
[dependencies.git2]
|
||||||
version = "0.13"
|
version = "0.13.24"
|
||||||
|
|
||||||
[dependencies.shellexpand]
|
[dependencies.shellexpand]
|
||||||
version = "2.1"
|
version = "2.1.0"
|
||||||
|
|
||||||
[dependencies.clap]
|
[dependencies.clap]
|
||||||
version = "3.0.0-beta.5"
|
version = "3.0.0-beta.5"
|
||||||
@@ -56,7 +56,10 @@ version = "3.0.0-beta.5"
|
|||||||
version = "0.15.0"
|
version = "0.15.0"
|
||||||
|
|
||||||
[dependencies.regex]
|
[dependencies.regex]
|
||||||
version = "1.5"
|
version = "1.5.4"
|
||||||
|
|
||||||
[dependencies.comfy-table]
|
[dependencies.comfy-table]
|
||||||
version = "5.0"
|
version = "5.0.0"
|
||||||
|
|
||||||
|
[dev-dependencies.tempdir]
|
||||||
|
version = "0.3.7"
|
||||||
|
|||||||
17
Justfile
17
Justfile
@@ -1,4 +1,6 @@
|
|||||||
lint:
|
check:
|
||||||
|
cargo check
|
||||||
|
cargo fmt --check
|
||||||
cargo clippy --no-deps
|
cargo clippy --no-deps
|
||||||
|
|
||||||
lint-fix:
|
lint-fix:
|
||||||
@@ -6,3 +8,16 @@ lint-fix:
|
|||||||
|
|
||||||
release:
|
release:
|
||||||
cargo build --release
|
cargo build --release
|
||||||
|
|
||||||
|
install:
|
||||||
|
cargo install --path .
|
||||||
|
|
||||||
|
test:
|
||||||
|
cargo test --lib --bins
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
29
README.md
29
README.md
@@ -79,6 +79,35 @@ $ grm status
|
|||||||
+----------+------------+----------------------------------+--------+---------+
|
+----------+------------+----------------------------------+--------+---------+
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Manage worktrees for projects
|
||||||
|
|
||||||
|
Optionally, GRM can also set up a repository to support multiple worktrees. See
|
||||||
|
[the git documentation](https://git-scm.com/docs/git-worktree) for details about
|
||||||
|
worktrees. Long story short: Worktrees allow you to have multiple independent
|
||||||
|
checkouts of the same repository in different directories, backed by a single
|
||||||
|
git repository.
|
||||||
|
|
||||||
|
To use this, specify `worktree_setup = true` for a repo in your configuration.
|
||||||
|
After the sync, you will see that the target directory is empty. Actually, the
|
||||||
|
repository was bare-cloned into a hidden directory: `.git-main-working-tree`.
|
||||||
|
Don't touch it! GRM provides a command to manage working trees.
|
||||||
|
|
||||||
|
Use `grm worktree add <name>` to create a new checkout of a new branch into
|
||||||
|
a subdirectory. An example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ grm worktree add mybranch
|
||||||
|
$ cd ./mybranch
|
||||||
|
$ git status
|
||||||
|
On branch mybranch
|
||||||
|
|
||||||
|
nothing to commit, working tree clean
|
||||||
|
```
|
||||||
|
|
||||||
|
If you're done with your worktree, use `grm worktree delete <name>` to remove it.
|
||||||
|
GRM will refuse to delete worktrees that contain uncommitted or unpushed changes,
|
||||||
|
otherwise you might lose work.
|
||||||
|
|
||||||
# Why?
|
# Why?
|
||||||
|
|
||||||
I have a **lot** of repositories on my machines. My own stuff, forks, quick
|
I have a **lot** of repositories on my machines. My own stuff, forks, quick
|
||||||
|
|||||||
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 │
|
||||||
|
╰──────────┴──────────┴────────┴──────────┴───────┴─────────╯
|
||||||
|
```
|
||||||
|
|
||||||
207
docs/src/worktrees.md
Normal file
207
docs/src/worktrees.md
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
70
src/cmd.rs
70
src/cmd.rs
@@ -19,6 +19,20 @@ pub struct Opts {
|
|||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub enum SubCommand {
|
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(
|
#[clap(
|
||||||
visible_alias = "run",
|
visible_alias = "run",
|
||||||
about = "Synchronize the repositories to the configured values"
|
about = "Synchronize the repositories to the configured values"
|
||||||
@@ -45,11 +59,7 @@ pub struct Sync {
|
|||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[clap()]
|
#[clap()]
|
||||||
pub struct OptionalConfig {
|
pub struct OptionalConfig {
|
||||||
#[clap(
|
#[clap(short, long, about = "Path to the configuration file")]
|
||||||
short,
|
|
||||||
long,
|
|
||||||
about = "Path to the configuration file"
|
|
||||||
)]
|
|
||||||
pub config: Option<String>,
|
pub config: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +69,56 @@ pub struct Find {
|
|||||||
pub path: String,
|
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 = "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 WorktreeCleanArgs {}
|
||||||
|
|
||||||
pub fn parse() -> Opts {
|
pub fn parse() -> Opts {
|
||||||
Opts::parse()
|
Opts::parse()
|
||||||
}
|
}
|
||||||
|
|||||||
703
src/lib.rs
703
src/lib.rs
@@ -5,18 +5,61 @@ use std::process;
|
|||||||
mod cmd;
|
mod cmd;
|
||||||
mod config;
|
mod config;
|
||||||
mod output;
|
mod output;
|
||||||
mod repo;
|
pub mod repo;
|
||||||
|
|
||||||
use config::{Config, Tree};
|
use config::{Config, Tree};
|
||||||
use output::*;
|
use output::*;
|
||||||
|
|
||||||
use comfy_table::{Table, Cell};
|
use comfy_table::{Cell, Table};
|
||||||
|
|
||||||
use repo::{
|
use repo::{
|
||||||
clone_repo, detect_remote_type, get_repo_status, init_repo, open_repo, Remote, Repo,
|
clone_repo, detect_remote_type, get_repo_status, init_repo, open_repo, Remote,
|
||||||
RepoErrorKind, RemoteTrackingStatus
|
RemoteTrackingStatus, Repo, RepoErrorKind,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const GIT_MAIN_WORKTREE_DIRECTORY: &str = ".git-main-working-tree";
|
||||||
|
const BRANCH_NAMESPACE_SEPARATOR: &str = "/";
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn setup() {
|
||||||
|
std::env::set_var("HOME", "/home/test");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_expand_tilde() {
|
||||||
|
setup();
|
||||||
|
assert_eq!(
|
||||||
|
expand_path(Path::new("~/file")),
|
||||||
|
Path::new("/home/test/file")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_expand_invalid_tilde() {
|
||||||
|
setup();
|
||||||
|
assert_eq!(
|
||||||
|
expand_path(Path::new("/home/~/file")),
|
||||||
|
Path::new("/home/~/file")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_expand_home() {
|
||||||
|
setup();
|
||||||
|
assert_eq!(
|
||||||
|
expand_path(Path::new("$HOME/file")),
|
||||||
|
Path::new("/home/test/file")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
expand_path(Path::new("${HOME}/file")),
|
||||||
|
Path::new("/home/test/file")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn path_as_string(path: &Path) -> String {
|
fn path_as_string(path: &Path) -> String {
|
||||||
path.to_path_buf().into_os_string().into_string().unwrap()
|
path.to_path_buf().into_os_string().into_string().unwrap()
|
||||||
}
|
}
|
||||||
@@ -57,6 +100,16 @@ fn expand_path(path: &Path) -> PathBuf {
|
|||||||
Path::new(&expanded_path).to_path_buf()
|
Path::new(&expanded_path).to_path_buf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_default_branch(repo: &git2::Repository) -> Result<git2::Branch, String> {
|
||||||
|
match repo.find_branch("main", git2::BranchType::Local) {
|
||||||
|
Ok(branch) => Ok(branch),
|
||||||
|
Err(_) => match repo.find_branch("master", git2::BranchType::Local) {
|
||||||
|
Ok(branch) => Ok(branch),
|
||||||
|
Err(_) => Err(String::from("Could not determine default branch")),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn sync_trees(config: Config) {
|
fn sync_trees(config: Config) {
|
||||||
for tree in config.trees {
|
for tree in config.trees {
|
||||||
let repos = tree.repos.unwrap_or_default();
|
let repos = tree.repos.unwrap_or_default();
|
||||||
@@ -65,21 +118,33 @@ fn sync_trees(config: Config) {
|
|||||||
|
|
||||||
for repo in &repos {
|
for repo in &repos {
|
||||||
let repo_path = root_path.join(&repo.name);
|
let repo_path = root_path.join(&repo.name);
|
||||||
|
let actual_git_directory = get_actual_git_directory(&repo_path, repo.worktree_setup);
|
||||||
|
|
||||||
let mut repo_handle = None;
|
let mut repo_handle = None;
|
||||||
|
|
||||||
if repo_path.exists() {
|
if repo_path.exists() {
|
||||||
repo_handle = Some(open_repo(&repo_path).unwrap_or_else(|error| {
|
if repo.worktree_setup && !actual_git_directory.exists() {
|
||||||
print_repo_error(&repo.name, &format!("Opening repository failed: {}", error));
|
print_repo_error(
|
||||||
|
&repo.name,
|
||||||
|
"Repo already exists, but is not using a worktree setup",
|
||||||
|
);
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}));
|
}
|
||||||
} else {
|
repo_handle = Some(open_repo(&repo_path, repo.worktree_setup).unwrap_or_else(
|
||||||
if matches!(&repo.remotes, None) || repo.remotes.as_ref().unwrap().len().clone() == 0 {
|
|error| {
|
||||||
|
print_repo_error(
|
||||||
|
&repo.name,
|
||||||
|
&format!("Opening repository failed: {}", error),
|
||||||
|
);
|
||||||
|
process::exit(1);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
} else if matches!(&repo.remotes, None) || repo.remotes.as_ref().unwrap().is_empty() {
|
||||||
print_repo_action(
|
print_repo_action(
|
||||||
&repo.name,
|
&repo.name,
|
||||||
"Repository does not have remotes configured, initializing new",
|
"Repository does not have remotes configured, initializing new",
|
||||||
);
|
);
|
||||||
repo_handle = match init_repo(&repo_path) {
|
repo_handle = match init_repo(&repo_path, repo.worktree_setup) {
|
||||||
Ok(r) => {
|
Ok(r) => {
|
||||||
print_repo_success(&repo.name, "Repository created");
|
print_repo_success(&repo.name, "Repository created");
|
||||||
Some(r)
|
Some(r)
|
||||||
@@ -95,7 +160,7 @@ fn sync_trees(config: Config) {
|
|||||||
} else {
|
} else {
|
||||||
let first = repo.remotes.as_ref().unwrap().first().unwrap();
|
let first = repo.remotes.as_ref().unwrap().first().unwrap();
|
||||||
|
|
||||||
match clone_repo(first, &repo_path) {
|
match clone_repo(first, &repo_path, repo.worktree_setup) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
print_repo_success(&repo.name, "Repository successfully cloned");
|
print_repo_success(&repo.name, "Repository successfully cloned");
|
||||||
}
|
}
|
||||||
@@ -108,10 +173,10 @@ fn sync_trees(config: Config) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if let Some(remotes) = &repo.remotes {
|
if let Some(remotes) = &repo.remotes {
|
||||||
let repo_handle = repo_handle
|
let repo_handle = repo_handle.unwrap_or_else(|| {
|
||||||
.unwrap_or_else(|| open_repo(&repo_path).unwrap_or_else(|_| process::exit(1)));
|
open_repo(&repo_path, repo.worktree_setup).unwrap_or_else(|_| process::exit(1))
|
||||||
|
});
|
||||||
|
|
||||||
let current_remotes: Vec<String> = match repo_handle.remotes() {
|
let current_remotes: Vec<String> = match repo_handle.remotes() {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
@@ -190,7 +255,7 @@ fn sync_trees(config: Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let current_repos = find_repos_without_details(&root_path).unwrap();
|
let current_repos = find_repos_without_details(&root_path).unwrap();
|
||||||
for repo in current_repos {
|
for (repo, _) in current_repos {
|
||||||
let name = path_as_string(repo.strip_prefix(&root_path).unwrap());
|
let name = path_as_string(repo.strip_prefix(&root_path).unwrap());
|
||||||
if !repos.iter().any(|r| r.name == name) {
|
if !repos.iter().any(|r| r.name == name) {
|
||||||
print_warning(&format!("Found unmanaged repository: {}", name));
|
print_warning(&format!("Found unmanaged repository: {}", name));
|
||||||
@@ -199,12 +264,16 @@ fn sync_trees(config: Config) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_repos_without_details(path: &Path) -> Option<Vec<PathBuf>> {
|
fn find_repos_without_details(path: &Path) -> Option<Vec<(PathBuf, bool)>> {
|
||||||
let mut repos: Vec<PathBuf> = Vec::new();
|
let mut repos: Vec<(PathBuf, bool)> = Vec::new();
|
||||||
|
|
||||||
let git_dir = path.join(".git");
|
let git_dir = path.join(".git");
|
||||||
|
let git_worktree = path.join(GIT_MAIN_WORKTREE_DIRECTORY);
|
||||||
|
|
||||||
if git_dir.exists() {
|
if git_dir.exists() {
|
||||||
repos.push(path.to_path_buf());
|
repos.push((path.to_path_buf(), false));
|
||||||
|
} else if git_worktree.exists() {
|
||||||
|
repos.push((path.to_path_buf(), true));
|
||||||
} else {
|
} else {
|
||||||
match fs::read_dir(path) {
|
match fs::read_dir(path) {
|
||||||
Ok(contents) => {
|
Ok(contents) => {
|
||||||
@@ -238,14 +307,33 @@ fn find_repos_without_details(path: &Path) -> Option<Vec<PathBuf>> {
|
|||||||
Some(repos)
|
Some(repos)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_repos(root: &Path) -> Option<Vec<Repo>> {
|
fn get_actual_git_directory(path: &Path, is_worktree: bool) -> PathBuf {
|
||||||
let mut repos: Vec<Repo> = Vec::new();
|
match is_worktree {
|
||||||
|
false => path.to_path_buf(),
|
||||||
|
true => path.join(GIT_MAIN_WORKTREE_DIRECTORY),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for path in find_repos_without_details(root).unwrap() {
|
fn find_repos(root: &Path) -> Option<(Vec<Repo>, bool)> {
|
||||||
let repo = match open_repo(&path) {
|
let mut repos: Vec<Repo> = Vec::new();
|
||||||
|
let mut repo_in_root = false;
|
||||||
|
|
||||||
|
for (path, is_worktree) in find_repos_without_details(root).unwrap() {
|
||||||
|
if path == root {
|
||||||
|
repo_in_root = true;
|
||||||
|
}
|
||||||
|
let repo = match open_repo(&path, is_worktree) {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
print_error(&format!("Error opening repo {}: {}", path.display(), e));
|
print_error(&format!(
|
||||||
|
"Error opening repo {}{}: {}",
|
||||||
|
path.display(),
|
||||||
|
match is_worktree {
|
||||||
|
true => " as worktree",
|
||||||
|
false => "",
|
||||||
|
},
|
||||||
|
e
|
||||||
|
));
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -330,18 +418,28 @@ fn find_repos(root: &Path) -> Option<Vec<Repo>> {
|
|||||||
false => path_as_string(path.strip_prefix(&root).unwrap()),
|
false => path_as_string(path.strip_prefix(&root).unwrap()),
|
||||||
},
|
},
|
||||||
remotes,
|
remotes,
|
||||||
|
worktree_setup: is_worktree,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Some(repos)
|
Some((repos, repo_in_root))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_in_tree(path: &Path) -> Option<Tree> {
|
fn find_in_tree(path: &Path) -> Option<Tree> {
|
||||||
let repos: Vec<Repo> = match find_repos(path) {
|
let (repos, repo_in_root): (Vec<Repo>, bool) = match find_repos(path) {
|
||||||
Some(vec) => vec,
|
Some((vec, repo_in_root)) => (vec, repo_in_root),
|
||||||
None => Vec::new(),
|
None => (Vec::new(), false),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut root = path.to_path_buf();
|
let mut root = path.to_path_buf();
|
||||||
|
if repo_in_root {
|
||||||
|
root = match root.parent() {
|
||||||
|
Some(root) => root.to_path_buf(),
|
||||||
|
None => {
|
||||||
|
print_error("Cannot detect root directory. Are you working in /?");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
let home = env_home();
|
let home = env_home();
|
||||||
if root.starts_with(&home) {
|
if root.starts_with(&home) {
|
||||||
// The tilde is not handled differently, it's just a normal path component for `Path`.
|
// The tilde is not handled differently, it's just a normal path component for `Path`.
|
||||||
@@ -361,6 +459,7 @@ fn add_table_header(table: &mut Table) {
|
|||||||
.apply_modifier(comfy_table::modifiers::UTF8_ROUND_CORNERS)
|
.apply_modifier(comfy_table::modifiers::UTF8_ROUND_CORNERS)
|
||||||
.set_header(vec![
|
.set_header(vec![
|
||||||
Cell::new("Repo"),
|
Cell::new("Repo"),
|
||||||
|
Cell::new("Worktree"),
|
||||||
Cell::new("Status"),
|
Cell::new("Status"),
|
||||||
Cell::new("Branches"),
|
Cell::new("Branches"),
|
||||||
Cell::new("HEAD"),
|
Cell::new("HEAD"),
|
||||||
@@ -368,12 +467,35 @@ fn add_table_header(table: &mut Table) {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_repo_status(table: &mut Table, repo_name: &str, repo_handle: &git2::Repository) {
|
fn add_worktree_table_header(table: &mut Table) {
|
||||||
let repo_status = get_repo_status(repo_handle);
|
table
|
||||||
|
.load_preset(comfy_table::presets::UTF8_FULL)
|
||||||
|
.apply_modifier(comfy_table::modifiers::UTF8_ROUND_CORNERS)
|
||||||
|
.set_header(vec![
|
||||||
|
Cell::new("Worktree"),
|
||||||
|
Cell::new("Status"),
|
||||||
|
Cell::new("Branch"),
|
||||||
|
Cell::new("Remote branch"),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_repo_status(
|
||||||
|
table: &mut Table,
|
||||||
|
repo_name: &str,
|
||||||
|
repo_handle: &git2::Repository,
|
||||||
|
is_worktree: bool,
|
||||||
|
) {
|
||||||
|
let repo_status = get_repo_status(repo_handle, is_worktree);
|
||||||
|
|
||||||
table.add_row(vec![
|
table.add_row(vec![
|
||||||
repo_name,
|
repo_name,
|
||||||
|
match is_worktree {
|
||||||
|
true => "\u{2714}",
|
||||||
|
false => "",
|
||||||
|
},
|
||||||
&match repo_status.changes {
|
&match repo_status.changes {
|
||||||
|
None => String::from("-"),
|
||||||
|
Some(changes) => match changes {
|
||||||
Some(changes) => {
|
Some(changes) => {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
if changes.files_new > 0 {
|
if changes.files_new > 0 {
|
||||||
@@ -386,41 +508,126 @@ fn add_repo_status(table: &mut Table, repo_name: &str, repo_handle: &git2::Repos
|
|||||||
out.push(format!("Deleted: {}\n", changes.files_deleted))
|
out.push(format!("Deleted: {}\n", changes.files_deleted))
|
||||||
}
|
}
|
||||||
out.into_iter().collect::<String>().trim().to_string()
|
out.into_iter().collect::<String>().trim().to_string()
|
||||||
|
}
|
||||||
|
None => String::from("\u{2714}"),
|
||||||
},
|
},
|
||||||
None => String::from("No changes"),
|
|
||||||
},
|
},
|
||||||
&repo_status.branches.iter().map(|(branch_name, remote_branch)| {
|
&repo_status
|
||||||
format!("branch: {}{}\n",
|
.branches
|
||||||
|
.iter()
|
||||||
|
.map(|(branch_name, remote_branch)| {
|
||||||
|
format!(
|
||||||
|
"branch: {}{}\n",
|
||||||
&branch_name,
|
&branch_name,
|
||||||
&match remote_branch {
|
&match remote_branch {
|
||||||
None => String::from(" <!local>"),
|
None => String::from(" <!local>"),
|
||||||
Some((remote_branch_name, remote_tracking_status)) => {
|
Some((remote_branch_name, remote_tracking_status)) => {
|
||||||
format!(" <{}>{}",
|
format!(
|
||||||
|
" <{}>{}",
|
||||||
remote_branch_name,
|
remote_branch_name,
|
||||||
&match remote_tracking_status {
|
&match remote_tracking_status {
|
||||||
RemoteTrackingStatus::UpToDate => String::from(" \u{2714}"),
|
RemoteTrackingStatus::UpToDate => String::from(" \u{2714}"),
|
||||||
RemoteTrackingStatus::Ahead(d) => format!(" [+{}]", &d),
|
RemoteTrackingStatus::Ahead(d) => format!(" [+{}]", &d),
|
||||||
RemoteTrackingStatus::Behind(d) => format!(" [-{}]", &d),
|
RemoteTrackingStatus::Behind(d) => format!(" [-{}]", &d),
|
||||||
RemoteTrackingStatus::Diverged(d1, d2) => format!(" [-{}/+{}]", &d1,&d2),
|
RemoteTrackingStatus::Diverged(d1, d2) =>
|
||||||
|
format!(" [+{}/-{}]", &d1, &d2),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}).collect::<String>().trim().to_string(),
|
})
|
||||||
&match repo_status.head {
|
.collect::<String>()
|
||||||
|
.trim()
|
||||||
|
.to_string(),
|
||||||
|
&match is_worktree {
|
||||||
|
true => String::from(""),
|
||||||
|
false => match repo_status.head {
|
||||||
Some(head) => head,
|
Some(head) => head,
|
||||||
None => String::from("Empty"),
|
None => String::from("Empty"),
|
||||||
},
|
},
|
||||||
&repo_status.remotes.iter().map(|r| format!("{}\n", r)).collect::<String>().trim().to_string(),
|
},
|
||||||
|
&repo_status
|
||||||
|
.remotes
|
||||||
|
.iter()
|
||||||
|
.map(|r| format!("{}\n", r))
|
||||||
|
.collect::<String>()
|
||||||
|
.trim()
|
||||||
|
.to_string(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_single_repo_status(path: &Path) {
|
fn add_worktree_status(table: &mut Table, worktree_name: &str, repo: &git2::Repository) {
|
||||||
|
let repo_status = get_repo_status(repo, false);
|
||||||
|
|
||||||
|
let head = repo.head().unwrap();
|
||||||
|
|
||||||
|
if !head.is_branch() {
|
||||||
|
print_error("No branch checked out in worktree");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let local_branch_name = head.shorthand().unwrap();
|
||||||
|
let local_branch = repo
|
||||||
|
.find_branch(local_branch_name, git2::BranchType::Local)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let upstream_output = match local_branch.upstream() {
|
||||||
|
Ok(remote_branch) => {
|
||||||
|
let remote_branch_name = remote_branch.name().unwrap().unwrap().to_string();
|
||||||
|
|
||||||
|
let (ahead, behind) = repo
|
||||||
|
.graph_ahead_behind(
|
||||||
|
local_branch.get().peel_to_commit().unwrap().id(),
|
||||||
|
remote_branch.get().peel_to_commit().unwrap().id(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"{}{}\n",
|
||||||
|
&remote_branch_name,
|
||||||
|
&match (ahead, behind) {
|
||||||
|
(0, 0) => String::from(""),
|
||||||
|
(d, 0) => format!(" [+{}]", &d),
|
||||||
|
(0, d) => format!(" [-{}]", &d),
|
||||||
|
(d1, d2) => format!(" [+{}/-{}]", &d1, &d2),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Err(_) => String::from(""),
|
||||||
|
};
|
||||||
|
|
||||||
|
table.add_row(vec![
|
||||||
|
worktree_name,
|
||||||
|
&match repo_status.changes {
|
||||||
|
None => String::from(""),
|
||||||
|
Some(changes) => match changes {
|
||||||
|
Some(changes) => {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
if changes.files_new > 0 {
|
||||||
|
out.push(format!("New: {}\n", changes.files_new))
|
||||||
|
}
|
||||||
|
if changes.files_modified > 0 {
|
||||||
|
out.push(format!("Modified: {}\n", changes.files_modified))
|
||||||
|
}
|
||||||
|
if changes.files_deleted > 0 {
|
||||||
|
out.push(format!("Deleted: {}\n", changes.files_deleted))
|
||||||
|
}
|
||||||
|
out.into_iter().collect::<String>().trim().to_string()
|
||||||
|
}
|
||||||
|
None => String::from("\u{2714}"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
local_branch_name,
|
||||||
|
&upstream_output,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_single_repo_status(path: &Path, is_worktree: bool) {
|
||||||
let mut table = Table::new();
|
let mut table = Table::new();
|
||||||
add_table_header(&mut table);
|
add_table_header(&mut table);
|
||||||
|
|
||||||
let repo_handle = open_repo(path);
|
let repo_handle = open_repo(path, is_worktree);
|
||||||
|
|
||||||
if let Err(error) = repo_handle {
|
if let Err(error) = repo_handle {
|
||||||
if error.kind == RepoErrorKind::NotFound {
|
if error.kind == RepoErrorKind::NotFound {
|
||||||
@@ -435,17 +642,17 @@ fn show_single_repo_status(path: &Path) {
|
|||||||
None => {
|
None => {
|
||||||
print_warning("Cannot detect repo name. Are you working in /?");
|
print_warning("Cannot detect repo name. Are you working in /?");
|
||||||
String::from("unknown")
|
String::from("unknown")
|
||||||
},
|
}
|
||||||
Some(file_name) => match file_name.to_str() {
|
Some(file_name) => match file_name.to_str() {
|
||||||
None => {
|
None => {
|
||||||
print_warning("Name of current directory is not valid UTF-8");
|
print_warning("Name of current directory is not valid UTF-8");
|
||||||
String::from("invalid")
|
String::from("invalid")
|
||||||
},
|
|
||||||
Some(name) => name.to_string(),
|
|
||||||
}
|
}
|
||||||
|
Some(name) => name.to_string(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
add_repo_status(&mut table, &repo_name, &repo_handle.unwrap());
|
add_repo_status(&mut table, &repo_name, &repo_handle.unwrap(), is_worktree);
|
||||||
|
|
||||||
println!("{}", table);
|
println!("{}", table);
|
||||||
}
|
}
|
||||||
@@ -463,15 +670,21 @@ fn show_status(config: Config) {
|
|||||||
let repo_path = root_path.join(&repo.name);
|
let repo_path = root_path.join(&repo.name);
|
||||||
|
|
||||||
if !repo_path.exists() {
|
if !repo_path.exists() {
|
||||||
print_repo_error(&repo.name, &"Repository does not exist. Run sync?".to_string());
|
print_repo_error(
|
||||||
|
&repo.name,
|
||||||
|
&"Repository does not exist. Run sync?".to_string(),
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let repo_handle = open_repo(&repo_path);
|
let repo_handle = open_repo(&repo_path, repo.worktree_setup);
|
||||||
|
|
||||||
if let Err(error) = repo_handle {
|
if let Err(error) = repo_handle {
|
||||||
if error.kind == RepoErrorKind::NotFound {
|
if error.kind == RepoErrorKind::NotFound {
|
||||||
print_repo_error(&repo.name, &"No git repository found. Run sync?".to_string());
|
print_repo_error(
|
||||||
|
&repo.name,
|
||||||
|
&"No git repository found. Run sync?".to_string(),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
print_repo_error(&repo.name, &format!("Opening repository failed: {}", error));
|
print_repo_error(&repo.name, &format!("Opening repository failed: {}", error));
|
||||||
}
|
}
|
||||||
@@ -480,17 +693,112 @@ fn show_status(config: Config) {
|
|||||||
|
|
||||||
let repo_handle = repo_handle.unwrap();
|
let repo_handle = repo_handle.unwrap();
|
||||||
|
|
||||||
add_repo_status(&mut table, &repo.name, &repo_handle);
|
add_repo_status(&mut table, &repo.name, &repo_handle, repo.worktree_setup);
|
||||||
}
|
}
|
||||||
println!("{}", table);
|
println!("{}", table);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum WorktreeRemoveFailureReason {
|
||||||
|
Changes(String),
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_worktree(
|
||||||
|
name: &str,
|
||||||
|
worktree_dir: &Path,
|
||||||
|
force: bool,
|
||||||
|
main_repo: &git2::Repository,
|
||||||
|
) -> Result<(), WorktreeRemoveFailureReason> {
|
||||||
|
if !worktree_dir.exists() {
|
||||||
|
return Err(WorktreeRemoveFailureReason::Error(format!(
|
||||||
|
"{} does not exist",
|
||||||
|
name
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let worktree_repo = match open_repo(worktree_dir, false) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
return Err(WorktreeRemoveFailureReason::Error(format!(
|
||||||
|
"Error opening repo: {}",
|
||||||
|
e
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let head = worktree_repo.head().unwrap();
|
||||||
|
if !head.is_branch() {
|
||||||
|
return Err(WorktreeRemoveFailureReason::Error(String::from(
|
||||||
|
"No branch checked out in worktree",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let branch_name = head.shorthand().unwrap();
|
||||||
|
if branch_name != name
|
||||||
|
&& !branch_name.ends_with(&format!("{}{}", BRANCH_NAMESPACE_SEPARATOR, name))
|
||||||
|
{
|
||||||
|
return Err(WorktreeRemoveFailureReason::Error(format!(
|
||||||
|
"Branch {} is checked out in worktree, this does not look correct",
|
||||||
|
&branch_name
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut branch = worktree_repo
|
||||||
|
.find_branch(branch_name, git2::BranchType::Local)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if !force {
|
||||||
|
let status = get_repo_status(&worktree_repo, false);
|
||||||
|
if status.changes.unwrap().is_some() {
|
||||||
|
return Err(WorktreeRemoveFailureReason::Changes(String::from(
|
||||||
|
"Changes found in worktree",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
match branch.upstream() {
|
||||||
|
Ok(remote_branch) => {
|
||||||
|
let (ahead, behind) = worktree_repo
|
||||||
|
.graph_ahead_behind(
|
||||||
|
branch.get().peel_to_commit().unwrap().id(),
|
||||||
|
remote_branch.get().peel_to_commit().unwrap().id(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if (ahead, behind) != (0, 0) {
|
||||||
|
return Err(WorktreeRemoveFailureReason::Changes(format!(
|
||||||
|
"Branch {} is not in line with remote branch",
|
||||||
|
name
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
return Err(WorktreeRemoveFailureReason::Changes(format!(
|
||||||
|
"No remote tracking branch for branch {} found",
|
||||||
|
name
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = std::fs::remove_dir_all(&worktree_dir) {
|
||||||
|
return Err(WorktreeRemoveFailureReason::Error(format!(
|
||||||
|
"Error deleting {}: {}",
|
||||||
|
&worktree_dir.display(),
|
||||||
|
e
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
main_repo.find_worktree(name).unwrap().prune(None).unwrap();
|
||||||
|
branch.delete().unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
let opts = cmd::parse();
|
let opts = cmd::parse();
|
||||||
|
|
||||||
match opts.subcmd {
|
match opts.subcmd {
|
||||||
cmd::SubCommand::Sync(sync) => {
|
cmd::SubCommand::Repos(repos) => match repos.action {
|
||||||
|
cmd::ReposAction::Sync(sync) => {
|
||||||
let config = match config::read_config(&sync.config) {
|
let config = match config::read_config(&sync.config) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -500,8 +808,7 @@ pub fn run() {
|
|||||||
};
|
};
|
||||||
sync_trees(config);
|
sync_trees(config);
|
||||||
}
|
}
|
||||||
cmd::SubCommand::Status(args) => {
|
cmd::ReposAction::Status(args) => match &args.config {
|
||||||
match &args.config {
|
|
||||||
Some(config_path) => {
|
Some(config_path) => {
|
||||||
let config = match config::read_config(config_path) {
|
let config = match config::read_config(config_path) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
@@ -511,21 +818,21 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
show_status(config);
|
show_status(config);
|
||||||
},
|
}
|
||||||
None => {
|
None => {
|
||||||
let dir = match std::env::current_dir() {
|
let dir = match std::env::current_dir() {
|
||||||
Ok(d) => d,
|
Ok(d) => d,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
print_error(&format!("Could not open current directory: {}", e));
|
print_error(&format!("Could not open current directory: {}", e));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
show_single_repo_status(&dir);
|
let has_worktree = dir.join(GIT_MAIN_WORKTREE_DIRECTORY).exists();
|
||||||
|
show_single_repo_status(&dir, has_worktree);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
cmd::ReposAction::Find(find) => {
|
||||||
cmd::SubCommand::Find(find) => {
|
|
||||||
let path = Path::new(&find.path);
|
let path = Path::new(&find.path);
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
print_error(&format!("Path \"{}\" does not exist", path.display()));
|
print_error(&format!("Path \"{}\" does not exist", path.display()));
|
||||||
@@ -537,13 +844,291 @@ pub fn run() {
|
|||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = Config {
|
let trees = vec![find_in_tree(path).unwrap()];
|
||||||
trees: vec![find_in_tree(path).unwrap()],
|
if trees.iter().all(|t| match &t.repos {
|
||||||
};
|
None => false,
|
||||||
|
Some(r) => r.is_empty(),
|
||||||
|
}) {
|
||||||
|
print_warning("No repositories found");
|
||||||
|
} else {
|
||||||
|
let config = Config { trees };
|
||||||
|
|
||||||
let toml = toml::to_string(&config).unwrap();
|
let toml = toml::to_string(&config).unwrap();
|
||||||
|
|
||||||
print!("{}", toml);
|
print!("{}", toml);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
cmd::SubCommand::Worktree(args) => {
|
||||||
|
let dir = match std::env::current_dir() {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(e) => {
|
||||||
|
print_error(&format!("Could not open current directory: {}", e));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let repo = match open_repo(&dir, true) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
match e.kind {
|
||||||
|
RepoErrorKind::NotFound => {
|
||||||
|
print_error("Current directory does not contain a worktree setup")
|
||||||
|
}
|
||||||
|
_ => print_error(&format!("Error opening repo: {}", e)),
|
||||||
|
}
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let worktrees = repo
|
||||||
|
.worktrees()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.unwrap().to_string())
|
||||||
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
|
match args.action {
|
||||||
|
cmd::WorktreeAction::Add(action_args) => {
|
||||||
|
if worktrees.contains(&action_args.name) {
|
||||||
|
print_error("Worktree already exists");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let branch_name = match action_args.branch_namespace {
|
||||||
|
Some(prefix) => format!(
|
||||||
|
"{}{}{}",
|
||||||
|
&prefix, BRANCH_NAMESPACE_SEPARATOR, &action_args.name
|
||||||
|
),
|
||||||
|
None => action_args.name.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut remote_branch_exists = false;
|
||||||
|
|
||||||
|
let checkout_commit = match &action_args.track {
|
||||||
|
Some(upstream_branch_name) => {
|
||||||
|
match repo.find_branch(upstream_branch_name, git2::BranchType::Remote) {
|
||||||
|
Ok(branch) => {
|
||||||
|
remote_branch_exists = true;
|
||||||
|
branch.into_reference().peel_to_commit().unwrap()
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
remote_branch_exists = false;
|
||||||
|
get_default_branch(&repo)
|
||||||
|
.unwrap()
|
||||||
|
.into_reference()
|
||||||
|
.peel_to_commit()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => get_default_branch(&repo)
|
||||||
|
.unwrap()
|
||||||
|
.into_reference()
|
||||||
|
.peel_to_commit()
|
||||||
|
.unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut target_branch =
|
||||||
|
match repo.find_branch(&branch_name, git2::BranchType::Local) {
|
||||||
|
Ok(branchref) => branchref,
|
||||||
|
Err(_) => repo.branch(&branch_name, &checkout_commit, false).unwrap(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(upstream_branch_name) = action_args.track {
|
||||||
|
if remote_branch_exists {
|
||||||
|
target_branch
|
||||||
|
.set_upstream(Some(&upstream_branch_name))
|
||||||
|
.unwrap();
|
||||||
|
} else {
|
||||||
|
print_error(&format!(
|
||||||
|
"Remote branch {} not found",
|
||||||
|
&upstream_branch_name
|
||||||
|
));
|
||||||
|
let split_at = upstream_branch_name.find('/').unwrap_or(0);
|
||||||
|
if split_at == 0 || split_at >= upstream_branch_name.len() - 1 {
|
||||||
|
print_error("Tracking branch needs to match the pattern <remote>/<branch_name>");
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (remote_name, remote_branch_name) =
|
||||||
|
&upstream_branch_name.split_at(split_at);
|
||||||
|
// strip the remaining slash
|
||||||
|
let remote_branch_name = &remote_branch_name[1..];
|
||||||
|
|
||||||
|
let mut remote = match repo.find_remote(remote_name) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(_) => {
|
||||||
|
print_error(&format!("Remote {} not found", remote_name));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut callbacks = git2::RemoteCallbacks::new();
|
||||||
|
callbacks.push_update_reference(|_, status| {
|
||||||
|
if let Some(message) = status {
|
||||||
|
return Err(git2::Error::new(
|
||||||
|
git2::ErrorCode::GenericError,
|
||||||
|
git2::ErrorClass::None,
|
||||||
|
message,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
callbacks.credentials(|_url, username_from_url, _allowed_types| {
|
||||||
|
git2::Cred::ssh_key_from_agent(username_from_url.unwrap())
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut push_options = git2::PushOptions::new();
|
||||||
|
push_options.remote_callbacks(callbacks);
|
||||||
|
|
||||||
|
let push_refspec = format!(
|
||||||
|
"+{}:refs/heads/{}",
|
||||||
|
target_branch.get().name().unwrap(),
|
||||||
|
remote_branch_name
|
||||||
|
);
|
||||||
|
remote
|
||||||
|
.push(&[push_refspec], Some(&mut push_options))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
target_branch
|
||||||
|
.set_upstream(Some(&upstream_branch_name))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let worktree = repo.worktree(
|
||||||
|
&action_args.name,
|
||||||
|
&dir.join(&action_args.name),
|
||||||
|
Some(git2::WorktreeAddOptions::new().reference(Some(target_branch.get()))),
|
||||||
|
);
|
||||||
|
|
||||||
|
match worktree {
|
||||||
|
Ok(_) => print_success(&format!("Worktree {} created", &action_args.name)),
|
||||||
|
Err(e) => {
|
||||||
|
print_error(&format!("Error creating worktree: {}", e));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd::WorktreeAction::Delete(action_args) => {
|
||||||
|
let worktree_dir = dir.join(&action_args.name);
|
||||||
|
|
||||||
|
match remove_worktree(
|
||||||
|
&action_args.name,
|
||||||
|
&worktree_dir,
|
||||||
|
action_args.force,
|
||||||
|
&repo,
|
||||||
|
) {
|
||||||
|
Ok(_) => print_success(&format!("Worktree {} deleted", &action_args.name)),
|
||||||
|
Err(error) => {
|
||||||
|
match error {
|
||||||
|
WorktreeRemoveFailureReason::Error(msg) => {
|
||||||
|
print_error(&msg);
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
WorktreeRemoveFailureReason::Changes(changes) => {
|
||||||
|
print_warning(&format!(
|
||||||
|
"Changes in worktree: {}. Refusing to delete",
|
||||||
|
changes
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd::WorktreeAction::Status(_args) => {
|
||||||
|
let mut table = Table::new();
|
||||||
|
add_worktree_table_header(&mut table);
|
||||||
|
for worktree in &worktrees {
|
||||||
|
let repo_dir = &dir.join(&worktree);
|
||||||
|
if repo_dir.exists() {
|
||||||
|
let repo = match open_repo(repo_dir, false) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
print_error(&format!("Error opening repo: {}", e));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
add_worktree_status(&mut table, worktree, &repo);
|
||||||
|
} else {
|
||||||
|
print_warning(&format!(
|
||||||
|
"Worktree {} does not have a directory",
|
||||||
|
&worktree
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for entry in std::fs::read_dir(&dir).unwrap() {
|
||||||
|
let dirname = path_as_string(
|
||||||
|
&entry
|
||||||
|
.unwrap()
|
||||||
|
.path()
|
||||||
|
.strip_prefix(&dir)
|
||||||
|
.unwrap()
|
||||||
|
.to_path_buf(),
|
||||||
|
);
|
||||||
|
if dirname == GIT_MAIN_WORKTREE_DIRECTORY {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !&worktrees.contains(&dirname) {
|
||||||
|
print_warning(&format!(
|
||||||
|
"Found {}, which is not a valid worktree directory!",
|
||||||
|
&dirname
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("{}", table);
|
||||||
|
}
|
||||||
|
cmd::WorktreeAction::Clean(_args) => {
|
||||||
|
for worktree in &worktrees {
|
||||||
|
let repo_dir = &dir.join(&worktree);
|
||||||
|
if repo_dir.exists() {
|
||||||
|
match remove_worktree(worktree, repo_dir, false, &repo) {
|
||||||
|
Ok(_) => print_success(&format!("Worktree {} deleted", &worktree)),
|
||||||
|
Err(error) => match error {
|
||||||
|
WorktreeRemoveFailureReason::Changes(changes) => {
|
||||||
|
print_warning(&format!(
|
||||||
|
"Changes found in {}: {}, skipping",
|
||||||
|
&worktree, &changes
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
WorktreeRemoveFailureReason::Error(e) => {
|
||||||
|
print_error(&e);
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print_warning(&format!(
|
||||||
|
"Worktree {} does not have a directory",
|
||||||
|
&worktree
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for entry in std::fs::read_dir(&dir).unwrap() {
|
||||||
|
let dirname = path_as_string(
|
||||||
|
&entry
|
||||||
|
.unwrap()
|
||||||
|
.path()
|
||||||
|
.strip_prefix(&dir)
|
||||||
|
.unwrap()
|
||||||
|
.to_path_buf(),
|
||||||
|
);
|
||||||
|
if dirname == GIT_MAIN_WORKTREE_DIRECTORY {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !&worktrees.contains(&dirname) {
|
||||||
|
print_warning(&format!(
|
||||||
|
"Found {}, which is not a valid worktree directory!",
|
||||||
|
&dirname
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
104
src/repo.rs
104
src/repo.rs
@@ -46,10 +46,18 @@ pub struct Remote {
|
|||||||
pub remote_type: RemoteType,
|
pub remote_type: RemoteType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn worktree_setup_default() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct Repo {
|
pub struct Repo {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
||||||
|
#[serde(default = "worktree_setup_default")]
|
||||||
|
pub worktree_setup: bool,
|
||||||
|
|
||||||
pub remotes: Option<Vec<Remote>>,
|
pub remotes: Option<Vec<Remote>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,11 +90,14 @@ pub struct RepoStatus {
|
|||||||
|
|
||||||
pub head: Option<String>,
|
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 worktrees: usize,
|
||||||
|
|
||||||
pub submodules: Vec<(String, SubmoduleStatus)>,
|
pub submodules: Option<Vec<(String, SubmoduleStatus)>>,
|
||||||
|
|
||||||
pub branches: Vec<(String, Option<(String, RemoteTrackingStatus)>)>,
|
pub branches: Vec<(String, Option<(String, RemoteTrackingStatus)>)>,
|
||||||
}
|
}
|
||||||
@@ -167,8 +178,16 @@ pub fn detect_remote_type(remote_url: &str) -> Option<RemoteType> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_repo(path: &Path) -> Result<Repository, RepoError> {
|
pub fn open_repo(path: &Path, is_worktree: bool) -> Result<Repository, RepoError> {
|
||||||
match Repository::open(path) {
|
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),
|
Ok(r) => Ok(r),
|
||||||
Err(e) => match e.code() {
|
Err(e) => match e.code() {
|
||||||
git2::ErrorCode::NotFound => Err(RepoError::new(RepoErrorKind::NotFound)),
|
git2::ErrorCode::NotFound => Err(RepoError::new(RepoErrorKind::NotFound)),
|
||||||
@@ -179,24 +198,43 @@ pub fn open_repo(path: &Path) -> Result<Repository, RepoError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_repo(path: &Path) -> Result<Repository, Box<dyn std::error::Error>> {
|
pub fn init_repo(path: &Path, is_worktree: bool) -> Result<Repository, Box<dyn std::error::Error>> {
|
||||||
match Repository::init(path) {
|
match is_worktree {
|
||||||
|
false => match Repository::init(path) {
|
||||||
Ok(r) => Ok(r),
|
Ok(r) => Ok(r),
|
||||||
Err(e) => Err(Box::new(e)),
|
Err(e) => Err(Box::new(e)),
|
||||||
|
},
|
||||||
|
true => match Repository::init_bare(path.join(super::GIT_MAIN_WORKTREE_DIRECTORY)) {
|
||||||
|
Ok(r) => Ok(r),
|
||||||
|
Err(e) => Err(Box::new(e)),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clone_repo(remote: &Remote, path: &Path) -> Result<(), Box<dyn std::error::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!(
|
print_action(&format!(
|
||||||
"Cloning into \"{}\" from \"{}\"",
|
"Cloning into \"{}\" from \"{}\"",
|
||||||
&path.display(),
|
&clone_target.display(),
|
||||||
&remote.url
|
&remote.url
|
||||||
));
|
));
|
||||||
match remote.remote_type {
|
match remote.remote_type {
|
||||||
RemoteType::Https => match Repository::clone(&remote.url, &path) {
|
RemoteType::Https => {
|
||||||
|
let mut builder = git2::build::RepoBuilder::new();
|
||||||
|
builder.bare(is_worktree);
|
||||||
|
match builder.clone(&remote.url, &clone_target) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(e) => Err(Box::new(e)),
|
Err(e) => Err(Box::new(e)),
|
||||||
},
|
}
|
||||||
|
}
|
||||||
RemoteType::Ssh => {
|
RemoteType::Ssh => {
|
||||||
let mut callbacks = RemoteCallbacks::new();
|
let mut callbacks = RemoteCallbacks::new();
|
||||||
callbacks.credentials(|_url, username_from_url, _allowed_types| {
|
callbacks.credentials(|_url, username_from_url, _allowed_types| {
|
||||||
@@ -207,9 +245,10 @@ pub fn clone_repo(remote: &Remote, path: &Path) -> Result<(), Box<dyn std::error
|
|||||||
fo.remote_callbacks(callbacks);
|
fo.remote_callbacks(callbacks);
|
||||||
|
|
||||||
let mut builder = git2::build::RepoBuilder::new();
|
let mut builder = git2::build::RepoBuilder::new();
|
||||||
|
builder.bare(is_worktree);
|
||||||
builder.fetch_options(fo);
|
builder.fetch_options(fo);
|
||||||
|
|
||||||
match builder.clone(&remote.url, path) {
|
match builder.clone(&remote.url, &clone_target) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(e) => Err(Box::new(e)),
|
Err(e) => Err(Box::new(e)),
|
||||||
}
|
}
|
||||||
@@ -217,7 +256,7 @@ pub fn clone_repo(remote: &Remote, path: &Path) -> Result<(), Box<dyn std::error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
let operation = match repo.state() {
|
||||||
git2::RepositoryState::Clean => None,
|
git2::RepositoryState::Clean => None,
|
||||||
state => Some(state),
|
state => Some(state),
|
||||||
@@ -232,15 +271,27 @@ pub fn get_repo_status(repo: &git2::Repository) -> RepoStatus {
|
|||||||
.map(|repo_name| repo_name.unwrap().to_string())
|
.map(|repo_name| repo_name.unwrap().to_string())
|
||||||
.collect::<Vec<String>>();
|
.collect::<Vec<String>>();
|
||||||
|
|
||||||
let head = match empty {
|
let head = match is_worktree {
|
||||||
|
true => None,
|
||||||
|
false => match empty {
|
||||||
true => None,
|
true => None,
|
||||||
false => Some(repo.head().unwrap().shorthand().unwrap().to_string()),
|
false => Some(repo.head().unwrap().shorthand().unwrap().to_string()),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let statuses = repo.statuses(None).unwrap();
|
let changes = match is_worktree {
|
||||||
|
|
||||||
let changes = match statuses.is_empty() {
|
|
||||||
true => None,
|
true => None,
|
||||||
|
false => {
|
||||||
|
let statuses = repo
|
||||||
|
.statuses(Some(
|
||||||
|
git2::StatusOptions::new()
|
||||||
|
.include_ignored(false)
|
||||||
|
.include_untracked(true),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match statuses.is_empty() {
|
||||||
|
true => Some(None),
|
||||||
false => {
|
false => {
|
||||||
let mut files_new = 0;
|
let mut files_new = 0;
|
||||||
let mut files_modified = 0;
|
let mut files_modified = 0;
|
||||||
@@ -256,7 +307,9 @@ pub fn get_repo_status(repo: &git2::Repository) -> RepoStatus {
|
|||||||
| git2::Status::WT_TYPECHANGE,
|
| git2::Status::WT_TYPECHANGE,
|
||||||
) {
|
) {
|
||||||
files_modified += 1;
|
files_modified += 1;
|
||||||
} else if status_bits.intersects(git2::Status::INDEX_NEW | git2::Status::WT_NEW) {
|
} else if status_bits
|
||||||
|
.intersects(git2::Status::INDEX_NEW | git2::Status::WT_NEW)
|
||||||
|
{
|
||||||
files_new += 1;
|
files_new += 1;
|
||||||
} else if status_bits
|
} else if status_bits
|
||||||
.intersects(git2::Status::INDEX_DELETED | git2::Status::WT_DELETED)
|
.intersects(git2::Status::INDEX_DELETED | git2::Status::WT_DELETED)
|
||||||
@@ -264,16 +317,26 @@ pub fn get_repo_status(repo: &git2::Repository) -> RepoStatus {
|
|||||||
files_deleted += 1;
|
files_deleted += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(RepoChanges {
|
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_new,
|
||||||
files_modified,
|
files_modified,
|
||||||
files_deleted,
|
files_deleted,
|
||||||
})
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let worktrees = repo.worktrees().unwrap().len();
|
let worktrees = repo.worktrees().unwrap().len();
|
||||||
|
|
||||||
|
let submodules = match is_worktree {
|
||||||
|
true => None,
|
||||||
|
false => {
|
||||||
let mut submodules = Vec::new();
|
let mut submodules = Vec::new();
|
||||||
for submodule in repo.submodules().unwrap() {
|
for submodule in repo.submodules().unwrap() {
|
||||||
let submodule_name = submodule.name().unwrap().to_string();
|
let submodule_name = submodule.name().unwrap().to_string();
|
||||||
@@ -299,6 +362,9 @@ pub fn get_repo_status(repo: &git2::Repository) -> RepoStatus {
|
|||||||
|
|
||||||
submodules.push((submodule_name, submodule_status));
|
submodules.push((submodule_name, submodule_status));
|
||||||
}
|
}
|
||||||
|
Some(submodules)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let mut branches = Vec::new();
|
let mut branches = Vec::new();
|
||||||
for (local_branch, _) in repo
|
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