new file mode 100644
@@ -0,0 +1,136 @@
+git-reverse-trailer-xrefs(1)
+============================
+
+NAME
+----
+git-reverse-trailer-xrefs - Record reverse-map of trailer commit references into notes
+
+SYNOPSIS
+--------
+[verse]
+`git reverse_trailer_xrefs` --xref-cherry-picks [--clear] [<options>] [<commit-ish>...]
+`git reverse_trailer_xrefs` --trailer-prefix=<prefix> --ref=<notes-ref> [--tag=<tag>] [<options>] [<commit-ish>...]
+`git reverse_trailer_xrefs` --ref=<notes-ref> --clear [<options>] [<commit-ish>...]
+
+
+DESCRIPTION
+-----------
+Record or clear reverse-map of trailer commit references in the
+specified notes ref.
+
+Some commit trailers reference other commits. For example,
+`git-cherry-pick -x` adds the following trailer to record the source
+commit.
+----------
+(cherry picked from commit <source-commit-id>)
+----------
+The reverse mappings of such cross references can be useful. For
+cherry-picks, it would allow finding all the cherry-picked commits of
+a given source commit. `git-reverse-trailer-xrefs` can be used to
+create and maintain such reverse mappings in notes.
+
+When used with `--xref-cherry-picks`, the cherry-pick trailers are
+parsed from the specified commits and the reverse mappings are
+recorded in the `refs/notes/xref-cherry-picks` notes of the source
+commits in the following format.
+----------
+Cherry-picked-to: <destination-commit-id>
+----------
+
+When a note with its notes ref starting with `refs/notes/xref-` is
+formatted to be displayed with the commit for, e.g., `git-show` or
+`git-log`, the destination commit is followed recursively and the
+matching notes are shown with increasing level of indentations.
+
+`--trailer-prefix`, `--notes` and `--tag` can be used to use a custom
+set of trailer, notes ref and reverse mapping tag.
+
+OPTIONS
+-------
+<commit-ish>...::
+ Commit-ish object names to describe. Defaults to HEAD if omitted.
+
+--xref-cherry-picks::
+ Use the preset to reverse map `git-cherry-pick -x`
+ trailers. `--trailer-prefix` is set to `(cherry-picked from
+ commit `, `--notes` is set to `refs/notes/xref-cherry-picks`
+ and `--tag` is set to `Cherry-picked-to`. This option can't be
+ specified with the three preset options.
+
+--trailer-prefix=<prefix>::
+ Process trailers which start with <prefix>. It is matched
+ character-by-character and should be followed by the
+ referenced commit ID. When there are multiple matching
+ trailers, the last one is used.
+
+--notes=<notes-ref>::
+ The notes ref to use for the reverse mapping. While this can
+ be any notes ref, it is recommented to use ones starting with
+ `refs/notes/xref-` as they are recognized as cross-referencing
+ notes and handled specially when updating and showing.
+
+--tag=<tag>::
+ Optional tag to use when generating reverse reference
+ notes. If specified, each note line is formatted as `<tag>:
+ <commit-id>`; otherwise, `<commit-id>`.
+
+--clear::
+ Instead of creating reverse mapping notes, clear them from the
+ specified commits.
+
+
+EXAMPLES
+--------
+
+Assume the following history. Development is happening in "master" and
+releases are branched off and fixes are cherry-picked into them.
+
+------------
+ D'---E'' release-B
+ /
+ C' E' release-D
+ / /
+A---B---C---D---E master
+------------
+
+The following cherry-picks took place.
+
+------------
+C -> C'
+D -> D'
+E -> E' -> E''
+------------
+
+The reverse mappings for all commits can be created using the
+following command.
+
+------------
+$ git reverse-trailer-xrefs --all --xref-cherry-picks
+------------
+
+With the notes added, where each commit ended up can be easily
+determined.
+
+------------
+$ git log --notes=xref-cherry-picks --oneline | git name-rev --name-only --stdin
+4b165af commit E
+Notes (xref-cherry-picks):
+ Cherry-picked-to: release-D
+ Cherry-picked-to: release-B
+
+82bf1f3 commit D
+Notes (xref-cherry-picks):
+ Cherry-picked-to: release-B~1
+
+364eccf commit C
+Notes (xref-cherry-picks):
+ Cherry-picked-to: release-B~2
+
+ed3adf3 commit B
+ddd1bf2 commit A
+------------
+
+
+GIT
+---
+Part of the linkgit:git[1] suite
@@ -1086,6 +1086,7 @@ BUILTIN_OBJS += builtin/multi-pack-index.o
BUILTIN_OBJS += builtin/mv.o
BUILTIN_OBJS += builtin/name-rev.o
BUILTIN_OBJS += builtin/notes.o
+BUILTIN_OBJS += builtin/reverse-trailer-xrefs.o
BUILTIN_OBJS += builtin/pack-objects.o
BUILTIN_OBJS += builtin/pack-redundant.o
BUILTIN_OBJS += builtin/pack-refs.o
@@ -195,6 +195,7 @@ extern int cmd_multi_pack_index(int argc, const char **argv, const char *prefix)
extern int cmd_mv(int argc, const char **argv, const char *prefix);
extern int cmd_name_rev(int argc, const char **argv, const char *prefix);
extern int cmd_notes(int argc, const char **argv, const char *prefix);
+extern int cmd_reverse_trailer_xrefs(int argc, const char **argv, const char *prefix);
extern int cmd_pack_objects(int argc, const char **argv, const char *prefix);
extern int cmd_pack_redundant(int argc, const char **argv, const char *prefix);
extern int cmd_patch_id(int argc, const char **argv, const char *prefix);
new file mode 100644
@@ -0,0 +1,160 @@
+#include "builtin.h"
+#include "cache.h"
+#include "strbuf.h"
+#include "repository.h"
+#include "config.h"
+#include "commit.h"
+#include "blob.h"
+#include "notes.h"
+#include "notes-utils.h"
+#include "trailer.h"
+#include "revision.h"
+#include "list-objects.h"
+#include "object-store.h"
+#include "parse-options.h"
+
+static const char * const reverse_trailer_xrefs_usage[] = {
+ N_("git reverse_trailer_xrefs --xref-cherry-picks [--clear] [<options>] [<commit-ish>...]"),
+ N_("git reverse_trailer_xrefs --trailer-prefix=<prefix> --notes=<notes-ref> --tag=<tag> [<options>] [<commit-ish>...]"),
+ N_("git reverse_trailer_xrefs --notes=<notes-ref> --clear [<options>] [<commit-ish>...]"),
+ NULL
+};
+
+#define CHERRY_PICKED_PREFIX "(cherry picked from commit "
+#define CHERRY_PICKS_REF "refs/notes/xref-cherry-picks"
+#define CHERRY_PICKED_TO_TAG "Cherry-picked-to"
+
+static int verbose;
+
+static void clear_trailer_xref_note(struct commit *commit, void *data)
+{
+ struct notes_tree *tree = data;
+ int status;
+
+ status = remove_note(tree, commit->object.oid.hash);
+
+ if (verbose) {
+ if (status)
+ fprintf(stderr, "Object %s has no note\n",
+ oid_to_hex(&commit->object.oid));
+ else
+ fprintf(stderr, "Removing note for object %s\n",
+ oid_to_hex(&commit->object.oid));
+ }
+}
+
+static void record_trailer_xrefs(struct commit *commit, void *data)
+{
+ trailer_rev_xrefs_record(data, commit);
+}
+
+static int note_trailer_xrefs(struct notes_tree *tree,
+ struct commit *dst_commit,
+ struct object_array *src_objs, const char *tag)
+{
+ char dst_hex[GIT_MAX_HEXSZ + 1];
+ struct strbuf note = STRBUF_INIT;
+ struct object_id note_oid;
+ int i, ret;
+
+ oid_to_hex_r(dst_hex, &dst_commit->object.oid);
+
+ for (i = 0; i < src_objs->nr; i++) {
+ const char *hex = src_objs->objects[i].name;
+
+ if (tag)
+ strbuf_addf(¬e, "%s: %s\n", tag, hex);
+ else
+ strbuf_addf(¬e, "%s\n", tag);
+ if (verbose)
+ fprintf(stderr, "Adding note %s -> %s\n", dst_hex, hex);
+ }
+
+ ret = write_object_file(note.buf, note.len, blob_type, ¬e_oid);
+ strbuf_release(¬e);
+ if (ret)
+ return ret;
+
+ ret = add_note(tree, &dst_commit->object.oid, ¬e_oid, NULL);
+ return ret;
+}
+
+static int reverse_trailer_xrefs(struct rev_info *revs, int clear,
+ const char *trailer_prefix,
+ const char *notes_ref, const char *tag)
+{
+ struct notes_tree tree = { };
+ int i, ret;
+
+ init_notes(&tree, notes_ref, NULL, NOTES_INIT_WRITABLE);
+
+ if (prepare_revision_walk(revs))
+ die("revision walk setup failed");
+
+ if (clear) {
+ traverse_commit_list(revs, clear_trailer_xref_note, NULL, &tree);
+ } else {
+ struct trailer_rev_xrefs rxrefs;
+ struct commit *dst_commit;
+ struct object_array *src_objs;
+
+ trailer_rev_xrefs_init(&rxrefs, trailer_prefix);
+ traverse_commit_list(revs, record_trailer_xrefs, NULL, &rxrefs);
+
+ trailer_rev_xrefs_for_each(&rxrefs, i, dst_commit, src_objs) {
+ ret = note_trailer_xrefs(&tree, dst_commit, src_objs,
+ tag);
+ if (ret)
+ return ret;
+ }
+
+ trailer_rev_xrefs_release(&rxrefs);
+ }
+
+ commit_notes(&tree, "Notes updated by 'git reverse-trailer-xrefs'");
+ return 0;
+}
+
+int cmd_reverse_trailer_xrefs(int argc, const char **argv, const char *prefix)
+{
+ struct rev_info revs;
+ struct setup_revision_opt s_r_opt = {
+ .def = "HEAD",
+ .revarg_opt = REVARG_CANNOT_BE_FILENAME
+ };
+ int cherry = 0, clear = 0;
+ const char *trailer_prefix = NULL, *notes_ref = NULL, *tag = NULL;
+ struct option options[] = {
+ OPT_BOOL(0, "xref-cherry-picks", &cherry, N_("use preset for xref-cherry-picks notes")),
+ OPT_STRING(0, "trailer-prefix", &trailer_prefix, N_("prefix"), N_("process trailers starting with <prefix>")),
+ OPT_STRING(0, "notes", ¬es_ref, N_("notes-ref"), N_("update notes in <notes-ref>")),
+ OPT_STRING(0, "tag", &tag, N_("tag"), N_("tag xref notes with <tag>")),
+ OPT_BOOL(0, "clear", &clear, N_("clear trailer xref notes from the specified commits")),
+ OPT__VERBOSE(&verbose, N_("verbose")),
+ OPT_END()
+ };
+
+ git_config(git_default_config, NULL);
+
+ argc = parse_options(argc, argv, prefix, options,
+ reverse_trailer_xrefs_usage,
+ PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN);
+
+ init_revisions(&revs, prefix);
+ argc = setup_revisions(argc, argv, &revs, &s_r_opt);
+
+ if (cherry) {
+ trailer_prefix = CHERRY_PICKED_PREFIX;
+ notes_ref = CHERRY_PICKS_REF;
+ tag = CHERRY_PICKED_TO_TAG;
+ }
+
+ if (!notes_ref || (!clear && (!trailer_prefix || !tag)))
+ die(_("insufficient arguments"));
+
+ if (argc > 1)
+ die(_("unrecognized argument: %s"), argv[1]);
+
+ return reverse_trailer_xrefs(&revs, clear,
+ trailer_prefix, notes_ref, tag);
+}
@@ -515,6 +515,7 @@ static struct cmd_struct commands[] = {
{ "mv", cmd_mv, RUN_SETUP | NEED_WORK_TREE },
{ "name-rev", cmd_name_rev, RUN_SETUP },
{ "notes", cmd_notes, RUN_SETUP },
+ { "reverse-trailer-xrefs", cmd_reverse_trailer_xrefs, RUN_SETUP },
{ "pack-objects", cmd_pack_objects, RUN_SETUP },
{ "pack-redundant", cmd_pack_redundant, RUN_SETUP | NO_PARSEOPT },
{ "pack-refs", cmd_pack_refs, RUN_SETUP },