Merge branch 'sa/rev-list-missing-only'

The git rev-list command has been augmented with a '--missing-only'
option that filters the output to only show missing objects,
stripping the leading '?' character and suppressing present objects,
which is useful when used in combination with '--missing=print' or
'--missing=print-info'.

* sa/rev-list-missing-only:
  rev-list: add --missing-only option to filter output
diff --git a/Documentation/rev-list-options.adoc b/Documentation/rev-list-options.adoc
index fd831f0..bd9f345 100644
--- a/Documentation/rev-list-options.adoc
+++ b/Documentation/rev-list-options.adoc
@@ -1083,6 +1083,19 @@
 considered as missing too, and the traversal will ignore them. In case
 we cannot get their Object ID though, an error will be raised.
 
+`--missing-only`::
+	When used together with `--missing=print` or `--missing=print-info`,
+	suppress all output for present objects and print only the missing
+	ones.  The selected `--missing=` format is preserved (so
+	`--missing=print-info` still emits `path=` / `type=` fields), but the
+	leading ``?'' prefix used by the non-`-z` forms is omitted.  This is
+	useful for scripting, as a simpler and faster alternative to
+	post-processing the output of `--missing=print`.
++
+This option is incompatible with `--count` and `--disk-usage`.
+It is an error to use `--missing-only` without `--missing=print` or
+`--missing=print-info`.
+
 `--exclude-promisor-objects`::
 	(For internal use only.)  Prefilter object traversal at
 	promisor boundary.  This is used with partial clone.  This is
diff --git a/builtin/rev-list.c b/builtin/rev-list.c
index 6b59623..0faa833 100644
--- a/builtin/rev-list.c
+++ b/builtin/rev-list.c
@@ -111,6 +111,13 @@ enum missing_action {
 	MA_ALLOW_PROMISOR, /* silently allow all missing PROMISOR objects */
 };
 static enum missing_action arg_missing_action;
+static int arg_missing_only;
+
+static inline int should_collect_missing(void)
+{
+	return arg_missing_action == MA_PRINT ||
+	       arg_missing_action == MA_PRINT_INFO;
+}
 
 /* display only the oid of each object encountered */
 static int arg_show_object_names = 1;
@@ -156,7 +163,14 @@ static void print_missing_object(struct missing_objects_map_entry *entry,
 {
 	struct strbuf sb = STRBUF_INIT;
 
-	if (line_term)
+	/*
+	 * --missing-only filters present objects out of the walk output.
+	 * It still uses the selected --missing= format for missing ones,
+	 * except the human "?" prefix is omitted (script-friendly OIDs).
+	 */
+	if (arg_missing_only && line_term)
+		printf("%s", oid_to_hex(&entry->entry.oid));
+	else if (line_term)
 		printf("?%s", oid_to_hex(&entry->entry.oid));
 	else
 		printf("%s%cmissing=yes", oid_to_hex(&entry->entry.oid),
@@ -246,6 +260,11 @@ static void show_commit(struct commit *commit, void *data)
 		return;
 	}
 
+	if (arg_missing_only) {
+		finish_commit(commit);
+		return;
+	}
+
 	if (show_disk_usage)
 		total_disk_usage += get_object_disk_usage(&commit->object);
 
@@ -384,6 +403,8 @@ static void show_object(struct object *obj, const char *name, void *cb_data)
 	if (finish_object(obj, name, cb_data))
 		return;
 	display_progress(progress, ++progress_counter);
+	if (arg_missing_only)
+		return;
 	if (show_disk_usage)
 		total_disk_usage += get_object_disk_usage(obj);
 	if (info->flags & REV_LIST_QUIET)
@@ -750,12 +771,17 @@ int cmd_rev_list(int argc,
 			revs.exclude_promisor_objects = 1;
 		} else if (skip_prefix(arg, "--missing=", &arg)) {
 			parse_missing_action_value(repo, arg);
+		} else if (!strcmp(arg, "--missing-only")) {
+			arg_missing_only = 1;
 		} else if (!strcmp(arg, "-z")) {
 			line_term = '\0';
 			info_term = '\0';
 		}
 	}
 
+	if (arg_missing_only && !should_collect_missing())
+		die(_("--missing-only requires --missing=print or --missing=print-info"));
+
 	die_for_incompatible_opt2(revs.exclude_promisor_objects,
 				  "--exclude_promisor_objects",
 				  arg_missing_action, "--missing");
@@ -865,6 +891,9 @@ int cmd_rev_list(int argc,
 			continue;
 		}
 
+		if (!strcmp(arg, "--missing-only"))
+			continue;
+
 		usage(rev_list_usage);
 
 	}
@@ -911,6 +940,11 @@ int cmd_rev_list(int argc,
 	    (revs.left_right || revs.cherry_mark))
 		die(_("marked counting and '%s' cannot be used together"), "--objects");
 
+	die_for_incompatible_opt2(arg_missing_only, "--missing-only",
+				  revs.count, "--count");
+	die_for_incompatible_opt2(arg_missing_only, "--missing-only",
+				  show_disk_usage, "--disk-usage");
+
 	save_commit_buffer = (revs.verbose_header ||
 			      revs.grep_filter.pattern_list ||
 			      revs.grep_filter.header_list);
@@ -968,8 +1002,7 @@ int cmd_rev_list(int argc,
 
 	if (arg_print_omitted)
 		oidset_init(&omitted_objects, DEFAULT_OIDSET_SIZE);
-	if (arg_missing_action == MA_PRINT ||
-	    arg_missing_action == MA_PRINT_INFO) {
+	if (should_collect_missing()) {
 		struct oidset_iter iter;
 		struct object_id *oid;
 
@@ -995,8 +1028,7 @@ int cmd_rev_list(int argc,
 			printf("~%s\n", oid_to_hex(oid));
 		oidset_clear(&omitted_objects);
 	}
-	if (arg_missing_action == MA_PRINT ||
-	    arg_missing_action == MA_PRINT_INFO) {
+	if (should_collect_missing()) {
 		struct missing_objects_map_entry *entry;
 		struct oidmap_iter iter;
 
diff --git a/t/t6022-rev-list-missing.sh b/t/t6022-rev-list-missing.sh
index 1e472a4..1bd2c3b 100755
--- a/t/t6022-rev-list-missing.sh
+++ b/t/t6022-rev-list-missing.sh
@@ -198,6 +198,55 @@
 	'
 done
 
+for obj in "HEAD~1" "HEAD~1^{tree}" "HEAD:1.t"
+do
+	test_expect_success "rev-list --missing-only with missing $obj" '
+		oid="$(git rev-parse $obj)" &&
+		path=".git/objects/$(test_oid_to_path $oid)" &&
+
+		mv "$path" "$path.hidden" &&
+		test_when_finished "mv $path.hidden $path" &&
+
+		git rev-list --missing=print --missing-only --objects \
+			--no-object-names HEAD >actual &&
+
+		echo $oid >expect &&
+		test_cmp expect actual
+	'
+done
+
+test_expect_success "--missing-only requires --missing=print or --missing=print-info" '
+	test_must_fail git rev-list --missing-only --objects HEAD 2>err &&
+	test_grep "requires --missing=print" err
+'
+
+test_expect_success "--missing-only is incompatible with --count" '
+	test_must_fail git rev-list --missing=print --missing-only \
+		--count --objects HEAD 2>err &&
+	test_grep "cannot be used together" err
+'
+
+test_expect_success "--missing-only is incompatible with --disk-usage" '
+	test_must_fail git rev-list --missing=print --missing-only \
+		--disk-usage --objects HEAD 2>err &&
+	test_grep "cannot be used together" err
+'
+
+test_expect_success "--missing-only works with --missing=print-info" '
+	oid="$(git rev-parse HEAD:1.t)" &&
+	path=".git/objects/$(test_oid_to_path $oid)" &&
+
+	mv "$path" "$path.hidden" &&
+	test_when_finished "mv $path.hidden $path" &&
+
+	git rev-list --missing=print-info --missing-only --objects \
+		--no-object-names HEAD >actual &&
+
+	# Filter keeps print-info fields; only the "?" prefix is dropped.
+	echo "$oid path=1.t type=blob" >expect &&
+	test_cmp expect actual
+'
+
 test_expect_success "-z nul-delimited --missing" '
 	test_when_finished rm -rf repo &&