revision: new rev^-n shorthand for rev^n..rev

"git log rev^..rev" is commonly used to show all work done on and merged from a side branch. This patch introduces a shorthand "rev^-" for this and additionally allows "rev^-$n" to mean "reachable from rev, excluding what is reachable from the nth parent of rev". For example, for a two-parent merge, you can use rev^-2 to get the set of commits which were made to the main branch while the topic branch was prepared. Signed-off-by: Vegard Nossum <vegard.nossum@oracle.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Vegard Nossum committed Sep 27, 2016 at 10:32 UTC 8779351dd7ee54c6ef2a7007670bad75a86edc75
4 files changed +180 -19
Documentation/revisions.txt
+15 -2
@@ -283,7 +283,7 @@ empty range that is both reachable and unreachable from HEAD.
283
284 Other <rev>{caret} Parent Shorthand Notations
285 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
286 -Two other shorthands exist, particularly useful for merge commits,
286 +Three other shorthands exist, particularly useful for merge commits,
287 for naming a set that is formed by a commit and its parent commits.
288
289 The 'r1{caret}@' notation means all parents of 'r1'.
@@ -291,8 +291,15 @@ The 'r1{caret}@' notation means all parents of 'r1'.
291 The 'r1{caret}!' notation includes commit 'r1' but excludes all of its parents.
292 By itself, this notation denotes the single commit 'r1'.
293
294 +The '<rev>{caret}-{<n>}' notation includes '<rev>' but excludes the <n>th
295 +parent (i.e. a shorthand for '<rev>{caret}<n>..<rev>'), with '<n>' = 1 if
296 +not given. This is typically useful for merge commits where you
297 +can just pass '<commit>{caret}-' to get all the commits in the branch
298 +that was merged in merge commit '<commit>' (including '<commit>'
299 +itself).
300 +
301 While '<rev>{caret}<n>' was about specifying a single commit parent, these
295 -two notations consider all its parents. For example you can say
302 +three notations also consider its parents. For example you can say
303 'HEAD{caret}2{caret}@', however you cannot say 'HEAD{caret}@{caret}2'.
304
305 Revision Range Summary
@@ -326,6 +333,10 @@ Revision Range Summary
333 as giving commit '<rev>' and then all its parents prefixed with
334 '{caret}' to exclude them (and their ancestors).
335
336 +'<rev>{caret}-{<n>}', e.g. 'HEAD{caret}-, HEAD{caret}-2'::
337 + Equivalent to '<rev>{caret}<n>..<rev>', with '<n>' = 1 if not
338 + given.
339 +
340 Here are a handful of examples using the Loeliger illustration above,
341 with each step in the notation's expansion and selection carefully
342 spelt out:
@@ -339,6 +350,8 @@ spelt out:
350 C I J F C
351 B..C = ^B C C
352 B...C = B ^F C G H D E B C
353 + B^- = B^..B
354 + = ^B^1 B E I J F B
355 C^@ = C^1
356 = F I J F
357 B^@ = B^1 B^2 B^3
builtin/rev-parse.c
+41 -13
@@ -298,14 +298,30 @@ static int try_parent_shorthands(const char *arg)
298 unsigned char sha1[20];
299 struct commit *commit;
300 struct commit_list *parents;
301 - int parents_only;
302 -
303 - if ((dotdot = strstr(arg, "^!")))
304 - parents_only = 0;
305 - else if ((dotdot = strstr(arg, "^@")))
306 - parents_only = 1;
307 -
308 - if (!dotdot || dotdot[2])
301 + int parent_number;
302 + int include_rev = 0;
303 + int include_parents = 0;
304 + int exclude_parent = 0;
305 +
306 + if ((dotdot = strstr(arg, "^!"))) {
307 + include_rev = 1;
308 + if (dotdot[2])
309 + return 0;
310 + } else if ((dotdot = strstr(arg, "^@"))) {
311 + include_parents = 1;
312 + if (dotdot[2])
313 + return 0;
314 + } else if ((dotdot = strstr(arg, "^-"))) {
315 + include_rev = 1;
316 + exclude_parent = 1;
317 +
318 + if (dotdot[2]) {
319 + char *end;
320 + exclude_parent = strtoul(dotdot + 2, &end, 10);
321 + if (*end != '\0' || !exclude_parent)
322 + return 0;
323 + }
324 + } else
325 return 0;
326
327 *dotdot = 0;
@@ -314,12 +330,24 @@ static int try_parent_shorthands(const char *arg)
330 return 0;
331 }
332
317 - if (!parents_only)
318 - show_rev(NORMAL, sha1, arg);
333 commit = lookup_commit_reference(sha1);
320 - for (parents = commit->parents; parents; parents = parents->next)
321 - show_rev(parents_only ? NORMAL : REVERSED,
322 - parents->item->object.oid.hash, arg);
334 + if (exclude_parent &&
335 + exclude_parent > commit_list_count(commit->parents)) {
336 + *dotdot = '^';
337 + return 0;
338 + }
339 +
340 + if (include_rev)
341 + show_rev(NORMAL, sha1, arg);
342 + for (parents = commit->parents, parent_number = 1;
343 + parents;
344 + parents = parents->next, parent_number++) {
345 + if (exclude_parent && parent_number != exclude_parent)
346 + continue;
347 +
348 + show_rev(include_parents ? NORMAL : REVERSED,
349 + parents->item->object.oid.hash, arg);
350 + }
351
352 *dotdot = '^';
353 return 1;
revision.c
+30 -4
@@ -1289,12 +1289,14 @@ void add_index_objects_to_pending(struct rev_info *revs, unsigned flags)
1289 }
1290 }
1291
1292 -static int add_parents_only(struct rev_info *revs, const char *arg_, int flags)
1292 +static int add_parents_only(struct rev_info *revs, const char *arg_, int flags,
1293 + int exclude_parent)
1294 {
1295 unsigned char sha1[20];
1296 struct object *it;
1297 struct commit *commit;
1298 struct commit_list *parents;
1299 + int parent_number;
1300 const char *arg = arg_;
1301
1302 if (*arg == '^') {
@@ -1316,7 +1318,15 @@ static int add_parents_only(struct rev_info *revs, const char *arg_, int flags)
1318 if (it->type != OBJ_COMMIT)
1319 return 0;
1320 commit = (struct commit *)it;
1319 - for (parents = commit->parents; parents; parents = parents->next) {
1321 + if (exclude_parent &&
1322 + exclude_parent > commit_list_count(commit->parents))
1323 + return 0;
1324 + for (parents = commit->parents, parent_number = 1;
1325 + parents;
1326 + parents = parents->next, parent_number++) {
1327 + if (exclude_parent && parent_number != exclude_parent)
1328 + continue;
1329 +
1330 it = &parents->item->object;
1331 it->flags |= flags;
1332 add_rev_cmdline(revs, it, arg_, REV_CMD_PARENTS_ONLY, flags);
@@ -1519,17 +1529,33 @@ int handle_revision_arg(const char *arg_, struct rev_info *revs, int flags, unsi
1529 }
1530 *dotdot = '.';
1531 }
1532 +
1533 dotdot = strstr(arg, "^@");
1534 if (dotdot && !dotdot[2]) {
1535 *dotdot = 0;
1525 - if (add_parents_only(revs, arg, flags))
1536 + if (add_parents_only(revs, arg, flags, 0))
1537 return 0;
1538 *dotdot = '^';
1539 }
1540 dotdot = strstr(arg, "^!");
1541 if (dotdot && !dotdot[2]) {
1542 *dotdot = 0;
1532 - if (!add_parents_only(revs, arg, flags ^ (UNINTERESTING | BOTTOM)))
1543 + if (!add_parents_only(revs, arg, flags ^ (UNINTERESTING | BOTTOM), 0))
1544 + *dotdot = '^';
1545 + }
1546 + dotdot = strstr(arg, "^-");
1547 + if (dotdot) {
1548 + int exclude_parent = 1;
1549 +
1550 + if (dotdot[2]) {
1551 + char *end;
1552 + exclude_parent = strtoul(dotdot + 2, &end, 10);
1553 + if (*end != '\0' || !exclude_parent)
1554 + return -1;
1555 + }
1556 +
1557 + *dotdot = 0;
1558 + if (!add_parents_only(revs, arg, flags ^ (UNINTERESTING | BOTTOM), exclude_parent))
1559 *dotdot = '^';
1560 }
1561
t/t6101-rev-parse-parents.sh
+94
@@ -102,4 +102,98 @@ test_expect_success 'short SHA-1 works' '
102 test_cmp_rev_output start "git rev-parse ${start%?}"
103 '
104
105 +# rev^- tests; we can use a simpler setup for these
106 +
107 +test_expect_success 'setup for rev^- tests' '
108 + test_commit one &&
109 + test_commit two &&
110 + test_commit three &&
111 +
112 + # Merge in a branch for testing rev^-
113 + git checkout -b branch &&
114 + git checkout HEAD^^ &&
115 + git merge -m merge --no-edit --no-ff branch &&
116 + git checkout -b merge
117 +'
118 +
119 +# The merged branch has 2 commits + the merge
120 +test_expect_success 'rev-list --count merge^- = merge^..merge' '
121 + git rev-list --count merge^..merge >expect &&
122 + echo 3 >actual &&
123 + test_cmp expect actual
124 +'
125 +
126 +# All rev^- rev-parse tests
127 +
128 +test_expect_success 'rev-parse merge^- = merge^..merge' '
129 + git rev-parse merge^..merge >expect &&
130 + git rev-parse merge^- >actual &&
131 + test_cmp expect actual
132 +'
133 +
134 +test_expect_success 'rev-parse merge^-1 = merge^..merge' '
135 + git rev-parse merge^1..merge >expect &&
136 + git rev-parse merge^-1 >actual &&
137 + test_cmp expect actual
138 +'
139 +
140 +test_expect_success 'rev-parse merge^-2 = merge^2..merge' '
141 + git rev-parse merge^2..merge >expect &&
142 + git rev-parse merge^-2 >actual &&
143 + test_cmp expect actual
144 +'
145 +
146 +test_expect_success 'rev-parse merge^-0 (invalid parent)' '
147 + test_must_fail git rev-parse merge^-0
148 +'
149 +
150 +test_expect_success 'rev-parse merge^-3 (invalid parent)' '
151 + test_must_fail git rev-parse merge^-3
152 +'
153 +
154 +test_expect_success 'rev-parse merge^-^ (garbage after ^-)' '
155 + test_must_fail git rev-parse merge^-^
156 +'
157 +
158 +test_expect_success 'rev-parse merge^-1x (garbage after ^-1)' '
159 + test_must_fail git rev-parse merge^-1x
160 +'
161 +
162 +# All rev^- rev-list tests (should be mostly the same as rev-parse; the reason
163 +# for the duplication is that rev-parse and rev-list use different parsers).
164 +
165 +test_expect_success 'rev-list merge^- = merge^..merge' '
166 + git rev-list merge^..merge >expect &&
167 + git rev-list merge^- >actual &&
168 + test_cmp expect actual
169 +'
170 +
171 +test_expect_success 'rev-list merge^-1 = merge^1..merge' '
172 + git rev-list merge^1..merge >expect &&
173 + git rev-list merge^-1 >actual &&
174 + test_cmp expect actual
175 +'
176 +
177 +test_expect_success 'rev-list merge^-2 = merge^2..merge' '
178 + git rev-list merge^2..merge >expect &&
179 + git rev-list merge^-2 >actual &&
180 + test_cmp expect actual
181 +'
182 +
183 +test_expect_success 'rev-list merge^-0 (invalid parent)' '
184 + test_must_fail git rev-list merge^-0
185 +'
186 +
187 +test_expect_success 'rev-list merge^-3 (invalid parent)' '
188 + test_must_fail git rev-list merge^-3
189 +'
190 +
191 +test_expect_success 'rev-list merge^-^ (garbage after ^-)' '
192 + test_must_fail git rev-list merge^-^
193 +'
194 +
195 +test_expect_success 'rev-list merge^-1x (garbage after ^-1)' '
196 + test_must_fail git rev-list merge^-1x
197 +'
198 +
199 test_done