You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

776 lines
22 KiB
Bash

#!/usr/bin/env bash
# vim:ts=4:sts=4:sw=4:et
#
# Author: Hari Sekhon
# Date: circa 2006 (forked from .bashrc)
#
# https://github.com/harisekhon/bash-tools
#
# License: see accompanying Hari Sekhon LICENSE file
#
# If you're using my code you're welcome to connect with me on LinkedIn and optionally send me feedback to help steer this or other code I publish
#
# https://www.linkedin.com/in/harisekhon
#
# ============================================================================ #
# R e v i s i o n C o n t r o l - G i t
# ============================================================================ #
# Primary revision control system
#
# if svn.sh and hg.sh functions are enabled, detects and calls svn and mercurial commands if inside those repos so some of the same commands work dynamically
if [ -f ~/.github_token ]; then
GITHUB_TOKEN="$(cat ~/.github_token)"
export GITHUB_TOKEN
fi
if [ -f ~/.gitlab_token ]; then
GITLAB_API_PRIVATE_TOKEN="$(cat ~/.gitlab_token)"
export GITLAB_API_PRIVATE_TOKEN
fi
if [ -z "${GITLAB_API_ENDPOINT:-}" ]; then
export GITLAB_API_ENDPOINT="https://gitlab.com/api/v3"
fi
# set location where you check out all the github repos
export github=~/github
export GIT_PAGER="less ${LESS:-}"
# shellcheck disable=SC2230
#if [ -z "${GIT_PAGER:-}" ] && \
if type -P diff-so-fancy &>/dev/null; then
# pre-loading a pattern to 'n' / 'N' / '?' / '/' search through will force you in to pager and disregard -F / --quit-if-one-screen
#export GIT_PAGER="diff-so-fancy --color=yes | less -RFX --tabs=4 --pattern '^(Date|added|deleted|modified): '"
export GIT_PAGER="diff-so-fancy --color=yes | $GIT_PAGER"
fi
alias gitconfig="\$EDITOR ~/.gitconfig"
alias gitignore="\$EDITOR ~/.gitignore_global"
alias gitrc=gitconfig
# false positive, not calling this from xargs
# shellcheck disable=SC2032
alias add=gitadd
alias gadd='git add'
alias import=gitimport
alias co=checkout
alias commit="git commit"
alias clone="git clone"
alias gitci=commit
alias ci=commit
alias gitco=checkout
alias up=pull
alias u=up
alias pu=push
alias gitp="git push"
alias gdiff="git diff"
# bypasses diff-so-fancy, could also just pipe through | cat to disable pager and color effects
alias gdiff2="git --no-pager diff"
alias gdiffc="git diff --cached"
alias gdiffm="gdiff origin/master.."
alias gd=gdiff
alias gdc=gdiffc
alias gdo=gdiffm
alias branch="githg branch"
alias br=branch
alias fetch='git fetch'
alias stash="git stash"
alias tag="githg tag"
alias um=updatemodules
#type browse &>/dev/null || alias browse=gbrowse
alias gbrowse=gitbrowse
alias gh=gitbrowse
alias github_actions='gitbrowse actions github'
alias github_workflows='github_actions'
alias gha='github_actions'
alias ghw='github_workflows'
alias wf='cd .github/workflows/'
alias ggrep="git grep"
# much quicker to just 'cd $github; f <pattern>'
#githubls(){
# # GitHub is svn compatible, use this to list files remotely
# svn ls "https://github.com/$1.git/branches/master/"
#}
#githubgrep(){
# for repo in $(sed 's/#.*//;s/:.*//;/^[[:space:]]*$/d' "$srcdir/setup/repolist.txt"); do
# githubls "HariSekhon/$repo"
# done |
# grep "$@"
#}
# git fetch -p or git remote prune origin
alias prune="co master; git pull; git remote prune origin; git branch --merged | grep -v -e '^\\*' -e 'master' | xargs git branch -d"
# don't use this unless you are a git pro and understand unwinding history and merge conflicts
alias GRH="git reset HEAD^"
alias master="switchbranch master"
alias prod="switchbranch prod"
alias staging="switchbranch staging"
alias stage=staging
alias dev="switchbranch dev"
# equivalent of hg root
git_root(){
git rev-parse --show-toplevel
}
gitgc(){
if ! [ -d .git ]; then
echo "not at top of a git repo, not .git/ directory found"
return 1
fi
du -sh .git
git gc --aggressive
du -sh .git
}
gitbrowse(){
local url
local filter="${2:-.*}"
url="$(git remote -v | grep "$filter" | awk '/git@|https:/{print $2}' | sed 's,://.*@,://,; s|git@github.com:|https://github.com/| ; s/\.git$//' | head -n1)"
if [ $# -gt 0 ] &&
[ -z "$url" ]; then
echo "git remote url not found"
return 1
fi
browser "$url/$1"
}
install_git_completion(){
if ! [ -f ~/.git-completion.bash ]; then
wget -O ~/.git-completion.bash https://raw.githubusercontent.com/git/git/master/contrib/completion/git-completion.bash
fi
}
# shellcheck disable=SC1090
[ -f ~/.git-completion.bash ] && . ~/.git-completion.bash
# usage: gi python,perl,go
# gi list
gitignore_api(){
local url
local langs
local options=()
local args=()
# noop - set to use 'tr' to separate items to newlines when given the 'list' arg
local commas_to_newlines="cat"
for arg; do
if [ "$arg" = -- ]; then
options+=("$arg")
else
args+=("$arg")
fi
done
# take args 'python perl', store as 'python,perl' for the API call
langs="$(IFS=, ; echo "${args[*]}")"
url="https://www.gitignore.io/api/$langs"
if [ "$langs" = "list" ]; then
commas_to_newlines="tr ',' '\\n'"
fi
{
if hash curl 2>/dev/null; then
curl -sSL "${options[*]}" "$url"
elif hash wget 2>/dev/null; then
wget -O - "${options[*]}" "$url"
fi
} | eval "$commas_to_newlines"
echo
}
alias gi=gitignore_api
git_repo(){
git remote -v | awk '{print $2}' | head -n1 | sed 's/[[:alnum:]]*@//; s,.*github.com[/:]*,,'
}
isGit(){
local target=${1:-.}
# There aren't local .hg dirs everywhere only at top level so this is difficult in bash
if [ -d "$target/.git" ]; then
return 0
elif [ -f "$target" ] &&
[ -d "${target%/*}/.git" ]; then
#-o "$target/../.git" -o "${target%/*}/../.git" ]; then
return 0
else
# This is because git command doesn't return correctly when running from outside git root, complains there is not .git
pushd "$(dirname "$target")" >/dev/null || return 1
# subdirs which are not handled by Git fail isGit
# returns false for a newly added not committed dir
#if git log -1 "$target" 2>/dev/null | grep -q '.*'; then
if [ -n "$(git log -1 "$(basename "$target")" 2>/dev/null)" ]; then
# shellcheck disable=SC2164
popd &>/dev/null
return 0
fi
# shellcheck disable=SC2164
popd &>/dev/null
return 2
fi
}
st(){
# shellcheck disable=SC2086
{
local target="${1:-.}"
shift
if ! [ -e "$target" ]; then
echo "$target does not exist"
return 1
fi
local target_basename
local target_dirname
target_basename="$(basename "$target")"
target_dirname="$(dirname "$target")"
#if [ -f "Vagrantfile" ]; then
# echo "> vagrant status"
# vagrant status
# shellcheck disable=SC2166
if [ "$target_basename" = "github" ] ||
[ "$target" = "." -a "$(pwd)" = ~/github ]; then
hr
for x in "$target"/*; do
[ -d "$x" ] || continue
pushd "$x" >/dev/null || { echo "failed to pushd to '$x'"; return 1; }
if git remote -v | grep -qi harisekhon; then
echo "> GitHub: git status $x $*"
git status . "$@"
echo
hr
echo
fi
# shellcheck disable=SC2164
popd &>/dev/null
done
elif isGit "$target"; then
if [ -d "$target" ]; then
pushd "$target" >/dev/null || { echo "Error: failed to pushd to $target"; return 1; }
echo "> git stash list" >&2
git stash list && echo
echo "> git status $target $*" >&2
#git -c color.status=always status -sb . "$@"
git -c color.status=always status . "$@"
else
pushd "$target_dirname" >/dev/null || { echo "Error: failed to pushed to '$target_dirname'"; return 1; }
echo "> git status $target $*" >&2
git -c color.status=always status "$target_basename" "$@"
fi
#git status "$target" "${*:2}"
# shellcheck disable=SC2164
popd &>/dev/null
elif type isHg &>/dev/null && isHg "$target"; then
echo "> hg status $target $*" >&2
hg status "$target" "$@" | grep -v "^?"
# to see relative paths instead of the default absolute paths
#hg status "$(hg root)"
elif type isSvn &>/dev/null && isSvn "$target"; then
echo "> svn st $*" >&2
svn st --ignore-externals "$target" "$@" | grep -v -e "^?" -e "^x";
else
echo "not a revision controlled resource as far as bashrc can tell"
fi
} |
# more calls less on Mac, and gets stuck in interactive mode ignoring the less alias switches
#more -R -n "$((LINES - 3))"
#less -RFX
eval ${GIT_PAGER:-cat}
}
stq(){
st "$@" | grep --color=no -e "=======" -e branch -e GitHub | eval "${GIT_PAGER:-cat}"
}
# disabling this as I don't use Mercurial or Svn any more,
# replacing with simpler function below that will pass through more things like --rebase
#pull(){
# local target="${1:-.}"
# if ! [ -e "$target" ]; then
# echo "$target does not exist"
# return 1
# fi
# local target_basename
# target_basename="$(basename "$target")"
# # shellcheck disable=SC2166
# if [ "$target_basename" = "github" ] || [ "$target" = "." -a "$(pwd)" = "$github" ]; then
# for x in "$target"/*; do
# [ -d "$x" ] || continue
# # get last character of string
# [ "${x: -1}" = 2 ] && continue
# pushd "$x" >/dev/null || { echo "failed to pushd to '$x'"; return 1; }
# if git remote -v | grep -qi harisekhon; then
# echo "> GitHub: git pull $x ${*:2}"
# git pull "${@:2}"
# echo
# echo "> GitHub: git submodule update --init --recursive"
# git submodule update --init --recursive
# echo
# fi
# # shellcheck disable=SC2164
# popd &>/dev/null
# done
# return
# elif isGit "$target"; then
# pushd "$target" >/dev/null &&
# echo "> git pull -v ${*:2}" >&2
# git pull -v "${@:2}"
# echo "> git submodule update --init --recursive"
# git submodule update --init --recursive
# #local orig_branch=$(git branch | awk '/^\*/ {print $2}')
# #for branch in $(git branch | cut -c 3- ); do
# # git checkout -q "$branch" &&
# # echo -n "$branch => " &&
# # git pull -v
# # echo
# # echo
# #done
# #git checkout -q "$orig_branch"
# # shellcheck disable=SC2164
# popd &>/dev/null
# elif type isHg &>/dev/null && isHg "$target"; then
# pushd "$target" >/dev/null &&
# echo "> hg pull && hg up" >&2 &&
# hg pull && hg up
# # shellcheck disable=SC2164
# popd &>/dev/null
# elif type isSvn &>/dev/null && isSvn "$target"; then
# echo "> svn up $target" >&2
# svn up "$target"
# else
# echo "not a revision controlled resource as far as bashrc can tell"
# return 1
# fi
#}
# simpler replacement function to above
pull(){
# shellcheck disable=SC2166
if [ "${PWD##*/}" = github ]; then
for x in *; do
[ -d "$x" ] || continue
# get last character of string - don't pull blah2, as I use them as clean checkouts
[ "${x: -1}" = 2 ] && continue
pushd "$x" >/dev/null || { echo "failed to pushd to '$x'"; return 1; }
if git remote -v | grep -qi "${GITHUB_USER:-${GIT_USER:-${USER:-}}}"; then
echo "> GitHub $x: git pull --all --no-edit $*"
git pull --all --no-edit "$@"
echo
echo "> GitHub $x: git submodule update --init --recursive"
git submodule update --init --recursive
echo
fi
# shellcheck disable=SC2164
popd &>/dev/null
done
return
else
echo "> git pull --all --no-edit $*"
git pull --all --no-edit "$@"
echo "> git submodule update --init --recursive"
git submodule update --init --recursive
fi
}
checkout(){
if isGit "."; then
git checkout "$@";
else
echo "not a Git checkout, cannot switch to branch $*"
return 1
fi
}
gitadd() {
local gitcimsg=""
for x in "$@"; do
if git status -s "$x" | grep -q '^[?A]'; then
gitcimsg+="$x, "
fi
done
[ -z "$gitcimsg" ] && return 1
gitcimsg="${gitcimsg%, }"
gitcimsg="added $gitcimsg"
git add "$@" &&
git commit -m "$gitcimsg" "$@"
}
gitimport() {
local gitcimsg=""
for x in "$@"; do
if git status -s "$x" | grep -q '^[?A]'; then
gitcimsg+="$x, "
fi
done
[ -z "$gitcimsg" ] && return 1
gitcimsg="${gitcimsg%, }"
gitcimsg="imported $gitcimsg"
git add "$@" &&
git commit -m "$gitcimsg" "$@"
}
# shellcheck disable=SC2086
gitu(){
if [ -z "$1" ]; then
echo "usage: gitu <file>"
return 3
fi
local targets
if [ -n "$(git diff "$@")" ]; then
targets="$*"
else
# follow symlinks to the actual files because diffing symlinks returns no changes
targets="$(resolve_symlinks "$@")"
fi
local basedir
# go to the highest directory level to git diff inside the git repo boundary, otherwise git diff will return nothing
basedir="$(basedir $targets)" &&
local trap_codes="INT ERR"
# expand now
# shellcheck disable=SC2064
trap "popd &>/dev/null; trap - $trap_codes; return 1" $trap_codes
pushd "$basedir" >/dev/null || return 1
targets="$(strip_basedirs $basedir $targets)"
# shellcheck disable=SC2086
if [ -z "$(git diff $targets)" ]; then
popd &>/dev/null || :
return 0
fi
# shellcheck disable=SC2086
git diff $targets &&
read -r &&
git add $targets &&
echo "committing $targets" &&
git commit -m "updated $targets" $targets
popd &>/dev/null || :
trap - $trap_codes
}
#githgu(){
# target="${1:-.}"
# #count=0
# while [ -L "$target" ]; do
# #target="$(readlink "$target")"
# #let count+=1
# #if [ $count -gt 10 ]; then
# # echo "looping over links more than 10 times in hggitu! "
# # exit 2
# #fi
# echo "$target is a symlink! "
# return 1
# done
# if ! [ -e "$target" ]; then
# echo "$target does not exist"
# return 1
# fi
# if isGit "$target"; then
# echo "> git" >&2
# #if [ -d "$target" ]; then
# # pushd "$target" >/dev/null
# #else
# # pushd "$(dirname "$target")" >/dev/null
# #fi
# #"$srcdir2/gitu" "${target##*/}" &&
# gitu "$target"
# #popd &>/dev/null
# elif type isHg &>/dev/null && isHg "$target"; then
# echo "> hg" >&2
# #if [ -d "$target" ]; then
# # pushd "$target" >/dev/null
# #else
# # pushd "$(dirname "$target")" >/dev/null
# #fi
# #"$srcdir2/hgu" "${target##*/}" &&
# hgu "$target"
# #popd &>/dev/null
# # Not supporting SVN any more
# #elif type isSvn &>/dev/null && isSvn "$target"; then
# # echo "> svn" >&2
# # svnu "$target"
# else
# echo "not a revision controlled resource as far as bashrc can tell"
# return 1
# fi
#}
push(){
pull . "$@" || return 1
if isGit .; then
echo "> git push -v $*"
#for remote in $(git remote); do
# git push -v $remote $@
#done
git push -v "$@"
elif type isHg &>/dev/null && isHg .; then
echo "> hg push $*"
hg push "$@"
else
echo "not in a Git or Mercurial controlled directory"
return 1
fi
}
switchbranch(){
if isGit "."; then
git checkout "$1";
elif type isHg &>/dev/null && isHg "."; then
hg update "$1"
else
echo "not a Git / Mercurial checkout, cannot switch to branch $1"
return 1
fi
}
gitrm(){
git rm "$@" &&
git commit -m "removed $*" "$@"
}
gitrename(){
git mv "$1" "$2" &&
git commit -m "renamed $1 to $2" "$1" "$2"
}
gitmv(){
git mv "$1" "$2" &&
git commit -m "moved $1 to $2" "$1" "$2"
}
gitd(){
git diff "${@:-.}"
}
gitadded(){
git log --name-status "$@" |
grep -e '^A[^u]' -e '^Date' |
grep -B 1 '^A' |
less
}
# doesn't need pipe | less, git drops you in to less anyway
gitl(){
git log --all --name-status --graph --decorate "$@"
}
gitlp(){
git log -p "$@"
}
gitl2(){
git log --pretty=format:"%n%an => %ar%n%s" --name-status "$@"
}
githg(){
if isGit .; then
git "$@"
elif type isHg &>/dev/null && isHg .; then
hg "$@"
else
echo "not a Git/Mercurial checkout"
return 1
fi
}
retag(){
local tag1="$1"
local checksum="$2"
local additional_tags="${*:2}"
for tag in $tag1 $additional_tags; do
git tag -d "$tag" || :
echo "Creating git tag '$tag'"
# quoting checksum causes failure with unrecognized checksum ''
git tag "$tag" "$checksum"
git tag |
grep -qF "$tag" ||
echo "FAILED"
done
}
gitfind(){
local refids
refids="$(git log --all --oneline | grep "$@" | awk '{print $1}')"
printf 'Branches:\n\n'
for refid in $refids; do
git branch --contains "$refid"
done | sort -u
printf '\nTags:\n\n'
for refid in $refids; do
git tag --contains "$refid"
done | sort -u
}
updatemodules(){
if isGit .; then
git pull
#git submodule update --init --remote
for submodule in $(git submodule | awk '{print $2}'); do
if [ -d "$submodule" ] && ! [ -L "$submodule" ]; then
pushd "$submodule" || continue
git stash
git checkout master
git pull
git submodule update
# shellcheck disable=SC2164
popd
fi
done
echo
for submodule in $(git submodule | awk '{print $2}'); do
if [ -d "$submodule" ] && ! [ -L "$submodule" ] && ! git status "$submodule" | grep -q nothing; then
git commit -m "updated $submodule" "$submodule" || break
fi
done &&
make updatem ||
echo FAILED
echo
for submodule in $(git submodule | awk '{print $2}'); do
if [ -d "$submodule" ] && ! [ -L "$submodule" ]; then
pushd "$submodule" || continue
git stash pop
# shellcheck disable=SC2164
popd
fi
done
else
echo "Not a Git repository! "
return 1
fi
}
upl(){
local repos="pylib pytools lib tools bash-tools nagios-plugins npk"
# pull all repos first so can handle merge requests if needed
for repo in $repos; do
echo
echo "* Pulling latest repo changes: $repo"
echo
pushd "$github/$repo" &&
git pull &&
popd &&
hr || return 1
done
echo
echo "UNATTEND FROM HERE"
echo
for repo in $repos; do
echo
echo "* Performing latest submodule updates: $repo"
echo
pushd "$github/$repo" &&
! updatemodules 2>&1 | tee /dev/stderr | grep -e ERROR -e FAIL &&
git push &&
popd &&
hr || return 1
done
}
#stagemerge(){
# if isGit "."; then
# git checkout prod && git pull &&
# git checkout staging && git pull &&
# git merge prod
# git checkout prod
# else
# echo "Not a Git working copy";
# fi
#}
gitdiff(){
local filename="${1:-}"
[ -n "$filename" ] || { echo "usage: gitdiff filename"; return 1; }
git diff "$filename" > "/tmp/gitdiff.tmp"
diffnet.pl "/tmp/hgdiff.tmp"
}
git_author_names(){
git log --all --pretty=format:"%an" | sort | uniq -c | sort -k1nr | less
}
git_author_emails(){
git log --all --pretty=format:"%ae" | sort | uniq -c | sort -k1nr | less
}
git_author_names_emails(){
git log --all --pretty=format:"%an %ae" | sort | uniq -c | sort -k1nr | less
}
git_authors(){
git_author_emails
}
git_commit_count(){
# interestingly, even on 10,000 commit repos, there are no duplicate short hashes shown from:
# git log --all --pretty=format:"%h" | sort | uniq -d
git log --all --pretty=format:"%h" | wc -l
}
git_revert_typechange(){
# want splitting to separate filenames
# shellcheck disable=SC2046
co $(git status --porcelain -s "${1:-.}" | awk '/^.T/{print $2}')
}
git_rm_untracked(){
if [ $# -lt 1 ]; then
echo "usage: rm_untracked <target_dir_or_files_or_glob>"
return 1
fi
# iterate on explicit targets only
# intentionally not including current directory to avoid accidentally wiping out untracked files - you must specify "rm_untracked ." if you really intend this
for x in "${@:-}"; do
# want splitting to separate filenames
# shellcheck disable=SC2046
rm -v $(git status --porcelain -s "$x" | awk '/^\?\?/{print $2}')
done
}
# example of usage of this in the function below - make sure to put '$repo' or "\$repo" somewhere in the argument body to make use of the iteration variable
foreachrepo(){
local repolist="${REPOLIST:-$bash_tools/setup/repolist.txt}"
while read -r repo; do
eval "$@"
done < <(sed 's/#.*$//; s/.*://; /^[[:space:]]*$/d' "$repolist")
}
github_authors(){
# deferring expansion into loop
# shellcheck disable=SC2016
foreachrepo 'echo "repo: $repo"; pushd "$github/$repo" >/dev/null || return 1; git_authors; popd >/dev/null || return 1; echo' | ${less:-less}
}
merge_conflicting_files(){
# merge conflicts:
#
# UU = both updated
# AA = both added
#
git status --porcelain | awk '/^UU|^AA/{$1=""; print}'
}
merge_deleted_files(){
git status --porcelain | awk '/^DU/{$1=""; print}'
}
# useful for Dockerfiles merging lots of branches
#
# while ! make mergemasterpull; do fixmerge "merged master"; done
#
fixmerge(){
local msg="${*:-merged}"
local merge_conflicted_files
local merge_deleted_files
merge_deleted_files="$(merge_deleted_files)"
if [ -n "$merge_deleted_files" ]; then
# false positive, not passing add function/alias add to git
# shellcheck disable=SC2033
xargs git add <<< "$merge_deleted_files"
fi
merge_conflicted_files="$(merge_conflicting_files)"
if [ -n "$merge_conflicted_files" ]; then
# shellcheck disable=SC2086
"$EDITOR" $merge_conflicted_files &&
git add $merge_conflicted_files
fi
git ci -m "$msg"
}