From fcea6a5e510285158ae9d8edcc5abddd91ab240d Mon Sep 17 00:00:00 2001 From: "Kyle J. McKay" Date: Mon, 13 Mar 2017 04:16:55 -0700 Subject: [PATCH] tg-contains.sh: new command to show containing TopGit branch(es) Because TopGit branches are regularly merged into one another (that's the whole point of the `tg update` command), the output of `git branch --contains` is usually less than helpful in finding the TopGit-controlled branch to which a fix should be applied for a bug introduced by a particular commit. Such a commit might be the result of running `git bisect` or some other tool that has identified the source of the problem. The `git branch --contains` output for such a commit will include the sought after TopGit branch, but it will also include all branches it's since been merged into with `tg update`. When the TopGit part of the DAG is complex this can cause many extra names to be output by `git branch --contains` that obscure the elusive correct result. Now there's `tg contains` to extract the correct needle from the `git branch --contains` haystack. In a TopGit repository with multiple patch series, the -v option will also provide the TopGit head of the patch series the result belongs to. It does, however, require some additional computing time to come up with that information which is why it's not enabled by default. Signed-off-by: Kyle J. McKay --- .gitignore | 3 + README | 62 ++++++++++++++++++++ tg-contains.sh | 176 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ tg.sh | 4 +- 4 files changed, 243 insertions(+), 2 deletions(-) create mode 100644 tg-contains.sh diff --git a/.gitignore b/.gitignore index 92fff38..1bb4632 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,9 @@ /tg-checkout /tg-checkout.txt /tg-checkout.html +/tg-contains +/tg-contains.txt +/tg-contains.html /tg-create /tg-create.txt /tg-create.html diff --git a/README b/README index 7367498..bd3be55 100644 --- a/README +++ b/README @@ -472,6 +472,7 @@ The ``tg`` tool has several subcommands: :`tg annihilate`_: Mark a TopGit-controlled branch as defunct :`tg base`_: Show base commit for one or more TopGit branches :`tg checkout`_: Shortcut for git checkout with name matching + :`tg contains`_: Which TopGit-controlled branch contains the commit :`tg create`_: Create a new TopGit-controlled branch :`tg delete`_: Delete a TopGit-controlled branch cleanly :`tg depend`_: Add a new dependency to a TopGit-controlled branch @@ -926,6 +927,67 @@ tg summary -i Use TopGit metadata from the index instead of the branch -w Use TopGit metadata from the working tree instead of the branch +tg contains +~~~~~~~~~~~ + Search all TopGit-controlled branches (and optionally their remotes) + to find which TopGit-controlled branch contains the specified commit. + + This is more than just basic branch containment as provided for by the + ``git branch --contains`` command. While the shown branch name(s) + will, indeed, be one (or more) of those output by the + ``git branch --contains`` command, the result(s) will exclude any + TopGit-controlled branches from the result(s) that have one (or more) + of their TopGit dependencies (either direct or indirect) appearing in + the ``git branch --contains`` output. + + Normally the result will be only the one, single TopGit-controlled + branch for which the specified committish appears in the ``tg log`` + output for that branch (unless the committish lies outside the + TopGit-controlled portion of the DAG and ``--no-strict`` was used). + + In other words, if a ``tg patch`` is generated for the found branch + (assuming one was found and a subsequent commit in the same branch + didn't then revert or otherwise back out the change), then that patch + will include the changes introduced by the specified committish + (unless, of course, that committish is outside the TopGit-controlled + portion of the DAG and ``--no-strict`` was given). + + This can be very helpful when, for example, a bug is discovered and + then after using ``git bisect`` (or some other tool) to find the + offending commit it's time to commit the fix. But because the + TopGit merging history can be quite complicated and maybe the one + doing the fix wasn't the bug's author (or the author's memory is just + going), it can sometimes be rather tedious to figure out which + TopGit branch the fix belongs in. The ``tg contains`` command can + quickly tell you the answer to that question. + + With the ``--remotes`` (or ``-r``) option a TopGit-controlled remote + branch name may be reported as the result but only if there is no + non-remote branch containing the committish (this can only happen + if at least one of the TopGit-controlled local branches are not yet + up-to-date with their remotes). + + With the ``--verbose`` option show which TopGit DAG head(s) (one or + more of the TopGit-controlled branch names output by + ``tg summary --heads``) have the result as a dependency (either direct + or indirect). Using this option will noticeably increase running time. + + With the default ``--strict`` option, results for which the base of the + TopGit-controlled branch contains the committish will be suppressed. + For example, if the committish was deep-down in the master branch + history somewhere far outside of the TopGit-controlled portion of + the DAG, with ``--no-strict``, whatever TopGit-controlled branch(es) + first picked up history containing that committish will be shown. + While this is a useful result it's usually not the desired result + which is why it's not the default. + + To summarize, even with ``--remotes``, remote results are only shown + if there are no non-remote results. Without ``--no-strict`` (because + ``--strict`` is the default) results outside the TopGit-controlled + portion of the DAG are never shown and even with ``--no-strict`` they + will only be shown if there are no ``--strict`` results. Finally, + the TopGit head info shown with ``--verbose`` only ever appears for + local (i.e. not a remote branch) results. tg checkout ~~~~~~~~~~~ diff --git a/tg-contains.sh b/tg-contains.sh new file mode 100644 index 0000000..4c8b0b6 --- /dev/null +++ b/tg-contains.sh @@ -0,0 +1,176 @@ +#!/bin/sh +# TopGit contains command +# (C) 2017 Kyle J. McKay +# All rights reserved +# GPLv2 + +USAGE="\ +Usage: ${tgname:-tg} [...] contains [-v] [-r] [--no-strict] [--] " + +usage() +{ + if [ "${1:-0}" != 0 ]; then + printf '%s\n' "$USAGE" >&2 + else + printf '%s\n' "$USAGE" + fi + exit ${1:-0} +} + +verbose= +remotes= +strict=1 + +while [ $# -gt 0 ]; do case "$1" in + -h|--help) + usage + ;; + -r|--remotes) + remotes=1 + ;; + --heads) + echo "Did you mean --verbose (-v) instead of --heads?" >&2 + usage 1 + ;; + -v|--verbose) + verbose=$(( ${verbose:-0} + 1 )) + ;; + -vv|-vvv|-vvvv|-vvvvv) + verbose=$(( ${verbose:-0} + ${#1} - 1 )) + ;; + --strict) + strict=1 + ;; + --no-strict) + strict= + ;; + --) + shift + break + ;; + -?*) + echo "Unknown option: $1" >&2 + usage 1 + ;; + *) + break + ;; +esac; shift; done +[ $# = 1 ] || usage 1 +[ "$1" != "@" ] || set -- HEAD + +set -e +findrev="$(git rev-parse --verify "$1"^0 --)" || exit 1 + +# true if $1 is contained by (or the same as) $2 +contained_by() +{ + [ "$(git rev-list --count --max-count=1 "$1" --not "$2" --)" = "0" ] +} + +# $1 => return correct $topbases value in here on success +# $2 => remote name +# $3 => remote branch name +# succeeds if both refs/remotes/$2/$3 and refs/remotes/$2/${$1#heads/}/$3 exist +v_is_remote_tgbranch() +{ + git rev-parse --quiet --verify "refs/remotes/$2/$3^0" -- >/dev/null || return 1 + if git rev-parse --quiet --verify "refs/remotes/$2/${topbases#heads/}/$3^0" -- >/dev/null; then + [ -z "$1" ] || eval "$1="'"$topbases"' + return 0 + fi + git rev-parse --quiet --verify "refs/remotes/$2/${oldbases#heads/}/$3^0" -- >/dev/null || return 1 + [ -z "$1" ] || eval "$1="'"$oldbases"' +} + +process_dep() +{ + if [ -n "$_dep_is_tgish" ] && [ -z "$_dep_missing$_dep_annihilated" ]; then + printf '%s\n' "$_dep ${_depchain##* }" + fi +} + +depslist= +make_deps_list() +{ + no_remotes=1 + base_remote= + depslist="$(get_temp depslist)" + $tg summary --topgit-heads | + while read -r onetghead; do + printf '%s %s\n' "$onetghead" "$onetghead" + recurse_deps process_dep "$onetghead" + done | LC_ALL=C sort -u >"$depslist" +} + +localcnt= +remotecnt= +localb="$(get_temp localb)" +localwide=0 +remoteb= +remotewide=0 +[ -z "$remotes" ] || remoteb="$(get_temp remoteb)" +while IFS= read -r branch && [ -n "$branch" ]; do + branch="${branch#??}" + if v_verify_topgit_branch "" "$branch" -f; then + if contained_by "$findrev" "refs/$topbases/$branch"; then + [ -z "$strict" ] || continue + depth="$(git rev-list --count --ancestry-path "refs/$topbases/$branch" --not "$findrev")" + depth=$(( ${depth:-0} + 1 )) + else + depth=0 + fi + localcnt=$(( ${localcnt:-0} + 1 )) + [ ${#branch} -le $localwide ] || localwide=${#branch} + printf '%s %s\n' "$depth" "$branch" >>"$localb" + remotecnt= + else + [ -n "$remotes" ] && [ -z "$localcnt" ] && [ "${branch#remotes/}" != "$branch" ] || continue + rbranch="${branch#remotes/}" + rremote="${rbranch%%/*}" + rbranch="${rbranch#*/}" + [ "remotes/$rremote/$rbranch" = "$branch" ] || continue + v_is_remote_tgbranch rtopbases "$rremote" "$rbranch" || continue + if contained_by "$findrev" "refs/remotes/$rremote/${rtopbases#heads/}/$rbranch"; then + [ -z "$strict" ] || continue + depth="$(git rev-list --count --ancestry-path "refs/remotes/$rremote/${rtopbases#heads/}/$rbranch" --not "$findrev")" + depth=$(( ${depth:-0} + 1 )) + else + depth=0 + fi + remotecnt=$(( ${remotecnt:-0} + 1 )) + [ ${#branch} -le $remotewide ] || remotewide=${#branch} + [ -n "$remoteb" ] || remoteb="$(get_temp remoteb)" + printf '%s %s\n' "$depth" "remotes/$rremote/$rbranch" >>"$remoteb" + fi +done <