chiark / gitweb /
f8c8de4ba859d76d254580ef88041b3bdce05f19
[topgit.git] / tg.sh
1 #!/bin/sh
2 # TopGit - A different patch queue manager
3 # (c) Petr Baudis <pasky@suse.cz>  2008
4 # GPLv2
5
6
7 ## Auxiliary functions
8
9 info()
10 {
11         echo "${TG_RECURSIVE}tg: $*"
12 }
13
14 die()
15 {
16         info "fatal: $*"
17         exit 1
18 }
19
20 # Make sure we are in the worktree, not under .git; die otherwise
21 ensure_git_repo_or_die()
22 {
23         local is_inside_repo is_inside_git_dir
24         is_inside_repo=1
25         is_inside_git_dir=$(git rev-parse --is-inside-git-dir 2>/dev/null) ||
26                 is_inside_repo=0
27
28         case "$is_inside_repo/$is_inside_git_dir" in
29         0*) die "Cannot run outside of a Git repository.";;
30         1/true) die "Cannot run from inside \`.git\` hierarchy, please switch to work-tree.";;
31         esac
32 }
33
34 # setup_hook NAME
35 setup_hook()
36 {
37         hook_call="\"\$($tg --hooks-path)\"/$1 \"\$@\""
38         if [ -f "$git_dir/hooks/$1" ] &&
39            fgrep -q "$hook_call" "$git_dir/hooks/$1"; then
40                 # Another job well done!
41                 return
42         fi
43         # Prepare incantation
44         if [ -x "$git_dir/hooks/$1" ]; then
45                 hook_call="$hook_call"' || exit $?'
46         else
47                 hook_call="exec $hook_call"
48         fi
49         # Insert call into the hook
50         {
51                 echo "#!/bin/sh"
52                 echo "$hook_call"
53                 [ ! -s "$git_dir/hooks/$1" ] || cat "$git_dir/hooks/$1"
54         } >"$git_dir/hooks/$1+"
55         chmod a+x "$git_dir/hooks/$1+"
56         mv "$git_dir/hooks/$1+" "$git_dir/hooks/$1"
57 }
58
59 # setup_ours (no arguments)
60 setup_ours()
61 {
62         if [ ! -s "$git_dir/info/attributes" ] || ! grep -q topmsg "$git_dir/info/attributes"; then
63                 {
64                         echo ".topmsg   merge=ours"
65                         echo ".topdeps  merge=ours"
66                 } >>"$git_dir/info/attributes"
67         fi
68         if ! git config merge.ours.driver >/dev/null; then
69                 git config merge.ours.name '"always keep ours" merge driver'
70                 git config merge.ours.driver 'touch %A'
71         fi
72 }
73
74 # measure_branch NAME [BASE]
75 measure_branch()
76 {
77         _bname="$1"; _base="$2"
78         [ -n "$_base" ] || _base="refs/top-bases/$_bname"
79         # The caller should've verified $name is valid
80         _commits="$(git rev-list "$_bname" ^"$_base" -- | wc -l)"
81         _nmcommits="$(git rev-list --no-merges "$_bname" ^"$_base" -- | wc -l)"
82         if [ $_commits -gt 1 ]; then
83                 _suffix="commits"
84         else
85                 _suffix="commit"
86         fi
87         echo "$_commits/$_nmcommits $_suffix"
88 }
89
90 # branch_contains B1 B2
91 # Whether B1 is a superset of B2.
92 branch_contains()
93 {
94         [ -z "$(git rev-list ^"$1" "$2" --)" ]
95 }
96
97 # ref_exists REF
98 # Whether REF is a valid ref name
99 ref_exists()
100 {
101         git rev-parse --verify "$@" >/dev/null 2>&1
102 }
103
104 # has_remote BRANCH
105 # Whether BRANCH has a remote equivalent (accepts top-bases/ too)
106 has_remote()
107 {
108         [ -n "$base_remote" ] && ref_exists "remotes/$base_remote/$1"
109 }
110
111 # recurse_deps CMD NAME [BRANCHPATH...]
112 # Recursively eval CMD on all dependencies of NAME.
113 # CMD can refer to $_name for queried branch name,
114 # $_dep for dependency name,
115 # $_depchain for space-seperated branch backtrace,
116 # and the $_dep_is_tgish boolean.
117 # It can modify $_ret to affect the return value
118 # of the whole function.
119 # If recurse_deps() hits missing dependencies, it will append
120 # them to space-separated $missing_deps list and skip them.
121 recurse_deps()
122 {
123         _cmd="$1"; shift
124         _name="$1"; # no shift
125         _depchain="$*"
126
127         _depsfile="$(mktemp -t tg-depsfile.XXXXXX)"
128         # Check also our base against remote base. Checking our head
129         # against remote head has to be done in the helper.
130         if has_remote "top-bases/$_name"; then
131                 echo "refs/remotes/$base_remote/top-bases/$_name" >>"$_depsfile"
132         fi
133         git cat-file blob "$_name:.topdeps" >>"$_depsfile"
134
135         _ret=0
136         while read _dep; do
137                 if ! ref_exists "$_dep" ; then
138                         # All hope is lost
139                         missing_deps="$missing_deps $_dep"
140                         continue
141                 fi
142
143                 _dep_is_tgish=1
144                 ref_exists "refs/top-bases/$_dep"  ||
145                         _dep_is_tgish=
146
147                 # Shoo shoo, keep our environment alone!
148                 [ -z "$_dep_is_tgish" ] ||
149                         (recurse_deps "$_cmd" "$_dep" "$@") ||
150                         _ret=$?
151
152                 eval "$_cmd"
153         done <"$_depsfile"
154         missing_deps="${missing_deps# }"
155         rm "$_depsfile"
156         return $_ret
157 }
158
159 # branch_needs_update
160 # This is a helper function for determining whether given branch
161 # is up-to-date wrt. its dependencies. It expects input as if it
162 # is called as a recurse_deps() helper.
163 # In case the branch does need update, it will echo it together
164 # with the branch backtrace on the output (see needs_update()
165 # description for details) and set $_ret to non-zero.
166 branch_needs_update()
167 {
168         _dep_base_update=
169         if [ -n "$_dep_is_tgish" ]; then
170                 if has_remote "$_dep"; then
171                         branch_contains "$_dep" "refs/remotes/$base_remote/$_dep" || _dep_base_update=%
172                 fi
173                 # This can possibly override the remote check result;
174                 # we want to sync with our base first
175                 branch_contains "$_dep" "refs/top-bases/$_dep" || _dep_base_update=:
176         fi
177
178         if [ -n "$_dep_base_update" ]; then
179                 # _dep needs to be synced with its base/remote
180                 echo "$_dep_base_update $_dep $_depchain"
181                 _ret=1
182         elif [ -n "$_name" ] && ! branch_contains "refs/top-bases/$_name" "$_dep"; then
183                 # Some new commits in _dep
184                 echo "$_dep $_depchain"
185                 _ret=1
186         fi
187 }
188
189 # needs_update NAME
190 # This function is recursive; it outputs reverse path from NAME
191 # to the branch (e.g. B_DIRTY B1 B2 NAME), one path per line,
192 # inner paths first. Innermost name can be ':' if the head is
193 # not in sync with the base or '%' if the head is not in sync
194 # with the remote (in this order of priority).
195 # It will also return non-zero status if NAME needs update.
196 # If needs_update() hits missing dependencies, it will append
197 # them to space-separated $missing_deps list and skip them.
198 needs_update()
199 {
200         recurse_deps branch_needs_update "$@"
201 }
202
203 # branch_empty NAME
204 branch_empty()
205 {
206         [ -z "$(git diff-tree "refs/top-bases/$1" "$1" | fgrep -v "     .top")" ]
207 }
208
209 # switch_to_base NAME [SEED]
210 switch_to_base()
211 {
212         _base="refs/top-bases/$1"; _seed="$2"
213         # We have to do all the hard work ourselves :/
214         # This is like git checkout -b "$_base" "$_seed"
215         # (or just git checkout "$_base"),
216         # but does not create a detached HEAD.
217         git read-tree -u -m HEAD "${_seed:-$_base}"
218         [ -z "$_seed" ] || git update-ref "$_base" "$_seed"
219         git symbolic-ref HEAD "$_base"
220 }
221
222 # Show the help messages.
223 do_help()
224 {
225         if [ -z "$1" ] ; then
226                 # This is currently invoked in all kinds of circumstances,
227                 # including when the user made a usage error. Should we end up
228                 # providing more than a short help message, then we should
229                 # differentiate.
230                 # Petr's comment: http://marc.info/?l=git&m=122718711327376&w=2
231
232                 ## Build available commands list for help output
233
234                 cmds=
235                 sep=
236                 for cmd in "@cmddir@"/tg-*; do
237                         ! [ -r "$cmd" ] && continue
238                         # strip directory part and "tg-" prefix
239                         cmd="$(basename "$cmd")"
240                         cmd="${cmd#tg-}"
241                         cmds="$cmds$sep$cmd"
242                         sep="|"
243                 done
244
245                 echo "TopGit v0.5 - A different patch queue manager"
246                 echo "Usage: tg [-r REMOTE] ($cmds|help) ..."
247         elif [ -r "@cmddir@"/tg-$1 ] ; then
248                 @cmddir@/tg-$1 -h || :
249                 echo
250                 if [ -r "@sharedir@/tg-$1.txt" ] ; then
251                         cat "@sharedir@/tg-$1.txt"
252                 fi
253         else
254                 echo "`basename $0`: no help for $1" 1>&2
255                 do_help
256                 exit 1
257         fi
258 }
259
260
261 ## Startup
262
263 [ -d "@cmddir@" ] ||
264         die "No command directory: '@cmddir@'"
265
266 ensure_git_repo_or_die
267
268 ## Initial setup
269
270 set -e
271 git_dir="$(git rev-parse --git-dir)"
272 root_dir="$(git rev-parse --show-cdup)"; root_dir="${root_dir:-.}"
273 # Make sure root_dir doesn't end with a trailing slash.
274 root_dir="${root_dir%/}"
275 base_remote="$(git config topgit.remote 2>/dev/null)" || :
276 tg="tg"
277 # make sure merging the .top* files will always behave sanely
278 setup_ours
279 setup_hook "pre-commit"
280
281 ## Dispatch
282
283 # We were sourced from another script for our utility functions;
284 # this is set by hooks.
285 [ -z "$tg__include" ] || return 0
286
287 if [ "$1" = "-r" ]; then
288         shift
289         if [ -z "$1" ]; then
290                 echo "Option -r requires an argument." >&2
291                 do_help
292                 exit 1
293         fi
294         base_remote="$1"; shift
295         tg="$tg -r $base_remote"
296 fi
297
298 cmd="$1"
299 [ -n "$cmd" ] || { do_help; exit 1; }
300 shift
301
302 case "$cmd" in
303 help|--help|-h)
304         do_help "$1"
305         exit 0;;
306 --hooks-path)
307         # Internal command
308         echo "@hooksdir@";;
309 *)
310         [ -r "@cmddir@"/tg-$cmd ] || {
311                 echo "Unknown subcommand: $cmd" >&2
312                 do_help
313                 exit 1
314         }
315         . "@cmddir@"/tg-$cmd;;
316 esac
317
318 # vim:noet