| 1 | #!/bin/bash |
| 2 | |
| 3 | # "set -e" isn't active inside command substitutions unless we use POSIX mode |
| 4 | # (which I don't want to do right now) or inherit_errexit (which would introduce |
| 5 | # a dependency on bash >= 4.4). So implement our own version for now. |
| 6 | # ~ Matt 2016-10-28 |
| 7 | set -o errtrace |
| 8 | trap 'exit $?' ERR |
| 9 | |
| 10 | USAGE="add <subtree_path> <upstream_commit> |
| 11 | or: git subtree-lite update <subtree_path> <upstream_commit> |
| 12 | or: git subtree-lite diff [<diff_option>...] <subtree_path> |
| 13 | or: git subtree-lite import <subtree_path> [<upstream_commit>]" |
| 14 | |
| 15 | read -r -d '' LONG_USAGE <<'LONG_USAGE' || true |
| 16 | "git subtree-lite" imports the content of another repository (called "upstream") |
| 17 | as of a particular commit into a subtree of your repository. It remembers the |
| 18 | commit ID that was imported in an ".upstream" file in the subtree, so if you |
| 19 | modify the subtree and then import a different upstream commit, the changes will |
| 20 | be merged. You can use "git subtree-lite" to bundle libraries, as an |
| 21 | alternative to "git submodule" or "git subtree". |
| 22 | |
| 23 | Semantically, a subtree managed with this tool is equivalent to a submodule |
| 24 | pointer with a layer of project-specific modifications. Like "git subtree" and |
| 25 | unlike a git submodule, the main project's version of the the content is in the |
| 26 | main tree, so no special handling is needed to read or write it. But like a git |
| 27 | submodule and unlike "git subtree", the upstream commit pointer is just data |
| 28 | that can be merged and reverted, and this tool doesn't clutter the main project |
| 29 | history with extra merge commits. The upstream commit pointer doesn't enjoy any |
| 30 | of the special tool support of submodules, but most of it isn't relevant with |
| 31 | the content in the main tree (an exception might be |
| 32 | {fetch,push}.recurseSubmodules for developers who update or diff the subtree). |
| 33 | |
| 34 | Operations that access the upstream repository require that you have a local |
| 35 | copy of it and set the "subtree-lite.<subtree_path>.repo" configuration option |
| 36 | to its path. The path may be absolute or relative to the .git directory of the |
| 37 | current repository (the common directory if you use "git worktree"). This |
| 38 | mechanism is subject to change and may be made more sophisticated and automated |
| 39 | (like submodules) in the future. |
| 40 | |
| 41 | Further background: |
| 42 | http://marc.info/?l=git&m=147752326122139&w=2 |
| 43 | |
| 44 | git subtree-lite add <subtree_path> <upstream_commit> |
| 45 | Add a subtree from the given upstream commit. Follow the directions to |
| 46 | configure the path to the upstream repository. |
| 47 | |
| 48 | git subtree-lite update <subtree_path> <upstream_commit> |
| 49 | Update the subtree to be based on the given upstream commit. |
| 50 | |
| 51 | (To remove a subtree, just use "git rm -r".) |
| 52 | |
| 53 | git subtree-lite diff [<diff_option>...] <subtree_path> |
| 54 | Diff the subtree against the original upstream content. Diff options are |
| 55 | accepted, but paths to limit the diff currently are not supported. |
| 56 | |
| 57 | git subtree-lite import <subtree_path> [<upstream_commit>] |
| 58 | Low-level command: generate the "imported commit" corresponding to the given |
| 59 | upstream commit (with the content moved to the subtree and the .upstream |
| 60 | file added) and print its ID. Only the tree of this commit is meaningful. |
| 61 | LONG_USAGE |
| 62 | |
| 63 | OPTIONS_SPEC= |
| 64 | # Let's simplify matters for now and not allow running in a subdirectory. |
| 65 | . "$(git --exec-path)/git-sh-setup" |
| 66 | require_work_tree |
| 67 | |
| 68 | function cleanup { |
| 69 | rm -rf "$tmpdir" |
| 70 | } |
| 71 | |
| 72 | tmpdir="$(mktemp --tmpdir -d git-subtree-lite.XXXXXXXXXX)" |
| 73 | trap "cleanup" EXIT |
| 74 | |
| 75 | function ensure_init_tmp_repo { |
| 76 | if [ -z "$tmp_repo" ]; then |
| 77 | tmp_repo="$tmpdir/repo" |
| 78 | git init --quiet --bare "$tmp_repo" |
| 79 | (cd "$subtree_repo" && readlink --canonicalize "$(git rev-parse --git-path objects)") >"$tmp_repo/objects/info/alternates" |
| 80 | fi |
| 81 | } |
| 82 | |
| 83 | function setup_subtree { |
| 84 | # XXX Introduce a name like submodules have? Either abuse .gitmodules and |
| 85 | # call "git submodule--helper name", or reimplement the lookup? |
| 86 | local opt_name="subtree-lite.$subtree_path.repo" |
| 87 | local common_dir="$(git rev-parse --git-common-dir)" |
| 88 | subtree_repo="$(git config "$opt_name")" || die "Please get a local copy of the upstream repository and run: |
| 89 | |
| 90 | git config $opt_name <path_to_upstream_repository> |
| 91 | |
| 92 | before using this tool. The path may be absolute or relative to |
| 93 | $(readlink --canonicalize "$common_dir") ." |
| 94 | subtree_repo="$(cd "$common_dir" && readlink --canonicalize "$subtree_repo")" |
| 95 | |
| 96 | upstream_file="$subtree_path/.upstream" |
| 97 | if [ -f "$upstream_file" ]; then |
| 98 | cur_upstream_commit="$(< "$upstream_file")" |
| 99 | else |
| 100 | cur_upstream_commit="" |
| 101 | fi |
| 102 | } |
| 103 | |
| 104 | function reproducible_commit_tree { |
| 105 | GIT_AUTHOR_NAME='git-subtree-lite' \ |
| 106 | GIT_AUTHOR_EMAIL='git-subtree-lite@invalid' \ |
| 107 | GIT_AUTHOR_DATE='@0 +0000' \ |
| 108 | GIT_COMMITTER_NAME='git-subtree-lite' \ |
| 109 | GIT_COMMITTER_EMAIL='git-subtree-lite@invalid' \ |
| 110 | GIT_COMMITTER_DATE='@0 +0000' \ |
| 111 | git commit-tree "$@" |
| 112 | } |
| 113 | |
| 114 | function canonicalize_upstream_commit { |
| 115 | (cd "$subtree_repo" && git rev-parse --verify "$1^{commit}") |
| 116 | } |
| 117 | |
| 118 | # TODO: We should cache this, but even if we don't stop git from GC-ing the |
| 119 | # underlying commits, how to stop the mapping from growing indefinitely? |
| 120 | function import_commit { |
| 121 | commit="$1" |
| 122 | ensure_init_tmp_repo |
| 123 | ( |
| 124 | cd "$tmp_repo" |
| 125 | rm -f index |
| 126 | git read-tree -i --prefix="$subtree_path/" "$commit" |
| 127 | # Hm, I suppose this would be a great place to add a way to exclude files |
| 128 | # the superproject doesn't care about. But not now. |
| 129 | |
| 130 | # Create the .upstream file. If we do it here, then the right thing ends up |
| 131 | # happening during both add/update and diff without any more code. |
| 132 | uf_blob="$(echo "$commit" | git hash-object -t blob -w --stdin)" |
| 133 | git update-index --add --cacheinfo "100644,$uf_blob,$subtree_path/.upstream" |
| 134 | t="$(git write-tree)" |
| 135 | # Reuse the same object for the same upstream commit until the repository is |
| 136 | # GC-ed. |
| 137 | c="$(reproducible_commit_tree -m "git-subtree-lite temporary commit" "$t")" |
| 138 | git update-ref HEAD "$c" |
| 139 | ) |
| 140 | # XXX: The incremental fetch protocol is only based on detection of common |
| 141 | # commits, so unless we already have the exact same imported commit, this |
| 142 | # fetch will send the entire tree. If we cache previous imported commits in |
| 143 | # the main repository, then we can add it as an alternate of the temporary |
| 144 | # repository (!) and the previous imported commits will be detected as common. |
| 145 | git fetch --quiet "$tmp_repo" HEAD 2>&1 | { grep -v --line-regexp 'warning: no common commits' >&2 || true; } |
| 146 | git rev-parse FETCH_HEAD # to stdout |
| 147 | } |
| 148 | |
| 149 | function cmd_add { |
| 150 | [ $# == 2 ] || usage |
| 151 | subtree_path="$1" |
| 152 | new_upstream_commit_expr="$2" |
| 153 | |
| 154 | setup_subtree |
| 155 | ! [ -e "$subtree_path" ] || die "Error: $subtree_path already exists on the filesystem." |
| 156 | ! [ -n "$(git ls-files "$subtree_path")" ] || die "Error: $subtree_path already exists in the git index." |
| 157 | |
| 158 | new_upstream_commit="$(canonicalize_upstream_commit "$new_upstream_commit_expr")" |
| 159 | new_imported_commit="$(import_commit "$new_upstream_commit")" |
| 160 | git read-tree --prefix= -u "$new_imported_commit" |
| 161 | } |
| 162 | |
| 163 | function cmd_update { |
| 164 | [ $# == 2 ] || usage |
| 165 | subtree_path="$1" |
| 166 | new_upstream_commit_expr="$2" |
| 167 | |
| 168 | setup_subtree |
| 169 | [ -n "$cur_upstream_commit" ] || die "Error: $subtree_path is not a subtree set up with this tool." |
| 170 | |
| 171 | new_upstream_commit="$(canonicalize_upstream_commit "$new_upstream_commit_expr")" |
| 172 | new_imported_commit="$(import_commit "$new_upstream_commit")" |
| 173 | old_imported_commit="$(import_commit "$cur_upstream_commit")" |
| 174 | tree2="$(git write-tree)" |
| 175 | # Note: the .upstream file is already in the imported commits. |
| 176 | git read-tree -mu "$old_imported_commit" "$tree2" "$new_imported_commit" |
| 177 | git merge-index -o git-merge-one-file -a |
| 178 | } |
| 179 | |
| 180 | function cmd_import { |
| 181 | [ $# -ge 1 ] && [ $# -le 2 ] || usage |
| 182 | subtree_path="$1" |
| 183 | setup_subtree |
| 184 | if [ -n "$2" ]; then |
| 185 | new_upstream_commit="$(canonicalize_upstream_commit "$2")" |
| 186 | elif [ -n "$cur_upstream_commit" ]; then |
| 187 | new_upstream_commit="$cur_upstream_commit" |
| 188 | else |
| 189 | die "Error: $subtree_path is not added yet and no upstream commit was given." |
| 190 | fi |
| 191 | import_commit "$new_upstream_commit" |
| 192 | } |
| 193 | |
| 194 | function cmd_diff { |
| 195 | [ $# -ge 1 ] || usage |
| 196 | subtree_path="${@: -1:1}" |
| 197 | setup_subtree |
| 198 | # This could be expensive, but it's the only way to honor uncommitted changes |
| 199 | # (and --cached). Even if we added the subtree repo to |
| 200 | # GIT_ALTERNATE_OBJECT_DIRECTORIES, git has no way to diff a subtree of the |
| 201 | # worktree against the root of a given commit. |
| 202 | cur_imported_commit="$(import_commit "$cur_upstream_commit")" |
| 203 | git diff "${@:1:$#-1}" "$cur_imported_commit" -- "$subtree_path" |
| 204 | } |
| 205 | |
| 206 | [ $# -ge 1 ] || usage |
| 207 | cmd="$1" |
| 208 | shift |
| 209 | case "$cmd" in |
| 210 | (add) |
| 211 | cmd_add "$@";; |
| 212 | (update) |
| 213 | cmd_update "$@";; |
| 214 | (import) |
| 215 | cmd_import "$@";; |
| 216 | (diff) |
| 217 | cmd_diff "$@";; |
| 218 | (*) die "Unknown command $cmd.";; |
| 219 | esac |
| 220 | |