Skip to content

Commit

Permalink
[wip] feature: adds git hook binding for automatic version bumps
Browse files Browse the repository at this point in the history
    - adds a post-commit script that only runs fist-bump if env flag is not set
    - adds an install script that copies post-commit script to .git/hooks/post-commit
    - adds some logic that checks whether a hook flag is set to run the install script
  • Loading branch information
this-oliver committed Oct 9, 2023
1 parent 4fa2a3e commit f76ae2c
Show file tree
Hide file tree
Showing 7 changed files with 255 additions and 75 deletions.
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
> Recommended to install as a dev dependency
```bash
npm install --save-dev fist-bump
npm install --save-dev fistbump
```

## usage
Expand All @@ -31,11 +31,21 @@ npx fistbump
# the updated commit message => '[1.1.0] feature: added new feature'
```

*good to know:* `fist-bump` will skip any commits that have '[skip]' and '[wip]' or an existing version tag in the commit message.
You can also run `fistbump` automatically after every commit by installing it as a git hook:

```bash
# install fistbump as a git hook (recommended)
npx fistbump --hook

# uninstall fistbump as a git hook
npx fistbump --unhook
```

> **good to know**: `fistbump` will skip any commits that have '[skip]' and '[wip]' or an existing version tag in the commit message.
## configuration

To customize this `fist-bump`, all you have to do is add a `fistbump` property to your `package.json` file.
To customize this `fistbump`, all you have to do is add a `fistbump` property to your `package.json` file.

```json
{
Expand Down
69 changes: 0 additions & 69 deletions scripts/bump.sh

This file was deleted.

38 changes: 38 additions & 0 deletions scripts/hooks/post-commit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/bin/sh

# post-commit.sh
#
# The purpose of this script is to add `fist-bump` to git hooks. This ensures that
# the version number is bumped before every commit automatically instead of having
# to (remember) to do it manually every time.
#
# how does it work?
#
# The script adds `fist-bump` to the `post-commit` git hook. However, in order to
# avoid an infinite loop, the script sets checks for an env var called `FIST_BUMP`
# (which is set after the first `post-commit` hook call) prior to running
# `fist-bump`. If the env var is set, `fist-bump` will not run.

# get root directory of project
root_dir=$(git rev-parse --show-toplevel)

# function to remove the flag environment variable
cleanup() {
unset FIST_BUMP
}

# set a trap to always cleanup on exit
trap cleanup EXIT

# Only run if the environment variable is not set
if [ "$FIST_BUMP" != "1" ]; then

# run fist-bump
./lib/index.js

# set environment variable
export FIST_BUMP=1

# amend the commit with the changes made by your script
git commit --amend --no-edit
fi
42 changes: 42 additions & 0 deletions scripts/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/bin/sh

# install.sh
#
# The purpose of this script is to add `post-commit.sh` to the end of the
# post-commit git hook (.git/hooks/post-commit). This ensures that the
# version it can run after every commit. This ensures that the version
# number is bumped before every commit automatically instead of having
# to (remember) to do it manually every time.

DELIMITER_START="# BEGIN FISTBUMP"
DELIMITER_END="# END FISTBUMP"

# get root directory of project
root_dir=$(git rev-parse --show-toplevel)

# create .git/hooks/post-commit if it doesn't exist
if [ ! -f "$root_dir/.git/hooks/post-commit" ]; then
touch "$root_dir/.git/hooks/post-commit"
echo "[fist-bump] .git/hooks/post-commit created"
fi

# get contents of post-commit.sh (minus the shebang)
post_commit=$(tail -n +2 "$root_dir/scripts/hooks/post-commit.sh")

# wrap contents in a delimiter comment (BEGIN/END FISTBUMP)
content="$DELIMITER_START\n$post_commit\n$DELIMITER_END"

# exit with success if hook has a delimiter comment
if grep -q "$DELIMITER_START" "$root_dir/.git/hooks/post-commit"; then
echo "[fist-bump] fist-bump already installed to .git/hooks/post-commit"
exit 0
fi

# add contents of post-commit.sh to the end of .git/hooks/post-commit
# overwrite .git/hooks/post-commit with extracted content
if echo "$content" >> "$root_dir/.git/hooks/post-commit"; then
echo "[fist-bump] fist-bump added to .git/hooks/post-commit"
else
echo "[fist-bump] Error: Failed to write to .git/hooks/post-commit (use sudo)"
exit 1
fi
35 changes: 35 additions & 0 deletions scripts/uninstall.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/sh

# uninstall.sh
#
# The purpose of this script is to remove the `post-commit.sh`
# script from the `post-commit` git hook.

DELIMITER_START="# BEGIN FISTBUMP"
DELIMITER_END="# END FISTBUMP"

# get root directory of project
root_dir=$(git rev-parse --show-toplevel)

# get contents of .git/hooks/post-commit
hook=$(cat "$root_dir/.git/hooks/post-commit")

# get post-commit.sh content
post_commit=$(tail -n +2 "$root_dir/scripts/hooks/post-commit.sh")

# exit with success if hook doesn't have a delimiter comment
if ! grep -q "$DELIMITER_START" "$root_dir/.git/hooks/post-commit"; then
echo "[fist-bump] fist-bump not installed to .git/hooks/post-commit"
exit 0
fi

# get contents of .git/hooks/post-commit excluding fist-bump content
content=$(echo "$hook" | sed "/$DELIMITER_START/,/$DELIMITER_END/d")

# overwrite .git/hooks/post-commit with extracted content
if echo "$content" > "$root_dir/.git/hooks/post-commit"; then
echo "[fist-bump] fist-bump removed from .git/hooks/post-commit"
else
echo "[fist-bump] Error: Failed to write to .git/hooks/post-commit (use sudo)"
exit 1
fi
108 changes: 107 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,111 @@
#!/usr/bin/env node

import { execute, getProjectRoot, logMessage } from "./utils";
import { bump } from "./bump";

bump();
const FLAG = {
INSTALL: [ "-I", "--install" ],
UNINSTALL: [ "-U", "--uninstall" ]
}

const MAXIMUM_ARGS = 3;

/**
* Returns true if a string is in the args
*
* @param string - sample arg
* @returns boolean
*/
function _inArgs(string: string): boolean {
return process.argv.includes(string)
}

/**
* Returns true if the install flag has been passed
*
* Valid flags:
* - `-I`
* - `--install`
*/
function _isInstall(): boolean {
let set = false;

for(const flag of FLAG.INSTALL){
if(_inArgs(flag)){
set = true;
break;
}
}

return set;
}

/**
* Returns true if the uninstall flag has been passed.
*
* Valid flags:
* - `-U`
* - `--uninstall`
*/
function _isUninstall(): boolean {
let set = false;

for(const flag of FLAG.UNINSTALL){
if(_inArgs(flag)){
set = true;
break;
}
}

return set;
}

/**
* Returns true if invalid argument is passed.
*
* Valid arguments:
* - `./lib/cli.js`,
* - `./lib/cli.js -H`
* - `./lib/cli.js --hook`,
*/
function _isInvalidArg(): boolean {
if (process.argv.length > MAXIMUM_ARGS) {
return true;
} else if (process.argv.length === MAXIMUM_ARGS && !(_isInstall() || _isUninstall())) {
return true;
} else {
return false;
}
}

/**
* Runs a script to install/uninstall a command that executes the
* fist-bump command when the post-commit git hook is triggered.
*
* @param type - `install` or `uninstall`
* @param silent - if true, the script will not log any messages (default: false)
*/
function _executeHookScript(type: "install" | "uninstall"): void {
const rootDir = getProjectRoot() || process.cwd();
const scriptPath = `${rootDir}/scripts/${type}.sh`;
try {
execute(`chmod +x ${scriptPath}`);
execute(`${scriptPath}`);
} catch (error) {
logMessage(`Failed to ${type} git hook. ${error}`, "error");
}
}
if(_isInvalidArg()) {
logMessage("Invalid argument passed. Only `--hook` or `-H` is allowed.", "error")
process.exit(1);
} else if(_isInstall()) {
_executeHookScript("install");
logMessage("Git hook installed successfully.");
} else if(_isUninstall()) {
_executeHookScript("uninstall");
logMessage("Git hook uninstalled successfully.");
} else {
bump();
}
22 changes: 20 additions & 2 deletions src/utils/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,32 @@ interface ExecuteOptions {
* If true, the command will not be logged to the console
*/
silent?: boolean;

/**
* Exit the process if the command fails
*/
fatal?: boolean;
}
/**
* Executes a shell command and returns the result
*/
export function execute(command: string, options?: ExecuteOptions): string {

// default to silent
return shell.exec(command, { silent: options?.silent ?? true }).stdout;
let output: string;

try {
// default to silent
output = shell.exec(command, { silent: options?.silent ?? true, fatal: options?.fatal ?? false }).stdout;
} catch (error) {
if((error as Error).message.includes("Permission denied")){
throw new Error("Permission denied. Try running the command with sudo.");
}

throw error;
}

return output;

}

/**
Expand Down

0 comments on commit f76ae2c

Please sign in to comment.