#!/bin/bash # # Copyright (c) 2012-2013 Luke Shumaker # set -eE USAGE='[-h|-v|--no-tag|--tag=tag] ...' LONG_USAGE='Like filter-branch, but can be used to update branches. This creates or updates from . If already exists, it only runs new commits through the filter. This way, this can be used to pull from a remote repository with a different layout than yours. New commits are detected by examining a "tag" in the commit messages of that can be used to tie those commits to corresponding commits in . By default, the tag is "git-rewrite-id: " followed by the commit hash of the corresponding commit; similar to the "git-svn-id" appended by `git-svn`. However, if already has suitable tags in the commit messages (such as "git-svn-id"), then that can be used, and the commit message does not need to be edited. -h Show this text -v Be more verbose --svn Use an existing "git-svn-id" instead of "git-rewrite-id" (in-branch was generated by git-svn) --tag= Use the exiting as found in , instead of the commit hash. --no-tag Disregard a previous "--tag="; set "git-rewrite-id" to the commit hash. --svn An alias for "--tag=git-svn-id" is passed directly to `git filter-branch`, and therefore has the same format. ' SUBDIRECTORY_OK=Yes OPTIONS_SPEC= . git-sh-setup . git-sh-i18n require_work_tree_exists # I $hastag is false, $ibranch's commit hashes are used as IDs hastag=false # whether ibranch already has tags tag='git-rewrite-id' ibranch='' obranch='' wbranch='' panic() { echo 'panic: malformed call to internal function' >&2 exit 1 } # Usage: verbose format arg1 arg2... # Print the printf string, but only if in verbose mode. # Verbose mode works by redefining this function during option parsing if -v is # encountered. verbose() { : } ################################################################################ # Usage: id2commit $branch $id # Returns the commit on $branch with $id. id2commit() { [[ $# == 2 ]] || panic local branch=$1 local id=$2 if [[ $branch == $ibranch ]] && ! $hastag; then printf '%s\n' "$id" else git log "$branch" --pretty=format:'%H' --grep "${tag}: ${id}" fi } # Usage: commit2id $branch $commit # Returns the id of $commit, which is on $branch. commit2id() { [[ $# == 2 ]] || panic local branch=$1 local commit=$2 if [[ $branch == $ibranch ]] && ! $hastag; then printf '%s\n' "$commit" else git log "$branch" -n1 --pretty=formtat:'%B' "$commit" | sed -n "s|^\s*${tag}: ||p" fi } # commit2commit # Usage: c2c $from $to $commit # Returns the commit on branch $to that has the same id as $commit, which is # on branch $from. c2c() { [[ $# == 3 ]] || panic local from=$1 local to=$2 local commit=$3 if [[ "$from" == "$to" ]]; then # optimization # also, properly normalizes $ibranch when $hastag=false git log "$commit" -n1 --pretty=format:'%H' else id2commit "$to" "$(commit2id "$from" "$commit")" fi } ################################################################################ main() { # Parse command line arguments ######################################### while true; do case "${1:-}" in --svn) hastag=true; tag='git-svn-id' ; shift 1 ;; --tag=*) hastag=true; tag="${1#*=}" ; shift 1 ;; --tag) hastag=true; tag=$2 ; shift 2 ;; --no-tag) hastag=false; tag='git-rewrite-id'; shift 1 ;; -h) usage; return 0;; -v) verbose() { local fmt=$1 shift printf "${fmt}\n" "$@" } shift ;; *) break;; esac done if [[ $# < 3 ]]; then usage return 1 fi ibranch=$1 obranch=$2 shift 2 local filters=(); if ! $hastag; then # Add a filter to append the id to the commit message. # This is a little confusing to read because of double quoting. filters=(--msg-filter "sed '\$a'\"${tag}: \${GIT_COMMIT}\"") # Here it is annotated; "@" indicates characters to be taken # literally, and "^" indicates string interpolation. # # Argument to git-filter-branch: # sed '$a'"${tag}: ${GIT_COMMIT}" # @@@@@@@@@^^^^^^@@@@@@@@@@@@@@@@ # Argument to sed: # $a${tag}: ${GIT_COMMIT} # @@^^^^^^@@^^^^^^^^^^^^^ fi filters+=("$@") # Main ################################################################# wbranch="$obranch.tmp" local revlist local rebase if git checkout "$obranch" -- 2>/dev/null; then # obranch exists, update it echo 'Updating existing rewritten branch...' local icommit="$(c2c "$ibranch" "$ibranch" "$ibranch")" local ocommit="$(c2c "$obranch" "$ibranch" "$obranch")" if [[ "$icommit" == "$ocommit" ]]; then echo "Nothing to do" return 0 fi local common="$(c2c "$obranch" "$ibranch" "$obranch")" if c2c "$ibranch" "$ibranch" "${common}^" &>/dev/null; then revlist="$(c2c "$obranch" "$ibranch" "$obranch")^..${wbranch}" else # There is only one commit from $ibranch in $obranch revlist="$wbranch" fi rebase=true else # obranch does not exist, create it echo 'Creating new rewritten branch...' revlist=$wbranch rebase=false fi git checkout "$ibranch" git branch -D "$wbranch" 2>/dev/null || true git checkout -b "$wbranch" git filter-branch -f "${filters[@]}" "$revlist" if $rebase; then # rebase the changes in wbranch onto obranch echo 'Rebasing rewrites onto existing branch...' local wcommit="$(c2c "$wbranch" "$ibranch" "$wbranch")" if [[ "$wcommit" == "$ocommit" ]]; then echo "Nothing to do" return 0 fi local commonish="$(c2c "$obranch" "$wbranch" "$obranch")" cmd=(git rebase --onto "$obranch" "$commonish" "$wbranch") verbose '' verbose ' o---o---o %s' "$obranch" verbose ' :' verbose ' o---o---o---o---o %s' "$ibranch" verbose ' \ :' verbose ' `-C---o---o %s' "$wbranch" verbose '' verbose ' C = %s' "$commonish" verbose '' verbose ' %s' "$(printf '%q ' "${cmd[@]}")" verbose '' "${cmd[@]}" git checkout "$obranch" git branch -d "$wbranch" else git branch -m "$wbranch" "$obranch" fi } main "$@"