Project Overview
gmash (Git Smash) provides a suite of Bash scripts that streamline common Git and GitHub repository operations. It standardizes workflows, reduces boilerplate, and exposes consistent version metadata across your projects.
What Is gmash?
gmash bundles environment setup, global variables, version metadata and helper functions into a lightweight CLI for:
- Repository creation and cloning
- Branching, syncing and pushing
- Automated version display
- GitHub interaction (PRs, issues, forks)
Why It Exists
Developers spend time writing repetitive Git commands and managing multiple remotes. gmash centralizes these tasks into reusable scripts, enforcing consistency and minimizing context‐switching.
Primary Capabilities
- Environment bootstrap with global variables (configured in
gmash-source/global.sh
) - Hierarchical version metadata display
- High‐level GitHub operations (init, fork, pull‐request)
- Customizable hooks for project‐specific workflows
Supported Platforms
- Any Unix‐like OS with Bash 4+ (Linux, macOS, WSL)
- Requires standard GNU utilities (
git
,curl
,sed
, etc.)
Licensing
gmash is licensed under the GNU Affero General Public License v3.0.
You may copy, modify, and distribute under AGPLv3 terms; network use triggers source availability requirements.
See LICENSE.txt for full details.
Quick Start Example
Clone the repository and verify the installed version:
# Clone gmash
git clone https://github.com/ayxg/gmash.git
cd gmash
# Add to PATH or invoke directly
chmod +x gmash
./gmash --version
# Output example:
# gmash version 0.4.1
# build: 2025-09-01
Ensure gmash-source
remains alongside the main script to load environment settings and version metadata.
Getting Started
Follow these steps to install gmash, compile its parsers, verify your setup, and run your first command.
Prerequisites
- Bash shell
- gengetopt (or gengetoptions) installed
- Git client
Verify gengetopt:
gengetopt --version
1. Clone the Repository
git clone https://github.com/ayxg/gmash.git
cd gmash
2. Add gmash to Your PATH
Add the project root to your PATH
so you can invoke gmash
from any location.
For a temporary session:
export PATH="$PWD:$PATH"
To make this permanent, append to your shell profile (~/.bashrc
, ~/.zshrc
, etc.):
echo 'export PATH="~/path/to/gmash:$PATH"' >> ~/.bashrc
source ~/.bashrc
3. Compile Parsers
gmash uses gengetopt to generate CLI parsers for its commands. Run the compile script:
bash gmash-source/gmash-compile.sh
This produces updated parser source files under gmash-source/parser/
and integrates them into the main dispatcher.
4. Verify Installation
Check that gmash
is on your PATH
and that parsers load correctly:
gmash --help
Expected output shows global options and a list of subcommands:
Usage: gmash [OPTIONS] COMMAND [ARGS]
Global options:
-h, --help Show help
-v, --version Show version
Commands:
dirs Manage repository directory shortcuts
find Search across repositories
gist Upload/downloader GitHub gists
lineage Display repository ancestry
mono Apply operations across many repos
subtree Operate on Git subtrees
5. Run Your First Command
List all configured directory shortcuts:
gmash dirs list
Search for the string “TODO” across all repos in your workspace:
gmash find --query TODO --path ~/projects
Both commands should execute without errors. You’re now ready to explore gmash’s full feature set!
Command Reference
This section describes every gmash sub-command, its flags, arguments, workflows and examples.
dirs prefix
Purpose
Add a specified prefix to each top-level file in a directory (or to a single file).
Synopsis
gmash dirs prefix --prefix <prefix> [--path <fileOrFolder>]
Options
-p, --prefix Prefix string to prepend (required)
-P, --path Target file or directory. Defaults to current working directory.
Environment Overrides
You can also set these via environment variables before invoking the command:
• GMASH_DIRS_PREFIX_PREFIX – equivalent to --prefix
• GMASH_DIRS_PREFIX_PATH – equivalent to --path
Behavior
- Validates that a non-empty prefix is provided.
- Verifies the target path exists.
- If the path is a file, renames only that file.
- If the path is a directory, renames every regular file at its top level.
- Emits errors on missing prefix/path or non-existent targets, returning non-zero.
Examples
Add “new_” to every file in the current directory:
gmash dirs prefix -p new_
Prefix a single file:
gmash dirs prefix --prefix v2_ --path ./release.tar.gz
Use environment variables instead of flags:
export GMASH_DIRS_PREFIX_PREFIX="backup_"
export GMASH_DIRS_PREFIX_PATH="/var/logs"
gmash dirs prefix
Verbose Output
If GMASH_VERBOSITY is set to “info” or higher, you’ll see steps logged via vecho_* calls:
• Input arguments
• Each rename action
• Final “Success.” message
Failure Cases
• Missing --prefix: prints “--prefix argument is required.”
• Non-existent path: prints “Target path ‘…’ does not exist.”
• Other filesystem errors: reported by mv(1) and wrapped by vecho_warn.
gmash find duplicate-code
Purpose
Locate duplicated C++ code blocks in a directory tree using Simian, helping enforce DRY principles.
Synopsis
gmash find duplicate-code [-p <path>] [-t <threshold>]
Description
- Scans all
*.h
,*.cpp
,*.hpp
files under the target directory. - Reports duplicated code sequences at or above the given line count.
Defaults:
•--path
→ current working directory
•--threshold
→ 10 lines
Parameters
-p, --path <path>
Directory root to scan. If omitted, uses$(pwd)
or$GMASH_FIND_DUPLICATE_CODE_PATH
.-t, --threshold <count>
Minimum matching lines to flag. Defaults to 10 or$GMASH_FIND_DUPLICATE_CODE_THRESHOLD
.
Environment Overrides
- Export
GMASH_FIND_DUPLICATE_CODE_PATH
/GMASH_FIND_DUPLICATE_CODE_THRESHOLD
to set defaults without CLI flags. - ARGV variables (
ARGV_PATH
,ARGV_THRESHOLD
) take precedence when functions are sourced directly.
Usage Examples
Standard invocation:
gmash find duplicate-code -p /home/alice/projects/lib -t 15
→ Scans /home/alice/projects/lib
, flagging duplicates ≥ 15 lines.
Using environment vars:
export GMASH_FIND_DUPLICATE_CODE_PATH=~/workspace/foo
export GMASH_FIND_DUPLICATE_CODE_THRESHOLD=20
gmash find duplicate-code
→ Equivalent to gmash find duplicate-code -p ~/workspace/foo -t 20
.
Direct function call (sourced context):
# inside a custom script after sourcing gmash:
gmash_find_duplicate_code "/opt/src" "25"
Implementation Snippet
gmash_find_duplicate_code(){
local _path=${ARGV_PATH:-$1}
local _threshold=${ARGV_THRESHOLD:-$2}
local _cpp_extensions="./*.h ./*.cpp ./*.hpp"
[ -z "$_threshold" ] && {
vecho_info "Defaulting --threshold to 10"
_threshold=10
}
if [ -n "$_path" ]; then
cd "$_path" || exit
_path_changed=1
else
vecho_info "Defaulting --path to current."
_path="$(pwd)"
fi
vecho_info "--path: $_path --threshold: $_threshold"
java -jar /c/lib/simian-4.0.0/simian-4.0.0.jar \
$_cpp_extensions -threshold="$_threshold" -formatter=plain -language=cpp
[ -n "$_path_changed" ] && cd - >/dev/null
vecho_done "Scan complete."
}
Practical Tips
- Ensure Java 8+ is installed and
simian-4.0.0.jar
is accessible at/c/lib/...
. - Use
-formatter=plain
output to pipe into CI logs or custom parsers. - Adjust extensions or include/exclude patterns by modifying
_cpp_extensions
or adding Simian options.
Grouping Files by Base Name (--no-extension
)
Purpose
When using gmash gist upload
in “all” mode (-A, --all
), the --no-extension
flag groups files sharing the same base name (ignoring extensions) into one gist. Ideal for multi-language modules (e.g., .cpp
+ .hpp
).
How It Works
- Scans each directory passed via
-f
/--file
. - Builds an associative array
file_map
keyed by basename without extension. - Appends matching files under each key.
- Creates one gist per key containing all associated files.
Relevant Snippet from gist.sh
elif [ "$_noextension" == 1 ]; then
declare -A file_map
for folder in "${_files[@]}"; do
for f in "$folder"/*; do
base_name=$(basename "$f")
name_no_ext="${base_name%%.*}"
file_map["$name_no_ext"]+=$'\n'"$f"
vecho_action "Mapping gist source: '$f' -> '$name_no_ext'"
done
done
for name_no_ext in "${!file_map[@]}"; do
vecho_process "Creating gist: $name_no_ext"
mapfile -t Z_GMASH_GIST_UPLOAD_SINGLE_FILES <<< "${file_map[$name_no_ext]}"
z_gmash_gist_upload_single \
"<title>" \
"$name_no_ext" \
"<readme>" \
"<description>" \
"<clonePath>" \
"<user>" \
"<no-readme>" \
"<no-title>" \
"<public>"
vecho_action "Finished gist: $name_no_ext"
Z_GMASH_GIST_UPLOAD_SINGLE_FILES=()
done
fi
Practical Usage
Assume two dirs lib1/
and lib2/
, each with:
foo.cpp
,foo.hpp
bar.cpp
,bar.hpp
To create one gist per module:
gmash gist upload \
-A \
-e \
-f lib1 \
-f lib2 \
-p ~/gists \
-n "project-modules" \
-d "Core modules without extensions"
Outcome:
- Gist
foo
with both.cpp
and.hpp
, cloned to~/gists/foo
- Gist
bar
likewise
Tips & Edge Cases
- Files with no dot group under their full name.
- Same basename in different folders merge into one gist.
- GMash defaults to secret gists; add
-P/--public
for visibility. - Clone dir names after basename (e.g.,
~/gists/foo
).
gmash lineage merge
Purpose
Merge git commits from multiple repositories into one linear history by grafting each project’s first commit onto the previous project’s last commit.
Configuration
• CURRENT_REPO, OLD1…OLD4 – paths or URLs in chronological order
• WORKDIR – target directory for merged-history clone
Key Implementation (lineage.sh
)
gmash_lineage_merge(){
# 1. Clean slate
rm -rf "$WORKDIR"
git clone "$CURRENT_REPO" "$WORKDIR"
cd "$WORKDIR"
# 2. Graft helper
function graft_repo {
local NAME=$1 URL=$2 PREV_LAST=$3
git remote add "$NAME" "$URL"
git fetch "$NAME"
NEW_FIRST=$(git log "$NAME/master" --reverse --format="%H" | head -n1)
NEW_LAST=$(git log "$NAME/master" --format="%H" | head -n1)
if [ -n "$PREV_LAST" ]; then
git replace --graft "$NEW_FIRST" "$PREV_LAST"
git filter-repo --replace-refs update-no-add
git diff "$PREV_LAST" "$NEW_FIRST" || true
fi
echo "$NEW_LAST"
}
# 3. Chain repos
LAST=$(graft_repo old1 "$OLD1" "")
LAST=$(graft_repo old2 "$OLD2" "$LAST")
LAST=$(graft_repo old3 "$OLD3" "$LAST")
LAST=$(graft_repo old4 "$OLD4" "$LAST")
# 4. Graft current onto last old
FIRST_CURRENT=$(git log main --reverse --format="%H" | head -n1)
git replace --graft "$FIRST_CURRENT" "$LAST"
git filter-repo --replace-refs update-no-add
git diff "$LAST" "$FIRST_CURRENT" || true
# 5. Inspect
git log --oneline --graph --decorate --all
}
Practical Usage
- Install dependencies:
• git (≥ 2.8)
• git-filter-repo - Set variables and run:
export CURRENT_REPO=/home/dev/projects/main.git export OLD1=/home/dev/archives/v0.git export OLD2=/home/dev/archives/v1.git export OLD3=/home/dev/archives/v2.git export OLD4=/home/dev/archives/v3.git export WORKDIR=./merged-history gmash lineage merge
- Review
merged-history
for a single, linear commit sequence. - To remove grafts:
cd merged-history git replace -d <commit-sha> git filter-repo --replace-refs clean
gmash mono patch
Purpose
Push commits from a monorepo into a Git subtree repo, attempting fast-forward first and falling back to a 3-way sync. Automatically handles conflicts or can open a PR on failure.
Synopsis
gmash mono patch [--user
Required parameters
• --user
, -u
GitHub user/org owning both repos
• --remote
, -r
Git remote alias for the subtree (unless --all
)
• --branch
, -b
Source mono branch (defaults to main
)
Optional parameters
• --tgtuser
Target owner (defaults to --user
)
• --tgtbr
Target branch in subtree (defaults to main
)
• --path
Subtree path in mono (defaults to projects/<remote>
)
• --all
Patch all configured subtrees
• --make-pr
On non-FF conflicts, fork & open a PR
• --squash
Squash mono commits into one before pushing
• --tempbr
Name of temporary sync branch
• --tempdir
Directory for temporary worktree
Fast-forward workflow
git subtree pull --prefix=<path> <remote> <tgtbr>
- If new commits are pulled,
git pull && git push
on mono - Compare
HEAD:<path>
vs<remote>/<tgtbr>
; if no diff, finish
3-way sync workflow on divergence
- Create detached worktree in
--tempdir
from mono branch - In worktree:
•git subtree split --prefix=<path> --branch=<tempbr>
•git checkout <tempbr>
•git fetch <remote> <tgtbr>
•git merge <remote>/<tgtbr> --allow-unrelated-histories -m "<merge-msg>"
•git push <remote> <tempbr>:<tgtbr>
- Back in mono:
•git worktree remove --force <tempdir>
•git merge -s subtree FETCH_HEAD -m "<sync-msg>"
•git push
Examples
Patch a single subtree “foo” from main
:
cd /path/to/mono-repo
git add . && git commit -am "API fix in foo module"
git push origin main
gmash mono patch \
--user acpp \
--remote foo \
--branch main \
--tgtbr main \
--path projects/foo
Force a PR on conflicts:
gmash mono patch -u acpp -r foo -b feature-x --make-pr
Patch every subtree in .git/config
:
gmash mono patch --user acpp --all
Practical tips
• Always commit & push mono changes before running mono patch
.
• Use --squash
to collapse commits into a single subtree update.
• Inspect git log <remote>/<tgtbr> --oneline
before patching.
• On persistent conflicts, rerun with --make-pr
and resolve via GitHub.
gmash subtree new: Adding a Subtree
Purpose
Import an external Git repo into your monorepo as a subtree, preserving history and automating branch and remote setup.
Essential Functionality
- Validates you’re on the target branch (
--tgtbr
, defaultmain
) - Computes defaults:
• name ←--remote
•--tgtuser
←--user
•--br
,--tgtbr
←main
•--path
←projects/<name>
- Locates or creates the subtree repo on GitHub (
gh repo create
) - Verifies uniqueness of remote alias, URL and path
- Runs, in order:
git remote add -f <remote> <url>
git subtree add --prefix=<path> <url> <tgtbr>
git subtree pull --prefix=<path> <remote> <tgtbr>
git push
Command-Line Usage
# Minimum required flags:
gmash subtree new \
--remote foo-box \
--user SophiaSGS \
--repo SophiaSGS/monorepo \
--branch main
# Full invocation with custom target:
gmash subtree new \
--remote foo-box \
--name foo_box \
--path submodules/foo_box \
--user SophiaSGS \
--tgtuser MyOrg \
--branch develop \
--tgtbr release
Under-the-Hood Snippet
# 1. Branch guard
[ "$(git rev-parse --abbrev-ref HEAD)" = "release" ] || {
echo "Error: must be on target branch 'release'" >&2
exit 1
}
# 2. Create or detect GitHub repo
if ! git ls-remote "https://github.com/MyOrg/foo_box.git" &>/dev/null; then
gh repo create MyOrg/foo_box --private --add-readme \
--description "[gmash] Generated subtree foo_box"
fi
# 3. Add remote, subtree and push
git remote add -f foo-box https://github.com/MyOrg/foo_box.git
git subtree add --prefix=submodules/foo_box https://github.com/MyOrg/foo_box.git release
git subtree pull --prefix=submodules/foo_box foo-box release
git push
Practical Guidance
• Install and authenticate GitHub CLI (gh
) for automatic repo creation.
• Ensure target --path
does not exist or is empty and isn’t git-ignored.
• Use a unique --remote
alias to avoid collisions.
• Omit --url
to let gmash infer or provision the repo; specify explicitly if managed elsewhere.
• Inspect the merge commit to confirm subtree history preservation.
Configuration & Environment
gmash uses command-line flags and environment variables to control logging, GitHub authentication, and internal paths. This section covers the global flags, supported environment variables, and key default paths exposed by gmash.
Global Flags
All gmash commands accept these flags:
• --verbose
, -v
Enable verbose logging for the current invocation (equivalent to GMASH_VERBOSE=1
).
• --version
Print version metadata (version, release date, commit hash) and exit.
• --help
, -h
Display command usage and exit.
Example
# Clone a repo with extra debug output
gmash --verbose clone my-org/my-repo
# Check installed gmash version
gmash --version
Environment Variables
Set these variables in your shell to configure gmash globally:
• GMASH_VERBOSE
When non-zero, gmash prints debug and informational messages via common.sh utilities.
• GITHUB_TOKEN
(or GH_TOKEN
)
Your GitHub personal access token. Required for operations on private repos and GitHub API calls.
Example
# Turn on verbose logging by default
export GMASH_VERBOSE=1
# Authenticate all GitHub API requests
export GITHUB_TOKEN=ghp_yourpersonalaccesstoken
# Now run any gmash command without --verbose
gmash clone private-org/secret-repo
Default Paths & Globals
global.sh defines these read-only variables for internal use and scripting extension:
• GMASH_ROOT
Absolute path to the gmash installation root (one level above gmash-source
).
• GMASH_SOURCE_DIR
$GMASH_ROOT/gmash-source
– directory containing shared scripts (common.sh, global.sh, etc.).
• GMASH_VERSION
, GMASH_RELEASE_DATE
, GMASH_COMMIT_HASH
Version metadata injected at build time.
You can inspect or override these if you embed gmash functionality into custom scripts.
Inspecting Paths
# Print the gmash installation root
bash -c 'source /usr/local/bin/gmash && echo $GMASH_ROOT'
# Print the shared scripts directory
bash -c 'source /usr/local/bin/gmash && echo $GMASH_SOURCE_DIR'
Overriding GMASH_ROOT
# Point gmash to a custom install location
export GMASH_ROOT="$HOME/.local/share/gmash"
gmash --version
## Development & Contribution Guide
This guide orients new contributors through the gmash codebase: directory structure, command dispatch, parser generation workflow, parser extensions, and external dependencies.
### Project Directory Layout
gmash-source/ ├── command/ # Top-level and subcommand implementations (e.g. gist.sh, find.sh) ├── def-parser/ # getoptions definition files (one per command group) ├── parser/ # Generated parser functions (gmash_parser_*.sh) ├── parser-extensions.sh # Helper functions to extend getoptions parsers ├── dispatch-parsers.sh # Central dispatcher (gmash_dispatch_parsers) └── gmash-compile.sh # Automates parser compilation via gengetoptions
### Modular Command Architecture
`dispatch-parsers.sh` defines the central CLI router:
```bash
gmash_dispatch_parsers(){
local _cmd=$1; shift
case $_cmd in
gist)
gmash_parser_gist "$@"
eval "set -- $GMASH_ARGR"
local _sub=$1; shift
case $_sub in
prepare)
cmd_parser_gist_prepare "$@"; cmd_call_gist_prepare ;;
clone)
cmd_parser_gist_clone "$@" ;;
*)
echo "$GMASH_DISPATCH_PARSERS_INVALID_SUBCMD"; return 1 ;;
esac
;;
find)
gmash_parser_find "$@"; … ;;
*)
echo "$GMASH_DISPATCH_PARSERS_INVALID_CMD"; return 1 ;;
esac
return 0
}
Practical invocation:
# Clone a gist with ID and output directory
gmash_dispatch_parsers gist clone --id 12345 --output-dir ./mygist
To add a new sub-command under find
, edit its nested case
in dispatch-parsers.sh
, add:
…
find)
gmash_parser_find "$@"; eval "set -- $GMASH_ARGR"
local _sub=$1; shift
case $_sub in
newsub)
cmd_parser_find_newsub "$@"; cmd_call_find_newsub ;;
*)
echo "$GMASH_DISPATCH_PARSERS_INVALID_SUBCMD"; return 1 ;;
esac
;;
…
Parser Generation Workflow
gmash-compile.sh
uses gengetoptions
to transform definition scripts into parser functions.
compile_parser Helper
compile_parser(){
local _cmd=$1 _sub=$2
local name=${_cmd:-main}
[ -n "$_sub" ] && name="${_cmd}_${_sub}"
local src=${_cmd:-main}
local out="$GMASH_PARSERS_BIN/$src.sh"
local redirect=(> "$out")
[ -n "$_sub" ] && redirect=(>> "$out")
echo "Compiling parser $name …"
gengetoptions parser \
-f "$GMASH_PARSERS_SRC/$src.sh" \
"gmash_def_parser_$name" \
"gmash_parser_$name" \
"${redirect[@]}"
echo "Done."
}
-f
points todef-parser/<command>.sh
.- Generates
gmash_def_parser_<name>
andgmash_parser_<name>
. - Main parser overwrites (
>
), sub-parsers append (>>
).
Definition File Conventions
In gmash-source/def-parser/<command>.sh
:
#!/bin/bash
source ../parser-extensions.sh
extend_parser
standard_parser_setup gmash_gist_help "Manage GitHub gists" \
"Usage: gmash gist [options] [args]"
standard_parser_help gmash_gist_help
# Repeatable “array” param
array FILE -f --file init:'FILES=()' var:"<path>" \
-- "Add a file to the gist (repeatable)"
flag VERBOSE -v --verbose "Show verbose output"
param ID -i --id "Gist identifier"
disp :usage -h --help
# Build the parser
eval "$(getoptions parser_definition - "$0")"
Regenerating Parsers
- Rebuild all parsers:
./gmash-compile.sh
- Rebuild one command group:
# In gmash-compile.sh, uncomment: compile_parser gist compile_parser gist clone
- Main parser:
compile_parser
with no args →gmash_parser_main
.
Integration in Entry Point
#!/bin/bash
source parser/main.sh
source parser/gist.sh
# … other parser sources …
gmash_dispatch_parsers "$@"
Parser Extensions (parser-extensions.sh
)
Provides functions to enrich getoptions
:
extend_parser
standard_parser_setup <helpFunc> <title> <usage>
standard_parser_help <helpFunc>
flag
,param
,option
,array
,disp
Example usage in a custom script:
#!/bin/bash
source parser-extensions.sh
extend_parser
standard_parser_setup example_help "Example Tool" "Usage: example [options] args"
flag DEBUG -d --debug "Enable debug mode"
array ITEMS -I --item init:'ITEMS=()' var:"<value>" -- "Collect multiple items"
disp :usage -h --help
eval "$(getoptions parser_definition - "$0")" exit 1
parse "$@"
External Dependencies
- Bash (POSIX-compatible features)
- gengetoptions: generates parser code from definition files
- getoptions: runtime library shipped with gengetoptions
- Standard Unix tools (
awk
,sed
, etc.)
Ensure gengetoptions
is installed and on your PATH
before running gmash-compile.sh
.