#!/bin/bash # I'm writing this in BASH because I hate myself, apparently. list_branches() { git for-each-ref refs/heads --format="%(refname:short)" } list_remote_branches() { git for-each-ref refs/heads --format="%(refname:short)" } case "$1" in "" | status) # TODO: Display help if not inside a repo # TODO: figure out how to git config --global color.status always automatically. git status \ | grep -v 'On branch' \ | grep -v '(use "git push" to publish your local commits)' \ | grep -v '(use "git reset HEAD ..." to unstage)' \ | grep -v '(use "git add ..." to update what will be committed)' \ | grep -v '(use "git checkout -- ..." to discard changes in working directory)' \ | grep -v '(use "git add ..." to include in what will be committed)' \ | grep -v 'nothing added to commit but untracked files present (use "git add" to track)' ;; stage) if [ -z "$2" ]; then echo Staging all modified files git add -u :/ else echo Staging "${@:2}" git add "${@:2}" fi ;; unstage) echo Unstaging if [ -z "$2" ]; then echo The stage has been reset. git reset HEAD else git reset HEAD "${@:2}" fi ;; tag) echo Tagging git tag "$2" ;; untag) # Delete tag (if tag exists) if git rev-parse "$2" >/dev/null 2>&1 then echo Deleting tag "$2" git tag -d "$2" fi # Get local branch name local_branch=$(git rev-parse --abbrev-ref HEAD) # Get associated remote remote=$(git config --get branch.$local_branch.remote) # If tag is not present on remote, stop here. exist=$(git ls-remote --tags "$remote" "$2") if [ "$exist" != '' ] then # Prompt user to delete on upstream. read -e -p "Would you like to delete the tag on remote '${remote}'? [Y/n]: " deltag deltag=${deltag:-Y} case "$deltag" in [Yy] | [Yy][Ee][Ss] ) git push --delete "$remote" "$2" ;; [Nn] | [Nn][Oo] ) ;; esac fi ;; add) # TODO: make add only add untracked files (useful when auto-completing) ;; rm) echo Removing git rm "$2" ;; reset) echo Resetting if [ -z "$2" ]; then echo The working directory has been reset. git checkout -f HEAD else git checkout "${@:2}" fi ;; commit) echo "Parent commit: $(git log --abbrev-commit -1 --pretty=format:'%C(bold blue)%s%Creset %Cgreen(%cr)%Creset')" if [ -z "$2" ]; then read -ep 'Message: ' msg git commit -m "$msg" else git commit -m "$2" fi ;; undo) case "$2" in commit) echo Undoing last commit git reset --soft HEAD~1 ;; esac ;; branch) echo 'Switching to branch' if [ -z "$2" ]; then echo '! Specify branch name' else #git stash save --include-untracked --quiet 'get-branch autostash' # Save branch index state if git rev-parse --verify --quiet "$2" > /dev/null; then git checkout "$2" else git checkout -b "$2" fi #git stash pop --quiet # Restore branch index state fi ;; rmbranch) echo 'Delete branch' if [ -z "$2" ]; then echo '! Specify branch name' else git branch -d "$2" fi ;; fetch) echo Updating if [ -z "$2" ]; then git fetch --all branches="$(list_branches)" else branches = "${@:2}" git fetch "$branches" fi # Fast-forward local branches. I owe a lot to http://stackoverflow.com/a/24451300/2168416 current_branch=$(git rev-parse --abbrev-ref HEAD) for local_branch in $branches; do remote=$(git config --get branch.$local_branch.remote) remote_branch=$(git config --get branch.$local_branch.merge | sed 's:refs/heads/::') # Git throws an error if we try the fetch command on the current branch. Sheesh if [ "$current_branch" = "$local_branch" ]; then git merge --ff-only $remote/$remote_branch else git fetch $remote $remote_branch:$local_branch fi done ;; ignore) ;; diff) # Note: we use --ignore-space-change with every diff because # changing the amount of indentation of large chunks of code # is common in Python and CoffeeScript. if [ -z "$2" ]; then echo 'Compare working tree with HEAD' git diff --ignore-space-change HEAD else if [ -z "$3" ]; then if [ "$2" = 'STAGE' ]; then echo 'Compare working tree with stage' git diff --ignore-space-change else echo "Compare working tree with $2" git diff --ignore-space-change "$2" fi else if [ "$2" = 'STAGE' ]; then echo "Compare stage with $3" git diff --cached --ignore-space-change "$3" else echo "Compare $2 with $3" git diff --ignore-space-change "$2" "$3" fi fi fi ;; review) echo 'Compare stage with HEAD' git diff --cached --ignore-space-change HEAD ;; push) echo 'Pushing' # Check to see if upstream is set. if git rev-parse --abbrev-ref @{upstream} >/dev/null ; then git push else # Get local branch name local_branch=$(git rev-parse --abbrev-ref HEAD) # Check for multiple remotes remote_count=$(git remote show | wc -l) remotes=$(git remote show | tr '\n' ' ' | sed 's/\s*$//g') if [ "$remote_count" = "1" ]; then # If only one remote remote="$remotes" else read -p "Which remote to push? (${remotes}): " remote fi read -p "Choose name for branch on '${remote}' [${local_branch}]: " remote_branch if [ "$remote_branch" = "" ]; then remote_branch="$local_branch" fi echo "I will run git push --set-upstream ${remote} ${remote_branch}" git push --set-upstream ${remote} ${remote_branch} fi ;; clone) # Github username, if available github_user=$(git config github.user) # Turn "username/repo" into full Github URL if [[ "$2" =~ ^[^/]+/[^/]+$ ]] then url="https://github.com/$2" # Turn "repo" into "username/repo" Github URL elif [[ "$2" =~ ^[^/]+$ ]] && ! [ -z "$github_user" ] then url="https://github.com/${github_user}/$2" else url="$2" fi echo Cloning $url git clone "$url" ;; squash) # I implement squash a little differently than most. # 1) Rebase squashing aggregates commit messages, which is usually # counter to the purpose of squashing, which is to hide the fact # that a change took several real commits. # 2) Rebasing also deletes commits by default, which is problematic # if you have pushed those commits to the server already. # This solution is more gentle in that it creates a new commit with # a new commit message containing the same changes as the old # series of commits, but doesn't delete the old commits, leaving them # as a branch. # TODO: check argument is numeric if [[ "$2" =~ ^[0-9]+$ ]] then echo "Squash the following commits together:" git log --abbrev-commit \ --color \ --graph \ --ancestry-path \ --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' \ HEAD...HEAD~$2 read -p 'Message: ' msg git stash save --include-untracked --quiet 'get-squash autostash' git reset --soft HEAD~$(($2+1)) git commit -m "$msg" git stash pop --quiet else echo "The second argument is expected to be an integer." fi ;; lg) git log --color \ --graph \ --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' \ --abbrev-commit -10 "${@:2}" ;; *) echo "Passing args straight to git..." git $@ ;; esac