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