X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ian/git?p=topgit.git;a=blobdiff_plain;f=tg.sh;h=87d34247b680f9d02d721745d5fe05d4810902f8;hp=5bb2d0cd123f09b1b038b04ed3b9a17bc4495a5d;hb=6a1c58e7652027c584b14cd0037ad5eee8851e5c;hpb=4fe0a8e009776b8c66357f08ef8171bdc2c0f468 diff --git a/tg.sh b/tg.sh index 5bb2d0c..87d3424 100644 --- a/tg.sh +++ b/tg.sh @@ -3,6 +3,7 @@ # (c) Petr Baudis 2008 # GPLv2 +TG_VERSION=0.8 ## Auxiliary functions @@ -13,10 +14,97 @@ info() die() { - info "fatal: $*" + info "fatal: $*" >&2 exit 1 } +# 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() +{ + path="$1" + case "${2-}" in + -w) + cat "$root_dir/${path#*:}" + ;; + -i) + # ':file' means cat from index + git cat-file blob ":${path#*:}" + ;; + '') + git cat-file blob "$path" + ;; + *) + 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() { @@ -32,6 +120,8 @@ 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" @@ -104,6 +194,13 @@ branch_annihilated() 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, @@ -114,16 +211,17 @@ branch_annihilated() # 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 @@ -153,7 +251,6 @@ recurse_deps() eval "$_cmd" done <"$_depsfile" missing_deps="${missing_deps# }" - rm "$_depsfile" return $_ret } @@ -201,10 +298,42 @@ needs_update() 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] @@ -243,10 +372,11 @@ do_help() sep="|" done - echo "TopGit v0.5 - A different patch queue manager" + 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" @@ -258,12 +388,61 @@ do_help() fi } +## Pager stuff + +# isatty FD +isatty() +{ + test -t $1 +} + +# setup_pager +# Spawn pager process and redirect the rest of our output to it +setup_pager() +{ + isatty 1 || return 0 + + # TG_PAGER = GIT_PAGER | PAGER | less + # NOTE: GIT_PAGER='' is significant + TG_PAGER=${GIT_PAGER-${PAGER-less}} + + [ -z "$TG_PAGER" -o "$TG_PAGER" = "cat" ] && return 0 + + + # now spawn pager + export LESS="${LESS:-FRSX}" # as in pager.c:pager_preexec() + + # 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) + + # 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) + # 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@'" +ensure_git_repo_or_die + ## Initial setup set -e @@ -276,12 +455,16 @@ tg="tg" # 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 @@ -314,4 +497,6 @@ help|--help|-h) . "@cmddir@"/tg-$cmd;; esac +fi + # vim:noet