diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 06fa527..44c8d89 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,53 +1,3 @@ -# Contributing - -GRM is still in very early development. I started GRM mainly to scratch my own -itches (and am heavily dogfooding it). If you have a new use case for GRM, go -for it! - -The branching strategy is a simplified -[git-flow](https://nvie.com/posts/a-successful-git-branching-model/). - -* `master` is the "production" branch. Each commit is a new release. -* `develop` is the branch where new stuff is coming in. -* feature branches branch off of `develop` and merge back into it. - -So to contribute, just fork the repo and create a pull request against -`develop`. If you plan bigger changes, please consider opening an issue first, -so we can discuss it. - -If you want, add yourself to the `CONTRIBUTORS` file in your pull request. - -## Code formatting - -For Rust, just use `cargo fmt`. For Python, use -[black](https://github.com/psf/black). I'd rather not spend any effort in -configuring the formatters (not possible for black anyway). For shell scripts, -use [`shfmt`](https://github.com/mvdan/sh). - -## Tooling - -GRM uses [`just`](https://github.com/casey/just) as a command runner. See -[here](https://github.com/casey/just#installation) for installation -instructions (it's most likely just a simple `cargo install just`). - -## Testing - -There are two distinct test suites: One for unit test (`just test-unit`) and -integration tests (`just test-integration`) that is part of the rust crate, and -a separate e2e test suite in python (`just test-e2e`). - -To run all tests, run `just test`. - -When contributing, consider whether it makes sense to add tests which could -prevent regressions in the future. When fixing bugs, it makes sense to add -tests that expose the wrong behaviour beforehand. - -To also ensure proper formatting and that the linter is happy, use `just check`. -If that succeeds, your code is most likely fine to push! - -## Documentation - -The documentation lives in `docs` and uses -[mdBook](https://github.com/rust-lang/mdBook). Please document new user-facing -features here! - +Check out [the developer +documentation](https://hakoerber.github.io/git-repo-manager/developing.html) it +you want to contribute! diff --git a/docs/book.toml b/docs/book.toml index b77b6dd..a7fdfd0 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -7,3 +7,8 @@ title = "Git Repo Manager" [output.html] mathjax-support = true + +# [output.linkcheck] +# follow-web-links = true +# traverse-parent-directories = false +# warning-policy = "error" diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 259d759..1834083 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -1,9 +1,20 @@ # Summary -- [Overview](./overview.md) +[Overview](./overview.md) + - [Installation](./installation.md) -- [Repository trees](./repos.md) +- [Tutorial](./tutorial.md) +- [Managing Repositories](./repos.md) + - [Local Configuration](./local_configuration.md) + - [Forge Integrations](./forge_integration.md) - [Git Worktrees](./worktrees.md) -- [Forge Integrations](./forge_integration.md) + - [Working with Worktrees](./worktree_working.md) + - [Worktrees and Remotes](./worktree_remotes.md) + - [Behaviour Details](./worktree_behaviour.md) - [FAQ](./faq.md) -- [Contributing](./contributing.md) +- [Developer Documentation](./developing.md) + - [Testing](./testing.md) + - [Dependency updates](./dependency_updates.md) + - [Releases](./releases.md) + - [Formatting & Style](./formatting_and_style.md) + - [The Docs Themselves](./documentation.md) diff --git a/docs/src/contributing.md b/docs/src/contributing.md deleted file mode 120000 index f939e75..0000000 --- a/docs/src/contributing.md +++ /dev/null @@ -1 +0,0 @@ -../../CONTRIBUTING.md \ No newline at end of file diff --git a/docs/src/dependency_updates.md b/docs/src/dependency_updates.md new file mode 100644 index 0000000..1f3aec7 --- /dev/null +++ b/docs/src/dependency_updates.md @@ -0,0 +1,10 @@ +# Dependency updates + +Rust has the same problem as the node ecosystem, just a few magnitudes smaller: +Dependency sprawl. GRM has a dozen direct dependencies, but over 150 transitive +ones. + +To keep them up to date, there is a script: +`depcheck/update-cargo-dependencies.py`. It updates direct dependencies to the +latest stable version and updates transitive dependencies where possible. To run +it, use `just update-dependencies`, which will create commits for each update. diff --git a/docs/src/developing.md b/docs/src/developing.md new file mode 100644 index 0000000..33eb147 --- /dev/null +++ b/docs/src/developing.md @@ -0,0 +1,69 @@ +# Overview + +GRM is still in very early development. I started GRM mainly to scratch my own +itches (and am heavily dogfooding it). If you have a new use case for GRM, go +for it! + +## Contributing + +To contribute, just fork the repo and create a pull request against `develop`. +If you plan bigger changes, please consider opening an issue first, so we can +discuss it. + +If you want, add yourself to the `CONTRIBUTORS` file in your pull request. + +## Branching strategy + +The branching strategy is a simplified +[git-flow](https://nvie.com/posts/a-successful-git-branching-model/). + +* `master` is the "production" branch. Each commit is a new release. +* `develop` is the branch where new stuff is coming in. +* feature branches branch off of `develop` and merge back into it. + +Feature branches are not required, there are also changes happening directly on +`develop`. + +## Required tooling + +You will need the following tools: + +* Rust (obviously) (easiest via `rustup`), with the nightly toolchain +* Python3 +* [`just`](https://github.com/casey/just), a command runner like `make`. See + [here](https://github.com/casey/just#installation) for installation + instructions (it's most likely just a simple `cargo install just`). +* Docker & docker-compose for the e2e tests +* `black` and `shfmt` for formatting. +* `shellcheck` for shell script linting +* `mdbook` for the documentation + +Here are the tools: + +| Distribution | Command | +| ------------- | --------------------------------------------------------------------------------------------------- | +| Arch Linux | `pacman -S --needed python3 rustup just docker docker-compose python-black shfmt shellcheck mdbook` | +| Ubuntu/Debian | `apt-get install --no-install-recommends python3 docker.io docker-compose black shellcheck` | + +Note that you will have to install `just` and `mdbook` manually on Ubuntu (e.g. +via `cargo install just mdbook` if your rust build environment is set up +correctly). Same for `shfmt`, which may just be a `go install +mvdan.cc/sh/v3/cmd/shfmt@latest`, depending on your go build environment. + +For details about rustup and the toolchains, see [the installation +section](./installation.md). + +## FAQ + +### Why nightly? + +For now, GRM requires the nightly toolchain for two reasons: + +* [`io_error_more`](https://github.com/rust-lang/rust/issues/86442) to get + better error messages on IO errors +* [`const_option_ext`](https://github.com/rust-lang/rust/issues/91930) to have + static variables read from the environment that fall back to hard coded + defaults + +Honestly, both of those are not really necessary or can be handled without +nightly. It's just that I'm using nightly anyway. diff --git a/docs/src/documentation.md b/docs/src/documentation.md new file mode 100644 index 0000000..470d6d3 --- /dev/null +++ b/docs/src/documentation.md @@ -0,0 +1,11 @@ +# Documentation + +The documentation lives in the `docs` folder and uses +[mdBook](https://github.com/rust-lang/mdBook). Please document new user-facing +features here! + +Using [GitHub actions](https://github.com/features/actions), the documentation +on `master` is automatically published to [the project +homepage](https://hakoerber.github.io/git-repo-manager/) via GitHub pages. See +`.github/workflows/gh-pages.yml` for the configuration of GitHub Actions. + diff --git a/docs/src/faq.md b/docs/src/faq.md index fde7266..948e85d 100644 --- a/docs/src/faq.md +++ b/docs/src/faq.md @@ -1,10 +1,3 @@ # 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! +Currently empty, as there are no questions that are asked frequently :D diff --git a/docs/src/forge_integration.md b/docs/src/forge_integration.md index ec33984..5f4e3ac 100644 --- a/docs/src/forge_integration.md +++ b/docs/src/forge_integration.md @@ -1,19 +1,20 @@ # Forge Integrations -In addition to manging repositories locally, `grm` also integrates with source +In addition to managing repositories locally, `grm` also integrates with source code hosting platforms. Right now, the following platforms are supported: * [GitHub](https://github.com/) * [GitLab](https://gitlab.com/) -Imagine you are just starting out with `grm` and want to clone all your repositories -from GitHub. This is as simple as: +Imagine you are just starting out with `grm` and want to clone all your +repositories from GitHub. This is as simple as: ```bash $ grm repos sync remote --provider github --owner --token-command "pass show github_grm_access_token" --path ~/projects ``` -You will end up with your projects cloned into `~/projects/{your_github_username}/` +You will end up with your projects cloned into +`~/projects/{your_github_username}/` ## Authentication @@ -34,8 +35,8 @@ See the GitLab documentation for personal access tokens: The required scopes are a bit weird. Actually, the following should suffice: -* * `read_user` to get user information (required to get the current authenticated - user name for the `--owner` filter. +* `read_user` to get user information (required to get the current + authenticated user name for the `--owner` filter. * A scope that allows reading private repositories. (`read_repository` is just for *cloning* private repos). This unfortunately does not exist. @@ -106,7 +107,7 @@ The options in the file map to the command line options of the `grm repos sync remote` command. You'd then run the `grm repos sync` command the same way as with a list of -repositories in a config: +repositories in a configuration: ```bash $ grm repos sync --config example.config.toml @@ -120,11 +121,11 @@ $ grm repos find config --config example.config.toml > repos.toml $ grm repos sync config --config repos.toml ``` -## Using with selfhosted GitLab +## Using with self-hosted GitLab By default, `grm` uses the default GitLab API endpoint -([https://gitlab.com](https://gitlab.com)). You can override the -endpoint by specifying the `--api-url` parameter. Like this: +([https://gitlab.com](https://gitlab.com)). You can override the endpoint by +specifying the `--api-url` parameter. Like this: ```bash $ grm repos sync remote --provider gitlab --api-url https://gitlab.example.com [...] @@ -138,26 +139,28 @@ can be overridden with the `--force-ssh` switch. ## About the token command To ensure maximum flexibility, `grm` has a single way to get the token it uses -to authenticate: Specify a command that returns the token via stdout. This easily -integrates with password managers like [`pass`](https://www.passwordstore.org/). +to authenticate: Specify a command that returns the token via stdout. This +easily integrates with password managers like +[`pass`](https://www.passwordstore.org/). Of course, you are also free to specify something like `echo mytoken` as the -command, as long as you are ok with the security implications (like having the -token in cleartext in your shell history). It may be better to have the token +command, as long as you are OK with the security implications (like having the +token in clear text in your shell history). It may be better to have the token in a file instead and read it: `cat ~/.gitlab_token`. -Generally, use whatever you want. The command just has to return sucessfully and -return the token as the first line of stdout. +Generally, use whatever you want. The command just has to return successfully +and return the token as the first line of stdout. ## Examples -Maybe you just want to locally clone all repos from your github user? +Maybe you just want to locally clone all repos from your GitHub user? ```bash $ grm repos sync remote --provider github --owner --root ~/github_projects --token-command "pass show github_grm_access_token" ``` -This will clone all repositories into `~/github_projects/{your_github_username}`. +This will clone all repositories into +`~/github_projects/{your_github_username}`. If instead you want to clone **all** repositories you have access to (e.g. via organizations or other users' private repos you have access to), just change the @@ -172,12 +175,13 @@ $ grm repos sync remote --provider github --access --root ~/github_projects --to ### GitHub Unfortunately, GitHub does not have a nice API endpoint to get **private** -repositories for a certain user ([`/users/{user}/repos/`](https://docs.github.com/en/rest/repos/repos#list-repositories-for-a-user) only returns public -repositories). +repositories for a certain user +([`/users/{user}/repos/`](https://docs.github.com/en/rest/repos/repos#list-repositories-for-a-user) +only returns public repositories). Therefore, using `--user {user}` will only show public repositories for GitHub. -Note that this does not apply to `--access`: If you have access to another user's -private repository, it will be listed. +Note that this does not apply to `--access`: If you have access to another +user's private repository, it will be listed. ## Adding integrations @@ -197,9 +201,9 @@ Each repo has to have the following properties: * A name (which also acts as the identifier for diff between local and remote repositories) -* An SSH url to push to -* An HTTPS url to clone and fetch from +* An SSH URL to push to +* An HTTPS URL to clone and fetch from * A flag that marks the repository as private -If you plan to implement another forge, please first open an issue so we can -go through the required setup. I'm happy to help! +If you plan to implement another forge, please first open an issue so we can go +through the required setup. I'm happy to help! diff --git a/docs/src/formatting_and_style.md b/docs/src/formatting_and_style.md new file mode 100644 index 0000000..ff51b33 --- /dev/null +++ b/docs/src/formatting_and_style.md @@ -0,0 +1,45 @@ +# Formatting & Style + +## Code formatting + +I'm allergic to discussions about formatting. I'd rather make the computer do it +for me. + +For Rust, just use `cargo fmt`. For Python, use +[black](https://github.com/psf/black). I'd rather not spend any effort in +configuring the formatters (not possible for black anyway). For shell scripts, +use [`shfmt`](https://github.com/mvdan/sh). + +To autoformat all code, use `just fmt` + +## Style + +Honestly, no idea about style. I'm still learning Rust, so I'm trying to find a +good style. Just try to keep it consistent when you add code. + +## Linting + +You can use `just lint` to run all lints. + +### Rust + +Clippy is the guard that prevents shitty code from getting into the code base. +When running `just check`, any clippy suggestions will make the command fail. +So make clippy happy! The easiest way: + +* Commit your changes (so clippy can change safely). +* Run `cargo clippy --fix` to do the easy changes automatically. +* Run `cargo clippy` and take a look at the messages. + +Until now, I had no need to override or silence any clippy suggestions. + +### Shell + +`shellcheck` lints all shell scripts. As they change very rarely, this is not +too important. + +## Unsafe code + +Any `unsafe` code is forbidden for now globally via `#![forbid(unsafe_code)]`. +I cannot think of any reason GRM may need `unsafe`. If it comes up, it needs to +be discussed. diff --git a/docs/src/installation.md b/docs/src/installation.md index da97400..84290ff 100644 --- a/docs/src/installation.md +++ b/docs/src/installation.md @@ -2,8 +2,9 @@ ## 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. +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: @@ -15,7 +16,7 @@ Then, install the build dependencies: | Distribution | Command | | ------------- | ------------------------------------------------------------------------------ | -| Archlinux | `pacman -S --needed gcc openssl pkg-config` | +| Arch Linux | `pacman -S --needed gcc openssl pkg-config` | | Ubuntu/Debian | `apt-get install --no-install-recommends pkg-config gcc libssl-dev zlib1g-dev` | Then, it's a simple command to install the latest stable version: @@ -33,12 +34,12 @@ $ cargo +nightly install --git https://github.com/hakoerber/git-repo-manager.git ## Static build Note that by default, you will get a dynamically linked executable. -Alternatively, you can also build a statically linked binary. For this, you -will need `musl` and a few other build dependencies installed installed: +Alternatively, you can also build a statically linked binary. For this, you will +need `musl` and a few other build dependencies installed installed: | Distribution | Command | | ------------- | --------------------------------------------------------------------------- | -| Archlinux | `pacman -S --needed gcc musl perl make` | +| Arch Linux | `pacman -S --needed gcc musl perl make` | | Ubuntu/Debian | `apt-get install --no-install-recommends gcc musl-tools libc-dev perl make` | (`perl` and `make` are required for the OpenSSL build script) diff --git a/docs/src/local_configuration.md b/docs/src/local_configuration.md new file mode 100644 index 0000000..6cbcea3 --- /dev/null +++ b/docs/src/local_configuration.md @@ -0,0 +1,83 @@ +# Local Configuration + +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 configuration. 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 --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 -c 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 local ~/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 │ +├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤ +│ 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 │ +╰──────────┴──────────┴────────┴──────────┴───────┴─────────╯ +``` + +## YAML + +By default, the repo configuration uses TOML. If you prefer YAML, just give it a +YAML file instead (file ending does not matter, `grm` will figure out the +format). For generating a configuration, pass `--format yaml` to `grm repo +find` which generates a YAML configuration instead of a TOML configuration. diff --git a/docs/src/overview.md b/docs/src/overview.md index 082678a..048f14d 100644 --- a/docs/src/overview.md +++ b/docs/src/overview.md @@ -1,8 +1,8 @@ # 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. +Manager](https://github.com/hakoerber/git-repo-manager/) (GRM for short), a tool +that helps you manage git repositories in a declarative way. GRM helps you manage git repositories in a declarative way. Configure your repositories in a TOML or YAML file, GRM does the rest. Take a look at [the @@ -12,12 +12,12 @@ 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. +`git-worktree`. See [the worktree chapter](./worktrees.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: +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 diff --git a/docs/src/releases.md b/docs/src/releases.md new file mode 100644 index 0000000..51c45bd --- /dev/null +++ b/docs/src/releases.md @@ -0,0 +1,27 @@ +# Releases + +To make a release, make sure you are on a clean `develop` branch, sync your +remotes and then run `./release (major|minor|patch)`. It will handle a +git-flow-y release, meaning that it will perform a merge from `develop` to +`master`, create a git tag, sync all remotes and run `cargo publish`. + +The release script will also run `just check` to make sure that nothing it +broken. + +As GRM is still `v0.x`, there is not much consideration for backwards +compatibility. Generally, update the patch version for small stuff and the minor +version for bigger / backwards incompatible changes. + +Generally, it's good to regularly release a new patch release with [updated +dependencies](./dependency_updates.md). As `./release.sh patch` is exposed as a +Justfile target (`release-patch`), it's possible to do both in one step: + +```bash +$ just update-dependencies release-patch +``` + +## Release notes + +There are currently no release notes. Things are changing quite quickly and +there is simply no need for a record of changes (except the git history of +course). diff --git a/docs/src/repos.md b/docs/src/repos.md index 88cd7c4..467cd0c 100644 --- a/docs/src/repos.md +++ b/docs/src/repos.md @@ -1,82 +1,13 @@ -# Managing tree of git repositories +# Managing 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. +GRM helps you manage a bunch of git repositories easily. There are generally two +ways to go about that: -Let's try it out: +You can either manage a list of repositories in a TOML or YAML file, and use GRM +to sync the configuration with the state of the repository. -## Get the example configuration +Or, you can pull repository information from a forge (e.g. GitHub, GitLab) and +clone the repositories. -```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 --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 -c 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 local ~/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 │ -├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤ -│ 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 │ -╰──────────┴──────────┴────────┴──────────┴───────┴─────────╯ -``` - -## YAML - -By default, the repo configuration uses TOML. If you prefer YAML, just give it -a YAML file instead (file ending does not matter, `grm` will figure out the format). -For generating a configuration, pass `--format yaml` to `grm repo find` -which generates a YAML config instead of a TOML configuration. +There are also hybrid modes where you pull information from a forge and create a +configuration file that you can use later. diff --git a/docs/src/testing.md b/docs/src/testing.md new file mode 100644 index 0000000..260a440 --- /dev/null +++ b/docs/src/testing.md @@ -0,0 +1,124 @@ +# Testing + +There are two distinct test suites: One for unit test (`just test-unit`) and +integration tests (`just test-integration`) that is part of the rust crate, and +a separate e2e test suite in python (`just test-e2e`). + +To run all tests, run `just test`. + +When contributing, consider whether it makes sense to add tests which could +prevent regressions in the future. When fixing bugs, it makes sense to add tests +that expose the wrong behavior beforehand. + +The unit and integration tests are very small and only test a few self-contained +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 +tests, as they test functionality the user will use in the end. + +The test suite is written in python and uses +[pytest](https://docs.pytest.org/en/stable/). There are helper functions that +set up temporary git repositories and remotes in a `tmpfs`. + +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 + certain output and exit code, that the target repositories have certain + branches, heads and remotes, ...) + +As there are many different scenarios, the tests make heavy use of the +[`@pytest.mark.parametrize`](https://docs.pytest.org/en/stable/how-to/parametrize.html#pytest-mark-parametrize) +decorator to get all permutations of input parameters (e.g. whether a +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. + +### Optimization + +Note: You will most likely not need to read this. + +Each test parameter will exponentially increase the number of tests that will be +run. As a general rule, comprehensiveness is more important than test suite +runtime (so if in doubt, better to add another parameter to catch every edge +case). But try to keep the total runtime sane. Currently, the whole `just e2e` +target runs ~8'000 tests and takes around 5 minutes on my machine, exlucding +binary and docker build time. I'd say that keeping it under 10 minutes is a good +idea. + +To optimize tests, look out for two patterns: Dependency and Orthogonality + +#### Dependency + +If a parameter depends on another one, it makes little sense to handle them +independently. Example: You have a paramter that specifies whether a +configuration is used, and another parameter that sets a certain value in that +configuration file. It might look something like this: + +```python +@pytest.mark.parametrize("use_config", [True, False]) +@pytest.mark.parametrize("use_value", ["0", "1"]) +def test(...): +``` + +This leads to 4 tests being instantiated. But there is little point in setting a +configuration value when no config is used, so the combinations `(False, "0")` +and `(False, "1")` are redundant. To remedy this, spell out the optimized +permutation manually: + +```python +@pytest.mark.parametrize("config", ((True, "0"), (True, "1"), (False, None))) +def test(...): + (use_config, use_value) = config +``` + +This cuts down the number of tests by 25%. If you have more dependent parameters +(e.g. additional configuration values), this gets even better. Generally, this +will cut down the number of tests to + +\\[ \frac{1}{o \cdot c} + \frac{1}{(o \cdot c) ^ {(n + 1)}} \\] + +with \\( o \\) being the number of values of a parent parameters a parameter is +dependent on, \\( c \\) being the cardinality of the test input (so you can +assume \\( o = 1 \\) and \\( c = 2 \\) for boolean parameters), and \\( n \\) +being the number of parameters that are optimized, i.e. folded into their +dependent parameter. + +As an example: Folding down two boolean parameters into one dependent parent +boolean parameter will cut down the number of tests to 62.5%! + +#### Orthogonality + +If different test parameters are independent of each other, there is little +point in testing their combinations. Instead, split them up into different test +functions. For boolean parameters, this will cut the number of tests in half. + +So instead of this: + +```python +@pytest.mark.parametrize("param1", [True, False]) +@pytest.mark.parametrize("param2", [True, False]) +def test(...): +``` + +Rather do this: + +```python +@pytest.mark.parametrize("param1", [True, False]) +def test_param1(...): + +@pytest.mark.parametrize("param2", [True, False]) +def test_param2(...): +``` + +The tests are running in Docker via docker-compose. This is mainly needed to +test networking functionality like GitLab integration, with the GitLab API being +mocked by a simple flask container. diff --git a/docs/src/tutorial.md b/docs/src/tutorial.md new file mode 100644 index 0000000..0b4378c --- /dev/null +++ b/docs/src/tutorial.md @@ -0,0 +1,183 @@ +# Tutorial + +Here, you'll find a quick overview over the most common functionality of GRM. + +## Managing existing repositories + +Let's say you have your git repositories at `~/code`. To start managing them via +GRM, first create a configuration: + +```bash +grm repos find local ~/code --format yaml > ~/code/config.yml +``` + +The result may look something like this: + +```yaml +--- +trees: + - root: ~/code + repos: + - name: git-repo-manager + worktree_setup: true + remotes: + - name: origin + url: "https://github.com/hakoerber/git-repo-manager.git" + type: https +``` + +To apply the configuration and check whether all repositories are in sync, run +the following: + +```bash +$ grm repos sync config --config ~/code/config.yml +[✔] git-repo-manager: OK +``` + +Well, obiously there are no changes. To check how changes would be applied, +let's change the name of the remote (currently `origin`): + +```bash +$ sed -i 's/name: origin/name: github/' ~/code/config.yml +$ grm repos sync config --config ~/code/config.yml +[⚙] git-repo-manager: Setting up new remote "github" to "https://github.com/hakoerber/git-repo-manager.git" +[⚙] git-repo-manager: Deleting remote "origin" +[✔] git-repo-manager: OK +``` + +GRM replaced the `origin` remote with `github`. + +The configuration (`~/code/config.yml` in this example) would usually be +something you'd track in git or synchronize between machines via some other +means. Then, on every machine, all your repositories are a single `grm repos +sync` away! + +## Getting repositories from a forge + +Let's say you have a bunch of repositories on GitHub and you'd like to clone +them all to your local machine. + +To authenticate, you'll need to get a personal access token, as described in +[the forge documentation](./forge_integration.md#github). Let's assume you put +your token into `~/.github_token` (please don't if you're doing this "for +real"!) + +Let's first see what kind of repos we can find: + +```bash +$ grm repos sync remote --provider github --token-command "cat ~/.github_token" --root ~/code/github.com/ --format yaml +--- +trees: [] +$ +``` + +Ummm, ok? No repos? This is because you have to *tell* GRM what to look for (if +you don't, GRM will just relax, as it's lazy). + +There are different filters (see [the forge +documentation](./forge_integration.md#filters) for more info). In our case, +we'll just use the `--owner` filter to get all repos that belong to us: + +```bash +$ grm repos find remote --provider github --token-command "cat ~/.github_token" --root ~/code/github.com/ --format yaml +--- +trees: + - root: ~/code/github.com + repos: + - name: git-repo-manager + worktree_setup: false + remotes: + - name: origin + url: "https://github.com/hakoerber/git-repo-manager.git" + type: https +``` + +Nice! The format is the same as we got from `grm repos find local` above. So if +we wanted, we could save this file and use it with `grm repos sync config` as +above. But there is an even easier way: We can directly clone the repositories! + +```bash +$ grm repos sync remote --provider github --token-command "cat ~/.github_token" --root ~/code/github.com/ +[⚙] Cloning into "~/code/github.com/git-repo-manager" from "https://github.com/hakoerber/git-repo-manager.git" +[✔] git-repo-manager: Repository successfully cloned +[✔] git-repo-manager: OK +``` + +Nice! Just to make sure, let's run the same command again: + +```bash +$ grm repos sync remote --provider github --token-command "cat ~/.github_token" --root ~/code/github.com/ +[✔] git-repo-manager: OK +``` + +GRM saw that the repository is already there and did nothing (remember, it's +lazy). + +## Using worktrees + +Worktrees are something that make it easier to work with multiple branches at +the same time in a repository. Let's say we wanted to hack on the codebase of +GRM: + +```bash +$ cd ~/code/github.com/git-repo-manager +$ ls +.gitignore +Cargo.toml +... +``` + +Well, this is just a normal git repository. But let's try worktrees! First, we +have to convert the existing repository to use the special worktree setup. For +all worktree operations, we will use `grm worktree` (or `grm wt` for short): + +```bash +$ grm wt convert +[✔] Conversion done +$ ls +$ +``` + +So, the code is gone? Not really, there is just no active worktree right now. So +let's add one for `master`: + + +```bash +$ grm wt add master --track origin/master +[✔] Conversion done +$ ls +master +$ (cd ./master && git status) +On branch master +nothing to commit, working tree clean +``` + +Now, a single worktree is kind of pointless (if we only have one, we could also +just use the normal setup, without worktrees). So let's another one for +`develop`: + +```bash +$ grm wt add develop --track origin/develop +[✔] Conversion done +$ ls +develop +master +$ (cd ./develop && git status) +On branch develop +nothing to commit, working tree clean +``` + +What's the point? The cool thing is that we can now start working in the +`develop` worktree, without affecting the `master` worktree at all. If you're +working on `develop` and want to quickly see what a certain file looks like in +`master`, just look inside `./master`, it's all there! + +This becomes especially interesting when you have many feature branches and are +working on multiple features at the same time. + +There are a lot of options that influence how worktrees are handled. Maybe you +want to automatically track `origin/master` when you add a worktree called +`master`? Maybe you want your feature branches to have a prefix, so when you're +working on the `feature1` worktree, the remote branch will be +`origin/awesomefeatures/feature1`? Check out [the chapter on +worktrees](./worktrees.md) for all the things that are possible. diff --git a/docs/src/worktree_behaviour.md b/docs/src/worktree_behaviour.md new file mode 100644 index 0000000..f71e740 --- /dev/null +++ b/docs/src/worktree_behaviour.md @@ -0,0 +1,32 @@ +# Behavior Details + +When working with worktrees and GRM, there is a lot going on under the hood. +Each time you create a new worktree, GRM has to figure out what commit to set +your new branch to and how to configure any potential remote branches. + +To state again, the most important guideline is the following: + +**The branch inside the worktree is always the same as the directory name of the +worktree.** + +The second set of guidelines relates to the commit to check out, and the remote +branches to use: + +* When a branch already exists, you will get a worktree for that branch +* Existing local branches are never changed +* Only do remote operations if specifically requested (via configuration file or + command line parameters) +* When you specify `--track`, you will get that exact branch as the tracking + branch +* When you specify `--no-track`, you will get no tracking branch + +Apart from that, GRM tries to do The Right ThingTM. It should be as +little surprising as possible. + +In 99% of the cases, you will not have to care about the details, as the normal +workflows are covered by the rules above. In case you want to know the exact +behavior "specification", take a look at the [module documentation for +`grm::worktree`](https://docs.rs/git-repo-manager/latest/grm/worktree/index.html). + +If you think existing behavior is super-duper confusing and you have a better +idea, do not hesitate to open a GitHub issue to discuss this! diff --git a/docs/src/worktree_remotes.md b/docs/src/worktree_remotes.md new file mode 100644 index 0000000..6de925b --- /dev/null +++ b/docs/src/worktree_remotes.md @@ -0,0 +1,75 @@ +# Worktrees and Remotes + +To fetch all remote references from all remotes in a worktree setup, you can use +the following command: + +``` +$ grm wt fetch +[✔] Fetched from all remotes +``` + +This is equivalent to running `git fetch --all` in any of the worktrees. + +Often, you may want to pull all remote changes into your worktrees. For this, +use the `git pull` equivalent: + +``` +$ grm wt pull +[✔] master: Done +[✔] my-cool-branch: Done +``` + +This will refuse when there are local changes, or if the branch cannot be fast +forwarded. If you want to rebase your local branches, use the `--rebase` switch: + +``` +$ grm wt pull --rebase +[✔] master: Done +[✔] my-cool-branch: Done +``` + +As noted, this will fail if there are any local changes in your worktree. If you +want to stash these changes automatically before the pull (and unstash them +afterwards), use the `--stash` option. + +This will rebase your changes onto the upstream branch. This is mainly helpful +for persistent branches that change on the remote side. + +There is a similar rebase feature that rebases onto the **default** branch +instead: + +``` +$ grm wt rebase +[✔] master: Done +[✔] my-cool-branch: Done +``` + +This is super helpful for feature branches. If you want to incorporate changes +made on the remote branches, use `grm wt rebase` and all your branches will be +up to date. If you want to also update to remote tracking branches in one go, +use the `--pull` flag, and `--rebase` if you want to rebase instead of aborting +on non-fast-forwards: + +``` +$ grm wt rebase --pull --rebase +[✔] master: Done +[✔] my-cool-branch: Done +``` + +"So, what's the difference between `pull --rebase` and `rebase --pull`? Why the +hell is there a `--rebase` flag in the `rebase` command?" + +Yes, it's kind of weird. Remember that `pull` only ever updates each worktree to +their remote branch, if possible. `rebase` rebases onto the **default** branch +instead. The switches to `rebase` are just convenience, so you do not have to +run two commands. + +* `rebase --pull` is the same as `pull` && `rebase` +* `rebase --pull --rebase` is the same as `pull --rebase` && `rebase` + +I understand that the UX is not the most intuitive. If you can think of an +improvement, please let me know (e.g. via an GitHub issue)! + +As with `pull`, `rebase` will also refuse to run when there are changes in your +worktree. And you can also use the `--stash` option to stash/unstash changes +automatically. diff --git a/docs/src/worktree_working.md b/docs/src/worktree_working.md new file mode 100644 index 0000000..2a1ccc3 --- /dev/null +++ b/docs/src/worktree_working.md @@ -0,0 +1,173 @@ +# Working with Worktrees + +## 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 behavior, 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 behavior 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 + +Often, you'll have a workflow that uses tracking branches by default. It would +be quite tedious to add `--track` every single time. Luckily, the `grm.toml` +file supports defaults for the tracking behavior. See this for an example: + +```toml +[track] +default = true +default_remote = "origin" +``` + +This will set up a tracking branch on `origin` that has the same name as the +local branch. + +Sometimes, you might want to have a certain prefix for all your tracking +branches. Maybe to prevent collisions with other contributors. You can simply +set `default_remote_prefix` in `grm.toml`: + +```toml +[track] +default = true +default_remote = "origin" +default_remote_prefix = "myname" +``` + +When using branch `my-feature-branch`, the remote tracking branch would be +`origin/myname/my-feature-branch` in this case. + +Note that `--track` overrides any configuration in `grm.toml`. If you want to +disable tracking, use `--no-track`. + +## 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 than +sorry! Note that you'd get a similar error message if your worktree had any +uncommitted files, for the same reason. Now you can either commit & push your +changes, or your tell `grm` that you know what you're doing: + +``` +$ grm wt delete mybranch --force +[✔] Worktree mybranch deleted +``` + +If you just want to delete all worktrees that do not contain any changes, you +can also use the following: + +``` +$ grm wt clean +``` + +Note that this will not delete the default branch of the repository. It can of +course still be delete with `grm wt delete` if necessary. + +### Converting an existing repository + +It is possible to convert an existing directory to a worktree setup, using `grm +wt convert`. This command has to be run in the root of the repository you want +to convert: + +``` +$ grm wt convert +[✔] Conversion successful +``` + +This command will refuse to run if you have any changes in your repository. +Commit them and try again! + +Afterwards, the directory is empty, as there are no worktrees checked out yet. +Now you can use the usual commands to set up worktrees. diff --git a/docs/src/worktrees.md b/docs/src/worktrees.md index d10fa73..274b7bc 100644 --- a/docs/src/worktrees.md +++ b/docs/src/worktrees.md @@ -1,58 +1,60 @@ # 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 working with branches a lot, you may notice that +there is a lot of work required to make everything run smoothly. -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 working -with branches a lot, you may notice that there is a lot of work required to make -everything run smoothly. - -Maybe you have 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: +Maybe you have 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 I "rediscovered" some code hidden in some -old stash I forgot about. +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 I "rediscovered" some code hidden +in some old stash I forgot about). Also, conflicts on a `git stash pop` are just +horrible. -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. +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). +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 -way to run into conflicts. Changing to a different branch is just a `cd` away (if -the worktree is already set up). +[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 way 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: +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.** +**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`. +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 @@ -67,303 +69,27 @@ 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). +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). Note that there are few specific things you can configure for a certain -workspace. This is all done in an optional `grm.toml` file right in the root -of the worktree. More on that later. +workspace. This is all done in an optional `grm.toml` file right in the root of +the worktree. More on that later. -### 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): +## Manual access -``` -$ 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 - -Often, you'll have a workflow that uses tracking branches by default. It would -be quite tedious to add `--track` every single time. Luckily, the `grm.toml` file -supports defaults for the tracking behaviour. See this for an example: - -```toml -[track] -default = true -default_remote = "origin" -``` - -This will set up a tracking branch on `origin` that has the same name as the local -branch. - -Sometimes, you might want to have a certain prefix for all your tracking branches. -Maybe to prevent collissions with other contributors. You can simply set -`default_remote_prefix` in `grm.toml`: - -```toml -[track] -default = true -default_remote = "origin" -default_remote_prefix = "myname" -``` - -When using branch `my-feature-branch`, the remote tracking branch would be -`origin/myname/my-feature-branch` in this case. - -Note that `--track` overrides any configuration in `grm.toml`. If you want to -disable tracking, use `--no-track`. - -### 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 than sorry! Note that you'd get a similar error message if your -worktree had any uncommitted files, for the same reason. Now you can either -commit & push your changes, or your tell `grm` that you know what you're doing: - -``` -$ grm wt delete mybranch --force -[✔] Worktree mybranch deleted -``` - -If you just want to delete all worktrees that do not contain any changes, you -can also use the following: - -``` -$ grm wt clean -``` - -Note that this will not delete the default branch of the repository. It can of -course still be delete with `grm wt delete` if neccessary. - -### Persistent branches - -You most likely have a few branches that are "special", that you don't want to -clean up and that are the usual target for feature branches to merge into. GRM -calls them "persistent branches" and treats them a bit differently: - -* Their worktrees will never be deleted by `grm wt clean` -* If the branches in other worktrees are merged into them, they will be cleaned - up, even though they may not be in line with their upstream. Same goes for - `grm wt delete`, which will not require a `--force` flag. Note that of - course, actual changes in the worktree will still block an automatic cleanup! -* As soon as you enable persistent branches, non-persistent branches will only - ever be cleaned up when merged into a persistent branch. - -To elaborate: This is mostly relevant for a feature-branch workflow. Whenever a -feature branch is merged, it can usually be thrown away. As merging is usually -done on some remote code management platform (GitHub, GitLab, ...), this means -that you usually keep a branch around until it is merged into one of the "main" -branches (`master`, `main`, `develop`, ...) - -Enable persistent branches by setting the following in the `grm.toml` in the -worktree root: - -```toml -persistent_branches = [ - "master", - "develop", -] -``` - -Note that setting persistent branches will disable any detection of "default" -branches. The first entry will be considered your repositories' default branch. - -### Converting an existing repository - -It is possible to convert an existing directory to a worktree setup, using `grm -wt convert`. This command has to be run in the root of the repository you want -to convert: - -``` -$ grm wt convert -[✔] Conversion successful -``` - -This command will refuse to run if you have any changes in your repository. -Commit them and try again! - -Afterwards, the directory is empty, as there are no worktrees checked out yet. -Now you can use the usual commands to set up worktrees. - -### Working with remotes - -To fetch all remote references from all remotes in a worktree setup, you can -use the following command: - -``` -$ grm wt fetch -[✔] Fetched from all remotes -``` - -This is equivalent to running `git fetch --all` in any of the worktrees. - -Often, you may want to pull all remote changes into your worktrees. For this, -use the `git pull` equivalent: - -``` -$ grm wt pull -[✔] master: Done -[✔] my-cool-branch: Done -``` - -This will refuse when there are local changes, or if the branch cannot be fast -forwarded. If you want to rebase your local branches, use the `--rebase` switch: - -``` -$ grm wt pull --rebase -[✔] master: Done -[✔] my-cool-branch: Done -``` - -As noted, this will fail if there are any local changes in your worktree. If you -want to stash these changes automatically before the pull (and unstash them -afterwards), use the `--stash` option. - -This will rebase your changes onto the upstream branch. This is mainly helpful -for persistent branches that change on the remote side. - -There is a similar rebase feature that rebases onto the **default** branch instead: - -``` -$ grm wt rebase -[✔] master: Done -[✔] my-cool-branch: Done -``` - -This is super helpful for feature branches. If you want to incorporate changes -made on the remote branches, use `grm wt rebase` and all your branches will -be up to date. If you want to also update to remote tracking branches in one go, -use the `--pull` flag, and `--rebase` if you want to rebase instead of aborting -on non-fast-forwards: - -``` -$ grm wt rebase --pull --rebase -[✔] master: Done -[✔] my-cool-branch: Done -``` - -"So, what's the difference between `pull --rebase` and `rebase --pull`? Why the -hell is there a `--rebase` flag in the `rebase` command?" - -Yes, it's kind of weird. Remember that `pull` only ever updates each worktree -to their remote branch, if possible. `rebase` rebases onto the **default** branch -instead. The switches to `rebase` are just convenience, so you do not have to -run two commands. - -* `rebase --pull` is the same as `pull` && `rebase` -* `rebase --pull --rebase` is the same as `pull --rebase` && `rebase` - -I understand that the UX is not the most intuitive. If you can think of an -improvement, please let me know (e.g. via an GitHub issue)! - -As with `pull`, `rebase` will also refuse to run when there are changes in your -worktree. And you can also use the `--stash` option to stash/unstash changes -automatically. - -### Manual access - -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: +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. +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.