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;
+}
