From 31b043ee1a99d044a20c2e9f057179dd40e425fb Mon Sep 17 00:00:00 2001 From: Matt Schreiber Date: Wed, 6 Sep 2023 20:51:17 -0400 Subject: [PATCH 01/23] feat: use `/bin/sh` interpreter rather than non-portable `/bin/bash` --- git-redate | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-redate b/git-redate index 066fd95..dc6bc19 100755 --- a/git-redate +++ b/git-redate @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh is_git_repo() { git rev-parse --show-toplevel > /dev/null 2>&1 From cb735e0b742c3225624d4e5d7ddecb67ffa281d9 Mon Sep 17 00:00:00 2001 From: Matt Schreiber Date: Wed, 6 Sep 2023 21:32:17 -0400 Subject: [PATCH 02/23] fix: replace Bash-specific code with POSIX-compatible code. --- git-redate | 60 +++++++++++++++++++++++++++++------------------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/git-redate b/git-redate index dc6bc19..3a49b1d 100755 --- a/git-redate +++ b/git-redate @@ -39,10 +39,10 @@ is_has_editor() { OUR_EDITOR="$EDITOR"; else make_editor_choice; - if [ ${CHOOSE_EDITOR} == 3 ] || [ ${CHOOSE_EDITOR} == "3" ]; then + if [ ${CHOOSE_EDITOR} = 3 ] || [ ${CHOOSE_EDITOR} = "3" ]; then get_editor_executable OUR_EDITOR=${EDITOR_PATH} - elif [ ${CHOOSE_EDITOR} == 1 ] || [ ${CHOOSE_EDITOR} == "1" ]; then + elif [ ${CHOOSE_EDITOR} = 1 ] || [ ${CHOOSE_EDITOR} = "1" ]; then OUR_EDITOR="vi"; else OUR_EDITOR="nano"; @@ -58,7 +58,7 @@ ALL=0 DEBUG=0 LIMITCHUNKS=20 -while [[ $# -ge 1 ]] +while [ "$#" -ge 1 ] do key="$1" @@ -99,7 +99,7 @@ trap "rm -f $tmpfile" EXIT datefmt=%cI -if [ "`git log -n1 --pretty=format:"$datefmt"`" == "$datefmt" ]; +if [ "`git log -n1 --pretty=format:"$datefmt"`" = "$datefmt" ]; then datefmt=%ci fi @@ -119,19 +119,21 @@ ${VISUAL:-${EDITOR:-${OUR_EDITOR}}} $tmpfile ITER=0 COLITER=0 -declare -a COLLECTION COUNTCOMMITS=$(awk 'END {print NR}' $tmpfile) while read commit || [ -n "$commit" ]; do - IFS="|" read date hash message <<< "$commit" - shopt -s nocasematch - if [[ "$date" == 'now' ]]; then + IFS="|" read date hash message </dev/null fi else if [ "${DEBUG}" -eq 1 ]; then - echo "Chunk $ITERATOR/"${#COLLECTION[@]}" Started" + echo "Chunk $ITERATOR/$COLITER Started" git filter-branch -f --env-filter "$each" HEAD~${COMMITS}..HEAD - echo "Chunk $ITERATOR/"${#COLLECTION[@]}" Finished" + echo "Chunk $ITERATOR/$COLITER Finished" else git filter-branch -f --env-filter "$each" HEAD~${COMMITS}..HEAD >/dev/null fi fi + + ITERATOR="$((ITERATOR+1))" done if [ $? = 0 ] ; then From 79b0b5783bc34918eba9b661f658cc9d2ff69f10 Mon Sep 17 00:00:00 2001 From: Matt Schreiber Date: Thu, 7 Sep 2023 20:56:49 -0400 Subject: [PATCH 03/23] feat: integrate with git's core shell library for features like CLI options processing and "am I in a git repo?" sanity-checks. --- git-redate | 46 +++++++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/git-redate b/git-redate index 3a49b1d..a0f7ecd 100755 --- a/git-redate +++ b/git-redate @@ -1,15 +1,25 @@ #!/bin/sh -is_git_repo() { - git rev-parse --show-toplevel > /dev/null 2>&1 - result=$? - if test $result != 0; then - >&2 echo 'Not a git repo!' - exit $result - fi -} +dashless="${0##*/}" +dashless="${dashless%%-*} ${dashless#*-}" + +# This variable is consulted by the code in "git-sh-setup", which is sourced +# below -- we don't need to export the variable. +# shellcheck disable=SC2034 +OPTIONS_SPEC="${dashless} [] + +${dashless} changes the dates of one or more git commits according to an interactively-specified scheme. +-- +d,debug show diagnostic output +c,commits= number of commits to re-date (default: 5) +l,limit= number of commits to re-date in a single batch (default: 20) +a,all re-date all commits +" + +# Allow running this command from a subdirectory of the working tree +# shellcheck disable=SC1091 +SUBDIRECTORY_OK=yes . "$(git --exec-path)/git-sh-setup" -is_git_repo make_editor_choice() { @@ -77,10 +87,20 @@ case $key in DEBUG=1 shift ;; + --no-debug) + DEBUG=0 + ;; -a| --all) ALL=1 shift ;; + --no-all) + ALL=0 + ;; + --) + shift + break + ;; *) # unknown option ;; @@ -88,13 +108,13 @@ esac shift done -die () { - echo >&2 `basename $0`: $* - exit 1 +croak () { + # `die` is from git-sh-setup + die "${dashless}: $*" } tmpfile=$(mktemp gitblah-XXXX) -[ -f "$tmpfile" ] || die "could not get tmpfile=[$tmpfile]" +[ -f "$tmpfile" ] || croak "could not get tmpfile=[$tmpfile]" trap "rm -f $tmpfile" EXIT From 2ef2f304fc872a17e04354d8d6b360ecf6e4c91d Mon Sep 17 00:00:00 2001 From: Matt Schreiber Date: Thu, 7 Sep 2023 20:56:57 -0400 Subject: [PATCH 04/23] feat: put tmpfile in `.git` --- git-redate | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/git-redate b/git-redate index a0f7ecd..3f0e49c 100755 --- a/git-redate +++ b/git-redate @@ -113,8 +113,12 @@ croak () { die "${dashless}: $*" } -tmpfile=$(mktemp gitblah-XXXX) -[ -f "$tmpfile" ] || croak "could not get tmpfile=[$tmpfile]" +GIT_DIR="${GIT_DIR:-$(git rev-parse --git-dir 2>/dev/null)}" + +if ! tmpfile=$(mktemp "${GIT_DIR:+${GIT_DIR}/}GIT_REDATE.XXXXXXXXXX") || ! [ -f "$tmpfile" ]; then + croak "could not get tmpfile=[$tmpfile]" +fi + trap "rm -f $tmpfile" EXIT From f452ac177e10fad3a7879997108c459346b5d77e Mon Sep 17 00:00:00 2001 From: Matt Schreiber Date: Thu, 7 Sep 2023 20:56:58 -0400 Subject: [PATCH 05/23] chore: fix shellcheck linting errors --- git-redate | 76 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/git-redate b/git-redate index 3f0e49c..5f1dc54 100755 --- a/git-redate +++ b/git-redate @@ -23,41 +23,41 @@ SUBDIRECTORY_OK=yes . "$(git --exec-path)/git-sh-setup" make_editor_choice() { - echo "Which editor do you want to use for this repo?\n"; - echo "1. VI\n"; - echo "2. NANO\n"; - echo "3. Your own\n" - echo "You Choose: "; + echo 'Which editor do you want to use for this repo?' + echo '1. VI' + echo '2. NANO' + echo '3. Your own' + echo 'You Choose: ' - read CHOOSE_EDITOR + read -r CHOOSE_EDITOR } get_editor_executable() { - echo "What is the path to your prefered test editor?\n"; - read EDITOR_PATH + echo 'What is the path to your prefered test editor?' + read -r EDITOR_PATH } is_has_editor() { - SETTINGS_FILE="~/.redate-settings"; + SETTINGS_FILE=~/.redate-settings if [ -f "$SETTINGS_FILE" ] then OUR_EDITOR=$(cat ${SETTINGS_FILE}); - elif [ ! -z "$EDITOR" ] + elif [ -n "${EDITOR:-}" ] then OUR_EDITOR="$EDITOR"; else - make_editor_choice; - if [ ${CHOOSE_EDITOR} = 3 ] || [ ${CHOOSE_EDITOR} = "3" ]; then + make_editor_choice + if [ "$CHOOSE_EDITOR" = 3 ]; then get_editor_executable OUR_EDITOR=${EDITOR_PATH} - elif [ ${CHOOSE_EDITOR} = 1 ] || [ ${CHOOSE_EDITOR} = "1" ]; then + elif [ "$CHOOSE_EDITOR" = 1 ]; then OUR_EDITOR="vi"; else OUR_EDITOR="nano"; fi - echo ${OUR_EDITOR} > ${SETTINGS_FILE} + echo "$OUR_EDITOR" > "$SETTINGS_FILE" fi } @@ -119,36 +119,42 @@ if ! tmpfile=$(mktemp "${GIT_DIR:+${GIT_DIR}/}GIT_REDATE.XXXXXXXXXX") || ! [ -f croak "could not get tmpfile=[$tmpfile]" fi -trap "rm -f $tmpfile" EXIT +cleanup() { + rm -f "${tmpfile?}" +} + +trap cleanup EXIT datefmt=%cI -if [ "`git log -n1 --pretty=format:"$datefmt"`" = "$datefmt" ]; +if [ "$(git log -n1 --pretty=format:"$datefmt")" = "$datefmt" ]; then datefmt=%ci fi if [ "${ALL}" -eq 1 ]; then - git log --pretty=format:"$datefmt | %H | %s" > $tmpfile; + git log --pretty=format:"$datefmt | %H | %s" > "$tmpfile" else - if [ -n "${COMMITS+set}" ]; - then git log -n ${COMMITS} --pretty=format:"$datefmt | %H | %s" > $tmpfile; - else git log -n 5 --pretty=format:"$datefmt | %H | %s" > $tmpfile; + if [ -n "${COMMITS+set}" ] + then + git log -n "$COMMITS" --pretty=format:"$datefmt | %H | %s" > "$tmpfile" + else + git log -n 5 --pretty=format:"$datefmt | %H | %s" > "$tmpfile" fi fi -${VISUAL:-${EDITOR:-${OUR_EDITOR}}} $tmpfile +${VISUAL:-${EDITOR:-${OUR_EDITOR}}} "$tmpfile" ITER=0 COLITER=0 -COUNTCOMMITS=$(awk 'END {print NR}' $tmpfile) +COUNTCOMMITS=$(awk 'END {print NR}' "$tmpfile") -while read commit || [ -n "$commit" ]; do +while read -r commit || [ -n "$commit" ]; do - IFS="|" read date hash message </dev/null + git filter-branch -f --env-filter "$each" -- --all >/dev/null || rc="$?" fi else if [ "${DEBUG}" -eq 1 ]; then echo "Chunk $ITERATOR/$COLITER Started" - git filter-branch -f --env-filter "$each" HEAD~${COMMITS}..HEAD + git filter-branch -f --env-filter "$each" "HEAD~${COMMITS}"..HEAD || rc="$?" echo "Chunk $ITERATOR/$COLITER Finished" else - git filter-branch -f --env-filter "$each" HEAD~${COMMITS}..HEAD >/dev/null + git filter-branch -f --env-filter "$each" "HEAD~${COMMITS}..HEAD" >/dev/null || rc="$?" fi fi ITERATOR="$((ITERATOR+1))" done -if [ $? = 0 ] ; then +if [ "$rc" = 0 ] ; then echo "Git commit dates updated. Run 'git push -f BRANCH_NAME' to push your changes." else echo "Git redate failed. Please make sure you run this on a clean working directory." + exit "$rc" fi From 7c10c333ca4ba43aa02a04888738c9360a5f1993 Mon Sep 17 00:00:00 2001 From: Matt Schreiber Date: Thu, 7 Sep 2023 20:56:58 -0400 Subject: [PATCH 06/23] fix: do not attempt to shift > ARGC arguments That is, remove redundant `shift` calls in the `--debug` and `--all` cases. --- git-redate | 2 -- 1 file changed, 2 deletions(-) diff --git a/git-redate b/git-redate index 5f1dc54..8644f58 100755 --- a/git-redate +++ b/git-redate @@ -85,14 +85,12 @@ case $key in ;; -d| --debug) DEBUG=1 - shift ;; --no-debug) DEBUG=0 ;; -a| --all) ALL=1 - shift ;; --no-all) ALL=0 From ab3edd4e2f7108c2af54f6f63c002527c0993aa9 Mon Sep 17 00:00:00 2001 From: Matt Schreiber Date: Thu, 7 Sep 2023 20:56:59 -0400 Subject: [PATCH 07/23] chore: simplify debugging code by conditionally defining a `debug` function based on whether the `-d/--debug` option was provided. If debugging is not enabled, the `debug` function is a NOP; otherwise, it echoes its arguments to stderr. --- git-redate | 47 ++++++++++++++++++++--------------------------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/git-redate b/git-redate index 8644f58..4c3733a 100755 --- a/git-redate +++ b/git-redate @@ -123,6 +123,16 @@ cleanup() { trap cleanup EXIT +if [ "$DEBUG" = 1 ]; then + debug() { + echo "$*" 1>&2 + } +else + debug() { + : + } +fi + datefmt=%cI if [ "$(git log -n1 --pretty=format:"$datefmt")" = "$datefmt" ]; @@ -184,25 +194,18 @@ END if [ $((ITER % LIMITCHUNKS)) = 0 ] then COLITER="$((COLITER+1))" - - if [ "${DEBUG}" -eq 1 ]; - then - echo "Chunk $COLITER Started" - fi + debug "Chunk $COLITER Started" fi ITER="$((ITER + 1))" eval "__GIT_REDATE_COLLECTION_${COLITER}=\"\${__GIT_REDATE_COLLECTION_${COLITER}:-}\${COMMIT_ENV}\"" - if [ "${DEBUG}" -eq 1 ] - then - echo "Commit $ITER/$COUNTCOMMITS Collected" - fi + debug "Commit $ITER/$COUNTCOMMITS Collected" - if [ "${DEBUG}" -eq 1 ] && [ $((ITER % LIMITCHUNKS)) = 0 ]; + if [ $((ITER % LIMITCHUNKS)) = 0 ]; then - echo "Chunk $COLITER Finished" + debug "Chunk $COLITER Finished" fi done < "$tmpfile" @@ -213,27 +216,17 @@ do each="" eval "each=\"\$__GIT_REDATE_COLLECTION_${ITERATOR}\"" + debug "Chunk $ITERATOR/$COLITER Started" + if [ "${ALL}" -eq 1 ]; then - if [ "${DEBUG}" -eq 1 ]; - then - echo "Chunk $ITERATOR/$COLITER Started" - git filter-branch -f --env-filter "$each" -- --all || rc="$?" - echo "Chunk $ITERATOR/$COLITER Finished" - else - git filter-branch -f --env-filter "$each" -- --all >/dev/null || rc="$?" - fi + git filter-branch -f --env-filter "$each" -- --all || rc="$?" else - if [ "${DEBUG}" -eq 1 ]; - then - echo "Chunk $ITERATOR/$COLITER Started" - git filter-branch -f --env-filter "$each" "HEAD~${COMMITS}"..HEAD || rc="$?" - echo "Chunk $ITERATOR/$COLITER Finished" - else - git filter-branch -f --env-filter "$each" "HEAD~${COMMITS}..HEAD" >/dev/null || rc="$?" - fi + git filter-branch -f --env-filter "$each" "HEAD~${COMMITS}"..HEAD || rc="$?" fi + debug "Chunk $ITERATOR/$COLITER Finished" + ITERATOR="$((ITERATOR+1))" done From af8bb17822de5101f3ca607c327946ab50be2b02 Mon Sep 17 00:00:00 2001 From: Matt Schreiber Date: Thu, 7 Sep 2023 20:57:42 -0400 Subject: [PATCH 08/23] feat: add dry-run mode --- git-redate | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/git-redate b/git-redate index 4c3733a..2fc8300 100755 --- a/git-redate +++ b/git-redate @@ -14,13 +14,13 @@ d,debug show diagnostic output c,commits= number of commits to re-date (default: 5) l,limit= number of commits to re-date in a single batch (default: 20) a,all re-date all commits +n,dry-run show re-dating commands, but do not execute them " # Allow running this command from a subdirectory of the working tree # shellcheck disable=SC1091 SUBDIRECTORY_OK=yes . "$(git --exec-path)/git-sh-setup" - make_editor_choice() { echo 'Which editor do you want to use for this repo?' @@ -66,6 +66,7 @@ is_has_editor ALL=0 DEBUG=0 +DRY_RUN=0 LIMITCHUNKS=20 while [ "$#" -ge 1 ] @@ -95,6 +96,12 @@ case $key in --no-all) ALL=0 ;; + -n| --dry-run) + DRY_RUN=1 + ;; + --no-dry-run) + DRY_RUN=0 + ;; --) shift break @@ -133,6 +140,15 @@ else } fi +if [ "$DRY_RUN" = 1 ]; then + run() { + echo "$*" 1>&2 + } +else + run() { + "$@" + } +fi datefmt=%cI if [ "$(git log -n1 --pretty=format:"$datefmt")" = "$datefmt" ]; @@ -220,9 +236,9 @@ do if [ "${ALL}" -eq 1 ]; then - git filter-branch -f --env-filter "$each" -- --all || rc="$?" + run git filter-branch -f --env-filter "$each" -- --all || rc="$?" else - git filter-branch -f --env-filter "$each" "HEAD~${COMMITS}"..HEAD || rc="$?" + run git filter-branch -f --env-filter "$each" "HEAD~${COMMITS}"..HEAD || rc="$?" fi debug "Chunk $ITERATOR/$COLITER Finished" From 3a86bc14ca13b733ddc0e9e42bdd992cdd09c8e8 Mon Sep 17 00:00:00 2001 From: Matt Schreiber Date: Thu, 7 Sep 2023 20:58:49 -0400 Subject: [PATCH 09/23] chore: send diagnostics to stderr BREAKING CHANGE: diagnostic output no longer goes to stdout. --- git-redate | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/git-redate b/git-redate index 2fc8300..f747b68 100755 --- a/git-redate +++ b/git-redate @@ -21,6 +21,10 @@ n,dry-run show re-dating commands, but do not execute them # shellcheck disable=SC1091 SUBDIRECTORY_OK=yes . "$(git --exec-path)/git-sh-setup" +diag() { + echo "$*" 1>&2 +} + make_editor_choice() { echo 'Which editor do you want to use for this repo?' @@ -132,7 +136,7 @@ trap cleanup EXIT if [ "$DEBUG" = 1 ]; then debug() { - echo "$*" 1>&2 + diag "$@" } else debug() { @@ -142,7 +146,7 @@ fi if [ "$DRY_RUN" = 1 ]; then run() { - echo "$*" 1>&2 + diag "$@" } else run() { @@ -247,8 +251,8 @@ do done if [ "$rc" = 0 ] ; then - echo "Git commit dates updated. Run 'git push -f BRANCH_NAME' to push your changes." + diag "Git commit dates updated. Run 'git push -f BRANCH_NAME' to push your changes." else - echo "Git redate failed. Please make sure you run this on a clean working directory." + diag "Git redate failed. Please make sure you run this on a clean working directory." exit "$rc" fi From 6347f4054d14736cad335957d6e663899761af08 Mon Sep 17 00:00:00 2001 From: Matt Schreiber Date: Thu, 7 Sep 2023 20:59:02 -0400 Subject: [PATCH 10/23] BREAKING CHANGE: use `$GIT_EDITOR` for date edits rather than custom editor selection code. --- git-redate | 48 ++---------------------------------------------- 1 file changed, 2 insertions(+), 46 deletions(-) diff --git a/git-redate b/git-redate index f747b68..8d81b71 100755 --- a/git-redate +++ b/git-redate @@ -22,52 +22,9 @@ n,dry-run show re-dating commands, but do not execute them SUBDIRECTORY_OK=yes . "$(git --exec-path)/git-sh-setup" diag() { - echo "$*" 1>&2 + echo "$@" 1>&2 } -make_editor_choice() { - - echo 'Which editor do you want to use for this repo?' - echo '1. VI' - echo '2. NANO' - echo '3. Your own' - echo 'You Choose: ' - - read -r CHOOSE_EDITOR -} - -get_editor_executable() { - - echo 'What is the path to your prefered test editor?' - read -r EDITOR_PATH -} - - -is_has_editor() { - SETTINGS_FILE=~/.redate-settings - if [ -f "$SETTINGS_FILE" ] - then - OUR_EDITOR=$(cat ${SETTINGS_FILE}); - elif [ -n "${EDITOR:-}" ] - then - OUR_EDITOR="$EDITOR"; - else - make_editor_choice - if [ "$CHOOSE_EDITOR" = 3 ]; then - get_editor_executable - OUR_EDITOR=${EDITOR_PATH} - elif [ "$CHOOSE_EDITOR" = 1 ]; then - OUR_EDITOR="vi"; - else - OUR_EDITOR="nano"; - fi - echo "$OUR_EDITOR" > "$SETTINGS_FILE" - fi -} - -is_has_editor - - ALL=0 DEBUG=0 DRY_RUN=0 @@ -172,8 +129,7 @@ else fi fi -${VISUAL:-${EDITOR:-${OUR_EDITOR}}} "$tmpfile" - +git_editor "$tmpfile" || exit ITER=0 COLITER=0 From 0b8f0fb38fa3a8e7c8615814a41f3d48f2afe890 Mon Sep 17 00:00:00 2001 From: Matt Schreiber Date: Thu, 7 Sep 2023 20:59:04 -0400 Subject: [PATCH 11/23] feat: support `redate.` git config settings BREAKING CHANGE: introduces type-checking of `COMMITS`, `DEBUG`, and friends. --- git-redate | 69 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/git-redate b/git-redate index 8d81b71..1e75a2f 100755 --- a/git-redate +++ b/git-redate @@ -25,11 +25,6 @@ diag() { echo "$@" 1>&2 } -ALL=0 -DEBUG=0 -DRY_RUN=0 -LIMITCHUNKS=20 - while [ "$#" -ge 1 ] do key="$1" @@ -37,12 +32,10 @@ key="$1" case $key in -c| --commits) COMMITS="$2" - if [ -z "${COMMITS}" ]; then COMMITS="5"; fi; shift ;; -l| --limit) LIMITCHUNKS="$2" - if [ -z "${LIMITCHUNKS}" ]; then LIMITCHUNKS="20"; fi; shift ;; -d| --debug) @@ -74,6 +67,68 @@ esac shift done +git_config_get_cond() { + setting="${1?}" + shift + + current="${1:-}" + shift + + default="${1?}" + shift + + if [ -n "${current:-}" ]; then + final="$current" + else + from_config="$(git config "$@" --get "${setting?}")" || { + case "$?" in + 128) + return 128 + ;; + esac + } + + if [ -n "$from_config" ]; then + final="$from_config" + else + final="${default?}" + fi + fi + + printf -- '%s' "$final" +} + +git_config_get_bool_cond() { + raw="$(git_config_get_cond "$@" --bool)" || return + + case "$raw" in + true) + printf -- '1' + ;; + false) + printf -- '0' + ;; + *) + printf -- '%s' "$raw" + esac +} + +git_config_get_posint_cond() { + raw="$(git_config_get_cond "$@" --type int)" || return + + if [ -z "$raw" ] || [ "$raw" -lt 1 ]; then + die "fatal: bad positive integer config value '${raw}' for '${1?}'" + return 128 + fi + + printf -- '%d' "$raw" +} + +ALL="$(git_config_get_bool_cond redate.all "${ALL:-}" 0)" || exit +COMMITS="$(git_config_get_posint_cond redate.commits "${COMMITS:-}" 5)" || exit +DEBUG="$(git_config_get_bool_cond redate.debug "${DEBUG:-}" 0)" || exit +LIMITCHUNKS="$(git_config_get_posint_cond redate.limit "${LIMITCHUNKS:-}" 20)" || exit + croak () { # `die` is from git-sh-setup die "${dashless}: $*" From f6ef8717f9cd9164f61c5c1b21fae51759a72e6b Mon Sep 17 00:00:00 2001 From: Matt Schreiber Date: Thu, 7 Sep 2023 20:59:04 -0400 Subject: [PATCH 12/23] chore: remove redundant `git log` that provides a default value for the `COMMITS` variable. This variable is already guaranteed to be set to a positive integer (or else `git redate` will have exited). --- git-redate | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/git-redate b/git-redate index 1e75a2f..96eb9c4 100755 --- a/git-redate +++ b/git-redate @@ -176,12 +176,7 @@ if [ "${ALL}" -eq 1 ]; then git log --pretty=format:"$datefmt | %H | %s" > "$tmpfile" else - if [ -n "${COMMITS+set}" ] - then - git log -n "$COMMITS" --pretty=format:"$datefmt | %H | %s" > "$tmpfile" - else - git log -n 5 --pretty=format:"$datefmt | %H | %s" > "$tmpfile" - fi + git log -n "$COMMITS" --pretty=format:"$datefmt | %H | %s" > "$tmpfile" fi git_editor "$tmpfile" || exit From addeaf13fe8cd9f9afddb6b6bfee585880943d9a Mon Sep 17 00:00:00 2001 From: Matt Schreiber Date: Thu, 7 Sep 2023 20:59:05 -0400 Subject: [PATCH 13/23] feat: remove repeated `--all` option checks within the body of the loop that applies the redate logic. Instead, conditionally define an `apply_redate` function depending on whether we saw `--all`, then call this function from within the loop. --- git-redate | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/git-redate b/git-redate index 96eb9c4..0555602 100755 --- a/git-redate +++ b/git-redate @@ -166,6 +166,16 @@ else } fi +if [ "$ALL" = 1 ]; then + apply_redate() { + run git filter-branch -f --env-filter "${1?}" -- --all + } +else + apply_redate() { + run git filter-branch -f --env-filter "${1?}" "HEAD~${COMMITS}..HEAD" + } +fi + datefmt=%cI if [ "$(git log -n1 --pretty=format:"$datefmt")" = "$datefmt" ]; then @@ -244,12 +254,7 @@ do debug "Chunk $ITERATOR/$COLITER Started" - if [ "${ALL}" -eq 1 ]; - then - run git filter-branch -f --env-filter "$each" -- --all || rc="$?" - else - run git filter-branch -f --env-filter "$each" "HEAD~${COMMITS}"..HEAD || rc="$?" - fi + apply_redate "$each" || rc="$?" debug "Chunk $ITERATOR/$COLITER Finished" From 9469ef007ed3972ab4bb1461c263e35cf05738e1 Mon Sep 17 00:00:00 2001 From: Matt Schreiber Date: Thu, 7 Sep 2023 20:59:05 -0400 Subject: [PATCH 14/23] chore: format code with shfmt --- git-redate | 109 +++++++++++++++++++++++++---------------------------- 1 file changed, 51 insertions(+), 58 deletions(-) diff --git a/git-redate b/git-redate index 0555602..968147f 100755 --- a/git-redate +++ b/git-redate @@ -25,46 +25,45 @@ diag() { echo "$@" 1>&2 } -while [ "$#" -ge 1 ] -do -key="$1" - -case $key in - -c| --commits) - COMMITS="$2" - shift - ;; - -l| --limit) - LIMITCHUNKS="$2" - shift - ;; - -d| --debug) - DEBUG=1 - ;; +while [ "$#" -ge 1 ]; do + key="$1" + + case $key in + -c | --commits) + COMMITS="$2" + shift + ;; + -l | --limit) + LIMITCHUNKS="$2" + shift + ;; + -d | --debug) + DEBUG=1 + ;; --no-debug) - DEBUG=0 - ;; - -a| --all) - ALL=1 - ;; + DEBUG=0 + ;; + -a | --all) + ALL=1 + ;; --no-all) - ALL=0 - ;; - -n| --dry-run) - DRY_RUN=1 - ;; + ALL=0 + ;; + -n | --dry-run) + DRY_RUN=1 + ;; --no-dry-run) - DRY_RUN=0 - ;; + DRY_RUN=0 + ;; --) - shift - break - ;; + shift + break + ;; *) - # unknown option - ;; -esac -shift + # unknown option + ;; + esac + shift done git_config_get_cond() { @@ -110,6 +109,7 @@ git_config_get_bool_cond() { ;; *) printf -- '%s' "$raw" + ;; esac } @@ -129,12 +129,12 @@ COMMITS="$(git_config_get_posint_cond redate.commits "${COMMITS:-}" 5)" || exit DEBUG="$(git_config_get_bool_cond redate.debug "${DEBUG:-}" 0)" || exit LIMITCHUNKS="$(git_config_get_posint_cond redate.limit "${LIMITCHUNKS:-}" 20)" || exit -croak () { +croak() { # `die` is from git-sh-setup die "${dashless}: $*" } -GIT_DIR="${GIT_DIR:-$(git rev-parse --git-dir 2>/dev/null)}" +GIT_DIR="${GIT_DIR:-$(git rev-parse --git-dir 2> /dev/null)}" if ! tmpfile=$(mktemp "${GIT_DIR:+${GIT_DIR}/}GIT_REDATE.XXXXXXXXXX") || ! [ -f "$tmpfile" ]; then croak "could not get tmpfile=[$tmpfile]" @@ -177,13 +177,11 @@ else fi datefmt=%cI -if [ "$(git log -n1 --pretty=format:"$datefmt")" = "$datefmt" ]; -then +if [ "$(git log -n1 --pretty=format:"$datefmt")" = "$datefmt" ]; then datefmt=%ci fi -if [ "${ALL}" -eq 1 ]; -then +if [ "$ALL" -eq 1 ]; then git log --pretty=format:"$datefmt | %H | %s" > "$tmpfile" else git log -n "$COMMITS" --pretty=format:"$datefmt | %H | %s" > "$tmpfile" @@ -197,39 +195,36 @@ COLITER=0 COUNTCOMMITS=$(awk 'END {print NR}' "$tmpfile") while read -r commit || [ -n "$commit" ]; do - - IFS="|" read -r date hash _ < Date: Thu, 7 Sep 2023 20:59:06 -0400 Subject: [PATCH 15/23] fix: add message for last chunk to be collected --- git-redate | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/git-redate b/git-redate index 968147f..3a6e4f1 100755 --- a/git-redate +++ b/git-redate @@ -239,6 +239,10 @@ END fi done < "$tmpfile" +if [ "$COLITER" -gt 0 ]; then + debug "Chunk $COLITER Finished" +fi + rc=0 ITERATOR=1 while [ "$ITERATOR" -le "$COLITER" ]; do From cbd8736ee425025087ff4fe608d4e033a9012b62 Mon Sep 17 00:00:00 2001 From: Matt Schreiber Date: Thu, 7 Sep 2023 21:48:32 -0400 Subject: [PATCH 16/23] feat: permit specifying commit ranges rather than numbers of commits starting from HEAD (or all commits). --- git-redate | 45 +++++++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/git-redate b/git-redate index 3a6e4f1..83fe960 100755 --- a/git-redate +++ b/git-redate @@ -60,7 +60,7 @@ while [ "$#" -ge 1 ]; do break ;; *) - # unknown option + break ;; esac shift @@ -166,15 +166,16 @@ else } fi -if [ "$ALL" = 1 ]; then - apply_redate() { - run git filter-branch -f --env-filter "${1?}" -- --all - } -else - apply_redate() { - run git filter-branch -f --env-filter "${1?}" "HEAD~${COMMITS}..HEAD" - } -fi +list_commits() { + git rev-list "$@" --no-commit-header --pretty=format:"$datefmt | %H | %s" +} + +apply_redate() { + env_filter="${1?}" || return + shift + + run git filter-branch -f --env-filter "$env_filter" -- "$@" +} datefmt=%cI if [ "$(git log -n1 --pretty=format:"$datefmt")" = "$datefmt" ]; then @@ -182,11 +183,27 @@ if [ "$(git log -n1 --pretty=format:"$datefmt")" = "$datefmt" ]; then fi if [ "$ALL" -eq 1 ]; then - git log --pretty=format:"$datefmt | %H | %s" > "$tmpfile" -else - git log -n "$COMMITS" --pretty=format:"$datefmt | %H | %s" > "$tmpfile" + set -- --all +elif [ "$#" -eq 0 ]; then + # Don't use `--max-count="$COMMITS"`, as `git filter-branch` passes options + # through to both `git rev-list` (which recognizes `--max-count`) *and* + # `git rev-parse` (which does not -- or, rather, which treats + # `--max-counts="$COUNT"` as a revision to parse). + if count="$(list_commits --count HEAD 2>/dev/null)"; then + if [ "$count" -lt "$COMMITS" ]; then + COMMITS="$count" + fi + fi + + if [ "$COMMITS" -eq 1 ]; then + set -- HEAD + else + set -- "HEAD~${COMMITS}..HEAD" + fi fi +list_commits "$@" > "$tmpfile" || exit + git_editor "$tmpfile" || exit ITER=0 @@ -251,7 +268,7 @@ while [ "$ITERATOR" -le "$COLITER" ]; do debug "Chunk $ITERATOR/$COLITER Started" - apply_redate "$each" || rc="$?" + apply_redate "$each" "$@" || rc="$?" debug "Chunk $ITERATOR/$COLITER Finished" From 0104765efb68c01cfceda8d697be70122900609c Mon Sep 17 00:00:00 2001 From: Matt Schreiber Date: Thu, 7 Sep 2023 22:19:10 -0400 Subject: [PATCH 17/23] chore: remove extraneous whitespace --- git-redate | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-redate b/git-redate index 83fe960..8511290 100755 --- a/git-redate +++ b/git-redate @@ -178,7 +178,7 @@ apply_redate() { } datefmt=%cI -if [ "$(git log -n1 --pretty=format:"$datefmt")" = "$datefmt" ]; then +if [ "$(git log -n1 --pretty=format:"$datefmt")" = "$datefmt" ]; then datefmt=%ci fi From 0cdd22ebe48e6f5c07464fee92b16ff266a88d30 Mon Sep 17 00:00:00 2001 From: Matt Schreiber Date: Thu, 7 Sep 2023 22:19:54 -0400 Subject: [PATCH 18/23] feat: show examples of usage modes in help text --- git-redate | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/git-redate b/git-redate index 8511290..1ec05ac 100755 --- a/git-redate +++ b/git-redate @@ -6,9 +6,14 @@ dashless="${dashless%%-*} ${dashless#*-}" # This variable is consulted by the code in "git-sh-setup", which is sourced # below -- we don't need to export the variable. # shellcheck disable=SC2034 -OPTIONS_SPEC="${dashless} [] +OPTIONS_SPEC="\ +${dashless} [] [[--] ] -${dashless} changes the dates of one or more git commits according to an interactively-specified scheme. +Change the dates of one or more git commits according to an interactively-specified scheme. + +${dashless} --all +${dashless} --count 10 +${dashless} HEAD~10..HEAD~6 -- d,debug show diagnostic output c,commits= number of commits to re-date (default: 5) From 486b5313c32c4dbb4177b959a82c710b6a31036a Mon Sep 17 00:00:00 2001 From: Matt Schreiber Date: Thu, 7 Sep 2023 22:22:45 -0400 Subject: [PATCH 19/23] doc: describe new options and configuration params --- README.md | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 034b2bc..8252491 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,38 @@ For window's users, you may paste the file into `${INSTALLATION_PATH}\mingw64\li Simply run: `git redate --commits [[number of commits to view]]`. You'll have to force push in order for your commit history to be rewritten. -To be able to edit all the commits at once add the --all option: `git redate --all` +To be able to edit all the commits at once add the --all option: `git redate --all`. This option can be negated with `--no-all`. + +You can also specify a range of commits[^git-gitrevisions-help] as a positional parameter, instead of `--count` or `--all`: `git redate HEAD~10..HEAD~6` **Make sure to run this on a clean working directory otherwise it won't work.** The `--commits` (a.k.a. `-c`) argument is optional, and defaults to 5 if not provided. + +> **Note** +> See `git redate -h` for further usage information. + +## Configuration + +You may set default values for various `git redate` options in your Git client configuration.[^git-config-help] + +The available options are: + +- `redate.all`: takes a boolean value (`true`, `false`, `yes`, `no`, etc.[^git-config-help]). When enabled, `git redate` defaults to editing all commits. +- `redate.commits`: takes a positive integer. Used as the default number of commits to edit. +- `redate.debug`: takes a boolean value. When enabled, `git redate` defaults to issuing extra diagnostic information to standard error. +- `redate.limit`: takes a positive integer. Used as the default number of commits to modify in a single `git filter-branch` operation. + +Example configuration: + +```INI +[redate] +all = no +commits = 10 +debug = true +limit = 35 +``` + +[^git-gitrevisions-help]: See [`git help gitrevisions`](https://git-scm.com/docs/gitrevisions) for various ways to spell commit ranges. +[^git-config-help]: See [`git help config`](https://git-scm.com/docs/git-config) for more info on configuring your Git client. From a9b06b77dee9feb5df0626cfafe77da819173dca Mon Sep 17 00:00:00 2001 From: Matt Schreiber Date: Fri, 3 Nov 2023 11:37:18 -0400 Subject: [PATCH 20/23] fix: omit previously-filtered commits when generating the interactive commit selection list. --- README.md | 3 +++ git-redate | 23 ++++++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8252491..ab4334c 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ The available options are: - `redate.commits`: takes a positive integer. Used as the default number of commits to edit. - `redate.debug`: takes a boolean value. When enabled, `git redate` defaults to issuing extra diagnostic information to standard error. - `redate.limit`: takes a positive integer. Used as the default number of commits to modify in a single `git filter-branch` operation. +- `redate.original`: takes a namespace. Used as the namespace where the re-dated commits will be stored.[^git-filter-branch-help] Example configuration: @@ -48,7 +49,9 @@ all = no commits = 10 debug = true limit = 35 +original = refs/redate ``` [^git-gitrevisions-help]: See [`git help gitrevisions`](https://git-scm.com/docs/gitrevisions) for various ways to spell commit ranges. [^git-config-help]: See [`git help config`](https://git-scm.com/docs/git-config) for more info on configuring your Git client. +[^git-filter-branch-help]: See [`git help filter-branch`](https://git-scm.com/docs/git-filter-branch) for more info on `git filter-branch` options. diff --git a/git-redate b/git-redate index 1ec05ac..e025611 100755 --- a/git-redate +++ b/git-redate @@ -15,11 +15,12 @@ ${dashless} --all ${dashless} --count 10 ${dashless} HEAD~10..HEAD~6 -- -d,debug show diagnostic output -c,commits= number of commits to re-date (default: 5) -l,limit= number of commits to re-date in a single batch (default: 20) -a,all re-date all commits -n,dry-run show re-dating commands, but do not execute them +d,debug show diagnostic output +c,commits= number of commits to re-date (default: 5) +l,limit= number of commits to re-date in a single batch (default: 20) +a,all re-date all commits +n,dry-run show re-dating commands, but do not execute them +original= namespace where original commits will be stored (default: refs/original) " # Allow running this command from a subdirectory of the working tree @@ -60,6 +61,10 @@ while [ "$#" -ge 1 ]; do --no-dry-run) DRY_RUN=0 ;; + --original) + ORIGINAL="$2" + shift + ;; --) shift break @@ -68,6 +73,7 @@ while [ "$#" -ge 1 ]; do break ;; esac + shift done @@ -133,6 +139,7 @@ ALL="$(git_config_get_bool_cond redate.all "${ALL:-}" 0)" || exit COMMITS="$(git_config_get_posint_cond redate.commits "${COMMITS:-}" 5)" || exit DEBUG="$(git_config_get_bool_cond redate.debug "${DEBUG:-}" 0)" || exit LIMITCHUNKS="$(git_config_get_posint_cond redate.limit "${LIMITCHUNKS:-}" 20)" || exit +ORIGINAL="$(git_config_get_cond redate.original "${ORIGINAL:-}" refs/original)" || exit croak() { # `die` is from git-sh-setup @@ -172,14 +179,16 @@ else fi list_commits() { - git rev-list "$@" --no-commit-header --pretty=format:"$datefmt | %H | %s" + # XXX `--exclude` must come before `--all`, etc.; it only affects + # *subsequent* commit-filtering options. + git rev-list --exclude "${ORIGINAL}/*" "$@" --no-commit-header --pretty=format:"$datefmt | %H | %s" } apply_redate() { env_filter="${1?}" || return shift - run git filter-branch -f --env-filter "$env_filter" -- "$@" + run git filter-branch -f --original "$ORIGINAL" --env-filter "$env_filter" -- "$@" } datefmt=%cI From 7ca6aecab9229ff346a68689f108a7a79e8d18a5 Mon Sep 17 00:00:00 2001 From: Matt Schreiber Date: Fri, 3 Nov 2023 11:38:37 -0400 Subject: [PATCH 21/23] feat: support signing rewritten commits by passing the `-S`/`--gpg-sign` flag through to `commit-tree`. --- README.md | 2 ++ git-redate | 50 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ab4334c..e72e423 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ The available options are: - `redate.debug`: takes a boolean value. When enabled, `git redate` defaults to issuing extra diagnostic information to standard error. - `redate.limit`: takes a positive integer. Used as the default number of commits to modify in a single `git filter-branch` operation. - `redate.original`: takes a namespace. Used as the namespace where the re-dated commits will be stored.[^git-filter-branch-help] +- `redate.gpgsign`: takes a boolean value. When enabled, `git redate` defaults to signing re-dated commits. Example configuration: @@ -50,6 +51,7 @@ commits = 10 debug = true limit = 35 original = refs/redate +gpgsign = yes ``` [^git-gitrevisions-help]: See [`git help gitrevisions`](https://git-scm.com/docs/gitrevisions) for various ways to spell commit ranges. diff --git a/git-redate b/git-redate index e025611..c7f2f57 100755 --- a/git-redate +++ b/git-redate @@ -21,6 +21,8 @@ l,limit= number of commits to re-date in a single batch (default: 20 a,all re-date all commits n,dry-run show re-dating commands, but do not execute them original= namespace where original commits will be stored (default: refs/original) +S,gpg-sign? sign commits (negate with --no-gpg-sign) +no-gpg-sign* do not sign commits " # Allow running this command from a subdirectory of the working tree @@ -65,6 +67,26 @@ while [ "$#" -ge 1 ]; do ORIGINAL="$2" shift ;; + -S | --gpg-sign) + GPGSIGN=1 + + # XXX assumes key IDs do not start with `-` characters + if [ "$#" -ge 2 ]; then + case "$2" in + -*) + # NOP, no key ID + ;; + *) + echo KEYID="$2" + KEYID="$2" + shift + ;; + esac + fi + ;; + --no-gpg-sign) + GPGSIGN=0 + ;; --) shift break @@ -140,6 +162,7 @@ COMMITS="$(git_config_get_posint_cond redate.commits "${COMMITS:-}" 5)" || exit DEBUG="$(git_config_get_bool_cond redate.debug "${DEBUG:-}" 0)" || exit LIMITCHUNKS="$(git_config_get_posint_cond redate.limit "${LIMITCHUNKS:-}" 20)" || exit ORIGINAL="$(git_config_get_cond redate.original "${ORIGINAL:-}" refs/original)" || exit +GPGSIGN="$(git_config_get_bool_cond redate.gpgsign "${GPGSIGN:-}" 0)" || exit croak() { # `die` is from git-sh-setup @@ -178,6 +201,31 @@ else } fi +run_filter_branch_base() { + run git filter-branch -f --original "$ORIGINAL" "$@" +} + +if [ "${GPGSIGN:-0}" = 1 ]; then + if [ -n "${KEYID:-}" ]; then + run_filter_branch() { + # Prevent shell injection via `KEYID` by exporting it inline and + # expanding the exported value safely within the commit filter + # command. + # shellcheck disable=SC2016 + __GIT_REDATE_KEYID="$KEYID" run_filter_branch_base --commit-filter 'git commit-tree -S"${__GIT_REDATE_KEYID?}" "$@"' "$@" + } + else + run_filter_branch() { + run_filter_branch_base --commit-filter 'git commit-tree -S "$@"' "$@" + } + fi +else + run_filter_branch() { + run_filter_branch_base "$@" + } +fi + + list_commits() { # XXX `--exclude` must come before `--all`, etc.; it only affects # *subsequent* commit-filtering options. @@ -188,7 +236,7 @@ apply_redate() { env_filter="${1?}" || return shift - run git filter-branch -f --original "$ORIGINAL" --env-filter "$env_filter" -- "$@" + run_filter_branch --env-filter "$env_filter" -- "$@" } datefmt=%cI From 7f121e18961e9152f7dab07230a4c55652821c06 Mon Sep 17 00:00:00 2001 From: Matt Schreiber Date: Fri, 3 Nov 2023 11:39:14 -0400 Subject: [PATCH 22/23] fix: normalize Git configuration settings by loading them via `git -c = --get `. --- git-redate | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/git-redate b/git-redate index c7f2f57..15914cb 100755 --- a/git-redate +++ b/git-redate @@ -123,7 +123,15 @@ git_config_get_cond() { if [ -n "$from_config" ]; then final="$from_config" else - final="${default?}" + # Normalize the default value by sending it through the `git + # config` machinery. + final="$(git -c "${setting?}=${default?}" config "$@" --get "${setting?}")" || { + case "$?" in + 128) + return 128 + ;; + esac + } fi fi From 96e51eadccc97916bd75868def9907440b9adcf0 Mon Sep 17 00:00:00 2001 From: Matt Schreiber Date: Mon, 6 Nov 2023 08:41:12 -0500 Subject: [PATCH 23/23] fix: exit if no commits matched selection criteria BREAKING CHANGE: `git redate` exits with nonzero status in case no commits were identified. --- git-redate | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/git-redate b/git-redate index 15914cb..50e5938 100755 --- a/git-redate +++ b/git-redate @@ -274,6 +274,10 @@ fi list_commits "$@" > "$tmpfile" || exit +if ! [ -s "$tmpfile" ]; then + croak "no matching commits found; nothing to re-date." +fi + git_editor "$tmpfile" || exit ITER=0