Changing from i3 to dwm - tidied ip bin folder

This commit is contained in:
Jonathan Hodgson 2019-05-17 12:52:12 +01:00
parent 6aaf779227
commit a0ef393f2b
47 changed files with 254 additions and 245 deletions

75
bin/git/git-cleaner Executable file
View file

@ -0,0 +1,75 @@
#!/usr/bin/bash
function help() {
cat <<HELP
Git Clean
https://jonathanh.co.uk
Some code came from Ben Alman
http://benalman.com/
Usage: $(basename "$0") [command]
Commands:
clean Remove current unstaged changes/untracked files**
cleanall Remove all saved tags, unstaged changes and untracked files**
** This action is destructive and cannot be undone!
Description:
Cleans unstaged changes and untracked files
Copyright (c) 2014 "Cowboy" Ben Alman
Licensed under the MIT license.
http://benalman.com/about/license/
HELP
}
function usage() {
echo "Usage: $(basename "$0") [clean | cleanall]"
}
function git_head_sha() {
git rev-parse --short HEAD
}
# Get absolute path to root of Git repo
function git_repo_toplevel() {
git rev-parse --show-toplevel
}
# Clean (permanently) current changes and remove the current saved tag
function clean() {
local head_sha=$(git_head_sha)
git tag -d "git-jump-$head_sha" &>/dev/null
if [[ $? == 0 ]]; then
echo "Removed stored data for commit $head_sha."
fi
local repo_root="$(git_repo_toplevel)"
git reset HEAD "$repo_root" >/dev/null
git clean -f -d -q -- "$repo_root" >/dev/null
git checkout -- "$repo_root" >/dev/null
echo "Unstaged changes and untracked files removed."
}
# Remove (permanently) all saved tags
function clean_all_tags() {
git for-each-ref refs/tags --format='%(refname:short)' | \
while read tag; do
if [[ "$tag" =~ ^git-jump- ]]; then
git tag -d "$tag"
fi
done
}
# Handle CLI arguments
if [[ "$1" == "clean" ]]; then
clean
elif [[ "$1" == "cleanall" ]]; then
clean_all_tags
clean
else
usage
exit 1
fi

24
bin/git/git-delete-submodule Executable file
View file

@ -0,0 +1,24 @@
#!/usr/bin/env bash
test -z "$1" && echo "submodule required" 1>&2 && exit 1
#cd "$(git root)"
test ! -f .gitmodules && echo ".gitmodules file not found" 1>&2 && exit 2
NAME="$(echo "$1" | sed 's/\/$//g')"
test -z \
"$(git config --file=.gitmodules submodule."$NAME".url)" \
&& echo "submodule not found" 1>&2 && exit 3
# 1. Delete the relevant section from .git/config and clean submodule files
git submodule deinit -f "$NAME" || exit 4
rmdir "$NAME"
rm -rf .git/modules/"$NAME"
# 2. Delete the relevant line from .gitmodules
git config --file=.gitmodules --remove-section submodule."$NAME"
git add .gitmodules
# 3. Run git rm --cached path_to_submodule
git rm --cached -rf "$NAME"
# 4. Need to confirm and commit the changes for yourself
echo
echo "Now submodule $NAME is deleted."
echo 'Confirm with `git submodule status` and commit the changes for yourself.'

30
bin/git/git-initial-commit Executable file
View file

@ -0,0 +1,30 @@
#!/usr/bin/bash
# What should the initial commit be for a repo?
#
# Idea came after reading this blog post: https://blog.no-panic.at/2014/10/20/funny-initial-git-commit-messages/
commits=(
"This is where it all begins..."
"Commit committed"
"Version control is awful"
"COMMIT ALL THE FILES!"
"The same thing we do every night, Pinky - try to take over the world!"
"Lock S-foils in attack position"
"This commit is a lie"
"I'll explain when you're older!"
"Here be Dragons"
"Reinventing the wheel. Again."
"This is not the commit message you are looking for"
"Batman! (this commit has no parents)"
"In the beginning, the code was without form and was void()…"
)
# Seed random generator
RANDOM=$$$(date +%s)
# Get random expression...
selectedexpression=${commits[$RANDOM % ${#commits[@]} ]}
# Write to Shell
git commit --allow-empty -m "$selectedexpression"

158
bin/git/git-jump Executable file
View file

@ -0,0 +1,158 @@
#!/usr/bin/bash
function help() {
cat <<HELP
Git Jump (Forward & Back)
http://benalman.com/
Modified slightly by Jonathan Hodgson
https://jonathanh.co.uk/
Copyright (c) 2017 Jonathan Hodgson
Licensed under the MIT license.
https://en.wikipedia.org/wiki/MIT_License
Usage: $(basename "$0") [command]
Commands:
next Jump forward to the next commit in this branch
prev Jump backward to the next commit in this branch
Git config:
git-jump.branch Branch to jump through. If not set, defaults to master
Description:
"Replay" Git commits by moving forward / backward through a branch's
history. Before jumping, any current unstaged changes and untracked
files are saved in a tag for later retrieval, which is restored when
jumped back to.
Original Licence:
Copyright (c) 2014 "Cowboy" Ben Alman
Licensed under the MIT license.
http://benalman.com/about/license/
HELP
}
function usage() {
echo "Usage: $(basename "$0") [next | prev]"
}
# Get branch stored in Git config or default to master
git_branch="$(git config git-jump.branch || echo "master")"
# Get some (short) SHAs
function git_branch_sha() {
git rev-parse --short "$git_branch"
}
function git_head_sha() {
git rev-parse --short HEAD
}
function git_prev_sha() {
git log --format='%h' "$git_branch" "$@" | awk "/^$(git_head_sha)/{getline; print}"
}
function git_next_sha() {
git_prev_sha --reverse
}
# Get absolute path to root of Git repo
function git_repo_toplevel() {
git rev-parse --show-toplevel
}
# Get subject of specified commit
function git_commit_subject() {
git log --format='%s' -n 1 $1
}
# Save changes for later retrieval
function save() {
local status=""
local head_sha=$(git_head_sha)
# Checkout current HEAD by SHA to force detached state
git checkout -q $head_sha
# Add all files in repo
git add "$(git_repo_toplevel)"
# Commit changes (if there were any)
git commit --no-verify -m "Git Jump: saved changes for $head_sha" >/dev/null
# If the commit was successful, tag it (overwriting any previous tag)
if [[ $? == 0 ]]; then
status="*"
git tag -f "git-jump-$head_sha" >/dev/null
fi
echo "Previous HEAD was $head_sha$status, $(git_commit_subject $head_sha)"
}
# Restore previously-saved changes
function restore() {
local status=""
# Save current changes before restoring
save
# Attempt to restore saved changes for specified commit
git checkout "git-jump-$1" 2>/dev/null
if [[ $? == 0 ]]; then
# If the restore was successful, figure out exactly what was saved, check
# out the original commit, then restore the saved changes on top of it
status="*"
local patch="$(git format-patch HEAD^ --stdout)"
git checkout HEAD^ 2>/dev/null
echo "$patch" | git apply -
else
# Otherwise, just restore the original commit
git checkout "$1" 2>/dev/null
fi
echo "HEAD is now $1$status, $(git_commit_subject $1)"
}
# Jump to next commit
function next() {
local next_sha=$(git_next_sha)
if [[ "$next_sha" == "$(git_head_sha)" ]]; then
# Abort if no more commits
echo "Already at last commit in $git_branch. Congratulations!"
else
# Checkout branch by name if at its HEAD
if [[ "$next_sha" == "$(git_branch_sha)" ]]; then
next_sha="$git_branch"
fi
echo "Jumping ahead to next commit."
restore $next_sha
fi
}
# Jump to previous commit
function prev() {
local prev_sha=$(git_prev_sha)
if [[ "$prev_sha" == "$(git_head_sha)" ]]; then
# Abort if no more commits
echo "Already at first commit in $git_branch."
else
echo "Jumping back to previous commit."
restore $prev_sha
fi
}
# Show help if requested
if [[ "$1" == "--help" || "$1" == "-h" ]]; then
help
exit
fi
# Check if branch is valid
git rev-parse "$git_branch" &>/dev/null
if [[ $? != 0 ]]; then
echo "Error: Branch \"$git_branch\" does not appear to be valid."
echo "Try $(basename "$0") --help for more information."
exit 1
fi
# Handle CLI arguments
if [[ "$1" == "next" ]]; then
next
elif [[ "$1" == "prev" ]]; then
prev
else
usage
exit 1
fi

12
bin/git/git-nuke Executable file
View file

@ -0,0 +1,12 @@
#!/bin/sh
#
# Nukes a branch locally and on the origin remote.
#
# $1 - Branch name.
#
# Examples
#
# git nuke add-git-nuke
git branch -D $1
git push origin :$1

149
bin/git/git-open Executable file
View file

@ -0,0 +1,149 @@
#!/bin/bash
# Opens the BitBucket/GitHub page for a repo/branch in your browser.
#
# git open
# git open [remote] [branch]
# are we in a git repo?
git rev-parse --is-inside-work-tree &>/dev/null
if [[ $? != 0 ]]; then
echo "Not a git repository." 1>&2
exit 1
fi
# assume origin if not provided
# fallback to upstream if neither is present.
remote="origin"
if [ -n "$1" ]; then
if [ "$1" == "issue" ]; then
currentBranch=$(git symbolic-ref -q --short HEAD)
regex='^issue'
if [[ $currentBranch =~ $regex ]]; then
issue=${currentBranch#*#}
else
echo "'git open issue' expect branch naming to be issues/#123" 1>&2
exit 1
fi
else
remote="$1"
fi
fi
remote_url="remote.${remote}.url"
giturl=$(git config --get "$remote_url")
if [ -z "$giturl" ]; then
echo "$remote_url not set." 1>&2
exit 1
fi
# get current branch
if [ -z "$2" ]; then
branch=$(git symbolic-ref -q --short HEAD)
else
branch="$2"
fi
# Make # and % characters url friendly
# github.com/paulirish/git-open/pull/24
branch=${branch//%/%25} && branch=${branch//#/%23}
# URL normalization
# GitHub gists
if grep -q gist.github <<<$giturl; then
giturl=${giturl/git\@gist.github\.com\:/https://gist.github.com/}
providerUrlDifference=tree
# GitHub
elif grep -q github <<<$giturl; then
giturl=${giturl/git\@github\.com\:/https://github.com/}
# handle SSH protocol (links like ssh://git@github.com/user/repo)
giturl=${giturl/#ssh\:\/\/git\@github\.com\//https://github.com/}
providerUrlDifference=tree
# Bitbucket
elif grep -q bitbucket <<<$giturl; then
giturl=${giturl/git\@bitbucket\.org\:/https://bitbucket.org/}
# handle SSH protocol (change ssh://https://bitbucket.org/user/repo to https://bitbucket.org/user/repo)
giturl=${giturl/#ssh\:\/\/git\@/https://}
rev="$(git rev-parse HEAD)"
git_pwd="$(git rev-parse --show-prefix)"
providerUrlDifference="src/${rev}/${git_pwd}"
branch="?at=${branch}"
# Atlassian Bitbucket Server
elif grep -q "/scm/" <<<$giturl; then
re='(.*)/scm/(.*)/(.*)\.git'
if [[ $giturl =~ $re ]]; then
giturl=${BASH_REMATCH[1]}/projects/${BASH_REMATCH[2]}/repos/${BASH_REMATCH[3]}
providerUrlDifference=browse
branch="?at=refs%2Fheads%2F${branch}"
fi
# GitLab
else
# custom GitLab
gitlab_domain=$(git config --get gitopen.gitlab.domain)
gitlab_ssh_domain=$(git config --get gitopen.gitlab.ssh.domain)
gitlab_ssh_domain=${gitlab_ssh_domain:-$gitlab_domain}
gitlab_ssh_port=$(git config --get gitopen.gitlab.ssh.port)
gitlab_protocol=$(git config --get gitopen.gitlab.protocol)
if [ -z "$gitlab_protocol" ]; then
gitlab_protocol=https
fi
if [ -n "$gitlab_domain" ]; then
if egrep -q "${gitlab_domain}|${gitlab_ssh_domain}" <<<$giturl; then
# Handle GitLab's default SSH notation (like git@gitlab.domain.com:user/repo)
giturl=${giturl/git\@${gitlab_ssh_domain}\:/${gitlab_protocol}://${gitlab_domain}/}
# handle SSH protocol (links like ssh://git@gitlab.domain.com/user/repo)
giturl=${giturl/#ssh\:\/\//${gitlab_protocol}://}
# remove git@ from the domain
giturl=${giturl/git\@${gitlab_ssh_domain}/${gitlab_domain}/}
# remove SSH port
if [ -n "$gitlab_ssh_port" ]; then
giturl=${giturl/\/:${gitlab_ssh_port}\///}
fi
providerUrlDifference=tree
fi
# hosted GitLab
elif grep -q gitlab <<<$giturl; then
giturl=${giturl/git\@gitlab\.com\:/https://gitlab.com/}
providerUrlDifference=tree
fi
fi
giturl=${giturl%\.git}
if [ -n "$issue" ]; then
giturl="${giturl}/issues/${issue}"
elif [ -n "$branch" ]; then
giturl="${giturl}/${providerUrlDifference}/${branch}"
fi
# simplify URL for master
giturl=${giturl/tree\/master/}
# get current open browser command
case $( uname -s ) in
Darwin) open=open;;
MINGW*) open=start;;
CYGWIN*) open=cygstart;;
MSYS*) open="powershell.exe NoProfile Start";;
*) open=${BROWSER:-xdg-open};;
esac
# open it in a browser
$open "$giturl" &> /dev/null
exit $?

349
bin/git/git-recall Executable file
View file

@ -0,0 +1,349 @@
#!/usr/bin/env bash
# usage info
usage() {
cat <<EOF
Usage: git recall [options]
Options:
-d, --date Show logs for last n days.
-a, --author Author name.
-f, --fetch fetch commits.
-h, --help This message.
-v, --version Show version.
-- End of options.
EOF
}
# Global variables
SINCE="1 days ago" # show logs for last day by default
AUTHOR=""
FETCH=false
GIT_FORMAT=""
GIT_LOG=""
COMMITS=""
COMMITS_UNCOL=() # commits without colors
LESSKEY=false
SED_BIN="" # Sed option to use according OS.
VERSION="1.1.10"
# Are we in a git repo?
if [[ ! -d ".git" ]] && ! git rev-parse --git-dir &>/dev/null; then
echo "abort: not a git repository." 1>&2
exit 1
fi
# Parse options
while [[ "$1" =~ ^- && ! "$1" == "--" ]]; do
case $1 in
-v | --version )
echo "$version"
exit
;;
-d | --date )
SINCE="$2 days ago"
shift;
;;
-a | --author )
AUTHOR="$2"
shift
;;
-f | --fetch )
FETCH=true
;;
-h | --help )
usage
exit
;;
* )
echo "abort: unknown argument" 1>&2
exit 1
esac
shift
done
if [[ "$1" == "--" ]]; then shift; fi
# Colored output.
function colored() {
GREEN=$(tput setaf 4)
YELLOW=$(tput setaf 3)
NORMAL=$(tput sgr0)
REVERSE=$(tput rev)
}
# Uncolored output.
function uncolored() {
GREEN=""
YELLOW=""
NORMAL=""
REVERSE=""
}
# Enable colors if supported by terminal.
if [[ -t 1 ]] && [[ -n "$TERM" ]] && which tput &>/dev/null && tput colors &>/dev/null; then
ncolors=$(tput colors)
if [[ -n "$ncolors" ]] && [[ "$ncolors" -ge 8 ]] ; then
colored
else
uncolored
fi
else
uncolored
fi
# Check if lesskey is installed.
if command -v lesskey &> /dev/null; then
LESSKEY=true
fi
# Set AUTHOR to current user if no param passed or display for all users if param equal to "all".
if [[ ! -n $AUTHOR ]]; then
AUTHOR=$(git config user.name 2>/dev/null)
elif [[ $AUTHOR = "all" ]]; then
AUTHOR=".*"
fi
# Fetch changes before.
if [[ $FETCH == true ]]; then
echo "${GREEN}Fetching changes...${NORMAL}"
git fetch --all &> /dev/null
tput cuu1
tput ed # clear screen
fi
# Log template.
GIT_FORMAT="%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset"
# Log command.
GIT_LOG="git log --pretty=format:'${GIT_FORMAT}'
--author \"$AUTHOR\"
--since \"$SINCE\" --abbrev-commit"
# Change temporary the IFS to store GIT_LOG's output into an array.
IFS=$'\n'
COMMITS=($(eval ${GIT_LOG} 2>/dev/null))
unset IFS
NI=${#COMMITS[@]} # Total number of items.
SN=$(( `tput lines` - 1 )) # Screen's number of lines.
CN=$(tput cols) # Screen's number of columns.
TN=$(( $NI < $((SN -1)) ? $NI : $((SN -1)))) # Number of lines that we will display.
OFFSET=0 #Incremented by one each time a commit's length is higher than teminal width.
# If there is no items, exit.
if [[ $NI = 0 ]]; then
if [[ $AUTHOR = ".*" ]]; then
echo "${YELLOW}All contributors did nothing during this period.${NORMAL}" && exit 0
else
echo "${YELLOW}The contributor \"${AUTHOR}\" did nothing during this period.${NORMAL}" && exit 0
fi
fi
# Set correct sed option according OS's type
case "$OSTYPE" in
darwin*) SED_BIN="sed -E" ;;
*) SED_BIN="sed -r" ;;
esac
# Create array with uncolred commits (removing escape sequences using sed)
for elt in "${COMMITS[@]}"
do
ELT="$(echo "$elt" | $SED_BIN "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g")" # remove colors escape codes
COMMITS_UNCOL+=("$ELT")
done
# Add +1 to OFFSET if a commit's length is bigger than the current terminal session's width. (This is to fix a redraw issue)
for C in "${COMMITS_UNCOL[@]}"
do
if [[ ${#C} -gt $CN ]]; then
OFFSET=$(( OFFSET + 1 ))
fi
done
# Set keys.
au="`echo -e '\e[A'`" # arrow up
au_1="k" # arrow up
ad="`echo -e '\e[B'`" # arrow down
ad_1="j" # arrow down
ec="`echo -e '\e'`" # escape
nl="`echo -e '\n'`" # newline
nl_1="e" # expand
co="c" # checkout
# Create a temporary lesskey file to change the keybindings so the user can use the TAB key to quit less. (more convenient)
if [[ $LESSKEY = true ]]; then
echo "\t quit" | lesskey -o $HOME/.lsh_less_keys_tmp -- - &> /dev/null
fi
## Get commit's diff.
function get_diff() {
ELT="$(echo "${COMMITS_UNCOL[$CP-1]}")"
DIFF_TIP=${ELT:0:7}
DIFF_CMD="git show $DIFF_TIP --color=always"
DIFF=$(eval ${DIFF_CMD} 2>/dev/null)
tmp_diff="$(echo "$DIFF" | $SED_BIN "s/\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]//g")" # remove colors escape codes
off=$(echo "$tmp_diff" | grep -c ".\{$CN\}") #Number of lines in the diff that are longer than terminal width.
DIFF_LINES_NUMBER="$(echo "$DIFF" | wc -l)"
DIFF_LINES_NUMBER=$(( DIFF_LINES_NUMBER + off ))
}
## This function will print the diff according the commit's tip. If the diff is too long, the result will be displayed using 'less'.
function print_diff() {
get_diff # get commit's diff
if [[ $(( TN + DIFF_LINES_NUMBER + OFFSET )) -ge $(( `tput lines` - 1 )) ]]; then
if [[ $LESSKEY = true ]]; then
echo "$DIFF" | less -r -k $HOME/.lsh_less_keys_tmp
else
echo "$DIFF" | less -r
fi
tput ed # Clear screen
else
stop=false
tput ed
for i in `seq 1 $TN`
do
echo -n "$NORMAL"
[[ $CP == "$i" ]] && echo -n "$REVERSE"
echo "${COMMITS[$i - 1]}"
[[ $CP == "$i" ]] && echo "$DIFF"
done
# Wait for user action.
while ! $stop
do
read -sn 1 key
case "$key" in
"$nl" | "$nl_1")
stop=true
;;
"q")
stop=true
END=true
;;
esac
done
[[ $END = false ]] && tput cuu $(( TN + DIFF_LINES_NUMBER + OFFSET )) && tput ed
[[ $END = true ]] && tput cuu 1
fi
}
function do_checkout(){
ELT="$(echo "${COMMITS_UNCOL[$CP-1]}")"
DIFF_TIP=${ELT:0:7}
eval "git checkout $DIFF_TIP 2> /dev/null"
}
# Calculate OFFSET to avoid bad redraw.
function calculate_offset {
tmp=1
index=$(( SI -1 ))
while [[ $tmp -lt $SN ]]
do
el=${COMMITS_UNCOL[$index]}
if [[ ${#el} -gt $CN ]] && [[ $CP -lt $((SN -1)) ]]; then
OFFSET_2=$(( OFFSET_2 + 1 ))
tmp=$(( tmp + 1 ))
fi
tmp=$(( tmp + 1 ))
index=$(( index + 1 ))
done
}
{ # capture stdout to stderr
tput civis # hide cursor.
CP=1 # current position
SI=1 # index
END=false # end while loop
EXT=0 # Used to extend the number of lines to display.
## Loops, reads inputs and prints commits until user presses 'q' to exit or TAB to show the diff.
while ! $END
do
END_INDEX=0 # Index for the last item to display
# When the number of item is higher than screen's number of lines, OFFSET_2 is recalculated each time we select a new item
# Set last index to print. (based on OFFSET)
if [[ $TN == $NI ]]; then
END_INDEX=$TN
OFFSET_2=$OFFSET
elif [[ $TN == $(( SN - 1 )) ]]; then
# Calculate new OFFSET.
if [[ $OFFSET != 0 ]]; then
[[ $CP -lt $((SN -1)) ]] && OFFSET_2=0
EXT=1
calculate_offset
fi
END_INDEX=$(( TN + SI -1 + EXT - OFFSET_2 ))
fi
# Loop and echo commits
for i in `seq $SI $END_INDEX`
do
echo -n "$NORMAL"
[[ $CP == $i ]] && echo -n "$REVERSE"
echo "${COMMITS[$i - 1]}"
done
read -sn 1 key
[[ "$key" == "$ec" ]] &&
{
read -sn 2 k2
key="$key$k2"
}
case "$key" in
"$au" | "$au_1")
CP=$(( CP - 1 ))
[[ $CP == 0 ]] && [[ $SI == 1 ]] && [[ $TN == $(( SN - 1 )) ]] && CP=$NI && SI=$(( NI - SN + 2 + OFFSET_2 ))
[[ $CP == 0 ]] && [[ $SI == 1 ]] && [[ $TN == $NI ]] && CP=$TN
[[ $CP == $(( SI - 1 )) ]] && [[ $SI != 1 ]] && SI=$(( SI - 1 ))
[[ $TN != $(( SN - 1 )) ]] && tput cuu $(( TN + OFFSET_2 ))
[[ $TN == $(( SN - 1 )) ]] && tput cuu $(( TN + EXT ))
[[ $SI != 1 ]] && tput ed # clear screen
;;
"$ad" | "$ad_1")
CP=$(( CP + 1 ))
[[ $CP == $(( NI + 1 )) ]] && CP=1 && SI=1
[[ $CP == $(( SN + SI - 1 + EXT - OFFSET_2 )) ]] && [[ $TN == $(( SN - 1 )) ]] && SI=$(( SI + 1 ))
[[ $TN != $(( SN - 1 )) ]] && tput cuu $(( TN + OFFSET_2 ))
[[ $TN == $(( SN - 1 )) ]] && tput cuu $(( TN + EXT ))
[[ $SI != 1 ]] && tput ed # clear screen
[[ $SI = 1 ]] && [[ $CP = 1 ]] && tput ed # clear screen
;;
"$nl" | "$nl_1")
[[ $TN == $NI ]] && tput cuu $(( TN + OFFSET_2 ))
[[ $TN != $NI ]] && tput cuu $(( TN + EXT ))
print_diff
;;
"$co")
si=false
END=true
do_checkout
tput cuu 1 #move cursor up one line. (remove extra line)
;;
"q")
si=false
END=true
tput cuu 1 #move cursor up one line. (remove extra line)
;;
* )
tput cuu $(( TN + OFFSET_2 ))
esac
done
# remove temporary less keybindings
[[ $LESSKEY = true ]] && rm $HOME/.lsh_less_keys_tmp
tput cnorm # unhide cursor
echo "$NORMAL" # normal colors
} >&2 # END capture

364
bin/git/git-wtf Executable file
View file

@ -0,0 +1,364 @@
#!/usr/bin/env ruby
HELP = <<EOS
git-wtf displays the state of your repository in a readable, easy-to-scan
format. It's useful for getting a summary of how a branch relates to a remote
server, and for wrangling many topic branches.
git-wtf can show you:
- How a branch relates to the remote repo, if it's a tracking branch.
- How a branch relates to integration branches, if it's a feature branch.
- How a branch relates to the feature branches, if it's an integration
branch.
git-wtf is best used before a git push, or between a git fetch and a git
merge. Be sure to set color.ui to auto or yes for maximum viewing pleasure.
EOS
KEY = <<EOS
KEY:
() branch only exists locally
{} branch only exists on a remote repo
[] branch exists locally and remotely
x merge occurs both locally and remotely
~ merge occurs only locally
(space) branch isn't merged in
(It's possible for merges to occur remotely and not locally, of course, but
that's a less common case and git-wtf currently doesn't display anything
special for it.)
EOS
USAGE = <<EOS
Usage: git wtf [branch+] [options]
If [branch] is not specified, git-wtf will use the current branch. The possible
[options] are:
-l, --long include author info and date for each commit
-a, --all show all branches across all remote repos, not just
those from origin
-A, --all-commits show all commits, not just the first 5
-s, --short don't show commits
-k, --key show key
-r, --relations show relation to features / integration branches
--dump-config print out current configuration and exit
git-wtf uses some heuristics to determine which branches are integration
branches, and which are feature branches. (Specifically, it assumes the
integration branches are named "master", "next" and "edge".) If it guesses
incorrectly, you will have to create a .git-wtfrc file.
To start building a configuration file, run "git-wtf --dump-config >
.git-wtfrc" and edit it. The config file is a YAML file that specifies the
integration branches, any branches to ignore, and the max number of commits to
display when --all-commits isn't used. git-wtf will look for a .git-wtfrc file
starting in the current directory, and recursively up to the root.
IMPORTANT NOTE: all local branches referenced in .git-wtfrc must be prefixed
with heads/, e.g. "heads/master". Remote branches must be of the form
remotes/<remote>/<branch>.
EOS
COPYRIGHT = <<EOS
git-wtf Copyright 2008--2009 William Morgan <wmorgan at the masanjin dot nets>.
This program is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more details.
You can find the GNU General Public License at: http://www.gnu.org/licenses/
EOS
require 'yaml'
CONFIG_FN = ".git-wtfrc"
class Numeric; def pluralize s; "#{to_s} #{s}" + (self != 1 ? "s" : "") end end
if ARGV.delete("--help") || ARGV.delete("-h")
puts USAGE
exit
end
## poor man's trollop
$long = ARGV.delete("--long") || ARGV.delete("-l")
$short = ARGV.delete("--short") || ARGV.delete("-s")
$all = ARGV.delete("--all") || ARGV.delete("-a")
$all_commits = ARGV.delete("--all-commits") || ARGV.delete("-A")
$dump_config = ARGV.delete("--dump-config")
$key = ARGV.delete("--key") || ARGV.delete("-k")
$show_relations = ARGV.delete("--relations") || ARGV.delete("-r")
ARGV.each { |a| abort "Error: unknown argument #{a}." if a =~ /^--/ }
## search up the path for a file
def find_file fn
while true
return fn if File.exist? fn
fn2 = File.join("..", fn)
return nil if File.expand_path(fn2) == File.expand_path(fn)
fn = fn2
end
end
want_color = `git config color.wtf`
want_color = `git config color.ui` if want_color.empty?
$color = case want_color.chomp
when "true"; true
when "auto"; $stdout.tty?
end
def red s; $color ? "\033[31m#{s}\033[0m" : s end
def green s; $color ? "\033[32m#{s}\033[0m" : s end
def yellow s; $color ? "\033[33m#{s}\033[0m" : s end
def cyan s; $color ? "\033[36m#{s}\033[0m" : s end
def grey s; $color ? "\033[1;30m#{s}\033[0m" : s end
def purple s; $color ? "\033[35m#{s}\033[0m" : s end
## the set of commits in 'to' that aren't in 'from'.
## if empty, 'to' has been merged into 'from'.
def commits_between from, to
if $long
`git log --pretty=format:"- %s [#{yellow "%h"}] (#{purple "%ae"}; %ar)" #{from}..#{to}`
else
`git log --pretty=format:"- %s [#{yellow "%h"}]" #{from}..#{to}`
end.split(/[\r\n]+/)
end
def show_commits commits, prefix=" "
if commits.empty?
puts "#{prefix} none"
else
max = $all_commits ? commits.size : $config["max_commits"]
max -= 1 if max == commits.size - 1 # never show "and 1 more"
commits[0 ... max].each { |c| puts "#{prefix}#{c}" }
puts grey("#{prefix}... and #{commits.size - max} more (use -A to see all).") if commits.size > max
end
end
def ahead_behind_string ahead, behind
[ahead.empty? ? nil : "#{ahead.size.pluralize 'commit'} ahead",
behind.empty? ? nil : "#{behind.size.pluralize 'commit'} behind"].
compact.join("; ")
end
def widget merged_in, remote_only=false, local_only=false, local_only_merge=false
left, right = case
when remote_only; %w({ })
when local_only; %w{( )}
else %w([ ])
end
middle = case
when merged_in && local_only_merge; green("~")
when merged_in; green("x")
else " "
end
print left, middle, right
end
def show b
have_both = b[:local_branch] && b[:remote_branch]
pushc, pullc, oosync = if have_both
[x = commits_between(b[:remote_branch], b[:local_branch]),
y = commits_between(b[:local_branch], b[:remote_branch]),
!x.empty? && !y.empty?]
end
if b[:local_branch]
puts "Local branch: " + green(b[:local_branch].sub(/^heads\//, ""))
if have_both
if pushc.empty?
puts "#{widget true} in sync with remote"
else
action = oosync ? "push after rebase / merge" : "push"
puts "#{widget false} NOT in sync with remote (you should #{action})"
show_commits pushc unless $short
end
end
end
if b[:remote_branch]
puts "Remote branch: #{cyan b[:remote_branch]} (#{b[:remote_url]})"
if have_both
if pullc.empty?
puts "#{widget true} in sync with local"
else
action = pushc.empty? ? "merge" : "rebase / merge"
puts "#{widget false} NOT in sync with local (you should #{action})"
show_commits pullc unless $short
end
end
end
puts "\n#{red "WARNING"}: local and remote branches have diverged. A merge will occur unless you rebase." if oosync
end
def show_relations b, all_branches
ibs, fbs = all_branches.partition { |name, br| $config["integration-branches"].include?(br[:local_branch]) || $config["integration-branches"].include?(br[:remote_branch]) }
if $config["integration-branches"].include? b[:local_branch]
puts "\nFeature branches:" unless fbs.empty?
fbs.each do |name, br|
next if $config["ignore"].member?(br[:local_branch]) || $config["ignore"].member?(br[:remote_branch])
next if br[:ignore]
local_only = br[:remote_branch].nil?
remote_only = br[:local_branch].nil?
name = if local_only
purple br[:name]
elsif remote_only
cyan br[:name]
else
green br[:name]
end
## for remote_only branches, we'll compute wrt the remote branch head. otherwise, we'll
## use the local branch head.
head = remote_only ? br[:remote_branch] : br[:local_branch]
remote_ahead = b[:remote_branch] ? commits_between(b[:remote_branch], head) : []
local_ahead = b[:local_branch] ? commits_between(b[:local_branch], head) : []
if local_ahead.empty? && remote_ahead.empty?
puts "#{widget true, remote_only, local_only} #{name} #{local_only ? "(local-only) " : ""}is merged in"
elsif local_ahead.empty?
puts "#{widget true, remote_only, local_only, true} #{name} merged in (only locally)"
else
behind = commits_between head, (br[:local_branch] || br[:remote_branch])
ahead = remote_only ? remote_ahead : local_ahead
puts "#{widget false, remote_only, local_only} #{name} #{local_only ? "(local-only) " : ""}is NOT merged in (#{ahead_behind_string ahead, behind})"
show_commits ahead unless $short
end
end
else
puts "\nIntegration branches:" unless ibs.empty? # unlikely
ibs.sort_by { |v, br| v }.each do |v, br|
next if $config["ignore"].member?(br[:local_branch]) || $config["ignore"].member?(br[:remote_branch])
next if br[:ignore]
local_only = br[:remote_branch].nil?
remote_only = br[:local_branch].nil?
name = remote_only ? cyan(br[:name]) : green(br[:name])
ahead = commits_between v, (b[:local_branch] || b[:remote_branch])
if ahead.empty?
puts "#{widget true, local_only} merged into #{name}"
else
#behind = commits_between b[:local_branch], v
puts "#{widget false, local_only} NOT merged into #{name} (#{ahead.size.pluralize 'commit'} ahead)"
show_commits ahead unless $short
end
end
end
end
#### EXECUTION STARTS HERE ####
## find config file and load it
$config = { "integration-branches" => %w(heads/master heads/next heads/edge), "ignore" => [], "max_commits" => 5 }.merge begin
fn = find_file CONFIG_FN
if fn && (h = YAML::load_file(fn)) # yaml turns empty files into false
h["integration-branches"] ||= h["versions"] # support old nomenclature
h
else
{}
end
end
if $dump_config
puts $config.to_yaml
exit
end
## first, index registered remotes
remotes = `git config --get-regexp ^remote\.\*\.url`.split(/[\r\n]+/).inject({}) do |hash, l|
l =~ /^remote\.(.+?)\.url (.+)$/ or next hash
hash[$1] ||= $2
hash
end
## next, index followed branches
branches = `git config --get-regexp ^branch\.`.split(/[\r\n]+/).inject({}) do |hash, l|
case l
when /branch\.(.*?)\.remote (.+)/
name, remote = $1, $2
hash[name] ||= {}
hash[name].merge! :remote => remote, :remote_url => remotes[remote]
when /branch\.(.*?)\.merge ((refs\/)?heads\/)?(.+)/
name, remote_branch = $1, $4
hash[name] ||= {}
hash[name].merge! :remote_mergepoint => remote_branch
end
hash
end
## finally, index all branches
remote_branches = {}
`git show-ref`.split(/[\r\n]+/).each do |l|
sha1, ref = l.chomp.split " refs/"
if ref =~ /^heads\/(.+)$/ # local branch
name = $1
next if name == "HEAD"
branches[name] ||= {}
branches[name].merge! :name => name, :local_branch => ref
elsif ref =~ /^remotes\/(.+?)\/(.+)$/ # remote branch
remote, name = $1, $2
remote_branches["#{remote}/#{name}"] = true
next if name == "HEAD"
ignore = !($all || remote == "origin")
branch = name
if branches[name] && branches[name][:remote] == remote
# nothing
else
name = "#{remote}/#{branch}"
end
branches[name] ||= {}
branches[name].merge! :name => name, :remote => remote, :remote_branch => "#{remote}/#{branch}", :remote_url => remotes[remote], :ignore => ignore
end
end
## assemble remotes
branches.each do |k, b|
next unless b[:remote] && b[:remote_mergepoint]
b[:remote_branch] = if b[:remote] == "."
b[:remote_mergepoint]
else
t = "#{b[:remote]}/#{b[:remote_mergepoint]}"
remote_branches[t] && t # only if it's still alive
end
end
show_dirty = ARGV.empty?
targets = if ARGV.empty?
[`git symbolic-ref HEAD`.chomp.sub(/^refs\/heads\//, "")]
else
ARGV.map { |x| x.sub(/^heads\//, "") }
end.map { |t| branches[t] or abort "Error: can't find branch #{t.inspect}." }
targets.each do |t|
show t
show_relations t, branches if $show_relations || t[:remote_branch].nil?
end
modified = show_dirty && `git ls-files -m` != ""
uncommitted = show_dirty && `git diff-index --cached HEAD` != ""
if $key
puts
puts KEY
end
puts if modified || uncommitted
puts "#{red "NOTE"}: working directory contains modified files." if modified
puts "#{red "NOTE"}: staging area contains staged but uncommitted files." if uncommitted
# the end!