Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .charlie/instructions/code-style.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,28 @@

## Scope

`scripts/git/*.sh`, `hooks/pre-commit`
`scripts/git/*.sh`, `hooks/*`, `.githooks/*`

## Context

- All scripts are pure bash targeting bash 4.0+; no Python, JS, or build system
- Scripts run on Linux, macOS, and Windows (Git Bash / WSL)
- `set -e` is intentionally omitted — git uses non-zero exit codes as signals, not errors

Check failure on line 14 in .charlie/instructions/code-style.md

View workflow job for this annotation

GitHub Actions / Markdown Linting

Line length

.charlie/instructions/code-style.md:14:81 MD013/line-length Line length [Expected: 80; Actual: 89] https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md013.md
- ShellCheck compliance is required; all scripts must pass without unacknowledged warnings

Check failure on line 15 in .charlie/instructions/code-style.md

View workflow job for this annotation

GitHub Actions / Markdown Linting

Line length

.charlie/instructions/code-style.md:15:81 MD013/line-length Line length [Expected: 80; Actual: 90] https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md013.md

## Rules

- [R1] Always use `set -uo pipefail` at the top of every script — never add `set -e`

Check failure on line 19 in .charlie/instructions/code-style.md

View workflow job for this annotation

GitHub Actions / Markdown Linting

Line length

.charlie/instructions/code-style.md:19:81 MD013/line-length Line length [Expected: 80; Actual: 84] https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md013.md
- [R2] Always source `_common.sh` (which sources `_config.sh`) — never source `_config.sh` directly

Check failure on line 20 in .charlie/instructions/code-style.md

View workflow job for this annotation

GitHub Actions / Markdown Linting

Line length

.charlie/instructions/code-style.md:20:81 MD013/line-length Line length [Expected: 80; Actual: 99] https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md013.md
- Pattern: `SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"` then `source "${SCRIPT_DIR}/_common.sh"`

Check failure on line 21 in .charlie/instructions/code-style.md

View workflow job for this annotation

GitHub Actions / Markdown Linting

Line length

.charlie/instructions/code-style.md:21:81 MD013/line-length Line length [Expected: 80; Actual: 114] https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md013.md
- [R3] Always use `[[ ... ]]` for conditionals — never `[ ... ]` or `test`
- [R4] Always use `"${var}"` expansion for variables — never bare `$var`
- [R5] Always use 2-space indentation — never tabs
- [R6] Always pass ShellCheck — add `# shellcheck disable=SCxxxx` with a one-line justification comment only when suppressing is unavoidable

Check failure on line 25 in .charlie/instructions/code-style.md

View workflow job for this annotation

GitHub Actions / Markdown Linting

Line length

.charlie/instructions/code-style.md:25:81 MD013/line-length Line length [Expected: 80; Actual: 140] https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md013.md
- [R7] Add `# shellcheck source=scripts/git/_config.sh` directive on source statements for sourced files

Check failure on line 26 in .charlie/instructions/code-style.md

View workflow job for this annotation

GitHub Actions / Markdown Linting

Line length

.charlie/instructions/code-style.md:26:81 MD013/line-length Line length [Expected: 80; Actual: 104] https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md013.md
- [R8] Never commit files listed in `CGW_LOCAL_FILES` (default: `CLAUDE.md MEMORY.md .claude/ logs/`)

Check failure on line 27 in .charlie/instructions/code-style.md

View workflow job for this annotation

GitHub Actions / Markdown Linting

Line length

.charlie/instructions/code-style.md:27:81 MD013/line-length Line length [Expected: 80; Actual: 101] https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md013.md
- [R9] Use forward slashes in all paths — never backslashes (bash on Windows requires forward slashes)

Check failure on line 28 in .charlie/instructions/code-style.md

View workflow job for this annotation

GitHub Actions / Markdown Linting

Line length

.charlie/instructions/code-style.md:28:81 MD013/line-length Line length [Expected: 80; Actual: 102] https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md013.md
- [R10] Use conventional commit prefixes: `feat|fix|docs|chore|test|refactor|style|perf` (extensible via `CGW_EXTRA_PREFIXES`)

Check failure on line 29 in .charlie/instructions/code-style.md

View workflow job for this annotation

GitHub Actions / Markdown Linting

Line length

.charlie/instructions/code-style.md:29:81 MD013/line-length Line length [Expected: 80; Actual: 126] https://github.com/DavidAnson/markdownlint/blob/v0.33.0/doc/md013.md
- [R11] Handle cross-platform venv: check `.venv/Scripts/` (Windows) before `.venv/bin/` (Unix) — see `get_python_path()` in `_common.sh`

## Examples
Expand Down
2 changes: 1 addition & 1 deletion .charlie/instructions/git-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ All git operations: commits, merges, pushes, cherry-picks, rollbacks.
- [R7] Wrap every logical operation in `log_section_start` / `log_section_end` for timing and audit trail
- [R8] Auto-detect non-interactive mode: set `CGW_NON_INTERACTIVE=1` when `[[ ! -t 0 ]]` (no TTY)
- [R9] Prefer `--force-with-lease` over `--force` for force-push operations
- [R10] Never bypass the pre-commit hook with `--no-verify`
- [R10] Never bypass git hooks (pre-commit, pre-push) with `--no-verify`

## Examples

Expand Down
61 changes: 61 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
# Pre-commit hook — blocks local-only files from being committed
# Generated by claude-git-workflow configure.sh
#
# Blocks ADDITIONS (A) and MODIFICATIONS (M) of local-only files.
# DELETIONS (D) are allowed (removing from git tracking is fine).
#
# The pattern below is populated by configure.sh from CGW_LOCAL_FILES.
# To regenerate after changing .cgw.conf, run: ./scripts/git/configure.sh --skip-skill

set -uo pipefail

echo "Checking for local-only files..."

PROBLEMATIC_FILES=$(git diff --cached --name-status | grep -E "^[AM][[:space:]]+(CLAUDE\.md|SESSION_LOG\.md|\.claude|logs)" || true)

if [[ -n "${PROBLEMATIC_FILES}" ]]; then
echo "ERROR: Attempting to add or modify local-only files!"
echo ""
echo "The following files must remain local only:"
echo "${PROBLEMATIC_FILES}"
echo ""
echo "DELETIONS are allowed (removing from git tracking)"
echo "ADDITIONS/MODIFICATIONS are blocked"
echo ""
echo "To fix: git reset HEAD <file>"
echo ""
exit 1
fi

DELETED_FILES=$(git diff --cached --name-status | grep -E "^D[[:space:]]+(CLAUDE\.md|SESSION_LOG\.md|\.claude|logs)" || true)
if [[ -n "${DELETED_FILES}" ]]; then
echo " Local-only files being removed from git tracking (allowed)"
fi

echo " No local-only files detected"

# Optional: lint check for staged files (non-blocking)
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)

if [[ -n "${STAGED_FILES}" ]]; then
# Python lint (ruff)
if command -v ruff > /dev/null 2>&1; then
PY_FILES=$(echo "${STAGED_FILES}" | grep '\.py$' || true)
if [[ -n "${PY_FILES}" ]]; then
echo ""
echo "Checking Python lint (non-blocking)..."
# shellcheck disable=SC2086
if ! ruff check ${PY_FILES} > /dev/null 2>&1; then
echo " WARNING: lint issues in staged Python files"
echo " Run 'ruff check --fix .' to auto-fix"
echo " (Commit proceeds — fix lint before pushing)"
else
echo " Lint check passed"
fi
fi
fi
fi

echo ""
exit 0
96 changes: 96 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# Pre-push hook — validates commits before they leave local repo
# Generated by claude-git-workflow configure.sh
#
# Checks all commits about to be pushed:
# 1. No local-only files tracked in any unpushed commit
# 2. All commit messages follow conventional format
#
# This complements push_validated.sh (CGW wrapper) by catching issues
# even when users bypass the wrapper and run git push directly.
# See Pro Git Ch8 p.362 — pre-push hook.
#
# The patterns below are populated by configure.sh from CGW_LOCAL_FILES
# and CGW_ALL_PREFIXES. To regenerate, run: ./scripts/git/configure.sh --skip-skill
#
# CONVENTIONAL PREFIXES: feat\|fix\|docs\|chore\|test\|refactor\|style\|perf
# LOCAL FILES PATTERN: CLAUDE\.md|SESSION_LOG\.md|\.claude|logs

set -uo pipefail

# ---------------------------------------------------------------------------
# Read stdin: remote <name>, url <url>, <local-ref> <local-sha> <remote-ref> <remote-sha>
# ---------------------------------------------------------------------------

LOCAL_FILES_PATTERN="CLAUDE\.md|SESSION_LOG\.md|\.claude|logs"
ALL_PREFIXES="feat\|fix\|docs\|chore\|test\|refactor\|style\|perf"

# Collect push info from stdin (git passes it to pre-push)
while read -r LOCAL_REF LOCAL_SHA REMOTE_REF REMOTE_SHA; do
# Skip deletions (empty sha = 0000...0)
if [[ "${LOCAL_SHA}" = "0000000000000000000000000000000000000000" ]]; then
continue
fi

# Determine commit range to check
if [[ "${REMOTE_SHA}" = "0000000000000000000000000000000000000000" ]]; then
# New branch being pushed — check all commits not in any remote branch
RANGE="${LOCAL_SHA}"
COMMITS=$(git rev-list "${RANGE}" --not --remotes 2>/dev/null || true)
else
RANGE="${REMOTE_SHA}..${LOCAL_SHA}"
COMMITS=$(git rev-list "${RANGE}" 2>/dev/null || true)
fi

[[ -z "${COMMITS}" ]] && continue

echo "pre-push: checking $(echo "${COMMITS}" | wc -l | tr -d ' ') commit(s) in ${LOCAL_REF}..."

# ── [1] Local-only file check ──────────────────────────────────────────
if [[ -n "${LOCAL_FILES_PATTERN}" ]]; then
for COMMIT in ${COMMITS}; do
FILES_IN_COMMIT=$(git diff-tree --no-commit-id -r --name-only "${COMMIT}" 2>/dev/null || true)
PROBLEM=$(echo "${FILES_IN_COMMIT}" | grep -E "${LOCAL_FILES_PATTERN}" || true)
if [[ -n "${PROBLEM}" ]]; then
echo ""
echo "ERROR [pre-push]: Commit ${COMMIT} contains local-only files:"
echo "${PROBLEM}" | sed 's/^/ /'
echo ""
echo "These files must not be pushed: ${LOCAL_FILES_PATTERN}"
echo "To fix: git rebase -i HEAD~N and drop/edit the offending commit"
echo ""
exit 1
fi
done
fi

# ── [2] Conventional commit format check ──────────────────────────────
if [[ -n "${ALL_PREFIXES}" ]]; then
for COMMIT in ${COMMITS}; do
MSG=$(git log -1 --format="%s" "${COMMIT}" 2>/dev/null || true)

# Skip merge commits (they often have non-conventional messages)
PARENT_COUNT=$(git cat-file -p "${COMMIT}" 2>/dev/null | grep -c "^parent " || true)
[[ "${PARENT_COUNT}" -ge 2 ]] && continue

if ! echo "${MSG}" | grep -qE "^(${ALL_PREFIXES}):"; then
echo ""
echo "WARNING [pre-push]: Commit ${COMMIT} has non-conventional message:"
echo " ${MSG}"
echo ""
echo "Expected format: <type>: <description>"
echo "Types: ${ALL_PREFIXES}"
echo ""
echo "Push blocked. Fix with: ./scripts/git/undo_last.sh amend-message '<type>: <msg>'"
echo "Or bypass (not recommended): git push --no-verify"
echo ""
exit 1
fi
done
fi

done

echo "pre-push: all checks passed"
echo ""
exit 0
112 changes: 109 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ claude-git-workflow\install.cmd
```bash
# 1. Copy scripts + hook template into your project
cp -r claude-git-workflow/scripts/git/ your-project/scripts/git/
mkdir -p your-project/hooks
cp claude-git-workflow/hooks/pre-commit your-project/hooks/
cp -r claude-git-workflow/hooks/ your-project/hooks/

# 2. Auto-configure (scans project, generates config, installs hooks + skill)
cd your-project && ./scripts/git/configure.sh
Expand Down Expand Up @@ -54,7 +53,17 @@ No manual config editing required for common setups. `configure.sh` auto-detects
| `check_lint.sh` | Read-only lint validation |
| `fix_lint.sh` | Auto-fix lint issues |
| `create_pr.sh` | Create GitHub PR from source → target (triggers Charlie CI + GitHub Actions) |
| `install_hooks.sh` | Install git pre-commit hooks |
| `install_hooks.sh` | Install git hooks (pre-commit + pre-push) |
| `setup_attributes.sh` | Generate `.gitattributes` for binary and text files (Python, TouchDesigner, GLSL, assets) |
| `clean_build.sh` | Safe cleanup of build artifacts with dry-run default (Python, TouchDesigner, GLSL) |
| `create_release.sh` | Create annotated version tags to trigger the GitHub Release workflow |
| `stash_work.sh` | Safe stash wrapper with untracked file support, named stashes, and logging |
| `repo_health.sh` | Repository health: integrity check, size report, large file detection, gc |
| `bisect_helper.sh` | Guided git bisect with backup tag, auto-detect good ref, automated test support |
| `rebase_safe.sh` | Safe rebase: backup tag, pushed-commit guard, abort/continue/skip, autostash |
| `branch_cleanup.sh` | Prune merged branches, stale remote-tracking refs, and old backup tags |
| `changelog_generate.sh` | Generate categorized markdown/text changelog from conventional commits |
| `undo_last.sh` | Undo last commit (keep staged), unstage files, discard changes, amend message |

Internal modules (not user-facing): `_common.sh` (shared utilities, sourced by every script), `_config.sh` (three-tier config resolution, sourced by `_common.sh`).

Expand Down Expand Up @@ -107,6 +116,9 @@ cp cgw.conf.example .cgw.conf
| `CGW_DEV_ONLY_FILES` | `` | Files to warn about in cherry-pick (space-separated) |
| `CGW_MERGE_MODE` | `direct` | Promotion mode: `direct` (merge locally) or `pr` (create GitHub PR) |
| `CGW_PROTECTED_BRANCHES` | `main` | Branches requiring `--force` for force-push |
| `CGW_LINT_EXTENSIONS` | `*.py` | File globs for `--modified-only` lint mode (e.g. `*.js *.ts`) |
| `CGW_MERGE_CONFLICT_STYLE` | `` | Set to `diff3` to show base version in conflict markers |
| `CGW_MERGE_IGNORE_WHITESPACE` | `0` | Set to `1` to ignore whitespace differences during merge |

---

Expand Down Expand Up @@ -211,6 +223,100 @@ Set `CGW_MERGE_MODE="pr"` in `.cgw.conf` to use PRs by default.
./scripts/git/cherry_pick_commits.sh --commit abc1234 # non-interactive
```

### Stash work in progress

```bash
./scripts/git/stash_work.sh push "wip: half-done refactor"
./scripts/git/stash_work.sh list
./scripts/git/stash_work.sh pop
./scripts/git/stash_work.sh apply stash@{1} # apply without removing
```

### Create a release

```bash
./scripts/git/create_release.sh v1.2.3 # tag only
./scripts/git/create_release.sh v1.2.3 --push # tag + push (triggers release.yml)
./scripts/git/create_release.sh v1.2.3 --dry-run # preview
```

### Configure .gitattributes (Python, TouchDesigner, GLSL)

```bash
./scripts/git/setup_attributes.sh --dry-run # preview
./scripts/git/setup_attributes.sh # write .gitattributes
```

### Clean build artifacts

```bash
./scripts/git/clean_build.sh # dry-run (safe preview)
./scripts/git/clean_build.sh --execute # actually delete
./scripts/git/clean_build.sh --td --execute # TouchDesigner artifacts only
```

### Repository health check

```bash
./scripts/git/repo_health.sh # integrity, size, large files
./scripts/git/repo_health.sh --gc # also run garbage collection
./scripts/git/repo_health.sh --large 5 # report files >5MB
```

### Undo last commit / unstage / amend

```bash
./scripts/git/undo_last.sh commit # undo last commit, keep changes staged
./scripts/git/undo_last.sh unstage src/file.py # remove file from staging area
./scripts/git/undo_last.sh discard src/file.py # discard working-tree changes (irreversible)
./scripts/git/undo_last.sh amend-message "fix: correct msg" # rewrite last commit message
```

Creates a backup tag before any destructive operation.

### Branch cleanup

```bash
./scripts/git/branch_cleanup.sh # dry-run preview (safe default)
./scripts/git/branch_cleanup.sh --execute # delete merged branches + prune remote refs
./scripts/git/branch_cleanup.sh --tags --execute # also remove old backup tags
./scripts/git/branch_cleanup.sh --older-than 30 --execute # only branches older than 30 days
```

### Safe rebase

```bash
./scripts/git/rebase_safe.sh --onto main # rebase current branch onto main
./scripts/git/rebase_safe.sh --squash-last 3 # interactive squash of last 3 commits
./scripts/git/rebase_safe.sh --squash-last 3 --autosquash # auto-apply fixup!/squash! prefixes
./scripts/git/rebase_safe.sh --abort # abort in-progress rebase
./scripts/git/rebase_safe.sh --continue # continue after resolving conflicts
```

Creates a backup tag (`pre-rebase-TIMESTAMP`) before rebasing. Warns if commits already pushed.

### Bisect a bug

```bash
# Automated: find first-bad commit using a test script
./scripts/git/bisect_helper.sh --good v1.0.0 --run "bash tests/smoke_test.sh"

# Manual: guided interactive bisect
./scripts/git/bisect_helper.sh --good v1.0.0
# → git bisect good / git bisect bad after each checkout

./scripts/git/bisect_helper.sh --abort # stop in-progress bisect session
```

### Generate changelog

```bash
./scripts/git/changelog_generate.sh # since latest semver tag → stdout
./scripts/git/changelog_generate.sh --from v1.0.0 # since specific tag
./scripts/git/changelog_generate.sh --from v1.0.0 --output CHANGELOG.md
./scripts/git/changelog_generate.sh --from v1.0.0 --format text # plain text
```

---

## Configuration Examples
Expand Down
31 changes: 31 additions & 0 deletions cgw.conf.example
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,37 @@ CGW_FORMAT_EXCLUDES="--exclude logs --exclude .venv"
# CGW_LINT_CMD=""
# CGW_FORMAT_CMD=""

# ============================================================================
# MODIFIED-ONLY LINT FILE EXTENSIONS (check_lint.sh, fix_lint.sh)
# ============================================================================
# Space-separated glob patterns controlling which files are matched in
# --modified-only mode. Override for non-Python projects.
#
# Default (Python):
CGW_LINT_EXTENSIONS="*.py"
#
# JavaScript / TypeScript:
# CGW_LINT_EXTENSIONS="*.js *.ts *.jsx *.tsx"
#
# Go:
# CGW_LINT_EXTENSIONS="*.go"
#
# C / C++:
# CGW_LINT_EXTENSIONS="*.c *.cpp *.h *.hpp"

# ============================================================================
# MERGE STRATEGY OPTIONS (merge_with_validation.sh)
# ============================================================================
# CGW_MERGE_CONFLICT_STYLE: Set to "diff3" to show the base version in conflict
# markers (recommended — makes manual resolution easier).
# Empty = git default two-way markers.
CGW_MERGE_CONFLICT_STYLE=""
# CGW_MERGE_CONFLICT_STYLE="diff3"

# CGW_MERGE_IGNORE_WHITESPACE: Set to "1" to add -Xignore-space-change to merge.
# Prevents false conflicts from whitespace-only differences (tabs/spaces, line endings).
CGW_MERGE_IGNORE_WHITESPACE="0"

# ============================================================================
# MARKDOWN LINT (commit_enhanced.sh, check_lint.sh, push_validated.sh)
# ============================================================================
Expand Down
Loading
Loading