diff --git a/README.md b/README.md index 436783c..4bd07a9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,47 @@ github-flow =========== -git flow esque helpers, designed with github in mind. \ No newline at end of file +git flow esque helpers, designed with github in mind. + +Installation +------------ + +To install, run the installation script: + +``` +curl https://raw.github.com/github-flow/github-flow/v1.1/install.sh | $(which bash) +``` + +To update, run the install script again. + +To install or update outside the default directory, pass a folder as the first parameter to bash. + +Usage +----- + +Usage is very similar to git-flow + +`git hub-clone ` Fork and clone a repository to use with git hub-flow. + +`git hub-feature start ` Begin working on a new feature. + +`git hub-feature update [feature]` Bring master branch up to date, and rebase `feature` off master. + +`git hub-feature publish [feature]` Push changes on `feature` to `origin` repository. + +`git hub-feature review [feature]` Open pull request on `upstream`/`feature` against `origin`/`feature`. + +`git hub-feature finish [feature]` Merge pull request for this feature, and prune branches. + +All commands with `[feature]` either take a parameter to specify the branch, or work off the current (non-`master`) branch. + + +Configuration +------------- + +Repositories are configured using three properties under `github-flow`: `origin`, `upstream`, and `master`. Origin and upstream are the logical remotes. `upstream` is the canonical upstream project, usually owned by an organization. `origin` is the users' fork, generally owned by the user's account. 'master' is the logical primary development branch, usually either "master" or "develop". By default, the values are the same as their keys. To use the traditional git-flow model of branching and working off develop, in a local repository run `git config --local --add github-flow.master develop` + +TODO, Bugs +---------- + +See the [bug tracker](https://github.com/github-flow/github-flow/issues). diff --git a/bin/git-hub-clone b/bin/git-hub-clone new file mode 100755 index 0000000..d5f2963 --- /dev/null +++ b/bin/git-hub-clone @@ -0,0 +1,55 @@ +#!/bin/bash + +source git-hub-helpers + +# Safely +_fork(){ + local FULL_REPO=$1 + local REPO=$2 + local USER=$3 + + github /repos/$FULL_REPO/forks >| .tmp_forks + + if [ ! $(cat .tmp_forks | json -a owner.login | fgrep "$(github_user)") ] ; then + github /repos/$FULL_REPO/forks .tmp_fork + # Sleep for a moment, to let github register the fork. + sleep 1 + fi + echo "Fork at https://github.com/$USER/$REPO" + rm .tmp_fork* +} + +# Clone the repo and set a variety of --local config options for github-flow. +clone(){ + local FULL_REPO=$1 + local REPO=$(echo $FULL_REPO | awk -F/ '{print $2}') + local USER=$(github_user) + + _fork $FULL_REPO $REPO $USER + + git clone $Q git@github.com:$USER/$REPO.git 2>&1 >/dev/null + cd $REPO + git remote add upstream git@github.com:$FULL_REPO.git + + git config --local --add github-flow.origin origin + git config --local --add github-flow.upstream upstream + git config --local --add github-flow.master master + + echo "Project cloned into $(pwd)" +} + +usage(){ + echo "$0 : Initialize a new working repository, and set git-hubflow configurations." +} + +help(){ + usage + echo "" + echo " : The organization and repository name to clone from." +} + +if [ "x$1" == "-h" ] ; then + help +else + clone $@ +fi diff --git a/bin/git-hub-feature b/bin/git-hub-feature index e6794d6..0583e62 100755 --- a/bin/git-hub-feature +++ b/bin/git-hub-feature @@ -2,62 +2,80 @@ source git-hub-helpers +# Begin working on a new feature start(){ - BRANCH=$1 - git branch $BRANCH $MASTER - git checkout $BRANCH + BRANCH=${1:?"Please specify a branch name."} + git stash + # Just a semantic `git checkout -b $BRANCH $MASTER` + git checkout -b $BRANCH $MASTER + git stash pop } +# Make the feature branch available on the user's fork. publish(){ - CUR_BRANCH=$(git rev-parse --abbrev-ref HEAD) BRANCH=${1:-$CUR_BRANCH} git hub-feature update $BRANCH - git push $ORIGIN $BRANCH + echo "Forcing updates to https://github.com/$GH_ORIGIN/tree/$BRANCH" + git push $Q --force $ORIGIN $BRANCH } +# Get all updates from upstream into the current branch. +# Can cause merge conflicts, when rebasing and when poping the stash. update(){ - CUR_BRANCH=$(git rev-parse --abbrev-ref HEAD) - git stash + _stash BRANCH=${1:-$CUR_BRANCH} git checkout $MASTER - git pull --rebase $UPSTREAM $MASTER - git checkout $BRANCH - git rebase $MASTER + echo "Rebasing upstream updates into master..." + git pull $Q --rebase $UPSTREAM $MASTER + echo "Rebasing master into $BRANCH..." + git checkout $Q $BRANCH + git rebase $Q $MASTER + + if [ $CUR_BRANCH != $BRANCH ] ; then + git checkout $Q $CUR_BRANCH + fi - git checkout $CUR_BRANCH - git stash pop + _stash_pop } +# Create a pull request to review the feature branch. review(){ - CUR_BRANCH=$(git rev-parse --abbrev-ref HEAD) BRANCH=${1:-$CUR_BRANCH} git hub-feature publish $BRANCH - TITLE='"title":"'"$BRANCH: $(git log -1 --pretty=format:'%s')"'"' - BODY='"body":"'"$(git log --pretty=format:'%h - %s <%an>' $MASTER.. | sed ':a;N;s/\n/\\n/g;b a')"'"' - BASE='"base":"'"$GH_UPSTREAM_USER:$MASTER"'"' - BASE_SHA='"base_sha":"'"$(git rev-parse $MASTER)"'"' - HEAD='"head":"'"$GH_ORIGIN_USER:$BRANCH"'"' - HEAD_SHA='"head_sha":"'"$(git rev-parse $BRANCH)"'"' + PULL_NUMBER=$(github_pr_number $BRANCH) - echo "{$TITLE,$BODY,$BASE,$HEAD,$BASE_SHA,$HEAD_SHA}" >| .tmp_pr_req + # Check if the PR already exists + if [ -z "$PULL_NUMBER" ] ; then + # Prepare the JSON for the pull request. + # The title is the most recent commit message at time of starting the review. + TITLE='"title":"'"$BRANCH: $(git log -1 --pretty=format:'%s')"'"' + BODY='"body":"'"$(git log --pretty=format:'%h - %s <%an>' $MASTER.. | sed ':a;N;s/\n/\\n/g;b a')"'"' + BASE='"base":"'"$GH_UPSTREAM_USER:$MASTER"'"' + BASE_SHA='"base_sha":"'"$(git rev-parse $MASTER)"'"' + HEAD='"head":"'"$GH_ORIGIN_USER:$BRANCH"'"' + HEAD_SHA='"head_sha":"'"$(git rev-parse $BRANCH)"'"' - github /repos/$GH_UPSTREAM/pulls .tmp_pr_req >| .tmp_pr_res + echo "{$TITLE,$BODY,$BASE,$HEAD,$BASE_SHA,$HEAD_SHA}" >| .tmp_pr_req - PULL_NUMBER=$(github_pr_number $BRANCH) - PULL_URL="http://github.com/$GH_UPSTREAM/pulls/$PULL_NUMBER" + github /repos/$GH_UPSTREAM/pulls .tmp_pr_req >| .tmp_pr_res + + PULL_NUMBER=$(github_pr_number $BRANCH) + fi + + PULL_URL="http://github.com/$GH_UPSTREAM/pull/$PULL_NUMBER" echo "Pull request is at $PULL_URL" rm .tmp_pr* } +# Close the feature branch, if it is mergeable. finish(){ - CUR_BRANCH=$(git rev-parse --abbrev-ref HEAD) BRANCH=${1:-$CUR_BRANCH} git hub-feature publish $BRANCH @@ -68,10 +86,13 @@ finish(){ if [ "x$(cat .tmp_merge | json merged)" == "xtrue" ] ; then echo "Pull request $PULL_NUMBER successfully merged." - git checkout $MASTER - git pull --rebase $UPSTREAM $MASTER - git push $ORIGIN :$BRANCH - git branch -D $BRANCH + _stash + git checkout $Q $MASTER >/dev/null + _stash_pop + + git pull $Q --rebase $UPSTREAM $MASTER + git push $Q $ORIGIN :$BRANCH + git branch $Q -D $BRANCH else cat .tmp_merge | json message fi diff --git a/bin/git-hub-helpers b/bin/git-hub-helpers old mode 100644 new mode 100755 index d206eb6..fc0c073 --- a/bin/git-hub-helpers +++ b/bin/git-hub-helpers @@ -1,10 +1,18 @@ #!/bin/bash +Q="--quiet" + API="https://api.github.com" +# Run a GET or POST request against the Github API +# First param is the API endpoint to hit +# Second param is an optional filename to use as the data. +# Handles auth. github(){ URL=$1 DATA=$2 + github_login + AUTH="Authorization: token $(cat ~/.github_token)" CURL="curl" if [ ! -z "$DATA" ] ; then @@ -14,20 +22,32 @@ github(){ $CURL -H "$AUTH" "$API$URL" 2>/dev/null } +# Run a PUT request against Github, with no data in the body. github_put(){ URL=$1 AUTH="Authorization: token $(cat ~/.github_token)" CURL="curl" - $CURL -X PUT -d "$2" -H "$AUTH" "$API$URL" 2>/dev/null + $CURL -X PUT -d "$2" -H "$AUTH" "$API$URL" +} + +# Try and get the username. Read and set it if it is unset. +github_user(){ + USERNAME=$(git config --get github-flow.username) + if [ $? -eq 1 ] ; then + read -p "Github username: " USERNAME + git config --global --add github-flow.username $USERNAME + fi + echo $USERNAME } +# Get an access token. github_login(){ - read -p "Github username: " USERNAME - curl -u $USERNAME -d '{"scopes":["repo"],"note":"git-hub-flow"}' $API/authorizations | json token >| ~/.github_token + [ -f ~/.github_token ] || curl -u $(github_user) -d '{"scopes":["repo"],"note":"git-hub-flow"}' $API/authorizations | json token >| ~/.github_token } +# Find the pr number from the current branch. github_pr_number(){ BRANCH=$1 read NUMBER LABEL </dev/null && TIMES_STASHED=1 + TIMES_STASHED=$((TIMES_STASHED + 1)) +} + +_stash_pop(){ + TIMES_STASHED=$((TIMES_STASHED - 1)) + [ $TIMES_STASHED -le 0 ] && git stash pop 2>&1 >/dev/null +} + +# Get some config info. +ORIGIN=$(git config --get github-flow.origin) +UPSTREAM=$(git config --get github-flow.upstream) +MASTER=$(git config --get github-flow.master) -GH_ORIGIN=$(git remote -v | grep $ORIGIN | head -1 | awk -F: '{print $2}' | awk -F. '{print $1'}) +# Helper to get an "OWNER/REPO" string. +find_repo(){ + while read line ; do + echo $line | sed -e 's!^.*git@github.com[/:]!!' -e 's!\.git.*$!!' + done +} + +# Get the info about the repos. +GH_ORIGIN=$(git remote -v 2>/dev/null | fgrep "$ORIGIN" | head -1 | find_repo) GH_ORIGIN_USER=$(echo $GH_ORIGIN | awk -F/ '{print $1}') -GH_UPSTREAM=$(git remote -v | grep $UPSTREAM | head -1 | awk -F: '{print $2}' | awk -F. '{print $1'}) +GH_UPSTREAM=$(git remote -v 2>/dev/null | fgrep "$UPSTREAM" | head -1 | find_repo) GH_UPSTREAM_USER=$(echo $GH_UPSTREAM | awk -F/ '{print $1}') + +# Always starting on some branch. +CUR_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null) diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..943477a --- /dev/null +++ b/install.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +INSTALL_DIR=${1:-$HOME} + +echo 'Checking for $(which json)' +which json > /dev/null 2>&1 +if [ $? -ne 0 ] ; then + echo '$(which json) not found, installing from npm. May require global permissions.' + npm install -g jsontool + if [ $? -ne 0 ] ; then + echo "Couldn't install jsontool from npm. Please run 'sudo npm install -g json', then rerun this installer." + exit 1 + fi +else + echo "jsontool found." +fi + +echo "Installing into $INSTALL_DIR/.ghf" + +# Create the install directory if it doesn't exist. +[ -d $INSTALL_DIR ] || mkdir -p $INSTALL_DIR + +cd $INSTALL_DIR + +# If it's already installed, update from origin. +if [ -d .ghf ] ; then + cd .ghf + echo "github-flow already installed, updating from origin." + git pull origin master +else + git clone https://github.com/github-flow/github-flow.git .ghf +fi + +# Only modify the path if the newly installed ghf doesn't exist. +if [ ! $(echo $PATH | fgrep "$INSTALL_DIR/.ghf/bin") ] ; then + echo "Adding .ghf to PATH." + + echo "PATH was '$PATH'" + + echo "PATH=$INSTALL_DIR/.ghf/bin:\$PATH" >> ~/.bashrc + + source ~/.bashrc + + echo "PATH is '$PATH'" + + echo "source ~/.bashrc to update path in the current shell." +fi