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