{"thread":{"id":"44155","subject":"[RFC PATCH v3] revision: new rev^-n shorthand for rev^n..rev","startedAt":"2016-09-25T19:16:51Z","lastAt":"2016-09-25T19:16:51Z","messageCount":1,"participants":["Vegard Nossum"],"isPatch":true,"patchVersion":3,"patchTotal":null},"messages":[{"id":"302532","messageId":"20160925191312.28760-1-vegard.nossum@oracle.com","threadId":"44155","inReplyTo":null,"subject":"[RFC PATCH v3] revision: new rev^-n shorthand for rev^n..rev","fromName":"Vegard Nossum","fromEmail":"vegard.nossum@oracle.com","sentAt":"2016-09-25T19:13:12Z","receivedAt":"2016-09-25T19:16:51Z","isPatch":true,"sender":{"key":"vegard.nossum@oracle.com","avatar":"https://avatars.githubusercontent.com/u/24173?v=4"},"body":"I often use rev^..rev to get all the commits in the branch that was merged\nin by the merge commit 'rev' (including the merge itself). To save typing\n(or copy-pasting, if the rev is long -- like a full SHA-1 or branch name)\nwe can make rev^- a shorthand for that.\n\nThe existing syntax rev^! seems like it should do the same thing, but it\ndoesn't really do the right thing for merge commits (it doesn't include\nthe commits from side branches).\n\nAs a natural generalisation, we also accept rev^-n where n excludes the\nnth parent of rev. For example, for a two-parent merge, you can use rev^-2\nto get the set of commits which were made to the main branch while the\ntopic branch was prepared.\n\nSigned-off-by: Vegard Nossum <vegard.nossum@oracle.com>\n\n---\n\n[v2: Use ^- instead of % as suggested by Junio Hamano and use some\n common helper functions for parsing.]\n\n[v3: Use 'struct object_id' instead of 'char[20]' and add some tests as\n suggested by Matthieu Moy; fix missing '-' in Documentation/revisions.txt\n as suggested by Ramsay Jones; misc changelog + documentation fixes as\n suggested by Philip Oakley.]\n\nThanks for the helpful comments so far! I tried to take everything into\naccount. It's my first git.git patch so I'm not too familiar with how the\ntests are set up. Should I add more tests than this?\n\n---\n Documentation/revisions.txt     | 15 +++++++\n builtin/rev-parse.c             | 28 +++++++++++++\n revision.c                      | 91 +++++++++++++++++++++++++++++++++++++++++\n revision.h                      |  1 +\n t/t6070-rev-parent-exclusion.sh | 38 +++++++++++++++++\n 5 files changed, 173 insertions(+)\n create mode 100755 t/t6070-rev-parent-exclusion.sh\n\ndiff --git Documentation/revisions.txt Documentation/revisions.txt\nindex 4bed5b1..8a27964 100644\n--- Documentation/revisions.txt\n+++ Documentation/revisions.txt\n@@ -281,6 +281,15 @@ is a shorthand for 'HEAD..origin' and asks \"What did the origin do since\n I forked from them?\"  Note that '..' would mean 'HEAD..HEAD' which is an\n empty range that is both reachable and unreachable from HEAD.\n \n+Parent Exclusion Notation\n+~~~~~~~~~~~~~~~~~~~~~~~~~\n+The '<rev>{caret}-{<n>}', Parent Exclusion Notation::\n+Shorthand for '<rev>{caret}<n>..<rev>', with '<n>' = 1 if not\n+given. This is typically useful for merge commits where you\n+can just pass '<commit>{caret}-' to get all the commits in the branch\n+that was merged in merge commit '<commit>' (including '<commit>'\n+itself).\n+\n Other <rev>{caret} Parent Shorthand Notations\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n Two other shorthands exist, particularly useful for merge commits,\n@@ -316,6 +325,10 @@ Revision Range Summary\n \t<rev2> but exclude those that are reachable from both.  When\n \teither <rev1> or <rev2> is omitted, it defaults to `HEAD`.\n \n+'<rev>{caret}-{<n>}', e.g. 'HEAD{caret}-, HEAD{caret}-2'::\n+\tEquivalent to '<rev>{caret}<n>..<rev>', with '<n>' = 1 if not\n+\tgiven.\n+\n '<rev>{caret}@', e.g. 'HEAD{caret}@'::\n   A suffix '{caret}' followed by an at sign is the same as listing\n   all parents of '<rev>' (meaning, include anything reachable from\n@@ -339,6 +352,8 @@ spelt out:\n    C                            I J F C\n    B..C   = ^B C                C\n    B...C  = B ^F C              G H D E B C\n+   B^-    = B^..B\n+\t  = B ^B^1              E I J F B\n    C^@    = C^1\n \t  = F                   I J F\n    B^@    = B^1 B^2 B^3\ndiff --git builtin/rev-parse.c builtin/rev-parse.c\nindex 76cf05e..2c573f1 100644\n--- builtin/rev-parse.c\n+++ builtin/rev-parse.c\n@@ -292,6 +292,32 @@ static int try_difference(const char *arg)\n \treturn 0;\n }\n \n+static int try_parent_exclusion(const char *arg)\n+{\n+\tint ret = 0;\n+\tchar *to_rev = NULL;\n+\tchar *from_rev = NULL;\n+\tstruct object_id to_oid;\n+\tstruct object_id from_oid;\n+\n+\tif (parse_parent_exclusion(arg, &to_rev, &from_rev))\n+\t\tgoto out;\n+\tif (get_oid(to_rev, &to_oid))\n+\t\tgoto out;\n+\tif (get_oid(from_rev, &from_oid))\n+\t\tgoto out;\n+\n+\tshow_rev(NORMAL, to_oid.hash, to_rev);\n+\tshow_rev(REVERSED, from_oid.hash, from_rev);\n+\n+\tret = 1;\n+\n+out:\n+\tfree(to_rev);\n+\tfree(from_rev);\n+\treturn ret;\n+}\n+\n static int try_parent_shorthands(const char *arg)\n {\n \tchar *dotdot;\n@@ -839,6 +865,8 @@ int cmd_rev_parse(int argc, const char **argv, const char *prefix)\n \t\t/* Not a flag argument */\n \t\tif (try_difference(arg))\n \t\t\tcontinue;\n+\t\tif (try_parent_exclusion(arg))\n+\t\t\tcontinue;\n \t\tif (try_parent_shorthands(arg))\n \t\t\tcontinue;\n \t\tname = arg;\ndiff --git revision.c revision.c\nindex 969b3d1..602bf88 100644\n--- revision.c\n+++ revision.c\n@@ -1419,6 +1419,93 @@ static void prepare_show_merge(struct rev_info *revs)\n \trevs->limited = 1;\n }\n \n+/*\n+ * If 'arg' is on the form '<rev>^-{<n>}', then return 0 and\n+ * '*to_rev' and '*from_rev' will contain '<rev>' and '<rev>^<n>',\n+ * respectively.\n+ */\n+int parse_parent_exclusion(const char *arg, char **to_rev, char **from_rev)\n+{\n+\tchar *caret;\n+\tunsigned int n = 1;\n+\n+\t/*\n+\t * <rev>^-{<n>} is shorthand for <rev>^<n>..<rev>, with <n> = 1 if\n+\t * not given. This is typically used for merge commits where you\n+\t * can just pass '<merge>^-' and it will show you all the commits in\n+\t * the branch that was merged.\n+\t */\n+\n+\tif (!(caret = strstr(arg, \"^-\")))\n+\t\treturn 1;\n+\tif (caret[2]) {\n+\t\tchar *end;\n+\t\tn = strtoul(&caret[2], &end, 10);\n+\t\tif (*end != '\\0')\n+\t\t\treturn 1;\n+\t}\n+\t*to_rev = xstrndup(arg, caret - arg);\n+\t*from_rev = xstrfmt(\"%s^%u\", *to_rev, n);\n+\treturn 0;\n+}\n+\n+static int handle_parent_exclusion(const char *arg, struct rev_info *revs, int flags)\n+{\n+\tint ret = 1;\n+\tchar *to_rev = NULL;\n+\tchar *from_rev = NULL;\n+\tstruct object_id to_oid;\n+\tstruct object_id from_oid;\n+\n+\tstruct object *a_obj, *b_obj;\n+\tunsigned int flags_exclude = flags ^ (UNINTERESTING | BOTTOM);\n+\tunsigned int a_flags;\n+\n+\t/*\n+\t * <rev>^-{<n>} is shorthand for <rev>^<n>..<rev>, with <n> = 1 if\n+\t * not given. This is typically used for merge commits where you\n+\t * can just pass <merge>^- and it will show you all the commits in\n+\t * the branches that were merged.\n+\t */\n+\n+\tif (parse_parent_exclusion(arg, &to_rev, &from_rev))\n+\t\tgoto out;\n+\n+\tif (get_oid(to_rev, &to_oid)) {\n+\t\tif (revs->ignore_missing)\n+\t\t\tgoto out;\n+\t\tdie(\"Unknown revision %s\", to_rev);\n+\t}\n+\n+\tif (get_oid(from_rev, &from_oid)) {\n+\t\tif (revs->ignore_missing)\n+\t\t\tgoto out;\n+\t\tdie(\"Unknown revision %s\", from_rev);\n+\t}\n+\n+\ta_obj = parse_object(from_oid.hash);\n+\tb_obj = parse_object(to_oid.hash);\n+\tif (!a_obj || !b_obj) {\n+\t\tif (revs->ignore_missing)\n+\t\t\tgoto out;\n+\t\tdie(\"Invalid revision range %s\", arg);\n+\t}\n+\n+\ta_flags = flags_exclude;\n+\ta_obj->flags |= a_flags;\n+\tb_obj->flags |= flags;\n+\tadd_rev_cmdline(revs, a_obj, from_rev, REV_CMD_LEFT, a_flags);\n+\tadd_pending_object(revs, a_obj, from_rev);\n+\tadd_rev_cmdline(revs, b_obj, to_rev, REV_CMD_RIGHT, flags);\n+\tadd_pending_object(revs, b_obj, to_rev);\n+\n+\tret = 0;\n+out:\n+\tfree(to_rev);\n+\tfree(from_rev);\n+\treturn ret;\n+}\n+\n int handle_revision_arg(const char *arg_, struct rev_info *revs, int flags, unsigned revarg_opt)\n {\n \tstruct object_context oc;\n@@ -1519,6 +1606,10 @@ int handle_revision_arg(const char *arg_, struct rev_info *revs, int flags, unsi\n \t\t}\n \t\t*dotdot = '.';\n \t}\n+\n+\tif (!handle_parent_exclusion(arg, revs, flags))\n+\t\treturn 0;\n+\n \tdotdot = strstr(arg, \"^@\");\n \tif (dotdot && !dotdot[2]) {\n \t\t*dotdot = 0;\ndiff --git revision.h revision.h\nindex 9fac1a6..ca5bebc 100644\n--- revision.h\n+++ revision.h\n@@ -243,6 +243,7 @@ extern int setup_revisions(int argc, const char **argv, struct rev_info *revs,\n extern void parse_revision_opt(struct rev_info *revs, struct parse_opt_ctx_t *ctx,\n \t\t\t       const struct option *options,\n \t\t\t       const char * const usagestr[]);\n+extern int parse_parent_exclusion(const char *arg, char **to_rev, char **from_rev);\n #define REVARG_CANNOT_BE_FILENAME 01\n #define REVARG_COMMITTISH 02\n extern int handle_revision_arg(const char *arg, struct rev_info *revs,\ndiff --git t/t6070-rev-parent-exclusion.sh t/t6070-rev-parent-exclusion.sh\nnew file mode 100755\nindex 0000000..e02da47\n--- /dev/null\n+++ t/t6070-rev-parent-exclusion.sh\n@@ -0,0 +1,38 @@\n+#!/bin/sh\n+\n+test_description='rev-list/rev-parse rev^- parsing'\n+\n+. ./test-lib.sh\n+\n+test_expect_success setup '\n+\ttest_commit one &&\n+\ttest_commit two &&\n+\ttest_commit three &&\n+\n+\t# Merge in a branch for testing ^-\n+\tgit checkout -b branch &&\n+\tgit checkout HEAD^^ &&\n+\tgit merge -m merge --no-edit --no-ff branch &&\n+\tgit checkout -b merge\n+'\n+\n+# The merged branch has 2 commits + the merge\n+test_expect_success 'rev-list --count merge^- = merge^..merge' '\n+\tgit rev-list --count merge^..merge >expect &&\n+\techo 3 >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'rev-parse merge^- = merge^..merge' '\n+\tgit rev-parse merge^..merge >expect &&\n+\tgit rev-parse merge^- >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'rev-list merge^- = merge^..merge' '\n+\tgit rev-list merge^..merge >expect &&\n+\tgit rev-list merge^- >actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_done\n-- \n2.10.0.rc0.1.g07c9292\n\n"}]}