From a38edab7c88b5503bb2b5f5cbd49f6b97e9a6a4e Mon Sep 17 00:00:00 2001 From: Patrick Steinhardt Date: Fri, 6 Dec 2024 14:24:41 +0100 Subject: Makefile: generate doc versions via GIT-VERSION-GEN The documentation we generate embeds information for the exact Git version used as well as the date of the commit. This information is injected by injecting attributes into the build process via command line argument. Refactor the logic so that we write the information into "asciidoc.conf" and "asciidoctor-extensions.rb" via `GIT-VERSION-GEN` for AsciiDoc and AsciiDoctor, respectively. Signed-off-by: Patrick Steinhardt Signed-off-by: Junio C Hamano --- Documentation/.gitignore | 2 + Documentation/Makefile | 25 ++++-- Documentation/asciidoc.conf | 79 ----------------- Documentation/asciidoc.conf.in | 82 ++++++++++++++++++ Documentation/asciidoctor-extensions.rb | 135 ----------------------------- Documentation/asciidoctor-extensions.rb.in | 131 ++++++++++++++++++++++++++++ GIT-VERSION-GEN | 2 + 7 files changed, 233 insertions(+), 223 deletions(-) delete mode 100644 Documentation/asciidoc.conf create mode 100644 Documentation/asciidoc.conf.in delete mode 100644 Documentation/asciidoctor-extensions.rb create mode 100644 Documentation/asciidoctor-extensions.rb.in diff --git a/Documentation/.gitignore b/Documentation/.gitignore index a48448de32..649df89474 100644 --- a/Documentation/.gitignore +++ b/Documentation/.gitignore @@ -15,3 +15,5 @@ tmp-doc-diff/ GIT-ASCIIDOCFLAGS /.build/ /GIT-EXCLUDED-PROGRAMS +/asciidoc.conf +/asciidoctor-extensions.rb diff --git a/Documentation/Makefile b/Documentation/Makefile index 0f55baa252..9371f29425 100644 --- a/Documentation/Makefile +++ b/Documentation/Makefile @@ -1,6 +1,8 @@ # Import tree-wide shared Makefile behavior and libraries include ../shared.mak +.PHONY: FORCE + # Guard against environment variables MAN1_TXT = MAN5_TXT = @@ -148,16 +150,12 @@ man5dir = $(mandir)/man5 man7dir = $(mandir)/man7 # DESTDIR = -GIT_DATE := $(shell git show --quiet --pretty='%as') - ASCIIDOC = asciidoc ASCIIDOC_EXTRA = ASCIIDOC_HTML = xhtml11 ASCIIDOC_DOCBOOK = docbook ASCIIDOC_CONF = -f asciidoc.conf -ASCIIDOC_COMMON = $(ASCIIDOC) $(ASCIIDOC_EXTRA) $(ASCIIDOC_CONF) \ - -amanmanual='Git Manual' -amansource='Git $(GIT_VERSION)' \ - -arevdate='$(GIT_DATE)' +ASCIIDOC_COMMON = $(ASCIIDOC) $(ASCIIDOC_EXTRA) $(ASCIIDOC_CONF) ASCIIDOC_DEPS = asciidoc.conf GIT-ASCIIDOCFLAGS TXT_TO_HTML = $(ASCIIDOC_COMMON) -b $(ASCIIDOC_HTML) TXT_TO_XML = $(ASCIIDOC_COMMON) -b $(ASCIIDOC_DOCBOOK) @@ -210,6 +208,14 @@ ASCIIDOC_DEPS = asciidoctor-extensions.rb GIT-ASCIIDOCFLAGS DBLATEX_COMMON = XMLTO_EXTRA += --skip-validation XMLTO_EXTRA += -x manpage.xsl + +asciidoctor-extensions.rb: asciidoctor-extensions.rb.in FORCE + $(QUIET_GEN)GIT_USER_AGENT="$(GIT_USER_AGENT)" $(SHELL_PATH) ../GIT-VERSION-GEN "$(shell pwd)/.." $< $@+ + @if cmp $@+ $@ >/dev/null 2>&1; then $(RM) $@+; else mv $@+ $@; fi +else +asciidoc.conf: asciidoc.conf.in FORCE + $(QUIET_GEN)GIT_USER_AGENT="$(GIT_USER_AGENT)" $(SHELL_PATH) ../GIT-VERSION-GEN "$(shell pwd)/.." $< $@+ + @if cmp $@+ $@ >/dev/null 2>&1; then $(RM) $@+; else mv $@+ $@; fi endif ASCIIDOC_DEPS += docinfo.html @@ -341,6 +347,7 @@ clean: $(RM) SubmittingPatches.txt $(RM) $(cmds_txt) $(mergetools_txt) *.made $(RM) GIT-ASCIIDOCFLAGS + $(RM) asciidoc.conf asciidoctor-extensions.rb docinfo.html: docinfo-html.in $(QUIET_GEN)$(RM) $@ && cat $< >$@ @@ -364,7 +371,7 @@ manpage-cmd = $(QUIET_XMLTO)$(XMLTO) -m $(MANPAGE_XSL) $(XMLTO_EXTRA) man $< %.xml : %.txt $(ASCIIDOC_DEPS) $(QUIET_ASCIIDOC)$(TXT_TO_XML) -d manpage -o $@ $< -user-manual.xml: user-manual.txt user-manual.conf asciidoctor-extensions.rb GIT-ASCIIDOCFLAGS +user-manual.xml: user-manual.txt user-manual.conf $(ASCIIDOC_DEPS) $(QUIET_ASCIIDOC)$(TXT_TO_XML) -d book -o $@ $< technical/api-index.txt: technical/api-index-skel.txt \ @@ -373,7 +380,7 @@ technical/api-index.txt: technical/api-index-skel.txt \ technical/%.html: ASCIIDOC_EXTRA += -a git-relative-html-prefix=../ $(patsubst %,%.html,$(API_DOCS) technical/api-index $(TECH_DOCS)): %.html : %.txt \ - asciidoc.conf GIT-ASCIIDOCFLAGS + $(ASCIIDOC_DEPS) $(QUIET_ASCIIDOC)$(TXT_TO_HTML) $*.txt SubmittingPatches.txt: SubmittingPatches @@ -416,13 +423,13 @@ $(patsubst %.txt,%.texi,$(MAN_TXT)): %.texi : %.xml howto-index.txt: howto-index.sh $(HOWTO_TXT) $(QUIET_GEN)'$(SHELL_PATH_SQ)' ./howto-index.sh $(sort $(HOWTO_TXT)) >$@ -$(patsubst %,%.html,$(ARTICLES)) : %.html : %.txt +$(patsubst %,%.html,$(ARTICLES)) : %.html : %.txt $(ASCIIDOC_DEPS) $(QUIET_ASCIIDOC)$(TXT_TO_HTML) $*.txt WEBDOC_DEST = /pub/software/scm/git/docs howto/%.html: ASCIIDOC_EXTRA += -a git-relative-html-prefix=../ -$(patsubst %.txt,%.html,$(HOWTO_TXT)): %.html : %.txt GIT-ASCIIDOCFLAGS +$(patsubst %.txt,%.html,$(HOWTO_TXT)): %.html : %.txt $(ASCIIDOC_DEPS) $(QUIET_ASCIIDOC) \ sed -e '1,/^$$/d' $< | \ $(TXT_TO_HTML) - >$@ diff --git a/Documentation/asciidoc.conf b/Documentation/asciidoc.conf deleted file mode 100644 index f6da6d1fbd..0000000000 --- a/Documentation/asciidoc.conf +++ /dev/null @@ -1,79 +0,0 @@ -## linkgit: macro -# -# Usage: linkgit:command[manpage-section] -# -# Note, {0} is the manpage section, while {target} is the command. -# -# Show Git link as: (
); if section is defined, else just show -# the command. - -[macros] -(?su)[\\]?(?Plinkgit):(?P\S*?)\[(?P.*?)\]= - -[attributes] -asterisk=* -plus=+ -caret=^ -startsb=[ -endsb=] -backslash=\ -tilde=~ -apostrophe=' -backtick=` -litdd=-- - -ifdef::backend-docbook[] -[linkgit-inlinemacro] -{0%{target}} -{0#} -{0#{target}{0}} -{0#} - -[literal-inlinemacro] -{eval:re.sub(r'(<[-a-zA-Z0-9.]+>)', r'\1', re.sub(r'([\[\s|()>]|^|\]|>)(\.?([-a-zA-Z0-9:+=~@,\/_^\$]+\.?)+)',r'\1\2', re.sub(r'(\.\.\.?)([^\]$.])', r'\1\2', macros.passthroughs[int(attrs['passtext'][1:-1])] if attrs['passtext'][1:-1].isnumeric() else attrs['passtext'][1:-1])))} - -endif::backend-docbook[] - -ifdef::backend-docbook[] -ifdef::doctype-manpage[] -# The following two small workarounds insert a simple paragraph after screen -[listingblock] -{title} - -| - -{title#} - -[verseblock] -{title} -{title%} -{title#} -| - -{title#} -{title%} -endif::doctype-manpage[] -endif::backend-docbook[] - -ifdef::backend-xhtml11[] -[attributes] -git-relative-html-prefix= -[linkgit-inlinemacro] -{target}{0?({0})} - -[literal-inlinemacro] -{eval:re.sub(r'(<[-a-zA-Z0-9.]+>)', r'\1', re.sub(r'([\[\s|()>]|^|\]|>)(\.?([-a-zA-Z0-9:+=~@,\/_^\$]+\.?)+)',r'\1\2', re.sub(r'(\.\.\.?)([^\]$.])', r'\1\2', macros.passthroughs[int(attrs['passtext'][1:-1])] if attrs['passtext'][1:-1].isnumeric() else attrs['passtext'][1:-1])))} - -endif::backend-xhtml11[] - -ifdef::backend-docbook[] -ifdef::doctype-manpage[] -[paradef-default] -synopsis-style=template="verseparagraph",filter="sed 's!…\\(\\]\\|$\\)!\\0!g;s!\\([\\[ |()]\\|^\\|\\]\\|>\\)\\([-=a-zA-Z0-9:+@,\\/_^\\$.]\\+\\|…\\)!\\1\\2!g;s!<[-a-zA-Z0-9.]\\+>!\\0!g'" -endif::doctype-manpage[] -endif::backend-docbook[] - -ifdef::backend-xhtml11[] -[paradef-default] -synopsis-style=template="verseparagraph",filter="sed 's!…\\(\\]\\|$\\)!\\0!g;s!\\([\\[ |()]\\|^\\|\\]\\|>\\)\\([-=a-zA-Z0-9:+@,\\/_^\\$.]\\+\\|…\\)!\\1\\2!g;s!<[-a-zA-Z0-9.]\\+>!\\0!g'" -endif::backend-xhtml11[] diff --git a/Documentation/asciidoc.conf.in b/Documentation/asciidoc.conf.in new file mode 100644 index 0000000000..dbe36a52ea --- /dev/null +++ b/Documentation/asciidoc.conf.in @@ -0,0 +1,82 @@ +## linkgit: macro +# +# Usage: linkgit:command[manpage-section] +# +# Note, {0} is the manpage section, while {target} is the command. +# +# Show Git link as: (
); if section is defined, else just show +# the command. + +[macros] +(?su)[\\]?(?Plinkgit):(?P\S*?)\[(?P.*?)\]= + +[attributes] +asterisk=* +plus=+ +caret=^ +startsb=[ +endsb=] +backslash=\ +tilde=~ +apostrophe=' +backtick=` +litdd=-- +manmanual='Git Manual' +mansource='Git @GIT_VERSION@' +revdate='@GIT_DATE@' + +ifdef::backend-docbook[] +[linkgit-inlinemacro] +{0%{target}} +{0#} +{0#{target}{0}} +{0#} + +[literal-inlinemacro] +{eval:re.sub(r'(<[-a-zA-Z0-9.]+>)', r'\1', re.sub(r'([\[\s|()>]|^|\]|>)(\.?([-a-zA-Z0-9:+=~@,\/_^\$]+\.?)+)',r'\1\2', re.sub(r'(\.\.\.?)([^\]$.])', r'\1\2', macros.passthroughs[int(attrs['passtext'][1:-1])] if attrs['passtext'][1:-1].isnumeric() else attrs['passtext'][1:-1])))} + +endif::backend-docbook[] + +ifdef::backend-docbook[] +ifdef::doctype-manpage[] +# The following two small workarounds insert a simple paragraph after screen +[listingblock] +{title} + +| + +{title#} + +[verseblock] +{title} +{title%} +{title#} +| + +{title#} +{title%} +endif::doctype-manpage[] +endif::backend-docbook[] + +ifdef::backend-xhtml11[] +[attributes] +git-relative-html-prefix= +[linkgit-inlinemacro] +{target}{0?({0})} + +[literal-inlinemacro] +{eval:re.sub(r'(<[-a-zA-Z0-9.]+>)', r'\1', re.sub(r'([\[\s|()>]|^|\]|>)(\.?([-a-zA-Z0-9:+=~@,\/_^\$]+\.?)+)',r'\1\2', re.sub(r'(\.\.\.?)([^\]$.])', r'\1\2', macros.passthroughs[int(attrs['passtext'][1:-1])] if attrs['passtext'][1:-1].isnumeric() else attrs['passtext'][1:-1])))} + +endif::backend-xhtml11[] + +ifdef::backend-docbook[] +ifdef::doctype-manpage[] +[paradef-default] +synopsis-style=template="verseparagraph",filter="sed 's!…\\(\\]\\|$\\)!\\0!g;s!\\([\\[ |()]\\|^\\|\\]\\|>\\)\\([-=a-zA-Z0-9:+@,\\/_^\\$.]\\+\\|…\\)!\\1\\2!g;s!<[-a-zA-Z0-9.]\\+>!\\0!g'" +endif::doctype-manpage[] +endif::backend-docbook[] + +ifdef::backend-xhtml11[] +[paradef-default] +synopsis-style=template="verseparagraph",filter="sed 's!…\\(\\]\\|$\\)!\\0!g;s!\\([\\[ |()]\\|^\\|\\]\\|>\\)\\([-=a-zA-Z0-9:+@,\\/_^\\$.]\\+\\|…\\)!\\1\\2!g;s!<[-a-zA-Z0-9.]\\+>!\\0!g'" +endif::backend-xhtml11[] diff --git a/Documentation/asciidoctor-extensions.rb b/Documentation/asciidoctor-extensions.rb deleted file mode 100644 index cb24480b63..0000000000 --- a/Documentation/asciidoctor-extensions.rb +++ /dev/null @@ -1,135 +0,0 @@ -require 'asciidoctor' -require 'asciidoctor/extensions' -require 'asciidoctor/converter/docbook5' -require 'asciidoctor/converter/html5' - -module Git - module Documentation - class LinkGitProcessor < Asciidoctor::Extensions::InlineMacroProcessor - use_dsl - - named :chrome - - def process(parent, target, attrs) - prefix = parent.document.attr('git-relative-html-prefix') - if parent.document.doctype == 'book' - "" \ - "#{target}(#{attrs[1]})" - elsif parent.document.basebackend? 'html' - %(#{target}(#{attrs[1]})) - elsif parent.document.basebackend? 'docbook' - "\n" \ - "#{target}" \ - "#{attrs[1]}\n" \ - "" - end - end - end - - class DocumentPostProcessor < Asciidoctor::Extensions::Postprocessor - def process document, output - if document.basebackend? 'docbook' - mansource = document.attributes['mansource'] - manversion = document.attributes['manversion'] - manmanual = document.attributes['manmanual'] - new_tags = "" \ - "#{mansource}\n" \ - "#{manversion}\n" \ - "#{manmanual}\n" - output = output.sub(/<\/refmeta>/, new_tags + "") - end - output - end - end - - class SynopsisBlock < Asciidoctor::Extensions::BlockProcessor - - use_dsl - named :synopsis - parse_content_as :simple - - def process parent, reader, attrs - outlines = reader.lines.map do |l| - l.gsub(/(\.\.\.?)([^\]$.])/, '`\1`\2') - .gsub(%r{([\[\] |()>]|^)([-a-zA-Z0-9:+=~@,/_^\$]+)}, '\1{empty}`\2`{empty}') - .gsub(/(<[-a-zA-Z0-9.]+>)/, '__\\1__') - .gsub(']', ']{empty}') - end - create_block parent, :verse, outlines, attrs - end - end - - class GitDBConverter < Asciidoctor::Converter::DocBook5Converter - - extend Asciidoctor::Converter::Config - register_for 'docbook5' - - def convert_inline_quoted node - if (type = node.type) == :asciimath - # NOTE fop requires jeuclid to process mathml markup - asciimath_available? ? %(#{(::AsciiMath.parse node.text).to_mathml 'mml:', 'xmlns:mml' => 'http://www.w3.org/1998/Math/MathML'}) : %() - elsif type == :latexmath - # unhandled math; pass source to alt and required mathphrase element; dblatex will process alt as LaTeX math - %() - elsif type == :monospaced - node.text.gsub(/(\.\.\.?)([^\]$.])/, '\1\2') - .gsub(%r{([\[\s|()>.]|^|\]|>)(\.?([-a-zA-Z0-9:+=~@,/_^\$]+\.{0,2})+)}, '\1\2') - .gsub(/(<[-a-zA-Z0-9.]+>)/, '\1') - else - open, close, supports_phrase = QUOTE_TAGS[type] - text = node.text - if node.role - if supports_phrase - quoted_text = %(#{open}#{text}#{close}) - else - quoted_text = %(#{open.chop} role="#{node.role}">#{text}#{close}) - end - else - quoted_text = %(#{open}#{text}#{close}) - end - node.id ? %(#{quoted_text}) : quoted_text - end - end - end - - # register a html5 converter that takes in charge to convert monospaced text into Git style synopsis - class GitHTMLConverter < Asciidoctor::Converter::Html5Converter - - extend Asciidoctor::Converter::Config - register_for 'html5' - - def convert_inline_quoted node - if node.type == :monospaced - node.text.gsub(/(\.\.\.?)([^\]$.])/, '\1\2') - .gsub(%r{([\[\s|()>.]|^|\]|>)(\.?([-a-zA-Z0-9:+=~@,/_^\$]+\.{0,2})+)}, '\1\2') - .gsub(/(<[-a-zA-Z0-9.]+>)/, '\1') - - else - open, close, tag = QUOTE_TAGS[node.type] - if node.id - class_attr = node.role ? %( class="#{node.role}") : '' - if tag - %(#{open.chop} id="#{node.id}"#{class_attr}>#{node.text}#{close}) - else - %(#{open}#{node.text}#{close}) - end - elsif node.role - if tag - %(#{open.chop} class="#{node.role}">#{node.text}#{close}) - else - %(#{open}#{node.text}#{close}) - end - else - %(#{open}#{node.text}#{close}) - end - end - end - end - end -end - -Asciidoctor::Extensions.register do - inline_macro Git::Documentation::LinkGitProcessor, :linkgit - block Git::Documentation::SynopsisBlock - postprocessor Git::Documentation::DocumentPostProcessor -end diff --git a/Documentation/asciidoctor-extensions.rb.in b/Documentation/asciidoctor-extensions.rb.in new file mode 100644 index 0000000000..c4c200dace --- /dev/null +++ b/Documentation/asciidoctor-extensions.rb.in @@ -0,0 +1,131 @@ +require 'asciidoctor' +require 'asciidoctor/extensions' +require 'asciidoctor/converter/docbook5' +require 'asciidoctor/converter/html5' + +module Git + module Documentation + class LinkGitProcessor < Asciidoctor::Extensions::InlineMacroProcessor + use_dsl + + named :chrome + + def process(parent, target, attrs) + prefix = parent.document.attr('git-relative-html-prefix') + if parent.document.doctype == 'book' + "" \ + "#{target}(#{attrs[1]})" + elsif parent.document.basebackend? 'html' + %(#{target}(#{attrs[1]})) + elsif parent.document.basebackend? 'docbook' + "\n" \ + "#{target}" \ + "#{attrs[1]}\n" \ + "" + end + end + end + + class DocumentPostProcessor < Asciidoctor::Extensions::Postprocessor + def process document, output + if document.basebackend? 'docbook' + new_tags = "" \ + "@GIT_VERSION@\n" \ + "Git Manual\n" + output = output.sub(/<\/refmeta>/, new_tags + "") + end + output + end + end + + class SynopsisBlock < Asciidoctor::Extensions::BlockProcessor + + use_dsl + named :synopsis + parse_content_as :simple + + def process parent, reader, attrs + outlines = reader.lines.map do |l| + l.gsub(/(\.\.\.?)([^\]$.])/, '`\1`\2') + .gsub(%r{([\[\] |()>]|^)([-a-zA-Z0-9:+=~@,/_^\$]+)}, '\1{empty}`\2`{empty}') + .gsub(/(<[-a-zA-Z0-9.]+>)/, '__\\1__') + .gsub(']', ']{empty}') + end + create_block parent, :verse, outlines, attrs + end + end + + class GitDBConverter < Asciidoctor::Converter::DocBook5Converter + + extend Asciidoctor::Converter::Config + register_for 'docbook5' + + def convert_inline_quoted node + if (type = node.type) == :asciimath + # NOTE fop requires jeuclid to process mathml markup + asciimath_available? ? %(#{(::AsciiMath.parse node.text).to_mathml 'mml:', 'xmlns:mml' => 'http://www.w3.org/1998/Math/MathML'}) : %() + elsif type == :latexmath + # unhandled math; pass source to alt and required mathphrase element; dblatex will process alt as LaTeX math + %() + elsif type == :monospaced + node.text.gsub(/(\.\.\.?)([^\]$.])/, '\1\2') + .gsub(%r{([\[\s|()>.]|^|\]|>)(\.?([-a-zA-Z0-9:+=~@,/_^\$]+\.{0,2})+)}, '\1\2') + .gsub(/(<[-a-zA-Z0-9.]+>)/, '\1') + else + open, close, supports_phrase = QUOTE_TAGS[type] + text = node.text + if node.role + if supports_phrase + quoted_text = %(#{open}#{text}#{close}) + else + quoted_text = %(#{open.chop} role="#{node.role}">#{text}#{close}) + end + else + quoted_text = %(#{open}#{text}#{close}) + end + node.id ? %(#{quoted_text}) : quoted_text + end + end + end + + # register a html5 converter that takes in charge to convert monospaced text into Git style synopsis + class GitHTMLConverter < Asciidoctor::Converter::Html5Converter + + extend Asciidoctor::Converter::Config + register_for 'html5' + + def convert_inline_quoted node + if node.type == :monospaced + node.text.gsub(/(\.\.\.?)([^\]$.])/, '\1\2') + .gsub(%r{([\[\s|()>.]|^|\]|>)(\.?([-a-zA-Z0-9:+=~@,/_^\$]+\.{0,2})+)}, '\1\2') + .gsub(/(<[-a-zA-Z0-9.]+>)/, '\1') + + else + open, close, tag = QUOTE_TAGS[node.type] + if node.id + class_attr = node.role ? %( class="#{node.role}") : '' + if tag + %(#{open.chop} id="#{node.id}"#{class_attr}>#{node.text}#{close}) + else + %(#{open}#{node.text}#{close}) + end + elsif node.role + if tag + %(#{open.chop} class="#{node.role}">#{node.text}#{close}) + else + %(#{open}#{node.text}#{close}) + end + else + %(#{open}#{node.text}#{close}) + end + end + end + end + end +end + +Asciidoctor::Extensions.register do + inline_macro Git::Documentation::LinkGitProcessor, :linkgit + block Git::Documentation::SynopsisBlock + postprocessor Git::Documentation::DocumentPostProcessor +end diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN index a1c8146f05..b4687784c1 100755 --- a/GIT-VERSION-GEN +++ b/GIT-VERSION-GEN @@ -53,6 +53,7 @@ fi GIT_VERSION=$(expr "$VN" : v*'\(.*\)') GIT_BUILT_FROM_COMMIT=$(git -C "$SOURCE_DIR" rev-parse -q --verify HEAD 2>/dev/null) +GIT_DATE=$(git -C "$SOURCE_DIR" show --quiet --format='%as' 2>/dev/null) if test -z "$GIT_USER_AGENT" then GIT_USER_AGENT="git/$GIT_VERSION" @@ -72,6 +73,7 @@ sed -e "s|@GIT_VERSION@|$GIT_VERSION|" \ -e "s|@GIT_PATCH_LEVEL@|$GIT_PATCH_LEVEL|" \ -e "s|@GIT_BUILT_FROM_COMMIT@|$GIT_BUILT_FROM_COMMIT|" \ -e "s|@GIT_USER_AGENT@|$GIT_USER_AGENT|" \ + -e "s|@GIT_DATE@|$GIT_DATE|" \ "$INPUT" >"$OUTPUT"+ if ! test -f "$OUTPUT" || ! cmp "$OUTPUT"+ "$OUTPUT" >/dev/null -- cgit v1.3