{"thread":{"id":"63296","subject":"[PATCH 0/4] Drop Perl dependency in a couple of subsystems","startedAt":"2025-04-15T09:57:17Z","lastAt":"2025-04-16T15:08:08Z","messageCount":14,"participants":["Patrick Steinhardt","Junio C Hamano"],"isPatch":true,"patchVersion":1,"patchTotal":4},"messages":[{"id":"516179","messageId":"20250415-b4-pks-drop-perl-v1-0-c6addf175858@pks.im","threadId":"63296","inReplyTo":null,"subject":"[PATCH 0/4] Drop Perl dependency in a couple of subsystems","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-15T09:57:07Z","receivedAt":"2025-04-15T09:57:17Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis patch series is a follow-up to my recent series that got rid of our\nstrict dependency on Perl for running our test suite [1]. The goal of it\nis to remove the dependency on Perl in a couple more areas:\n\n  - git-filter-branch(1)\n\n  - git-request-pull(1)\n\n  - Building our documentation.\n\nWith these converted you can get a fully-functioning Git distribution\nwith only a small handful of features missing:\n\n  - gitweb.\n\n  - Scripts to import repositories from GNU Arch, CVS and Subversion.\n\n  - git-send-email.\n\n  - Our Perl bindings, obviously.\n\n  - The netrc credential helper from \"contrib/\".\n\nNone of these feel critical for day-to-day usage of Git, so Perl is now\na truly-optional dependency of Git.\n\nThe series is built on top of 485f5f86361 (The fourth batch, 2025-04-08)\nwith ps/test-wo-perl-prereq at 7a7b6022670 (t5703: refactor test to not\ndepend on Perl, 2025-04-03) merged into it.\n\nThanks!\n\nPatrick\n\n[1]: <20250320-b4-pks-t-perlless-v1-0-b1eefe27ac55@pks.im>\n\n---\nPatrick Steinhardt (4):\n      filter-branch: stop depending on Perl\n      request-pull: stop depending on Perl\n      Documentation: stop depending on Perl to massage user manual\n      Documentation: stop depending on Perl to generate command list\n\n Documentation/Makefile      |   8 ++--\n Documentation/cmd-list.perl |  80 ----------------------------------\n Documentation/cmd-list.sh   | 104 ++++++++++++++++++++++++++++++++++++++++++++\n Documentation/fix-texi.perl |  15 -------\n Documentation/fix-texi.sh   |  21 +++++++++\n Documentation/meson.build   |   4 +-\n git-filter-branch.sh        |  37 ++++++++--------\n git-request-pull.sh         |  74 ++++++++++++++++---------------\n meson.build                 |   2 +-\n t/t5150-request-pull.sh     |   6 ---\n 10 files changed, 191 insertions(+), 160 deletions(-)\n\n\n---\nbase-commit: b09b3d4791ed248fed65de18cd6d8c084f7f0280\nchange-id: 20250409-b4-pks-drop-perl-574571234e3c\n\n"},{"id":"516180","messageId":"20250415-b4-pks-drop-perl-v1-1-c6addf175858@pks.im","threadId":"63296","inReplyTo":"20250415-b4-pks-drop-perl-v1-0-c6addf175858@pks.im","subject":"[PATCH 1/4] filter-branch: stop depending on Perl","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-15T09:57:08Z","receivedAt":"2025-04-15T09:57:18Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While git-filter-branch(1) is written as a shell script, the\n`--state-branch` feature depends on Perl to save and extract the object\nID mappings. This can lead to subtle breakage though:\n\n  - We execute `perl` directly without respecting the `PERL_PATH`\n    configured by the distribution. As such, it may happen that we use\n    the wrong version of Perl.\n\n  - We install the script unchanged even if Perl isn't available at all\n    on the system, so using `--state-branch` would lead to failure\n    altogether in that case.\n\nFix this by dropping Perl and instead implementing the feature with\nshell scripting exclusively.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n git-filter-branch.sh | 37 +++++++++++++++++++------------------\n 1 file changed, 19 insertions(+), 18 deletions(-)\n\ndiff --git a/git-filter-branch.sh b/git-filter-branch.sh\nindex 3a51d4507c7..24fa317aaaa 100755\n--- a/git-filter-branch.sh\n+++ b/git-filter-branch.sh\n@@ -295,15 +295,18 @@ then\n \tif test -n \"$state_commit\"\n \tthen\n \t\techo \"Populating map from $state_branch ($state_commit)\" 1>&2\n-\t\tperl -e'open(MAP, \"-|\", \"git show $ARGV[0]:filter.map\") or die;\n-\t\t\twhile (<MAP>) {\n-\t\t\t\tm/(.*):(.*)/ or die;\n-\t\t\t\topen F, \">../map/$1\" or die;\n-\t\t\t\tprint F \"$2\" or die;\n-\t\t\t\tclose(F) or die;\n-\t\t\t}\n-\t\t\tclose(MAP) or die;' \"$state_commit\" \\\n-\t\t\t\t|| die \"Unable to load state from $state_branch:filter.map\"\n+\n+\t\tgit show \"$state_commit:filter.map\" >\"$tempdir\"/filter-map ||\n+\t\t\tdie \"Unable to load state from $state_branch:filter.map\"\n+\t\twhile read line\n+\t\tdo\n+\t\t\tcase \"$line\" in\n+\t\t\t*:*)\n+\t\t\t\techo \"${line%:*}\" >../map/\"${line#*:}\";;\n+\t\t\t*)\n+\t\t\t\tdie \"Unable to load state from $state_branch:filter.map\";;\n+\t\t\tesac\n+\t\tdone <\"$tempdir\"/filter-map\n \telse\n \t\techo \"Branch $state_branch does not exist. Will create\" 1>&2\n \tfi\n@@ -633,15 +636,13 @@ if test -n \"$state_branch\"\n then\n \techo \"Saving rewrite state to $state_branch\" 1>&2\n \tstate_blob=$(\n-\t\tperl -e'opendir D, \"../map\" or die;\n-\t\t\topen H, \"|-\", \"git hash-object -w --stdin\" or die;\n-\t\t\tforeach (sort readdir(D)) {\n-\t\t\t\tnext if m/^\\.\\.?$/;\n-\t\t\t\topen F, \"<../map/$_\" or die;\n-\t\t\t\tchomp($f = <F>);\n-\t\t\t\tprint H \"$_:$f\\n\" or die;\n-\t\t\t}\n-\t\t\tclose(H) or die;' || die \"Unable to save state\")\n+\t\tfor file in ../map/*\n+\t\tdo\n+\t\t\tfrom_commit=$(basename \"$file\")\n+\t\t\tto_commit=$(cat \"$file\")\n+\t\t\techo \"$from_commit:$to_commit\"\n+\t\tdone | git hash-object -w --stdin || die \"Unable to save state\"\n+\t)\n \tstate_tree=$(printf '100644 blob %s\\tfilter.map\\n' \"$state_blob\" | git mktree)\n \tif test -n \"$state_commit\"\n \tthen\n\n-- \n2.49.0.805.g082f7c87e0.dirty\n\n"},{"id":"516181","messageId":"20250415-b4-pks-drop-perl-v1-2-c6addf175858@pks.im","threadId":"63296","inReplyTo":"20250415-b4-pks-drop-perl-v1-0-c6addf175858@pks.im","subject":"[PATCH 2/4] request-pull: stop depending on Perl","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-15T09:57:09Z","receivedAt":"2025-04-15T09:57:19Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While git-request-pull(1) is written as a shell script, for it to\nfunction we depend on Perl being available. The script gets installed\nunconditionally though, regardless of whether or not Perl is even\navailable on the system. When it's not available, the `@PERL_PATH@`\nvariable may be substituted with a nonexistent executable path and thus\ncause the script to fail.\n\nRefactor the script so that it does not depend on Perl at all anymore.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n git-request-pull.sh     | 74 ++++++++++++++++++++++++++-----------------------\n t/t5150-request-pull.sh |  6 ----\n 2 files changed, 40 insertions(+), 40 deletions(-)\n\ndiff --git a/git-request-pull.sh b/git-request-pull.sh\nindex 775ba8ea11a..59276fe265b 100755\n--- a/git-request-pull.sh\n+++ b/git-request-pull.sh\n@@ -78,41 +78,47 @@ fi\n merge_base=$(git merge-base $baserev $headrev) ||\n die \"fatal: No commits in common between $base and $head\"\n \n-# $head is the refname from the command line.\n-# Find a ref with the same name as $head that exists at the remote\n+find_matching_ref () {\n+\twhile read sha1 ref\n+\tdo\n+\t\tcase \"$ref\" in\n+\t\t*\"^{}\")\n+\t\t\tref=\"${ref%\"^{}\"}\"\n+\t\t\tderef=true\n+\t\t\t;;\n+\t\t*)\n+\t\t\tderef=\n+\t\t\t;;\n+\t\tesac\n+\n+\t\tif test \"$sha1\" = \"${remote:-HEAD}\"\n+\t\tthen\n+\t\t\techo \"$sha1 $sha1\"\n+\t\t\tbreak\n+\t\tfi\n+\n+\t\tcase \"$ref\" in\n+\t\t\"${remote:-HEAD}\"|*\"/${remote:-HEAD}\")\n+\t\t\tif test -z \"$deref\"\n+\t\t\tthen\n+\t\t\t\t# Remember the matching unpeeled object on the\n+\t\t\t\t# remote side.\n+\t\t\t\tremote_sha1=\"$sha1\"\n+\t\t\tfi\n+\n+\t\t\tif test \"$sha1\" = \"$headrev\"\n+\t\t\tthen\n+\t\t\t\techo \"${remote_sha1:-$headrev} $ref\"\n+\t\t\t\tbreak\n+\t\t\tfi\n+\t\t\t;;\n+\t\tesac\n+\tdone\n+}\n+\n+# Find a ref with the same name as $remote that exists at the remote\n # and points to the same commit as the local object.\n-find_matching_ref='\n-\tmy ($head,$headrev) = (@ARGV);\n-\tmy $pattern = qr{/\\Q$head\\E$};\n-\tmy ($remote_sha1, $found);\n-\n-\twhile (<STDIN>) {\n-\t\tchomp;\n-\t\tmy ($sha1, $ref, $deref) = /^(\\S+)\\s+([^^]+)(\\S*)$/;\n-\n-\t\tif ($sha1 eq $head) {\n-\t\t\t$found = $remote_sha1 = $sha1;\n-\t\t\tbreak;\n-\t\t}\n-\n-\t\tif ($ref eq $head || $ref =~ $pattern) {\n-\t\t\tif ($deref eq \"\") {\n-\t\t\t\t# Remember the matching object on the remote side\n-\t\t\t\t$remote_sha1 = $sha1;\n-\t\t\t}\n-\t\t\tif ($sha1 eq $headrev) {\n-\t\t\t\t$found = $ref;\n-\t\t\t\tbreak;\n-\t\t\t}\n-\t\t}\n-\t}\n-\tif ($found) {\n-\t\t$remote_sha1 = $headrev if ! defined $remote_sha1;\n-\t\tprint \"$remote_sha1 $found\\n\";\n-\t}\n-'\n-\n-set fnord $(git ls-remote \"$url\" | @PERL_PATH@ -e \"$find_matching_ref\" \"${remote:-HEAD}\" \"$headrev\")\n+set fnord $(git ls-remote \"$url\" | find_matching_ref)\n remote_sha1=$2\n ref=$3\n \ndiff --git a/t/t5150-request-pull.sh b/t/t5150-request-pull.sh\nindex cb67bac1c47..270ce6ea487 100755\n--- a/t/t5150-request-pull.sh\n+++ b/t/t5150-request-pull.sh\n@@ -7,12 +7,6 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n . ./test-lib.sh\n \n-if ! test_have_prereq PERL\n-then\n-\tskip_all='skipping request-pull tests, perl not available'\n-\ttest_done\n-fi\n-\n test_expect_success 'setup' '\n \n \tgit init --bare upstream.git &&\n\n-- \n2.49.0.805.g082f7c87e0.dirty\n\n"},{"id":"516182","messageId":"20250415-b4-pks-drop-perl-v1-3-c6addf175858@pks.im","threadId":"63296","inReplyTo":"20250415-b4-pks-drop-perl-v1-0-c6addf175858@pks.im","subject":"[PATCH 3/4] Documentation: stop depending on Perl to massage user manual","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-15T09:57:10Z","receivedAt":"2025-04-15T09:57:19Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"fix-texi.perl\" script is used to fix up the output of\n`docbook2x-texi`:\n\n  - It changes the filename to be \"git.info\".\n\n  - It changes the directory category and entry.\n\nThe script is written in Perl, but it can be rather trivially converted\nto a shell script. Do so to remove the dependency on Perl for building\nthe user manual.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/Makefile      |  4 ++--\n Documentation/fix-texi.perl | 15 ---------------\n Documentation/fix-texi.sh   | 21 +++++++++++++++++++++\n 3 files changed, 23 insertions(+), 17 deletions(-)\n\ndiff --git a/Documentation/Makefile b/Documentation/Makefile\nindex 0d3a2c6bfe6..6485d40f620 100644\n--- a/Documentation/Makefile\n+++ b/Documentation/Makefile\n@@ -398,9 +398,9 @@ user-manual.html: user-manual.xml $(XSLT)\n git.info: user-manual.texi\n \t$(QUIET_MAKEINFO)$(MAKEINFO) --no-split -o $@ user-manual.texi\n \n-user-manual.texi: user-manual.xml\n+user-manual.texi: user-manual.xml fix-texi.sh\n \t$(QUIET_DB2TEXI)$(DOCBOOK2X_TEXI) user-manual.xml --encoding=UTF-8 --to-stdout >$@+ && \\\n-\t$(PERL_PATH) fix-texi.perl <$@+ >$@ && \\\n+\t$(SHELL_PATH) fix-texi.sh <$@+ >$@ && \\\n \t$(RM) $@+\n \n user-manual.pdf: user-manual.xml\ndiff --git a/Documentation/fix-texi.perl b/Documentation/fix-texi.perl\ndeleted file mode 100755\nindex ff7d78f620a..00000000000\n--- a/Documentation/fix-texi.perl\n+++ /dev/null\n@@ -1,15 +0,0 @@\n-#!/usr/bin/perl -w\n-\n-while (<>) {\n-\tif (/^\\@setfilename/) {\n-\t\t$_ = \"\\@setfilename git.info\\n\";\n-\t} elsif (/^\\@direntry/) {\n-\t\tprint '@dircategory Development\n-@direntry\n-* Git: (git).           A fast distributed revision control system\n-@end direntry\n-';\t}\n-\tunless (/^\\@direntry/../^\\@end direntry/) {\n-\t\tprint;\n-\t}\n-}\ndiff --git a/Documentation/fix-texi.sh b/Documentation/fix-texi.sh\nnew file mode 100755\nindex 00000000000..bc300f7b0f1\n--- /dev/null\n+++ b/Documentation/fix-texi.sh\n@@ -0,0 +1,21 @@\n+#!/bin/sh\n+\n+awk '\n+\t/^@setfilename/{\n+\t\tprint \"@setfilename git.info\"\n+\t\tnext\n+\t}\n+\t/^@direntry/{\n+\t\tdirentry=1\n+\t\tprint \"@dircategory Development\"\n+\t\tprint \"@direntry\"\n+\t\tprint \"* Git: (git).           A fast distributed revision control system\"\n+\t\tprint \"@end direntry\"\n+\t\tnext\n+\t}\n+\t/^@end direntry/{\n+\t\tdirentry=0\n+\t\tnext\n+\t}\n+\t!direntry\n+'\n\n-- \n2.49.0.805.g082f7c87e0.dirty\n\n"},{"id":"516183","messageId":"20250415-b4-pks-drop-perl-v1-4-c6addf175858@pks.im","threadId":"63296","inReplyTo":"20250415-b4-pks-drop-perl-v1-0-c6addf175858@pks.im","subject":"[PATCH 4/4] Documentation: stop depending on Perl to generate command list","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-15T09:57:11Z","receivedAt":"2025-04-15T09:57:20Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"cmd-list.perl\" script is used to extract the list of commands part\nof a specific category and extracts the description of each command from\nits respective manpage. The generated output is then included in git(1)\nto list all Git commands.\n\nThe script is written in Perl. Refactor it to use shell scripting\nexclusively so that we can get rid of the mandatory dependency on Perl\nto build our documentation.\n\nThe converted script is slower compared to its Perl implementation. But\nby being careful and not spawning external commands in `format_one ()`\nwe can mitigate the performance hit to a reasonable level:\n\n    Benchmark 1: Perl\n      Time (mean ± σ):      10.3 ms ±   0.2 ms    [User: 7.0 ms, System: 3.3 ms]\n      Range (min … max):    10.0 ms …  11.1 ms    200 runs\n\n    Benchmark 2: Shell\n      Time (mean ± σ):      74.4 ms ±   0.4 ms    [User: 48.6 ms, System: 24.7 ms]\n      Range (min … max):    73.1 ms …  75.5 ms    200 runs\n\n    Summary\n      Perl ran\n        7.23 ± 0.13 times faster than Shell\n\nWhile a sevenfold slowdown is significant, the benefit of not requiring\nPerl for a fully-functioning Git installation outweighs waiting a couple\nof milliseconds longer during the build process.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/Makefile      |   4 +-\n Documentation/cmd-list.perl |  80 ----------------------------------\n Documentation/cmd-list.sh   | 104 ++++++++++++++++++++++++++++++++++++++++++++\n Documentation/meson.build   |   4 +-\n meson.build                 |   2 +-\n 5 files changed, 109 insertions(+), 85 deletions(-)\n\ndiff --git a/Documentation/Makefile b/Documentation/Makefile\nindex 6485d40f620..b109d25e9c8 100644\n--- a/Documentation/Makefile\n+++ b/Documentation/Makefile\n@@ -317,8 +317,8 @@ cmds_txt = cmds-ancillaryinterrogators.adoc \\\n \n $(cmds_txt): cmd-list.made\n \n-cmd-list.made: cmd-list.perl ../command-list.txt $(MAN1_TXT)\n-\t$(QUIET_GEN)$(PERL_PATH) ./cmd-list.perl .. . $(cmds_txt) && \\\n+cmd-list.made: cmd-list.sh ../command-list.txt $(MAN1_TXT)\n+\t$(QUIET_GEN)$(SHELL_PATH) ./cmd-list.sh .. . $(cmds_txt) && \\\n \tdate >$@\n \n mergetools-%.adoc: generate-mergetool-list.sh ../git-mergetool--lib.sh $(wildcard ../mergetools/*)\ndiff --git a/Documentation/cmd-list.perl b/Documentation/cmd-list.perl\ndeleted file mode 100755\nindex 0a0c1b3f611..00000000000\n--- a/Documentation/cmd-list.perl\n+++ /dev/null\n@@ -1,80 +0,0 @@\n-#!/usr/bin/perl -w\n-\n-use File::Compare qw(compare);\n-\n-sub format_one {\n-\tmy ($source_dir, $out, $nameattr) = @_;\n-\tmy ($name, $attr) = @$nameattr;\n-\tmy ($path) = \"$source_dir/Documentation/$name.adoc\";\n-\tmy ($state, $description);\n-\tmy $mansection;\n-\t$state = 0;\n-\topen I, '<', \"$path\" or die \"No such file $path.adoc\";\n-\twhile (<I>) {\n-\t\tif (/^(?:git|scalar)[a-z0-9-]*\\(([0-9])\\)$/) {\n-\t\t\t$mansection = $1;\n-\t\t\tnext;\n-\t\t}\n-\t\tif (/^NAME$/) {\n-\t\t\t$state = 1;\n-\t\t\tnext;\n-\t\t}\n-\t\tif ($state == 1 && /^----$/) {\n-\t\t\t$state = 2;\n-\t\t\tnext;\n-\t\t}\n-\t\tnext if ($state != 2);\n-\t\tchomp;\n-\t\t$description = $_;\n-\t\tlast;\n-\t}\n-\tclose I;\n-\tif (!defined $description) {\n-\t\tdie \"No description found in $path.adoc\";\n-\t}\n-\tif (my ($verify_name, $text) = ($description =~ /^($name) - (.*)/)) {\n-\t\tprint $out \"linkgit:$name\\[$mansection\\]::\\n\\t\";\n-\t\tif ($attr =~ / deprecated /) {\n-\t\t\tprint $out \"(deprecated) \";\n-\t\t}\n-\t\tprint $out \"$text.\\n\\n\";\n-\t}\n-\telse {\n-\t\tdie \"Description does not match $name: $description\";\n-\t}\n-}\n-\n-my ($source_dir, $build_dir, @categories) = @ARGV;\n-\n-open IN, \"<$source_dir/command-list.txt\";\n-while (<IN>) {\n-\tlast if /^### command list/;\n-}\n-\n-my %cmds = ();\n-for (sort <IN>) {\n-\tnext if /^#/;\n-\n-\tchomp;\n-\tmy ($name, $cat, $attr) = /^(\\S+)\\s+(.*?)(?:\\s+(.*))?$/;\n-\t$attr = '' unless defined $attr;\n-\tpush @{$cmds{$cat}}, [$name, \" $attr \"];\n-}\n-close IN;\n-\n-for my $out (@categories) {\n-\tmy ($cat) = $out =~ /^cmds-(.*)\\.adoc$/;\n-\tmy ($path) = \"$build_dir/$out\";\n-\topen O, '>', \"$path+\" or die \"Cannot open output file $out+\";\n-\tfor (@{$cmds{$cat}}) {\n-\t\tformat_one($source_dir, \\*O, $_);\n-\t}\n-\tclose O;\n-\n-\tif (-f \"$path\" && compare(\"$path\", \"$path+\") == 0) {\n-\t\tunlink \"$path+\";\n-\t}\n-\telse {\n-\t\trename \"$path+\", \"$path\";\n-\t}\n-}\ndiff --git a/Documentation/cmd-list.sh b/Documentation/cmd-list.sh\nnew file mode 100755\nindex 00000000000..fa90781f3c7\n--- /dev/null\n+++ b/Documentation/cmd-list.sh\n@@ -0,0 +1,104 @@\n+#!/bin/sh\n+\n+set -e\n+\n+format_one () {\n+\tsource_dir=\"$1\"\n+\tcommand=\"$2\"\n+\tattributes=\"$3\"\n+\n+\tpath=\"$source_dir/Documentation/$command.adoc\"\n+\tif ! test -f \"$path\"\n+\tthen\n+\t\techo >&2 \"No such file $path\"\n+\t\texit 1\n+\tfi\n+\n+\tstate=0\n+\twhile read line\n+\tdo\n+\t\tcase \"$state\" in\n+\t\t\t0)\n+\t\t\t\tcase \"$line\" in\n+\t\t\t\tgit*\\(*\\)|scalar*\\(*\\))\n+\t\t\t\t\tmansection=\"${line##*\\(}\"\n+\t\t\t\t\tmansection=\"${mansection%\\)}\"\n+\t\t\t\t\t;;\n+\t\t\t\tNAME)\n+\t\t\t\t\tstate=1;;\n+\t\t\t\tesac\n+\t\t\t\t;;\n+\t\t\t1)\n+\t\t\t\tif test \"$line\" = \"----\"\n+\t\t\t\tthen\n+\t\t\t\t\tstate=2\n+\t\t\t\tfi\n+\t\t\t\t;;\n+\t\t\t2)\n+\t\t\t\tdescription=\"$line\"\n+\t\t\t\tbreak\n+\t\t\t\t;;\n+\t\tesac\n+\tdone <\"$path\"\n+\n+\tif test -z \"$mansection\"\n+\tthen\n+\t\techo \"No man section found in $path\" >&2\n+\t\texit 1\n+\tfi\n+\n+\tif test -z \"$description\"\n+\tthen\n+\t\techo >&2 \"No description found in $path\"\n+\t\texit 1\n+\tfi\n+\n+\tcase \"$description\" in\n+\t\t\"$command - \"*)\n+\t\t\ttext=\"${description#$command - }\"\n+\n+\t\t\tprintf \"linkgit:%s[%s]::\\n\\t\" \"$command\" \"$mansection\"\n+\t\t\tcase \"$attributes\" in\n+\t\t\t\t*\" deprecated \"*)\n+\t\t\t\t\tprintf \"(deprecated) \"\n+\t\t\t\t\t;;\n+\t\t\tesac\n+\t\t\tprintf \"$text.\\n\\n\"\n+\t\t\t;;\n+\t\t*)\n+\t\t\techo >&2 \"Description does not match $command: $description\"\n+\t\t\texit 1\n+\t\t\t;;\n+\tesac\n+}\n+\n+source_dir=\"$1\"\n+build_dir=\"$2\"\n+shift 2\n+\n+for out in \"$@\"\n+do\n+\tcategory=\"${out#cmds-}\"\n+\tcategory=\"${category%.adoc}\"\n+\tpath=\"$build_dir/$out\"\n+\n+\twhile read command command_category attributes\n+\tdo\n+\t\tcase \"$command\" in\n+\t\t\"#\"*)\n+\t\t\tcontinue;;\n+\t\tesac\n+\n+\t\tcase \"$command_category\" in\n+\t\t\"$category\")\n+\t\t\tformat_one \"$source_dir\" \"$command\" \" $attributes \";;\n+\t\tesac\n+\tdone <\"$source_dir/command-list.txt\" >\"$build_dir/$out+\"\n+\n+\tif cmp \"$build_dir/$out+\" \"$build_dir/$out\" >/dev/null 2>&1\n+\tthen\n+\t\trm \"$build_dir/$out+\"\n+\telse\n+\t\tmv \"$build_dir/$out+\" \"$build_dir/$out\"\n+\tfi\n+done\ndiff --git a/Documentation/meson.build b/Documentation/meson.build\nindex 8b9e692c599..b731c76e9e7 100644\n--- a/Documentation/meson.build\n+++ b/Documentation/meson.build\n@@ -315,12 +315,12 @@ cmd_lists = [\n \n documentation_deps += custom_target(\n   command: [\n-    perl,\n+    shell,\n     '@INPUT@',\n     meson.project_source_root(),\n     meson.current_build_dir(),\n   ] + cmd_lists,\n-  input: 'cmd-list.perl',\n+  input: 'cmd-list.sh',\n   output: cmd_lists\n )\n \ndiff --git a/meson.build b/meson.build\nindex 8bab8f3481f..97753d2cfa7 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -779,7 +779,7 @@ endif\n # features. It is optional if you want to neither execute tests nor use any of\n # these optional features.\n perl_required = get_option('perl')\n-if get_option('gitweb').enabled() or 'netrc' in get_option('credential_helpers') or get_option('docs') != []\n+if get_option('gitweb').enabled() or 'netrc' in get_option('credential_helpers')\n   perl_required = true\n endif\n \n\n-- \n2.49.0.805.g082f7c87e0.dirty\n\n"},{"id":"516204","messageId":"xmqq1ptto245.fsf@gitster.g","threadId":"63296","inReplyTo":"20250415-b4-pks-drop-perl-v1-1-c6addf175858@pks.im","subject":"Re: [PATCH 1/4] filter-branch: stop depending on Perl","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-04-15T15:50:34Z","receivedAt":"2025-04-15T15:50:37Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> While git-filter-branch(1) is written as a shell script, the\n> `--state-branch` feature depends on Perl to save and extract the object\n> ID mappings. This can lead to subtle breakage though:\n>\n>   - We execute `perl` directly without respecting the `PERL_PATH`\n>     configured by the distribution. As such, it may happen that we use\n>     the wrong version of Perl.\n>\n>   - We install the script unchanged even if Perl isn't available at all\n>     on the system, so using `--state-branch` would lead to failure\n>     altogether in that case.\n>\n> Fix this by dropping Perl and instead implementing the feature with\n> shell scripting exclusively.\n\nExcellent.\n\nI just realized that the first bullet point above is a good argument\nagainst \"#!/usr/bin/env perl\" as well.  If we were to go the\n\"#!/usr/bin/env perl\" direction, we should stop pretending with\n$PERL_PATH that we'd consistently use the same version of Perl\nchosen at build-install time, and we just trust users' $PATH, for\nconsistency.\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  git-filter-branch.sh | 37 +++++++++++++++++++------------------\n>  1 file changed, 19 insertions(+), 18 deletions(-)\n>\n> diff --git a/git-filter-branch.sh b/git-filter-branch.sh\n> index 3a51d4507c7..24fa317aaaa 100755\n> --- a/git-filter-branch.sh\n> +++ b/git-filter-branch.sh\n> @@ -295,15 +295,18 @@ then\n>  \tif test -n \"$state_commit\"\n>  \tthen\n>  \t\techo \"Populating map from $state_branch ($state_commit)\" 1>&2\n> -\t\tperl -e'open(MAP, \"-|\", \"git show $ARGV[0]:filter.map\") or die;\n> -\t\t\twhile (<MAP>) {\n> -\t\t\t\tm/(.*):(.*)/ or die;\n> -\t\t\t\topen F, \">../map/$1\" or die;\n> -\t\t\t\tprint F \"$2\" or die;\n> -\t\t\t\tclose(F) or die;\n> -\t\t\t}\n> -\t\t\tclose(MAP) or die;' \"$state_commit\" \\\n> -\t\t\t\t|| die \"Unable to load state from $state_branch:filter.map\"\n> +\n> +\t\tgit show \"$state_commit:filter.map\" >\"$tempdir\"/filter-map ||\n> +\t\t\tdie \"Unable to load state from $state_branch:filter.map\"\n> +\t\twhile read line\n> +\t\tdo\n> +\t\t\tcase \"$line\" in\n> +\t\t\t*:*)\n> +\t\t\t\techo \"${line%:*}\" >../map/\"${line#*:}\";;\n> +\t\t\t*)\n> +\t\t\t\tdie \"Unable to load state from $state_branch:filter.map\";;\n> +\t\t\tesac\n> +\t\tdone <\"$tempdir\"/filter-map\n\nQuite straight-forward.  Do problematic bytes like backslashes\nappear in the blob that Perl m/(.*):(.*)/ would have passed intact\nwithout problems but may cause problems to the reimplementation?  I\ndoubt it.\n\nNot in a scope of this change, but use of \"git show\" in place of\n\"git cat-file blob\" makes my skin somewhat tingle.\n\n>  \telse\n>  \t\techo \"Branch $state_branch does not exist. Will create\" 1>&2\n>  \tfi\n> @@ -633,15 +636,13 @@ if test -n \"$state_branch\"\n>  then\n>  \techo \"Saving rewrite state to $state_branch\" 1>&2\n>  \tstate_blob=$(\n> -\t\tperl -e'opendir D, \"../map\" or die;\n> -\t\t\topen H, \"|-\", \"git hash-object -w --stdin\" or die;\n> -\t\t\tforeach (sort readdir(D)) {\n> -\t\t\t\tnext if m/^\\.\\.?$/;\n> -\t\t\t\topen F, \"<../map/$_\" or die;\n> -\t\t\t\tchomp($f = <F>);\n> -\t\t\t\tprint H \"$_:$f\\n\" or die;\n> -\t\t\t}\n> -\t\t\tclose(H) or die;' || die \"Unable to save state\")\n> +\t\tfor file in ../map/*\n\nOK.  * won't match . or .. so we no longer need to filter.  Like the\noriginal, if we had any random cruft file, we may copy such garbage\nto the output, but that is not a new problem at all.\n\nNicely done.\n\n> +\t\tdo\n> +\t\t\tfrom_commit=$(basename \"$file\")\n> +\t\t\tto_commit=$(cat \"$file\")\n> +\t\t\techo \"$from_commit:$to_commit\"\n> +\t\tdone | git hash-object -w --stdin || die \"Unable to save state\"\n> +\t)\n>  \tstate_tree=$(printf '100644 blob %s\\tfilter.map\\n' \"$state_blob\" | git mktree)\n>  \tif test -n \"$state_commit\"\n>  \tthen\n"},{"id":"516207","messageId":"xmqqtt6pmmd6.fsf@gitster.g","threadId":"63296","inReplyTo":"20250415-b4-pks-drop-perl-v1-2-c6addf175858@pks.im","subject":"Re: [PATCH 2/4] request-pull: stop depending on Perl","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-04-15T16:16:05Z","receivedAt":"2025-04-15T16:16:08Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> While git-request-pull(1) is written as a shell script, for it to\n> function we depend on Perl being available. The script gets installed\n> unconditionally though, regardless of whether or not Perl is even\n> available on the system. When it's not available, the `@PERL_PATH@`\n> variable may be substituted with a nonexistent executable path and thus\n> cause the script to fail.\n>\n> Refactor the script so that it does not depend on Perl at all anymore.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  git-request-pull.sh     | 74 ++++++++++++++++++++++++++-----------------------\n>  t/t5150-request-pull.sh |  6 ----\n>  2 files changed, 40 insertions(+), 40 deletions(-)\n>\n> diff --git a/git-request-pull.sh b/git-request-pull.sh\n> index 775ba8ea11a..59276fe265b 100755\n> --- a/git-request-pull.sh\n> +++ b/git-request-pull.sh\n> @@ -78,41 +78,47 @@ fi\n>  merge_base=$(git merge-base $baserev $headrev) ||\n>  die \"fatal: No commits in common between $base and $head\"\n>  \n> -# $head is the refname from the command line.\n> -# Find a ref with the same name as $head that exists at the remote\n> +find_matching_ref () {\n> +\twhile read sha1 ref\n> +\tdo\n> +\t\tcase \"$ref\" in\n> +\t\t*\"^{}\")\n> +\t\t\tref=\"${ref%\"^{}\"}\"\n> +\t\t\tderef=true\n\nThis got a bit tighter (the original allowed ls-remote output to be\nlater extended to throw \"^something-else\" at the end of line), which\nI do not know is something we need to worry about.  I think retaining\nthe original semantics is easy in this case, e.g.,\n\n\t\t*\"^\"?*)\n\t\t\tref=\"${ref%\"^*\"}\"\n\nor something, if we wanted to.  As this is meant to be faithful\nrewrite to lose Perl, not a bugfix to correct overly loose pattern\nmatching in the original, I think we want to in this case.\n\n> +\t\t\t;;\n> +\t\t*)\n> +\t\t\tderef=\n> +\t\t\t;;\n> +\t\tesac\n> +\n> +\t\tif test \"$sha1\" = \"${remote:-HEAD}\"\n> +\t\tthen\n> +\t\t\techo \"$sha1 $sha1\"\n> +\t\t\tbreak\n> +\t\tfi\n> +\n> +\t\tcase \"$ref\" in\n> +\t\t\"${remote:-HEAD}\"|*\"/${remote:-HEAD}\")\n> +\t\t\tif test -z \"$deref\"\n> +\t\t\tthen\n> +\t\t\t\t# Remember the matching unpeeled object on the\n> +\t\t\t\t# remote side.\n> +\t\t\t\tremote_sha1=\"$sha1\"\n> +\t\t\tfi\n> +\n> +\t\t\tif test \"$sha1\" = \"$headrev\"\n> +\t\t\tthen\n> +\t\t\t\techo \"${remote_sha1:-$headrev} $ref\"\n> +\t\t\t\tbreak\n> +\t\t\tfi\n> +\t\t\t;;\n> +\t\tesac\n> +\tdone\n> +}\n> +\n> +# Find a ref with the same name as $remote that exists at the remote\n>  # and points to the same commit as the local object.\n> ...\n> -set fnord $(git ls-remote \"$url\" | @PERL_PATH@ -e \"$find_matching_ref\" \"${remote:-HEAD}\" \"$headrev\")\n> +set fnord $(git ls-remote \"$url\" | find_matching_ref)\n>  remote_sha1=$2\n>  ref=$3\n\nThis is faithful to the original, and having find_matching_ref in\nthe process substitution means we do not have to worry about\nlocalizing variables in there.  Good.\n\n\n"},{"id":"516209","messageId":"xmqqmschmllq.fsf@gitster.g","threadId":"63296","inReplyTo":"20250415-b4-pks-drop-perl-v1-4-c6addf175858@pks.im","subject":"Re: [PATCH 4/4] Documentation: stop depending on Perl to generate command list","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-04-15T16:32:33Z","receivedAt":"2025-04-15T16:32:36Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> The \"cmd-list.perl\" script is used to extract the list of commands part\n> of a specific category and extracts the description of each command from\n> its respective manpage. The generated output is then included in git(1)\n> to list all Git commands.\n>\n> The script is written in Perl. Refactor it to use shell scripting\n> exclusively so that we can get rid of the mandatory dependency on Perl\n> to build our documentation.\n>\n> The converted script is slower compared to its Perl implementation. But\n> by being careful and not spawning external commands in `format_one ()`\n> we can mitigate the performance hit to a reasonable level:\n>\n>     Benchmark 1: Perl\n>       Time (mean ± σ):      10.3 ms ±   0.2 ms    [User: 7.0 ms, System: 3.3 ms]\n>       Range (min … max):    10.0 ms …  11.1 ms    200 runs\n>\n>     Benchmark 2: Shell\n>       Time (mean ± σ):      74.4 ms ±   0.4 ms    [User: 48.6 ms, System: 24.7 ms]\n>       Range (min … max):    73.1 ms …  75.5 ms    200 runs\n>\n>     Summary\n>       Perl ran\n>         7.23 ± 0.13 times faster than Shell\n>\n> While a sevenfold slowdown is significant, the benefit of not requiring\n> Perl for a fully-functioning Git installation outweighs waiting a couple\n> of milliseconds longer during the build process.\n\nI personally do not feel Perl such a drag but whether it is 10ms vs\n75ms, as long as we won't run the script excessively (and either\nmeson or make should be set-up to avoid unnecessary work already), I\ndo not think a shell script being slightly slower than a Perl script\nis a big deal.  Thanks for working on this.\n\n> diff --git a/Documentation/cmd-list.sh b/Documentation/cmd-list.sh\n> new file mode 100755\n> index 00000000000..fa90781f3c7\n> --- /dev/null\n> +++ b/Documentation/cmd-list.sh\n> @@ -0,0 +1,104 @@\n> +#!/bin/sh\n> +\n> +set -e\n> +\n> +format_one () {\n> +\tsource_dir=\"$1\"\n> +\tcommand=\"$2\"\n> +\tattributes=\"$3\"\n> +\n> +\tpath=\"$source_dir/Documentation/$command.adoc\"\n> +\tif ! test -f \"$path\"\n> +\tthen\n> +\t\techo >&2 \"No such file $path\"\n> +\t\texit 1\n> +\tfi\n> +\n> +\tstate=0\n> +\twhile read line\n> +\tdo\n> +\t\tcase \"$state\" in\n> +\t\t\t0)\n\nStyle. label and \"case\" and \"esac\" align, just like ...\n\n> +\t\t\t\tcase \"$line\" in\n> +\t\t\t\tgit*\\(*\\)|scalar*\\(*\\))\n> +\t\t\t\t\tmansection=\"${line##*\\(}\"\n\n... this one.\n\n> +\t\t\t\t\tmansection=\"${mansection%\\)}\"\n> +\t\t\t\t\t;;\n> +\t\t\t\tNAME)\n> +\t\t\t\t\tstate=1;;\n> +\t\t\t\tesac\n> +\t\t\t\t;;\n\n> +\tcase \"$description\" in\n> +\t\t\"$command - \"*)\n\nLikewise.\n\n> +\t\t\ttext=\"${description#$command - }\"\n> +\n> +\t\t\tprintf \"linkgit:%s[%s]::\\n\\t\" \"$command\" \"$mansection\"\n> +\t\t\tcase \"$attributes\" in\n> +\t\t\t\t*\" deprecated \"*)\n\nDitto.\n\n> +\t\t\t\t\tprintf \"(deprecated) \"\n> +\t\t\t\t\t;;\n> +\t\t\tesac\n> +\t\t\tprintf \"$text.\\n\\n\"\n> +\t\t\t;;\n> +\t\t*)\n> +\t\t\techo >&2 \"Description does not match $command: $description\"\n> +\t\t\texit 1\n> +\t\t\t;;\n> +\tesac\n> +}\n> +\n> +source_dir=\"$1\"\n> +build_dir=\"$2\"\n> +shift 2\n> +\n> +for out in \"$@\"\n\nLet's omit 'in \"$@\"' when iterationg over \"$@\".\n\n> +do\n> +\tcategory=\"${out#cmds-}\"\n> +\tcategory=\"${category%.adoc}\"\n> +\tpath=\"$build_dir/$out\"\n> +\n> +\twhile read command command_category attributes\n> +\tdo\n> +\t\tcase \"$command\" in\n> +\t\t\"#\"*)\n> +\t\t\tcontinue;;\n> +\t\tesac\n> +\n> +\t\tcase \"$command_category\" in\n> +\t\t\"$category\")\n> +\t\t\tformat_one \"$source_dir\" \"$command\" \" $attributes \";;\n> +\t\tesac\n> +\tdone <\"$source_dir/command-list.txt\" >\"$build_dir/$out+\"\n> +\n> +\tif cmp \"$build_dir/$out+\" \"$build_dir/$out\" >/dev/null 2>&1\n> +\tthen\n> +\t\trm \"$build_dir/$out+\"\n> +\telse\n> +\t\tmv \"$build_dir/$out+\" \"$build_dir/$out\"\n> +\tfi\n> +done\n\nOK.\n\n"},{"id":"516279","messageId":"20250416-b4-pks-drop-perl-v2-0-bdd0492e9498@pks.im","threadId":"63296","inReplyTo":"20250415-b4-pks-drop-perl-v1-0-c6addf175858@pks.im","subject":"[PATCH v2 0/4] Drop Perl dependency in a couple of subsystems","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-16T12:16:06Z","receivedAt":"2025-04-16T12:16:19Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis patch series is a follow-up to my recent series that got rid of our\nstrict dependency on Perl for running our test suite [1]. The goal of it\nis to remove the dependency on Perl in a couple more areas:\n\n  - git-filter-branch(1)\n\n  - git-request-pull(1)\n\n  - Building our documentation.\n\nWith these converted you can get a fully-functioning Git distribution\nwith only a small handful of features missing:\n\n  - gitweb.\n\n  - Scripts to import repositories from GNU Arch, CVS and Subversion.\n\n  - git-send-email.\n\n  - Our Perl bindings, obviously.\n\n  - The netrc credential helper from \"contrib/\".\n\nNone of these feel critical for day-to-day usage of Git, so Perl is now\na truly-optional dependency of Git.\n\nThe series is built on top of 485f5f86361 (The fourth batch, 2025-04-08)\nwith ps/test-wo-perl-prereq at 7a7b6022670 (t5703: refactor test to not\ndepend on Perl, 2025-04-03) merged into it.\n\nChanges in v2:\n  - Loosen up semantics when parsing \"^{}\" from git-ls-remote(1) so that\n    the new behaviour matches the old one.\n  - Fix indentation of a couple of case statements.\n  - Remove explicit `in \"$@\"` in a for loop.\n  - Link to v1: https://lore.kernel.org/r/20250415-b4-pks-drop-perl-v1-0-c6addf175858@pks.im\n\nThanks!\n\nPatrick\n\n[1]: <20250320-b4-pks-t-perlless-v1-0-b1eefe27ac55@pks.im>\n\n---\nPatrick Steinhardt (4):\n      filter-branch: stop depending on Perl\n      request-pull: stop depending on Perl\n      Documentation: stop depending on Perl to massage user manual\n      Documentation: stop depending on Perl to generate command list\n\n Documentation/Makefile      |   8 ++--\n Documentation/cmd-list.perl |  80 ----------------------------------\n Documentation/cmd-list.sh   | 104 ++++++++++++++++++++++++++++++++++++++++++++\n Documentation/fix-texi.perl |  15 -------\n Documentation/fix-texi.sh   |  21 +++++++++\n Documentation/meson.build   |   4 +-\n git-filter-branch.sh        |  37 ++++++++--------\n git-request-pull.sh         |  74 ++++++++++++++++---------------\n meson.build                 |   2 +-\n t/t5150-request-pull.sh     |   6 ---\n 10 files changed, 191 insertions(+), 160 deletions(-)\n\nRange-diff versus v1:\n\n1:  d0aa04cb2e9 = 1:  b48e33ada07 filter-branch: stop depending on Perl\n2:  0fc604b849d ! 2:  31f54bd63e4 request-pull: stop depending on Perl\n    @@ git-request-pull.sh: fi\n     +\twhile read sha1 ref\n     +\tdo\n     +\t\tcase \"$ref\" in\n    -+\t\t*\"^{}\")\n    -+\t\t\tref=\"${ref%\"^{}\"}\"\n    ++\t\t*\"^\"?*)\n    ++\t\t\tref=\"${ref%\"^\"*}\"\n     +\t\t\tderef=true\n     +\t\t\t;;\n     +\t\t*)\n3:  75dd9df3d83 = 3:  788fb204917 Documentation: stop depending on Perl to massage user manual\n4:  9c4b5c330b4 ! 4:  c770a80762b Documentation: stop depending on Perl to generate command list\n    @@ Documentation/cmd-list.sh (new)\n     +\twhile read line\n     +\tdo\n     +\t\tcase \"$state\" in\n    -+\t\t\t0)\n    -+\t\t\t\tcase \"$line\" in\n    -+\t\t\t\tgit*\\(*\\)|scalar*\\(*\\))\n    -+\t\t\t\t\tmansection=\"${line##*\\(}\"\n    -+\t\t\t\t\tmansection=\"${mansection%\\)}\"\n    -+\t\t\t\t\t;;\n    -+\t\t\t\tNAME)\n    -+\t\t\t\t\tstate=1;;\n    -+\t\t\t\tesac\n    -+\t\t\t\t;;\n    -+\t\t\t1)\n    -+\t\t\t\tif test \"$line\" = \"----\"\n    -+\t\t\t\tthen\n    -+\t\t\t\t\tstate=2\n    -+\t\t\t\tfi\n    -+\t\t\t\t;;\n    -+\t\t\t2)\n    -+\t\t\t\tdescription=\"$line\"\n    -+\t\t\t\tbreak\n    ++\t\t0)\n    ++\t\t\tcase \"$line\" in\n    ++\t\t\tgit*\\(*\\)|scalar*\\(*\\))\n    ++\t\t\t\tmansection=\"${line##*\\(}\"\n    ++\t\t\t\tmansection=\"${mansection%\\)}\"\n     +\t\t\t\t;;\n    ++\t\t\tNAME)\n    ++\t\t\t\tstate=1;;\n    ++\t\t\tesac\n    ++\t\t\t;;\n    ++\t\t1)\n    ++\t\t\tif test \"$line\" = \"----\"\n    ++\t\t\tthen\n    ++\t\t\t\tstate=2\n    ++\t\t\tfi\n    ++\t\t\t;;\n    ++\t\t2)\n    ++\t\t\tdescription=\"$line\"\n    ++\t\t\tbreak\n    ++\t\t\t;;\n     +\t\tesac\n     +\tdone <\"$path\"\n     +\n    @@ Documentation/cmd-list.sh (new)\n     +\tfi\n     +\n     +\tcase \"$description\" in\n    -+\t\t\"$command - \"*)\n    -+\t\t\ttext=\"${description#$command - }\"\n    ++\t\"$command - \"*)\n    ++\t\ttext=\"${description#$command - }\"\n     +\n    -+\t\t\tprintf \"linkgit:%s[%s]::\\n\\t\" \"$command\" \"$mansection\"\n    -+\t\t\tcase \"$attributes\" in\n    -+\t\t\t\t*\" deprecated \"*)\n    -+\t\t\t\t\tprintf \"(deprecated) \"\n    -+\t\t\t\t\t;;\n    -+\t\t\tesac\n    -+\t\t\tprintf \"$text.\\n\\n\"\n    -+\t\t\t;;\n    -+\t\t*)\n    -+\t\t\techo >&2 \"Description does not match $command: $description\"\n    -+\t\t\texit 1\n    ++\t\tprintf \"linkgit:%s[%s]::\\n\\t\" \"$command\" \"$mansection\"\n    ++\t\tcase \"$attributes\" in\n    ++\t\t*\" deprecated \"*)\n    ++\t\t\tprintf \"(deprecated) \"\n     +\t\t\t;;\n    ++\t\tesac\n    ++\t\tprintf \"$text.\\n\\n\"\n    ++\t\t;;\n    ++\t*)\n    ++\t\techo >&2 \"Description does not match $command: $description\"\n    ++\t\texit 1\n    ++\t\t;;\n     +\tesac\n     +}\n     +\n    @@ Documentation/cmd-list.sh (new)\n     +build_dir=\"$2\"\n     +shift 2\n     +\n    -+for out in \"$@\"\n    ++for out\n     +do\n     +\tcategory=\"${out#cmds-}\"\n     +\tcategory=\"${category%.adoc}\"\n\n---\nbase-commit: b09b3d4791ed248fed65de18cd6d8c084f7f0280\nchange-id: 20250409-b4-pks-drop-perl-574571234e3c\n\n"},{"id":"516280","messageId":"20250416-b4-pks-drop-perl-v2-1-bdd0492e9498@pks.im","threadId":"63296","inReplyTo":"20250416-b4-pks-drop-perl-v2-0-bdd0492e9498@pks.im","subject":"[PATCH v2 1/4] filter-branch: stop depending on Perl","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-16T12:16:07Z","receivedAt":"2025-04-16T12:16:20Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While git-filter-branch(1) is written as a shell script, the\n`--state-branch` feature depends on Perl to save and extract the object\nID mappings. This can lead to subtle breakage though:\n\n  - We execute `perl` directly without respecting the `PERL_PATH`\n    configured by the distribution. As such, it may happen that we use\n    the wrong version of Perl.\n\n  - We install the script unchanged even if Perl isn't available at all\n    on the system, so using `--state-branch` would lead to failure\n    altogether in that case.\n\nFix this by dropping Perl and instead implementing the feature with\nshell scripting exclusively.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n git-filter-branch.sh | 37 +++++++++++++++++++------------------\n 1 file changed, 19 insertions(+), 18 deletions(-)\n\ndiff --git a/git-filter-branch.sh b/git-filter-branch.sh\nindex 3a51d4507c7..24fa317aaaa 100755\n--- a/git-filter-branch.sh\n+++ b/git-filter-branch.sh\n@@ -295,15 +295,18 @@ then\n \tif test -n \"$state_commit\"\n \tthen\n \t\techo \"Populating map from $state_branch ($state_commit)\" 1>&2\n-\t\tperl -e'open(MAP, \"-|\", \"git show $ARGV[0]:filter.map\") or die;\n-\t\t\twhile (<MAP>) {\n-\t\t\t\tm/(.*):(.*)/ or die;\n-\t\t\t\topen F, \">../map/$1\" or die;\n-\t\t\t\tprint F \"$2\" or die;\n-\t\t\t\tclose(F) or die;\n-\t\t\t}\n-\t\t\tclose(MAP) or die;' \"$state_commit\" \\\n-\t\t\t\t|| die \"Unable to load state from $state_branch:filter.map\"\n+\n+\t\tgit show \"$state_commit:filter.map\" >\"$tempdir\"/filter-map ||\n+\t\t\tdie \"Unable to load state from $state_branch:filter.map\"\n+\t\twhile read line\n+\t\tdo\n+\t\t\tcase \"$line\" in\n+\t\t\t*:*)\n+\t\t\t\techo \"${line%:*}\" >../map/\"${line#*:}\";;\n+\t\t\t*)\n+\t\t\t\tdie \"Unable to load state from $state_branch:filter.map\";;\n+\t\t\tesac\n+\t\tdone <\"$tempdir\"/filter-map\n \telse\n \t\techo \"Branch $state_branch does not exist. Will create\" 1>&2\n \tfi\n@@ -633,15 +636,13 @@ if test -n \"$state_branch\"\n then\n \techo \"Saving rewrite state to $state_branch\" 1>&2\n \tstate_blob=$(\n-\t\tperl -e'opendir D, \"../map\" or die;\n-\t\t\topen H, \"|-\", \"git hash-object -w --stdin\" or die;\n-\t\t\tforeach (sort readdir(D)) {\n-\t\t\t\tnext if m/^\\.\\.?$/;\n-\t\t\t\topen F, \"<../map/$_\" or die;\n-\t\t\t\tchomp($f = <F>);\n-\t\t\t\tprint H \"$_:$f\\n\" or die;\n-\t\t\t}\n-\t\t\tclose(H) or die;' || die \"Unable to save state\")\n+\t\tfor file in ../map/*\n+\t\tdo\n+\t\t\tfrom_commit=$(basename \"$file\")\n+\t\t\tto_commit=$(cat \"$file\")\n+\t\t\techo \"$from_commit:$to_commit\"\n+\t\tdone | git hash-object -w --stdin || die \"Unable to save state\"\n+\t)\n \tstate_tree=$(printf '100644 blob %s\\tfilter.map\\n' \"$state_blob\" | git mktree)\n \tif test -n \"$state_commit\"\n \tthen\n\n-- \n2.49.0.805.g082f7c87e0.dirty\n\n"},{"id":"516283","messageId":"20250416-b4-pks-drop-perl-v2-2-bdd0492e9498@pks.im","threadId":"63296","inReplyTo":"20250416-b4-pks-drop-perl-v2-0-bdd0492e9498@pks.im","subject":"[PATCH v2 2/4] request-pull: stop depending on Perl","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-16T12:16:08Z","receivedAt":"2025-04-16T12:16:20Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While git-request-pull(1) is written as a shell script, for it to\nfunction we depend on Perl being available. The script gets installed\nunconditionally though, regardless of whether or not Perl is even\navailable on the system. When it's not available, the `@PERL_PATH@`\nvariable may be substituted with a nonexistent executable path and thus\ncause the script to fail.\n\nRefactor the script so that it does not depend on Perl at all anymore.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n git-request-pull.sh     | 74 ++++++++++++++++++++++++++-----------------------\n t/t5150-request-pull.sh |  6 ----\n 2 files changed, 40 insertions(+), 40 deletions(-)\n\ndiff --git a/git-request-pull.sh b/git-request-pull.sh\nindex 775ba8ea11a..6a7b7936784 100755\n--- a/git-request-pull.sh\n+++ b/git-request-pull.sh\n@@ -78,41 +78,47 @@ fi\n merge_base=$(git merge-base $baserev $headrev) ||\n die \"fatal: No commits in common between $base and $head\"\n \n-# $head is the refname from the command line.\n-# Find a ref with the same name as $head that exists at the remote\n+find_matching_ref () {\n+\twhile read sha1 ref\n+\tdo\n+\t\tcase \"$ref\" in\n+\t\t*\"^\"?*)\n+\t\t\tref=\"${ref%\"^\"*}\"\n+\t\t\tderef=true\n+\t\t\t;;\n+\t\t*)\n+\t\t\tderef=\n+\t\t\t;;\n+\t\tesac\n+\n+\t\tif test \"$sha1\" = \"${remote:-HEAD}\"\n+\t\tthen\n+\t\t\techo \"$sha1 $sha1\"\n+\t\t\tbreak\n+\t\tfi\n+\n+\t\tcase \"$ref\" in\n+\t\t\"${remote:-HEAD}\"|*\"/${remote:-HEAD}\")\n+\t\t\tif test -z \"$deref\"\n+\t\t\tthen\n+\t\t\t\t# Remember the matching unpeeled object on the\n+\t\t\t\t# remote side.\n+\t\t\t\tremote_sha1=\"$sha1\"\n+\t\t\tfi\n+\n+\t\t\tif test \"$sha1\" = \"$headrev\"\n+\t\t\tthen\n+\t\t\t\techo \"${remote_sha1:-$headrev} $ref\"\n+\t\t\t\tbreak\n+\t\t\tfi\n+\t\t\t;;\n+\t\tesac\n+\tdone\n+}\n+\n+# Find a ref with the same name as $remote that exists at the remote\n # and points to the same commit as the local object.\n-find_matching_ref='\n-\tmy ($head,$headrev) = (@ARGV);\n-\tmy $pattern = qr{/\\Q$head\\E$};\n-\tmy ($remote_sha1, $found);\n-\n-\twhile (<STDIN>) {\n-\t\tchomp;\n-\t\tmy ($sha1, $ref, $deref) = /^(\\S+)\\s+([^^]+)(\\S*)$/;\n-\n-\t\tif ($sha1 eq $head) {\n-\t\t\t$found = $remote_sha1 = $sha1;\n-\t\t\tbreak;\n-\t\t}\n-\n-\t\tif ($ref eq $head || $ref =~ $pattern) {\n-\t\t\tif ($deref eq \"\") {\n-\t\t\t\t# Remember the matching object on the remote side\n-\t\t\t\t$remote_sha1 = $sha1;\n-\t\t\t}\n-\t\t\tif ($sha1 eq $headrev) {\n-\t\t\t\t$found = $ref;\n-\t\t\t\tbreak;\n-\t\t\t}\n-\t\t}\n-\t}\n-\tif ($found) {\n-\t\t$remote_sha1 = $headrev if ! defined $remote_sha1;\n-\t\tprint \"$remote_sha1 $found\\n\";\n-\t}\n-'\n-\n-set fnord $(git ls-remote \"$url\" | @PERL_PATH@ -e \"$find_matching_ref\" \"${remote:-HEAD}\" \"$headrev\")\n+set fnord $(git ls-remote \"$url\" | find_matching_ref)\n remote_sha1=$2\n ref=$3\n \ndiff --git a/t/t5150-request-pull.sh b/t/t5150-request-pull.sh\nindex cb67bac1c47..270ce6ea487 100755\n--- a/t/t5150-request-pull.sh\n+++ b/t/t5150-request-pull.sh\n@@ -7,12 +7,6 @@ export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME\n \n . ./test-lib.sh\n \n-if ! test_have_prereq PERL\n-then\n-\tskip_all='skipping request-pull tests, perl not available'\n-\ttest_done\n-fi\n-\n test_expect_success 'setup' '\n \n \tgit init --bare upstream.git &&\n\n-- \n2.49.0.805.g082f7c87e0.dirty\n\n"},{"id":"516281","messageId":"20250416-b4-pks-drop-perl-v2-4-bdd0492e9498@pks.im","threadId":"63296","inReplyTo":"20250416-b4-pks-drop-perl-v2-0-bdd0492e9498@pks.im","subject":"[PATCH v2 4/4] Documentation: stop depending on Perl to generate command list","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-16T12:16:10Z","receivedAt":"2025-04-16T12:16:21Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"cmd-list.perl\" script is used to extract the list of commands part\nof a specific category and extracts the description of each command from\nits respective manpage. The generated output is then included in git(1)\nto list all Git commands.\n\nThe script is written in Perl. Refactor it to use shell scripting\nexclusively so that we can get rid of the mandatory dependency on Perl\nto build our documentation.\n\nThe converted script is slower compared to its Perl implementation. But\nby being careful and not spawning external commands in `format_one ()`\nwe can mitigate the performance hit to a reasonable level:\n\n    Benchmark 1: Perl\n      Time (mean ± σ):      10.3 ms ±   0.2 ms    [User: 7.0 ms, System: 3.3 ms]\n      Range (min … max):    10.0 ms …  11.1 ms    200 runs\n\n    Benchmark 2: Shell\n      Time (mean ± σ):      74.4 ms ±   0.4 ms    [User: 48.6 ms, System: 24.7 ms]\n      Range (min … max):    73.1 ms …  75.5 ms    200 runs\n\n    Summary\n      Perl ran\n        7.23 ± 0.13 times faster than Shell\n\nWhile a sevenfold slowdown is significant, the benefit of not requiring\nPerl for a fully-functioning Git installation outweighs waiting a couple\nof milliseconds longer during the build process.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/Makefile      |   4 +-\n Documentation/cmd-list.perl |  80 ----------------------------------\n Documentation/cmd-list.sh   | 104 ++++++++++++++++++++++++++++++++++++++++++++\n Documentation/meson.build   |   4 +-\n meson.build                 |   2 +-\n 5 files changed, 109 insertions(+), 85 deletions(-)\n\ndiff --git a/Documentation/Makefile b/Documentation/Makefile\nindex 6485d40f620..b109d25e9c8 100644\n--- a/Documentation/Makefile\n+++ b/Documentation/Makefile\n@@ -317,8 +317,8 @@ cmds_txt = cmds-ancillaryinterrogators.adoc \\\n \n $(cmds_txt): cmd-list.made\n \n-cmd-list.made: cmd-list.perl ../command-list.txt $(MAN1_TXT)\n-\t$(QUIET_GEN)$(PERL_PATH) ./cmd-list.perl .. . $(cmds_txt) && \\\n+cmd-list.made: cmd-list.sh ../command-list.txt $(MAN1_TXT)\n+\t$(QUIET_GEN)$(SHELL_PATH) ./cmd-list.sh .. . $(cmds_txt) && \\\n \tdate >$@\n \n mergetools-%.adoc: generate-mergetool-list.sh ../git-mergetool--lib.sh $(wildcard ../mergetools/*)\ndiff --git a/Documentation/cmd-list.perl b/Documentation/cmd-list.perl\ndeleted file mode 100755\nindex 0a0c1b3f611..00000000000\n--- a/Documentation/cmd-list.perl\n+++ /dev/null\n@@ -1,80 +0,0 @@\n-#!/usr/bin/perl -w\n-\n-use File::Compare qw(compare);\n-\n-sub format_one {\n-\tmy ($source_dir, $out, $nameattr) = @_;\n-\tmy ($name, $attr) = @$nameattr;\n-\tmy ($path) = \"$source_dir/Documentation/$name.adoc\";\n-\tmy ($state, $description);\n-\tmy $mansection;\n-\t$state = 0;\n-\topen I, '<', \"$path\" or die \"No such file $path.adoc\";\n-\twhile (<I>) {\n-\t\tif (/^(?:git|scalar)[a-z0-9-]*\\(([0-9])\\)$/) {\n-\t\t\t$mansection = $1;\n-\t\t\tnext;\n-\t\t}\n-\t\tif (/^NAME$/) {\n-\t\t\t$state = 1;\n-\t\t\tnext;\n-\t\t}\n-\t\tif ($state == 1 && /^----$/) {\n-\t\t\t$state = 2;\n-\t\t\tnext;\n-\t\t}\n-\t\tnext if ($state != 2);\n-\t\tchomp;\n-\t\t$description = $_;\n-\t\tlast;\n-\t}\n-\tclose I;\n-\tif (!defined $description) {\n-\t\tdie \"No description found in $path.adoc\";\n-\t}\n-\tif (my ($verify_name, $text) = ($description =~ /^($name) - (.*)/)) {\n-\t\tprint $out \"linkgit:$name\\[$mansection\\]::\\n\\t\";\n-\t\tif ($attr =~ / deprecated /) {\n-\t\t\tprint $out \"(deprecated) \";\n-\t\t}\n-\t\tprint $out \"$text.\\n\\n\";\n-\t}\n-\telse {\n-\t\tdie \"Description does not match $name: $description\";\n-\t}\n-}\n-\n-my ($source_dir, $build_dir, @categories) = @ARGV;\n-\n-open IN, \"<$source_dir/command-list.txt\";\n-while (<IN>) {\n-\tlast if /^### command list/;\n-}\n-\n-my %cmds = ();\n-for (sort <IN>) {\n-\tnext if /^#/;\n-\n-\tchomp;\n-\tmy ($name, $cat, $attr) = /^(\\S+)\\s+(.*?)(?:\\s+(.*))?$/;\n-\t$attr = '' unless defined $attr;\n-\tpush @{$cmds{$cat}}, [$name, \" $attr \"];\n-}\n-close IN;\n-\n-for my $out (@categories) {\n-\tmy ($cat) = $out =~ /^cmds-(.*)\\.adoc$/;\n-\tmy ($path) = \"$build_dir/$out\";\n-\topen O, '>', \"$path+\" or die \"Cannot open output file $out+\";\n-\tfor (@{$cmds{$cat}}) {\n-\t\tformat_one($source_dir, \\*O, $_);\n-\t}\n-\tclose O;\n-\n-\tif (-f \"$path\" && compare(\"$path\", \"$path+\") == 0) {\n-\t\tunlink \"$path+\";\n-\t}\n-\telse {\n-\t\trename \"$path+\", \"$path\";\n-\t}\n-}\ndiff --git a/Documentation/cmd-list.sh b/Documentation/cmd-list.sh\nnew file mode 100755\nindex 00000000000..077def3b721\n--- /dev/null\n+++ b/Documentation/cmd-list.sh\n@@ -0,0 +1,104 @@\n+#!/bin/sh\n+\n+set -e\n+\n+format_one () {\n+\tsource_dir=\"$1\"\n+\tcommand=\"$2\"\n+\tattributes=\"$3\"\n+\n+\tpath=\"$source_dir/Documentation/$command.adoc\"\n+\tif ! test -f \"$path\"\n+\tthen\n+\t\techo >&2 \"No such file $path\"\n+\t\texit 1\n+\tfi\n+\n+\tstate=0\n+\twhile read line\n+\tdo\n+\t\tcase \"$state\" in\n+\t\t0)\n+\t\t\tcase \"$line\" in\n+\t\t\tgit*\\(*\\)|scalar*\\(*\\))\n+\t\t\t\tmansection=\"${line##*\\(}\"\n+\t\t\t\tmansection=\"${mansection%\\)}\"\n+\t\t\t\t;;\n+\t\t\tNAME)\n+\t\t\t\tstate=1;;\n+\t\t\tesac\n+\t\t\t;;\n+\t\t1)\n+\t\t\tif test \"$line\" = \"----\"\n+\t\t\tthen\n+\t\t\t\tstate=2\n+\t\t\tfi\n+\t\t\t;;\n+\t\t2)\n+\t\t\tdescription=\"$line\"\n+\t\t\tbreak\n+\t\t\t;;\n+\t\tesac\n+\tdone <\"$path\"\n+\n+\tif test -z \"$mansection\"\n+\tthen\n+\t\techo \"No man section found in $path\" >&2\n+\t\texit 1\n+\tfi\n+\n+\tif test -z \"$description\"\n+\tthen\n+\t\techo >&2 \"No description found in $path\"\n+\t\texit 1\n+\tfi\n+\n+\tcase \"$description\" in\n+\t\"$command - \"*)\n+\t\ttext=\"${description#$command - }\"\n+\n+\t\tprintf \"linkgit:%s[%s]::\\n\\t\" \"$command\" \"$mansection\"\n+\t\tcase \"$attributes\" in\n+\t\t*\" deprecated \"*)\n+\t\t\tprintf \"(deprecated) \"\n+\t\t\t;;\n+\t\tesac\n+\t\tprintf \"$text.\\n\\n\"\n+\t\t;;\n+\t*)\n+\t\techo >&2 \"Description does not match $command: $description\"\n+\t\texit 1\n+\t\t;;\n+\tesac\n+}\n+\n+source_dir=\"$1\"\n+build_dir=\"$2\"\n+shift 2\n+\n+for out\n+do\n+\tcategory=\"${out#cmds-}\"\n+\tcategory=\"${category%.adoc}\"\n+\tpath=\"$build_dir/$out\"\n+\n+\twhile read command command_category attributes\n+\tdo\n+\t\tcase \"$command\" in\n+\t\t\"#\"*)\n+\t\t\tcontinue;;\n+\t\tesac\n+\n+\t\tcase \"$command_category\" in\n+\t\t\"$category\")\n+\t\t\tformat_one \"$source_dir\" \"$command\" \" $attributes \";;\n+\t\tesac\n+\tdone <\"$source_dir/command-list.txt\" >\"$build_dir/$out+\"\n+\n+\tif cmp \"$build_dir/$out+\" \"$build_dir/$out\" >/dev/null 2>&1\n+\tthen\n+\t\trm \"$build_dir/$out+\"\n+\telse\n+\t\tmv \"$build_dir/$out+\" \"$build_dir/$out\"\n+\tfi\n+done\ndiff --git a/Documentation/meson.build b/Documentation/meson.build\nindex 8b9e692c599..b731c76e9e7 100644\n--- a/Documentation/meson.build\n+++ b/Documentation/meson.build\n@@ -315,12 +315,12 @@ cmd_lists = [\n \n documentation_deps += custom_target(\n   command: [\n-    perl,\n+    shell,\n     '@INPUT@',\n     meson.project_source_root(),\n     meson.current_build_dir(),\n   ] + cmd_lists,\n-  input: 'cmd-list.perl',\n+  input: 'cmd-list.sh',\n   output: cmd_lists\n )\n \ndiff --git a/meson.build b/meson.build\nindex 8bab8f3481f..97753d2cfa7 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -779,7 +779,7 @@ endif\n # features. It is optional if you want to neither execute tests nor use any of\n # these optional features.\n perl_required = get_option('perl')\n-if get_option('gitweb').enabled() or 'netrc' in get_option('credential_helpers') or get_option('docs') != []\n+if get_option('gitweb').enabled() or 'netrc' in get_option('credential_helpers')\n   perl_required = true\n endif\n \n\n-- \n2.49.0.805.g082f7c87e0.dirty\n\n"},{"id":"516282","messageId":"20250416-b4-pks-drop-perl-v2-3-bdd0492e9498@pks.im","threadId":"63296","inReplyTo":"20250416-b4-pks-drop-perl-v2-0-bdd0492e9498@pks.im","subject":"[PATCH v2 3/4] Documentation: stop depending on Perl to massage user manual","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-16T12:16:09Z","receivedAt":"2025-04-16T12:16:21Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The \"fix-texi.perl\" script is used to fix up the output of\n`docbook2x-texi`:\n\n  - It changes the filename to be \"git.info\".\n\n  - It changes the directory category and entry.\n\nThe script is written in Perl, but it can be rather trivially converted\nto a shell script. Do so to remove the dependency on Perl for building\nthe user manual.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n Documentation/Makefile      |  4 ++--\n Documentation/fix-texi.perl | 15 ---------------\n Documentation/fix-texi.sh   | 21 +++++++++++++++++++++\n 3 files changed, 23 insertions(+), 17 deletions(-)\n\ndiff --git a/Documentation/Makefile b/Documentation/Makefile\nindex 0d3a2c6bfe6..6485d40f620 100644\n--- a/Documentation/Makefile\n+++ b/Documentation/Makefile\n@@ -398,9 +398,9 @@ user-manual.html: user-manual.xml $(XSLT)\n git.info: user-manual.texi\n \t$(QUIET_MAKEINFO)$(MAKEINFO) --no-split -o $@ user-manual.texi\n \n-user-manual.texi: user-manual.xml\n+user-manual.texi: user-manual.xml fix-texi.sh\n \t$(QUIET_DB2TEXI)$(DOCBOOK2X_TEXI) user-manual.xml --encoding=UTF-8 --to-stdout >$@+ && \\\n-\t$(PERL_PATH) fix-texi.perl <$@+ >$@ && \\\n+\t$(SHELL_PATH) fix-texi.sh <$@+ >$@ && \\\n \t$(RM) $@+\n \n user-manual.pdf: user-manual.xml\ndiff --git a/Documentation/fix-texi.perl b/Documentation/fix-texi.perl\ndeleted file mode 100755\nindex ff7d78f620a..00000000000\n--- a/Documentation/fix-texi.perl\n+++ /dev/null\n@@ -1,15 +0,0 @@\n-#!/usr/bin/perl -w\n-\n-while (<>) {\n-\tif (/^\\@setfilename/) {\n-\t\t$_ = \"\\@setfilename git.info\\n\";\n-\t} elsif (/^\\@direntry/) {\n-\t\tprint '@dircategory Development\n-@direntry\n-* Git: (git).           A fast distributed revision control system\n-@end direntry\n-';\t}\n-\tunless (/^\\@direntry/../^\\@end direntry/) {\n-\t\tprint;\n-\t}\n-}\ndiff --git a/Documentation/fix-texi.sh b/Documentation/fix-texi.sh\nnew file mode 100755\nindex 00000000000..bc300f7b0f1\n--- /dev/null\n+++ b/Documentation/fix-texi.sh\n@@ -0,0 +1,21 @@\n+#!/bin/sh\n+\n+awk '\n+\t/^@setfilename/{\n+\t\tprint \"@setfilename git.info\"\n+\t\tnext\n+\t}\n+\t/^@direntry/{\n+\t\tdirentry=1\n+\t\tprint \"@dircategory Development\"\n+\t\tprint \"@direntry\"\n+\t\tprint \"* Git: (git).           A fast distributed revision control system\"\n+\t\tprint \"@end direntry\"\n+\t\tnext\n+\t}\n+\t/^@end direntry/{\n+\t\tdirentry=0\n+\t\tnext\n+\t}\n+\t!direntry\n+'\n\n-- \n2.49.0.805.g082f7c87e0.dirty\n\n"},{"id":"516295","messageId":"Z__Hz3D6d-4v_z7r@pks.im","threadId":"63296","inReplyTo":"xmqqtt6pmmd6.fsf@gitster.g","subject":"Re: [PATCH 2/4] request-pull: stop depending on Perl","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-16T15:07:59Z","receivedAt":"2025-04-16T15:08:08Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Apr 15, 2025 at 09:16:05AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> > diff --git a/git-request-pull.sh b/git-request-pull.sh\n> > index 775ba8ea11a..59276fe265b 100755\n> > --- a/git-request-pull.sh\n> > +++ b/git-request-pull.sh\n> > @@ -78,41 +78,47 @@ fi\n> >  merge_base=$(git merge-base $baserev $headrev) ||\n> >  die \"fatal: No commits in common between $base and $head\"\n> >  \n> > -# $head is the refname from the command line.\n> > -# Find a ref with the same name as $head that exists at the remote\n> > +find_matching_ref () {\n> > +\twhile read sha1 ref\n> > +\tdo\n> > +\t\tcase \"$ref\" in\n> > +\t\t*\"^{}\")\n> > +\t\t\tref=\"${ref%\"^{}\"}\"\n> > +\t\t\tderef=true\n> \n> This got a bit tighter (the original allowed ls-remote output to be\n> later extended to throw \"^something-else\" at the end of line), which\n> I do not know is something we need to worry about.  I think retaining\n> the original semantics is easy in this case, e.g.,\n> \n> \t\t*\"^\"?*)\n> \t\t\tref=\"${ref%\"^*\"}\"\n\nThis should probably read `ref=\"${ref%\"^\"*}\"`, with the `*` outside of\nthe quotes.\n\n> or something, if we wanted to.  As this is meant to be faithful\n> rewrite to lose Perl, not a bugfix to correct overly loose pattern\n> matching in the original, I think we want to in this case.\n\nFair enough. It should've been at least mentioned in the commit message,\nbut let's instead just retain the current behaviour as suggested.\n\nPatrick\n"}]}