Skip to content

Instantly share code, notes, and snippets.

@arcanis
Last active January 2, 2016 14:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save arcanis/8315017 to your computer and use it in GitHub Desktop.
Save arcanis/8315017 to your computer and use it in GitHub Desktop.
Custom git command for Pivotal / Github

Git Pivotal

git pivotal --start <branch_01234567>|<01234567>
  Creates a new branch, sets its upstream to the correct remote branch then
  sets the task state to 'started'. If the branch name is omitted, the script
  will use the name of the current branch. If the branch name only contain an
  issue ID, the script will try to make use of the _git_pivotal completion.

git pivotal --clean
  Fetchs the remote parent (master or develop), starts an interactive rebase
  with it, then push the result to the upstream branch.

git pivotal --deliver
  Creates a Github pull request then sets the task state to 'delivered'. If
  there's already a closed pull request on the same branch and same base, the
  old one will be reopened instead.

Branches whose name begins with 'hotfix/' depends on master (ie. that --clean
will rebase on master, and --deliver will open a pull request on master), any
other will depends on develop.

Workflow

#1 > git pivotal --start feature/make-things-more-awesome-424242
#2 > echo "Everything is awesome" >> awesome.txt
#3 > git add awesome.txt
#4 > git commit -m "Adds awesomeness"
#5 > git pivotal --clean
#6 > git pivotal --deliver

A few notes :

  • #1 : Assuming that the issue name is "Make things more awesome", git pivotal --start 424242 would have worked too. And you can use Pivotal's copy-issue-id-on-click for increased productivity. A few more seconds that you won't have to spend on Pivotal, yeah !
  • #1 : You can make it an hotfix (ie. based on master rather than develop) by using the hotfix/ prefix rather than feature/ or bug/, but you will have to edit the branch
  • #5 : Not strictly required, it will rebase (interactive) your branch with develop (or develop for hotfixes) then force-push, so it's useful when you have done with your issue

Requirements

You will need underscore-cli, which is used to parse JSON response from the Pivotal Tracker API.

$> sudo npm install -g underscore-cli

Installation

Put git-pivotal somewhere inside your $PATH (for example /usr/local/bin), and add a source <_git_pivotal.sh> somewhere inside your bashrc file.

sudo wget -O /usr/local/bin/git-pivotal.sh https://gist.githubusercontent.com/arcanis/8315017/raw/git-pivotal
cd $HOME && wget -O _git_pivotal.sh https://gist.githubusercontent.com/arcanis/8315017/raw/_git_pivotal.sh
echo 'source ~/_git_pivotal.sh' >> $HOME/.bashrc

In your git repository, type :

git config project.pivotal.id        <pivotal tracker project id (ex: 75256)>
git config project.github.repository <github repository name (ex: foo/bar)
git config pivotal.token             <pivotaltracker api token (ex: 01234567890ABCDEFGH)>
git config github.token              <github api token (ex: 01234567890ABCDEFGH)>

Et voilà ! You can find your Pivotal api token at the bottom of your Pivotal profile page, and you can create Github's tokens on your Github profile page too.

_git_pivotal()
{
local IFS=$'\n'
local PIVOTAL_PROJECT=$(git config project.pivotal.id)
local PIVOTAL_TOKEN=$(git config pivotal.token)
local PIVOTAL_ISSUES="/tmp/pivotal-issues.${PIVOTAL_PROJECT}"
if [ "${PIVOTAL_PROJECT}" = "" ] || [ "${PIVOTAL_TOKEN}" = "" ]; then
return
fi
if [ ! -f "${PIVOTAL_ISSUES}" ] || test $(find "${PIVOTAL_ISSUES}" -mmin +30); then
local data="$(curl -X GET -H "X-TrackerToken: ${PIVOTAL_TOKEN}" "https://www.pivotaltracker.com/services/v5/me" 2> /dev/null )"
local uid=$(printf "%s" "${data}" | underscore select ":root > .id" | grep -Po "[0-9]+")
local data="$(curl -X GET -H "X-TrackerToken: ${PIVOTAL_TOKEN}" "https://www.pivotaltracker.com/services/v5/projects/${PIVOTAL_PROJECT}/stories?filter=mywork:${uid}" 2> /dev/null)"
local issues=( $(printf "%s" "${data}" | underscore --outfmt text map "value.id+'_'+value.story_type+'_'+value.name.trim().replace(/_/g,'-')" 2> /dev/null) )
local branches=( )
for issue in ${issues[@]}; do
local id=$( printf "%s" "${issue}" | cut -d_ -f1 )
local type=$( printf "%s" "${issue}" | cut -d_ -f2 )
local name=$( printf "%s" "${issue}" | cut -d_ -f3 )
local slug=$(echo -n "${name}" | sed -e 's/[^[:alnum:]]/-/g' | tr -s '-' | tr A-Z a-z | sed 's/^-*//g' | sed 's/-*$//g')
local branch="${type}/${slug}_${id}"
branches+=("${branch}")
done
printf "%s\n" "${branches[@]}" | sort > "${PIVOTAL_ISSUES}"
else
local branches=( $(cat "${PIVOTAL_ISSUES}") )
fi
COMPREPLY=( $(compgen -W '$(printf "%q\n" "${branches[@]}")' -- "${COMP_WORDS[COMP_CWORD]}" ) )
}
export -f _git_pivotal
#!/usr/bin/env bash
## default options
mode=
githubApi=https://api.github.com
pivotalApi=https://www.pivotaltracker.com/services/v5
projectGithubRepository=$(git config project.github.repository)
projectPivotalId=$(git config project.pivotal.id)
pivotalToken=$(git config pivotal.token)
githubToken=$(git config github.token)
estimation=1
## usage function
function usage() {
local warned=
echo
if [ "$projectGithubRepository" = "" ]; then
echo "WARNING : Missing 'project.github.repository' git configuration entry"
warned=yes
fi
if [ "$projectPivotalId" = "" ]; then
echo "WARNING : Missing 'project.pivotal.id' git configuration entry"
warned=yes
fi
if [ "$pivotalToken" = "" ]; then
echo "WARNING : Missing 'pivotal.token' git configuration entry"
warned=yes
fi
if [ "$githubToken" = "" ]; then
echo "WARNING : Missing 'github.token' git configuration entry"
warned=yes
fi
if ! type -t _git_pivotal > /dev/null; then
echo "WARNING : The _git_pivotal completion function has not been found"
warned=yes
fi
if [ "${warned}" != "" ]; then
echo
fi
echo "git pivotal --start <branch_01234567>|<01234567>"
echo " Creates a new branch, sets its upstream to the correct remote branch then"
echo " sets the task state to 'started'. If the branch name is omitted, the script"
echo " will use the name of the current branch. If the branch name only contain an"
echo " issue ID, the script will try to make use of the _git_pivotal completion."
echo
echo "git pivotal --clean"
echo " Fetchs the remote parent (master or develop), starts an interactive rebase"
echo " with it, then push the result to the upstream branch."
echo
echo "git pivotal --deliver"
echo " Creates a Github pull request then sets the task state to 'delivered'. If"
echo " there's already a closed pull request on the same branch and same base, the"
echo " old one will be reopened instead."
echo
echo "Branches whose name begins with 'hotfix/' depends on master (ie. that --clean"
echo "will rebase on master, and --deliver will open a pull request on master), any"
echo "other will depends on develop."
echo
}
## tools
pivotal_get() {
curl -s -X GET -H "X-TrackerToken: ${pivotalToken}" -H "Content-Type: application/json" -d "${2}" "${pivotalApi}${1}"
}
pivotal_post() {
curl -s -X POST -H "X-TrackerToken: ${pivotalToken}" -H "Content-Type: application/json" -d "${2}" "${pivotalApi}${1}"
}
pivotal_put() {
curl -s -X PUT -H "X-TrackerToken: ${pivotalToken}" -H "Content-Type: application/json" -d "${2}" "${pivotalApi}${1}"
}
github_get() {
curl -s -X GET -H "Authorization: token ${githubToken}" -H "Content-Type: application/json" -d "${2}" "${githubApi}${1}"
}
github_post() {
curl -s -X POST -H "Authorization: token ${githubToken}" -H "Content-Type: application/json" -d "${2}" "${githubApi}${1}"
}
github_put() {
curl -s -X PUT -H "Authorization: token ${githubToken}" -H "Content-Type: application/json" -d "${2}" "${githubApi}${1}"
}
github_patch() {
curl -s -X PATCH -H "Authorization: token ${githubToken}" -H "Content-Type: application/json" -d "${2}" "${githubApi}${1}"
}
## command line parsing
while [[ $1 = -?* ]]; do
case $1 in
-h|--help)
usage
exit
;;
--start)
mode=start
;;
--estimation)
shift
estimation=$1
;;
--clean)
mode=clean
;;
--deliver)
mode=deliver
;;
*) ;;
esac
shift
done
## configuration check
if [ "${projectGithubRepository}" = "" ] ||
[ "${projectPivotalId}" = "" ] ||
[ "${pivotalToken}" = "" ] ||
[ "${githubToken}" = "" ]; then
usage
exit
fi
## execution
case "${mode}" in
start)
branch="${1}"
if [ "${branch}" = "" ]; then
branch="$(git rev-parse --abbrev-ref HEAD)"
fi
if [[ "${branch}" =~ ^[0-9]+$ ]] && type -t _git_pivotal > /dev/null; then
_git_pivotal # Require the _git_pivotal hook somewhere (export -f with bash is fine)
branch=$(printf '%s\n' ${COMPREPLY[@]} | grep -P '_'"${branch}"'$')
fi
pivotalStoryId=$(printf '%s' "${branch}" | grep -Po '_[0-9]+$' | grep -Po '[0-9]+$')
if [ "${pivotalStoryId}" = "" ]; then
usage
exit
fi
if printf '%s' "${branch}" | grep -P "^hotfix/"; then
parent=master
else
parent=develop
fi
if ! git show-ref --verify --quiet "refs/heads/${branch}"; then
git checkout "${parent}" || exit
git checkout -b "${branch}" || exit
git push --set-upstream origin "${branch}"
else
git checkout "${branch}" || exit
fi
pivotal_put /projects/"${projectPivotalId}"/stories/"${pivotalStoryId}" '{"estimate":'${estimation}',"current_state":"started"}' > /dev/null
echo "Story started."
;;
clean)
branch="$(git rev-parse --abbrev-ref HEAD)"
if printf '%s' "${branch}" | grep -P "^hotfix/"; then
parent=master
else
parent=develop
fi
git fetch
git rebase -i origin/"${parent}"
git push -f
;;
deliver)
branch="$(git rev-parse --abbrev-ref HEAD)"
pivotalStoryId=$(printf '%s' "${branch}" | grep -Po '_[0-9]+$' | grep -Po '[0-9]+$')
if [ "${pivotalStoryId}" = "" ]; then
usage
exit
fi
if printf '%s' "${branch}" | grep -P "^hotfix/"; then
parent=master
else
parent=develop
fi
git push || exit
sleep 3
prUser=$(printf "%s" "${projectGithubRepository}" | grep -oP '^[^/]+')
prRequest=$(github_get /repos/"${projectGithubRepository}"/pulls"?state=closed&head=${prUser}:${branch}&base=${parent}" | underscore --outfmt json extract 0 2> /dev/null)
# Is there already a PR ?
if [ "${prRequest}" = "" ]; then
# Nope, create a new one
prRequest=$(github_post /repos/"${projectGithubRepository}"/pulls '{"title":"[#'"${pivotalStoryId}"'] '"${branch}"'", "head":"'"${branch}"'", "base":"'"${parent}"'"}')
else
prNumber=$(printf "%s" "${prRequest}" | underscore --outfmt text extract number)
prMerged=$(github_get /repos/"${projectGithubRepository}"/pulls/"${prNumber}"/merge | underscore --outfmt text select ':root > .id')
# Has the pull request already been merged ?
if [[ "${prMerged}" != "" ]]; then
# Yep, create a new one
prRequest=$(github_post /repos/"${projectGithubRepository}"/pulls '{"title":"[#'"${pivotalStoryId}"'] '"${branch}"'", "head":"'"${branch}"'", "base":"'"${parent}"'"}')
else
# Nope, reopen the old one
github_patch /repos/"${projectGithubRepository}"/pulls/"${prNumber}" '{"state":"open"}' > /dev/null
fi
fi
prLink=$(printf "%s" "${prRequest}" | underscore --outfmt text extract html_url)
if [ "${prLink}" = "" ]; then
echo "Pull request creation failed."
exit
fi
echo "Pull request ready (${prLink})."
pivotal_post /projects/"${projectPivotalId}"/stories/"${pivotalStoryId}"/comments '{"text":"Pull request is up at '"${prLink}"'"}' > /dev/null
pivotal_put /projects/"${projectPivotalId}"/stories/"${pivotalStoryId}" '{"current_state":"delivered"}' > /dev/null
echo "Story delivered."
;;
*)
usage
exit
;;
esac
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment