# (c) Petr Baudis <pasky@suse.cz> 2008
# GPLv2
-TG_VERSION=0.6
+TG_VERSION=0.8
## Auxiliary functions
die()
{
- info "fatal: $*"
+ info "fatal: $*" >&2
exit 1
}
-# cat_file "topic:file"
-# Like `git cat-file blob $1`, but topics '(i)' and '(w)' means index and worktree
+# Make sure we are in the worktree, not under .git; die otherwise
+ensure_git_repo_or_die()
+{
+ local is_inside_repo is_inside_git_dir
+ is_inside_repo=1
+ is_inside_git_dir=$(git rev-parse --is-inside-git-dir 2>/dev/null) ||
+ is_inside_repo=0
+
+ case "$is_inside_repo/$is_inside_git_dir" in
+ 0*) die "Cannot run outside of a Git repository.";;
+ 1/true) die "Cannot run from inside \`.git\` hierarchy, please switch to work-tree.";;
+ esac
+}
+
+# cat_file TOPIC:PATH [FROM]
+# cat the file PATH from branch TOPIC when FROM is empty.
+# FROM can be -i or -w, than the file will be from the index or worktree,
+# respectively. The caller should than ensure that HEAD is TOPIC, to make sense.
cat_file()
{
- arg="$1"
- case "$arg" in
- '(w):'*)
- arg=$(echo "$arg" | tail --bytes=+5)
- cat "$arg"
- return
+ path="$1"
+ case "${2-}" in
+ -w)
+ cat "$root_dir/${path#*:}"
;;
- '(i):'*)
+ -i)
# ':file' means cat from index
- arg=$(echo "$arg" | tail --bytes=+5)
- git cat-file blob ":$arg"
+ git cat-file blob ":${path#*:}"
+ ;;
+ '')
+ git cat-file blob "$path"
;;
*)
- git cat-file blob "$arg"
+ die "Wrong argument to cat_file: '$2'"
+ ;;
esac
}
+# get tree for the committed topic
+get_tree_()
+{
+ echo "$1"
+}
+
+# get tree for the base
+get_tree_b()
+{
+ echo "refs/top-bases/$1"
+}
+
+# get tree for the index
+get_tree_i()
+{
+ git write-tree
+}
+
+# get tree for the worktree
+get_tree_w()
+{
+ i_tree=$(git write-tree)
+ (
+ # the file for --index-output needs to sit next to the
+ # current index file
+ : ${GIT_INDEX_FILE:="$git_dir/index"}
+ TMP_INDEX="$(mktemp "${GIT_INDEX_FILE}-tg.XXXXXX")"
+ git read-tree -m $i_tree --index-output="$TMP_INDEX" &&
+ GIT_INDEX_FILE="$TMP_INDEX" &&
+ export GIT_INDEX_FILE &&
+ git diff --name-only -z HEAD |
+ git update-index -z --add --remove --stdin &&
+ git write-tree &&
+ rm -f "$TMP_INDEX"
+ )
+}
+
+# pretty_tree NAME [-b | -i | -w]
+# Output tree ID of a cleaned-up tree without tg's artifacts.
+# NAME will be ignored for -i and -w, but needs to be present
+pretty_tree()
+{
+ name=$1
+ source=${2#?}
+ git ls-tree --full-tree "$(get_tree_$source "$name")" |
+ awk -F ' ' '$2 !~ /^.top/' |
+ git mktree
+}
+
# setup_hook NAME
setup_hook()
{
else
hook_call="exec $hook_call"
fi
+ # Don't call hook if tg is not installed
+ hook_call="if which \"$tg\" > /dev/null; then $hook_call; fi"
# Insert call into the hook
{
echo "#!/bin/sh"
test "$(git rev-parse "$mb^{tree}")" = "$(git rev-parse "$_name^{tree}")";
}
+# is_sha1 REF
+# Whether REF is a SHA1 (compared to a symbolic name).
+is_sha1()
+{
+ [ "$(git rev-parse "$1")" = "$1" ]
+}
+
# recurse_deps CMD NAME [BRANCHPATH...]
# Recursively eval CMD on all dependencies of NAME.
# CMD can refer to $_name for queried branch name,
# of the whole function.
# If recurse_deps() hits missing dependencies, it will append
# them to space-separated $missing_deps list and skip them.
+# remote dependencies are processed if no_remotes is unset.
recurse_deps()
{
_cmd="$1"; shift
_name="$1"; # no shift
_depchain="$*"
- _depsfile="$(mktemp -t tg-depsfile.XXXXXX)"
- # Check also our base against remote base. Checking our head
- # against remote head has to be done in the helper.
- if has_remote "top-bases/$_name"; then
+ _depsfile="$(get_temp tg-depsfile)"
+ # If no_remotes is unset check also our base against remote base.
+ # Checking our head against remote head has to be done in the helper.
+ if test -z "$no_remotes" && has_remote "top-bases/$_name"; then
echo "refs/remotes/$base_remote/top-bases/$_name" >>"$_depsfile"
fi
eval "$_cmd"
done <"$_depsfile"
missing_deps="${missing_deps# }"
- rm "$_depsfile"
return $_ret
}
recurse_deps branch_needs_update "$@"
}
-# branch_empty NAME
+# branch_empty NAME [-i | -w]
branch_empty()
{
- [ -z "$(git diff-tree "refs/top-bases/$1" "$1" | fgrep -v " .top")" ]
+ [ "$(pretty_tree "$1" -b)" = "$(pretty_tree "$1" ${2-})" ]
+}
+
+# list_deps [-i | -w]
+# -i/-w apply only to HEAD
+list_deps()
+{
+ local head
+ local head_from
+ local from
+ head_from=${1-}
+ head="$(git symbolic-ref -q HEAD)" ||
+ head="..detached.."
+
+ git for-each-ref refs/top-bases |
+ while read rev type ref; do
+ name="${ref#refs/top-bases/}"
+ if branch_annihilated "$name"; then
+ continue;
+ fi
+
+ from=$head_from
+ [ "refs/heads/$name" = "$head" ] ||
+ from=
+ cat_file "$name:.topdeps" $from | while read dep; do
+ dep_is_tgish=true
+ ref_exists "refs/top-bases/$dep" ||
+ dep_is_tgish=false
+ if ! "$dep_is_tgish" || ! branch_annihilated $dep; then
+ echo "$name $dep"
+ fi
+ done
+ done
}
# switch_to_base NAME [SEED]
echo "TopGit v$TG_VERSION - A different patch queue manager"
echo "Usage: tg [-r REMOTE] ($cmds|help) ..."
elif [ -r "@cmddir@"/tg-$1 ] ; then
- @cmddir@/tg-$1 -h || :
+ setup_pager
+ @cmddir@/tg-$1 -h 2>&1 || :
echo
- if [ -r "@sharedir@/tg-$1.txt" ] ; then
- cat "@sharedir@/tg-$1.txt"
+ if [ -r "@docdir@/tg-$1.txt" ] ; then
+ cat "@docdir@/tg-$1.txt"
fi
else
echo "`basename $0`: no help for $1" 1>&2
fi
}
+# Check whether we are supposed to output the help message
+should_do_help()
+{
+ # we are being sourced for utility functions, never run help
+ [ -z "$tg__include" ] || return 1
+
+ local prev
+ while [ -n "$1" ]; do
+ case "$1" in
+ help|--help|-h)
+ shift
+ echo "${1:-$prev}"
+ return 0
+ esac
+ prev="$1"
+ shift
+ done
+
+ # run help when there was no previous topic, meaning that there where
+ # no arguments at all
+ test -z "$prev"
+}
+
## Pager stuff
# isatty FD
# now spawn pager
- export LESS=${LESS:-FRSX} # as in pager.c:pager_preexec()
+ export LESS="${LESS:-FRSX}" # as in pager.c:pager_preexec()
- _pager_fifo_dir="$(mktemp -t -d tg-pager-fifo.XXXXXX)"
- _pager_fifo="$_pager_fifo_dir/0"
- mkfifo -m 600 "$_pager_fifo"
+ # setup_pager should be called only once per command
+ pager_fifo="$tg_tmp_dir/pager"
+ mkfifo -m 600 "$pager_fifo"
- "$TG_PAGER" < "$_pager_fifo" &
- exec > "$_pager_fifo" # dup2(pager_fifo.in, 1)
+ "$TG_PAGER" < "$pager_fifo" &
+ exec > "$pager_fifo" # dup2(pager_fifo.in, 1)
# this is needed so e.g. `git diff` will still colorize it's output if
# requested in ~/.gitconfig with color.diff=auto
export GIT_PAGER_IN_USE=1
# atexit(close(1); wait pager)
- trap "exec >&-; rm \"$_pager_fifo\"; rmdir \"$_pager_fifo_dir\"; wait" EXIT
+ # deliberately overwrites the global EXIT trap
+ trap "exec >&-; rm -rf \"$tg_tmp_dir\"; wait" EXIT
+}
+
+# get_temp NAME [-d]
+# creates a new temporary file (or directory with -d) in the global
+# temporary directory $tg_tmp_dir with pattern prefix NAME
+get_temp()
+{
+ mktemp ${2-} "$tg_tmp_dir/$1.XXXXXX"
}
## Startup
[ -d "@cmddir@" ] ||
die "No command directory: '@cmddir@'"
+# check if we should run help and get the topic while we're at it
+help_topic="$(should_do_help "$@")" && { do_help "$help_topic"; exit 0; }
+
+ensure_git_repo_or_die
+
## Initial setup
set -e
# make sure merging the .top* files will always behave sanely
setup_ours
setup_hook "pre-commit"
+# create global temporary directories, inside GIT_DIR
+tg_tmp_dir="$(mktemp -d "$git_dir/tg-tmp.XXXXXX")"
+trap "rm -rf \"$tg_tmp_dir\"" EXIT
## Dispatch
# We were sourced from another script for our utility functions;
-# this is set by hooks.
-[ -z "$tg__include" ] || return 0
+# this is set by hooks. Skip the rest of the file. A simple return doesn't
+# work as expected in every shell. See http://bugs.debian.org/516188
+if [ -z "$tg__include" ]; then
if [ "$1" = "-r" ]; then
shift
shift
case "$cmd" in
-help|--help|-h)
- do_help "$1"
- exit 0;;
--hooks-path)
# Internal command
echo "@hooksdir@";;
. "@cmddir@"/tg-$cmd;;
esac
+fi
+
# vim:noet