Initial commit of "git subtree-lite".
[utils/git-subtree-lite.git] / git-subtree-lite
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