Skip to content

Commit

Permalink
feat: add setup subcommand to automate prepare-commit-msg githook s…
Browse files Browse the repository at this point in the history
…etup (#58)

* feat: add setup subcommand

Automate the setup of prepare-commit-msg githook
which will append Co-authored-by trailers to the commit msg

Updates typos and mistakes in the CLI help information

* test(setup): add integration tests for setup subcommand

Also add additional test for prepare-commit-msg local hook

* docs(readme): add instructions for automatic setup of githook

Extract and update manual setup documentation

* refactor(setup): import std::fs::Permissions only in unix

Its not used on windows

* fix(setup): print paths in setup output correctly

* refactor(setup): use home crate instead of dirs to get home dir

Make setup integration tests work on windows

This is testable on windows because it looks for USERPROFILE env first
before invoking the SHGetKnownFolderPath function with FOLDERID_Profile

* ci: fix generation of test coverage for setup.rs

Use cargo-llvm-cov instead of grcov

The trade-off is that this will count unit tests as part of coverage at this moment
  • Loading branch information
Mubashwer authored Apr 7, 2024
1 parent aa903cc commit 8077a21
Show file tree
Hide file tree
Showing 17 changed files with 828 additions and 58 deletions.
22 changes: 6 additions & 16 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,25 +86,15 @@ jobs:
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

- name: Run cargo test
run: cargo test --locked
env:
CARGO_INCREMENTAL: 0
RUSTFLAGS: "-Cinstrument-coverage"
LLVM_PROFILE_FILE: "git_mob_tool-%p-%m.profraw"

- name: Download grcov
run: |
mkdir -p "${HOME}/.local/bin"
curl -sL https://github.com/mozilla/grcov/releases/download/v0.8.19/grcov-x86_64-unknown-linux-gnu.tar.bz2 | tar jxf - -C "${HOME}/.local/bin"
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Generate coverage report
run: grcov . -s . --binary-path ./target/debug/ -t lcov --branch --ignore-not-existing --ignore "/*" --ignore "tests/**/*" --ignore src/lib.rs --excl-br-start "mod tests \{" --excl-start "mod tests \{" -o target/tests.lcov
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov

- name: Generate code coverage
run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info

- name: Upload coverage report to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./target/tests.lcov
files: lcov.info
fail_ci_if_error: true
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ path = "src/main.rs"

[dependencies]
clap = { version = "4.5.4", features = ["derive"] }
home = "0.5.9"
inquire = "0.6.2"

[dev-dependencies]
Expand Down
28 changes: 10 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,34 +44,26 @@ This CLI app will help you add them automatically and also help you store and ma
$ cargo install git-mob-tool
```

## Configuration
## Setup & Configuration

- Store your team members' details with keys
- Set up a global [`prepare-commit-msg`](https://git-scm.com/docs/githooks#_prepare_commit_msg) githook which appends the `Co-authored-by` trailers to the commit message.

```console
$ git mob coauthor --add lm "Leo Messi" [email protected]
$ git mob coauthor --add em "Emi Martinez" [email protected]
$ git mob coauthor --add sa "Sergio Aguero" [email protected]
$ git mob setup --global
```

- Set a global [`githooks`](https://git-scm.com/docs/githooks) directory
If a repository overrides `core.hooksPath` git configuration variable (e.g when using husky), then you will additionally need to run `git mob setup --local` for each such repository. This will set up a local (repository-specific) `prepare-commit-msg` githook which invokes the global one.

```console
$ mkdir ~/git
$ git config --global core.hooksPath "~/git"
```
_If you prefer to set this up manually or encounter any issues with the automated setup process, you can follow steps outlined [here.](./docs/manual_setup.md)_

- Download the [`prepare-commit-msg`](./prepare-commit-msg) file into the directory
- Ensure it is set as executable (Linux and macOS)
- Store your team members' details with keys

```console
$ chmod +x ./prepare-commit-msg
```console
$ git mob coauthor --add lm "Leo Messi" [email protected]
$ git mob coauthor --add em "Emi Martinez" [email protected]
$ git mob coauthor --add sa "Sergio Aguero" [email protected]
```

This `githook` will be used to append the `Co-authored-by` trailers to the commit's message.

_This githook also adds a Jira Issue ID as a prefix to the commit message if the branch name starts with a string resembling one. If you don't want want this, comment out [line 12 which calls the function `add_jira_issue_id_prefix`](./prepare-commit-msg#LL12)._

## Usage

- To mob with some team member(s):
Expand Down
36 changes: 36 additions & 0 deletions docs/manual_setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Manual Setup

- Set a global [githooks](https://git-scm.com/docs/githooks) directory

```console
$ mkdir -p ~/.git/hooks
$ git config --global core.hooksPath "~/.git/hooks"
```

- Download the [`prepare-commit-msg`](../src/commands/prepare-commit-msg) file into the directory
- Ensure it is set as executable (Linux and macOS)

```console
$ chmod +x ./prepare-commit-msg
```

This githook will append the `Co-authored-by` trailers to the commit message.

_If you want this githook to add a Jira Issue ID as a prefix to the commit message when the git branch name begins with a string resembling one, uncomment [line 12 to call the function `add_jira_issue_id_prefix`](../src/commands/prepare-commit-msg#LL12)._

## If a repository overrides `core.hooksPath` git configuration variable (e.g when using husky), then you will need to do additional steps for each such repository

- Retrieve the local (repository-specific) hooks directory

```console
$ git config --local core.hooksPath
```

- Download the [`prepare-commit-msg.local`](../src/commands/prepare-commit-msg.local) as `prepare-commit-msg` file into the directory
- Ensure it is set as executable (Linux and macOS)

```console
$ chmod +x ./prepare-commit-msg
```

This githook will invoke the global `prepare-commit-msg` githook that you originally set up.
11 changes: 8 additions & 3 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::coauthor_repo::CoauthorRepo;
use crate::commands::{coauthor::Coauthor, mob::Mob};
use crate::commands::{coauthor::Coauthor, mob::Mob, setup::Setup};
use clap::{Parser, Subcommand};
use std::error::Error;
use std::io::Write;
Expand All @@ -26,9 +26,11 @@ use std::str;
///
/// Usage example:
///
/// git mob co-author --add lm "Leo Messi" [email protected]
/// git mob setup --global
///
/// git pair with
/// git mob coauthor --add lm "Leo Messi" [email protected]
///
/// git mob --with lm
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
Expand All @@ -38,6 +40,8 @@ struct Cli {

#[derive(Subcommand)]
enum Commands {
/// Create prepare-commit-msg githook which append Co-authored-by trailers to commit message
Setup(Setup),
/// Add/delete/list co-author(s) from co-author repository
///
/// User must store co-author(s) to co-author repository by using keys
Expand All @@ -57,6 +61,7 @@ fn run_inner(
) -> Result<(), Box<dyn Error>> {
match &cli.command {
None => cli.mob.handle(coauthor_repo, out)?,
Some(Commands::Setup(setup)) => setup.handle(out)?,
Some(Commands::Coauthor(coauthor)) => coauthor.handle(coauthor_repo, out)?,
}
Ok(())
Expand Down
14 changes: 7 additions & 7 deletions src/coauthor_repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ mod tests {
Ok(CmdOutput {
stdout: stdout.clone(),
stderr: stderr.clone(),
status_code: status_code.clone(),
status_code,
})
});
mock_cmd_runner
Expand Down Expand Up @@ -632,7 +632,7 @@ mod tests {
command_runner
.expect_execute()
.once()
.withf(|program, args| program == "git" && args == &["config", "--global", "--get-all", "coauthors-mob.entry"])
.withf(|program, args| program == "git" && args == ["config", "--global", "--get-all", "coauthors-mob.entry"])
.returning(|_, _| {
Ok(CmdOutput {
stdout: b"Leo Messi <[email protected]>\nEmi Martinez <[email protected]>\n".into(),
Expand All @@ -645,7 +645,7 @@ mod tests {
.once()
.withf(|program, args| {
program == "git"
&& args == &["config", "--global", "--remove-section", "coauthors-mob"]
&& args == ["config", "--global", "--remove-section", "coauthors-mob"]
})
.returning(|_, _| {
Ok(CmdOutput {
Expand Down Expand Up @@ -681,7 +681,7 @@ mod tests {
command_runner
.expect_execute()
.once()
.withf( |program, args| program == "git" && args == &["config", "--global", "--get-all", "coauthors-mob.entry"])
.withf( |program, args| program == "git" && args == ["config", "--global", "--get-all", "coauthors-mob.entry"])
.returning( |_, _| {
Ok(CmdOutput {
stdout: b"Leo Messi <[email protected]>\nEmi Martinez <[email protected]>\n".into(),
Expand All @@ -694,7 +694,7 @@ mod tests {
.once()
.withf(|program, args| {
program == "git"
&& args == &["config", "--global", "--remove-section", "coauthors-mob"]
&& args == ["config", "--global", "--remove-section", "coauthors-mob"]
})
.returning(|_, _| {
Ok(CmdOutput {
Expand All @@ -719,7 +719,7 @@ mod tests {
command_runner
.expect_execute()
.once()
.withf( |program, args| program == "git" && args == &["config", "--global", "--get-all", "coauthors-mob.entry"])
.withf( |program, args| program == "git" && args == ["config", "--global", "--get-all", "coauthors-mob.entry"])
.returning( |_, _| {
Ok(CmdOutput {
stdout: b"Leo Messi <[email protected]>\nEmi Martinez <[email protected]>\n".into(),
Expand All @@ -732,7 +732,7 @@ mod tests {
.once()
.withf(|program, args| {
program == "git"
&& args == &["config", "--global", "--remove-section", "coauthors-mob"]
&& args == ["config", "--global", "--remove-section", "coauthors-mob"]
})
.returning(|_, _| {
Ok(CmdOutput {
Expand Down
6 changes: 3 additions & 3 deletions src/commands/coauthor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ use std::io::Write;
pub(crate) struct Coauthor {
/// Adds co-author to co-author repository
///
/// Usage example: git mob co-author --add lm "Leo Messi" [email protected]
/// Usage example: git mob coauthor --add lm "Leo Messi" [email protected]
#[arg(short = 'a', long = "add", num_args=3, value_names=["COAUTHOR_KEY", "COAUTHOR_NAME", "COAUTHOR_EMAIL"])]
pub(crate) add: Option<Vec<String>>,
/// Remove co-author from co-author repository
///
/// Usage example: git mob co-author --delete lm
/// Usage example: git mob coauthor --delete lm
#[arg(short = 'd', long = "delete", value_name = "COAUTHOR_KEY")]
pub(crate) delete: Option<String>,
/// Lists co-author(s) with keys(s) from co-author repository
///
/// Usage example: git mob co-author --list
/// Usage example: git mob coauthor --list
#[arg(short = 'l', long = "list")]
pub(crate) list: bool,
}
Expand Down
5 changes: 3 additions & 2 deletions src/commands/mob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,9 @@ mod tests {
let mut out = Vec::new();
let result = mob_cmd.handle(&mock_coauthor_repo, &mut out);

assert!(result.is_err_and(|err| err.to_string()
== "No co-author(s) found. At least one co-author must be added".to_string()));
assert!(result
.is_err_and(|err| err.to_string()
== *"No co-author(s) found. At least one co-author must be added"));

Ok(())
}
Expand Down
1 change: 1 addition & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub(crate) mod coauthor;
pub(crate) mod mob;
pub(crate) mod setup;
42 changes: 42 additions & 0 deletions src/commands/prepare-commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/bin/sh

set -e

main() {
# Do nothing during rebase
[ -z "$(git branch --show-current)" ] && exit 0

# Do nothing during amend (without message) or reuse of a commit message
[ "$2" = 'commit' ] && exit 0

#add_jira_issue_id_prefix "$@"
add_co_authored_by_trailers "$@"
}

add_co_authored_by_trailers() {
# Uses https://github.com/Mubashwer/git-mob
trailers=$(git mob --list | sed "s/^/Co-authored-by: /")
[ -n "$trailers" ] || return 0

printf "%s\n" "$trailers" |
sed "s/^/--trailer\n/" |
tr '\n' '\0' |
xargs -0 git interpret-trailers --if-exists addIfDifferent --in-place "$1"

printf "%s\n\n" "$trailers"
}

add_jira_issue_id_prefix() {
# If the branch name starts with string resembling a Jira Issue ID, fetch it
jira_issue_id=$(git branch --show-current | grep -o -E "^[a-zA-Z]+-[0-9]+" | tr '[:lower:]' '[:upper:]')
commit_msg_file_text=$(cat "$1")
commit_msg=$(echo "$commit_msg_file_text" | grep -v "^[[:space:]]*#" || true)

# If the Jira Issue ID is identified and the commit message does not already start with it
# then prepend the commit message with it
if [ -n "$jira_issue_id" ] && echo "$commit_msg" | grep -q -i -v "^\[\?$jira_issue_id\]\?"; then
printf "[%s] %s\n" "$jira_issue_id" "$commit_msg_file_text" > "$1"
fi
}

main "$@"
21 changes: 21 additions & 0 deletions src/commands/prepare-commit-msg.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/sh

set -e

# Get the global hooks directory
hooks_dir=$(git config --global core.hooksPath)

# Check if the global hooks directory is set
if [ -z "$hooks_dir" ]; then
printf "Error: Global hooks directory is not set" >&2
exit 1
fi

# Invoke the prepare-commit-msg hook from the global hooks directory
hook_path="$hooks_dir/prepare-commit-msg"
if [ -x "$hook_path" ]; then
exec "$hook_path" "$@"
else
printf "Error: prepare-commit-msg hook not found in the global hooks directory" >&2
exit 1
fi
Loading

0 comments on commit 8077a21

Please sign in to comment.