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]
|
||||
name = "git-repo-manager"
|
||||
version = "0.7.5"
|
||||
version = "0.7.12"
|
||||
edition = "2021"
|
||||
|
||||
authors = [
|
||||
@@ -44,33 +44,33 @@ path = "src/grm/main.rs"
|
||||
version = "=0.5.9"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "=1.0.137"
|
||||
version = "=1.0.150"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.git2]
|
||||
version = "=0.14.4"
|
||||
version = "=0.15.0"
|
||||
|
||||
[dependencies.shellexpand]
|
||||
version = "=2.1.0"
|
||||
version = "=3.0.0"
|
||||
|
||||
[dependencies.clap]
|
||||
version = "=3.2.8"
|
||||
version = "=4.0.29"
|
||||
features = ["derive", "cargo"]
|
||||
|
||||
[dependencies.console]
|
||||
version = "=0.15.0"
|
||||
version = "=0.15.2"
|
||||
|
||||
[dependencies.regex]
|
||||
version = "=1.5.6"
|
||||
version = "=1.7.0"
|
||||
|
||||
[dependencies.comfy-table]
|
||||
version = "=6.0.0"
|
||||
version = "=6.1.3"
|
||||
|
||||
[dependencies.serde_yaml]
|
||||
version = "=0.8.24"
|
||||
version = "=0.9.14"
|
||||
|
||||
[dependencies.serde_json]
|
||||
version = "=1.0.82"
|
||||
version = "=1.0.89"
|
||||
|
||||
[dependencies.isahc]
|
||||
version = "=1.7.2"
|
||||
@@ -78,7 +78,7 @@ default-features = false
|
||||
features = ["json", "http2", "text-decoding"]
|
||||
|
||||
[dependencies.parse_link_header]
|
||||
version = "=0.3.2"
|
||||
version = "=0.3.3"
|
||||
|
||||
[dependencies.url-escape]
|
||||
version = "=0.1.1"
|
||||
|
||||
9
Justfile
9
Justfile
@@ -1,5 +1,7 @@
|
||||
set positional-arguments
|
||||
|
||||
set shell := ["/bin/bash", "-c"]
|
||||
|
||||
static_target := "x86_64-unknown-linux-musl"
|
||||
|
||||
check: fmt-check lint test
|
||||
@@ -32,6 +34,13 @@ build-release:
|
||||
build-release-static:
|
||||
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.sh patch
|
||||
|
||||
|
||||
36
README.md
36
README.md
@@ -1,7 +1,10 @@
|
||||
# GRM — Git Repository Manager
|
||||
|
||||
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/)
|
||||
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
|
||||
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
|
||||
|
||||
* [`toml`](https://docs.rs/toml/) for the configuration file
|
||||
* [`serde`](https://docs.rs/serde/) 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/) and [`shellexpand`](https://docs.rs/shellexpand) for good UX
|
||||
* [`toml`](https://docs.rs/toml/) for the configuration file.
|
||||
* [`serde`](https://docs.rs/serde/), together with
|
||||
[`serde_yaml`](https://docs.rs/serde_yaml/) and
|
||||
[`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
|
||||
|
||||
* [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)
|
||||
- [Working with Worktrees](./worktree_working.md)
|
||||
- [Worktrees and Remotes](./worktree_remotes.md)
|
||||
- [Behaviour Details](./worktree_behaviour.md)
|
||||
- [Behavior Details](./worktree_behavior.md)
|
||||
- [FAQ](./faq.md)
|
||||
- [Developer Documentation](./developing.md)
|
||||
- [Testing](./testing.md)
|
||||
|
||||
@@ -16,7 +16,7 @@ functions (like validation of certain input).
|
||||
## E2E tests
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
files)
|
||||
* 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
|
||||
branches, heads and remotes, ...)
|
||||
|
||||
@@ -39,8 +39,8 @@ configuration exists, what a config value is set to, how the repository looks
|
||||
like, ...)
|
||||
|
||||
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
|
||||
of these behaviours.
|
||||
happen. What are the failure modes? What affects the behavior? Parametrize each
|
||||
of these behaviors.
|
||||
|
||||
### Optimization
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ def get_temporary_directory(dir=None):
|
||||
def grm(args, cwd=None, is_invalid=False):
|
||||
cmd = subprocess.run([binary] + args, cwd=cwd, capture_output=True, text=True)
|
||||
if not is_invalid:
|
||||
assert "USAGE" not in cmd.stderr
|
||||
assert "usage" not in cmd.stderr.lower()
|
||||
print(f"grmcmd: {args}")
|
||||
print(f"stdout:\n{cmd.stdout}")
|
||||
print(f"stderr:\n{cmd.stderr}")
|
||||
|
||||
@@ -5,9 +5,9 @@ from helpers import *
|
||||
|
||||
def test_invalid_command():
|
||||
cmd = grm(["whatever"], is_invalid=True)
|
||||
assert "USAGE" in cmd.stderr
|
||||
assert "usage" in cmd.stderr.lower()
|
||||
|
||||
|
||||
def test_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)
|
||||
|
||||
assert cmd.returncode == 0
|
||||
assert len(cmd.stderr) == 0
|
||||
assert "did not specify any filters" in cmd.stderr.lower()
|
||||
|
||||
if default or configtype == "toml":
|
||||
output = toml.loads(cmd.stdout)
|
||||
|
||||
@@ -70,7 +70,7 @@ def test_worktree_add(
|
||||
explicit_track_branch_name = f"{default_remote}/{worktree_name}"
|
||||
|
||||
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.
|
||||
#
|
||||
# `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`
|
||||
#
|
||||
# `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.
|
||||
os.environ["GIT_COMMITTER_DATE"] = str(timestamp)
|
||||
|
||||
@@ -67,7 +67,7 @@ fi
|
||||
|
||||
for remote in $(git remote); do
|
||||
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
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -33,6 +33,7 @@ pub struct ConfigTrees {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ConfigProviderFilter {
|
||||
pub access: Option<bool>,
|
||||
pub owner: Option<bool>,
|
||||
@@ -41,6 +42,7 @@ pub struct ConfigProviderFilter {
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ConfigProvider {
|
||||
pub provider: RemoteProvider,
|
||||
pub token_command: String,
|
||||
@@ -52,7 +54,6 @@ pub struct ConfigProvider {
|
||||
pub api_url: Option<String>,
|
||||
|
||||
pub worktree: Option<bool>,
|
||||
pub init_worktree: Option<bool>,
|
||||
|
||||
pub remote_name: Option<String>,
|
||||
}
|
||||
@@ -182,6 +183,12 @@ impl Config {
|
||||
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 {
|
||||
RemoteProvider::Github => {
|
||||
match provider::Github::new(filter, token, config.api_url) {
|
||||
@@ -241,7 +248,7 @@ impl Config {
|
||||
|
||||
pub fn normalize(&mut 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() {
|
||||
if tree.root.starts_with(&home) {
|
||||
// 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
|
||||
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,
|
||||
Err(e) => {
|
||||
return Err(format!(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use clap::{AppSettings, Parser};
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(
|
||||
@@ -7,7 +7,6 @@ use clap::{AppSettings, Parser};
|
||||
author = clap::crate_authors!("\n"),
|
||||
about = clap::crate_description!(),
|
||||
long_version = clap::crate_version!(),
|
||||
global_setting(AppSettings::DeriveDisplayOrder),
|
||||
propagate_version = true,
|
||||
)]
|
||||
pub struct Opts {
|
||||
@@ -65,7 +64,7 @@ pub struct FindLocalArgs {
|
||||
pub path: String,
|
||||
|
||||
#[clap(
|
||||
arg_enum,
|
||||
value_enum,
|
||||
short,
|
||||
long,
|
||||
help = "Format to produce",
|
||||
@@ -85,7 +84,7 @@ pub struct FindConfigArgs {
|
||||
pub config: String,
|
||||
|
||||
#[clap(
|
||||
arg_enum,
|
||||
value_enum,
|
||||
short,
|
||||
long,
|
||||
help = "Format to produce",
|
||||
@@ -100,14 +99,14 @@ pub struct FindRemoteArgs {
|
||||
#[clap(short, long, help = "Path to the configuration file")]
|
||||
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,
|
||||
|
||||
#[clap(short, long, help = "Name of the remote to use")]
|
||||
pub remote_name: Option<String>,
|
||||
|
||||
#[clap(
|
||||
multiple_occurrences = true,
|
||||
action = clap::ArgAction::Append,
|
||||
name = "user",
|
||||
long,
|
||||
help = "Users to get repositories from"
|
||||
@@ -115,7 +114,7 @@ pub struct FindRemoteArgs {
|
||||
pub users: Vec<String>,
|
||||
|
||||
#[clap(
|
||||
multiple_occurrences = true,
|
||||
action = clap::ArgAction::Append,
|
||||
name = "group",
|
||||
long,
|
||||
help = "Groups to get repositories from"
|
||||
@@ -138,7 +137,7 @@ pub struct FindRemoteArgs {
|
||||
pub root: String,
|
||||
|
||||
#[clap(
|
||||
arg_enum,
|
||||
value_enum,
|
||||
short,
|
||||
long,
|
||||
help = "Format to produce",
|
||||
@@ -149,11 +148,10 @@ pub struct FindRemoteArgs {
|
||||
#[clap(
|
||||
long,
|
||||
help = "Use worktree setup for repositories",
|
||||
possible_values = &["true", "false"],
|
||||
value_parser = ["true", "false"],
|
||||
default_value = "false",
|
||||
default_missing_value = "true",
|
||||
min_values = 0,
|
||||
max_values = 1,
|
||||
num_args = 0..=1,
|
||||
)]
|
||||
pub worktree: String,
|
||||
|
||||
@@ -174,12 +172,11 @@ pub struct Config {
|
||||
|
||||
#[clap(
|
||||
long,
|
||||
value_parser = ["true", "false"],
|
||||
help = "Check out the default worktree after clone",
|
||||
possible_values = &["true", "false"],
|
||||
default_value = "true",
|
||||
default_missing_value = "true",
|
||||
min_values = 0,
|
||||
max_values = 1,
|
||||
num_args = 0..=1,
|
||||
)]
|
||||
pub init_worktree: String,
|
||||
}
|
||||
@@ -189,14 +186,14 @@ pub type RemoteProvider = super::provider::RemoteProvider;
|
||||
#[derive(Parser)]
|
||||
#[clap()]
|
||||
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,
|
||||
|
||||
#[clap(short, long, help = "Name of the remote to use")]
|
||||
pub remote_name: Option<String>,
|
||||
|
||||
#[clap(
|
||||
multiple_occurrences = true,
|
||||
action = clap::ArgAction::Append,
|
||||
name = "user",
|
||||
long,
|
||||
help = "Users to get repositories from"
|
||||
@@ -204,7 +201,7 @@ pub struct SyncRemoteArgs {
|
||||
pub users: Vec<String>,
|
||||
|
||||
#[clap(
|
||||
multiple_occurrences = true,
|
||||
action = clap::ArgAction::Append,
|
||||
name = "group",
|
||||
long,
|
||||
help = "Groups to get repositories from"
|
||||
@@ -229,11 +226,10 @@ pub struct SyncRemoteArgs {
|
||||
#[clap(
|
||||
long,
|
||||
help = "Use worktree setup for repositories",
|
||||
possible_values = &["true", "false"],
|
||||
value_parser = ["true", "false"],
|
||||
default_value = "false",
|
||||
default_missing_value = "true",
|
||||
min_values = 0,
|
||||
max_values = 1,
|
||||
num_args = 0..=1,
|
||||
)]
|
||||
pub worktree: String,
|
||||
|
||||
@@ -243,11 +239,10 @@ pub struct SyncRemoteArgs {
|
||||
#[clap(
|
||||
long,
|
||||
help = "Check out the default worktree after clone",
|
||||
possible_values = &["true", "false"],
|
||||
value_parser = ["true", "false"],
|
||||
default_value = "true",
|
||||
default_missing_value = "true",
|
||||
min_values = 0,
|
||||
max_values = 1,
|
||||
num_args = 0..=1,
|
||||
)]
|
||||
pub init_worktree: String,
|
||||
}
|
||||
@@ -259,7 +254,7 @@ pub struct OptionalConfig {
|
||||
pub config: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(clap::ArgEnum, Clone)]
|
||||
#[derive(clap::ValueEnum, Clone)]
|
||||
pub enum ConfigFormat {
|
||||
Yaml,
|
||||
Toml,
|
||||
@@ -299,7 +294,7 @@ pub struct WorktreeAddArgs {
|
||||
#[clap(short = 't', long = "track", help = "Remote branch to track")]
|
||||
pub track: Option<String>,
|
||||
|
||||
#[clap(long = "--no-track", help = "Disable tracking")]
|
||||
#[clap(long = "no-track", help = "Disable tracking")]
|
||||
pub no_track: bool,
|
||||
}
|
||||
#[derive(Parser)]
|
||||
@@ -328,22 +323,19 @@ pub struct WorktreeFetchArgs {}
|
||||
|
||||
#[derive(Parser)]
|
||||
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,
|
||||
#[clap(long = "--stash", help = "Stash & unstash changes before & after pull")]
|
||||
#[clap(long = "stash", help = "Stash & unstash changes before & after pull")]
|
||||
pub stash: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct WorktreeRebaseArgs {
|
||||
#[clap(long = "--pull", help = "Perform a pull before rebasing")]
|
||||
#[clap(long = "pull", help = "Perform a pull before rebasing")]
|
||||
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,
|
||||
#[clap(
|
||||
long = "--stash",
|
||||
help = "Stash & unstash changes before & after rebase"
|
||||
)]
|
||||
#[clap(long = "stash", help = "Stash & unstash changes before & after rebase")]
|
||||
pub stash: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ fn main() {
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
print_error(&format!("Error syncing trees: {}", error));
|
||||
print_error(&format!("Sync error: {}", error));
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,10 @@ fn main() {
|
||||
let filter =
|
||||
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 repos = match args.provider {
|
||||
@@ -62,7 +66,7 @@ fn main() {
|
||||
match provider::Github::new(filter, token, args.api_url) {
|
||||
Ok(provider) => provider,
|
||||
Err(error) => {
|
||||
print_error(&format!("Error: {}", error));
|
||||
print_error(&format!("Sync error: {}", error));
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -76,7 +80,7 @@ fn main() {
|
||||
match provider::Gitlab::new(filter, token, args.api_url) {
|
||||
Ok(provider) => provider,
|
||||
Err(error) => {
|
||||
print_error(&format!("Error: {}", error));
|
||||
print_error(&format!("Sync error: {}", error));
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -112,13 +116,13 @@ fn main() {
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
print_error(&format!("Error syncing trees: {}", error));
|
||||
print_error(&format!("Sync error: {}", error));
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
print_error(&format!("Error: {}", error));
|
||||
print_error(&format!("Sync error: {}", error));
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
@@ -278,6 +282,10 @@ fn main() {
|
||||
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 {
|
||||
provider::RemoteProvider::Github => {
|
||||
match match provider::Github::new(filter, token, config.api_url) {
|
||||
@@ -383,6 +391,10 @@ fn main() {
|
||||
let filter =
|
||||
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 repos = match args.provider {
|
||||
|
||||
@@ -106,7 +106,7 @@ fn find_repos(root: &Path) -> Result<Option<(Vec<repo::Repo>, Vec<String>, bool)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
let name = path.strip_prefix(&root).unwrap();
|
||||
let name = path.strip_prefix(root).unwrap();
|
||||
let namespace = name.parent().unwrap();
|
||||
(
|
||||
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()
|
||||
}
|
||||
|
||||
pub fn env_home() -> PathBuf {
|
||||
pub fn env_home() -> String {
|
||||
match std::env::var("HOME") {
|
||||
Ok(path) => Path::new(&path).to_path_buf(),
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
print_error(&format!("Unable to read HOME: {}", e));
|
||||
process::exit(1);
|
||||
@@ -58,16 +58,12 @@ pub fn env_home() -> PathBuf {
|
||||
}
|
||||
|
||||
pub fn expand_path(path: &Path) -> PathBuf {
|
||||
fn home_dir() -> Option<PathBuf> {
|
||||
Some(env_home())
|
||||
}
|
||||
|
||||
let expanded_path = match shellexpand::full_with_context(
|
||||
&path_as_string(path),
|
||||
home_dir,
|
||||
|| Some(env_home()),
|
||||
|name| -> Result<Option<String>, &'static str> {
|
||||
match name {
|
||||
"HOME" => Ok(Some(path_as_string(home_dir().unwrap().as_path()))),
|
||||
"HOME" => Ok(Some(env_home())),
|
||||
_ => Ok(None),
|
||||
}
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@ use std::collections::HashMap;
|
||||
|
||||
const DEFAULT_REMOTE_NAME: &str = "origin";
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, clap::ArgEnum, Clone)]
|
||||
#[derive(Debug, Deserialize, Serialize, clap::ValueEnum, Clone)]
|
||||
pub enum RemoteProvider {
|
||||
#[serde(alias = "github", alias = "GitHub")]
|
||||
Github,
|
||||
@@ -89,6 +89,10 @@ impl Filter {
|
||||
access,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty(&self) -> bool {
|
||||
self.users.is_empty() && self.groups.is_empty() && !self.owner && !self.access
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ApiErrorResponse<T>
|
||||
@@ -270,12 +274,16 @@ pub trait Provider {
|
||||
}
|
||||
|
||||
for group in &self.filter().groups {
|
||||
let group_projects = self
|
||||
.get_group_projects(group)
|
||||
.map_err(|error| match error {
|
||||
ApiErrorResponse::Json(x) => x.to_string(),
|
||||
ApiErrorResponse::String(s) => s,
|
||||
})?;
|
||||
let group_projects = self.get_group_projects(group).map_err(|error| {
|
||||
format!(
|
||||
"group \"{}\": {}",
|
||||
group,
|
||||
match error {
|
||||
ApiErrorResponse::Json(x) => x.to_string(),
|
||||
ApiErrorResponse::String(s) => s,
|
||||
}
|
||||
)
|
||||
})?;
|
||||
for group_project in group_projects {
|
||||
let mut already_present = false;
|
||||
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)?;
|
||||
|
||||
// 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
|
||||
.0
|
||||
.find_commit(operation.id())
|
||||
@@ -357,7 +357,7 @@ impl Worktree {
|
||||
let operation = operation.map_err(convert_libgit2_error)?;
|
||||
|
||||
// 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
|
||||
.0
|
||||
.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,
|
||||
Err(error) => {
|
||||
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() {
|
||||
match self.remove_worktree(
|
||||
directory,
|
||||
@@ -1387,12 +1387,12 @@ impl RepoHandle {
|
||||
.map_err(|error| format!("Getting worktrees failed: {}", error))?;
|
||||
|
||||
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(
|
||||
entry
|
||||
.map_err(|error| error.to_string())?
|
||||
.path()
|
||||
.strip_prefix(&directory)
|
||||
.strip_prefix(directory)
|
||||
// that unwrap() is safe as each entry is
|
||||
// guaranteed to be a subentry of &directory
|
||||
.unwrap(),
|
||||
|
||||
@@ -111,7 +111,7 @@ pub fn get_worktree_status_table(
|
||||
|
||||
add_worktree_table_header(&mut table);
|
||||
for worktree in &worktrees {
|
||||
let worktree_dir = &directory.join(&worktree.name());
|
||||
let worktree_dir = &directory.join(worktree.name());
|
||||
if worktree_dir.exists() {
|
||||
let repo = match repo::RepoHandle::open(worktree_dir, false) {
|
||||
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);
|
||||
|
||||
@@ -81,9 +81,9 @@
|
||||
//! * Instead of just picking `origin/prefix/foobar`, grm will complain because
|
||||
//! 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
|
||||
//! 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
|
||||
//! 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
|
||||
//! 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
|
||||
//! 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
|
||||
//! remotes, you would generally have a `default_remote` configured).
|
||||
//!
|
||||
@@ -372,7 +372,7 @@ impl<'a> Worktree<'a, WithRemoteTrackingBranch<'a>> {
|
||||
// TECHDEBT
|
||||
// We must not call this with `Some()` without a valid target.
|
||||
// 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
|
||||
//
|
||||
// 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:
|
||||
//
|
||||
// The git CLI will create the worktree's configuration directory
|
||||
|
||||
Reference in New Issue
Block a user