git-subtree.shon commit Okay, that was a little too aggressive. (96db2c0)
   1#!/bin/bash
   2#
   3# git-subtree.sh: split/join git repositories in subdirectories of this one
   4#
   5# Copyright (C) 2009 Avery Pennarun <apenwarr@gmail.com>
   6#
   7if [ $# -eq 0 ]; then
   8    set -- -h
   9fi
  10OPTS_SPEC="\
  11git subtree split [options...] <commit...> -- <path>
  12git subtree merge 
  13
  14git subtree does foo and bar!
  15--
  16h,help        show the help
  17q             quiet
  18v             verbose
  19onto=         try connecting new tree to an existing one
  20rejoin        merge the new branch back into HEAD
  21ignore-joins  ignore prior --rejoin commits
  22"
  23eval $(echo "$OPTS_SPEC" | git rev-parse --parseopt -- "$@" || echo exit $?)
  24. git-sh-setup
  25require_work_tree
  26
  27quiet=
  28command=
  29onto=
  30rejoin=
  31ignore_joins=
  32
  33debug()
  34{
  35        if [ -z "$quiet" ]; then
  36                echo "$@" >&2
  37        fi
  38}
  39
  40assert()
  41{
  42        if "$@"; then
  43                :
  44        else
  45                die "assertion failed: " "$@"
  46        fi
  47}
  48
  49
  50#echo "Options: $*"
  51
  52while [ $# -gt 0 ]; do
  53        opt="$1"
  54        shift
  55        case "$opt" in
  56                -q) quiet=1 ;;
  57                --onto) onto="$1"; shift ;;
  58                --no-onto) onto= ;;
  59                --rejoin) rejoin=1 ;;
  60                --no-rejoin) rejoin= ;;
  61                --ignore-joins) ignore_joins=1 ;;
  62                --no-ignore-joins) ignore_joins= ;;
  63                --) break ;;
  64        esac
  65done
  66
  67command="$1"
  68shift
  69case "$command" in
  70        split|merge) ;;
  71        *) die "Unknown command '$command'" ;;
  72esac
  73
  74revs=$(git rev-parse --default HEAD --revs-only "$@") || exit $?
  75dirs="$(git rev-parse --sq --no-revs --no-flags "$@")" || exit $?
  76
  77#echo "dirs is {$dirs}"
  78eval $(echo set -- $dirs)
  79if [ "$#" -ne 1 ]; then
  80        die "Must provide exactly one subtree dir (got $#)"
  81fi
  82dir="$1"
  83
  84debug "command: {$command}"
  85debug "quiet: {$quiet}"
  86debug "revs: {$revs}"
  87debug "dir: {$dir}"
  88
  89cache_setup()
  90{
  91        cachedir="$GIT_DIR/subtree-cache/$$"
  92        rm -rf "$cachedir" || die "Can't delete old cachedir: $cachedir"
  93        mkdir -p "$cachedir" || die "Can't create new cachedir: $cachedir"
  94        debug "Using cachedir: $cachedir" >&2
  95}
  96
  97cache_get()
  98{
  99        for oldrev in $*; do
 100                if [ -r "$cachedir/$oldrev" ]; then
 101                        read newrev <"$cachedir/$oldrev"
 102                        echo $newrev
 103                fi
 104        done
 105}
 106
 107cache_set()
 108{
 109        oldrev="$1"
 110        newrev="$2"
 111        if [ "$oldrev" != "latest_old" \
 112             -a "$oldrev" != "latest_new" \
 113             -a -e "$cachedir/$oldrev" ]; then
 114                die "cache for $oldrev already exists!"
 115        fi
 116        echo "$newrev" >"$cachedir/$oldrev"
 117}
 118
 119find_existing_splits()
 120{
 121        debug "Looking for prior splits..."
 122        dir="$1"
 123        revs="$2"
 124        git log --grep="^git-subtree-dir: $dir\$" \
 125                --pretty=format:'%s%n%n%b%nEND' "$revs" |
 126        while read a b junk; do
 127                case "$a" in
 128                        git-subtree-mainline:) main="$b" ;;
 129                        git-subtree-split:) sub="$b" ;;
 130                        *)
 131                                if [ -n "$main" -a -n "$sub" ]; then
 132                                        debug "  Prior: $main -> $sub"
 133                                        cache_set $main $sub
 134                                        echo "^$main^ ^$sub^"
 135                                        main=
 136                                        sub=
 137                                fi
 138                                ;;
 139                esac
 140        done
 141}
 142
 143copy_commit()
 144{
 145        # We're doing to set some environment vars here, so
 146        # do it in a subshell to get rid of them safely later
 147        git log -1 --pretty=format:'%an%n%ae%n%ad%n%cn%n%ce%n%cd%n%s%n%n%b' "$1" |
 148        (
 149                read GIT_AUTHOR_NAME
 150                read GIT_AUTHOR_EMAIL
 151                read GIT_AUTHOR_DATE
 152                read GIT_COMMITTER_NAME
 153                read GIT_COMMITTER_EMAIL
 154                read GIT_COMMITTER_DATE
 155                export  GIT_AUTHOR_NAME \
 156                        GIT_AUTHOR_EMAIL \
 157                        GIT_AUTHOR_DATE \
 158                        GIT_COMMITTER_NAME \
 159                        GIT_COMMITTER_EMAIL \
 160                        GIT_COMMITTER_DATE
 161                (echo -n '*'; cat ) |  # FIXME
 162                git commit-tree "$2" $3  # reads the rest of stdin
 163        ) || die "Can't copy commit $1"
 164}
 165
 166merge_msg()
 167{
 168        dir="$1"
 169        latest_old="$2"
 170        latest_new="$3"
 171        cat <<-EOF
 172                Split '$dir/' into commit '$latest_new'
 173                
 174                git-subtree-dir: $dir
 175                git-subtree-mainline: $latest_old
 176                git-subtree-split: $latest_new
 177        EOF
 178}
 179
 180toptree_for_commit()
 181{
 182        commit="$1"
 183        git log -1 --pretty=format:'%T' "$commit" -- || exit $?
 184}
 185
 186subtree_for_commit()
 187{
 188        commit="$1"
 189        dir="$2"
 190        git ls-tree "$commit" -- "$dir" |
 191        while read mode type tree name; do
 192                assert [ "$name" = "$dir" ]
 193                echo $tree
 194                break
 195        done
 196}
 197
 198tree_changed()
 199{
 200        tree=$1
 201        shift
 202        if [ $# -ne 1 ]; then
 203                return 0   # weird parents, consider it changed
 204        else
 205                ptree=$(toptree_for_commit $1)
 206                if [ "$ptree" != "$tree" ]; then
 207                        return 0   # changed
 208                else
 209                        return 1   # not changed
 210                fi
 211        fi
 212}
 213
 214copy_or_skip()
 215{
 216        rev="$1"
 217        tree="$2"
 218        newparents="$3"
 219        assert [ -n "$tree" ]
 220
 221        identical=
 222        p=
 223        for parent in $newparents; do
 224                ptree=$(toptree_for_commit $parent) || exit $?
 225                if [ "$ptree" = "$tree" ]; then
 226                        # an identical parent could be used in place of this rev.
 227                        identical="$parent"
 228                fi
 229                if [ -n "$ptree" ]; then
 230                        parentmatch="$parentmatch$parent"
 231                        p="$p -p $parent"
 232                fi
 233        done
 234        
 235        if [ -n "$identical" -a "$parentmatch" = "$identical" ]; then
 236                echo $identical
 237        else
 238                copy_commit $rev $tree "$p" || exit $?
 239        fi
 240}
 241
 242cmd_split()
 243{
 244        debug "Splitting $dir..."
 245        cache_setup || exit $?
 246        
 247        if [ -n "$onto" ]; then
 248                debug "Reading history for --onto=$onto..."
 249                git rev-list $onto |
 250                while read rev; do
 251                        # the 'onto' history is already just the subdir, so
 252                        # any parent we find there can be used verbatim
 253                        debug "  cache: $rev"
 254                        cache_set $rev $rev
 255                done
 256        fi
 257        
 258        if [ -n "$ignore_joins" ]; then
 259                unrevs=
 260        else
 261                unrevs="$(find_existing_splits "$dir" "$revs")"
 262        fi
 263        
 264        debug "git rev-list --reverse $revs $unrevs"
 265        git rev-list --reverse --parents $revs $unrevsx |
 266        while read rev parents; do
 267                debug
 268                debug "Processing commit: $rev"
 269                exists=$(cache_get $rev)
 270                if [ -n "$exists" ]; then
 271                        debug "  prior: $exists"
 272                        continue
 273                fi
 274                debug "  parents: $parents"
 275                newparents=$(cache_get $parents)
 276                debug "  newparents: $newparents"
 277                
 278                tree=$(subtree_for_commit $rev "$dir")
 279                debug "  tree is: $tree"
 280                [ -z $tree ] && continue
 281
 282                newrev=$(copy_or_skip "$rev" "$tree" "$newparents") || exit $?
 283                debug "  newrev is: $newrev"
 284                cache_set $rev $newrev
 285                cache_set latest_new $newrev
 286                cache_set latest_old $rev
 287        done || exit $?
 288        latest_new=$(cache_get latest_new)
 289        if [ -z "$latest_new" ]; then
 290                die "No new revisions were found"
 291        fi
 292        
 293        if [ -n "$rejoin" ]; then
 294                debug "Merging split branch into HEAD..."
 295                latest_old=$(cache_get latest_old)
 296                git merge -s ours \
 297                        -m "$(merge_msg $dir $latest_old $latest_new)" \
 298                        $latest_new >&2
 299        fi
 300        echo $latest_new
 301        exit 0
 302}
 303
 304cmd_merge()
 305{
 306        die "merge command not implemented yet"
 307}
 308
 309"cmd_$command"