rev-parse --parseopt: add the --stuck-long mode

Add the --stuck-long option to output the options in their long form
if available, and with their arguments stuck.

Contrary to the default form (non stuck arguments and short options),
this can be parsed unambiguously when using options with optional
arguments :

 - in the non stuck form, when an option is taking an optional argument
   you cannot know if the next argument is its optional argument, or the
   next option.

 - the long options form allows to differentiate between an empty argument
   '--option=' and an unset argument '--option', which is not possible
   with short options.

Signed-off-by: Nicolas Vigier <boklm@mars-attacks.org>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
diff --git a/Documentation/git-rev-parse.txt b/Documentation/git-rev-parse.txt
index d068a65..a436b24 100644
--- a/Documentation/git-rev-parse.txt
+++ b/Documentation/git-rev-parse.txt
@@ -50,6 +50,10 @@
 	the first non-option argument.  This can be used to parse sub-commands
 	that take options themselves.
 
+--stuck-long::
+	Only meaningful in `--parseopt` mode. Output the options in their
+	long form if available, and with their arguments stuck.
+
 Options for Filtering
 ~~~~~~~~~~~~~~~~~~~~~
 
@@ -285,7 +289,9 @@
 	`<flags>` are of `*`, `=`, `?` or `!`.
 	* Use `=` if the option takes an argument.
 
-	* Use `?` to mean that the option is optional (though its use is discouraged).
+	* Use `?` to mean that the option takes an optional argument. You
+	  probably want to use the `--stuck-long` mode to be able to
+	  unambiguously parse the optional argument.
 
 	* Use `*` to mean that this option should not be listed in the usage
 	  generated for the `-h` argument. It's shown for `--help-all` as
diff --git a/builtin/rev-parse.c b/builtin/rev-parse.c
index c76b89d..3e8c4cc 100644
--- a/builtin/rev-parse.c
+++ b/builtin/rev-parse.c
@@ -30,6 +30,8 @@
 static int abbrev_ref_strict;
 static int output_sq;
 
+static int stuck_long;
+
 /*
  * Some arguments are relevant "revision" arguments,
  * others are about output format or other details.
@@ -320,12 +322,15 @@
 	struct strbuf *parsed = o->value;
 	if (unset)
 		strbuf_addf(parsed, " --no-%s", o->long_name);
-	else if (o->short_name)
+	else if (o->short_name && (o->long_name == NULL || !stuck_long))
 		strbuf_addf(parsed, " -%c", o->short_name);
 	else
 		strbuf_addf(parsed, " --%s", o->long_name);
 	if (arg) {
-		strbuf_addch(parsed, ' ');
+		if (!stuck_long)
+			strbuf_addch(parsed, ' ');
+		else if (o->long_name)
+			strbuf_addch(parsed, '=');
 		sq_quote_buf(parsed, arg);
 	}
 	return 0;
@@ -351,6 +356,8 @@
 		OPT_BOOL(0, "stop-at-non-option", &stop_at_non_option,
 					N_("stop parsing after the "
 					   "first non-option argument")),
+		OPT_BOOL(0, "stuck-long", &stuck_long,
+					N_("output in stuck long form")),
 		OPT_END(),
 	};
 
diff --git a/t/t1502-rev-parse-parseopt.sh b/t/t1502-rev-parse-parseopt.sh
index 13c88c9..83b1300 100755
--- a/t/t1502-rev-parse-parseopt.sh
+++ b/t/t1502-rev-parse-parseopt.sh
@@ -12,9 +12,11 @@
     -h, --help            show the help
     --foo                 some nifty option --foo
     --bar ...             some cool option --bar with an argument
+    -b, --baz             a short and long option
 
 An option group Header
     -C[...]               option C with an optional argument
+    -d, --data[=...]      short and long option with an optional argument
 
 Extras
     --extra1              line above used to cause a segfault but no longer does
@@ -31,9 +33,11 @@
 
 foo       some nifty option --foo
 bar=      some cool option --bar with an argument
+b,baz     a short and long option
 
  An option group Header
 C?        option C with an optional argument
+d,data?   short and long option with an optional argument
 
 Extras
 extra1    line above used to cause a segfault but no longer does
@@ -45,16 +49,16 @@
 '
 
 cat > expect <<EOF
-set -- --foo --bar 'ham' -- 'arg'
+set -- --foo --bar 'ham' -b -- 'arg'
 EOF
 
 test_expect_success 'test --parseopt' '
-	git rev-parse --parseopt -- --foo --bar=ham arg < optionspec > output &&
+	git rev-parse --parseopt -- --foo --bar=ham --baz arg < optionspec > output &&
 	test_cmp expect output
 '
 
 test_expect_success 'test --parseopt with mixed options and arguments' '
-	git rev-parse --parseopt -- --foo arg --bar=ham < optionspec > output &&
+	git rev-parse --parseopt -- --foo arg --bar=ham --baz < optionspec > output &&
 	test_cmp expect output
 '
 
@@ -99,4 +103,36 @@
 	test_cmp expect output
 '
 
+cat > expect <<EOF
+set -- --foo --bar='z' --baz -C'Z' --data='A' -- 'arg'
+EOF
+
+test_expect_success 'test --parseopt --stuck-long' '
+	git rev-parse --parseopt --stuck-long -- --foo --bar=z -b arg -CZ -dA <optionspec >output &&
+	test_cmp expect output
+'
+
+cat > expect <<EOF
+set -- --data='' -C --baz -- 'arg'
+EOF
+
+test_expect_success 'test --parseopt --stuck-long and empty optional argument' '
+	git rev-parse --parseopt --stuck-long -- --data= arg -C -b <optionspec >output &&
+	test_cmp expect output
+'
+
+cat > expect <<EOF
+set -- --data --baz -- 'arg'
+EOF
+
+test_expect_success 'test --parseopt --stuck-long and long option with unset optional argument' '
+	git rev-parse --parseopt --stuck-long -- --data arg -b <optionspec >output &&
+	test_cmp expect output
+'
+
+test_expect_success 'test --parseopt --stuck-long and short option with unset optional argument' '
+	git rev-parse --parseopt --stuck-long -- -d arg -b <optionspec >output &&
+	test_cmp expect output
+'
+
 test_done