{"thread":{"id":"49592","subject":"[RFC] cherry-pick notes to find out cherry-picks from the origin","startedAt":"2018-10-17T14:39:28Z","lastAt":"2018-12-06T22:15:33Z","messageCount":6,"participants":["Tejun Heo","Jeff King"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"360755","messageId":"20181017143921.GR270328@devbig004.ftw2.facebook.com","threadId":"49592","inReplyTo":null,"subject":"[RFC] cherry-pick notes to find out cherry-picks from the origin","fromName":"Tejun Heo","fromEmail":"tj@kernel.org","sentAt":"2018-10-17T14:39:21Z","receivedAt":"2018-10-17T14:39:28Z","isPatch":false,"sender":{"key":"tj@kernel.org","avatar":null},"body":"Hello, Junio, Jeff.\n\nA while ago, I proposed changes to name-rev and describe so that they\ncan identify the commits cherry-picked from the one which is being\nshown.\n\n  https://public-inbox.org/git/20180726153714.GX1934745@devbig577.frc2.facebook.com/T/\n\nWhile the use-cases - e.g. tracking down which release / stable\nbranches a given commit ended up in - weren't controversial, it was\nsuggested that it'd make more sense to use notes to link cherry-picks\ninstead of building the feature into name-rev.\n\nThe patch appended to this message implements most of it (sans tests\nand documentation).  It's composed of the following two parts.\n\n* A new built-in command note-cherry-picks, which walks the specified\n  commits and if they're marked with the cherry-pick trailer, adds the\n  backlink to the origin commit using Cherry-picked-to tag in a\n  cherry-picks note.\n\n* When formatting a cherry-picks note for display, nested cherry-picks\n  are followed from each Cherry-picked-to tag and printed out with\n  matching indentations.\n\nCombined with name-rev --stdin, it can produce outputs like the following.\n\n    commit 82cddd79f962de0bb1e7cdd95d48b48633335816 (branch2)\n    Author: Tejun Heo <tj@kernel.org>\n    Date:   Wed Jul 25 21:31:35 2018 -0700\n\n\tcommit 4\n\n    (cherry picked from commit 10f7ce0a0e524279f022b48460c088a108b45d54 (master~1))\n    (cherry picked from commit d433e3b4d5a19b3d29e2c8349fe88ceade5f6190 (branch1))\n\n    commit d433e3b4d5a19b3d29e2c8349fe88ceade5f6190 (branch1)\n    Author: Tejun Heo <tj@kernel.org>\n    Date:   Wed Jul 25 21:31:35 2018 -0700\n\n\tcommit 4\n\n\t(cherry picked from commit 10f7ce0a0e524279f022b48460c088a108b45d54 (master~1))\n\n    Notes (cherry-picks):\n\tCherry-picked-to: 82cddd79f962de0bb1e7cdd95d48b48633335816 (branch2)\n\n    commit 10f7ce0a0e524279f022b48460c088a108b45d54 (master~1)\n    Author: Tejun Heo <tj@kernel.org>\n    Date:   Wed Jul 25 21:31:35 2018 -0700\n\n\tcommit 4\n\n    Notes (cherry-picks):\n\tCherry-picked-to: d433e3b4d5a19b3d29e2c8349fe88ceade5f6190 (branch1)\n\tCherry-picked-to:   82cddd79f962de0bb1e7cdd95d48b48633335816 (branch2)\n\tCherry-picked-to: 2dd08fe869986c26bc1152a0bcec8c2fa48c50f7 (branch5)\n\tCherry-picked-to: fa8b79edc5dfff21753c2ccfc1a1828336c4c070 (branch4~5)\n\tCherry-picked-to:   a1fb6024e3bda5549de1d15d8fa37e8c3a7eecbe (branch4~2)\n\tCherry-picked-to:     58964342321a65e316ff47db33f7063743bc0de8 (branch4)\n\tCherry-picked-to:   45e0d5f31c869dcc89b9737853e64489e2ad80b0 (branch4~1)\n\tCherry-picked-to: 58a8d36b2532feb0a14b4fc2a50d587e64f38324 (branch3)\n\nLocally, the notes can be kept up-to-date with a trivial post-commit\nhook which invokes note-cherry-picks on the new commit; however, I'm\nhaving a bit of trouble figuring out a way to keep it up-to-date when\nmultiple trees are involved.  AFAICS, there are two options.\n\n1. Ensuring that the notes are always generated on local commits and\n   whenever new commits are received through fetch/pulls.\n\n2. Ensuring that the notes are always generated on local commits and\n   transported with push/pulls.\n\n3. A hybrid approach - also generate notes on the receiving end and\n   ensure that fetch/pulls receives the notes together (ie. similar to\n   --tags option to git-fetch).\n\n#1 seems simpler and more robust to me.  Unfortunately, I can't see a\nway to implement any of the three options with the existing hooks.\nFor #1, there's no post-fetch hook.  For #2 and #3, there doesn't seem\nto be a fool-proof way to make sure that the notes are transported\ntogether.  Any suggestions would be greatly appreciated.\n\nPlease let me know what you think.\n\nThanks.\n\n---\n Makefile                    |    1 \n builtin.h                   |    1 \n builtin/note-cherry-picks.c |  197 ++++++++++++++++++++++++++++++++++++++++++++\n builtin/notes.c             |   17 ++-\n git.c                       |    1 \n notes.c                     |   95 +++++++++++++++++++++\n notes.h                     |    7 +\n object.c                    |    4 \n object.h                    |    6 +\n 9 files changed, 320 insertions(+), 9 deletions(-)\n\ndiff --git a/Makefile b/Makefile\nindex 5c8307b7c..fb0ff3ce9 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1073,6 +1073,7 @@ BUILTIN_OBJS += builtin/multi-pack-index.o\n BUILTIN_OBJS += builtin/mv.o\n BUILTIN_OBJS += builtin/name-rev.o\n BUILTIN_OBJS += builtin/notes.o\n+BUILTIN_OBJS += builtin/note-cherry-picks.o\n BUILTIN_OBJS += builtin/pack-objects.o\n BUILTIN_OBJS += builtin/pack-redundant.o\n BUILTIN_OBJS += builtin/pack-refs.o\ndiff --git a/builtin.h b/builtin.h\nindex 962f0489a..d9d019abc 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -195,6 +195,7 @@ extern int cmd_multi_pack_index(int argc, const char **argv, const char *prefix)\n extern int cmd_mv(int argc, const char **argv, const char *prefix);\n extern int cmd_name_rev(int argc, const char **argv, const char *prefix);\n extern int cmd_notes(int argc, const char **argv, const char *prefix);\n+extern int cmd_note_cherry_picks(int argc, const char **argv, const char *prefix);\n extern int cmd_pack_objects(int argc, const char **argv, const char *prefix);\n extern int cmd_pack_redundant(int argc, const char **argv, const char *prefix);\n extern int cmd_patch_id(int argc, const char **argv, const char *prefix);\ndiff --git a/builtin/note-cherry-picks.c b/builtin/note-cherry-picks.c\nnew file mode 100644\nindex 000000000..343e22c0d\n--- /dev/null\n+++ b/builtin/note-cherry-picks.c\n@@ -0,0 +1,197 @@\n+#include \"builtin.h\"\n+#include \"cache.h\"\n+#include \"strbuf.h\"\n+#include \"repository.h\"\n+#include \"config.h\"\n+#include \"commit.h\"\n+#include \"notes.h\"\n+#include \"trailer.h\"\n+#include \"revision.h\"\n+#include \"argv-array.h\"\n+#include \"commit-slab.h\"\n+#include \"list-objects.h\"\n+#include \"object-store.h\"\n+#include \"parse-options.h\"\n+\n+define_commit_slab(commit_cherry_picks, struct object_array *);\n+\n+static const char * const note_cherry_picks_usage[] = {\n+\tN_(\"git note-cherry-picks [<options>] [<commit-ish>...]\"),\n+\tNULL\n+};\n+\n+static const char cherry_picked_prefix[] = \"(cherry picked from commit \";\n+static const char cherry_picked_to_tag[] = \"Cherry-picked-to: \";\n+static int verbose, clear;\n+static struct object_array cherry_picked = OBJECT_ARRAY_INIT;\n+static struct commit_cherry_picks cherry_picks;\n+\n+static struct object_array *get_commit_cherry_picks(struct commit *commit)\n+{\n+\tstruct object_array **slot =\n+\t\tcommit_cherry_picks_peek(&cherry_picks, commit);\n+\n+\treturn slot ? *slot : NULL;\n+}\n+\n+static struct object_array *get_create_commit_cherry_picks(struct commit *commit)\n+{\n+\tstruct object_array **slot =\n+\t\tcommit_cherry_picks_at(&cherry_picks, commit);\n+\tstruct object_array *cps = *slot;\n+\tint i;\n+\n+\tif (cps)\n+\t\treturn cps;\n+\n+\tadd_object_array(&commit->object, oid_to_hex(&commit->object.oid),\n+\t\t\t &cherry_picked);\n+\t*slot = cps = xmalloc(sizeof(struct object_array));\n+\t*cps = (struct object_array)OBJECT_ARRAY_INIT;\n+\n+\tread_cherry_picks_note(&commit->object.oid, cps);\n+\tif (verbose) {\n+\t\tfor (i = 0; i < cps->nr; i++)\n+\t\t\tfprintf(stderr, \"Read  note %s -> %s\\n\",\n+\t\t\t\toid_to_hex(&commit->object.oid),\n+\t\t\t\tcps->objects[i].name);\n+\t}\n+\treturn cps;\n+}\n+\n+static void record_cherry_pick(struct commit *commit, void *unused)\n+{\n+\tstruct process_trailer_options opts = PROCESS_TRAILER_OPTIONS_INIT;\n+\tenum object_type type;\n+\tunsigned long size;\n+\tvoid *buffer;\n+\tstruct trailer_info info;\n+\tint i;\n+\n+\tbuffer = read_object_file(&commit->object.oid, &type, &size);\n+\ttrailer_info_get(&info, buffer, &opts);\n+\n+\t/* when nested, the last trailer describes the latest cherry-pick */\n+\tfor (i = info.trailer_nr - 1; i >= 0; i--) {\n+\t\tconst int prefix_len = sizeof(cherry_picked_prefix) - 1;\n+\t\tchar *line = info.trailers[i];\n+\n+\t\tif (!strncmp(line, cherry_picked_prefix, prefix_len)) {\n+\t\t\tstruct object_id from_oid;\n+\t\t\tstruct object *from_object;\n+\t\t\tstruct commit *from_commit;\n+\t\t\tstruct object_array *from_cps;\n+\t\t\tchar cherry_hex[GIT_MAX_HEXSZ + 1];\n+\n+\t\t\tif (get_oid_hex(line + prefix_len, &from_oid))\n+\t\t\t\tcontinue;\n+\n+\t\t\tfrom_object = parse_object(the_repository, &from_oid);\n+\t\t\tif (!from_object || from_object->type != OBJ_COMMIT)\n+\t\t\t\tcontinue;\n+\n+\t\t\tfrom_commit = (struct commit *)from_object;\n+\t\t\tfrom_cps = get_create_commit_cherry_picks(from_commit);\n+\n+\t\t\toid_to_hex_r(cherry_hex, &commit->object.oid);\n+\n+\t\t\tif (!object_array_contains_name(from_cps, cherry_hex))\n+\t\t\t\tadd_object_array(&commit->object, cherry_hex,\n+\t\t\t\t\t\t from_cps);\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\tfree(buffer);\n+}\n+\n+static void clear_cherry_pick_note(struct commit *commit, void *prefix)\n+{\n+\tstruct argv_array args;\n+\n+\targv_array_init(&args);\n+\targv_array_pushl(&args, \"notes\", \"--ref\", \"cherry-picks\", \"remove\",\n+\t\t\t \"--ignore-missing\",\n+\t\t\t oid_to_hex(&commit->object.oid), NULL);\n+\tcmd_notes(args.argc, args.argv, prefix);\n+}\n+\n+static int note_cherry_picks(struct commit *commit, const char *prefix)\n+{\n+\tchar from_hex[GIT_MAX_HEXSZ + 1];\n+\tstruct strbuf note = STRBUF_INIT;\n+\tstruct argv_array args;\n+\tstruct object_array *cps;\n+\tint i, ret;\n+\n+\tcps = get_commit_cherry_picks(commit);\n+\tif (!cps)\n+\t\treturn 0;\n+\n+\toid_to_hex_r(from_hex, &commit->object.oid);\n+\n+\tfor (i = 0; i < cps->nr; i++) {\n+\t\tconst char *cherry_hex = cps->objects[i].name;\n+\n+\t\tstrbuf_addf(&note, \"%s%s\\n\", NOTES_CHERRY_PICKED_TO, cherry_hex);\n+\t\tif (verbose)\n+\t\t\tfprintf(stderr, \"Write note %s -> %s\\n\",\n+\t\t\t\tfrom_hex, cherry_hex);\n+\t}\n+\n+\targv_array_init(&args);\n+\targv_array_pushl(&args, \"notes\", \"--ref\", \"cherry-picks\", \"add\",\n+\t\t\t \"--force\", \"--message\", note.buf, from_hex, NULL);\n+\tif (!verbose)\n+\t\targv_array_push(&args, \"--quiet\");\n+\tret = cmd_notes(args.argc, args.argv, prefix);\n+\tstrbuf_release(&note);\n+\treturn ret;\n+}\n+\n+int cmd_note_cherry_picks(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct rev_info revs;\n+\tint i, ret;\n+\tstruct setup_revision_opt s_r_opt = {\n+\t\t.def = \"HEAD\",\n+\t\t.revarg_opt = REVARG_CANNOT_BE_FILENAME\n+\t};\n+\tstruct option options[] = {\n+\t\tOPT_BOOL(0, \"clear\", &clear, N_(\"clear cherry-pick notes from the specified commits\")),\n+\t\tOPT__VERBOSE(&verbose, N_(\"verbose\")),\n+\t\tOPT_END()\n+\t};\n+\n+\tgit_config(git_default_config, NULL);\n+\n+\tinit_revisions(&revs, prefix);\n+\targc = setup_revisions(argc, argv, &revs, &s_r_opt);\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     note_cherry_picks_usage, 0);\n+\tif (argc > 1)\n+\t\tdie(_(\"unrecognized argument: %s\"), argv[1]);\n+\n+\tif (prepare_revision_walk(&revs))\n+\t\tdie(\"revision walk setup failed\");\n+\n+\tif (clear) {\n+\t\ttraverse_commit_list(&revs, clear_cherry_pick_note, NULL,\n+\t\t\t\t     (void *)prefix);\n+\t\treturn 0;\n+\t}\n+\n+\tinit_commit_cherry_picks(&cherry_picks);\n+\ttraverse_commit_list(&revs, record_cherry_pick, NULL, NULL);\n+\n+\tobject_array_remove_duplicates(&cherry_picked);\n+\n+\tfor (i = 0; i < cherry_picked.nr; i++) {\n+\t\tret = note_cherry_picks((void *)cherry_picked.objects[i].item,\n+\t\t\t\t\tprefix);\n+\t\tif (ret)\n+\t\t\treturn ret;\n+\t}\n+\n+\treturn 0;\n+}\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex c05cd004a..6b623b25c 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -26,7 +26,7 @@\n \n static const char * const git_notes_usage[] = {\n \tN_(\"git notes [--ref <notes-ref>] [list [<object>]]\"),\n-\tN_(\"git notes [--ref <notes-ref>] add [-f] [--allow-empty] [-m <msg> | -F <file> | (-c | -C) <object>] [<object>]\"),\n+\tN_(\"git notes [--ref <notes-ref>] add [-f] [-q] [--allow-empty] [-m <msg> | -F <file> | (-c | -C) <object>] [<object>]\"),\n \tN_(\"git notes [--ref <notes-ref>] copy [-f] <from-object> <to-object>\"),\n \tN_(\"git notes [--ref <notes-ref>] append [--allow-empty] [-m <msg> | -F <file> | (-c | -C) <object>] [<object>]\"),\n \tN_(\"git notes [--ref <notes-ref>] edit [--allow-empty] [<object>]\"),\n@@ -394,7 +394,7 @@ static int append_edit(int argc, const char **argv, const char *prefix);\n \n static int add(int argc, const char **argv, const char *prefix)\n {\n-\tint force = 0, allow_empty = 0;\n+\tint force = 0, quiet = 0, allow_empty = 0;\n \tconst char *object_ref;\n \tstruct notes_tree *t;\n \tstruct object_id object, new_note;\n@@ -416,6 +416,7 @@ static int add(int argc, const char **argv, const char *prefix)\n \t\tOPT_BOOL(0, \"allow-empty\", &allow_empty,\n \t\t\tN_(\"allow storing empty note\")),\n \t\tOPT__FORCE(&force, N_(\"replace existing notes\"), PARSE_OPT_NOCOMPLETE),\n+\t\tOPT__QUIET(&quiet, N_(\"suppress informational messages\")),\n \t\tOPT_END()\n \t};\n \n@@ -455,8 +456,9 @@ static int add(int argc, const char **argv, const char *prefix)\n \t\t\targv[0] = \"edit\";\n \t\t\treturn append_edit(argc, argv, prefix);\n \t\t}\n-\t\tfprintf(stderr, _(\"Overwriting existing notes for object %s\\n\"),\n-\t\t\toid_to_hex(&object));\n+\t\tif (!quiet)\n+\t\t\tfprintf(stderr, _(\"Overwriting existing notes for object %s\\n\"),\n+\t\t\t\toid_to_hex(&object));\n \t}\n \n \tprepare_note_data(&object, &d, note);\n@@ -466,8 +468,9 @@ static int add(int argc, const char **argv, const char *prefix)\n \t\t\tBUG(\"combine_notes_overwrite failed\");\n \t\tcommit_notes(t, \"Notes added by 'git notes add'\");\n \t} else {\n-\t\tfprintf(stderr, _(\"Removing note for object %s\\n\"),\n-\t\t\toid_to_hex(&object));\n+\t\tif (!quiet)\n+\t\t\tfprintf(stderr, _(\"Removing note for object %s\\n\"),\n+\t\t\t\toid_to_hex(&object));\n \t\tremove_note(t, object.hash);\n \t\tcommit_notes(t, \"Notes removed by 'git notes add'\");\n \t}\n@@ -898,7 +901,7 @@ static int remove_one_note(struct notes_tree *t, const char *name, unsigned flag\n static int remove_cmd(int argc, const char **argv, const char *prefix)\n {\n \tunsigned flag = 0;\n-\tint from_stdin = 0;\n+\tint from_stdin = 0, quiet = 0;\n \tstruct option options[] = {\n \t\tOPT_BIT(0, \"ignore-missing\", &flag,\n \t\t\tN_(\"attempt to remove non-existent note is not an error\"),\ndiff --git a/git.c b/git.c\nindex a6f4b44af..aedafff02 100644\n--- a/git.c\n+++ b/git.c\n@@ -512,6 +512,7 @@ static struct cmd_struct commands[] = {\n \t{ \"mv\", cmd_mv, RUN_SETUP | NEED_WORK_TREE },\n \t{ \"name-rev\", cmd_name_rev, RUN_SETUP },\n \t{ \"notes\", cmd_notes, RUN_SETUP },\n+\t{ \"note-cherry-picks\", cmd_note_cherry_picks, RUN_SETUP },\n \t{ \"pack-objects\", cmd_pack_objects, RUN_SETUP },\n \t{ \"pack-redundant\", cmd_pack_redundant, RUN_SETUP | NO_PARSEOPT },\n \t{ \"pack-refs\", cmd_pack_refs, RUN_SETUP },\ndiff --git a/notes.c b/notes.c\nindex 25cdce28b..19fa3451d 100644\n--- a/notes.c\n+++ b/notes.c\n@@ -1189,6 +1189,26 @@ void free_notes(struct notes_tree *t)\n \tmemset(t, 0, sizeof(struct notes_tree));\n }\n \n+static void walk_cherry_picks(struct object_id *from_oid, struct strbuf *sb,\n+\t\t\t      int level)\n+{\n+\tstruct object_array cps = OBJECT_ARRAY_INIT;\n+\tint i;\n+\n+\tread_cherry_picks_note(from_oid, &cps);\n+\n+\tfor (i = 0; i < cps.nr; i++) {\n+\t\tstrbuf_addf(sb, \"    %s%*s%s\\n\",\n+\t\t\t    NOTES_CHERRY_PICKED_TO, 2 * level, \"\",\n+\t\t\t    cps.objects[i].name);\n+\t\tif (cps.objects[i].item)\n+\t\t\twalk_cherry_picks(&cps.objects[i].item->oid, sb,\n+\t\t\t\t\t  level + 1);\n+\t}\n+\n+\tobject_array_clear(&cps);\n+}\n+\n /*\n  * Fill the given strbuf with the notes associated with the given object.\n  *\n@@ -1208,6 +1228,7 @@ static void format_note(struct notes_tree *t, const struct object_id *object_oid\n \tchar *msg, *msg_p;\n \tunsigned long linelen, msglen;\n \tenum object_type type;\n+\tint format_cherry_picks;\n \n \tif (!t)\n \t\tt = &default_notes_tree;\n@@ -1250,6 +1271,8 @@ static void format_note(struct notes_tree *t, const struct object_id *object_oid\n \t\t}\n \t}\n \n+\tformat_cherry_picks = !raw && !strcmp(t->ref, \"refs/notes/cherry-picks\");\n+\n \tfor (msg_p = msg; msg_p < msg + msglen; msg_p += linelen + 1) {\n \t\tlinelen = strchrnul(msg_p, '\\n') - msg_p;\n \n@@ -1257,6 +1280,17 @@ static void format_note(struct notes_tree *t, const struct object_id *object_oid\n \t\t\tstrbuf_addstr(sb, \"    \");\n \t\tstrbuf_add(sb, msg_p, linelen);\n \t\tstrbuf_addch(sb, '\\n');\n+\n+\t\tif (format_cherry_picks &&\n+\t\t    starts_with(msg_p, NOTES_CHERRY_PICKED_TO)) {\n+\t\t\tstruct object_id oid;\n+\n+\t\t\tif (get_oid_hex(msg_p + strlen(NOTES_CHERRY_PICKED_TO),\n+\t\t\t\t\t&oid))\n+\t\t\t\tcontinue;\n+\n+\t\t\twalk_cherry_picks(&oid, sb, 1);\n+\t\t}\n \t}\n \n \tfree(msg);\n@@ -1309,3 +1343,64 @@ void expand_loose_notes_ref(struct strbuf *sb)\n \t\texpand_notes_ref(sb);\n \t}\n }\n+\n+void read_cherry_picks_note(const struct object_id *commit_oid,\n+\t\t\t    struct object_array *result)\n+{\n+\tstatic struct notes_tree notes_tree;\n+\tconst struct object_id *note_oid;\n+\tunsigned long size;\n+\tenum object_type type;\n+\tchar *note;\n+\tstruct strbuf **lines, **pline;\n+\n+\tif (!notes_tree.initialized)\n+\t\tinit_notes(&notes_tree, NOTES_CHERRY_PICKS_REF, NULL, 0);\n+\n+\tnote_oid = get_note(&notes_tree, commit_oid);\n+\tif (!note_oid)\n+\t\treturn;\n+\n+\tnote = read_object_file(note_oid, &type, &size);\n+\tif (!size) {\n+\t\tfree(note);\n+\t\treturn;\n+\t}\n+\n+\tlines = strbuf_split_buf(note, size, '\\n', 0);\n+\n+\tfor (pline = lines; *pline; pline++) {\n+\t\tstruct strbuf *line = *pline;\n+\t\tconst char *cherry_hex;\n+\t\tstruct object_id cherry_oid;\n+\t\tstruct object *cherry_obj;\n+\n+\t\tstrbuf_trim(line);\n+\n+\t\tif (!starts_with(line->buf, NOTES_CHERRY_PICKED_TO)) {\n+\t\t\twarning(\"read invalid cherry-pick note on %s: %s\",\n+\t\t\t\toid_to_hex(commit_oid), line->buf);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tcherry_hex = line->buf + strlen(NOTES_CHERRY_PICKED_TO);\n+\n+\t\tif (get_oid_hex(cherry_hex, &cherry_oid)) {\n+\t\t\twarning(\"read invalid cherry-pick sha1 on %s: %s\",\n+\t\t\t\toid_to_hex(commit_oid), line->buf);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tcherry_obj = parse_object(the_repository, &cherry_oid);\n+\t\tif (!cherry_obj || cherry_obj->type != OBJ_COMMIT) {\n+\t\t\twarning(\"read invalid cherry-pick commit on %s: %s\",\n+\t\t\t\toid_to_hex(commit_oid), line->buf);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tadd_object_array(cherry_obj, cherry_hex, result);\n+\t}\n+\n+\tstrbuf_list_free(lines);\n+\tfree(note);\n+}\ndiff --git a/notes.h b/notes.h\nindex 414bc6855..b58899c74 100644\n--- a/notes.h\n+++ b/notes.h\n@@ -2,10 +2,14 @@\n #define NOTES_H\n \n #include \"string-list.h\"\n+#include \"object.h\"\n \n struct object_id;\n struct strbuf;\n \n+#define NOTES_CHERRY_PICKS_REF\t\t\"refs/notes/cherry-picks\"\n+#define NOTES_CHERRY_PICKED_TO\t\t\"Cherry-picked-to: \"\n+\n /*\n  * Function type for combining two notes annotating the same object.\n  *\n@@ -317,4 +321,7 @@ void expand_notes_ref(struct strbuf *sb);\n  */\n void expand_loose_notes_ref(struct strbuf *sb);\n \n+void read_cherry_picks_note(const struct object_id *commit_oid,\n+\t\t\t    struct object_array *result);\n+\n #endif\ndiff --git a/object.c b/object.c\nindex e54160550..f79652a34 100644\n--- a/object.c\n+++ b/object.c\n@@ -404,7 +404,7 @@ void object_array_clear(struct object_array *array)\n /*\n  * Return true iff array already contains an entry with name.\n  */\n-static int contains_name(struct object_array *array, const char *name)\n+int object_array_contains_name(struct object_array *array, const char *name)\n {\n \tunsigned nr = array->nr, i;\n \tstruct object_array_entry *object = array->objects;\n@@ -422,7 +422,7 @@ void object_array_remove_duplicates(struct object_array *array)\n \n \tarray->nr = 0;\n \tfor (src = 0; src < nr; src++) {\n-\t\tif (!contains_name(array, objects[src].name)) {\n+\t\tif (!object_array_contains_name(array, objects[src].name)) {\n \t\t\tif (src != array->nr)\n \t\t\t\tobjects[array->nr] = objects[src];\n \t\t\tarray->nr++;\ndiff --git a/object.h b/object.h\nindex 0feb90ae6..ee14ce595 100644\n--- a/object.h\n+++ b/object.h\n@@ -172,6 +172,12 @@ typedef int (*object_array_each_func_t)(struct object_array_entry *, void *);\n void object_array_filter(struct object_array *array,\n \t\t\t object_array_each_func_t want, void *cb_data);\n \n+/*\n+ * Returns 1 if array already contains an entry with the specified name.\n+ * Otherwise, 0.\n+ */\n+int object_array_contains_name(struct object_array *array, const char *name);\n+\n /*\n  * Remove from array all but the first entry with a given name.\n  * Warning: this function uses an O(N^2) algorithm.\n"},{"id":"361408","messageId":"20181024122401.GD4182586@devbig004.ftw2.facebook.com","threadId":"49592","inReplyTo":"20181017143921.GR270328@devbig004.ftw2.facebook.com","subject":"Re: [RFC] cherry-pick notes to find out cherry-picks from the origin","fromName":"Tejun Heo","fromEmail":"tj@kernel.org","sentAt":"2018-10-24T12:24:01Z","receivedAt":"2018-10-24T12:24:10Z","isPatch":false,"sender":{"key":"tj@kernel.org","avatar":null},"body":"Ping, thanks.\n\n-- \ntejun\n"},{"id":"363182","messageId":"20181113180049.GL2509588@devbig004.ftw2.facebook.com","threadId":"49592","inReplyTo":"20181024122401.GD4182586@devbig004.ftw2.facebook.com","subject":"Re: [RFC] cherry-pick notes to find out cherry-picks from the origin","fromName":"Tejun Heo","fromEmail":"tj@kernel.org","sentAt":"2018-11-13T18:00:49Z","receivedAt":"2018-11-13T18:01:00Z","isPatch":false,"sender":{"key":"tj@kernel.org","avatar":null},"body":"On Wed, Oct 24, 2018 at 05:24:01AM -0700, Tejun Heo wrote:\n> Ping, thanks.\n\nPing again.  Any comments?  Wasn't this the direction you guys were\nsuggesting?\n\nThanks.\n\n-- \ntejun\n"},{"id":"363435","messageId":"20181115144044.GA16450@sigill.intra.peff.net","threadId":"49592","inReplyTo":"20181017143921.GR270328@devbig004.ftw2.facebook.com","subject":"Re: [RFC] cherry-pick notes to find out cherry-picks from the origin","fromName":"Jeff King","fromEmail":"peff@peff.net","sentAt":"2018-11-15T14:40:44Z","receivedAt":"2018-11-15T14:40:49Z","isPatch":false,"sender":{"key":"peff@peff.net","avatar":"https://avatars.githubusercontent.com/u/45925?v=4"},"body":"On Wed, Oct 17, 2018 at 07:39:21AM -0700, Tejun Heo wrote:\n\n> A while ago, I proposed changes to name-rev and describe so that they\n> can identify the commits cherry-picked from the one which is being\n> shown.\n> \n>   https://public-inbox.org/git/20180726153714.GX1934745@devbig577.frc2.facebook.com/T/\n> \n> While the use-cases - e.g. tracking down which release / stable\n> branches a given commit ended up in - weren't controversial, it was\n> suggested that it'd make more sense to use notes to link cherry-picks\n> instead of building the feature into name-rev.\n\nSorry for the slow reply. This was on my to-look-at pile, but for\nsome reason I accidentally put in my done pile.\n\n> The patch appended to this message implements most of it (sans tests\n> and documentation).  It's composed of the following two parts.\n> \n> * A new built-in command note-cherry-picks, which walks the specified\n>   commits and if they're marked with the cherry-pick trailer, adds the\n>   backlink to the origin commit using Cherry-picked-to tag in a\n>   cherry-picks note.\n\nThat makes sense. I think this could also be an option to cherry-pick,\nto instruct it to create the note when the cherry-pick is made.\n\nBut you may still want a command to backfill older cherry-picks, or\nthose done by other people who do not care themselves about maintaining\nthe notes tree.\n\nIt _feels_ like this is something that should be do-able by plugging a\nfew commands together, rather than writing a new C program. I.e.,\nsomething like:\n\n  git rev-list --format='%(trailers)' HEAD |\n  perl -lne '\n\t/^commit ([0-9]+)/ and $commit = $1;\n\t/^\\(cherry picked from commit ([0-9]+)/\n\t\tand print \"$commit $1\";\n  ' |\n  while read from to; do\n\t# One process per note isn't very efficient. Ideally there would\n\t# be an \"append --stdin\" mode. Double points if it understands\n\t# how to avoid adding existing lines.\n\tgit notes append -m \"Cherry-picked-to: $to\" $from\n  done\n\nwhich is roughly what your program is doing.  Not that I'm entirely\nopposed to doing something in C (we've been moving away from shell\nscripts anyway). But mostly I am wondering if we can leverage existing\ntools, and fill in their gaps in a way that lets people easily do\nsimilar things.\n\nAnd on that note...\n\n> * When formatting a cherry-picks note for display, nested cherry-picks\n>   are followed from each Cherry-picked-to tag and printed out with\n>   matching indentations.\n\nThat makes sense to me, but does this have to be strictly related to\ncherry-picks? I.e., in the more generic form, could we have a way of\nmarking a note as \"transitive\" for display, and the notes-display code\nwould automatically recognize and walk hashes?\n\nThat would serve your purpose, but would also allow similar things to\neasily be done in the future.\n\n> Combined with name-rev --stdin, it can produce outputs like the following.\n> [...]\n\nYeah, that looks pretty good.\n\n> Locally, the notes can be kept up-to-date with a trivial post-commit\n> hook which invokes note-cherry-picks on the new commit; however, I'm\n> having a bit of trouble figuring out a way to keep it up-to-date when\n> multiple trees are involved.  AFAICS, there are two options.\n> \n> 1. Ensuring that the notes are always generated on local commits and\n>    whenever new commits are received through fetch/pulls.\n> \n> 2. Ensuring that the notes are always generated on local commits and\n>    transported with push/pulls.\n> \n> 3. A hybrid approach - also generate notes on the receiving end and\n>    ensure that fetch/pulls receives the notes together (ie. similar to\n>    --tags option to git-fetch).\n> \n> #1 seems simpler and more robust to me.  Unfortunately, I can't see a\n> way to implement any of the three options with the existing hooks.\n> For #1, there's no post-fetch hook.  For #2 and #3, there doesn't seem\n> to be a fool-proof way to make sure that the notes are transported\n> together.  Any suggestions would be greatly appreciated.\n\nYeah, I think (1) is the simplest: it becomes a purely local thing that\nyou've generated these annotations. Unfortunately, no, I don't think\nthere's anything like a post-fetch hook. This might be a good reason to\nhave one. One can always do \"git fetch && update-notes\" of course, but\nhaving fetch feed the script the set of updated ref tips would be very\nhelpful (so you know you can traverse from $old..$new looking for\ncherry-picks).\n\nFor (2) and (3), you can push/pull a notes tree, but setting up the\nrefspecs and config is fairly manual. Using the cherry-pick option I\nsuggested above would most locally-made picks, but you'd probably still\nneed to backfill sometimes anyway.\n\nI don't think you'd need to be (nor am I sure you _could_ be) as fancy\nas fetching notes and their respective commits together. The notes are\nbound together in a single tree, so you cannot say \"I don't have commit\n1234abcd, I don't need the note for it\". You get the whole tree. And\nthat is not such a bad thing. The big reason _not_ to fetch tags that\npoint to commits we don't have is that the tag fetch implies\nreachability, which may mean sucking in a big chunk of history. Whereas\nyou can happily have a note pointing to a commit you don't have (though\nI suppose you should avoid running \"git notes prune\" in that case).\n\n> ---\n>  Makefile                    |    1 \n>  builtin.h                   |    1 \n>  builtin/note-cherry-picks.c |  197 ++++++++++++++++++++++++++++++++++++++++++++\n>  builtin/notes.c             |   17 ++-\n>  git.c                       |    1 \n>  notes.c                     |   95 +++++++++++++++++++++\n>  notes.h                     |    7 +\n>  object.c                    |    4 \n>  object.h                    |    6 +\n>  9 files changed, 320 insertions(+), 9 deletions(-)\n\nI only looked briefly over your implementation, but didn't see anything\nobviously wrong. I do think it would be nice to make it more generic, as\nmuch as possible. I think the most generic form is really:\n\n  traverse-and-show-trailers | invert-trailers | add-notes\n\nIn theory I should be able to do the same inversion step on any trailer\nwhich mentions another commit.\n\nIf it is going to stay in C and be cherry-pick-specific, one obvious\nimprovement would be to use the notes API directly, rather than spawning\nsubprocesses. That should be much more efficient if you have a lot of\nnotes to write.\n\n-Peff\n"},{"id":"364631","messageId":"20181205161103.GO2509588@devbig004.ftw2.facebook.com","threadId":"49592","inReplyTo":"20181115144044.GA16450@sigill.intra.peff.net","subject":"Re: [RFC] cherry-pick notes to find out cherry-picks from the origin","fromName":"Tejun Heo","fromEmail":"tj@kernel.org","sentAt":"2018-12-05T16:20:31Z","receivedAt":"2018-12-05T16:20:38Z","isPatch":false,"sender":{"key":"tj@kernel.org","avatar":null},"body":"Hello, Jeff.\n\nOn Thu, Nov 15, 2018 at 09:40:44AM -0500, Jeff King wrote:\n> Sorry for the slow reply. This was on my to-look-at pile, but for\n> some reason I accidentally put in my done pile.\n\nNo worries and sorry about my late reply too.  Things were a bit\nhectic.\n\n> > * A new built-in command note-cherry-picks, which walks the specified\n> >   commits and if they're marked with the cherry-pick trailer, adds the\n> >   backlink to the origin commit using Cherry-picked-to tag in a\n> >   cherry-picks note.\n> \n> That makes sense. I think this could also be an option to cherry-pick,\n> to instruct it to create the note when the cherry-pick is made.\n> \n> But you may still want a command to backfill older cherry-picks, or\n> those done by other people who do not care themselves about maintaining\n> the notes tree.\n\nSo, I wanted to do both with the same command.  git-cherry-pick knows\nwhich commits are new, so it can just pass those commits to\nnote-cherry-picks.  When backfilling the whole tree or newly pulled\ncommits, the appropriate command can invoke note-cherry-picks with the\nnew commits which should be super cheap.\n\n> It _feels_ like this is something that should be do-able by plugging a\n> few commands together, rather than writing a new C program. I.e.,\n> something like:\n> \n>   git rev-list --format='%(trailers)' HEAD |\n>   perl -lne '\n> \t/^commit ([0-9]+)/ and $commit = $1;\n> \t/^\\(cherry picked from commit ([0-9]+)/\n> \t\tand print \"$commit $1\";\n>   ' |\n>   while read from to; do\n> \t# One process per note isn't very efficient. Ideally there would\n> \t# be an \"append --stdin\" mode. Double points if it understands\n> \t# how to avoid adding existing lines.\n> \tgit notes append -m \"Cherry-picked-to: $to\" $from\n>   done\n> \n> which is roughly what your program is doing.  Not that I'm entirely\n> opposed to doing something in C (we've been moving away from shell\n> scripts anyway). But mostly I am wondering if we can leverage existing\n\nBut the above wouldn't clean up stale commits, which could happen with\ne.g. abandoned releases, and would be prone to creating duplicates.\nWe sure can add all those to shell / perl scripts but it's difficult\nfor me to see the upsides of doing it that way.\n\n> tools, and fill in their gaps in a way that lets people easily do\n> similar things.\n\nI'll respond to this together below.\n\n> And on that note...\n> \n> > * When formatting a cherry-picks note for display, nested cherry-picks\n> >   are followed from each Cherry-picked-to tag and printed out with\n> >   matching indentations.\n> \n> That makes sense to me, but does this have to be strictly related to\n> cherry-picks? I.e., in the more generic form, could we have a way of\n> marking a note as \"transitive\" for display, and the notes-display code\n> would automatically recognize and walk hashes?\n> \n> That would serve your purpose, but would also allow similar things to\n> easily be done in the future.\n\nBelow.\n\n> > Combined with name-rev --stdin, it can produce outputs like the following.\n> > [...]\n> \n> Yeah, that looks pretty good.\n> \n> > Locally, the notes can be kept up-to-date with a trivial post-commit\n> > hook which invokes note-cherry-picks on the new commit; however, I'm\n> > having a bit of trouble figuring out a way to keep it up-to-date when\n> > multiple trees are involved.  AFAICS, there are two options.\n> > \n> > 1. Ensuring that the notes are always generated on local commits and\n> >    whenever new commits are received through fetch/pulls.\n> > \n> > 2. Ensuring that the notes are always generated on local commits and\n> >    transported with push/pulls.\n> > \n> > 3. A hybrid approach - also generate notes on the receiving end and\n> >    ensure that fetch/pulls receives the notes together (ie. similar to\n> >    --tags option to git-fetch).\n> > \n> > #1 seems simpler and more robust to me.  Unfortunately, I can't see a\n> > way to implement any of the three options with the existing hooks.\n> > For #1, there's no post-fetch hook.  For #2 and #3, there doesn't seem\n> > to be a fool-proof way to make sure that the notes are transported\n> > together.  Any suggestions would be greatly appreciated.\n> \n> Yeah, I think (1) is the simplest: it becomes a purely local thing that\n> you've generated these annotations. Unfortunately, no, I don't think\n> there's anything like a post-fetch hook. This might be a good reason to\n> have one. One can always do \"git fetch && update-notes\" of course, but\n> having fetch feed the script the set of updated ref tips would be very\n> helpful (so you know you can traverse from $old..$new looking for\n> cherry-picks).\n\nThis sounds great to me.\n\n> I only looked briefly over your implementation, but didn't see anything\n> obviously wrong. I do think it would be nice to make it more generic, as\n> much as possible. I think the most generic form is really:\n> \n>   traverse-and-show-trailers | invert-trailers | add-notes\n> \n> In theory I should be able to do the same inversion step on any trailer\n> which mentions another commit.\n\nHmm... yeah, I can definitely separate out the first part into a\nseparate function, move dedup of notes to add-notes, and then glue\nthem together.\n\nI'm a bit hesitant to expose the first part as a separate command\nwithout knowing what other use cases would actually look like.  I'll\nmake it so that it's easy to do so when a new use case arises.\n\n> If it is going to stay in C and be cherry-pick-specific, one obvious\n> improvement would be to use the notes API directly, rather than spawning\n> subprocesses. That should be much more efficient if you have a lot of\n> notes to write.\n\nI'll try to make the components more generic and then glue them\ntogether in C and make it use the APIs directly.\n\nThanks.\n\n-- \ntejun\n"},{"id":"364712","messageId":"20181206221526.GP2509588@devbig004.ftw2.facebook.com","threadId":"49592","inReplyTo":"20181205161103.GO2509588@devbig004.ftw2.facebook.com","subject":"Re: [RFC] cherry-pick notes to find out cherry-picks from the origin","fromName":"Tejun Heo","fromEmail":"tj@kernel.org","sentAt":"2018-12-06T22:15:26Z","receivedAt":"2018-12-06T22:15:33Z","isPatch":false,"sender":{"key":"tj@kernel.org","avatar":null},"body":"Hello, Jeff.\n\nSo, this is what I currently have.  It still does the same thing but a\nlot more generic in terms of both interface and implementation.\n\n* All core logics are implemented as core helpers / features.\n\n  * Trailer parsing and reverse-mapping in trailer_rev_xrefs_*().\n\n  * Note refs which start with xref- (cross-reference) are recognized\n    by notes core.  When notes are added, a dedicated combine_notes\n    function is used to remove duplicates and curl unreachable\n    commits.  When xref- notes are formatted for printing, it\n    automatically follows and prints nested xrefs.\n\n* note-cherry-picks is replaced with reverse-trailer-xrefs which can\n  use other trailers, note refs and tags.  --xref-cherry-picks option\n  makes it use the cherry-pick presets.\n\nPlease note that the patch is still a bit rough.  I'm polishing and\ndocumenting.  Please let me know what you think.\n\nThanks.\n\n---\n Makefile                        |    1 \n builtin.h                       |    1 \n builtin/reverse-trailer-xrefs.c |  148 ++++++++++++++++++++++++\n git.c                           |    1 \n notes.c                         |  245 +++++++++++++++++++++++++++++++++++++++-\n notes.h                         |   10 +\n object.c                        |    4 \n object.h                        |    6 \n trailer.c                       |  102 ++++++++++++++++\n trailer.h                       |   26 ++++\n 10 files changed, 540 insertions(+), 4 deletions(-)\n\ndiff --git a/Makefile b/Makefile\nindex 1a44c811a..3c23ecf9d 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -1086,6 +1086,7 @@ BUILTIN_OBJS += builtin/multi-pack-index.o\n BUILTIN_OBJS += builtin/mv.o\n BUILTIN_OBJS += builtin/name-rev.o\n BUILTIN_OBJS += builtin/notes.o\n+BUILTIN_OBJS += builtin/reverse-trailer-xrefs.o\n BUILTIN_OBJS += builtin/pack-objects.o\n BUILTIN_OBJS += builtin/pack-redundant.o\n BUILTIN_OBJS += builtin/pack-refs.o\ndiff --git a/builtin.h b/builtin.h\nindex 6538932e9..51089e258 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -195,6 +195,7 @@ extern int cmd_multi_pack_index(int argc, const char **argv, const char *prefix)\n extern int cmd_mv(int argc, const char **argv, const char *prefix);\n extern int cmd_name_rev(int argc, const char **argv, const char *prefix);\n extern int cmd_notes(int argc, const char **argv, const char *prefix);\n+extern int cmd_reverse_trailer_xrefs(int argc, const char **argv, const char *prefix);\n extern int cmd_pack_objects(int argc, const char **argv, const char *prefix);\n extern int cmd_pack_redundant(int argc, const char **argv, const char *prefix);\n extern int cmd_patch_id(int argc, const char **argv, const char *prefix);\ndiff --git a/builtin/reverse-trailer-xrefs.c b/builtin/reverse-trailer-xrefs.c\nnew file mode 100644\nindex 000000000..b2879be6c\n--- /dev/null\n+++ b/builtin/reverse-trailer-xrefs.c\n@@ -0,0 +1,148 @@\n+#include \"builtin.h\"\n+#include \"cache.h\"\n+#include \"strbuf.h\"\n+#include \"repository.h\"\n+#include \"config.h\"\n+#include \"commit.h\"\n+#include \"blob.h\"\n+#include \"notes.h\"\n+#include \"notes-utils.h\"\n+#include \"trailer.h\"\n+#include \"revision.h\"\n+#include \"list-objects.h\"\n+#include \"object-store.h\"\n+#include \"parse-options.h\"\n+\n+static const char * const reverse_trailer_xrefs_usage[] = {\n+\tN_(\"git reverse_trailer_xrefs [<options>] [<commit-ish>...]\"),\n+\tNULL\n+};\n+\n+static const char cherry_picked_prefix[] = \"(cherry picked from commit \";\n+static int verbose;\n+\n+static void clear_trailer_xref_note(struct commit *commit, void *data)\n+{\n+\tstruct notes_tree *tree = data;\n+\tint status;\n+\n+\tstatus = remove_note(tree, commit->object.oid.hash);\n+\n+\tif (verbose) {\n+\t\tif (status)\n+\t\t\tfprintf(stderr, \"Object %s has no note\\n\",\n+\t\t\t\toid_to_hex(&commit->object.oid));\n+\t\telse\n+\t\t\tfprintf(stderr, \"Removing note for object %s\\n\",\n+\t\t\t\toid_to_hex(&commit->object.oid));\n+\t}\n+}\n+\n+static void record_trailer_xrefs(struct commit *commit, void *data)\n+{\n+\ttrailer_rev_xrefs_record(data, commit);\n+}\n+\n+static int note_trailer_xrefs(struct notes_tree *tree,\n+\t\t\t      struct commit *from_commit, struct object_array *to_objs,\n+\t\t\t      const char *tag)\n+{\n+\tchar from_hex[GIT_MAX_HEXSZ + 1];\n+\tstruct strbuf note = STRBUF_INIT;\n+\tstruct object_id note_oid;\n+\tint i, ret;\n+\n+\toid_to_hex_r(from_hex, &from_commit->object.oid);\n+\n+\tfor (i = 0; i < to_objs->nr; i++) {\n+\t\tconst char *hex = to_objs->objects[i].name;\n+\n+\t\tif (tag)\n+\t\t\tstrbuf_addf(&note, \"%s: %s\\n\", tag, hex);\n+\t\telse\n+\t\t\tstrbuf_addf(&note, \"%s\\n\", tag);\n+\t\tif (verbose)\n+\t\t\tfprintf(stderr, \"Adding note %s -> %s\\n\", from_hex, hex);\n+\t}\n+\n+\tret = write_object_file(note.buf, note.len, blob_type, &note_oid);\n+\tstrbuf_release(&note);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tret = add_note(tree, &from_commit->object.oid, &note_oid, NULL);\n+\treturn ret;\n+}\n+\n+int cmd_reverse_trailer_xrefs(int argc, const char **argv, const char *prefix)\n+{\n+\tstatic struct notes_tree tree;\n+\tstruct rev_info revs;\n+\tint i, ret;\n+\tstruct setup_revision_opt s_r_opt = {\n+\t\t.def = \"HEAD\",\n+\t\t.revarg_opt = REVARG_CANNOT_BE_FILENAME\n+\t};\n+\tint cherry = 0, clear = 0;\n+\tconst char *trailer_prefix = NULL, *notes_ref = NULL, *tag = NULL;\n+\tstruct option options[] = {\n+\t\tOPT_BOOL(0, \"xref-cherry-picks\", &cherry, N_(\"use options for xref-cherry-picks notes\")),\n+\t\tOPT_STRING(0, \"trailer-prefix\", &trailer_prefix, N_(\"prefix\"), N_(\"process trailers starting with <prefix>\")),\n+\t\tOPT_STRING(0, \"ref\", &notes_ref, N_(\"notes-ref\"), N_(\"update notes in <notes-ref>\")),\n+\t\tOPT_STRING(0, \"tag\", &tag, N_(\"tag\"), N_(\"tag xref notes with <tag>\")),\n+\t\tOPT_BOOL(0, \"clear\", &clear, N_(\"clear trailer xref notes from the specified commits\")),\n+\t\tOPT__VERBOSE(&verbose, N_(\"verbose\")),\n+\t\tOPT_END()\n+\t};\n+\n+\tgit_config(git_default_config, NULL);\n+\n+\tinit_revisions(&revs, prefix);\n+\targc = setup_revisions(argc, argv, &revs, &s_r_opt);\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     reverse_trailer_xrefs_usage, 0);\n+\n+\t/* allow inidividual options to override parts of --cherry */\n+\tif (cherry) {\n+\t\tif (!trailer_prefix)\n+\t\t\ttrailer_prefix = cherry_picked_prefix;\n+\t\tif (!notes_ref)\n+\t\t\tnotes_ref = NOTES_CHERRY_PICKS_REF;\n+\t\tif (!tag)\n+\t\t\ttag = NOTES_CHERRY_PICKED_TO_TAG;\n+\t}\n+\n+\tif (!notes_ref || (!clear && (!trailer_prefix || !tag)))\n+\t\tdie(_(\"insufficient arguments\"));\n+\n+\tif (argc > 1)\n+\t\tdie(_(\"unrecognized argument: %s\"), argv[1]);\n+\n+\tif (!tree.initialized)\n+\t\tinit_notes(&tree, notes_ref, NULL, NOTES_INIT_WRITABLE);\n+\n+\tif (prepare_revision_walk(&revs))\n+\t\tdie(\"revision walk setup failed\");\n+\n+\tif (clear) {\n+\t\ttraverse_commit_list(&revs, clear_trailer_xref_note, NULL, &tree);\n+\t} else {\n+\t\tstruct trailer_rev_xrefs rxrefs;\n+\t\tstruct commit *from_commit;\n+\t\tstruct object_array *to_objs;\n+\n+\t\ttrailer_rev_xrefs_init(&rxrefs, trailer_prefix);\n+\t\ttraverse_commit_list(&revs, record_trailer_xrefs, NULL, &rxrefs);\n+\n+\t\ttrailer_rev_xrefs_for_each(&rxrefs, i, from_commit, to_objs) {\n+\t\t\tret = note_trailer_xrefs(&tree, from_commit, to_objs,\n+\t\t\t\t\t\t tag);\n+\t\t\tif (ret)\n+\t\t\t\treturn ret;\n+\t\t}\n+\t}\n+\n+\tcommit_notes(&tree, \"Notes updated by 'git reverse-trailer-xrefs'\");\n+\n+\treturn 0;\n+}\ndiff --git a/git.c b/git.c\nindex 2f604a41e..4948c8e01 100644\n--- a/git.c\n+++ b/git.c\n@@ -515,6 +515,7 @@ static struct cmd_struct commands[] = {\n \t{ \"mv\", cmd_mv, RUN_SETUP | NEED_WORK_TREE },\n \t{ \"name-rev\", cmd_name_rev, RUN_SETUP },\n \t{ \"notes\", cmd_notes, RUN_SETUP },\n+\t{ \"reverse-trailer-xrefs\", cmd_reverse_trailer_xrefs, RUN_SETUP },\n \t{ \"pack-objects\", cmd_pack_objects, RUN_SETUP },\n \t{ \"pack-redundant\", cmd_pack_redundant, RUN_SETUP | NO_PARSEOPT },\n \t{ \"pack-refs\", cmd_pack_refs, RUN_SETUP },\ndiff --git a/notes.c b/notes.c\nindex 25cdce28b..c32064bfe 100644\n--- a/notes.c\n+++ b/notes.c\n@@ -9,6 +9,7 @@\n #include \"tree-walk.h\"\n #include \"string-list.h\"\n #include \"refs.h\"\n+#include \"hashmap.h\"\n \n /*\n  * Use a non-balancing simple 16-tree structure with struct int_node as\n@@ -79,6 +80,10 @@ static struct notes_tree **display_notes_trees;\n \n static void load_subtree(struct notes_tree *t, struct leaf_node *subtree,\n \t\tstruct int_node *node, unsigned int n);\n+static void parse_xref_note(const char *note, unsigned long size,\n+\t\t\t    const struct object_id *commit_oid,\n+\t\t\t    struct object_array *result,\n+\t\t\t    struct string_list *result_lines);\n \n /*\n  * Search the tree until the appropriate location for the given key is found:\n@@ -914,6 +919,60 @@ int combine_notes_cat_sort_uniq(struct object_id *cur_oid,\n \treturn ret;\n }\n \n+int combine_notes_cat_xrefs(struct object_id *cur_oid,\n+\t\t\t    const struct object_id *new_oid)\n+{\n+\tchar *cur_msg = NULL, *new_msg = NULL;\n+\tunsigned long cur_len, new_len;\n+\tenum object_type cur_type, new_type;\n+\tstruct object_array xrefs = OBJECT_ARRAY_INIT;\n+\tstruct string_list lines = STRING_LIST_INIT_DUP;\n+\tstruct strbuf output = STRBUF_INIT;\n+\tint i, j, cur_nr, ret;\n+\n+\t/* read in both note blob objects */\n+\tif (!is_null_oid(new_oid))\n+\t\tnew_msg = read_object_file(new_oid, &new_type, &new_len);\n+\tif (!new_msg || !new_len || new_type != OBJ_BLOB) {\n+\t\tfree(new_msg);\n+\t\treturn 0;\n+\t}\n+\tif (!is_null_oid(cur_oid))\n+\t\tcur_msg = read_object_file(cur_oid, &cur_type, &cur_len);\n+\tif (!cur_msg || !cur_len || cur_type != OBJ_BLOB) {\n+\t\tfree(cur_msg);\n+\t\tfree(new_msg);\n+\t\toidcpy(cur_oid, new_oid);\n+\t\treturn 0;\n+\t}\n+\n+\t/* parse xrefs and de-dup */\n+\tparse_xref_note(cur_msg, cur_len, NULL, &xrefs, &lines);\n+\tcur_nr = xrefs.nr;\n+\tparse_xref_note(new_msg, new_len, NULL, &xrefs, &lines);\n+\n+\tfor (i = 0; i < cur_nr; i++)\n+\t\tfor (j = cur_nr; j < xrefs.nr; j++)\n+\t\t\tif (!strcmp(xrefs.objects[i].name,\n+\t\t\t\t    xrefs.objects[j].name))\n+\t\t\t\tlines.items[j].string[0] = '\\0';\n+\n+\t/* write out the combined object */\n+\tfor (i = 0; i < lines.nr; i++)\n+\t\tif (lines.items[i].string[0] != '\\0')\n+\t\t\tstrbuf_addf(&output, \"%s\\n\", lines.items[i].string);\n+\n+\tret = write_object_file(output.buf, output.len, blob_type, cur_oid);\n+\n+\tstrbuf_release(&output);\n+\tobject_array_clear(&xrefs);\n+\tstring_list_clear(&lines, 0);\n+\tfree(cur_msg);\n+\tfree(new_msg);\n+\n+\treturn ret;\n+}\n+\n static int string_list_add_one_ref(const char *refname, const struct object_id *oid,\n \t\t\t\t   int flag, void *cb)\n {\n@@ -996,8 +1055,12 @@ void init_notes(struct notes_tree *t, const char *notes_ref,\n \tif (!notes_ref)\n \t\tnotes_ref = default_notes_ref();\n \n-\tif (!combine_notes)\n-\t\tcombine_notes = combine_notes_concatenate;\n+\tif (!combine_notes) {\n+\t\tif (starts_with(notes_ref, \"refs/notes/xref-\"))\n+\t\t\tcombine_notes = combine_notes_cat_xrefs;\n+\t\telse\n+\t\t\tcombine_notes = combine_notes_concatenate;\n+\t}\n \n \tt->root = (struct int_node *) xcalloc(1, sizeof(struct int_node));\n \tt->first_non_note = NULL;\n@@ -1189,6 +1252,67 @@ void free_notes(struct notes_tree *t)\n \tmemset(t, 0, sizeof(struct notes_tree));\n }\n \n+/*\n+ * Parse a \"[TAG:]HEX\" line.  @xref is trimmed.  If @tag_p is not NULL and\n+ * TAG exists, the string is split.  Returns the pointer to the OID and\n+ * *@tag_p is updated to the TAG if requested.\n+ */\n+static char *parse_xref(char *xref, char **tag_p)\n+{\n+\tchar *p, *hex;\n+\n+\twhile (isspace(*xref))\n+\t\txref++;\n+\n+\tp = strchr(xref, ':');\n+\tif (p) {\n+\t\tif (tag_p) {\n+\t\t\t*tag_p = xref;\n+\t\t\t*p = '\\0';\n+\t\t}\n+\t\tp++;\n+\t\twhile (isspace(*p))\n+\t\t\tp++;\n+\t\thex = p;\n+\t} else {\n+\t\tif (tag_p)\n+\t\t\t*tag_p = NULL;\n+\t\thex = xref;\n+\t}\n+\n+\tp = hex;\n+\twhile (*p != '\\0' && !isspace(*p))\n+\t\tp++;\n+\t*p = '\\0';\n+\treturn hex;\n+}\n+\n+static void walk_xrefs(const char *tree_ref, struct object_id *from_oid,\n+\t\t       struct strbuf *sb, int level)\n+{\n+\tstruct object_array xrefs = OBJECT_ARRAY_INIT;\n+\tstruct string_list lines = STRING_LIST_INIT_DUP;\n+\tint i;\n+\n+\tread_xref_note(tree_ref, from_oid, &xrefs, &lines);\n+\n+\tfor (i = 0; i < xrefs.nr; i++) {\n+\t\tchar *line = lines.items[i].string;\n+\t\tchar *tag;\n+\n+\t\tparse_xref(line, &tag);\n+\t\tstrbuf_addf(sb, \"    %s%s%*s%s\\n\",\n+\t\t\t    tag ?: \"\", tag ? \": \" : \"\", 2 * level, \"\",\n+\t\t\t    xrefs.objects[i].name);\n+\t\tif (xrefs.objects[i].item)\n+\t\t\twalk_xrefs(tree_ref, &xrefs.objects[i].item->oid, sb,\n+\t\t\t\t   level + 1);\n+\t}\n+\n+\tobject_array_clear(&xrefs);\n+\tstring_list_clear(&lines, 0);\n+}\n+\n /*\n  * Fill the given strbuf with the notes associated with the given object.\n  *\n@@ -1208,6 +1332,7 @@ static void format_note(struct notes_tree *t, const struct object_id *object_oid\n \tchar *msg, *msg_p;\n \tunsigned long linelen, msglen;\n \tenum object_type type;\n+\tint format_xrefs;\n \n \tif (!t)\n \t\tt = &default_notes_tree;\n@@ -1250,6 +1375,8 @@ static void format_note(struct notes_tree *t, const struct object_id *object_oid\n \t\t}\n \t}\n \n+\tformat_xrefs = !raw && starts_with(t->ref, \"refs/notes/xref-\");\n+\n \tfor (msg_p = msg; msg_p < msg + msglen; msg_p += linelen + 1) {\n \t\tlinelen = strchrnul(msg_p, '\\n') - msg_p;\n \n@@ -1257,6 +1384,14 @@ static void format_note(struct notes_tree *t, const struct object_id *object_oid\n \t\t\tstrbuf_addstr(sb, \"    \");\n \t\tstrbuf_add(sb, msg_p, linelen);\n \t\tstrbuf_addch(sb, '\\n');\n+\n+\t\tif (format_xrefs) {\n+\t\t\tstruct object_id oid;\n+\n+\t\t\tmsg_p[linelen] = '\\0';\n+\t\t\tif (!get_oid_hex(parse_xref(msg_p, NULL), &oid))\n+\t\t\t\twalk_xrefs(t->ref, &oid, sb, 1);\n+\t\t}\n \t}\n \n \tfree(msg);\n@@ -1309,3 +1444,109 @@ void expand_loose_notes_ref(struct strbuf *sb)\n \t\texpand_notes_ref(sb);\n \t}\n }\n+\n+struct notes_tree_entry {\n+\tstruct hashmap_entry ent;\n+\tstruct notes_tree tree;\n+};\n+\n+static int notes_tree_cmp(const void *hashmap_cmp_fn_data,\n+\t\t\t  const void *entry, const void *entry_or_key,\n+\t\t\t  const void *keydata)\n+{\n+\tconst struct notes_tree_entry *e1 = entry;\n+\tconst struct notes_tree_entry *e2 = entry_or_key;\n+\n+\treturn strcmp(e1->tree.ref, e2->tree.ref);\n+}\n+\n+static void parse_xref_note(const char *note, unsigned long size,\n+\t\t\t    const struct object_id *commit_oid,\n+\t\t\t    struct object_array *result,\n+\t\t\t    struct string_list *result_lines)\n+{\n+\tstruct strbuf **lines, **pline;\n+\n+\tlines = strbuf_split_buf(note, size, '\\n', 0);\n+\n+\tfor (pline = lines; *pline; pline++) {\n+\t\tstruct strbuf *line = *pline;\n+\t\tconst char *target_hex;\n+\t\tstruct object_id target_oid;\n+\t\tstruct object *target_obj;\n+\n+\t\ttarget_hex = parse_xref(line->buf, NULL);\n+\t\tif (get_oid_hex(target_hex, &target_oid)) {\n+\t\t\tif (commit_oid)\n+\t\t\t\twarning(\"read invalid sha1 on %s: %s\",\n+\t\t\t\t\toid_to_hex(commit_oid), line->buf);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\ttarget_obj = parse_object(the_repository, &target_oid);\n+\t\tif (!target_obj || target_obj->type != OBJ_COMMIT) {\n+\t\t\tif (commit_oid)\n+\t\t\t\twarning(\"read invalid commit on %s: %s\",\n+\t\t\t\t\toid_to_hex(commit_oid), line->buf);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tadd_object_array(target_obj, target_hex, result);\n+\t\tif (result_lines) {\n+\t\t\tassert(result_lines->strdup_strings);\n+\t\t\tstring_list_append(result_lines, line->buf);\n+\t\t}\n+\t}\n+\n+\tstrbuf_list_free(lines);\n+}\n+\n+/*\n+ * Read a cross-referencing note.\n+ *\n+ * Notes in @notes_ref contains lines of \"[TAG:]HEX\" pointing to other\n+ * commits.  Read the target commits and add the objects to @result.  If\n+ * @result_lines is non-NULL, it should point to a strdup'ing string_list.\n+ * The verbatim note lines matching the target commits are appened to the\n+ * list.\n+ */\n+void read_xref_note(const char *notes_ref, const struct object_id *commit_oid,\n+\t\t    struct object_array *result,\n+\t\t    struct string_list *result_lines)\n+{\n+\tstatic struct hashmap *notes_tree_map = NULL;\n+\tunsigned hash = memhash(notes_ref, strlen(notes_ref));\n+\tstruct notes_tree_entry key, *ent;\n+\tconst struct object_id *note_oid;\n+\tunsigned long size;\n+\tenum object_type type;\n+\tchar *note;\n+\n+\tif (!notes_tree_map) {\n+\t\tnotes_tree_map = xcalloc(1, sizeof(struct hashmap));\n+\t\thashmap_init(notes_tree_map, notes_tree_cmp, NULL, 0);\n+\t}\n+\n+\thashmap_entry_init(&key.ent, hash);\n+\tkey.tree.ref = (char *)notes_ref;\n+\tent = hashmap_get(notes_tree_map, &key, NULL);\n+\tif (!ent) {\n+\t\tent = xcalloc(1, sizeof(struct notes_tree_entry));\n+\t\tinit_notes(&ent->tree, notes_ref, NULL, 0);\n+\t\thashmap_entry_init(&ent->ent, hash);\n+\t\thashmap_put(notes_tree_map, ent);\n+\t}\n+\n+\tnote_oid = get_note(&ent->tree, commit_oid);\n+\tif (!note_oid)\n+\t\treturn;\n+\n+\tnote = read_object_file(note_oid, &type, &size);\n+\tif (!size) {\n+\t\tfree(note);\n+\t\treturn;\n+\t}\n+\n+\tparse_xref_note(note, size, commit_oid, result, result_lines);\n+\tfree(note);\n+}\ndiff --git a/notes.h b/notes.h\nindex 414bc6855..fb8153334 100644\n--- a/notes.h\n+++ b/notes.h\n@@ -2,10 +2,14 @@\n #define NOTES_H\n \n #include \"string-list.h\"\n+#include \"object.h\"\n \n struct object_id;\n struct strbuf;\n \n+#define NOTES_CHERRY_PICKS_REF\t\t\"refs/notes/xref-cherry-picks\"\n+#define NOTES_CHERRY_PICKED_TO_TAG\t\"Cherry-picked-to\"\n+\n /*\n  * Function type for combining two notes annotating the same object.\n  *\n@@ -38,6 +42,8 @@ int combine_notes_ignore(struct object_id *cur_oid,\n \t\t\t const struct object_id *new_oid);\n int combine_notes_cat_sort_uniq(struct object_id *cur_oid,\n \t\t\t\tconst struct object_id *new_oid);\n+int combine_notes_cat_xrefs(struct object_id *cur_oid,\n+\t\t\t    const struct object_id *new_oid);\n \n /*\n  * Notes tree object\n@@ -317,4 +323,8 @@ void expand_notes_ref(struct strbuf *sb);\n  */\n void expand_loose_notes_ref(struct strbuf *sb);\n \n+void read_xref_note(const char *notes_ref, const struct object_id *commit_oid,\n+\t\t    struct object_array *result,\n+\t\t    struct string_list *result_lines);\n+\n #endif\ndiff --git a/object.c b/object.c\nindex e54160550..f79652a34 100644\n--- a/object.c\n+++ b/object.c\n@@ -404,7 +404,7 @@ void object_array_clear(struct object_array *array)\n /*\n  * Return true iff array already contains an entry with name.\n  */\n-static int contains_name(struct object_array *array, const char *name)\n+int object_array_contains_name(struct object_array *array, const char *name)\n {\n \tunsigned nr = array->nr, i;\n \tstruct object_array_entry *object = array->objects;\n@@ -422,7 +422,7 @@ void object_array_remove_duplicates(struct object_array *array)\n \n \tarray->nr = 0;\n \tfor (src = 0; src < nr; src++) {\n-\t\tif (!contains_name(array, objects[src].name)) {\n+\t\tif (!object_array_contains_name(array, objects[src].name)) {\n \t\t\tif (src != array->nr)\n \t\t\t\tobjects[array->nr] = objects[src];\n \t\t\tarray->nr++;\ndiff --git a/object.h b/object.h\nindex 796792cb3..a0b3dd312 100644\n--- a/object.h\n+++ b/object.h\n@@ -172,6 +172,12 @@ typedef int (*object_array_each_func_t)(struct object_array_entry *, void *);\n void object_array_filter(struct object_array *array,\n \t\t\t object_array_each_func_t want, void *cb_data);\n \n+/*\n+ * Returns 1 if array already contains an entry with the specified name.\n+ * Otherwise, 0.\n+ */\n+int object_array_contains_name(struct object_array *array, const char *name);\n+\n /*\n  * Remove from array all but the first entry with a given name.\n  * Warning: this function uses an O(N^2) algorithm.\ndiff --git a/trailer.c b/trailer.c\nindex 0796f326b..3afa38d25 100644\n--- a/trailer.c\n+++ b/trailer.c\n@@ -2,6 +2,7 @@\n #include \"config.h\"\n #include \"string-list.h\"\n #include \"run-command.h\"\n+#include \"object-store.h\"\n #include \"commit.h\"\n #include \"tempfile.h\"\n #include \"trailer.h\"\n@@ -1170,3 +1171,104 @@ void format_trailers_from_commit(struct strbuf *out, const char *msg,\n \tformat_trailer_info(out, &info, opts);\n \ttrailer_info_release(&info);\n }\n+\n+implement_static_commit_slab(trailer_rxrefs_slab, struct object_array *);\n+\n+static struct object_array *get_trailer_rxrefs(\n+\t\t\tstruct trailer_rev_xrefs *rxrefs, struct commit *commit)\n+{\n+\tstruct object_array **slot =\n+\t\ttrailer_rxrefs_slab_peek(&rxrefs->slab, commit);\n+\n+\treturn slot ? *slot : NULL;\n+}\n+\n+static struct object_array *get_create_trailer_rxrefs(\n+\t\t\tstruct trailer_rev_xrefs *rxrefs, struct commit *commit)\n+{\n+\tstruct object_array **slot =\n+\t\ttrailer_rxrefs_slab_at(&rxrefs->slab, commit);\n+\n+\tif (*slot)\n+\t\treturn *slot;\n+\n+\tadd_object_array(&commit->object, oid_to_hex(&commit->object.oid),\n+\t\t\t &rxrefs->from_commits);\n+\t*slot = xmalloc(sizeof(struct object_array));\n+\t**slot = (struct object_array)OBJECT_ARRAY_INIT;\n+\treturn *slot;\n+}\n+\n+void trailer_rev_xrefs_init(struct trailer_rev_xrefs *rxrefs, const char *tag)\n+{\n+\trxrefs->tag = xstrdup(tag);\n+\trxrefs->tag_len = strlen(tag);\n+\tinit_trailer_rxrefs_slab(&rxrefs->slab);\n+\trxrefs->from_commits = (struct object_array)OBJECT_ARRAY_INIT;\n+}\n+\n+void trailer_rev_xrefs_record(struct trailer_rev_xrefs *rxrefs,\n+\t\t\t      struct commit *commit)\n+{\n+\tstruct process_trailer_options opts = PROCESS_TRAILER_OPTIONS_INIT;\n+\tenum object_type type;\n+\tunsigned long size;\n+\tvoid *buffer;\n+\tstruct trailer_info info;\n+\tint i;\n+\n+\tbuffer = read_object_file(&commit->object.oid, &type, &size);\n+\ttrailer_info_get(&info, buffer, &opts);\n+\n+\t/* when nested, the last trailer describes the latest cherry-pick */\n+\tfor (i = info.trailer_nr - 1; i >= 0; i--) {\n+\t\tchar *line = info.trailers[i];\n+\n+\t\tif (starts_with(line, rxrefs->tag)) {\n+\t\t\tstruct object_id from_oid;\n+\t\t\tstruct object *from_object;\n+\t\t\tstruct commit *from_commit;\n+\t\t\tstruct object_array *to_objs;\n+\t\t\tchar cherry_hex[GIT_MAX_HEXSZ + 1];\n+\n+\t\t\tif (get_oid_hex(line + rxrefs->tag_len, &from_oid))\n+\t\t\t\tcontinue;\n+\n+\t\t\tfrom_object = parse_object(the_repository, &from_oid);\n+\t\t\tif (!from_object || from_object->type != OBJ_COMMIT)\n+\t\t\t\tcontinue;\n+\n+\t\t\tfrom_commit = (struct commit *)from_object;\n+\t\t\tto_objs = get_create_trailer_rxrefs(rxrefs, from_commit);\n+\n+\t\t\toid_to_hex_r(cherry_hex, &commit->object.oid);\n+\t\t\tadd_object_array(&commit->object, cherry_hex, to_objs);\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\tfree(buffer);\n+}\n+\n+void trailer_rev_xrefs_release(struct trailer_rev_xrefs *rxrefs)\n+{\n+\tclear_trailer_rxrefs_slab(&rxrefs->slab);\n+\tobject_array_clear(&rxrefs->from_commits);\n+\tfree(rxrefs->tag);\n+}\n+\n+void trailer_rev_xrefs_next(struct trailer_rev_xrefs *rxrefs, int *idx_p,\n+\t\t\t    struct commit **from_commit_p,\n+\t\t\t    struct object_array **to_objs_p)\n+{\n+\tif (*idx_p >= rxrefs->from_commits.nr) {\n+\t\t*from_commit_p = NULL;\n+\t\t*to_objs_p = NULL;\n+\t\treturn;\n+\t}\n+\n+\t*from_commit_p = (struct commit *)\n+\t\trxrefs->from_commits.objects[*idx_p].item;\n+\t*to_objs_p = get_trailer_rxrefs(rxrefs, *from_commit_p);\n+\t(*idx_p)++;\n+}\ndiff --git a/trailer.h b/trailer.h\nindex b99773964..5a9704e19 100644\n--- a/trailer.h\n+++ b/trailer.h\n@@ -2,6 +2,8 @@\n #define TRAILER_H\n \n #include \"list.h\"\n+#include \"object.h\"\n+#include \"commit-slab.h\"\n \n struct strbuf;\n \n@@ -99,4 +101,28 @@ void trailer_info_release(struct trailer_info *info);\n void format_trailers_from_commit(struct strbuf *out, const char *msg,\n \t\t\t\t const struct process_trailer_options *opts);\n \n+declare_commit_slab(trailer_rxrefs_slab, struct object_array *);\n+\n+struct trailer_rev_xrefs {\n+\tchar *tag;\n+\tint tag_len;\n+\tstruct trailer_rxrefs_slab slab;\n+\tstruct object_array from_commits;\n+};\n+\n+void trailer_rev_xrefs_init(struct trailer_rev_xrefs *rxrefs, const char *tag);\n+void trailer_rev_xrefs_record(struct trailer_rev_xrefs *rxrefs,\n+\t\t\t      struct commit *commit);\n+void trailer_rev_xrefs_release(struct trailer_rev_xrefs *rxrefs);\n+\n+void trailer_rev_xrefs_next(struct trailer_rev_xrefs *rxrefs,\n+\t\t\t    int *idx_p, struct commit **from_commit_p,\n+\t\t\t    struct object_array **to_objs_p);\n+\n+#define trailer_rev_xrefs_for_each(rxrefs, idx, from_commit, to_objs)\t\t\\\n+\tfor ((idx) = 0,\t\t\t\t\t\t\t\t\\\n+\t     trailer_rev_xrefs_next(rxrefs, &(idx), &(from_commit), &(to_objs));\\\n+\t     (from_commit);\t\t\t\t\t\t\t\\\n+\t     trailer_rev_xrefs_next(rxrefs, &(idx), &(from_commit), &(to_objs)))\n+\n #endif /* TRAILER_H */\n"}]}