completion: collapse extra --no-.. options

The commands that make use of --git-completion-helper feature could now produce a lot of --no-xxx options that a command can take. This in many case could nearly double the amount of completable options, using more screen estate and also harder to search for the wanted option. This patch attempts to mitigate that by collapsing extra --no- options, the ones that are added by --git-completion-helper and not in original struct option arrays. The "--no-..." option will be displayed in this case to hint about more options, e.g. > ~/w/git $ git clone -- --bare --origin= --branch= --progress --checkout --quiet --config= --recurse-submodules --depth= --reference= --dissociate --reference-if-able= --filter= --separate-git-dir= --hardlinks --shallow-exclude= --ipv4 --shallow-since= --ipv6 --shallow-submodules --jobs= --shared --local --single-branch --mirror --tags --no-... --template= --no-checkout --upload-pack= --no-hardlinks --verbose --no-tags and when you complete it with --no-<tab>, all negative options will be presented: > ~/w/git $ git clone --no- --no-bare --no-quiet --no-branch --no-recurse-submodules --no-checkout --no-reference --no-config --no-reference-if-able --no-depth --no-separate-git-dir --no-dissociate --no-shallow-exclude --no-filter --no-shallow-since --no-hardlinks --no-shallow-submodules --no-ipv4 --no-shared --no-ipv6 --no-single-branch --no-jobs --no-tags --no-local --no-template --no-mirror --no-upload-pack --no-origin --no-verbose --no-progress Corner case: to make sure that people will never accidentally complete the fake option "--no-..." there must be one real --no- in the first complete listing even if it's not from the original struct option. PS. This could could be made simpler with ";&" to fall through from "--no-*" block and share the code but ";&" is not available on bash-3 (i.e. Mac) Signed-off-by: Nguyễn Thái Ngọc Duy <pclouds@gmail.com> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Nguyễn Thái Ngọc Duy committed Jun 6, 2018 at 11:41 UTC b221b5ab9b92f3def37184db6e052ec87d7635b4
3 files changed +115 -29
contrib/completion/git-completion.bash
+23
@@ -266,9 +266,32 @@ __gitcomp ()
266 case "$cur_" in
267 --*=)
268 ;;
269 + --no-*)
270 + local c i=0 IFS=$' \t\n'
271 + for c in $1; do
272 + if [[ $c == "--" ]]; then
273 + continue
274 + fi
275 + c="$c${4-}"
276 + if [[ $c == "$cur_"* ]]; then
277 + case $c in
278 + --*=*|*.) ;;
279 + *) c="$c " ;;
280 + esac
281 + COMPREPLY[i++]="${2-}$c"
282 + fi
283 + done
284 + ;;
285 *)
286 local c i=0 IFS=$' \t\n'
287 for c in $1; do
288 + if [[ $c == "--" ]]; then
289 + c="--no-...${4-}"
290 + if [[ $c == "$cur_"* ]]; then
291 + COMPREPLY[i++]="${2-}$c "
292 + fi
293 + break
294 + fi
295 c="$c${4-}"
296 if [[ $c == "$cur_"* ]]; then
297 case $c in
parse-options.c
+54 -18
@@ -427,12 +427,61 @@ void parse_options_start(struct parse_opt_ctx_t *ctx,
427 parse_options_check(options);
428 }
429
430 +static void show_negated_gitcomp(const struct option *opts, int nr_noopts)
431 +{
432 + int printed_dashdash = 0;
433 +
434 + for (; opts->type != OPTION_END; opts++) {
435 + int has_unset_form = 0;
436 + const char *name;
437 +
438 + if (!opts->long_name)
439 + continue;
440 + if (opts->flags & (PARSE_OPT_HIDDEN | PARSE_OPT_NOCOMPLETE))
441 + continue;
442 + if (opts->flags & PARSE_OPT_NONEG)
443 + continue;
444 +
445 + switch (opts->type) {
446 + case OPTION_STRING:
447 + case OPTION_FILENAME:
448 + case OPTION_INTEGER:
449 + case OPTION_MAGNITUDE:
450 + case OPTION_CALLBACK:
451 + case OPTION_BIT:
452 + case OPTION_NEGBIT:
453 + case OPTION_COUNTUP:
454 + case OPTION_SET_INT:
455 + has_unset_form = 1;
456 + break;
457 + default:
458 + break;
459 + }
460 + if (!has_unset_form)
461 + continue;
462 +
463 + if (skip_prefix(opts->long_name, "no-", &name)) {
464 + if (nr_noopts < 0)
465 + printf(" --%s", name);
466 + } else if (nr_noopts >= 0) {
467 + if (nr_noopts && !printed_dashdash) {
468 + printf(" --");
469 + printed_dashdash = 1;
470 + }
471 + printf(" --no-%s", opts->long_name);
472 + nr_noopts++;
473 + }
474 + }
475 +}
476 +
477 static int show_gitcomp(struct parse_opt_ctx_t *ctx,
478 const struct option *opts)
479 {
480 + const struct option *original_opts = opts;
481 + int nr_noopts = 0;
482 +
483 for (; opts->type != OPTION_END; opts++) {
484 const char *suffix = "";
435 - int has_unset_form = 0;
485
486 if (!opts->long_name)
487 continue;
@@ -447,8 +496,6 @@ static int show_gitcomp(struct parse_opt_ctx_t *ctx,
496 case OPTION_INTEGER:
497 case OPTION_MAGNITUDE:
498 case OPTION_CALLBACK:
450 - has_unset_form = 1;
451 -
499 if (opts->flags & PARSE_OPT_NOARG)
500 break;
501 if (opts->flags & PARSE_OPT_OPTARG)
@@ -457,28 +504,17 @@ static int show_gitcomp(struct parse_opt_ctx_t *ctx,
504 break;
505 suffix = "=";
506 break;
460 - case OPTION_BIT:
461 - case OPTION_NEGBIT:
462 - case OPTION_COUNTUP:
463 - case OPTION_SET_INT:
464 - has_unset_form = 1;
465 - break;
507 default:
508 break;
509 }
510 if (opts->flags & PARSE_OPT_COMP_ARG)
511 suffix = "=";
512 + if (starts_with(opts->long_name, "no-"))
513 + nr_noopts++;
514 printf(" --%s%s", opts->long_name, suffix);
472 -
473 - if (has_unset_form && !(opts->flags & PARSE_OPT_NONEG)) {
474 - const char *name;
475 -
476 - if (skip_prefix(opts->long_name, "no-", &name))
477 - printf(" --%s", name);
478 - else
479 - printf(" --no-%s", opts->long_name);
480 - }
515 }
516 + show_negated_gitcomp(original_opts, -1);
517 + show_negated_gitcomp(original_opts, nr_noopts);
518 fputc('\n', stdout);
519 exit(0);
520 }
t/t9902-completion.sh
+38 -11
@@ -459,6 +459,42 @@ test_expect_success '__gitcomp - suffix' '
459 EOF
460 '
461
462 +test_expect_success '__gitcomp - ignore optional negative options' '
463 + test_gitcomp "--" "--abc --def --no-one -- --no-two" <<-\EOF
464 + --abc Z
465 + --def Z
466 + --no-one Z
467 + --no-... Z
468 + EOF
469 +'
470 +
471 +test_expect_success '__gitcomp - ignore/narrow optional negative options' '
472 + test_gitcomp "--a" "--abc --abcdef --no-one -- --no-two" <<-\EOF
473 + --abc Z
474 + --abcdef Z
475 + EOF
476 +'
477 +
478 +test_expect_success '__gitcomp - ignore/narrow optional negative options' '
479 + test_gitcomp "--n" "--abc --def --no-one -- --no-two" <<-\EOF
480 + --no-one Z
481 + --no-... Z
482 + EOF
483 +'
484 +
485 +test_expect_success '__gitcomp - expand all negative options' '
486 + test_gitcomp "--no-" "--abc --def --no-one -- --no-two" <<-\EOF
487 + --no-one Z
488 + --no-two Z
489 + EOF
490 +'
491 +
492 +test_expect_success '__gitcomp - expand/narrow all negative options' '
493 + test_gitcomp "--no-o" "--abc --def --no-one -- --no-two" <<-\EOF
494 + --no-one Z
495 + EOF
496 +'
497 +
498 test_expect_success '__gitcomp - doesnt fail because of invalid variable name' '
499 __gitcomp "$invalid_variable_name"
500 '
@@ -1237,29 +1273,20 @@ test_expect_success 'double dash "git" itself' '
1273 test_expect_success 'double dash "git checkout"' '
1274 test_completion "git checkout --" <<-\EOF
1275 --quiet Z
1240 - --no-quiet Z
1276 --detach Z
1242 - --no-detach Z
1277 --track Z
1244 - --no-track Z
1278 --orphan=Z
1246 - --no-orphan Z
1279 --ours Z
1280 --theirs Z
1281 --merge Z
1250 - --no-merge Z
1282 --conflict=Z
1252 - --no-conflict Z
1283 --patch Z
1254 - --no-patch Z
1284 --ignore-skip-worktree-bits Z
1256 - --no-ignore-skip-worktree-bits Z
1285 --ignore-other-worktrees Z
1258 - --no-ignore-other-worktrees Z
1286 --recurse-submodules Z
1260 - --no-recurse-submodules Z
1287 --progress Z
1262 - --no-progress Z
1288 + --no-quiet Z
1289 + --no-... Z
1290 EOF
1291 '
1292