From: Tejun Heo <htejun@xxxxxx> Some trailers refer to other commits. Let's call them xrefs (cross-references). For example, a cherry pick trailer points to the source commit. It is sometimes useful to build a reverse map of these xrefs - ie. source -> cherry-pick instead of cherry-pick -> source. This patch implements git-reverse-trailer-xrefs which can process a given trailer for the specified commits and generate and update the matching refs/notes/xref- notes. The command reverse-maps trailers beginning with --trailer-prefix and stores them in --ref notes with each line tagged with --tag. It also has a refs/notes/xref-cherry-picks preset for the three params. Signed-off-by: Tejun Heo <htejun@xxxxxx> --- Documentation/git-reverse-trailer-xrefs.txt | 136 +++++++++++++++++ Makefile | 1 + builtin.h | 1 + builtin/reverse-trailer-xrefs.c | 160 ++++++++++++++++++++ git.c | 1 + 5 files changed, 299 insertions(+) create mode 100644 Documentation/git-reverse-trailer-xrefs.txt create mode 100644 builtin/reverse-trailer-xrefs.c diff --git a/Documentation/git-reverse-trailer-xrefs.txt b/Documentation/git-reverse-trailer-xrefs.txt new file mode 100644 index 000000000..20d260486 --- /dev/null +++ b/Documentation/git-reverse-trailer-xrefs.txt @@ -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 diff --git a/Makefile b/Makefile index 1a44c811a..3c23ecf9d 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/builtin.h b/builtin.h index 6538932e9..51089e258 100644 --- a/builtin.h +++ b/builtin.h @@ -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); diff --git a/builtin/reverse-trailer-xrefs.c b/builtin/reverse-trailer-xrefs.c new file mode 100644 index 000000000..0edb3b91a --- /dev/null +++ b/builtin/reverse-trailer-xrefs.c @@ -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); +} diff --git a/git.c b/git.c index 2f604a41e..4948c8e01 100644 --- a/git.c +++ b/git.c @@ -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 }, -- 2.17.1