Skip to content

Instantly share code, notes, and snippets.

@Jeff-Russ
Last active December 16, 2022 19:41
Show Gist options
  • Save Jeff-Russ/5d1f08c10c235557dc502a3e78a823e0 to your computer and use it in GitHub Desktop.
Save Jeff-Russ/5d1f08c10c235557dc502a3e78a823e0 to your computer and use it in GitHub Desktop.
gitReview allows navigation through commit histories and release tags, avoiding detached HEAD annoyances.
# Copyright (c) 2022, Jeff Russ
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.
# NOTE: This script requires zsh and peco (see https://github.com/peco/peco)
# It be sourced from ~/.zshrc and the gitReviewSetup function should be called
# once per terminal session where the $PWD is under git revision control.
# One option is to place something line this in ~/.zshrc:
#
# source path/to/gitReview
#
# function useGitReview() {
# git rev-parse --is-inside-work-tree &> /dev/null && gitReviewSetup
# }
#
function git-head-detached() { # used by gitReview
# If you use in an if statement, do not use [ ] Rather...
# call this in an if like this: `if git-head-detached ; then` ...
# or in a shell-style "ternary" like: `git-head-detached && callable || other`
local result=$(git rev-parse --abbrev-ref --symbolic-full-name HEAD)
if [ "$result" = "HEAD" ]; then
if [ "$1" != "--quiet" ] ; then printf "You are in 'detached HEAD' state.\n"; fi
return 0 # 0 means no failure, which is kind of like true
else
if [ "$1" != "--quiet" ] ; then printf "HEAD is not detached.\n* $result\n"; fi
return 1 # means some failure, which is kind of like false
fi
}
function git-undetach-head() { # used by gitReview
if ! git-head-detached --quiet ; then
[ "$1" != "--quiet" ] && printf "HEAD was not detached\n"
return 0
fi
if git switch - >/dev/null 2>&1 ; then
if ! git-head-detached --quiet ; then # try checking out current branch
[ "$1" != "--quiet" ] && printf "Running \`git switch -\`...HEAD is no longer detached.\n"
return 0
# else we'll try re-checking out the current branch
fi
# else we'll try re-checking out the current branch
fi
local curr_branch="$GR_PREV_BRANCH"
if [ -z $curr_branch ] ; then
if git branch | grep main ; then
curr_branch=main
elif git branch | grep master ; then
curr_branch=master
else
[ "$1" != "--quiet" ] && printf "Unable to recover detached HEAD state! Try git checkout <branchname>\n"
return 1
fi
fi
if git checkout "$curr_branch" >/dev/null 2>&1 ; then
if ! git-head-detached --quiet ; then
[ "$1" != "--quiet" ] && printf "Running \`git checkout "$curr_branch"\`...HEAD is no longer detached.\n"
return 0
# else we are hopeless
fi
# else we are hopeless
fi
[ "$1" != "--quiet" ] && printf "Unable to recover detached HEAD state!\n"
return 1
}
function gitReviewSetup() {
# gitReviewSetup is a constructor-like function for the user function gitReview
# (and non-user functions/vars it needs), which are only available to the user this contructor
# finds it needs to be defined and can safely be used.
function _GR_CLEAR_ALL() {
GR_PREV_BRANCH=""
GR_PREV_BRANCH_COMMIT_CNT=""
GR_PREV_SHORT_HASH=""
GLOG_ITEMS=()
GLOG_MENU=()
GLOG_MENU_IDX=-1
}
_GR_CLEAR_ALL
function _GR_MENU_REFRESH() {
local repopulated=false
if ! git-head-detached --quiet ; then
# Normally we would repopulate GLOG_ITEMS (and, by extension, GLOG_MENU which is a
# decorated copy of it) the GLOG_MENU (via ) whenever the current branch is not
# the same as the last branch. But we cannot determine the branch when in detached
# HEAD state, yet we still want this function to be runnable in this case so we
# can move the '>' log pointer if need be.
local curr_branch="$(git symbolic-ref --short HEAD)"
local curr_branch_commmit_cnt=$(git rev-list --count "$curr_branch")
if [[ "$curr_branch" != "$GR_PREV_BRANCH" ]] || [[ "$curr_branch_commmit_cnt" != "$GR_PREV_BRANCH_COMMIT_CNT" ]] ; then
repopulated=true
if [ "$1" != "--quiet" ] && printf "repopulating gitReview log..."
GR_PREV_BRANCH="$curr_branch"
GR_PREV_BRANCH_COMMIT_CNT="$curr_branch_commmit_cnt"
GLOG_ITEMS=("${(@f)$(git --no-pager log --tags --pretty='%h%d %as (%an) %s')}")
fi
fi
local curr_short_hash=$(git rev-parse --short HEAD)
local repoint=false
if [[ "$curr_short_hash" != "$GR_PREV_SHORT_HASH" ]] ; then
if [ "$1" != "--quiet" ] && printf "moving log pointer..."
repoint=true
GR_PREV_SHORT_HASH="$curr_short_hash"
fi
if $repopulated || $repoint; then
GLOG_MENU=()
for i in {1..$#GLOG_ITEMS}; do
local short_hash="${GLOG_ITEMS[${i}]%% *}"
local prepend=" [$i]"
if [[ $short_hash == $curr_short_hash ]] ; then
GLOG_MENU_IDX=$i
prepend=">[$i]"
fi
GLOG_MENU[$i]="${prepend} ${GLOG_ITEMS[$i]}"
done
if [ "$1" != "--quiet" ] && printf "done.\n"
fi
}
if git-head-detached --quiet; then
_GR_CLEAR_ALL
printf "NOTE: cannot generate complete log menu for gitReview because of detached HEAD state.\n"
printf "If you need to run gitReview and you don't have any unsaved changes, first try running:\n git-undetach-head && gitReviewSetup\n"
else
printf "setting up gitReview..."
GR_CURR_BRANCH=$(git symbolic-ref --short HEAD)
_GR_MENU_REFRESH --quiet
function gitReview()
{
if [ "$1" = "--help" ];then
printf "gitReview allows navigation through commit histories with easy \n"
printf "(often automatically performed) recovery from detached HEAD state.\n\n"
printf "gitReview (no args) You'll be prompted to select a commit from a list of all\n"
printf " commits on the current branch. Selection is made via peco's search.\n\n"
printf "gitReview --tag You'll be prompted to select a commit from a list of only commits\n"
printf "gitReview -t with tags on the current branch. Selection is made via peco's search.\n\n"
printf "gitReview --tag=<n> where n is any string without spaces (no quotes allowed) allows you\n"
printf "gitReview -t=<n> to directly select a commit by it's tag value (such as a version number)\n\n"
printf "gitReview -i=<i> Directly select commit by index in output (line number, where first is 1).\n\n"
printf "gitReview --remote=remotename/branchname Display the remote log in the same formatting as local\n"
printf "gitReview -r=remotename/branchname (these are view only - no checking out hashes)\n"
printf " If you add ^branchname as second arg, you'll only see\n"
printf " newly fetched (but not pulled/rebased parts of the log),\n"
printf " but this is equivalent to -r=branchname..origin/branchname\.\nn"
printf "gitReview --remote Without a specified value, these default to\n"
printf "gitReview -r to the first line of output from \`git remote\`n\n"
return 0
fi
_GR_MENU_REFRESH
if [ $# -eq 0 ] || [[ "$1" == --tag* ]] || [[ "$1" == "-t"* ]] || [[ "$1" == -i=* ]] ; then
local selected
if [ $# -eq 0 ] ; then
if which peco >/dev/null 2>&1 ; then
current_i="$GLOG_I"
selected=$(printf '%s\n' "${GLOG_MENU[@]}" | peco --initial-index=$((GLOG_MENU_IDX - 1)) --prompt="select a commit") # see: peco --help
else
printf "peco not installed! see https://github.com/peco/peco\n"
return 1
fi
elif [[ "$1" == -i=* ]] ; then
local i=${1#*=}
selected="${GLOG_MENU[$i]}"
elif [[ "$1" == "--tag="* ]] || [[ "$1" == "-t="* ]] ; then
local tag=${1#*=}
selected=$(printf '%s\n' "${GLOG_MENU[@]}" | grep "tag: $tag" | head -1)
else # [[ "$1" == --tag* ]] ; then
if which peco >/dev/null 2>&1 ; then
selected=$(printf '%s\n' "${GLOG_MENU[@]}" | grep "tag: " | peco --initial-index=$((GLOG_MENU_IDX - 1)) --prompt="select a commit") # see: peco --help
else
printf "peco not installed! See https://github.com/peco/peco\n"
return 1
fi
fi
[ -z "$selected" ] && printf "No selection\n" && return 0;
local new_i=$(echo "$selected" | cut -d "[" -f2 | cut -d "]" -f1)
if [ $new_i -eq 1 ]; then
printf "running \`git-undetach-head\`\n"
if git-undetach-head --quiet; then
_GR_MENU_REFRESH --quiet
return 0
else
printf "Unable to recover detached HEAD state!\n"
printf "If you need to run gitReview and you don't have any unsaved changes, first try running:\n git-undetach-head && gitReviewSetup\n"
return 1
fi
fi
[[ ${selected:0:1} == ">" ]] && printf "That is not a different commmit\n" && return 0;
local shorthash=$(echo ${selected:1} | cut -d " " -f 2)
git switch "$shorthash" --detach
_GR_MENU_REFRESH --quiet
elif [[ "$1" == --remote* ]] || [[ "$1" == "-r"* ]] ; then # view remote log (view only)
local remotename_branchname
if [[ "$1" == --remote=* ]] || [[ "$1" == "-r="* ]] ; then
remotename_branchname=${1#*=}
else
if ! git-head-detached --quiet ; then
local curr_branch="$(git symbolic-ref --short HEAD)"
local remotename="$(git remote | head -n 1)" # gets first line of output
[ -z "$remotename" ] && printf "No git remote found!\n" && return 1;
remotename_branchname="${remotename}/${curr_branch}"
fi
fi
shift
echo "git log $remotename_branchname $@ --tags --pretty='%C(auto)%h%d %as (%an) %s'"
git log $remotename_branchname $@ --tags --pretty='%C(auto)%h%d %as (%an) %s'
return 0
else
printf "Option not recognized.\n\n"
gitReview --help
return 1
fi
}
printf "done.\n"
fi
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment