Skip to content

Instantly share code, notes, and snippets.

@drmingdrmer
Last active December 15, 2019 08:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save drmingdrmer/241dd1b3655f04190b0c6d06aa1dd258 to your computer and use it in GitHub Desktop.
Save drmingdrmer/241dd1b3655f04190b0c6d06aa1dd258 to your computer and use it in GitHub Desktop.
git-gotofix: go back to an earlier commit in which <fn> is modified.
#!/bin/sh
usage()
{
cat >&2 <<-END
usage: git gotofix <fn>
Fix <fn> at the commit where <fn> is lastly modified.
And get HEAD back to where before using git-gotofix.
This script accomplish this by running a customized
git-rebase--interactive.
Example:
# go to commit in which <fn> was modified. And VIM is
# automatically opened this file.
git gotofix <fn>
# hack..hack..
# get fix ready.
git add <fn>
# apply changes and go back to where you started.
git rebase --continue
# or discard changes and restore.
git rebase --abort
END
}
clr()
{
# clr light read blabla
local black=0
local white=7
local red=1
local green=2
local brown=3
local blue=4
local purple=5
local cyan=6
local light=""
local color=$1
shift
if [ "$color" == "light" ]; then
light="tput bold; "
color=$1
shift
fi
local code=$(eval 'echo $'$color)
local cmd="${light}tput setaf $code"
local color_str="$(eval "$cmd")"
echo $color_str"$@""$(tput sgr0)"
}
shlib_init_colors()
{
Black="$( tput setaf 0)"
BlackBG="$( tput setab 0)"
DarkGrey="$( tput bold; tput setaf 0)"
LightGrey="$( tput setaf 7)"
LightGreyBG="$( tput setab 7)"
White="$( tput bold; tput setaf 7)"
Red="$( tput setaf 1)"
RedBG="$( tput setab 1)"
LightRed="$( tput bold; tput setaf 1)"
Green="$( tput setaf 2)"
GreenBG="$( tput setab 2)"
LightGreen="$( tput bold; tput setaf 2)"
Brown="$( tput setaf 3)"
BrownBG="$( tput setab 3)"
Yellow="$( tput bold; tput setaf 3)"
Blue="$( tput setaf 4)"
BlueBG="$( tput setab 4)"
LightBlue="$( tput bold; tput setaf 4)"
Purple="$( tput setaf 5)"
PurpleBG="$( tput setab 5)"
Pink="$( tput bold; tput setaf 5)"
Cyan="$( tput setaf 6)"
CyanBG="$( tput setab 6)"
LightCyan="$( tput bold; tput setaf 6)"
NC="$( tput sgr0)" # No Color
}
screen_width()
{
local chr="${1--}"
chr="${chr:0:1}"
local width=$(tput cols 2||echo 80)
width="${COLUMNS:-$width}"
echo $width
}
hr()
{
# generate a full screen width horizontal ruler
local width=$(screen_width)
printf -vl "%${width}s\n" && echo ${l// /$chr};
}
remove_color()
{
# remove color control chars from stdin or first argument
local sed=gsed
which -s $sed || sed=sed
local s="$1"
if [ -z "$s" ]; then
$sed -r "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[mGK]//g"
else
echo "$s" | remove_color
fi
}
text_hr()
{
# generate a full screen width sperator line with text.
# text_hr "-" "a title"
# > a title -----------------------------------------
#
# variable LR=l|m|r controls alignment
local chr="$1"
shift
local bb="$(echo "$@" | remove_color)"
local text_len=${#bb}
local width=$(screen_width)
let width=width-text_len
local lr=${LR-m}
case $lr in
m)
let left=width/2
let right=width-left
echo "$(printf -vl "%${left}s\n" && echo ${l// /$chr})$@$(printf -vl "%${right}s\n" && echo ${l// /$chr})"
;;
r)
echo "$(printf -vl "%${width}s\n" && echo ${l// /$chr})$@"
;;
*)
# l by default
echo "$@$(printf -vl "%${width}s\n" && echo ${l// /$chr})"
;;
esac
}
SHLIB_LOG_VERBOSE=1
SHLIB_LOG_FORMAT='[$(date +"%Y-%m-%d %H:%M:%S")] $level $title $mes'
die()
{
err "$@" >&2
exit 1
}
die_empty()
{
if test -z "$1"
then
shift
die empty: "$@"
fi
}
set_verbose()
{
SHLIB_LOG_VERBOSE=${1-1}
}
log()
{
local color="$1"
local title="$2"
local level="$_LOG_LEVEL"
shift
shift
local mes="$@"
local NC="$(tput sgr0)"
if [ -t 1 ]; then
title="${color}${title}${NC}"
level="${color}${level}${NC}"
fi
eval "echo \"$SHLIB_LOG_FORMAT\""
}
dd()
{
debug "$@"
}
debug()
{
if [ ".$SHLIB_LOG_VERBOSE" = ".1" ]; then
local LightCyan="$(tput bold ; tput setaf 6)"
_LOG_LEVEL=DEBUG log "$LightCyan" "$@" >&2
fi
}
info()
{
local Brown="$(tput setaf 3)"
_LOG_LEVEL=" INFO" log "$Brown" "$@"
}
ok() {
local Green="$(tput setaf 2)"
_LOG_LEVEL=" OK" log "${Green}" "$@"
}
err() {
local Red="$(tput setaf 1)"
_LOG_LEVEL="ERROR" log "${Red}" "$@"
}
git_hash()
{
git rev-parse $1 \
|| die "'git_hash $@'"
}
git_is_merge()
{
test $(git cat-file -p "$1" | grep "^parent " | wc -l) -gt 1
}
git_parents()
{
git rev-list --parents -n 1 ${1-HEAD} | { read self parents; echo $parents; }
}
git_rev_list()
{
# --parents
# print parent in this form:
# <commit> <parent-1> <parent-2> ..
git rev-list \
--reverse \
--topo-order \
--default HEAD \
--simplify-merges \
"$@" \
|| die "'git rev-list $@'"
}
git_tree_hash()
{
git rev-parse "$1^{tree}"
}
git_ver()
{
local git_version=$(git --version | awk '{print $NF}')
local git_version_1=${git_version%%.*}
local git_version_2=${git_version#*.}
git_version_2=${git_version_2%%.*}
printf "%03d%03d" $git_version_1 $git_version_2
}
git_working_root()
{
git rev-parse --show-toplevel
}
git_rev_exist()
{
git rev-parse --verify --quiet "$1" >/dev/null
}
git_branch_default_remote()
{
local branchname=$1
git config --get branch.${branchname}.remote
}
git_branch_default_upstream_ref()
{
local branchname=$1
git config --get branch.${branchname}.merge
}
git_branch_default_upstream()
{
git rev-parse --abbrev-ref --symbolic-full-name "$1"@{upstream}
# OR
# git_branch_default_upstream_ref "$@" | sed 's/^refs\/heads\///'
}
git_branch_exist()
{
git_rev_exist "refs/heads/$1"
}
git_head_branch()
{
git symbolic-ref --short HEAD
}
git_commit_date()
{
# git_commit_date author|commit <ref> [date-format]
# by default output author-date
local what_date="%ad"
if [ "$1" = "commit" ]; then
# commit date instead of author date
what_date="%cd"
fi
shift
local ref=$1
shift
local fmt="%Y-%m-%d %H:%M:%S"
if [ "$#" -gt 0 ]; then
fmt="$1"
fi
shift
git log -n1 --format="$what_date" --date=format:"$fmt" "$ref"
}
git_commit_copy()
{
# We're going to set some environment vars here, so
# do it in a subshell to get rid of them safely later
dd copy_commit "{$1}" "{$2}" "{$3}"
git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" |
(
read GIT_AUTHOR_NAME
read GIT_AUTHOR_EMAIL
read GIT_AUTHOR_DATE
read GIT_COMMITTER_NAME
read GIT_COMMITTER_EMAIL
read GIT_COMMITTER_DATE
export GIT_AUTHOR_NAME \
GIT_AUTHOR_EMAIL \
GIT_AUTHOR_DATE \
GIT_COMMITTER_NAME \
GIT_COMMITTER_EMAIL \
GIT_COMMITTER_DATE
# (echo -n "$annotate"; cat ) |
git commit-tree "$2" $3 # reads the rest of stdin
) || die "Can't copy commit $1"
}
git_object_type()
{
# $0 ref|hash
# output "commit", "tree" etc
git cat-file -t "$@" 2>/dev/null
}
git_object_add_by_commit_path()
{
# add an blob or tree object to target_path in index
# the object to add is specified by commit and path
local target_path="$1"
local src_commit="$2"
local src_path="$3"
local src_dir="$(dirname "$src_path")/"
local src_name="$(basename "$src_path")"
local src_treeish="$(git rev-parse "$src_commit:$src_dir")"
git_object_add_by_tree_name "$target_path" "$src_treeish" "$src_name"
}
git_object_add_by_tree_name()
{
# add an blob or tree object to target_path in index
local target_path="$1"
local src_treeish="$2"
local src_name="$3"
dd "arg: target_path: ($target_path) src_treeish: ($src_treeish) src_name: ($src_name)"
local target_dir="$(dirname $target_path)/"
local target_fn="$(basename $target_path)"
local treeish
if [ -z "$src_name" ] || [ "$src_name" = "." ] || [ "$src_name" = "./" ]; then
treeish="$src_treeish"
else
treeish=$(git ls-tree "$src_treeish" "$src_name" | awk '{print $3}')
if [ -z "$treeish" ]; then
die "source treeish not found: in tree: ($src_treeish) name: ($src_name)"
fi
fi
dd "hash of object to add is: $treeish"
if [ "$(git_object_type $treeish)" = "blob" ]; then
# the treeish imported is a file, not a dir
# first create a wrapper tree or replace its containing tree
dd "object to add is blob"
local dir_treeish
local target_dir_treeish="$(git rev-parse "HEAD:$target_dir")"
if [ -n "target_dir_treeish" ]; then
dir_treeish="$(git rev-parse "HEAD:$target_dir")"
dd "target dir presents: $target_dir"
else
dd "target dir absent"
dir_treeish=""
fi
treeish=$(git_tree_add_blob "$dir_treeish" "$target_fn" $src_treeish $src_name) || die create wrapper treeish
target_path="$target_dir"
dd "wrapper treeish: $treeish"
dd "target_path set to: $target_path"
else
dd "object to add is tree"
fi
git_treeish_add_to_prefix "$target_path" "$treeish"
}
git_treeish_add_to_prefix()
{
local target_path="$1"
local treeish="$2"
dd treeish content:
git ls-tree $treeish
git rm "$target_path" -r --cached || dd removing target "$target_path"
if [ "$target_path" = "./" ]; then
git read-tree "$treeish" \
|| die "read-tree $target_path $treeish"
else
git read-tree --prefix="$target_path" "$treeish" \
|| die "read-tree $target_path $treeish"
fi
}
git_tree_add_tree()
{
# output new tree hash in stdout
# treeish can be empty
local treeish="$1"
local target_fn="$2"
local item_hash="$3"
local item_name="$4"
{
if [ -n "$treeish" ]; then
git ls-tree "$treeish" \
| fgrep -v " $item_name"
fi
cat "040000 tree $item_hash $target_fn"
} | git mktree
}
git_tree_add_blob()
{
# output new tree hash in stdout
# treeish can be empty
local treeish="$1"
local target_fn="$2"
local blob_treeish="$3"
local blob_name="$4"
{
if [ -n "$treeish" ]; then
git ls-tree "$treeish" \
| fgrep -v " $target_fn"
fi
git ls-tree "$blob_treeish" "$blob_name" \
| awk -v target_fn="$target_fn" -F" " '{print $1" "target_fn}'
} | git mktree
}
git_workdir_save()
{
local index_hash=$(git write-tree)
# add modified file to index and read index tree
git add -u
local working_hash=$(git write-tree)
# restore index tree
git read-tree $index_hash
echo $index_hash $working_hash
}
git_workdir_load()
{
local index_hash=$1
local working_hash=$2
git_object_type $index_hash || die "invalid index hash: $index_hash"
git_object_type $working_hash || die "invalid workdir hash: $working_hash"
# First create a temp commit to restore working tree.
#
# git-read-index to index and git-reset does not work because deleted file in
# index does not apply to working tree.
#
# But there is an issue with this:
# git checkout --orphan br
# git_workdir_load
# would fails, because ORIG_HEAD is not a commit.
local working_commit=$(echo "x" | git commit-tree $working_hash) || die get working commit
git reset --hard $working_commit || die reset to tmp commit
git reset --soft ORIG_HEAD || die reset to ORIG_HEAD
git read-tree $index_hash || die "load saved index tree from $index_hash"
}
git_workdir_is_clean()
{
local untracked="$1"
if [ "$untracked" == "untracked" ]; then
[ -z "$(git status --porcelain)" ]
else
[ -z "$(git status --porcelain --untracked-files=no)" ]
fi
}
git_copy_commit()
{
git_commit_copy "$@"
}
git_diff_ln_new()
{
# output changed line number of a file: <from> <end>; inclusive:
# 27 28
# 307 309
# 350 350
#
# Usage:
#
# diff working tree with HEAD:
# git_diff_ln_new HEAD -- <fn>
#
# diff working tree with staged:
# git_diff_ln_new -- <fn>
#
# diff staged(cached) with HEAD:
# git_diff_ln_new --cached -- <fn>
#
# in git-diff output:
# for add lines:
# @@ -53 +72,8
#
# for remove lines:
# @@ -155 +179,0
git diff -U0 "$@" \
| grep '^@@' \
| awk '{
# @@ -155 +179,0
# $1 $2 $3
l = $3
gsub("^+", "", l)
# add default offset: ",1"
split(l",1", x, ",")
# inclusive line range:
x[2] = x[1] + x[2] - 1
# line remove format: @@ -155, +179,0
# do need to output line range for removed.
if (x[2] >= x[1]) {
print x[1] " " x[2]
}
}'
}
os_detect()
{
local os
case $(uname -s) in
Linux)
os=linux ;;
*[bB][sS][dD])
os=bsd ;;
Darwin)
os=mac ;;
*)
os=unix ;;
esac
echo $os
}
mac_ac_power_connection()
{
# Connected: (Yes|No)
system_profiler SPPowerDataType \
| sed '1,/^ *AC Charger Information:/d' \
| grep Connected:
}
mac_power()
{
# $0 is-battery exit code 0 if using battery.
# $0 is-ac-power exit code 0 if using ac power.
local cmd="$1"
local os=$(os_detect)
if [ "$os" != "mac" ]; then
err "not mac but: $os"
return 1
fi
case $cmd in
is-battery)
mac_ac_power_connection | grep -q No
;;
is-ac-power)
mac_ac_power_connection | grep -q Yes
;;
*)
err "invalid cmd: $cmd"
return 1
;;
esac
}
fn_match()
{
# $0 a.txt *.txt
case "$1" in
$2)
return 0
;;
esac
return 1
}
obj=$1
is_obj_file=0
linenr=0
ori_linenr=0
stashed=0
if [ -z "$obj" ] || [ "$obj" = "-h" ] || [ "$obj" = "--help" ]; then
usage
exit 1
fi
if git diff --name-only --relative HEAD --quiet; then
:
else
git stash
stashed=1
fi
if [ "$(git_object_type $obj)" = "commit" ]; then
commit=$obj
else
# it is a file path
# find the latest commit touched this file
is_obj_file=1
if [ ".$2" != "." ]; then
# gotofix <fn> <linenr>
# get the commit last time edited this line
commit=$(git blame "$obj" -L "$2",+1 -p | head -n1 | { read sha other; echo $sha; })
ori_linenr=$(git blame "$obj" -L "$2",+1 -p | head -n1 | { read sha ori_linenr other; echo $ori_linenr; })
info "going to commit: $commit $ori_linenr"
else
# find the last commit that changed any line of this file
commit=$(git log -n1 --format="%h" "$obj")
fi
fi
sed_remove_after_blank="'"'/^$/,$d'"'"
# only replace the first line
sed_edit_commit="1 s/pick /edit /"
# add quote
sed_edit_commit="'$sed_edit_commit'"
GIT_EDITOR='f(){ cat "$1" | sed '$sed_remove_after_blank' | sed '$sed_edit_commit' >"$1-tmp"; mv "$1-tmp" $1; }; f' \
git rebase --interactive $commit~
if [ "$stashed" == "1" ]; then
git stash pop || die fail to pop stash
fi
if [ "$is_obj_file" = "1" ]; then
vim "$obj" +$ori_linenr -c "normal zR"
git rebase --continue
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment