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