From: Emily Shaffer <emilyshaffer@google.com>
To: Matthew DeVore <matvore@google.com>
Cc: git@vger.kernel.org, Matthew DeVore <matvore@gmail.com>,
matvore@comcast.net, jonathantanmy@google.com,
jrnieder@gmail.com, steadmon@google.com
Subject: Re: [RFC] xl command for visualizing recent history
Date: Wed, 30 Oct 2019 17:39:29 -0700 [thread overview]
Message-ID: <20191031003929.GA22855@google.com> (raw)
In-Reply-To: <20191029003023.122196-1-matvore@google.com>
On Mon, Oct 28, 2019 at 05:30:23PM -0700, Matthew DeVore wrote:
> From: Matthew DeVore <matvore@gmail.com>
Hi Matthew,
Good to hear from you. One comment - the subject of your mail is "[RFC]"
but I think folks are used to receiving mails with RFC patches if the
subject line is formatted like it comes out of 'git format-patch' - that
is, [RFC PATCH].
>
> "git xl" shows a graph of recent history, including all existing
> branches (unless flagged with a config option) and their upstream
> counterparts. It is named such because it is easy to type and the
> letter "x" looks like a small graph.
For me, that's not a very compelling reason to name something, and the
only command with such a cryptic name in Git that I can think of is 'git
am'. (mv, gc, rm, and p4 are somewhat self explanatory, and everything
else besides 'gitk' is named with a full word.)
>
> Like "git branch" it supports filtering the branches shown via
> positional arguments.
>
> Besides just showing the graph, it also associates refs with all visible
> commits with names in the form of "h/#" where # is an incrementing
> index. After showing the graph, these refs can be used to ergonomically
> invoke some follow-up command like rebase or diff.
It looks like there's a decent amount of this commit message which
really ought to be a note to the reviewers instead. Everything above the
'---' goes into the commit message; everything below it will get
scrubbed when the patch is applied, so you can give more casual notes
there - for example this paragraph, as well as "Omissions I might/will
fix".
> The test cases show non-trivial output which can be used to get an idea
> for what the command is good for, though it doesn't capture the
> coloring.
>
> The primary goals of this command are:
>
> a) deduce what the user wants to see based on what they haven't pushed
> upstream yet
> b) show the active branches spatially rather than as a linear list (as
> in "git branch")
> c) allow the user to easily refer to commits that appeared in the
> output
>
> I considered making the h/# tags stable across invocations such that a
> particular hash will only be tagged with a different number if ~100
> other hashes are tagged since the hash was last tagged. I didn't
> actually implement it this way, instead opting for always re-numbering
> the hashes on each invocation. This means the hash number is
> predictable based on the position the hash appears in the output, which
> is probably better that encouraging users to memorize hash numbers (or
> use them in scripts!).
If you're worried about folks using something like this in a script (and
I would be, given that it's dynamically assigning nicknames to hashes)
then you probably ought to mark it as a porcelain command in
command-list.txt.
>
> Omissions I might/will fix depending on feedback:
>
> a) rather than show HEAD in the graph, show <checked_out_branch> when
> possible (i.e. "[<master>]" rather than "[HEAD master]").
>
> b) don't parse output from `git log` but instead do everything
> in-process.
>
> c) documentation
Sorry not to review the rest of the diff today; I'll try to get to it
sometime soon.
- Emily
> ---
> Makefile | 1 +
> builtin.h | 1 +
> git.c | 1 +
> t/t4400-xl.sh | 270 ++++++++++++++++++++++++++++
> xl.c | 485 ++++++++++++++++++++++++++++++++++++++++++++++++++
> 5 files changed, 758 insertions(+)
> create mode 100755 t/t4400-xl.sh
> create mode 100644 xl.c
>
> diff --git a/Makefile b/Makefile
> index 03b800da0c..491661f848 100644
> --- a/Makefile
> +++ b/Makefile
> @@ -1022,20 +1022,21 @@ LIB_OBJS += varint.o
> LIB_OBJS += version.o
> LIB_OBJS += versioncmp.o
> LIB_OBJS += walker.o
> LIB_OBJS += wildmatch.o
> LIB_OBJS += worktree.o
> LIB_OBJS += wrapper.o
> LIB_OBJS += write-or-die.o
> LIB_OBJS += ws.o
> LIB_OBJS += wt-status.o
> LIB_OBJS += xdiff-interface.o
> +LIB_OBJS += xl.o
> LIB_OBJS += zlib.o
>
> BUILTIN_OBJS += builtin/add.o
> BUILTIN_OBJS += builtin/am.o
> BUILTIN_OBJS += builtin/annotate.o
> BUILTIN_OBJS += builtin/apply.o
> BUILTIN_OBJS += builtin/archive.o
> BUILTIN_OBJS += builtin/bisect--helper.o
> BUILTIN_OBJS += builtin/blame.o
> BUILTIN_OBJS += builtin/branch.o
> diff --git a/builtin.h b/builtin.h
> index 5cf5df69f7..568d09cf7f 100644
> --- a/builtin.h
> +++ b/builtin.h
> @@ -241,16 +241,17 @@ int cmd_update_server_info(int argc, const char **argv, const char *prefix);
> int cmd_upload_archive(int argc, const char **argv, const char *prefix);
> int cmd_upload_archive_writer(int argc, const char **argv, const char *prefix);
> int cmd_upload_pack(int argc, const char **argv, const char *prefix);
> int cmd_var(int argc, const char **argv, const char *prefix);
> int cmd_verify_commit(int argc, const char **argv, const char *prefix);
> int cmd_verify_tag(int argc, const char **argv, const char *prefix);
> int cmd_version(int argc, const char **argv, const char *prefix);
> int cmd_whatchanged(int argc, const char **argv, const char *prefix);
> int cmd_worktree(int argc, const char **argv, const char *prefix);
> int cmd_write_tree(int argc, const char **argv, const char *prefix);
> +int cmd_xl(int argc, const char **argv, const char *prefix);
> int cmd_verify_pack(int argc, const char **argv, const char *prefix);
> int cmd_show_ref(int argc, const char **argv, const char *prefix);
> int cmd_pack_refs(int argc, const char **argv, const char *prefix);
> int cmd_replace(int argc, const char **argv, const char *prefix);
>
> #endif
> diff --git a/git.c b/git.c
> index ce6ab0ece2..4a1da83a7e 100644
> --- a/git.c
> +++ b/git.c
> @@ -594,20 +594,21 @@ static struct cmd_struct commands[] = {
> { "upload-archive--writer", cmd_upload_archive_writer, NO_PARSEOPT },
> { "upload-pack", cmd_upload_pack },
> { "var", cmd_var, RUN_SETUP_GENTLY | NO_PARSEOPT },
> { "verify-commit", cmd_verify_commit, RUN_SETUP },
> { "verify-pack", cmd_verify_pack },
> { "verify-tag", cmd_verify_tag, RUN_SETUP },
> { "version", cmd_version },
> { "whatchanged", cmd_whatchanged, RUN_SETUP },
> { "worktree", cmd_worktree, RUN_SETUP | NO_PARSEOPT },
> { "write-tree", cmd_write_tree, RUN_SETUP },
> + { "xl", cmd_xl, RUN_SETUP },
> };
>
> static struct cmd_struct *get_builtin(const char *s)
> {
> int i;
> for (i = 0; i < ARRAY_SIZE(commands); i++) {
> struct cmd_struct *p = commands + i;
> if (!strcmp(s, p->cmd))
> return p;
> }
> diff --git a/t/t4400-xl.sh b/t/t4400-xl.sh
> new file mode 100755
> index 0000000000..f6e35bd4da
> --- /dev/null
> +++ b/t/t4400-xl.sh
> @@ -0,0 +1,270 @@
> +#!/bin/sh
> +
> +test_description='git xl'
> +. ./test-lib.sh
> +
> +xl () {
> + git xl "$@" >actual_raw &&
> + sed -e "s/ *$//" actual_raw
> +}
> +
> +test_expect_success 'basic' '
> + test_commit foo &&
> + git checkout -b branch2 &&
> + test_commit bar &&
> +
> + xl >actual &&
> + hashvl1=$(git rev-parse h/1 | test_copy_bytes 8) &&
> + hashvl2=$(git rev-parse h/2 | test_copy_bytes 8) &&
> +
> + echo "\
> +$hashvl1 * 1 committer@example.com [HEAD branch2]
> + | bar
> + |
> +$hashvl2 * 2 committer@example.com [master]
> + foo
> +" >expect &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'specify ref names' '
> + xl master >actual &&
> +
> + hashvl1=$(git rev-parse h/1 | test_copy_bytes 8) &&
> + hashvl2=$(git rev-parse h/2 | test_copy_bytes 8) &&
> +
> + echo "\
> +$hashvl1 * 1 committer@example.com [HEAD]
> + | bar
> + |
> +$hashvl2 * 2 committer@example.com [master]
> + foo
> +" >expect &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'deduce graph base' '
> + git checkout -b branch3 master &&
> + test_commit baz &&
> + git branch -d master &&
> + xl >actual &&
> +
> + hashvl1=$(git rev-parse h/1 | test_copy_bytes 8) &&
> + hashvl2=$(git rev-parse h/2 | test_copy_bytes 8) &&
> + xl_base=$(git rev-parse xl_base | test_copy_bytes 8) &&
> +
> + echo "\
> +$hashvl1 * 1 committer@example.com [HEAD branch3]
> + | baz
> + |
> +$hashvl2 | * 2 committer@example.com [branch2]
> + |/ bar
> + |
> +$xl_base * 3 committer@example.com
> + foo
> +" >expect &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'show upstream branch' '
> + git init --bare upstream_repo.git &&
> + git remote add upstream_repo upstream_repo.git &&
> +
> + git push -u upstream_repo HEAD &&
> + git branch --set-upstream-to=upstream_repo/branch3 &&
> + test_commit not_yet_pushed &&
> +
> + # Exclude branch2 by requesting at least one other ref explicitly.
> + xl branch3 >actual &&
> +
> + hashvl1=$(git rev-parse h/1 | test_copy_bytes 8) &&
> + hashvl2=$(git rev-parse h/2 | test_copy_bytes 8) &&
> +
> + echo "\
> +$hashvl1 * 1 committer@example.com [HEAD branch3]
> + | not_yet_pushed
> + |
> +$hashvl2 * 2 committer@example.com [upstream_repo/branch3]
> + baz
> +" >expect &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'de-dupe upstream branches' '
> + git checkout -b branch4 upstream_repo/branch3 &&
> + test_commit baz4 &&
> +
> + # Make sure we do not show the same upstream branch name twice
> + # even though two local branches share the same upstream branch.
> + xl >actual &&
> +
> + hashvl1=$(git rev-parse h/1 | test_copy_bytes 8) &&
> + hashvl2=$(git rev-parse h/2 | test_copy_bytes 8) &&
> + hashvl3=$(git rev-parse h/3 | test_copy_bytes 8) &&
> + hashvl4=$(git rev-parse h/4 | test_copy_bytes 8) &&
> + hashvl5=$(git rev-parse h/5 | test_copy_bytes 8) &&
> +
> + echo "\
> +$hashvl1 * 1 committer@example.com [HEAD branch4]
> + | baz4
> + |
> +$hashvl2 | * 2 committer@example.com [branch3]
> + |/ not_yet_pushed
> + |
> +$hashvl3 * 3 committer@example.com [upstream_repo/branch3]
> + | baz
> + |
> +$hashvl4 | * 4 committer@example.com [branch2]
> + |/ bar
> + |
> +$hashvl5 * 5 committer@example.com
> + foo
> +" >expect &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'multiple merge bases' '
> + git merge -m merge1 branch3 &&
> + test_commit baz5 &&
> +
> + git checkout branch3 &&
> + git merge -m merge2 h/1 &&
> + test_commit baz6 &&
> +
> + git branch --unset-upstream branch3 &&
> + xl branch3 branch4 >actual &&
> +
> + hashvl1=$(git rev-parse h/1 | test_copy_bytes 8) &&
> + hashvl2=$(git rev-parse h/2 | test_copy_bytes 8) &&
> + hashvl3=$(git rev-parse h/3 | test_copy_bytes 8) &&
> + hashvl4=$(git rev-parse h/4 | test_copy_bytes 8) &&
> + hashvl5=$(git rev-parse h/5 | test_copy_bytes 8) &&
> + hashvl6=$(git rev-parse h/6 | test_copy_bytes 8) &&
> +
> + echo "\
> +$hashvl1 * 1 committer@example.com [HEAD branch3]
> + | baz6
> + |
> +$hashvl2 * 2 committer@example.com
> + |\ merge2
> + | |
> +$hashvl3 | | * 3 committer@example.com [branch4]
> + | | | baz5
> + | | |
> +$hashvl4 | | * 4 committer@example.com
> + | | |\ merge1
> + | |/ /
> + | | /
> + | |/
> + |/|
> +$hashvl5 * | 5 committer@example.com
> + / not_yet_pushed
> + |
> +$hashvl6 * 6 committer@example.com
> + baz4
> +" >expect &&
> + test_cmp expect actual
> +'
> +
> +test_expect_success 'orphan branches' '
> + # If there are some branches to display which do not have a common
> + # ancestor with the other branches, we show them in a separate graph.
> + git checkout --orphan branch-a h/6 &&
> + git commit -m baz7 &&
> + xl >actual &&
> +
> + hashvl1=$(git rev-parse h/1 | test_copy_bytes 8) &&
> + hashvl2=$(git rev-parse h/2 | test_copy_bytes 8) &&
> + hashvl3=$(git rev-parse h/3 | test_copy_bytes 8) &&
> + hashvl4=$(git rev-parse h/4 | test_copy_bytes 8) &&
> + hashvl5=$(git rev-parse h/5 | test_copy_bytes 8) &&
> + hashvl6=$(git rev-parse h/6 | test_copy_bytes 8) &&
> + hashvl7=$(git rev-parse h/7 | test_copy_bytes 8) &&
> + hashvl8=$(git rev-parse h/8 | test_copy_bytes 8) &&
> + hashvl9=$(git rev-parse h/9 | test_copy_bytes 8) &&
> + hashv10=$(git rev-parse h/10 | test_copy_bytes 8) &&
> +
> + echo "\
> +$hashvl1 * 1 committer@example.com [HEAD branch-a]
> + baz7
> +
> +$hashvl2 * 2 committer@example.com [branch3]
> + | baz6
> + |
> +$hashvl3 * 3 committer@example.com
> + |\ merge2
> + | |
> +$hashvl4 | | * 4 committer@example.com [branch4]
> + | | | baz5
> + | | |
> +$hashvl5 | | * 5 committer@example.com
> + | | |\ merge1
> + | |/ /
> + | | /
> + | |/
> + |/|
> +$hashvl6 * | 6 committer@example.com
> + | | not_yet_pushed
> + | |
> +$hashvl7 | * 7 committer@example.com
> + |/ baz4
> + |
> +$hashvl8 * 8 committer@example.com
> + | baz
> + |
> +$hashvl9 | * 9 committer@example.com [branch2]
> + |/ bar
> + |
> +$hashv10 * 10 committer@example.com
> + foo
> +" >expect &&
> + test_cmp expect actual &&
> +
> + # Verify xl_base_# refs have been set correctly.
> + test_cmp_rev xl_base_1 h/1 &&
> + test_cmp_rev xl_base_2 h/10
> +'
> +
> +test_expect_success 'hide branches when branch.<branch-name>.no-xl is on' '
> + git checkout branch4 &&
> + git config branch.branch-a.no-xl true &&
> + git config branch.branch2.no-xl true &&
> + xl >actual &&
> +
> + hashvl1=$(git rev-parse h/1 | test_copy_bytes 8) &&
> + hashvl2=$(git rev-parse h/2 | test_copy_bytes 8) &&
> + hashvl3=$(git rev-parse h/3 | test_copy_bytes 8) &&
> + hashvl4=$(git rev-parse h/4 | test_copy_bytes 8) &&
> + hashvl5=$(git rev-parse h/5 | test_copy_bytes 8) &&
> + hashvl6=$(git rev-parse h/6 | test_copy_bytes 8) &&
> + hashvl7=$(git rev-parse h/7 | test_copy_bytes 8) &&
> +
> + echo "\
> +$hashvl1 * 1 committer@example.com [branch3]
> + | baz6
> + |
> +$hashvl2 * 2 committer@example.com
> + |\ merge2
> + | |
> +$hashvl3 | | * 3 committer@example.com [HEAD branch4]
> + | | | baz5
> + | | |
> +$hashvl4 | | * 4 committer@example.com
> + | | |\ merge1
> + | |/ /
> + | | /
> + | |/
> + |/|
> +$hashvl5 * | 5 committer@example.com
> + | | not_yet_pushed
> + | |
> +$hashvl6 | * 6 committer@example.com
> + |/ baz4
> + |
> +$hashvl7 * 7 committer@example.com [upstream_repo/branch3]
> + baz
> +" >expect &&
> + test_cmp expect actual
> +'
> +
> +test_done
> diff --git a/xl.c b/xl.c
> new file mode 100644
> index 0000000000..539e590f6b
> --- /dev/null
> +++ b/xl.c
> @@ -0,0 +1,485 @@
> +#include "builtin.h"
> +#include "cache.h"
> +#include "color.h"
> +#include "commit-reach.h"
> +#include "config.h"
> +#include "oidmap.h"
> +#include "ref-filter.h"
> +#include "refs.h"
> +#include "refs/refs-internal.h"
> +#include "remote.h"
> +#include "run-command.h"
> +#include "strbuf.h"
> +
> +#include <errno.h>
> +#include <stdarg.h>
> +#include <stdint.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +
> +static void set_ref(
> + struct ref_transaction *ref_tr,
> + char const *name,
> + const struct object_id *oid)
> +{
> + struct strbuf err = STRBUF_INIT;
> +
> + if (ref_transaction_update(ref_tr, name, oid, NULL, 0, NULL, &err))
> + die("%s", err.buf);
> +
> + strbuf_release(&err);
> +}
> +
> +struct hash_to_ref {
> + struct oidmap_entry e;
> +
> + struct ref_array_item **refs;
> + size_t nr;
> + size_t alloc;
> +};
> +
> +/* An array of ref_array_item's which are not owned by this structure. */
> +struct ref_selection {
> + struct ref_array_item **items;
> + size_t alloc;
> + size_t nr;
> +};
> +
> +static void populate_hash_to_ref_map(
> + struct oidmap *m,
> + struct ref_selection *refs)
> +{
> + size_t ref_i;
> + for (ref_i = 0; ref_i < refs->nr; ref_i++) {
> + struct hash_to_ref *h2r;
> + struct ref_array_item *ref = refs->items[ref_i];
> +
> + h2r = oidmap_get(m, &ref->objectname);
> + if (!h2r) {
> + h2r = xcalloc(1, sizeof(*h2r));
> + oidcpy(&h2r->e.oid, &ref->objectname);
> + oidmap_put(m, h2r);
> + }
> + ALLOC_GROW_BY(h2r->refs, h2r->nr, 1, h2r->alloc);
> + h2r->refs[h2r->nr - 1] = ref;
> + }
> +}
> +
> +/*
> + * Helps invoke `git log` for a certain kind of graph format and process that
> + * output. One instance of this object lives for the entire invocation of
> + * `git xl` even if multiple disjoint graphs are included.
> + */
> +struct log_processing {
> + struct strbuf raw_line;
> + struct strbuf line_buf;
> + struct strbuf line_prefix;
> + struct strbuf sym_refs;
> + struct strbuf tag_name;
> +
> + struct child_process log_proc;
> +
> + /* A buffered stream of the output of `git log` */
> + FILE *stream;
> +
> + /*
> + * Number of hashes found and abbreviated since the first graph was
> + * started.
> + */
> + size_t hash_count;
> +
> + unsigned graph_count;
> +
> + /*
> + * Maps object IDs to hash_to_ref objects which contain all the ref
> + * names that ref to the object.
> + */
> + const struct oidmap *h2r;
> +
> + /*
> + * All references that the user desires to be included in a graph. This
> + * array may get resorted.
> + */
> + struct ref_selection *refs;
> +
> + /*
> + * Index pointing to the first element that has not been included in a
> + * graph yet.
> + */
> + size_t ref_i;
> +
> + /* Transaction for creating h/# and xl_base(_#) refs. */
> + struct ref_transaction *ref_tr;
> +};
> +
> +#define LOG_PROCESSING_INIT { \
> + STRBUF_INIT, \
> + STRBUF_INIT, \
> + STRBUF_INIT, \
> + STRBUF_INIT, \
> + STRBUF_INIT, \
> +}
> +
> +static void log_processing_finish_proc(struct log_processing *p)
> +{
> + int err;
> +
> + fclose(p->stream);
> + p->stream = NULL;
> + err = finish_command(&p->log_proc);
> + if (err)
> + die(_("log failed or could not be terminated: 0x%x"), err);
> +}
> +
> +static void log_processing_release(struct log_processing *p)
> +{
> + if (p->stream)
> + BUG("last log stdout was not closed");
> + strbuf_release(&p->raw_line);
> + strbuf_release(&p->line_buf);
> + strbuf_release(&p->line_prefix);
> + strbuf_release(&p->sym_refs);
> + strbuf_release(&p->tag_name);
> +}
> +
> +#define XL_HASH_PREFIX "<{xl_hash}>"
> +
> +/*
> + * Begins a `git log` sub process with a subset of the branches requested.
> + *
> + * This log invocation shows a graph (using --graph) with full hashes. The
> + * hashes are prefixed with XL_HASH_PREFIX so they can get easily extracted.
> + *
> + * This function also sets the xl_base or xl_base_# ref to the merge base of
> + * the branches included.
> + */
> +static int log_processing_start_proc(struct log_processing *p)
> +{
> + size_t ref_i;
> + size_t start_ref_i = p->ref_i;
> + size_t end_ref_i = p->refs->nr;
> + struct commit *merge_base;
> +
> + if (p->ref_i == p->refs->nr)
> + return 0;
> +
> + /*
> + * Split the p->refs[] sub array starting at start_ref_i into two
> + * sections, re-ordering if needed.
> + *
> + * The first section contains all commits which share a common ancestor
> + * with p->refs->items[start_ref_i]. The second section contains all
> + * other commits. In the process, we determine the merge base of the
> + * subset. If there are multiple merge bases, we only keep track of one.
> + * This is because `git log --graph <branch1...branchN>` only needs one
> + * of the merge bases to intelligently limit the graph size.
> + *
> + * After the loop is complete, end_ref_i will point to the first item
> + * in the second section.
> + */
> + merge_base = lookup_commit(
> + the_repository, &p->refs->items[start_ref_i]->objectname);
> + for (ref_i = start_ref_i + 1; ref_i < end_ref_i;) {
> + struct commit *next = lookup_commit(
> + the_repository, &p->refs->items[ref_i]->objectname);
> + struct commit_list *clist = repo_get_merge_bases(
> + the_repository, merge_base, next);
> +
> + if (!clist) {
> + /*
> + * The ref at ref_i does not share a common ancestor
> + * with the refs processed since start_ref_i. Move the
> + * ref at ref_i to the end of the refs array, and move
> + * the item already at the end of the array to ref_i.
> + * This allows us to postpone processing this orphan
> + * branch until the next `git log` invocation.
> + */
> + struct ref_array_item *tmp = p->refs->items[ref_i];
> + p->refs->items[ref_i] = p->refs->items[--end_ref_i];
> + p->refs->items[end_ref_i] = tmp;
> + } else {
> + merge_base = clist->item;
> + free_commit_list(clist);
> + ref_i++;
> + }
> + }
> +
> + p->graph_count++;
> + if (!start_ref_i && end_ref_i == p->refs->nr) {
> + /* Only a single log graph in this invocation of `git xl`. */
> + set_ref(p->ref_tr, "xl_base", &merge_base->object.oid);
> + } else {
> + /* Multiple log graphs - use a counter to disambiguate bases. */
> + struct strbuf xl_base_ref_name = STRBUF_INIT;
> + strbuf_addf(&xl_base_ref_name, "xl_base_%u", p->graph_count);
> + set_ref(p->ref_tr, xl_base_ref_name.buf,
> + &merge_base->object.oid);
> + strbuf_release(&xl_base_ref_name);
> + }
> +
> + child_process_init(&p->log_proc);
> + p->log_proc.git_cmd = 1;
> + p->log_proc.out = -1;
> + p->log_proc.no_stdin = 1;
> +
> + argv_array_pushl(&p->log_proc.args, "log", "--graph", NULL);
> + argv_array_pushf(&p->log_proc.args, "--color=%s",
> + want_color(GIT_COLOR_UNKNOWN) ? "always" : "never");
> + argv_array_push(&p->log_proc.args,
> + "--format=format:" XL_HASH_PREFIX "%H %ce\n%s\n ");
> + for (ref_i = start_ref_i; ref_i < end_ref_i; ref_i++)
> + argv_array_push(
> + &p->log_proc.args, p->refs->items[ref_i]->refname);
> + argv_array_pushf(&p->log_proc.args, "^%s^@",
> + oid_to_hex(&merge_base->object.oid));
> + argv_array_push(&p->log_proc.args, "--");
> +
> + if (start_command(&p->log_proc))
> + die(_("cannot start log"));
> +
> + p->stream = xfdopen(p->log_proc.out, "r");
> +
> + p->ref_i = end_ref_i;
> +
> + return 1;
> +}
> +
> +static const char *color_on(const char *c)
> +{
> + return want_color(GIT_COLOR_UNKNOWN) ? c : "";
> +}
> +
> +static const char *color_off(void)
> +{
> + return want_color(GIT_COLOR_UNKNOWN) ? "\e[0m" : "";
> +}
> +
> +static void maybe_format_symrefs(
> + struct strbuf *sym_refs,
> + struct oidmap const *h2r,
> + const struct object_id *oid)
> +{
> + struct hash_to_ref const *h2r_entry;
> + size_t ref_i;
> +
> + h2r_entry = oidmap_get(h2r, oid);
> +
> + if (!h2r_entry)
> + return;
> +
> + strbuf_addf(sym_refs, " %s[", color_on("\e[1m"));
> +
> + for (ref_i = 0; ref_i < h2r_entry->nr; ref_i++) {
> + char *shortened_ref = shorten_unambiguous_ref(
> + h2r_entry->refs[ref_i]->refname, /*strict=*/1);
> +
> + if (ref_i)
> + strbuf_addch(sym_refs, ' ');
> +
> + strbuf_addstr(sym_refs, shortened_ref);
> + free(shortened_ref);
> + }
> +
> + strbuf_addf(sym_refs, "]%s", color_off());
> +}
> +
> +static int process_log_line(struct log_processing *p)
> +{
> + const char *in;
> + size_t hash_prefix_len = strlen(XL_HASH_PREFIX);
> +
> + strbuf_reset(&p->raw_line);
> + strbuf_reset(&p->line_buf);
> + strbuf_reset(&p->line_prefix);
> + strbuf_reset(&p->sym_refs);
> + strbuf_reset(&p->tag_name);
> +
> + if (strbuf_getline_lf(&p->raw_line, p->stream) == EOF)
> + return 0;
> +
> + in = p->raw_line.buf;
> +
> + while (*in) {
> + struct object_id oid;
> + const char *after_hash;
> +
> + if (p->line_prefix.len ||
> + strncmp(XL_HASH_PREFIX, in, hash_prefix_len) ||
> + parse_oid_hex(in + hash_prefix_len, &oid, &after_hash)) {
> + strbuf_addch(&p->line_buf, *in++);
> + continue;
> + }
> +
> + p->hash_count++;
> + strbuf_addf(&p->line_buf,
> + "%s %ld %s",
> + color_on("\e[48;5;213m\e[30m"),
> + p->hash_count,
> + color_off());
> +
> + strbuf_addf(&p->line_prefix,
> + "%s%.8s%s",
> + color_on("\e[38;5;147m"),
> + in + hash_prefix_len,
> + color_off());
> + in = after_hash;
> +
> + strbuf_addf(&p->tag_name, "h/%ld", p->hash_count);
> + set_ref(p->ref_tr, p->tag_name.buf, &oid);
> +
> + maybe_format_symrefs(&p->sym_refs, p->h2r, &oid);
> + }
> +
> + fprintf(stdout, "%8s %s%s\n",
> + p->line_prefix.buf,
> + p->line_buf.buf,
> + p->sym_refs.buf);
> +
> + return 1;
> +}
> +
> +static void empty_hash_to_ref_map(struct oidmap *m)
> +{
> + struct oidmap_iter i;
> + struct hash_to_ref *h2r;
> + oidmap_iter_init(m, &i);
> +
> + while ((h2r = oidmap_iter_next(&i)) != NULL) {
> + FREE_AND_NULL(h2r->refs);
> + h2r->alloc = 0;
> + h2r->nr = 0;
> + }
> +}
> +
> +static int add_ref(struct ref_array *refs, const char *name)
> +{
> + struct object_id oid;
> + size_t ref_i;
> +
> + /* If we already have the ref, don't add it again. */
> + for (ref_i = 0; ref_i < refs->nr; ref_i++) {
> + if (!strcmp(refs->items[ref_i]->refname, name))
> + return 0;
> + }
> +
> + if (get_oid(name, &oid))
> + die("unknown object: %s", name);
> + ref_array_push(refs, name, &oid);
> +
> + return 1;
> +}
> +
> +static void select_ref(
> + struct ref_selection *ref_sel,
> + struct ref_array *refs,
> + size_t ref_i)
> +{
> + ALLOC_GROW_BY(ref_sel->items, ref_sel->nr, 1, ref_sel->alloc);
> + ref_sel->items[ref_sel->nr - 1] = refs->items[ref_i];
> +}
> +
> +static void populate_branch_args(
> + struct ref_array *refs,
> + struct ref_selection *ref_sel,
> + const char **argv)
> +{
> + struct ref_filter filter = {0};
> + size_t ref_i;
> + size_t ref_i_end;
> + struct strbuf no_xl_config_key = STRBUF_INIT;
> +
> + filter.name_patterns = argv;
> + filter_refs(refs, &filter, FILTER_REFS_BRANCHES);
> +
> + ref_i_end = refs->nr;
> +
> + /* Add upstream branches of each branch. */
> + for (ref_i = 0; ref_i < ref_i_end; ref_i++) {
> + struct branch *branch = branch_get(refs->items[ref_i]->refname);
> + char *short_name;
> + const char *upstream;
> + int no_xl = 0;
> +
> + if (!branch) {
> + /*
> + * Not actually a branch, but might be HEAD. Select this
> + * ref for display.
> + */
> + select_ref(ref_sel, refs, ref_i);
> + continue;
> + }
> +
> + /*
> + * Do not show the branch or its upstream if user configured
> + * branch.<branch-name>.no-xl = true
> + */
> + short_name = shorten_unambiguous_ref(
> + branch->name, /*strict=*/1);
> + strbuf_reset(&no_xl_config_key);
> + strbuf_addf(&no_xl_config_key, "branch.%s.no-xl", short_name);
> + FREE_AND_NULL(short_name);
> +
> + if (!git_config_get_bool(no_xl_config_key.buf, &no_xl) && no_xl)
> + continue;
> +
> + select_ref(ref_sel, refs, ref_i);
> + upstream = branch_get_upstream(branch, NULL);
> +
> + /*
> + * Add the upstream branch if it has not been added as the
> + * upstream of some other local branch.
> + */
> + if (upstream && add_ref(refs, upstream))
> + select_ref(ref_sel, refs, refs->nr - 1);
> + }
> +
> + strbuf_release(&no_xl_config_key);
> +}
> +
> +int cmd_xl(int argc, const char **argv, const char *prefx)
> +{
> + struct oidmap hash_to_ref_map = OIDMAP_INIT;
> + struct ref_selection ref_sel = {0};
> + struct ref_array refs = {0};
> + struct strbuf ref_tr_err = STRBUF_INIT;
> + struct ref_transaction *ref_tr;
> + struct log_processing log_processing = LOG_PROCESSING_INIT;
> +
> + git_config(git_color_config, NULL);
> +
> + /*
> + * Add HEAD first. This way, if we output multiple graphs, the first
> + * one will include the currently checked-out ref.
> + */
> + add_ref(&refs, "HEAD");
> +
> + populate_branch_args(&refs, &ref_sel, argv + 1);
> +
> + oidmap_init(&hash_to_ref_map, 16);
> + populate_hash_to_ref_map(&hash_to_ref_map, &ref_sel);
> +
> + if (!(ref_tr = ref_transaction_begin(&ref_tr_err)))
> + die("%s", ref_tr_err.buf);
> +
> + log_processing.h2r = &hash_to_ref_map;
> + log_processing.ref_tr = ref_tr;
> + log_processing.refs = &ref_sel;
> + while (log_processing_start_proc(&log_processing)) {
> + while (process_log_line(&log_processing)) {}
> + log_processing_finish_proc(&log_processing);
> + }
> +
> + if (ref_transaction_commit(ref_tr, &ref_tr_err))
> + die("%s", ref_tr_err.buf);
> +
> + empty_hash_to_ref_map(&hash_to_ref_map);
> + oidmap_free(&hash_to_ref_map, 1);
> + ref_array_clear(&refs);
> + ref_transaction_free(ref_tr);
> + strbuf_release(&ref_tr_err);
> + log_processing_release(&log_processing);
> + FREE_AND_NULL(ref_sel.items);
> +
> + return 0;
> +}
> --
> 2.19.0.605.g01d371f741-goog
>
next prev parent reply other threads:[~2019-10-31 0:39 UTC|newest]
Thread overview: 13+ messages / expand[flat|nested] mbox.gz Atom feed top
2019-10-29 0:30 [RFC] xl command for visualizing recent history Matthew DeVore
2019-10-31 0:39 ` Emily Shaffer [this message]
2019-10-31 8:26 ` Johannes Schindelin
2019-10-31 20:04 ` Phillip Wood
2019-11-01 18:58 ` Johannes Schindelin
2020-01-03 20:14 ` Matthew DeVore
2020-01-03 21:30 ` Junio C Hamano
2020-01-04 20:30 ` Johannes Schindelin
2020-01-04 22:56 ` Junio C Hamano
2020-01-04 21:21 ` Johannes Schindelin
2020-02-07 1:39 ` Matthew DeVore
2020-01-03 2:51 ` Matthew DeVore
2019-10-31 10:16 ` Johannes Schindelin
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20191031003929.GA22855@google.com \
--to=emilyshaffer@google.com \
--cc=git@vger.kernel.org \
--cc=jonathantanmy@google.com \
--cc=jrnieder@gmail.com \
--cc=matvore@comcast.net \
--cc=matvore@gmail.com \
--cc=matvore@google.com \
--cc=steadmon@google.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).