From 4b9823fdf7245eb594dfa93b45a16bbef75b4c44 Mon Sep 17 00:00:00 2001 From: Jack Plowman <62281988+JackPlowman@users.noreply.github.com> Date: Sun, 14 Jun 2026 20:31:48 +0100 Subject: [PATCH 1/2] update --- .zshrc | 3 +- .../scripts/squash_by_day.py | 46 +++++++++++++++++++ .../scripts/squash_by_day.sh | 5 ++ 3 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 development-configuration/scripts/squash_by_day.py create mode 100644 development-configuration/scripts/squash_by_day.sh diff --git a/.zshrc b/.zshrc index 0dd2f1e..91cb836 100644 --- a/.zshrc +++ b/.zshrc @@ -89,12 +89,11 @@ alias rebase="bash ~/development-configuration/scripts/rebase_and_push.sh" alias main="bash ~/development-configuration/scripts/checkout_main_and_pull.sh" alias master="bash ~/development-configuration/scripts/checkout_master_and_pull.sh" alias fetch-all="bash ~/development-configuration/scripts/fetch_all.sh" +alias squash_by_day="bash ~/development-configuration/scripts/squash_by_day.sh" # Added by LM Studio CLI (lms) export PATH="$PATH:/Users/jackplowman/.lmstudio/bin" # End of LM Studio CLI section - - # Added by Antigravity CLI installer export PATH="/Users/jackplowman/.local/bin:$PATH" diff --git a/development-configuration/scripts/squash_by_day.py b/development-configuration/scripts/squash_by_day.py new file mode 100644 index 0000000..3bd0850 --- /dev/null +++ b/development-configuration/scripts/squash_by_day.py @@ -0,0 +1,46 @@ +# /// script +# requires-python = ">=3.14" +# dependencies = [] +# /// + +import sys +import subprocess + +def main(): + if len(sys.argv) < 2: + print("Usage: uv run squash_by_day.py ") + sys.exit(1) + + todo_file = sys.argv[1] + + with open(todo_file, "r") as f: + lines = f.readlines() + + out = [] + prev_date = None + + for line in lines: + if line.startswith("pick "): + commit_hash = line.split()[1] + # Look up the commit date (YYYY-MM-DD) + date = subprocess.check_output( + ["git", "log", "-1", "--format=%cd", "--date=short", commit_hash] + ).decode("utf-8").strip() + + if prev_date == date: + # Same day as the previous commit, squash into it. + # Note: change "squash " to "fixup " below to discard the squashed commit messages. + out.append(line.replace("pick ", "squash ", 1)) + else: + # First commit of a new day, keep it as 'pick' + out.append(line) + + prev_date = date + else: + out.append(line) + + with open(todo_file, "w") as f: + f.writelines(out) + +if __name__ == "__main__": + main() diff --git a/development-configuration/scripts/squash_by_day.sh b/development-configuration/scripts/squash_by_day.sh new file mode 100644 index 0000000..f3cf179 --- /dev/null +++ b/development-configuration/scripts/squash_by_day.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +BASE_BRANCH=$(git rev-parse --abbrev-ref origin/HEAD 2>/dev/null) +# Run the automated rebase targeting the base branch instead of HEAD~N +GIT_SEQUENCE_EDITOR='uv run squash_by_day.py' GIT_EDITOR=true git rebase -i $BASE_BRANCH From c6ebe5164e7cfeba28cbb71d44a4c4f242d1f61b Mon Sep 17 00:00:00 2001 From: Jack Plowman <62281988+JackPlowman@users.noreply.github.com> Date: Sun, 14 Jun 2026 21:03:29 +0100 Subject: [PATCH 2/2] Daily squash --- .../scripts/squash_by_day.py | 46 ------- .../scripts/squash_by_day.sh | 119 +++++++++++++++++- 2 files changed, 115 insertions(+), 50 deletions(-) delete mode 100644 development-configuration/scripts/squash_by_day.py diff --git a/development-configuration/scripts/squash_by_day.py b/development-configuration/scripts/squash_by_day.py deleted file mode 100644 index 3bd0850..0000000 --- a/development-configuration/scripts/squash_by_day.py +++ /dev/null @@ -1,46 +0,0 @@ -# /// script -# requires-python = ">=3.14" -# dependencies = [] -# /// - -import sys -import subprocess - -def main(): - if len(sys.argv) < 2: - print("Usage: uv run squash_by_day.py ") - sys.exit(1) - - todo_file = sys.argv[1] - - with open(todo_file, "r") as f: - lines = f.readlines() - - out = [] - prev_date = None - - for line in lines: - if line.startswith("pick "): - commit_hash = line.split()[1] - # Look up the commit date (YYYY-MM-DD) - date = subprocess.check_output( - ["git", "log", "-1", "--format=%cd", "--date=short", commit_hash] - ).decode("utf-8").strip() - - if prev_date == date: - # Same day as the previous commit, squash into it. - # Note: change "squash " to "fixup " below to discard the squashed commit messages. - out.append(line.replace("pick ", "squash ", 1)) - else: - # First commit of a new day, keep it as 'pick' - out.append(line) - - prev_date = date - else: - out.append(line) - - with open(todo_file, "w") as f: - f.writelines(out) - -if __name__ == "__main__": - main() diff --git a/development-configuration/scripts/squash_by_day.sh b/development-configuration/scripts/squash_by_day.sh index f3cf179..02b137e 100644 --- a/development-configuration/scripts/squash_by_day.sh +++ b/development-configuration/scripts/squash_by_day.sh @@ -1,5 +1,116 @@ -#!/bin/bash +#!/usr/bin/env bash +set -euo pipefail -BASE_BRANCH=$(git rev-parse --abbrev-ref origin/HEAD 2>/dev/null) -# Run the automated rebase targeting the base branch instead of HEAD~N -GIT_SEQUENCE_EDITOR='uv run squash_by_day.py' GIT_EDITOR=true git rebase -i $BASE_BRANCH +usage() { + cat <<'EOF' +Usage: squash-commits-by-day.sh [BASE] + +Rebase the current branch onto BASE, then combine its commits by author date. +Each resulting commit is named "Squash for YYYY-MM-DD". + +When BASE is omitted, the script tries origin/HEAD, main, then master. + +The selected history must be linear and the worktree must be clean. +This command rewrites Git history and does not create a backup branch. +EOF +} + +die() { + printf 'Error: %s\n' "$*" >&2 + exit 1 +} + +if [[ ${1:-} == "-h" || ${1:-} == "--help" ]]; then + usage + exit 0 +fi + +(( $# <= 1 )) || die "expected at most one BASE argument" +git rev-parse --git-dir >/dev/null 2>&1 || die "not inside a Git repository" +git symbolic-ref -q HEAD >/dev/null || die "HEAD must be attached to a branch" +[[ -z $(git status --porcelain) ]] || die "worktree and index must be clean" + +branch=$(git symbolic-ref --short HEAD) + +if [[ $# == 1 ]]; then + base_ref=$1 +else + base_ref="" + remote_default=$(git symbolic-ref -q --short refs/remotes/origin/HEAD || true) + for candidate in "$remote_default" main master; do + if [[ -n $candidate ]] && git rev-parse --verify "$candidate^{commit}" >/dev/null 2>&1; then + base_ref=$candidate + break + fi + done + [[ -n $base_ref ]] || die "could not detect a default branch; pass BASE explicitly" +fi + +base=$(git rev-parse --verify "$base_ref^{commit}") || die "invalid BASE: $base_ref" +[[ $base != $(git rev-parse HEAD) ]] || die "current branch is already at BASE ($base_ref)" + +printf 'Rebasing %s onto %s...\n' "$branch" "$base_ref" +if ! git rebase "$base_ref"; then + die "rebase stopped; resolve conflicts and run 'git rebase --continue' or abort it" +fi + +base=$(git rev-parse --verify "$base_ref^{commit}") +old_head=$(git rev-parse HEAD) +revision_range="$base..HEAD" + +commits=() +while IFS= read -r commit; do + commits+=("$commit") +done < <(git rev-list --reverse --topo-order "$revision_range") + +(( ${#commits[@]} > 0 )) || die "no commits to rewrite" + +for commit in "${commits[@]}"; do + parents=$(git rev-list --parents -n 1 "$commit") + [[ $(wc -w <<<"$parents" | tr -d ' ') -le 2 ]] || \ + die "selected history contains merge commit $commit" +done + +new_parent=$base +group_date="" +group_last="" + +create_group_commit() { + local commit=$1 day=$2 tree author_name author_email author_date new_commit + tree=$(git rev-parse "$commit^{tree}") + author_name=$(git show -s --format=%an "$commit") + author_email=$(git show -s --format=%ae "$commit") + author_date=$(git show -s --format=%aI "$commit") + + if [[ -n $new_parent ]]; then + new_commit=$(printf 'Squash for %s\n' "$day" | \ + GIT_AUTHOR_NAME="$author_name" GIT_AUTHOR_EMAIL="$author_email" \ + GIT_AUTHOR_DATE="$author_date" GIT_COMMITTER_DATE="$author_date" \ + git commit-tree "$tree" -p "$new_parent") + else + new_commit=$(printf 'Squash for %s\n' "$day" | \ + GIT_AUTHOR_NAME="$author_name" GIT_AUTHOR_EMAIL="$author_email" \ + GIT_AUTHOR_DATE="$author_date" GIT_COMMITTER_DATE="$author_date" \ + git commit-tree "$tree") + fi + new_parent=$new_commit +} + +for commit in "${commits[@]}"; do + commit_date=$(git show -s --format=%aI "$commit") + commit_date=${commit_date:0:10} + + if [[ -n $group_date && $commit_date != "$group_date" ]]; then + create_group_commit "$group_last" "$group_date" + fi + group_date=$commit_date + group_last=$commit +done +create_group_commit "$group_last" "$group_date" + +git update-ref "refs/heads/$branch" "$new_parent" "$old_head" +git reset --hard "$new_parent" >/dev/null + +printf 'Rebased and rewrote %s onto %s.\n' "$branch" "$base_ref" +git log --oneline --decorate --date=short --format='%h %ad %s' \ + ${base:+"$base.."}HEAD