Git fork

Documentation: stop depending on Perl to generate command list

The "cmd-list.perl" script is used to extract the list of commands part
of a specific category and extracts the description of each command from
its respective manpage. The generated output is then included in git(1)
to list all Git commands.

The script is written in Perl. Refactor it to use shell scripting
exclusively so that we can get rid of the mandatory dependency on Perl
to build our documentation.

The converted script is slower compared to its Perl implementation. But
by being careful and not spawning external commands in `format_one ()`
we can mitigate the performance hit to a reasonable level:

Benchmark 1: Perl
Time (mean ± σ): 10.3 ms ± 0.2 ms [User: 7.0 ms, System: 3.3 ms]
Range (min … max): 10.0 ms … 11.1 ms 200 runs

Benchmark 2: Shell
Time (mean ± σ): 74.4 ms ± 0.4 ms [User: 48.6 ms, System: 24.7 ms]
Range (min … max): 73.1 ms … 75.5 ms 200 runs

Summary
Perl ran
7.23 ± 0.13 times faster than Shell

While a sevenfold slowdown is significant, the benefit of not requiring
Perl for a fully-functioning Git installation outweighs waiting a couple
of milliseconds longer during the build process.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
Signed-off-by: Junio C Hamano <gitster@pobox.com>

authored by

Patrick Steinhardt and committed by
Junio C Hamano
a7fa5b2f 521c9884

+109 -85
+2 -2
Documentation/Makefile
··· 317 317 318 318 $(cmds_txt): cmd-list.made 319 319 320 - cmd-list.made: cmd-list.perl ../command-list.txt $(MAN1_TXT) 321 - $(QUIET_GEN)$(PERL_PATH) ./cmd-list.perl .. . $(cmds_txt) && \ 320 + cmd-list.made: cmd-list.sh ../command-list.txt $(MAN1_TXT) 321 + $(QUIET_GEN)$(SHELL_PATH) ./cmd-list.sh .. . $(cmds_txt) && \ 322 322 date >$@ 323 323 324 324 mergetools-%.adoc: generate-mergetool-list.sh ../git-mergetool--lib.sh $(wildcard ../mergetools/*)
-80
Documentation/cmd-list.perl
··· 1 - #!/usr/bin/perl -w 2 - 3 - use File::Compare qw(compare); 4 - 5 - sub format_one { 6 - my ($source_dir, $out, $nameattr) = @_; 7 - my ($name, $attr) = @$nameattr; 8 - my ($path) = "$source_dir/Documentation/$name.adoc"; 9 - my ($state, $description); 10 - my $mansection; 11 - $state = 0; 12 - open I, '<', "$path" or die "No such file $path.adoc"; 13 - while (<I>) { 14 - if (/^(?:git|scalar)[a-z0-9-]*\(([0-9])\)$/) { 15 - $mansection = $1; 16 - next; 17 - } 18 - if (/^NAME$/) { 19 - $state = 1; 20 - next; 21 - } 22 - if ($state == 1 && /^----$/) { 23 - $state = 2; 24 - next; 25 - } 26 - next if ($state != 2); 27 - chomp; 28 - $description = $_; 29 - last; 30 - } 31 - close I; 32 - if (!defined $description) { 33 - die "No description found in $path.adoc"; 34 - } 35 - if (my ($verify_name, $text) = ($description =~ /^($name) - (.*)/)) { 36 - print $out "linkgit:$name\[$mansection\]::\n\t"; 37 - if ($attr =~ / deprecated /) { 38 - print $out "(deprecated) "; 39 - } 40 - print $out "$text.\n\n"; 41 - } 42 - else { 43 - die "Description does not match $name: $description"; 44 - } 45 - } 46 - 47 - my ($source_dir, $build_dir, @categories) = @ARGV; 48 - 49 - open IN, "<$source_dir/command-list.txt"; 50 - while (<IN>) { 51 - last if /^### command list/; 52 - } 53 - 54 - my %cmds = (); 55 - for (sort <IN>) { 56 - next if /^#/; 57 - 58 - chomp; 59 - my ($name, $cat, $attr) = /^(\S+)\s+(.*?)(?:\s+(.*))?$/; 60 - $attr = '' unless defined $attr; 61 - push @{$cmds{$cat}}, [$name, " $attr "]; 62 - } 63 - close IN; 64 - 65 - for my $out (@categories) { 66 - my ($cat) = $out =~ /^cmds-(.*)\.adoc$/; 67 - my ($path) = "$build_dir/$out"; 68 - open O, '>', "$path+" or die "Cannot open output file $out+"; 69 - for (@{$cmds{$cat}}) { 70 - format_one($source_dir, \*O, $_); 71 - } 72 - close O; 73 - 74 - if (-f "$path" && compare("$path", "$path+") == 0) { 75 - unlink "$path+"; 76 - } 77 - else { 78 - rename "$path+", "$path"; 79 - } 80 - }
+104
Documentation/cmd-list.sh
··· 1 + #!/bin/sh 2 + 3 + set -e 4 + 5 + format_one () { 6 + source_dir="$1" 7 + command="$2" 8 + attributes="$3" 9 + 10 + path="$source_dir/Documentation/$command.adoc" 11 + if ! test -f "$path" 12 + then 13 + echo >&2 "No such file $path" 14 + exit 1 15 + fi 16 + 17 + state=0 18 + while read line 19 + do 20 + case "$state" in 21 + 0) 22 + case "$line" in 23 + git*\(*\)|scalar*\(*\)) 24 + mansection="${line##*\(}" 25 + mansection="${mansection%\)}" 26 + ;; 27 + NAME) 28 + state=1;; 29 + esac 30 + ;; 31 + 1) 32 + if test "$line" = "----" 33 + then 34 + state=2 35 + fi 36 + ;; 37 + 2) 38 + description="$line" 39 + break 40 + ;; 41 + esac 42 + done <"$path" 43 + 44 + if test -z "$mansection" 45 + then 46 + echo "No man section found in $path" >&2 47 + exit 1 48 + fi 49 + 50 + if test -z "$description" 51 + then 52 + echo >&2 "No description found in $path" 53 + exit 1 54 + fi 55 + 56 + case "$description" in 57 + "$command - "*) 58 + text="${description#$command - }" 59 + 60 + printf "linkgit:%s[%s]::\n\t" "$command" "$mansection" 61 + case "$attributes" in 62 + *" deprecated "*) 63 + printf "(deprecated) " 64 + ;; 65 + esac 66 + printf "$text.\n\n" 67 + ;; 68 + *) 69 + echo >&2 "Description does not match $command: $description" 70 + exit 1 71 + ;; 72 + esac 73 + } 74 + 75 + source_dir="$1" 76 + build_dir="$2" 77 + shift 2 78 + 79 + for out 80 + do 81 + category="${out#cmds-}" 82 + category="${category%.adoc}" 83 + path="$build_dir/$out" 84 + 85 + while read command command_category attributes 86 + do 87 + case "$command" in 88 + "#"*) 89 + continue;; 90 + esac 91 + 92 + case "$command_category" in 93 + "$category") 94 + format_one "$source_dir" "$command" " $attributes ";; 95 + esac 96 + done <"$source_dir/command-list.txt" >"$build_dir/$out+" 97 + 98 + if cmp "$build_dir/$out+" "$build_dir/$out" >/dev/null 2>&1 99 + then 100 + rm "$build_dir/$out+" 101 + else 102 + mv "$build_dir/$out+" "$build_dir/$out" 103 + fi 104 + done
+2 -2
Documentation/meson.build
··· 315 315 316 316 documentation_deps += custom_target( 317 317 command: [ 318 - perl, 318 + shell, 319 319 '@INPUT@', 320 320 meson.project_source_root(), 321 321 meson.current_build_dir(), 322 322 ] + cmd_lists, 323 - input: 'cmd-list.perl', 323 + input: 'cmd-list.sh', 324 324 output: cmd_lists 325 325 ) 326 326
+1 -1
meson.build
··· 779 779 # features. It is optional if you want to neither execute tests nor use any of 780 780 # these optional features. 781 781 perl_required = get_option('perl') 782 - if get_option('gitweb').enabled() or 'netrc' in get_option('credential_helpers') or get_option('docs') != [] 782 + if get_option('gitweb').enabled() or 'netrc' in get_option('credential_helpers') 783 783 perl_required = true 784 784 endif 785 785