From 00000620fb85121d37bdcd8a9184153a7869e78a Mon Sep 17 00:00:00 2001 From: Fee Gevaert Date: Fri, 6 Oct 2023 17:06:38 +0200 Subject: [PATCH 1/4] made rebasing functionality --- git-linearize | 42 ++++++++++++++++++++++++++++++++++++++++-- test/linearize.bats | 21 +++++++++++++++++++++ testytest.sh | 29 +++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 2 deletions(-) create mode 100755 testytest.sh diff --git a/git-linearize b/git-linearize index 877b53f..5744f56 100755 --- a/git-linearize +++ b/git-linearize @@ -22,6 +22,7 @@ EL_FORMAT="%07d0" VERBOSE_LOG=0 EL_CMD="linearize" EL_IF_BRANCH="" +EL_REBASE_SPLITS=true while test $# -gt 0; do case "$1" in @@ -43,6 +44,7 @@ while test $# -gt 0; do echo " --short use shorter 6 digit prefix (quick mode)" echo " --format [format] specify your own prefix format (pritnf style)" echo " --if-branch [name] only run if the current branch is [name]" + echo " --no-rebase do not rebase branches that split off of this branch onto the new linearized branch" echo "" echo " All command generally support all general options. For example, specifying --format to --install-hook means" echo " that git-linearize will be called with the same format in the future when triggered by the hook." @@ -79,6 +81,10 @@ while test $# -gt 0; do shift VERBOSE_LOG=1 ;; + --no-rebase) + shift + EL_REBASE_SPLITS=false + ;; *) break ;; @@ -226,6 +232,20 @@ function cmd_linearize() { # Remember which branch we are on pre_branch=$(git branch --show-current) + if $EL_REBASE_SPLITS; then + split_commits=() + for branch in $(git for-each-ref --format="%(refname:short)" refs/heads/); do + if [ $branch == $pre_branch ]; then + continue + fi + # merge_base=$(git merge-base "${commits[0]}" "$branch") + merge_base=$(git merge-base $pre_branch $branch) + echodebug "Found merge base $merge_base for branch $branch" + split_commits+=("$merge_base") + done + echodebug "Split commits: ${split_commits[*]}" + fi + # Find start commit did_reset=0 i=0 @@ -252,7 +272,7 @@ function cmd_linearize() { git branch -D extremely-linear | debug || true git checkout -b extremely-linear | debug - # Create a stash of the local unasved state + # Create a stash of the local unsaved state # Will be restored at the end if we had any unsaved changes stashed_commit="$(git stash create)" @@ -270,7 +290,25 @@ function cmd_linearize() { new_sha=$(git rev-parse HEAD) echoinfo "[x] $sha1 is now $new_sha" - fi + + if $EL_REBASE_SPLITS; then + if [[ " ${split_commits[*]} " =~ " ${sha1} " ]]; then + # List all branches that share this commit + # Then loop through them and rebase them onto the new commit. + echodebug "branches $EL_REBASE_SPLITS that contain this commit: $(git branch --contains $sha1)" + for branch in $(git branch --contains $sha1); do + if [ $branch != $pre_branch ]; then + echodebug "rebasing $branch onto $new_sha" + git switch $branch | debug + git rebase --onto $new_sha $sha1 | debug + git switch extremely-linear | debug + fi + done + else + echo "$sha1 not in $split_commits" + fi + fi + fi # skip if hash is already nice done if ((did_reset)); then diff --git a/test/linearize.bats b/test/linearize.bats index a7b293f..6cc2193 100644 --- a/test/linearize.bats +++ b/test/linearize.bats @@ -159,3 +159,24 @@ load funcs.bash run_git_linearize "--short -v" grep "unsaved" Date: Fri, 6 Oct 2023 17:39:01 +0200 Subject: [PATCH 2/4] used associative array to prevent unnecessary rebasing which messes up everything --- git-linearize | 24 ++++++++++-------------- testytest.sh | 5 ++++- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/git-linearize b/git-linearize index 5744f56..ed977c4 100755 --- a/git-linearize +++ b/git-linearize @@ -233,7 +233,7 @@ function cmd_linearize() { pre_branch=$(git branch --show-current) if $EL_REBASE_SPLITS; then - split_commits=() + declare -A split_commit_map for branch in $(git for-each-ref --format="%(refname:short)" refs/heads/); do if [ $branch == $pre_branch ]; then continue @@ -241,9 +241,8 @@ function cmd_linearize() { # merge_base=$(git merge-base "${commits[0]}" "$branch") merge_base=$(git merge-base $pre_branch $branch) echodebug "Found merge base $merge_base for branch $branch" - split_commits+=("$merge_base") + split_commit_map["$merge_base"]="$branch" done - echodebug "Split commits: ${split_commits[*]}" fi # Find start commit @@ -292,20 +291,17 @@ function cmd_linearize() { echoinfo "[x] $sha1 is now $new_sha" if $EL_REBASE_SPLITS; then - if [[ " ${split_commits[*]} " =~ " ${sha1} " ]]; then + # https://stackoverflow.com/a/46243464/14681457 + if [[ -n "${split_commit_map[$sha1]+unset}" ]]; then # List all branches that share this commit # Then loop through them and rebase them onto the new commit. - echodebug "branches $EL_REBASE_SPLITS that contain this commit: $(git branch --contains $sha1)" - for branch in $(git branch --contains $sha1); do - if [ $branch != $pre_branch ]; then - echodebug "rebasing $branch onto $new_sha" - git switch $branch | debug - git rebase --onto $new_sha $sha1 | debug - git switch extremely-linear | debug - fi - done + branch=${split_commit_map[$sha1]} + git switch $branch | debug + git rebase --onto $new_sha $sha1 | debug + git switch extremely-linear | debug else - echo "$sha1 not in $split_commits" + # echo "$sha1 not in ${split_commit_map[*]}" + printf "$sha1 not in %s\n" "${!split_commit_map[@]}" | debug fi fi fi # skip if hash is already nice diff --git a/testytest.sh b/testytest.sh index 2699c17..cf6d9c3 100755 --- a/testytest.sh +++ b/testytest.sh @@ -10,6 +10,9 @@ make_dummy_commit make_dummy_commit git switch main make_dummy_commit +git checkout -b branch-2 +make_dummy_commit +git switch main make_dummy_commit # a-b-e-f main # \ @@ -26,4 +29,4 @@ else echo "nay! $(git merge-base main new-branch)" fi -# git log --graph --all \ No newline at end of file +git log --graph --all \ No newline at end of file From 00000631184f9d1540c6e18e65bf5262c9692fab Mon Sep 17 00:00:00 2001 From: Fee Gevaert Date: Fri, 6 Oct 2023 17:49:12 +0200 Subject: [PATCH 3/4] linearize: fixed comment --- git-linearize | 3 +-- testytest.sh | 8 +++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/git-linearize b/git-linearize index ed977c4..550e445 100755 --- a/git-linearize +++ b/git-linearize @@ -293,8 +293,7 @@ function cmd_linearize() { if $EL_REBASE_SPLITS; then # https://stackoverflow.com/a/46243464/14681457 if [[ -n "${split_commit_map[$sha1]+unset}" ]]; then - # List all branches that share this commit - # Then loop through them and rebase them onto the new commit. + # rebase the branch onto the new commit branch=${split_commit_map[$sha1]} git switch $branch | debug git rebase --onto $new_sha $sha1 | debug diff --git a/testytest.sh b/testytest.sh index cf6d9c3..4336b3f 100755 --- a/testytest.sh +++ b/testytest.sh @@ -17,7 +17,7 @@ make_dummy_commit # a-b-e-f main # \ # c-d new-branch -run_git_linearize "-v --short" +run_git_linearize "--short" # 0-1-2 main # a-c-d new-branch # git switch new-branch @@ -29,4 +29,10 @@ else echo "nay! $(git merge-base main new-branch)" fi +if [[ $(git merge-base main branch-2) == 000002* ]]; then + echo "yay!" +else + echo "nay! $(git merge-base main new-branch)" +fi + git log --graph --all \ No newline at end of file From 2f4346bc397cda03ddde1f7826d19dcd843bcf85 Mon Sep 17 00:00:00 2001 From: Fee Gevaert Date: Thu, 12 Oct 2023 18:22:34 +0200 Subject: [PATCH 4/4] made branchless-dependent version --- git-branchless-linearize | 347 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100755 git-branchless-linearize diff --git a/git-branchless-linearize b/git-branchless-linearize new file mode 100755 index 000000000..b9234e5 --- /dev/null +++ b/git-branchless-linearize @@ -0,0 +1,347 @@ +#!/bin/bash +# +# This script makes sure that all commit hashes have nice and incremental prefixes. +# +# Move this script to somewhere on your path, and run it as "git linearize". +# +# Built by Gustav Westling (@zegl) and friends. +# https://github.com/zegl/extremely-linear +# +# Built with https://github.com/not-an-aardvark/lucky-commit +# +# Remember, this is a script that you've downloaded from the internet. +# Don't run it on anything important unless you know what you're doing. +# (Which I'm sure that you do, only epic rockstar ninja developers would use a program like this one). +# +# + +set -euo pipefail + +# Argument parsing :-) +EL_FORMAT="%07d0" +VERBOSE_LOG=0 +EL_CMD="linearize" +EL_IF_BRANCH="" +EL_REBASE_SPLITS=true + +while test $# -gt 0; do + case "$1" in + -h | --help) + LIGHT_GREEN='\033[1;32m' + NC='\033[0m' # No Color + echo -e "${LIGHT_GREEN}git linearize using git-branchless - Create an extremely linear git history - github.com/zegl/extremely-linear${NC}" + echo "" + echo "git linearize [command] [options]" + echo "" + echo "command: (default command is to run the linearization process)" + echo " -h, --help show brief help" + echo " --install-hook installs git-linearize as a post-commit hook (current repo only)" + echo " --make-epoch makes the current commit the linearized epoch (00000000), use to adopt git-linearize in" + echo " existing repositories without having to rewrite the full history" + echo "" + echo "general options (for all commands):" + echo " -v, --verbose more verbose logging" + echo " --short use shorter 6 digit prefix (quick mode)" + echo " --format [format] specify your own prefix format (pritnf style)" + echo " --full Rewrite entire history, including public commits on the main branch" + echo " --no-rebase do not rebase branches that split off of this branch onto the new linearized branch" + echo "" + echo " All command generally support all general options. For example, specifying --format to --install-hook means" + echo " that git-linearize will be called with the same format in the future when triggered by the hook." + exit 0 + ;; + --short) + EL_FORMAT="%06d" + shift + ;; + --format) + shift + if test $# -gt 0; then + export EL_FORMAT=$1 + else + echo "no format specified" + exit 1 + fi + shift + ;; + --install-hook) + shift + EL_CMD="install_hook" + ;; + --make-epoch) + shift + EL_CMD="make_epoch" + ;; + --if-branch) + shift + EL_IF_BRANCH=$1 + shift + ;; + -v | --verbose) + shift + VERBOSE_LOG=1 + ;; + --) + shift + EL_REBASE_SPLITS=false + ;; + *) + break + ;; + esac +done + +######################################################################################################################## +# Helper functions # +######################################################################################################################## +function echoinfo() { + LIGHT_GREEN='\033[1;32m' + NC='\033[0m' # No Color + printf "${LIGHT_GREEN}%s${NC}\n" "$1" +} + +function echoerr() { + RED='\033[0;31m' + NC='\033[0m' # No Color + printf "${RED}%s${NC}\n" "$1" >&2 +} + +function echodebug() { + if ((VERBOSE_LOG)); then + echoinfo "$1" + fi +} + +function debug() { + if ((VERBOSE_LOG)); then + cat >&2 + fi +} + +function git_root() { + git rev-parse --show-toplevel +} + +function git_current_branch() { + git rev-parse --abbrev-ref HEAD +} + +function linearize_root_commit() { + # shellcheck disable=SC2059 # Disabled because EL_FORMAT contains the format + printf "$EL_FORMAT" 0 +} + +function find_format_base() { + local pattern='%0[0-7]*([bxd])' + + if [[ $EL_FORMAT =~ $pattern ]]; then + local match="${BASH_REMATCH[1]}" + # echo $match + case "$match" in + b) echo 2 ;; # Binary format specifier + x) echo 16 ;; # Hexadecimal format specifier + o) echo 8 ;; # Octal format specifier + d) echo 10 ;; # Decimal format specifier + *) echoerr "[x] Your format was kind of ok, but I still won't do anything with it >:)" + exit 1 ;; # Unknown format specifier + esac + else + echoerr "[x] Could not understand your format" + exit 1 # No valid printf pattern found + fi +} + +# function find_root_on_current_branch() { +# # Find the most recent epoch commit (if any) on the current branch +# # Returns the full commit hash of the root commit +# git log --oneline --no-abbrev-commit | +# grep "^$(linearize_root_commit)" | +# head -n1 | +# awk '{print $1}' +# } + +# function git_has_linearize_root() { +# # Use custom root if there the repositoru has a 00000000-commit that has a parent (is not the repo root) +# if git show "$(find_root_on_current_branch)^" >/dev/null 2>&1; then +# return 0 +# else +# return 1 +# fi +# } + +# function git_is_ready() { +# # current directory has changes to tracked files +# if git status --porcelain | grep -v -q "??"; then +# return 1 +# fi + +# # no changes, or only changes to untracked files +# return 0 +# } + +######################################################################################################################## +# Dependencies check # +######################################################################################################################## +if ! command -v lucky_commit &>/dev/null; then + echoerr "[!] Dependency lucky_commit was not found on your PATH" + exit 1 +fi + +if ! command -v git-branchless &>/dev/null; then + echoerr "[!] Dependency git-branchless was not found on your PATH" + exit 1 +fi + +if ! git sl ; then + echoerr "[!] Git branchless not initialized. Will do it for you :)" + git branchless init +fi + +######################################################################################################################## +# cmd_install_hook installs git-linearize as a post-commit hook in the current repository # +# # +# Trigger with "--install-hook" # +# # +# If the arguments "--if-branch [name]" or "--format [format]" or "--short" are passed to "--install-hook" they will # +# be forwarded to the execution of git-linearize when triggered by the hook. # +######################################################################################################################## +function cmd_install_hook() { + FILE="$(git_root)/.git/hooks/post-commit" + if [ -f "$FILE" ]; then + echoerr "post-commit hook already exists at $FILE. Aborting!" + exit 1 + fi + + FORWARD_FORMAT="" + if [[ -n $EL_FORMAT ]]; then + FORWARD_IF_BRANCH="--format ${EL_FORMAT}" + fi + + cat >"$FILE" <<-EOM + #!/bin/bash + git linearize ${FORWARD_FORMAT} + EOM + chmod +x "$FILE" + + echoinfo "Installed hook to .git/hooks/post-commit!" +} + +######################################################################################################################## +# cmd_linearize is the default command of git-linearize, it rebases the current branch and gives all commits # +# incremental commit sha1 prefixes. # +# # +# Use "--if-branch [name]" to only run if the currently checked out branch matches the specified name. # +######################################################################################################################## +function cmd_linearize() { + # Check branch + # if [[ -n $EL_IF_BRANCH ]] && [[ ${EL_IF_BRANCH} != "$(git_current_branch)" ]]; then + # echodebug "[x] Current branch is $(git_current_branch), expected ${EL_IF_BRANCH}. Skipping. :-)" + # exit 0 + # fi + + # if ! git_is_ready; then + # echoerr "[x] The current git directory is not clean. Skipping. :-)" + # echoerr " Don't worry, git linearize will clean up all commits all at once later." + # exit 0 + # fi + + # if git_has_linearize_root; then + # found_root=$(find_root_on_current_branch) + # echodebug "[x] Repository has a custom root commit (${found_root}), using it as the root" + # commits=$(git log --format=format:%H --reverse "${found_root}^...HEAD") + # else + # echodebug "[x] Repository has a no custom root commit, using the repository root" + # commits=$(git log --format=format:%H --reverse) + # fi + + # Remember which branch we are on <- this does not work with HEAD nicely + pre_branch=$(git_current_branch) + fbase=$(find_format_base) + prefix=$(printf "$EL_FORMAT" 0) + flength=${#prefix} + echo $fbase + # go back unto the first properly formatted commit + while true; do + # $get the sha + # sha stands for "Short HAsh" to avoid confusion + sha=$(git rev-parse --short HEAD) + # first try printing the sha + flsha="${sha:0:$flength}" + if printf "$EL_FORMAT" $flsha >/dev/null 2>&1; then + #this still fails if the commit format is shorter than the output of git rev-parse --short + i=$(($fbase#$flsha)) + if [ $(printf "$EL_FORMAT" $i 2>/dev/null) == $flsha ]; then + echoinfo "Found first well-formatted commit! $flsha" + i=$(($i+1)) + git next > /dev/null 2>&1 + break + else + echodebug "Found commit $flsha that could format" + echodebug "However, using $EL_FORMAT, base $fbase: $i does not give good results" + fi + fi + # go to previous commit + if ! git prev > /dev/null 2>&1; then + echoinfo "Found no well-formatted commit until the root" + i=0 + break + fi + done + + echoinfo "Starting formatting" + while true; do + #lha stands for Long HAsh + lha=$(git rev-parse HEAD) + prefix=$(printf "$EL_FORMAT" $i) + echoinfo "Looking for $lha #$i" + lucky_commit $prefix + #fha stands for Formatted HAsh + fha=$(git rev-parse HEAD) + echoinfo "Formatted $lha to $fha" + git move -fs $lha > /dev/null 2>&1 # | debug + git hide $lha > /dev/null 2>&1 + i=$(($i+1)) + if ! git next > /dev/null 2>&1; then + if [ $pre_branch == "HEAD" ]; then + echoinfo "encountered a branch and don't have a target: $pre_branch, aborting" + exit 0 + fi + logs=($(git log --reverse HEAD..$pre_branch --format=format:%H)) + if [ ${#logs[@]} -eq 0 ]; then + echoinfo "All done, have a good day!" + exit 0 + elif [ ${logs[0]} == $(git rev-parse $pre_branch) ]; then + git checkout $pre_branch | debug + else + git checkout ${logs[0]} | debug + fi + fi + done +} + +######################################################################################################################## +# cmd_make_epoch can be run with the --make-epoch flag # +# # +# Marks the current commit as the epoch (00000000), instead of using the repository root commit as the epoch. # +# Respects the --format/--short flags # +######################################################################################################################## +function cmd_make_epoch() { + prefix=$(linearize_root_commit) + echodebug "[x] Fixing $(git rev-parse HEAD) (looking for prefix=$prefix)" + lucky_commit "$prefix" + new_sha=$(git rev-parse HEAD) + echoinfo "[x] All done, ${new_sha} is the new git-linearize epoch :-)" +} + +# Time to run something! +case "$EL_CMD" in +linearize) + cmd_linearize + ;; +install_hook) + cmd_install_hook + ;; +make_epoch) + cmd_make_epoch + ;; +esac