Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b4ed2837e | |||
| 701e64df6f | |||
| 23fc942db7 | |||
| 38bba1472e | |||
| 7d131bbacf | |||
| 6e79dd318a | |||
| 5fc1d2148f | |||
| de184de5a0 | |||
| 8a3b2ae1c5 | |||
| 93e38b0572 | |||
| 43bbb8e143 | |||
| 0fd9ce68b8 | |||
| 68f2b81e3f | |||
| d7a39fa4e4 | |||
| 1f646fd5f8 | |||
| 96cbf8c568 | |||
| bf199c1b17 | |||
| 0f7a70c895 | |||
| 3151b97bc0 | |||
| 8ce5cfecd4 | |||
| 6da27c6444 | |||
| 3026b3e6de | |||
| 725414cc71 | |||
| defb8fafca | |||
| f747c085c9 | |||
| 85dd794b53 | |||
| be8d85cb66 | |||
| 0b7527fc7d | |||
| 3a568a774a | |||
| a6ecb66547 | |||
| 8a04db8130 | |||
| d5bbbe6171 | |||
| c6a27525fd | |||
| 5880066531 | |||
| 918b63047b | |||
| 0fa2a65c81 | |||
| 87d5b7ad85 | |||
| 7db3596302 | |||
| e65c744f9c | |||
| bd79602d3a | |||
| 6e876aaefc | |||
| 04753e8d9c | |||
| 5811476e27 | |||
| 5ac814b857 | |||
| 3a87772606 | |||
| 001911bed9 | |||
| d1d729c33d | |||
| b4cabae4ec | |||
| 196bbeb2c2 | |||
| 4d60012f44 | |||
| b2f413b033 | |||
| c25bb8bf55 | |||
| 3b923e3e13 | |||
|
|
061265bbd0 | ||
| a08a8d2000 | |||
| fea0299c95 | |||
| 444930199c | |||
| 95704b9a40 | |||
| 8d300827d0 | |||
| 9ab79b120a | |||
| 8cc9470aca | |||
| 9e95701a6e | |||
| fe90401688 | |||
| aeaaee9915 | |||
| 52b024c1ba | |||
| 3a95613132 | |||
| 2ea2c994d8 | |||
| 04686b6dfa | |||
| 78201d4759 | |||
| 5fe6600dc3 | |||
| 2a65f78cd4 | |||
| 4852dad71e | |||
| 0746be904a | |||
| 53c2ee404c | |||
| bd694c3f7d | |||
| 95e9fcbffe | |||
| 98665a3231 | |||
| 7a51ad135f | |||
| e386935bc7 | |||
| c62562e6f0 | |||
| 00e37996b7 | |||
| a7e2c61984 | |||
| 58919b2d58 | |||
| dd36eb886f | |||
|
|
d2e01db0ae | ||
| bfd7b01ea4 |
491
Cargo.lock
generated
491
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
22
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "git-repo-manager"
|
name = "git-repo-manager"
|
||||||
version = "0.7.5"
|
version = "0.7.12"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
authors = [
|
authors = [
|
||||||
@@ -44,33 +44,33 @@ path = "src/grm/main.rs"
|
|||||||
version = "=0.5.9"
|
version = "=0.5.9"
|
||||||
|
|
||||||
[dependencies.serde]
|
[dependencies.serde]
|
||||||
version = "=1.0.137"
|
version = "=1.0.150"
|
||||||
features = ["derive"]
|
features = ["derive"]
|
||||||
|
|
||||||
[dependencies.git2]
|
[dependencies.git2]
|
||||||
version = "=0.14.4"
|
version = "=0.15.0"
|
||||||
|
|
||||||
[dependencies.shellexpand]
|
[dependencies.shellexpand]
|
||||||
version = "=2.1.0"
|
version = "=3.0.0"
|
||||||
|
|
||||||
[dependencies.clap]
|
[dependencies.clap]
|
||||||
version = "=3.2.8"
|
version = "=4.0.29"
|
||||||
features = ["derive", "cargo"]
|
features = ["derive", "cargo"]
|
||||||
|
|
||||||
[dependencies.console]
|
[dependencies.console]
|
||||||
version = "=0.15.0"
|
version = "=0.15.2"
|
||||||
|
|
||||||
[dependencies.regex]
|
[dependencies.regex]
|
||||||
version = "=1.5.6"
|
version = "=1.7.0"
|
||||||
|
|
||||||
[dependencies.comfy-table]
|
[dependencies.comfy-table]
|
||||||
version = "=6.0.0"
|
version = "=6.1.3"
|
||||||
|
|
||||||
[dependencies.serde_yaml]
|
[dependencies.serde_yaml]
|
||||||
version = "=0.8.24"
|
version = "=0.9.14"
|
||||||
|
|
||||||
[dependencies.serde_json]
|
[dependencies.serde_json]
|
||||||
version = "=1.0.82"
|
version = "=1.0.89"
|
||||||
|
|
||||||
[dependencies.isahc]
|
[dependencies.isahc]
|
||||||
version = "=1.7.2"
|
version = "=1.7.2"
|
||||||
@@ -78,7 +78,7 @@ default-features = false
|
|||||||
features = ["json", "http2", "text-decoding"]
|
features = ["json", "http2", "text-decoding"]
|
||||||
|
|
||||||
[dependencies.parse_link_header]
|
[dependencies.parse_link_header]
|
||||||
version = "=0.3.2"
|
version = "=0.3.3"
|
||||||
|
|
||||||
[dependencies.url-escape]
|
[dependencies.url-escape]
|
||||||
version = "=0.1.1"
|
version = "=0.1.1"
|
||||||
|
|||||||
9
Justfile
9
Justfile
@@ -1,5 +1,7 @@
|
|||||||
set positional-arguments
|
set positional-arguments
|
||||||
|
|
||||||
|
set shell := ["/bin/bash", "-c"]
|
||||||
|
|
||||||
static_target := "x86_64-unknown-linux-musl"
|
static_target := "x86_64-unknown-linux-musl"
|
||||||
|
|
||||||
check: fmt-check lint test
|
check: fmt-check lint test
|
||||||
@@ -32,6 +34,13 @@ build-release:
|
|||||||
build-release-static:
|
build-release-static:
|
||||||
cargo build --release --target {{static_target}} --features=static-build
|
cargo build --release --target {{static_target}} --features=static-build
|
||||||
|
|
||||||
|
pushall:
|
||||||
|
for r in $(git remote) ; do \
|
||||||
|
for branch in develop master ; do \
|
||||||
|
git push $r $branch ; \
|
||||||
|
done ; \
|
||||||
|
done
|
||||||
|
|
||||||
release-patch:
|
release-patch:
|
||||||
./release.sh patch
|
./release.sh patch
|
||||||
|
|
||||||
|
|||||||
36
README.md
36
README.md
@@ -1,7 +1,10 @@
|
|||||||
# GRM — Git Repository Manager
|
# GRM — Git Repository Manager
|
||||||
|
|
||||||
GRM helps you manage git repositories in a declarative way. Configure your
|
GRM helps you manage git repositories in a declarative way. Configure your
|
||||||
repositories in a [TOML](https://toml.io/) file, GRM does the rest.
|
repositories in a [TOML](https://toml.io/) or YAML file, GRM does the rest.
|
||||||
|
|
||||||
|
Also, GRM can be used to work with git worktrees in an opinionated,
|
||||||
|
straightforward fashion.
|
||||||
|
|
||||||
**Take a look at the [official documentation](https://hakoerber.github.io/git-repo-manager/)
|
**Take a look at the [official documentation](https://hakoerber.github.io/git-repo-manager/)
|
||||||
for installation & quickstart.**
|
for installation & quickstart.**
|
||||||
@@ -34,23 +37,26 @@ like Terraform for your local git repositories. Write a config, run the tool, an
|
|||||||
your repos are ready. The only thing that is tracked by git it the list of
|
your repos are ready. The only thing that is tracked by git it the list of
|
||||||
repositories itself.
|
repositories itself.
|
||||||
|
|
||||||
# Future & Ideas
|
|
||||||
|
|
||||||
* Operations over all repos (e.g. pull)
|
|
||||||
* Show status of managed repositories (dirty, compare to remotes, ...)
|
|
||||||
|
|
||||||
# Optional Features
|
|
||||||
|
|
||||||
* Support multiple file formats (YAML, JSON).
|
|
||||||
* Add systemd timer unit to run regular syncs
|
|
||||||
|
|
||||||
# Crates
|
# Crates
|
||||||
|
|
||||||
* [`toml`](https://docs.rs/toml/) for the configuration file
|
* [`toml`](https://docs.rs/toml/) for the configuration file.
|
||||||
* [`serde`](https://docs.rs/serde/) because we're using Rust, after all
|
* [`serde`](https://docs.rs/serde/), together with
|
||||||
* [`git2`](https://docs.rs/git2/), a safe wrapper around `libgit2`, for all git operations
|
[`serde_yaml`](https://docs.rs/serde_yaml/) and
|
||||||
* [`clap`](https://docs.rs/clap/), [`console`](https://docs.rs/console/) and [`shellexpand`](https://docs.rs/shellexpand) for good UX
|
[`serde_json`](https://docs.rs/serde_json/). Because we're using Rust, after
|
||||||
|
all.
|
||||||
|
* [`git2`](https://docs.rs/git2/), a safe wrapper around `libgit2`, for all git operations.
|
||||||
|
* [`clap`](https://docs.rs/clap/), [`console`](https://docs.rs/console/), [`comfy_table`](https://docs.rs/comfy-table/) and [`shellexpand`](https://docs.rs/shellexpand) for good UX.
|
||||||
|
* [`isahc`](https://docs.rs/isahc/) as the HTTP client for forge integrations.
|
||||||
|
|
||||||
# Links
|
# Links
|
||||||
|
|
||||||
* [crates.io](https://crates.io/crates/git-repo-manager)
|
* [crates.io](https://crates.io/crates/git-repo-manager)
|
||||||
|
|
||||||
|
# Mirrors
|
||||||
|
|
||||||
|
This repository can be found on multiple forges:
|
||||||
|
|
||||||
|
* https://github.com/hakoerber/git-repo-manager
|
||||||
|
* https://code.hkoerber.de/hannes/git-repo-manager/
|
||||||
|
* https://codeberg.org/hakoerber/git-repo-manager
|
||||||
|
* https://git.sr.ht/~hkoerber/git-repo-manager
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
- [Git Worktrees](./worktrees.md)
|
- [Git Worktrees](./worktrees.md)
|
||||||
- [Working with Worktrees](./worktree_working.md)
|
- [Working with Worktrees](./worktree_working.md)
|
||||||
- [Worktrees and Remotes](./worktree_remotes.md)
|
- [Worktrees and Remotes](./worktree_remotes.md)
|
||||||
- [Behaviour Details](./worktree_behaviour.md)
|
- [Behavior Details](./worktree_behavior.md)
|
||||||
- [FAQ](./faq.md)
|
- [FAQ](./faq.md)
|
||||||
- [Developer Documentation](./developing.md)
|
- [Developer Documentation](./developing.md)
|
||||||
- [Testing](./testing.md)
|
- [Testing](./testing.md)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ functions (like validation of certain input).
|
|||||||
## E2E tests
|
## E2E tests
|
||||||
|
|
||||||
The main focus of the testing setup lays on the e2e tests. Each user-facing
|
The main focus of the testing setup lays on the e2e tests. Each user-facing
|
||||||
behaviour *should* have a corresponding e2e test. These are the most important
|
behavior *should* have a corresponding e2e test. These are the most important
|
||||||
tests, as they test functionality the user will use in the end.
|
tests, as they test functionality the user will use in the end.
|
||||||
|
|
||||||
The test suite is written in python and uses
|
The test suite is written in python and uses
|
||||||
@@ -28,7 +28,7 @@ Effectively, each tests works like this:
|
|||||||
* Set up some prerequisites (e.g. different git repositories or configuration
|
* Set up some prerequisites (e.g. different git repositories or configuration
|
||||||
files)
|
files)
|
||||||
* Run `grm`
|
* Run `grm`
|
||||||
* Check that everything is according to expected behaviour (e.g. that `grm` had
|
* Check that everything is according to expected behavior (e.g. that `grm` had
|
||||||
certain output and exit code, that the target repositories have certain
|
certain output and exit code, that the target repositories have certain
|
||||||
branches, heads and remotes, ...)
|
branches, heads and remotes, ...)
|
||||||
|
|
||||||
@@ -39,8 +39,8 @@ configuration exists, what a config value is set to, how the repository looks
|
|||||||
like, ...)
|
like, ...)
|
||||||
|
|
||||||
Whenever you write a new test, think about the different circumstances that can
|
Whenever you write a new test, think about the different circumstances that can
|
||||||
happen. What are the failure modes? What affects the behaviour? Parametrize each
|
happen. What are the failure modes? What affects the behavior? Parametrize each
|
||||||
of these behaviours.
|
of these behaviors.
|
||||||
|
|
||||||
### Optimization
|
### Optimization
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ def get_temporary_directory(dir=None):
|
|||||||
def grm(args, cwd=None, is_invalid=False):
|
def grm(args, cwd=None, is_invalid=False):
|
||||||
cmd = subprocess.run([binary] + args, cwd=cwd, capture_output=True, text=True)
|
cmd = subprocess.run([binary] + args, cwd=cwd, capture_output=True, text=True)
|
||||||
if not is_invalid:
|
if not is_invalid:
|
||||||
assert "USAGE" not in cmd.stderr
|
assert "usage" not in cmd.stderr.lower()
|
||||||
print(f"grmcmd: {args}")
|
print(f"grmcmd: {args}")
|
||||||
print(f"stdout:\n{cmd.stdout}")
|
print(f"stdout:\n{cmd.stdout}")
|
||||||
print(f"stderr:\n{cmd.stderr}")
|
print(f"stderr:\n{cmd.stderr}")
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ from helpers import *
|
|||||||
|
|
||||||
def test_invalid_command():
|
def test_invalid_command():
|
||||||
cmd = grm(["whatever"], is_invalid=True)
|
cmd = grm(["whatever"], is_invalid=True)
|
||||||
assert "USAGE" in cmd.stderr
|
assert "usage" in cmd.stderr.lower()
|
||||||
|
|
||||||
|
|
||||||
def test_help():
|
def test_help():
|
||||||
cmd = grm(["--help"])
|
cmd = grm(["--help"])
|
||||||
assert "USAGE" in cmd.stdout
|
assert "usage" in cmd.stdout.lower()
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ def test_repos_find_remote_no_filter(provider, configtype, default, use_config):
|
|||||||
cmd = grm(args)
|
cmd = grm(args)
|
||||||
|
|
||||||
assert cmd.returncode == 0
|
assert cmd.returncode == 0
|
||||||
assert len(cmd.stderr) == 0
|
assert "did not specify any filters" in cmd.stderr.lower()
|
||||||
|
|
||||||
if default or configtype == "toml":
|
if default or configtype == "toml":
|
||||||
output = toml.loads(cmd.stdout)
|
output = toml.loads(cmd.stdout)
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ def test_worktree_add(
|
|||||||
explicit_track_branch_name = f"{default_remote}/{worktree_name}"
|
explicit_track_branch_name = f"{default_remote}/{worktree_name}"
|
||||||
|
|
||||||
timestamp = datetime.datetime.now().replace(microsecond=0).isoformat()
|
timestamp = datetime.datetime.now().replace(microsecond=0).isoformat()
|
||||||
# GitPython has some weird behaviour here. It is not possible to use kwargs
|
# GitPython has some weird behavior here. It is not possible to use kwargs
|
||||||
# to set the commit and author date.
|
# to set the commit and author date.
|
||||||
#
|
#
|
||||||
# `committer_date=x` (which is documented) does not work, as `git commit`
|
# `committer_date=x` (which is documented) does not work, as `git commit`
|
||||||
@@ -79,7 +79,7 @@ def test_worktree_add(
|
|||||||
# `author_date=x` does not work, as it's now called --date in `git commit`
|
# `author_date=x` does not work, as it's now called --date in `git commit`
|
||||||
#
|
#
|
||||||
# `date=x` should work, but is refused by GitPython, as it does not know
|
# `date=x` should work, but is refused by GitPython, as it does not know
|
||||||
# about the new behaviour in `git commit`
|
# about the new behavior in `git commit`
|
||||||
#
|
#
|
||||||
# Fortunately, there are env variables that control those timestamps.
|
# Fortunately, there are env variables that control those timestamps.
|
||||||
os.environ["GIT_COMMITTER_DATE"] = str(timestamp)
|
os.environ["GIT_COMMITTER_DATE"] = str(timestamp)
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ fi
|
|||||||
|
|
||||||
for remote in $(git remote); do
|
for remote in $(git remote); do
|
||||||
if git ls-remote --tags "${remote}" | grep -q "refs/tags/v${new_version}$"; then
|
if git ls-remote --tags "${remote}" | grep -q "refs/tags/v${new_version}$"; then
|
||||||
printf 'Tag %s already exists on %s' "v${new_version}" "${remote}" >&2
|
printf 'Tag %s already exists on %s\n' "v${new_version}" "${remote}" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ pub struct ConfigTrees {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct ConfigProviderFilter {
|
pub struct ConfigProviderFilter {
|
||||||
pub access: Option<bool>,
|
pub access: Option<bool>,
|
||||||
pub owner: Option<bool>,
|
pub owner: Option<bool>,
|
||||||
@@ -41,6 +42,7 @@ pub struct ConfigProviderFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct ConfigProvider {
|
pub struct ConfigProvider {
|
||||||
pub provider: RemoteProvider,
|
pub provider: RemoteProvider,
|
||||||
pub token_command: String,
|
pub token_command: String,
|
||||||
@@ -52,7 +54,6 @@ pub struct ConfigProvider {
|
|||||||
pub api_url: Option<String>,
|
pub api_url: Option<String>,
|
||||||
|
|
||||||
pub worktree: Option<bool>,
|
pub worktree: Option<bool>,
|
||||||
pub init_worktree: Option<bool>,
|
|
||||||
|
|
||||||
pub remote_name: Option<String>,
|
pub remote_name: Option<String>,
|
||||||
}
|
}
|
||||||
@@ -182,6 +183,12 @@ impl Config {
|
|||||||
filters.access.unwrap_or(false),
|
filters.access.unwrap_or(false),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if filter.empty() {
|
||||||
|
print_warning(
|
||||||
|
"The configuration does not contain any filters, so no repos will match",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let repos = match config.provider {
|
let repos = match config.provider {
|
||||||
RemoteProvider::Github => {
|
RemoteProvider::Github => {
|
||||||
match provider::Github::new(filter, token, config.api_url) {
|
match provider::Github::new(filter, token, config.api_url) {
|
||||||
@@ -241,7 +248,7 @@ impl Config {
|
|||||||
|
|
||||||
pub fn normalize(&mut self) {
|
pub fn normalize(&mut self) {
|
||||||
if let Config::ConfigTrees(config) = self {
|
if let Config::ConfigTrees(config) = self {
|
||||||
let home = path::env_home().display().to_string();
|
let home = path::env_home();
|
||||||
for tree in &mut config.trees_mut().iter_mut() {
|
for tree in &mut config.trees_mut().iter_mut() {
|
||||||
if tree.root.starts_with(&home) {
|
if tree.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`.
|
||||||
@@ -299,7 +306,7 @@ pub fn read_config<'a, T>(path: &str) -> Result<T, String>
|
|||||||
where
|
where
|
||||||
T: for<'de> serde::Deserialize<'de>,
|
T: for<'de> serde::Deserialize<'de>,
|
||||||
{
|
{
|
||||||
let content = match std::fs::read_to_string(&path) {
|
let content = match std::fs::read_to_string(path) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use clap::{AppSettings, Parser};
|
use clap::Parser;
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[clap(
|
#[clap(
|
||||||
@@ -7,7 +7,6 @@ use clap::{AppSettings, Parser};
|
|||||||
author = clap::crate_authors!("\n"),
|
author = clap::crate_authors!("\n"),
|
||||||
about = clap::crate_description!(),
|
about = clap::crate_description!(),
|
||||||
long_version = clap::crate_version!(),
|
long_version = clap::crate_version!(),
|
||||||
global_setting(AppSettings::DeriveDisplayOrder),
|
|
||||||
propagate_version = true,
|
propagate_version = true,
|
||||||
)]
|
)]
|
||||||
pub struct Opts {
|
pub struct Opts {
|
||||||
@@ -65,7 +64,7 @@ pub struct FindLocalArgs {
|
|||||||
pub path: String,
|
pub path: String,
|
||||||
|
|
||||||
#[clap(
|
#[clap(
|
||||||
arg_enum,
|
value_enum,
|
||||||
short,
|
short,
|
||||||
long,
|
long,
|
||||||
help = "Format to produce",
|
help = "Format to produce",
|
||||||
@@ -85,7 +84,7 @@ pub struct FindConfigArgs {
|
|||||||
pub config: String,
|
pub config: String,
|
||||||
|
|
||||||
#[clap(
|
#[clap(
|
||||||
arg_enum,
|
value_enum,
|
||||||
short,
|
short,
|
||||||
long,
|
long,
|
||||||
help = "Format to produce",
|
help = "Format to produce",
|
||||||
@@ -100,14 +99,14 @@ pub struct FindRemoteArgs {
|
|||||||
#[clap(short, long, help = "Path to the configuration file")]
|
#[clap(short, long, help = "Path to the configuration file")]
|
||||||
pub config: Option<String>,
|
pub config: Option<String>,
|
||||||
|
|
||||||
#[clap(arg_enum, short, long, help = "Remote provider to use")]
|
#[clap(value_enum, short, long, help = "Remote provider to use")]
|
||||||
pub provider: RemoteProvider,
|
pub provider: RemoteProvider,
|
||||||
|
|
||||||
#[clap(short, long, help = "Name of the remote to use")]
|
#[clap(short, long, help = "Name of the remote to use")]
|
||||||
pub remote_name: Option<String>,
|
pub remote_name: Option<String>,
|
||||||
|
|
||||||
#[clap(
|
#[clap(
|
||||||
multiple_occurrences = true,
|
action = clap::ArgAction::Append,
|
||||||
name = "user",
|
name = "user",
|
||||||
long,
|
long,
|
||||||
help = "Users to get repositories from"
|
help = "Users to get repositories from"
|
||||||
@@ -115,7 +114,7 @@ pub struct FindRemoteArgs {
|
|||||||
pub users: Vec<String>,
|
pub users: Vec<String>,
|
||||||
|
|
||||||
#[clap(
|
#[clap(
|
||||||
multiple_occurrences = true,
|
action = clap::ArgAction::Append,
|
||||||
name = "group",
|
name = "group",
|
||||||
long,
|
long,
|
||||||
help = "Groups to get repositories from"
|
help = "Groups to get repositories from"
|
||||||
@@ -138,7 +137,7 @@ pub struct FindRemoteArgs {
|
|||||||
pub root: String,
|
pub root: String,
|
||||||
|
|
||||||
#[clap(
|
#[clap(
|
||||||
arg_enum,
|
value_enum,
|
||||||
short,
|
short,
|
||||||
long,
|
long,
|
||||||
help = "Format to produce",
|
help = "Format to produce",
|
||||||
@@ -149,11 +148,10 @@ pub struct FindRemoteArgs {
|
|||||||
#[clap(
|
#[clap(
|
||||||
long,
|
long,
|
||||||
help = "Use worktree setup for repositories",
|
help = "Use worktree setup for repositories",
|
||||||
possible_values = &["true", "false"],
|
value_parser = ["true", "false"],
|
||||||
default_value = "false",
|
default_value = "false",
|
||||||
default_missing_value = "true",
|
default_missing_value = "true",
|
||||||
min_values = 0,
|
num_args = 0..=1,
|
||||||
max_values = 1,
|
|
||||||
)]
|
)]
|
||||||
pub worktree: String,
|
pub worktree: String,
|
||||||
|
|
||||||
@@ -174,12 +172,11 @@ pub struct Config {
|
|||||||
|
|
||||||
#[clap(
|
#[clap(
|
||||||
long,
|
long,
|
||||||
|
value_parser = ["true", "false"],
|
||||||
help = "Check out the default worktree after clone",
|
help = "Check out the default worktree after clone",
|
||||||
possible_values = &["true", "false"],
|
|
||||||
default_value = "true",
|
default_value = "true",
|
||||||
default_missing_value = "true",
|
default_missing_value = "true",
|
||||||
min_values = 0,
|
num_args = 0..=1,
|
||||||
max_values = 1,
|
|
||||||
)]
|
)]
|
||||||
pub init_worktree: String,
|
pub init_worktree: String,
|
||||||
}
|
}
|
||||||
@@ -189,14 +186,14 @@ pub type RemoteProvider = super::provider::RemoteProvider;
|
|||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[clap()]
|
#[clap()]
|
||||||
pub struct SyncRemoteArgs {
|
pub struct SyncRemoteArgs {
|
||||||
#[clap(arg_enum, short, long, help = "Remote provider to use")]
|
#[clap(value_enum, short, long, help = "Remote provider to use")]
|
||||||
pub provider: RemoteProvider,
|
pub provider: RemoteProvider,
|
||||||
|
|
||||||
#[clap(short, long, help = "Name of the remote to use")]
|
#[clap(short, long, help = "Name of the remote to use")]
|
||||||
pub remote_name: Option<String>,
|
pub remote_name: Option<String>,
|
||||||
|
|
||||||
#[clap(
|
#[clap(
|
||||||
multiple_occurrences = true,
|
action = clap::ArgAction::Append,
|
||||||
name = "user",
|
name = "user",
|
||||||
long,
|
long,
|
||||||
help = "Users to get repositories from"
|
help = "Users to get repositories from"
|
||||||
@@ -204,7 +201,7 @@ pub struct SyncRemoteArgs {
|
|||||||
pub users: Vec<String>,
|
pub users: Vec<String>,
|
||||||
|
|
||||||
#[clap(
|
#[clap(
|
||||||
multiple_occurrences = true,
|
action = clap::ArgAction::Append,
|
||||||
name = "group",
|
name = "group",
|
||||||
long,
|
long,
|
||||||
help = "Groups to get repositories from"
|
help = "Groups to get repositories from"
|
||||||
@@ -229,11 +226,10 @@ pub struct SyncRemoteArgs {
|
|||||||
#[clap(
|
#[clap(
|
||||||
long,
|
long,
|
||||||
help = "Use worktree setup for repositories",
|
help = "Use worktree setup for repositories",
|
||||||
possible_values = &["true", "false"],
|
value_parser = ["true", "false"],
|
||||||
default_value = "false",
|
default_value = "false",
|
||||||
default_missing_value = "true",
|
default_missing_value = "true",
|
||||||
min_values = 0,
|
num_args = 0..=1,
|
||||||
max_values = 1,
|
|
||||||
)]
|
)]
|
||||||
pub worktree: String,
|
pub worktree: String,
|
||||||
|
|
||||||
@@ -243,11 +239,10 @@ pub struct SyncRemoteArgs {
|
|||||||
#[clap(
|
#[clap(
|
||||||
long,
|
long,
|
||||||
help = "Check out the default worktree after clone",
|
help = "Check out the default worktree after clone",
|
||||||
possible_values = &["true", "false"],
|
value_parser = ["true", "false"],
|
||||||
default_value = "true",
|
default_value = "true",
|
||||||
default_missing_value = "true",
|
default_missing_value = "true",
|
||||||
min_values = 0,
|
num_args = 0..=1,
|
||||||
max_values = 1,
|
|
||||||
)]
|
)]
|
||||||
pub init_worktree: String,
|
pub init_worktree: String,
|
||||||
}
|
}
|
||||||
@@ -259,7 +254,7 @@ pub struct OptionalConfig {
|
|||||||
pub config: Option<String>,
|
pub config: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(clap::ArgEnum, Clone)]
|
#[derive(clap::ValueEnum, Clone)]
|
||||||
pub enum ConfigFormat {
|
pub enum ConfigFormat {
|
||||||
Yaml,
|
Yaml,
|
||||||
Toml,
|
Toml,
|
||||||
@@ -299,7 +294,7 @@ pub struct WorktreeAddArgs {
|
|||||||
#[clap(short = 't', long = "track", help = "Remote branch to track")]
|
#[clap(short = 't', long = "track", help = "Remote branch to track")]
|
||||||
pub track: Option<String>,
|
pub track: Option<String>,
|
||||||
|
|
||||||
#[clap(long = "--no-track", help = "Disable tracking")]
|
#[clap(long = "no-track", help = "Disable tracking")]
|
||||||
pub no_track: bool,
|
pub no_track: bool,
|
||||||
}
|
}
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
@@ -328,22 +323,19 @@ pub struct WorktreeFetchArgs {}
|
|||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub struct WorktreePullArgs {
|
pub struct WorktreePullArgs {
|
||||||
#[clap(long = "--rebase", help = "Perform a rebase instead of a fast-forward")]
|
#[clap(long = "rebase", help = "Perform a rebase instead of a fast-forward")]
|
||||||
pub rebase: bool,
|
pub rebase: bool,
|
||||||
#[clap(long = "--stash", help = "Stash & unstash changes before & after pull")]
|
#[clap(long = "stash", help = "Stash & unstash changes before & after pull")]
|
||||||
pub stash: bool,
|
pub stash: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub struct WorktreeRebaseArgs {
|
pub struct WorktreeRebaseArgs {
|
||||||
#[clap(long = "--pull", help = "Perform a pull before rebasing")]
|
#[clap(long = "pull", help = "Perform a pull before rebasing")]
|
||||||
pub pull: bool,
|
pub pull: bool,
|
||||||
#[clap(long = "--rebase", help = "Perform a rebase when doing a pull")]
|
#[clap(long = "rebase", help = "Perform a rebase when doing a pull")]
|
||||||
pub rebase: bool,
|
pub rebase: bool,
|
||||||
#[clap(
|
#[clap(long = "stash", help = "Stash & unstash changes before & after rebase")]
|
||||||
long = "--stash",
|
|
||||||
help = "Stash & unstash changes before & after rebase"
|
|
||||||
)]
|
|
||||||
pub stash: bool,
|
pub stash: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
print_error(&format!("Error syncing trees: {}", error));
|
print_error(&format!("Sync error: {}", error));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,6 +55,10 @@ fn main() {
|
|||||||
let filter =
|
let filter =
|
||||||
provider::Filter::new(args.users, args.groups, args.owner, args.access);
|
provider::Filter::new(args.users, args.groups, args.owner, args.access);
|
||||||
|
|
||||||
|
if filter.empty() {
|
||||||
|
print_warning("You did not specify any filters, so no repos will match");
|
||||||
|
}
|
||||||
|
|
||||||
let worktree = args.worktree == "true";
|
let worktree = args.worktree == "true";
|
||||||
|
|
||||||
let repos = match args.provider {
|
let repos = match args.provider {
|
||||||
@@ -62,7 +66,7 @@ fn main() {
|
|||||||
match provider::Github::new(filter, token, args.api_url) {
|
match provider::Github::new(filter, token, args.api_url) {
|
||||||
Ok(provider) => provider,
|
Ok(provider) => provider,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
print_error(&format!("Error: {}", error));
|
print_error(&format!("Sync error: {}", error));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,7 +80,7 @@ fn main() {
|
|||||||
match provider::Gitlab::new(filter, token, args.api_url) {
|
match provider::Gitlab::new(filter, token, args.api_url) {
|
||||||
Ok(provider) => provider,
|
Ok(provider) => provider,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
print_error(&format!("Error: {}", error));
|
print_error(&format!("Sync error: {}", error));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,13 +116,13 @@ fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
print_error(&format!("Error syncing trees: {}", error));
|
print_error(&format!("Sync error: {}", error));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
print_error(&format!("Error: {}", error));
|
print_error(&format!("Sync error: {}", error));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,6 +282,10 @@ fn main() {
|
|||||||
filters.access.unwrap_or(false),
|
filters.access.unwrap_or(false),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if filter.empty() {
|
||||||
|
print_warning("You did not specify any filters, so no repos will match");
|
||||||
|
}
|
||||||
|
|
||||||
let repos = match config.provider {
|
let repos = match config.provider {
|
||||||
provider::RemoteProvider::Github => {
|
provider::RemoteProvider::Github => {
|
||||||
match match provider::Github::new(filter, token, config.api_url) {
|
match match provider::Github::new(filter, token, config.api_url) {
|
||||||
@@ -383,6 +391,10 @@ fn main() {
|
|||||||
let filter =
|
let filter =
|
||||||
provider::Filter::new(args.users, args.groups, args.owner, args.access);
|
provider::Filter::new(args.users, args.groups, args.owner, args.access);
|
||||||
|
|
||||||
|
if filter.empty() {
|
||||||
|
print_warning("You did not specify any filters, so no repos will match");
|
||||||
|
}
|
||||||
|
|
||||||
let worktree = args.worktree == "true";
|
let worktree = args.worktree == "true";
|
||||||
|
|
||||||
let repos = match args.provider {
|
let repos = match args.provider {
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ fn find_repos(root: &Path) -> Result<Option<(Vec<repo::Repo>, Vec<String>, bool)
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let name = path.strip_prefix(&root).unwrap();
|
let name = path.strip_prefix(root).unwrap();
|
||||||
let namespace = name.parent().unwrap();
|
let namespace = name.parent().unwrap();
|
||||||
(
|
(
|
||||||
if namespace != Path::new("") {
|
if namespace != Path::new("") {
|
||||||
|
|||||||
12
src/path.rs
12
src/path.rs
@@ -47,9 +47,9 @@ pub 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn env_home() -> PathBuf {
|
pub fn env_home() -> String {
|
||||||
match std::env::var("HOME") {
|
match std::env::var("HOME") {
|
||||||
Ok(path) => Path::new(&path).to_path_buf(),
|
Ok(path) => path,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
print_error(&format!("Unable to read HOME: {}", e));
|
print_error(&format!("Unable to read HOME: {}", e));
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
@@ -58,16 +58,12 @@ pub fn env_home() -> PathBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn expand_path(path: &Path) -> PathBuf {
|
pub fn expand_path(path: &Path) -> PathBuf {
|
||||||
fn home_dir() -> Option<PathBuf> {
|
|
||||||
Some(env_home())
|
|
||||||
}
|
|
||||||
|
|
||||||
let expanded_path = match shellexpand::full_with_context(
|
let expanded_path = match shellexpand::full_with_context(
|
||||||
&path_as_string(path),
|
&path_as_string(path),
|
||||||
home_dir,
|
|| Some(env_home()),
|
||||||
|name| -> Result<Option<String>, &'static str> {
|
|name| -> Result<Option<String>, &'static str> {
|
||||||
match name {
|
match name {
|
||||||
"HOME" => Ok(Some(path_as_string(home_dir().unwrap().as_path()))),
|
"HOME" => Ok(Some(env_home())),
|
||||||
_ => Ok(None),
|
_ => Ok(None),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
const DEFAULT_REMOTE_NAME: &str = "origin";
|
const DEFAULT_REMOTE_NAME: &str = "origin";
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, clap::ArgEnum, Clone)]
|
#[derive(Debug, Deserialize, Serialize, clap::ValueEnum, Clone)]
|
||||||
pub enum RemoteProvider {
|
pub enum RemoteProvider {
|
||||||
#[serde(alias = "github", alias = "GitHub")]
|
#[serde(alias = "github", alias = "GitHub")]
|
||||||
Github,
|
Github,
|
||||||
@@ -89,6 +89,10 @@ impl Filter {
|
|||||||
access,
|
access,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn empty(&self) -> bool {
|
||||||
|
self.users.is_empty() && self.groups.is_empty() && !self.owner && !self.access
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum ApiErrorResponse<T>
|
pub enum ApiErrorResponse<T>
|
||||||
@@ -270,12 +274,16 @@ pub trait Provider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for group in &self.filter().groups {
|
for group in &self.filter().groups {
|
||||||
let group_projects = self
|
let group_projects = self.get_group_projects(group).map_err(|error| {
|
||||||
.get_group_projects(group)
|
format!(
|
||||||
.map_err(|error| match error {
|
"group \"{}\": {}",
|
||||||
ApiErrorResponse::Json(x) => x.to_string(),
|
group,
|
||||||
ApiErrorResponse::String(s) => s,
|
match error {
|
||||||
})?;
|
ApiErrorResponse::Json(x) => x.to_string(),
|
||||||
|
ApiErrorResponse::String(s) => s,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})?;
|
||||||
for group_project in group_projects {
|
for group_project in group_projects {
|
||||||
let mut already_present = false;
|
let mut already_present = false;
|
||||||
for repo in &repos {
|
for repo in &repos {
|
||||||
|
|||||||
12
src/repo.rs
12
src/repo.rs
@@ -233,7 +233,7 @@ impl Worktree {
|
|||||||
let operation = operation.map_err(convert_libgit2_error)?;
|
let operation = operation.map_err(convert_libgit2_error)?;
|
||||||
|
|
||||||
// This is required to preserve the commiter of the rebased
|
// This is required to preserve the commiter of the rebased
|
||||||
// commits, which is the expected behaviour.
|
// commits, which is the expected behavior.
|
||||||
let rebased_commit = repo
|
let rebased_commit = repo
|
||||||
.0
|
.0
|
||||||
.find_commit(operation.id())
|
.find_commit(operation.id())
|
||||||
@@ -357,7 +357,7 @@ impl Worktree {
|
|||||||
let operation = operation.map_err(convert_libgit2_error)?;
|
let operation = operation.map_err(convert_libgit2_error)?;
|
||||||
|
|
||||||
// This is required to preserve the commiter of the rebased
|
// This is required to preserve the commiter of the rebased
|
||||||
// commits, which is the expected behaviour.
|
// commits, which is the expected behavior.
|
||||||
let rebased_commit = repo
|
let rebased_commit = repo
|
||||||
.0
|
.0
|
||||||
.find_commit(operation.id())
|
.find_commit(operation.id())
|
||||||
@@ -785,7 +785,7 @@ impl RepoHandle {
|
|||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
for entry in match std::fs::read_dir(&root_dir) {
|
for entry in match std::fs::read_dir(root_dir) {
|
||||||
Ok(iterator) => iterator,
|
Ok(iterator) => iterator,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
return Err(WorktreeConversionFailureReason::Error(format!(
|
return Err(WorktreeConversionFailureReason::Error(format!(
|
||||||
@@ -1343,7 +1343,7 @@ impl RepoHandle {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
let repo_dir = &directory.join(&worktree.name());
|
let repo_dir = &directory.join(worktree.name());
|
||||||
if repo_dir.exists() {
|
if repo_dir.exists() {
|
||||||
match self.remove_worktree(
|
match self.remove_worktree(
|
||||||
directory,
|
directory,
|
||||||
@@ -1387,12 +1387,12 @@ impl RepoHandle {
|
|||||||
.map_err(|error| format!("Getting worktrees failed: {}", error))?;
|
.map_err(|error| format!("Getting worktrees failed: {}", error))?;
|
||||||
|
|
||||||
let mut unmanaged_worktrees = Vec::new();
|
let mut unmanaged_worktrees = Vec::new();
|
||||||
for entry in std::fs::read_dir(&directory).map_err(|error| error.to_string())? {
|
for entry in std::fs::read_dir(directory).map_err(|error| error.to_string())? {
|
||||||
let dirname = path::path_as_string(
|
let dirname = path::path_as_string(
|
||||||
entry
|
entry
|
||||||
.map_err(|error| error.to_string())?
|
.map_err(|error| error.to_string())?
|
||||||
.path()
|
.path()
|
||||||
.strip_prefix(&directory)
|
.strip_prefix(directory)
|
||||||
// that unwrap() is safe as each entry is
|
// that unwrap() is safe as each entry is
|
||||||
// guaranteed to be a subentry of &directory
|
// guaranteed to be a subentry of &directory
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ pub fn get_worktree_status_table(
|
|||||||
|
|
||||||
add_worktree_table_header(&mut table);
|
add_worktree_table_header(&mut table);
|
||||||
for worktree in &worktrees {
|
for worktree in &worktrees {
|
||||||
let worktree_dir = &directory.join(&worktree.name());
|
let worktree_dir = &directory.join(worktree.name());
|
||||||
if worktree_dir.exists() {
|
if worktree_dir.exists() {
|
||||||
let repo = match repo::RepoHandle::open(worktree_dir, false) {
|
let repo = match repo::RepoHandle::open(worktree_dir, false) {
|
||||||
Ok(repo) => repo,
|
Ok(repo) => repo,
|
||||||
@@ -185,7 +185,11 @@ pub fn get_status_table(config: config::Config) -> Result<(Vec<Table>, Vec<Strin
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
add_repo_status(&mut table, &repo.name, &repo_handle, repo.worktree_setup)?;
|
if let Err(err) =
|
||||||
|
add_repo_status(&mut table, &repo.name, &repo_handle, repo.worktree_setup)
|
||||||
|
{
|
||||||
|
errors.push(format!("{}: Couldn't add repo status: {}", &repo.name, err));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tables.push(table);
|
tables.push(table);
|
||||||
|
|||||||
@@ -81,9 +81,9 @@
|
|||||||
//! * Instead of just picking `origin/prefix/foobar`, grm will complain because
|
//! * Instead of just picking `origin/prefix/foobar`, grm will complain because
|
||||||
//! it also selected `remote2/foobar`.
|
//! it also selected `remote2/foobar`.
|
||||||
//!
|
//!
|
||||||
//! This is just emergent behaviour of the logic above. Fixing it would require
|
//! This is just emergent behavior of the logic above. Fixing it would require
|
||||||
//! additional logic for that edge case. I assume that it's just so rare to get
|
//! additional logic for that edge case. I assume that it's just so rare to get
|
||||||
//! that behaviour that it's acceptable for now.
|
//! that behavior that it's acceptable for now.
|
||||||
//!
|
//!
|
||||||
//! Now we either have a commit, we aborted, or we do not have commit. In the
|
//! Now we either have a commit, we aborted, or we do not have commit. In the
|
||||||
//! last case, as stated above, we check out the "default" branch.
|
//! last case, as stated above, we check out the "default" branch.
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
//! `foobar`. As both `remote1/foobar` and `remote2/foobar` as the same, the new
|
//! `foobar`. As both `remote1/foobar` and `remote2/foobar` as the same, the new
|
||||||
//! worktree will use that as the state of the new branch. But as `grm` cannot
|
//! worktree will use that as the state of the new branch. But as `grm` cannot
|
||||||
//! tell which remote branch to track, it will not set up remote tracking. This
|
//! tell which remote branch to track, it will not set up remote tracking. This
|
||||||
//! behaviour may be a bit confusing, but first, there is no good way to resolve
|
//! behavior may be a bit confusing, but first, there is no good way to resolve
|
||||||
//! this, and second, the situation should be really rare (when having multiple
|
//! this, and second, the situation should be really rare (when having multiple
|
||||||
//! remotes, you would generally have a `default_remote` configured).
|
//! remotes, you would generally have a `default_remote` configured).
|
||||||
//!
|
//!
|
||||||
@@ -372,7 +372,7 @@ impl<'a> Worktree<'a, WithRemoteTrackingBranch<'a>> {
|
|||||||
// TECHDEBT
|
// TECHDEBT
|
||||||
// We must not call this with `Some()` without a valid target.
|
// We must not call this with `Some()` without a valid target.
|
||||||
// I'm sure this can be improved, just not sure how.
|
// I'm sure this can be improved, just not sure how.
|
||||||
&*self.extra.target_commit.unwrap(),
|
&self.extra.target_commit.unwrap(),
|
||||||
)?
|
)?
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -463,7 +463,7 @@ impl<'a> Worktree<'a, WithRemoteTrackingBranch<'a>> {
|
|||||||
//
|
//
|
||||||
// > failed to make directory '/{repo}/.git-main-working-tree/worktrees/dir/test
|
// > failed to make directory '/{repo}/.git-main-working-tree/worktrees/dir/test
|
||||||
//
|
//
|
||||||
// This is a discrepancy between the behaviour of libgit2 and the
|
// This is a discrepancy between the behavior of libgit2 and the
|
||||||
// git CLI when creating worktrees with slashes:
|
// git CLI when creating worktrees with slashes:
|
||||||
//
|
//
|
||||||
// The git CLI will create the worktree's configuration directory
|
// The git CLI will create the worktree's configuration directory
|
||||||
|
|||||||
Reference in New Issue
Block a user