>From aefa07bc66bb4a116eb84eb46d7f070f9632c990 Mon Sep 17 00:00:00 2001
From: Tejun Heo <t...@kernel.org>
Date: Thu, 26 Jul 2018 04:14:52 -0700

It's often useful to track cherry-picks of a given commit.  Add
--cherry-picks support to git-name-rev.  When specified, name_rev also
shows the commits cherry-picked from the listed target commits with
indentations.

  $ git name-rev --cherry-picks 10f7ce0a0e524279f022
  10f7ce0a0e524279f022 master~1
    d433e3b4d5a19b3d29e2c8349fe88ceade5f6190 branch1
      82cddd79f962de0bb1e7cdd95d48b48633335816 branch2
    58a8d36b2532feb0a14b4fc2a50d587e64f38324 branch3
    fa8b79edc5dfff21753c2ccfc1a1828336c4c070 branch4~1

Note that branch2 is further indented because it's a nested cherry
pick from d433e3b4d5a1.

"git-describe --contains" is a wrapper around git-name-rev.  Also add
--cherry-picks support to git-describe.

Signed-off-by: Tejun Heo <t...@kernel.org>
---
 Documentation/git-describe.txt   |   5 ++
 Documentation/git-name-rev.txt   |   4 ++
 builtin/describe.c               |   7 +-
 builtin/name-rev.c               | 117 +++++++++++++++++++++++++++++--
 t/t6121-describe-cherry-picks.sh |  63 +++++++++++++++++
 5 files changed, 190 insertions(+), 6 deletions(-)
 create mode 100755 t/t6121-describe-cherry-picks.sh

diff --git a/Documentation/git-describe.txt b/Documentation/git-describe.txt
index e027fb8c4..13a229bd7 100644
--- a/Documentation/git-describe.txt
+++ b/Documentation/git-describe.txt
@@ -60,6 +60,11 @@ OPTIONS
        the tag that comes after the commit, and thus contains it.
        Automatically implies --tags.
 
+--cherry-picks::
+       Also show the commits cherry-picked from the target commits.
+       Cherry-picks are shown indented below their from-commmits.
+       Can only be used with --contains.
+
 --abbrev=<n>::
        Instead of using the default 7 hexadecimal digits as the
        abbreviated object name, use <n> digits, or as many digits
diff --git a/Documentation/git-name-rev.txt b/Documentation/git-name-rev.txt
index 5cb0eb085..df16c4a89 100644
--- a/Documentation/git-name-rev.txt
+++ b/Documentation/git-name-rev.txt
@@ -61,6 +61,10 @@ OPTIONS
 --always::
        Show uniquely abbreviated commit object as fallback.
 
+--cherry-picks::
+       Also show the commits cherry-picked from the target commits.
+       Cherry-picks are shown indented below their from-commmits.
+
 EXAMPLES
 --------
 
diff --git a/builtin/describe.c b/builtin/describe.c
index 1e87f68d5..94c84004d 100644
--- a/builtin/describe.c
+++ b/builtin/describe.c
@@ -528,9 +528,10 @@ static void describe(const char *arg, int last_one)
 
 int cmd_describe(int argc, const char **argv, const char *prefix)
 {
-       int contains = 0;
+       int contains = 0, cherry_picks = 0;
        struct option options[] = {
                OPT_BOOL(0, "contains",   &contains, N_("find the tag that 
comes after the commit")),
+               OPT_BOOL(0, "cherry-picks", &cherry_picks, N_("also include 
cherry-picks with --contains")),
                OPT_BOOL(0, "debug",      &debug, N_("debug search strategy on 
stderr")),
                OPT_BOOL(0, "all",        &all, N_("use any ref")),
                OPT_BOOL(0, "tags",       &tags, N_("use any tag, even 
unannotated")),
@@ -570,6 +571,8 @@ int cmd_describe(int argc, const char **argv, const char 
*prefix)
 
        if (longformat && abbrev == 0)
                die(_("--long is incompatible with --abbrev=0"));
+       if (cherry_picks && !contains)
+               die(_("--cherry-picks can only be used with --contains"));
 
        if (contains) {
                struct string_list_item *item;
@@ -579,6 +582,8 @@ int cmd_describe(int argc, const char **argv, const char 
*prefix)
                argv_array_pushl(&args, "name-rev",
                                 "--peel-tag", "--name-only", "--no-undefined",
                                 NULL);
+               if (cherry_picks)
+                       argv_array_push(&args, "--cherry-picks");
                if (always)
                        argv_array_push(&args, "--always");
                if (!all) {
diff --git a/builtin/name-rev.c b/builtin/name-rev.c
index 0eb440359..7b21556ad 100644
--- a/builtin/name-rev.c
+++ b/builtin/name-rev.c
@@ -7,9 +7,13 @@
 #include "parse-options.h"
 #include "sha1-lookup.h"
 #include "commit-slab.h"
+#include "trailer.h"
+#include "object-store.h"
 
 #define CUTOFF_DATE_SLOP 86400 /* one day */
 
+static const char cherry_picked_prefix[] = "(cherry picked from commit ";
+
 typedef struct rev_name {
        const char *tip_name;
        timestamp_t taggerdate;
@@ -19,9 +23,12 @@ typedef struct rev_name {
 } rev_name;
 
 define_commit_slab(commit_rev_name, struct rev_name *);
+define_commit_slab(commit_cherry_picks, struct object_array *);
 
 static timestamp_t cutoff = TIME_MAX;
 static struct commit_rev_name rev_names;
+static struct commit_cherry_picks cherry_picks;
+static int do_cherry_picks = 0;
 
 /* How many generations are maximally preferred over _one_ merge traversal? */
 #define MERGE_TRAVERSAL_WEIGHT 65535
@@ -38,6 +45,26 @@ static void set_commit_rev_name(struct commit *commit, 
struct rev_name *name)
        *commit_rev_name_at(&rev_names, commit) = name;
 }
 
+static struct object_array *get_commit_cherry_picks(struct commit *commit)
+{
+       struct object_array **slot =
+               commit_cherry_picks_peek(&cherry_picks, commit);
+
+       return slot ? *slot : NULL;
+}
+
+static struct object_array *get_create_commit_cherry_picks(struct commit 
*commit)
+{
+       struct object_array **slot =
+               commit_cherry_picks_at(&cherry_picks, commit);
+
+       if (!*slot) {
+               *slot = xmalloc(sizeof(struct object_array));
+               **slot = (struct object_array)OBJECT_ARRAY_INIT;
+       }
+       return *slot;
+}
+
 static int is_better_name(struct rev_name *name,
                          const char *tip_name,
                          timestamp_t taggerdate,
@@ -76,6 +103,47 @@ static int is_better_name(struct rev_name *name,
        return 0;
 }
 
+static void record_cherry_pick(struct commit *commit)
+{
+       enum object_type type;
+       unsigned long size;
+       void *buffer;
+       struct trailer_info info;
+       int i;
+
+       buffer = read_object_file(&commit->object.oid, &type, &size);
+       trailer_info_get(&info, buffer);
+
+       /* when nested, the last trailer describes the latest cherry-pick */
+       for (i = info.trailer_nr - 1; i >= 0; i--) {
+               const int prefix_len = sizeof(cherry_picked_prefix) - 1;
+               char *line = info.trailers[i];
+
+               if (!strncmp(line, cherry_picked_prefix, prefix_len)) {
+                       struct object_id from_oid;
+                       struct object *from_object;
+                       struct commit *from_commit;
+                       struct object_array *from_cps;
+
+                       if (get_oid_hex(line + prefix_len, &from_oid)) {
+                               fprintf(stderr, "Could not get sha1 from %s", 
line);
+                               break;
+                       }
+
+                       from_object = parse_object(&from_oid);
+                       if (!from_object || from_object->type != OBJ_COMMIT)
+                               break;
+
+                       from_commit = (struct commit *)from_object;
+                       from_cps = get_create_commit_cherry_picks(from_commit);
+                       add_object_array(&commit->object, NULL, from_cps);
+                       break;
+               }
+       }
+
+       free(buffer);
+}
+
 static void name_rev(struct commit *commit,
                const char *tip_name, timestamp_t taggerdate,
                int generation, int distance, int from_tag,
@@ -91,6 +159,10 @@ static void name_rev(struct commit *commit,
        if (commit->date < cutoff)
                return;
 
+       /* if a cherry pick we see for the first time, remember it */
+       if (do_cherry_picks && !name)
+               record_cherry_pick(commit);
+
        if (deref) {
                tip_name = to_free = xstrfmt("%s^0", tip_name);
 
@@ -402,6 +474,32 @@ static void name_rev_line(char *p, struct name_ref_data 
*data)
        strbuf_release(&buf);
 }
 
+static void show_cherry_picks(struct object *obj, int always,
+                             int allow_undefined, int name_only, int level)
+{
+       struct object_array *cps;
+       int i;
+
+       if (obj->type != OBJ_COMMIT)
+               return;
+
+       cps = get_commit_cherry_picks((struct commit *)obj);
+       if (!cps)
+               return;
+
+       for (i = 0; i < cps->nr; i++) {
+               struct object *cherry_pick = cps->objects[i].item;
+               int j;
+
+               for (j = 0; j < level; j++)
+                       fputs("  ", stdout);
+
+               show_name(cherry_pick, NULL, always, allow_undefined, 
name_only);
+               show_cherry_picks(cherry_pick, always, allow_undefined,
+                                 name_only, level + 1);
+       }
+}
+
 int cmd_name_rev(int argc, const char **argv, const char *prefix)
 {
        struct object_array revs = OBJECT_ARRAY_INIT;
@@ -420,6 +518,7 @@ int cmd_name_rev(int argc, const char **argv, const char 
*prefix)
                OPT_BOOL(0, "undefined", &allow_undefined, N_("allow to print 
`undefined` names (default)")),
                OPT_BOOL(0, "always",     &always,
                           N_("show abbreviated commit object as fallback")),
+               OPT_BOOL(0, "cherry-picks", &do_cherry_picks, N_("include 
cherry-picked commits")),
                {
                        /* A Hidden OPT_BOOL */
                        OPTION_SET_INT, 0, "peel-tag", &peel_tag, NULL,
@@ -430,6 +529,7 @@ int cmd_name_rev(int argc, const char **argv, const char 
*prefix)
        };
 
        init_commit_rev_name(&rev_names);
+       init_commit_cherry_picks(&cherry_picks);
        git_config(git_default_config, NULL);
        argc = parse_options(argc, argv, prefix, opts, name_rev_usage, 0);
        if (all + transform_stdin + !!argc > 1) {
@@ -464,10 +564,9 @@ int cmd_name_rev(int argc, const char **argv, const char 
*prefix)
                        continue;
                }
 
-               if (commit) {
+               if (commit)
                        if (cutoff > commit->date)
                                cutoff = commit->date;
-               }
 
                if (peel_tag) {
                        if (!commit) {
@@ -506,9 +605,17 @@ int cmd_name_rev(int argc, const char **argv, const char 
*prefix)
                }
        } else {
                int i;
-               for (i = 0; i < revs.nr; i++)
-                       show_name(revs.objects[i].item, revs.objects[i].name,
-                                 always, allow_undefined, data.name_only);
+               for (i = 0; i < revs.nr; i++) {
+                       struct object *obj = revs.objects[i].item;
+                       const char *name = revs.objects[i].name;
+
+                       show_name(obj, name, always, allow_undefined,
+                                 data.name_only);
+
+                       if (do_cherry_picks)
+                               show_cherry_picks(obj, always, allow_undefined,
+                                                 data.name_only, 1);
+               }
        }
 
        UNLEAK(revs);
diff --git a/t/t6121-describe-cherry-picks.sh b/t/t6121-describe-cherry-picks.sh
new file mode 100755
index 000000000..838e0acc0
--- /dev/null
+++ b/t/t6121-describe-cherry-picks.sh
@@ -0,0 +1,63 @@
+#!/bin/sh
+
+test_description='git describe should show cherry-picks correctly
+
+           C
+ o----o----x
+      |\ 
+      | .--o
+      |\  C1
+      | .--o
+       \  C2
+        .--o
+          C3
+
+C1 and C3 are cherry-picks from C, and C2 from C1.  Verify git desribe
+handles c and its cherry-picks correctly.
+'
+. ./test-lib.sh
+
+GIT_AUTHOR_EMAIL=bogus_email_address
+export GIT_AUTHOR_EMAIL
+
+test_expect_success \
+    'prepare repository with topic branches with cherry-picks' \
+    'test_tick &&
+     echo First > A &&
+     git update-index --add A &&
+     git commit -m "Add A." &&
+
+     test_tick &&
+     git checkout -b T1 master &&
+     git checkout -b T2 master &&
+     git checkout -b T3 master &&
+     git checkout master &&
+
+     test_tick &&
+     echo Second > B &&
+     git update-index --add B &&
+     git commit -m "Add B." &&
+
+     test_tick &&
+     git checkout -f T1 &&
+     rm -f B &&
+     git cherry-pick -x master &&
+
+     test_tick &&
+     git checkout -f T2 &&
+     rm -f B &&
+     git cherry-pick -x T1 &&
+
+     test_tick &&
+     git checkout -f T3 &&
+     rm -f B &&
+     git cherry-pick -x master
+'
+
+test_expect_success 'Verify describing cherry-picks' '
+     git describe --contains --all --cherry-picks master >actual &&
+     echo -e "master\n  T1\n    T2\n  T3" >expect &&
+     test_cmp expect actual
+'
+
+test_done
-- 
2.17.1

Reply via email to