Skip to content

fix: install.cmd bug fixes, test suite gap closure, and UX improvements#3

Merged
forkni merged 8 commits intomainfrom
development
Apr 9, 2026
Merged

fix: install.cmd bug fixes, test suite gap closure, and UX improvements#3
forkni merged 8 commits intomainfrom
development

Conversation

@forkni
Copy link
Copy Markdown
Owner

@forkni forkni commented Apr 9, 2026

Changes

  • a4e114e fix: add readline (-e) to free-text read prompts so arrow keys work
  • f767ca3 fix: configure.sh no longer modifies .gitignore; preserves branch settings on reconfigure
  • 8433bf3 test: add integration tests for 10 previously uncovered scripts; fix branch_cleanup exit code
  • 515fee0 fix: resolve CGW_ALL_PREFIXES unbound variable in configure.sh pre-push hook install
  • 4a23beb docs: align all documentation and skill with current codebase (25 scripts, pre-push hook)
  • 4d32ece feat: add branch_cleanup, changelog_generate, undo_last, pre-push hook (Pro Git audit)
  • 8a9a4d7 feat: add bisect_helper.sh and rebase_safe.sh (Pro Git Ch3/Ch7)

Branch

developmentmain

@charliecreates charliecreates Bot requested a review from CharlieHelps April 9, 2026 19:00
Copy link
Copy Markdown

@charliecreates charliecreates Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several hook files (.githooks/*) violate the repository’s shell standards (not bash, missing set -uo pipefail, using [ ... ]), and pre-commit contains a portability/correctness bug by using \s in an ERE grep. changelog_generate.sh currently produces incorrect output for --to (and effectively ignores merge-inclusion settings) due to _build_changelog() rebuilding the range as ..HEAD. branch_cleanup.sh can mis-handle detached-HEAD output due to parsing git branch formatting instead of using plumbing-friendly refs. The modified-only lint argument handling is configuration-fragile and the new ShellCheck suppressions don’t meet the repo’s suppression-justification rule.

Additional notes (2)
  • Maintainability | scripts/git/check_lint.sh:105-105
    --modified-only now strips the last token from CGW_LINT_CHECK_ARGS / CGW_FORMAT_CHECK_ARGS via ${var% *} under the assumption that the last token is always a path (like .). That’s fragile:

  • If the configured args don’t end in a path token, you’ll silently drop a real option.

  • If the args are a single word, % * returns the whole string (fine), but the behavior becomes configuration-dependent and hard to reason about.

Also: the # shellcheck disable=SC2086 suppressions don’t include the required one-line justification (per .charlie/instructions/code-style.md [R6]).

  • Readability | scripts/git/commit_enhanced.sh:215-228
    Behavior/prompt mismatch: the interactive prompt says “Stage all changes?” but the implementation is now git add -u (tracked-only). That will not stage new files, which can easily lead to incomplete commits and confusion.

If the intent is “avoid accidentally adding local-only files”, you already call unstage_local_only_files, so you can keep the UX of “all changes” while still protecting local-only paths.

Summary of changes

Summary

This PR significantly expands claude-git-workflow with new scripts, hook support, docs, and tests.

Hooking & installation

  • Adds pre-push support alongside pre-commit:
    • New hook templates under hooks/ and tracked hooks under .githooks/.
    • configure.sh now generates .githooks/pre-push and install_hooks.sh installs both hooks.
    • install.cmd updated to copy/validate hooks/pre-push and back up existing hook templates.

New git utility scripts

  • Adds several new workflow scripts:
    • History/safety: rebase_safe.sh, bisect_helper.sh, undo_last.sh
    • Maintenance: branch_cleanup.sh, repo_health.sh, clean_build.sh, setup_attributes.sh
    • Release/changelog: create_release.sh, changelog_generate.sh
    • Convenience: stash_work.sh

Config + behavior changes

  • _common.sh logging now uses ${PROJECT_ROOT}/logs so logs go to the repo root regardless of CWD.
  • _config.sh introduces new knobs: CGW_LINT_EXTENSIONS, CGW_MERGE_CONFLICT_STYLE, CGW_MERGE_IGNORE_WHITESPACE.
  • check_lint.sh / fix_lint.sh now support modified-only linting via CGW_LINT_EXTENSIONS.
  • merge_with_validation.sh now applies optional merge strategy flags.
  • commit_enhanced.sh stages tracked-only changes via git add -u and adds a non-blocking whitespace check.

Docs + tests

  • README and skill docs updated to include new scripts/features.
  • Adds a broad set of new Bats integration tests covering new and previously untested scripts.

Comment thread .githooks/pre-commit Outdated
Comment on lines +1 to +35
#!/bin/sh
# 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

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

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

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\s+(CLAUDE\.md|SESSION_LOG\.md|\.claude|logs)")
if [ -n "$DELETED_FILES" ]; then
echo " Local-only files being removed from git tracking (allowed)"
fi

echo " No local-only files detected"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hook violates the repo’s shell scripting instructions and also has a correctness bug in its regex.

  • Instruction violations:
    • .charlie/instructions/code-style.md [R1]: missing set -uo pipefail
    • [R3]: uses [ ... ] instead of [[ ... ]]
    • [R5]: indentation uses 4 spaces (and overall inconsistent with the 2-space rule)
    • Scope says hooks are bash; this is #!/bin/sh
  • Correctness: grep -E does not support \s portably in ERE, so patterns like "^[AM]\s+..." are likely to never match (or to behave inconsistently across platforms).
Suggestion

Convert this to the project-standard bash hook style and fix the whitespace matching. For example:

#!/usr/bin/env bash
set -uo pipefail

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

if [[ -n "${problematic_files}" ]]; then
  echo "ERROR: Attempting to add or modify local-only files!" >&2
  echo "${problematic_files}" >&2
  exit 1
fi

Also consider anchoring directory matches to avoid false positives (e.g. (^|/)(\.claude/|logs/)). Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion.

Comment thread .githooks/pre-push Outdated
Comment on lines +1 to +90
#!/bin/sh
# 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: __CGW_ALL_PREFIXES__
# LOCAL FILES PATTERN: __CGW_LOCAL_FILES_PATTERN__

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

LOCAL_FILES_PATTERN="__CGW_LOCAL_FILES_PATTERN__"
ALL_PREFIXES="__CGW_ALL_PREFIXES__"

# 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hook has the same baseline issues as the pre-commit hook (scope/rules mismatch), plus it currently uses tabs for indentation.

  • Instruction violations (per .charlie/instructions/code-style.md): [R1] (missing set -uo pipefail), [R3] ([ ... ]), [R5] (tabs), and the hook is #!/bin/sh even though the hook/shell standards in this repo are bash-centric.
  • Portability: relying on /bin/sh while using bash-leaning conventions elsewhere increases platform variability (especially on macOS where /bin/sh is dash-like behavior differs).
Suggestion

Standardize the hook to the same bash+strict-mode baseline used everywhere else in this repo, and replace tab indentation with 2 spaces. Mechanically:

  1. Change shebang to #!/usr/bin/env bash
  2. Add set -uo pipefail
  3. Replace [ ... ] with [[ ... ]]
  4. Re-indent with 2 spaces

Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion.

Comment on lines +109 to +247
# Determine git log range
local log_range
if [[ -n "${from_ref}" ]]; then
log_range="${from_ref}..${to_ref}"
else
log_range="${to_ref}"
fi

# Get to_ref description for header
local to_desc
to_desc=$(git describe --tags --exact-match "${to_ref}" 2>/dev/null || \
git log -1 --format="%h" "${to_ref}" 2>/dev/null || echo "${to_ref}")
local to_date
to_date=$(git log -1 --format="%ad" --date=short "${to_ref}" 2>/dev/null || date +%Y-%m-%d)

# Collect commits in range
local merge_flag="--no-merges"
[[ ${include_merges} -eq 1 ]] && merge_flag=""

# shellcheck disable=SC2086 # merge_flag intentionally word-splits when empty
local commits
commits=$(git log ${merge_flag} --format="%H|%s|%b" "${log_range}" 2>/dev/null || true)

if [[ -z "${commits}" ]]; then
echo "No commits found in range: ${log_range}" >&2
exit 0
fi

# Categorize commits by conventional type
# Categories: feat, fix, docs, perf, refactor, style, test, chore, other
local -a cat_feat=() cat_fix=() cat_docs=() cat_perf=()
local -a cat_refactor=() cat_style=() cat_test=() cat_chore=() cat_other=()

while IFS='|' read -r hash subject body; do
[[ -z "${hash}" ]] && continue

local prefix rest
if echo "${subject}" | grep -qE "^[a-zA-Z]+:"; then
prefix=$(echo "${subject}" | sed 's/:.*//')
rest=$(echo "${subject}" | sed 's/^[^:]*: *//')
else
prefix="other"
rest="${subject}"
fi

# Get short hash and PR reference if any
local short_hash
short_hash=$(git log -1 --format="%h" "${hash}" 2>/dev/null || echo "${hash:0:7}")

local entry="${rest} (${short_hash})"

case "${prefix}" in
feat) cat_feat+=("${entry}") ;;
fix) cat_fix+=("${entry}") ;;
docs) cat_docs+=("${entry}") ;;
perf) cat_perf+=("${entry}") ;;
refactor) cat_refactor+=("${entry}") ;;
style) cat_style+=("${entry}") ;;
test) cat_test+=("${entry}") ;;
chore) cat_chore+=("${entry}") ;;
*) cat_other+=("${entry}") ;;
esac
done <<< "${commits}"

# Build output
local output=""
output=$(_build_changelog \
"${output_format}" "${to_desc}" "${to_date}" "${from_ref}" \
"${cat_feat[@]+"${cat_feat[@]}"}" \
"BREAK_FEAT" \
"${cat_fix[@]+"${cat_fix[@]}"}" \
"BREAK_FIX" \
"${cat_docs[@]+"${cat_docs[@]}"}" \
"BREAK_DOCS" \
"${cat_perf[@]+"${cat_perf[@]}"}" \
"BREAK_PERF" \
"${cat_refactor[@]+"${cat_refactor[@]}"}" \
"BREAK_REFACTOR" \
"${cat_style[@]+"${cat_style[@]}"}" \
"BREAK_STYLE" \
"${cat_test[@]+"${cat_test[@]}"}" \
"BREAK_TEST" \
"${cat_chore[@]+"${cat_chore[@]}"}" \
"BREAK_CHORE" \
"${cat_other[@]+"${cat_other[@]}"}" \
2>/dev/null || true)

# Write output
if [[ -n "${output_file}" ]]; then
echo "${output}" >"${output_file}"
echo "✓ Changelog written to: ${output_file}" >&2
else
echo "${output}"
fi
}

_build_changelog() {
local fmt="$1" version="$2" date_str="$3" from_ref="$4"
shift 4

# Parse the positional args back into categories using BREAK_ sentinel tokens
local current_cat="feat"
declare -A cats
cats[feat]="" cats[fix]="" cats[docs]="" cats[perf]=""
cats[refactor]="" cats[style]="" cats[test]="" cats[chore]="" cats[other]=""

# Re-collect — simpler: re-run git log with format per category
# (The sentinel approach doesn't work cleanly with arrays passed through positional args)
# Instead, recollect directly here.
local merge_flag="--no-merges"

local log_range
if [[ -n "${from_ref}" ]]; then
log_range="${from_ref}..HEAD"
else
log_range="HEAD"
fi

# Read commits again (simpler than passing arrays through function args)
while IFS='|' read -r hash subject; do
[[ -z "${hash}" ]] && continue
local prefix rest short_hash
prefix=$(echo "${subject}" | grep -oE "^[a-zA-Z]+" || echo "other")
rest=$(echo "${subject}" | sed 's/^[^:]*: *//')
short_hash=$(git log -1 --format="%h" "${hash}" 2>/dev/null || echo "${hash:0:7}")
local entry=" - ${rest} (${short_hash})"
case "${prefix}" in
feat) cats[feat]+="${entry}"$'\n' ;;
fix) cats[fix]+="${entry}"$'\n' ;;
docs) cats[docs]+="${entry}"$'\n' ;;
perf) cats[perf]+="${entry}"$'\n' ;;
refactor) cats[refactor]+="${entry}"$'\n' ;;
style) cats[style]+="${entry}"$'\n' ;;
test) cats[test]+="${entry}"$'\n' ;;
chore) cats[chore]+="${entry}"$'\n' ;;
*) cats[other]+="${entry}"$'\n' ;;
esac
done < <(git log --no-merges --format="%H|%s" "${log_range}" 2>/dev/null || true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changelog_generate.sh computes log_range using --from/--to, but _build_changelog() ignores --to entirely and re-reads commits using from_ref..HEAD. That makes --to <ref> incorrect and also makes --include-merges ineffective (the rebuild hardcodes --no-merges).

This isn’t just cleanup debt—the output can be wrong for releases generated against non-HEAD refs.

Suggestion

Fix _build_changelog() to use the already computed range and options instead of re-reading HEAD. Two viable approaches:

  • Preferred: build the final output directly in main() from the categorized arrays you already collected (no second git log).
  • Or: pass to_ref + include_merges into _build_changelog() and use log_range="${from_ref}..${to_ref}" (not ..HEAD), and respect --include-merges.

Reply with "@CharlieHelps yes please" if you want me to add a commit that refactors this to a single-pass implementation.

Comment thread scripts/git/configure.sh
Comment on lines +322 to +343
# Also install pre-push hook if template exists alongside pre-commit
local pre_push_template="${hooks_template_dir}/pre-push"
if [[ -f "${pre_push_template}" ]]; then
# Build CGW_ALL_PREFIXES for substitution into pre-push template.
# Can't source _config.sh here (see top-of-file comment), so compute locally
# by reading CGW_EXTRA_PREFIXES from the just-written .cgw.conf.
local _base_prefixes="feat|fix|docs|chore|test|refactor|style|perf"
local _extra_prefixes
_extra_prefixes=$(grep -m1 '^CGW_EXTRA_PREFIXES=' "${PROJECT_ROOT}/.cgw.conf" \
| sed 's/CGW_EXTRA_PREFIXES=//;s/"//g' || true)
local _all_prefixes
if [[ -n "${_extra_prefixes}" ]]; then
_all_prefixes="${_base_prefixes}|${_extra_prefixes}"
else
_all_prefixes="${_base_prefixes}"
fi
local all_prefixes_escaped="${_all_prefixes//|/\\|}"
sed -e "s|__CGW_LOCAL_FILES_PATTERN__|${sed_files_pattern}|g" \
-e "s|__CGW_ALL_PREFIXES__|${all_prefixes_escaped}|g" \
"${pre_push_template}" >"${PROJECT_ROOT}/.githooks/pre-push"
chmod +x "${PROJECT_ROOT}/.githooks/pre-push"
fi
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pre-push hook installation builds CGW_ALL_PREFIXES via parsing .cgw.conf and then substitutes into a sed replacement, but the escaping is incomplete.

If CGW_EXTRA_PREFIXES contains & or backslashes, sed replacement can corrupt the generated hook. (Pipes are handled, but & is special in sed replacements and should be escaped, and \\ handling should be consistent with the local-files pattern handling.)

Suggestion

Escape _all_prefixes for sed replacement the same way you already do for files_pattern:

local sed_prefixes="${_all_prefixes//\\/\\\\}"
sed_prefixes="${sed_prefixes//&/\\&}"
# (then escape delimiter '|')
sed_prefixes="${sed_prefixes//|/\\|}"

Then substitute with sed_prefixes instead of all_prefixes_escaped. Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this change.

Comment on lines +103 to +126
# ── [1] Merged local branches ─────────────────────────────────────────────
echo "--- [1] Merged Local Branches (merged into ${CGW_TARGET_BRANCH}) ---"

local -a merged_branches=()
while IFS= read -r branch; do
branch="${branch# }" # strip leading spaces from git branch output
branch="${branch#* }" # strip leading asterisk+space for current branch

# Skip empty
[[ -z "${branch}" ]] && continue

# Skip protected branches
local is_protected=0
for pb in "${protected[@]}"; do
[[ "${branch}" == "${pb}" ]] && is_protected=1 && break
done
[[ ${is_protected} -eq 1 ]] && continue

# Skip current branch
[[ "${branch}" == "${current_branch}" ]] && continue

merged_branches+=("${branch}")
done < <(git branch --merged "${CGW_TARGET_BRANCH}" 2>/dev/null)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Branch enumeration via git branch --merged ... is brittle:

  • In detached-HEAD states, git branch can emit lines like * (HEAD detached at ...), and after the current stripping logic (${branch#* }) you may end up with a non-branch token that then gets scheduled for deletion (and will fail noisily).
  • The parsing approach is also sensitive to formatting changes in git branch output.
Suggestion

Use plumbing-friendly output instead of parsing git branch formatting. For example:

while IFS= read -r branch; do
  [[ -z "${branch}" ]] && continue
  # ... protected/current checks ...
  merged_branches+=("${branch}")
done < <(git for-each-ref --format='%(refname:short)' refs/heads --merged "${CGW_TARGET_BRANCH}")

This avoids detached-HEAD pseudo-lines entirely. Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion.

Comment on lines +24 to +50
set -uo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/_common.sh"

init_logging "bisect_helper"

_bisect_original_branch=""

_cleanup_bisect() {
# If bisect is active and we were interrupted, abort it and return to original branch
if git rev-parse --git-dir >/dev/null 2>&1; then
if git bisect log >/dev/null 2>&1; then
echo "" >&2
echo "⚠ Interrupted — aborting bisect session" >&2
git bisect reset 2>/dev/null || true
fi
fi
if [[ -n "${_bisect_original_branch}" ]]; then
local current
current=$(git branch --show-current 2>/dev/null || true)
if [[ -n "${current}" ]] && [[ "${current}" != "${_bisect_original_branch}" ]]; then
git checkout "${_bisect_original_branch}" 2>/dev/null || true
fi
fi
}
trap _cleanup_bisect EXIT INT TERM
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New scripts are missing the required ShellCheck source directive on the _common.sh import, violating .charlie/instructions/code-style.md [R7].

This also uses tabs for indentation throughout, violating [R5] (2-space indentation).

Suggestion

Add the ShellCheck source directive and reindent to 2 spaces:

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=scripts/git/_common.sh
source "${SCRIPT_DIR}/_common.sh"

Then apply the same change across the other new scripts that source _common.sh. Reply with "@CharlieHelps yes please" if you’d like me to add a commit that normalizes all new scripts for [R5]/[R7] compliance.

Comment thread hooks/pre-push Outdated
Comment on lines +1 to +90
#!/bin/sh
# 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: __CGW_ALL_PREFIXES__
# LOCAL FILES PATTERN: __CGW_LOCAL_FILES_PATTERN__

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

LOCAL_FILES_PATTERN="__CGW_LOCAL_FILES_PATTERN__"
ALL_PREFIXES="__CGW_ALL_PREFIXES__"

# 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as .githooks/pre-commit: this hook is sh, lacks set -uo pipefail, uses [ ... ], and has tab indentation. Since this file is a template intended to be copied into user repos, it should follow the same bash conventions as the rest of the project ([R1], [R3], [R5]).

Suggestion

Convert the template to bash + style compliance:

  • #!/usr/bin/env bash + set -uo pipefail
  • Replace [ ... ] with [[ ... ]]
  • Replace tabs with 2-space indentation

Also consider factoring the duplicated content so hooks/pre-push is the single source of truth and .githooks/pre-push is generated/installed from it.

Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion.

Comment thread scripts/git/fix_lint.sh Outdated
Comment on lines +88 to +90
# CGW_LINT_EXTENSIONS controls which files are considered (default: *.py)
# shellcheck disable=SC2086
modified_files=$(git diff --name-only --diff-filter=ACMR HEAD -- ${CGW_LINT_EXTENSIONS:-*.py})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same glob-expansion bug as check_lint.sh: unquoted ${CGW_LINT_EXTENSIONS} can be expanded by the shell, producing incorrect pathspecs and missing files.

Suggestion

Use the same quoted-array approach as in check_lint.sh:

local -a lint_exts=()
read -r -a lint_exts <<< "${CGW_LINT_EXTENSIONS:-*.py}"
modified_files=$(git diff --name-only --diff-filter=ACMR HEAD -- "${lint_exts[@]}")

Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion.

Comment thread scripts/git/_common.sh
Comment on lines 49 to +65
init_logging() {
local script_name="$1"
# Use PROJECT_ROOT for an absolute path so logs land in the right place
# even when the script is invoked from a subdirectory. PROJECT_ROOT is set
# by _config.sh before any script calls init_logging.
local log_dir="${PROJECT_ROOT}/logs"

if [[ ! -d "logs" ]]; then
mkdir -p "logs"
if [[ ! -d "${log_dir}" ]]; then
mkdir -p "${log_dir}"
fi

get_timestamp

# shellcheck disable=SC2034
logfile="logs/${script_name}_${timestamp}.log"
logfile="${log_dir}/${script_name}_${timestamp}.log"
# shellcheck disable=SC2034
reportfile="logs/${script_name}_analysis_${timestamp}.log"
reportfile="${log_dir}/${script_name}_analysis_${timestamp}.log"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

init_logging() now assumes PROJECT_ROOT is set and non-empty. If any script calls init_logging before _config.sh has established PROJECT_ROOT (or if PROJECT_ROOT somehow becomes empty), this will write into /logs or an unintended path.

Suggestion

Add a defensive fallback to the previous relative behavior when PROJECT_ROOT is empty/unset:

local log_dir="logs"
if [[ -n "${PROJECT_ROOT:-}" ]]; then
  log_dir="${PROJECT_ROOT}/logs"
fi

Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion.

fi
}
trap _cleanup_cherry_pick INT TERM
trap _cleanup_cherry_pick EXIT INT TERM
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switching traps from INT TERM to EXIT INT TERM can change successful-script behavior: the cleanup now runs on normal exit too, which may unintentionally reset state (e.g., checking out the original branch even after a successful operation). Ensure the cleanup functions are explicitly gated (e.g., only run on interrupt/error) and don’t override intended final state.

Suggestion

Gate EXIT cleanup with an explicit flag, or trap ERR (where appropriate) instead of unconditional EXIT. Example pattern:

_cleanup_needed=0
trap '_cleanup_needed=1' INT TERM
trap '[[ ${_cleanup_needed} -eq 1 ]] && _cleanup_cherry_pick' EXIT

(or set _cleanup_needed=1 only after the script actually changes branches).

Reply with "@CharlieHelps yes please" if you’d like me to add a commit with this suggestion.

@charliecreates charliecreates Bot removed the request for review from CharlieHelps April 9, 2026 19:15
@forkni forkni merged commit a793b75 into main Apr 9, 2026
7 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant