Revision control

++ #! /usr/bin/perl
++ # Copyright (C) 2022 The Qt Company Ltd.
++ # Copyright (C) 2014 Petroules Corporation.
++ #
++ # You may use this file under the terms of the 3-clause BSD license.
++ # See the file LICENSE from this package for details.
++ #
++
++ use strict;
++ use warnings;
++ use Config;
++ use Cwd;
++ use Encode;
++ use File::Spec;
++ use List::Util qw(first any);
++
++ if ($#ARGV < 0 or $#ARGV > 3 or ($#ARGV >= 1 && $ARGV[1] !~ /^(easy|strict|gerrit-rest)$/)) {
++ print STDERR "Usage: $0 <sha1> [{easy|strict|gerrit-rest} [<instance> [<branch>]]]\n";
++ exit 2;
++ }
++ my $sha1 = $ARGV[0];
++ my $gerrit_rest = ($#ARGV >= 1 && $ARGV[1] eq "gerrit-rest");
++ my $strict = $gerrit_rest || ($#ARGV >= 1 && $ARGV[1] eq "strict");
++ my $instance = ($#ARGV >= 2) ? $ARGV[2] : "default";
++ my $target = $#ARGV == 3 ? $ARGV[3] : undef;
++
++ my $repo = getcwd();
++ $repo =~ s,/?\.git$,,;
++ $repo =~ s,^.*/,,;
++ my %config = ();
++ for (`git config --list`) {
++ if (/^sanity\.\Q$repo\E\.([^=]+)=(.*$)/) {
++ $config{$1} = $2;
++ } elsif (/^sanity\.\Q$instance\E\.([^=]+)=(.*$)/) {
++ $config{$1} = $2;
++ }
++ }
++
++ my %cfg = ();
++ if (defined $ENV{GIT_PUSH}) {
++ foreach my $c (split ",", $ENV{GIT_PUSH}) {
++ $cfg{$c} = 1;
++ }
++ }
++ if (defined $config{flags}) {
++ foreach my $c (split ",", $config{flags}) {
++ $cfg{$c} = 1;
++ }
++ }
++ # The whitelist is space-separated, supporting double-quoted strings with spaces.
++ # Remember to backslash-escape the quotes if you edit .gitconfig manually.
++ my %good_names;
++ $_ = decode_utf8($config{goodnames} || "");
++ $good_names{$1 || $2} = 1 while (/(\w+)|\"([^\"]+)\"/g);
++ my $fail = 0;
++ my $file = "!!!!!";
++ my $lineno = 0;
++ my $summary;
++ # Hash (by file) of hashes (by line) of lists (reports) of lists (msg, extra)
++ my %complaints;
++ my %footnotes;
++
++ sub parse_bool($)
++ {
++ return !($_[0] =~ /^(?:false|no|0)$/i);
++ }
++
++ sub printerr()
++ {
++ die "cannot run git: ".$! if ($? < 0);
++ die "git crashed with signal ".$? if ($? & 127);
++ die "git exited with status ".($? >> 8) if ($?);
++ }
++
++ sub do_complain($$$;$@)
++ {
++ my ($line, $msg, $key, $level, @extra) = @_;
++
++ $level = 0 if (!defined($level) || ($level < 0 && $strict && length($key)));
++ if ($level >= 0) {
++ $fail = $level + 1 if ($level >= $fail);
++ $msg .= " (key \"".$key."\")" if (!$gerrit_rest);
++ } else {
++ $msg = "Hint: ".$msg;
++ }
++ my $fn = $line < 0 ? "" : $file;
++ push @{$complaints{$fn}{$line}}, [ $msg, @extra ];
++ }
++
++ sub complain($$;$)
++ {
++ my ($msg, $key, $level) = @_;
++ do_complain(0, $msg, $key, $level);
++ }
++
++ sub complain_ln($$;$)
++ {
++ my ($msg, $key, $level) = @_;
++ do_complain($lineno, $msg, $key, $level);
++ }
++
++ sub complain_cln($$;$)
++ {
++ my ($msg, $key, $level) = @_;
++ do_complain($gerrit_rest ? $lineno : 0, $msg, $key, $level);
++ }
++
++ use constant {
++ STS_BLANK => 0, # Empty lines except after footer
++ STS_TEXT => 1, # Non-footer text
++ STS_FOOTER => 2, # Footer
++ STS_FTR_BLNK => 3 # Empty lines after footer
++ };
++
++ my $clike = 0;
++ my $qmake = 0;
++ my $spell_check = !defined($cfg{spell});
++
++ # Load spelling errors dataset if available
++ our %MISTAKES;
++ our %MISTAKES_BASE;
++ BEGIN {
++ eval { require Lingua::EN::CommonMistakes };
++ if (!$@) {
++ # Load US-specific and non-US-specific mistakes so we can give a hint
++ # about US vs British English where appropriate
++ Lingua::EN::CommonMistakes->import(qw(:no-punct %MISTAKES_BASE));
++ Lingua::EN::CommonMistakes->import(qw(:american :no-punct %MISTAKES));
++ }
++ }
++
++ my @spell_fails;
++
++ sub complain_spelling()
++ {
++ if (@spell_fails) {
++ do_complain(1e9, "Possible spelling errors", "spell", 0, @spell_fails);
++ @spell_fails = ();
++ }
++ }
++
++ # Given a line of text, searches for likely spelling errors.
++ sub check_spelling()
++ {
++ return if (!%MISTAKES_BASE);
++
++ my %seen;
++ my (@words) = split(/\b/);
++ foreach my $word (@words) {
++ $word = lc $word;
++ next if $seen{$word};
++ $seen{$word} = 1;
++ if (my $correction = $MISTAKES{$word}) {
++ if ($word eq 'nam') {
++ # Ignore if in a fragments commonly used in "Lorem ipsum" texts:
++ next if (/\bNam eget dui\b/ || /\bNam quam nunc\b/);
++ }
++ my $sfx = "";
++ if (!$MISTAKES_BASE{$word}) {
++ $sfx = ' [*]';
++ $footnotes{'[*] Please note, Qt prefers American English.'} = 1;
++ }
++ if ($gerrit_rest) {
++ complain_ln("$word -> $correction?$sfx", "spell");
++ } else {
++ push @spell_fails, " $lineno: $word -> $correction$sfx";
++ }
++ }
++ }
++ }
++
++ my $seenIdentify; # Cached result of PATH check
++ sub haveIdentify()
++ {
++ $seenIdentify = defined(first { $_ && -x "$_/identify$Config{_exe}" } File::Spec->path())
++ unless defined($seenIdentify);
++ return $seenIdentify;
++ }
++
++ sub dodgyPng($$)
++ {
++ # Not uncommonly, PNGs get generated with a bogus sRGB chunk; this
++ # can readily be removed using pngcrush, eliminating the warning
++ # from identify. This check may find other kinds of errors; we'll
++ # have to learn how to fix those when we meet them. The identify
++ # program is part of the ImageMagick package.
++ my ($commit, $file) = @_;
++ my $err = `git cat-file blob "$commit:$file" 2>/dev/null | \
++ identify - 2>&1 1>/dev/null`;
++ return $err =~ m/\bMagickPNGWarningHandler\b/;
++ }
++
++ my @style_fails = ();
++
++ sub styleFail($)
++ {
++ my $why = shift;
++ if ($gerrit_rest) {
++ complain_ln("$why", "", -1);
++ } else {
++ push @style_fails, " $lineno: ".$why;
++ }
++ }
++
++ sub complain_style()
++ {
++ if (@style_fails) {
++ do_complain(1e9, "Style issues", "", -1, @style_fails);
++ @style_fails = ();
++ }
++ }
++
++ sub check_email($$$$)
++ {
++ my ($real, $addr, $line, $label) = @_;
++
++ if ($addr =~ /\.\(none\)$/) {
++ if ($gerrit_rest) {
++ do_complain($line, "Bogus address", "email", 1);
++ } else {
++ do_complain(-1, "Bogus $label address", "email", 1);
++ }
++ }
++ if ($addr =~ /\@theqtcompany\.com$/) {
++ if ($gerrit_rest) {
++ do_complain($line, "Obsolete domain. Please update to qt.io.", "email", 1);
++ } else {
++ do_complain(-1, "Obsolete domain in $label address. Please update to qt.io.", "email", 1);
++ }
++ }
++ # UTF-8 is a pretty safe bet - all modern Unix systems use it, and
++ # when not printing to a Windows console, msysgit also uses it.
++ eval { $real = decode_utf8($real, Encode::FB_CROAK); };
++ return if ($@);
++ # We complain about names which
++ # - contain no spaces
++ # - have words starting with lowercase (except for common Nobiliary particles)
++ # - have single letters not followed by a dot or apostrophe
++ if (!defined($good_names{$real}) && ($real !~ / / || $real =~ /\b(?!da|de|du|d'|la|van|von)\p{Ll}|\b\pL([^.'\pL]|$)/)) {
++ if ($gerrit_rest) {
++ do_complain($line, "Real Name has an unexpected form", "email");
++ } else {
++ do_complain(-1, ucfirst($label)." real name has an unexpected form", "email");
++ }
++ }
++ }
++
++ sub check_apple_terminology()
++ {
++ if ($clike) {
++ if (/\bQ_OS_MAC\b.*&&.*!.*\bQ_OS_IOS\b/) {
++ complain_ln("Use of deprecated idiom 'defined(Q_OS_MAC) && !defined(Q_OS_IOS)';" .
++ " use Q_OS_MACOS instead", "terminology");
++ }
++
++ if (/\bQ_OS_MAC\b/) {
++ complain_ln("Using deprecated define Q_OS_MAC; use Q_OS_MACOS (for macOS only)" .
++ " or Q_OS_DARWIN (for all Apple platforms) instead", "terminology");
++ }
++
++ if (/\bQ_OS_MACX\b/) {
++ complain_ln("Using deprecated define Q_OS_MACX; use Q_OS_MACOS instead", "terminology");
++ }
++
++ if (/\bQ_OS_OSX\b/) {
++ complain_ln("Using deprecated define Q_OS_OSX; use Q_OS_MACOS instead", "terminology");
++ }
++ } elsif ($qmake) {
++ # check qmake scopes
++ if (s/\bmac\s*:\s*!\s*ios\b/X/) {
++ complain_ln("Use of deprecated idiom 'mac:!ios', use 'macos' instead", "terminology");
++ }
++
++ if (s/^([^#]*)\bmac\b( *[|:{])/$1X$2/) {
++ complain_ln("Using deprecated qmake scope 'mac': use 'macos' (for macOS only)" .
++ " or 'darwin' (for all Apple platforms) instead", "terminology");
++ }
++
++ # Match the word macx but avoid matching macx- and macx* since these are valid for mkspecs
++ if (/\bmacx\b(?![-*])/) {
++ complain_ln("Using deprecated qmake scope 'macx'; use 'macos' instead", "terminology");
++ }
++
++ if (/\bosx\b/) {
++ complain_ln("Using deprecated qmake scope 'osx'; use 'macos' instead", "terminology");
++ }
++ }
++
++ if (/(?<!\bContents\/)\bMacOS\b/ or /\bOS[\s]*X\b/ or /\bmacintosh\b/i) {
++ complain_ln("Possible incorrect use of Apple-related terminology; " .
++ "the correct name of Apple's desktop operating system is 'macOS'", "", -1);
++ }
++ }
++
++ sub check_invisible_unicode()
++ {
++ # Check for invisible Unicode characters that can hide malicious code.
++ # This protects against GlassWorm-style stealth payloads that exploit
++ # the gap between what humans see in editors and what machines execute.
++ #
++ # Blocks the most dangerous invisible characters while allowing:
++ # - U+200C (ZWNJ) and U+200D (ZWJ): Essential for emoji and complex scripts
++ # - U+FE00-FE0F (variation selectors): Used with emoji
++ # - U+FEFF (BOM): Only at file start (position 0)
++ #
++ # Always blocked:
++ # - U+200B (zero-width space): Primary stealth attack vector
++ # - U+2060-2069 (invisible operators/separators): Stealth attack vectors
++ # - U+E0100-E01EF (variation selectors supplement): Rarely legitimate
++ #
++ # This check cannot be bypassed via configuration. Legitimate uses of blocked
++ # characters (e.g., in Unicode test data) require manual admin approval.
++ if (/[\x{200B}] # Zero-width space
++ |[\x{2060}-\x{2069}] # Invisible operators, word joiner, separators
++ |[\x{E0100}-\x{E01EF}]/x) { # Variation selectors supplement
++ complain_ln("Invisible Unicode characters detected (possible stealth payload)",
++ "stealth", 1);
++ }
++ # BOM (U+FEFF) is only allowed at position 0 of the file
++ if (/\x{FEFF}/) {
++ if ($lineno != 1 || !/^\x{FEFF}/) {
++ complain_ln("Invisible Unicode characters detected (possible stealth payload)",
++ "stealth", 1);
++ }
++ }
++ }
++
++ # The hard-coded fallbacks could be avoided by init-repository setting things up.
++ my $with_pickbot = parse_bool($config{'with-pickbot'} // "false");
++ my @LTS = split(/\s+/, $config{'lts-branch'} || "5.6 5.9 5.12 5.15 6.2 6.5");
++ # Assume that all WIP branches are reliably prefixed with wip/.
++ # Use this when the repo has official branches whose names
++ # don't follow Qt policy. If this bypass is not enabled,
++ # only numeric branches are included in the branch filters.
++ my $wipPrefixed = parse_bool($config{'wip-prefixed'} // "false");
++
++ my %allHeads = ();
++ my %nonDevHeads = ();
++ my $foundLts = 0;
++ my $foundDev = 0;
++ my $seenOrigin = 0;
++ open HEADS, '-|', 'git', 'for-each-ref',
++ '--format=%(objectname) %(refname)',
++ 'refs/heads/', 'refs/remotes/origin/' or die "cannot run git: $!";
++ while (<HEADS>) {
++ last if (!m,^([[:xdigit:]]{40}) refs/(.*),);
++ my ($rev, $name) = ($1, $2);
++ if ($name =~ s,^remotes/,,) {
++ if (!$seenOrigin) {
++ ($foundDev, $foundLts, %allHeads, %nonDevHeads) = (0, 0);
++ $seenOrigin = 1;
++ }
++ } elsif ($seenOrigin) {
++ next;
++ }
++ if ($name =~ m,/dev$,) {
++ $foundDev = 1;
++ } elsif ($wipPrefixed
++ ? ($name =~ m,(?<!^wip)/(.+)$,)
++ : ($name =~ m,/((?:(?:tqtc/)?lts-)?\d+(?:\.\d+)+)$,)) {
++ my $branch = $1;
++ ONCE: while (1) {
++ foreach my $lts (@LTS) {
++ if ($branch =~ /^\Q$lts\E(?:\.|$)/) {
++ $foundLts = 1;
++ push(@{$nonDevHeads{$lts}}, $rev);
++ last ONCE;
++ }
++ }
++ push(@{$nonDevHeads{''}}, $rev);
++ last;
++ }
++ } elsif ($name !~ m,/(?:master|main)$,) {
++ next;
++ }
++ $name =~ s,^[^/]+/,,; # Strip heads/ or origin/
++ $allHeads{$name} = $rev;
++ }
++ close HEADS;
++ printerr;
++
++ my $handleLts = 0;
++ my $ltsForSha1 = '';
++ if (!$with_pickbot && $foundDev && $foundLts) {
++ $handleLts = 1;
++ if (defined($target)) {
++ $ltsForSha1 = $1 if ($target =~ /^(\d+\.\d+)/ && any { $1 eq $_ } @LTS);
++ } else {
++ chomp(my $sha1Base = `git merge-base $sha1 $allHeads{dev}`);
++ foreach my $lts (@LTS) {
++ next if (!defined($nonDevHeads{$lts}));
++ my $key = "cached-lts-$lts-fork";
++ $key =~ s/\./-/g; # so the dot in $lts isn't a section name boundary in the key
++ my $diverge = $config{$key};
++ if (!$diverge) {
++ chomp($diverge = `git merge-base $allHeads{$lts} $allHeads{dev}`);
++ system("git config sanity.$instance.$key $diverge");
++ }
++ if ($sha1Base eq $diverge) {
++ $ltsForSha1 = $lts;
++ last;
++ }
++ }
++ }
++ }
++
++ sub check_cherry_pick($)
++ {
++ my ($orig) = @_;
++
++ if ($handleLts) {
++ my @sources;
++ foreach my $br (keys %nonDevHeads) {
++ push @sources, @{$nonDevHeads{$br}} if ($br ne $ltsForSha1);
++ }
++ if (@sources) { # Expected to be always true ...
++ # git creates a hypothetical merge between the sources, so
++ # valid commits will lie in the ancestry of this merge,
++ # making the commit itself the best merge base.
++ chomp(my $origBase = `git merge-base $orig @sources 2> /dev/null`);
++ return if ($origBase eq $orig);
++ }
++ } elsif (!$with_pickbot) {
++ # The general policy when no LTS branch is present.
++ # No line, so people are less likely to get stupid ideas.
++ complain("Cherry-picks are strongly discouraged", "cherry");
++ }
++ return if (!%allHeads); # Some truly weird repo
++ my @allHds = values %allHeads;
++ chomp(my $origBase = `git merge-base $orig @allHds 2> /dev/null`);
++ if ($origBase ne $orig) {
++ # Most likely not integrated yet, but may also be a wip/ branch.
++ complain_ln("Cherry-pick's source is not a valid merged commit", "cherry");
++ } elsif ($handleLts) {
++ complain("Cherry-pick will be subsequently merged with its source branch", "cherry");
++ }
++ }
++
++ my $havecherry = defined($cfg{cherry});
++ my $badlog = defined($cfg{log});
++ my $badurl_rx = $config{badurl};
++ my $badurl = !defined($badurl_rx) || defined($cfg{url});
++ my $badid = defined($cfg{changeid});
++ my $badrev = 0;
++ my $badsign = 0;
++ my $parents = 0;
++ my $inchangelog = 0;
++ my $doftr = !defined($cfg{footer});
++ my ($footer, $cherry) = (STS_BLANK, 0);
++ my ($revert1, $revert2, $nonrevert) = (0, 0, 0);
++ my ($coverity, $cid) = (0, 0);
++ my $msgline = 0;
++ my %picktos = ();
++ my $prevline = "";
++ my $changelog3rdparty = 0;
++ my $reopens = 0;
++ open MSG, "git cat-file -p ".$sha1." |" or die "cannot run git: $!";
++ binmode(MSG, ":utf8") or die "cannot set encoding: $!";
++ while (<MSG>) {
++ last if ($_ eq "\n");
++ if (/^parent /) {
++ $parents++ ;
++ } elsif (/^author ([^<]*?) *<([^>]+)/) {
++ check_email($1, $2, $parents + 1, "author");
++ } elsif (/^committer ([^<]*?) *<([^>]+)/) {
++ check_email($1, $2, $parents + 3, "committer");
++ }
++ }
++ while (<MSG>) {
++ chomp;
++ $lineno++;
++ $msgline++;
++ if ($msgline == 1) {
++ $summary = $_;
++ $lineno = $parents + 6 if ($gerrit_rest);
++ $revert1 = 1 if (/^Revert ".*"$/);
++ if (/^revert(ed|ing)? (commit )?[0-9a-f]{7,40}\.?$/i) {
++ complain_cln("Summary of revert mentions only SHA1", "log");
++ # TODO: confirm reverted commit is real
++ }
++ if (/\bQT[A-Z]+-\d+\b/) {
++ complain_cln("Bug reference in summary", "log");
++ }
++ if (/(?<!etc)\.$/) {
++ complain_cln("Summary ends with period", "log");
++ }
++ # Detect wrong tense, but exclude cases like 'Speed up ...'
++ if (s/^[^:]+: //r =~ /^\w+(ing|[^e](ed)|es)\s/) {
++ complain_cln("Summary does not use imperative mood", "log");
++ }
++ if (!$badlog && length($_) < 7) {
++ complain_cln("Summary is too short", "log");
++ } elsif (!$badlog && !$revert1 && length($_) > 120) {
++ complain_cln("Summary is excessively long", "log");
++ } elsif ($parents < 2 && !$revert1 && length($_) > 72) {
++ complain_cln("Aim for shorter summaries", "", -1);
++ }
++ } else {
++ if (/^This reverts commit [[:xdigit:]]{40}\.?$/) {
++ $revert2 = 1;
++ } elsif (!/^[-\w]+:|^$/) {
++ $nonrevert = 1;
++ }
++ if ($msgline == 2) {
++ if (!$badlog && $_ ne "") {
++ complain_cln("2nd line is not empty", "log");
++ }
++ } elsif ($_ eq "") {
++ $cherry = 0;
++ $inchangelog = 0;
++ # Empty line following footer(s).
++ $footer = ($footer == STS_FOOTER) ? STS_FTR_BLNK : STS_BLANK;
++ if ($prevline eq "") {
++ complain_cln("Two trailing newlines", "log");
++ }
++ } elsif ($cherry) {
++ $cherry = 0 if (/\)/);
++ } else {
++ my $ftr = 0;
++ if (/^\((?:partial(?:ly)? )?(?:cherry[- ]pick|(?:back-?)?port|adapt)(?:ed)?(?: from| of)?(?: commit)? (\w+\/)?([[:xdigit:]]{7,40})/) {
++ if (!$1) {
++ check_cherry_pick($2) if (!defined($cfg{cherry}));
++ $havecherry = 1;
++ } elsif ($with_pickbot) {
++ $havecherry = 1;
++ }
++ $cherry = 1 if (!/\)/);
++ # Some bad footers are ok above cherry-pick lines.
++ $badrev = 0;
++ $badsign = 0;
++ # cherry-pick lines count as footers as well.
++ $ftr = 1;
++ $inchangelog = 0;
++ } elsif (/^(?:[A-Z][A-Za-z]+(?:-[A-Za-z][a-z]+)+|Fixes|Reopens|C[Hh][Aa][Nn][Gg][Ee][Ll][Oo][Gg]|Q[Uu][Ii][Pp]):(.+)/) {
++ if ($1 !~ /^ /) {
++ complain_ln("Missing space after colon in footer", "");
++ }
++ $ftr = 1;
++ $inchangelog = 0;
++ if (/^Change-?Log:/i) {
++ complain_ln("Use [ChangeLog] tag instead", "changelog") if (!defined($cfg{changelog}));
++ } elsif (/^Reviewed-by:/i) {
++ $badrev = $lineno;
++ } elsif (/^Signed-off-by:/i) {
++ $badsign = $lineno;
++ } elsif (/^Task-number: *(.*)/i) {
++ complain_ln("Multiple tasks in one footer", "") if ($1 =~ /[ ,]/);
++ complain_ln("Capitalization of \"Task-number\" is wrong", "") if (!/^Task-number:/);
++ } elsif (/^Fixes: *(.*)/) {
++ complain_ln("Multiple tasks in one footer", "") if ($1 =~ /[ ,]/);
++ } elsif (/^Reopens: *(.*)/) {
++ $reopens = 1;
++ complain_ln("Multiple tasks in one footer", "") if ($1 =~ /[ ,]/);
++ } elsif (/^Pick-to: *(.*)/i) {
++ for my $pick (split(/\s+/, $1)) {
++ if (exists($picktos{$pick})) {
++ complain_ln("Duplicate entry '$pick' in Pick-to footer", "pickto", 1);
++ next;
++ }
++ $picktos{$pick} = 1;
++ my $valid_pick = 0;
++ if ($repo !~ /\/tqtc-/ && $pick =~ /^(\d+\.\d+)\.\d+$/
++ && grep { $_ eq $1 } @LTS) {
++ # Bypass validity check for LTS release branch pick in public repos
++ $valid_pick = 1;
++ } else {
++ my @pick_alternatives = ($pick, "lts-$pick", "tqtc/lts-$pick");
++ foreach my $pick_alt (@pick_alternatives) {
++ if (defined($allHeads{$pick_alt})) {
++ $valid_pick = 1;
++ last;
++ }
++ }
++ }
++ if (!$valid_pick) {
++ complain_ln("Pick-to entry '$pick' is not a valid branch in $repo",
++ "pickto");
++ } elsif (defined($target) && ($pick eq $target)) {
++ complain_ln("Pick-to entry '$pick' equals source branch",
++ "pickto", 1)
++ }
++ }
++ complain_ln("Capitalization of \"Pick-to\" is wrong", "") if (!/^Pick-to:/);
++ } elsif (/^QUIP: *(.*)/i) {
++ my $quip = $1;
++ complain_ln("Multiple QUIPs in one footer", "") if ($quip =~ /[ ,]/);
++ complain_ln("Non-numeric QUIP identifier", "") if ($quip =~ /^[^0-9]/);
++ complain_ln("Capitalization of \"QUIP\" is wrong", "") if (!/^QUIP:/);
++ } elsif (/^Coverity-Id: *(.*)/i) {
++ $cid = 1;
++ my $cidval = $1;
++ complain_ln("Multiple CIDs in one footer", "") if ($cidval =~ /[ ,]/);
++ complain_ln("Non-numeric CID", "") if ($cidval =~ /[^0-9 ,]/); # [ ,]: already checked above
++ complain_ln("Leading zero", "") if ($cidval =~ /^0/);
++ complain_ln("Capitalization of \"Coverity-Id\" is wrong", "") if (!/^Coverity-Id:/);
++ }
++ } elsif (/^\[change-?log\]/i) {
++ $inchangelog = 1;
++ if (!/^\[ChangeLog\]/ && !defined($cfg{changelog})) {
++ complain_ln("Bad form of [ChangeLog] tag", "changelog");
++ }
++ if (/\[(?:third|3rd)[ _-]*party[ _-]*code\]/i) {
++ $changelog3rdparty = $lineno;
++ if (!/\[Third-Party Code\]/) {
++ complain_ln("Bad form of [Third-Party Code] tag", "changelog");
++ }
++ }
++ } elsif (/^\[[A-Z][- \w]+\]/) {
++ complain_ln("Likely change-log entry without [ChangeLog] tag", "", -1);
++ }
++ if ($inchangelog) {
++ complain_ln("Missing space between ChangeLog tags and text", "changelog") if (/\][^\s\[]/);
++ complain_ln("JIRA task referenced from ChangeLog", "changelog") if (/\[QT[A-Z]+-\d+\]/);
++ complain_ln("Redundant repository reference in ChangeLog", "", -1) if (/\[\Q$repo\E\]/i);
++ # Note that this is not mutually exclusive with the else-branch below -
++ # a ChangeLog section is just regular text.
++ }
++ if ($ftr && $doftr) {
++ if ($footer == STS_TEXT) {
++ complain_ln("Missing empty line between text and footers", "footer");
++ } elsif ($footer == STS_FTR_BLNK) {
++ do_complain($lineno - 1, "Empty lines between footers", "footer");
++ }
++ $footer = STS_FOOTER;
++ } else {
++ if ($footer == STS_FOOTER || $footer == STS_FTR_BLNK) {
++ complain_ln("Text following footers", "footer");
++ }
++ $footer = STS_TEXT;
++ }
++ }
++ $prevline = $_;
++ }
++
++ if (!$badid && /\bI[0-9a-f]{40}\b/ && !/^Change-Id: /) {
++ complain_ln("Gerrit change id outside Change-Id footer", "changeid");
++ }
++ if (!$badurl && /$badurl_rx/o) {
++ complain_ln("URL pointing to Gerrit or JIRA", "url");
++ }
++ if (/\b[Cc]overity\b|\b[Cc][Ii][Dd][Ss]?:? *#?[0-9]+/) {
++ $coverity = $lineno;
++ }
++ check_spelling() if ($spell_check);
++ check_apple_terminology();
++ check_invisible_unicode();
++ styleFail("Trailing whitespace") if (s/[ \t]+\r?$//);
++ styleFail("Space indent followed by a TAB character") if (/^ +\t/);
++ styleFail("TAB character in non-leading whitespace") if (/\S *\t/);
++ }
++ close MSG;
++ printerr;
++
++ if ($revert1 && $revert2 && !$nonrevert) {
++ complain("Revert without explanation", "revert", 1);
++ }
++ if ($badrev && !defined($cfg{revby})) {
++ do_complain($badrev, "Bogus Reviewed-by footer", "revby", 1);
++ }
++ if ($badsign) {
++ do_complain($badsign, "Unnecessary Signed-off-by footer", "", -1);
++ }
++ if ($coverity && !$cid) {
++ do_complain($coverity, "Mention of Coverity without Coverity-Id footer is probably incorrect", "");
++ }
++ if ($with_pickbot && !$havecherry) {
++ my $onDev;
++ # The development branch: dev, else master, else main.
++ my $masterName = $foundDev ? "dev"
++ : (defined($allHeads{master}) || !defined($allHeads{main})) ? "master"
++ : "main";
++ if (defined($target)) {
++ # Only the wip/ bypass takes the optional tqtc/ prefix: a tqtc- repo
++ # mirrors public branches under their plain names, so its dev is "dev".
++ # A tqtc/dev, where present, is a separate line alongside it - and
++ # $masterName is also the Pick-to token suggested below.
++ $onDev = ($target eq $masterName) || ($target =~ m,^(?:tqtc/)?wip/,);
++ } else {
++ my $masterRev = $allHeads{$masterName};
++ chomp(my $sha1Base = `git merge-base $sha1 $masterRev`);
++ # Catch both ancestor and descendant cases.
++ $onDev = ($sha1Base eq $sha1 || $sha1Base eq $masterRev);
++ }
++ if (!$onDev) {
++ if (%picktos) {
++ if (exists($picktos{$masterName})) {
++ complain("Picking downward from $masterName is preferred", "", -1);
++ } else {
++ complain("Omission of $masterName from Pick-to footer might be incorrect",
++ "", -1);
++ }
++ if ($target =~ /(\d+\.\d+)\.\d+$/) {
++ # Verify that the upstream stable branch is in the pick-to if the current
++ # target is a release branch.
++ if (!exists($picktos{$1})) {
++ complain("Omission of $1 from the Pick-to footer is probably incorrect",
++ "cherry")
++ }
++ }
++ } else {
++ complain("Omission of Pick-to footer is probably incorrect", "cherry");
++ }
++ } elsif (%picktos) {
++ my %majors;
++ # Populate a hash of majors with arrays of stable branches
++ foreach (keys %allHeads) {
++ if (/^(\d+)\.(\d+)$/) {
++ push @{$majors{$1}}, $2;
++ }
++ }
++ foreach (sort keys %majors) {
++ if (@{$majors{$_}} >= 2) {
++ my @major = sort { $a <=> $b } @{$majors{$_}};
++ my ($latest, $previous) = ("$_.$major[-1]", "$_.$major[-2]");
++ if (exists($picktos{$previous}) && !exists($picktos{$latest})) {
++ complain("Omission of $latest from cherry-pick targets".
++ " is probably incorrect", "cherry");
++ }
++ }
++ }
++ }
++ } elsif (!$havecherry && length($ltsForSha1)) {
++ complain("Commit on LTS branch is not a cherry-pick", "cherry");
++ }
++ if ($havecherry && $reopens) {
++ complain("Inclusion of Reopens on a cherry-pick is probably incorrect.", "cherry");
++ }
++
++ complain_spelling();
++ complain_style();
++
++ my $chunk = 0;
++ my @addi = ();
++ my @deli = ();
++ my $nonws;
++ my $ws;
++ my $mixws_check = 0;
++ my %ws_lines = (); # hash of lists
++ my $braces = 0;
++ my $open_key = qr/\s*#\s*if|.*{/;
++ my $close_key = qr/\s*#\s*endif|.*}/;
++ my $kill_all_ws = qr/\s+((?:\"(?:\\.|[^\"])*\"|\S)+)/; # Collapse all whitespace not inside strings.
++ my $kill_nl_ws = qr/((?:\"(?:\\.|[^\"])*\"|\S)+)\s+/; # Collapse all non-leading whitespace not inside strings.
++
++ $lineno = 0;
++
++ sub flushChunk()
++ {
++ my $loc_nonws = 0;
++ my $nlonly = 1;
++ my ($ai, $di) = (0, 0);
++ while (!$loc_nonws) {
++ my ($a, $d) = ("", "");
++ while ($ai < @addi) {
++ $a = $addi[$ai++];
++ $a =~ s/\s+$//;
++ if (length($a)) {
++ $nlonly = 0;
++ last;
++ }
++ }
++ while ($di < @deli) {
++ $d = $deli[$di++];
++ $d =~ s/\s+$//;
++ if (length($d)) {
++ $nlonly = 0;
++ last;
++ }
++ }
++ last if (!length($a) && !length($d));
++
++ $a =~ /^$close_key/o and $braces--;
++ $d =~ /^$close_key/o and $braces++;
++ if ($braces) {
++ $a =~ s/$kill_nl_ws/$1/go;
++ $d =~ s/$kill_nl_ws/$1/go;
++ } else {
++ $a =~ s/$kill_all_ws/$1/go;
++ $d =~ s/$kill_all_ws/$1/go;
++ }
++ $loc_nonws = 1 if ($a ne $d);
++ $a =~ /^$open_key/o and $braces++;
++ $d =~ /^$open_key/o and $braces--;
++ }
++ while ($ai < @addi) {
++ my $a = $addi[$ai++];
++ $a =~ /^$close_key/o and $braces--;
++ $a =~ /^$open_key/o and $braces++;
++ }
++ while ($di < @deli) {
++ my $d = $deli[$di++];
++ $d =~ /^$close_key/o and $braces++;
++ $d =~ /^$open_key/o and $braces--;
++ }
++ if ($loc_nonws) {
++ $nonws = 1;
++ } elsif (!$nlonly) {
++ $ws = 1;
++ push @{$ws_lines{$file}}, $lineno - $#addi;
++ }
++ @addi = @deli = ();
++ $chunk = 0;
++ }
++
++ sub formatSize($)
++ {
++ my $sz = shift;
++ if ($sz >= 10 * 1024 * 1024) {
++ return int($sz / (1024 * 1024))."MiB";
++ } elsif ($sz >= 10 * 1024) {
++ return int($sz / 1024)."KiB";
++ } else {
++ return int($sz)."B";
++ }
++ }
++
++ sub isExe($)
++ {
++ my $sha = shift;
++ open(my $hndl, "git cat-file -p $sha |") or return 0;
++ read($hndl, my $hdr, 4) or return 0;
++ return $hdr =~ /^(\x7FELF|MZ)/;
++ }
++
++ my $no_copyright = 0;
++
++ sub flushFile()
++ {
++ if ($no_copyright && $lineno > ($file =~ /^tests\/.*\.qml$/ ? 20 : 10)) {
++ complain("Missing copyright header", "copyright");
++ }
++ complain_spelling();
++ complain_style();
++ }
++
++ my $object_like_macro = qr/
++ (
++ QT_USE_NAMESPACE |
++ QT_BEGIN_INCLUDE_NAMESPACE |
++ QT_END_INCLUDE_NAMESPACE |
++ QT_BEGIN_NAMESPACE |
++ QT_END_NAMESPACE |
++ Q_OBJECT |
++ Q_GADGET |
++ QML_ELEMENT
++ );
++ /x;
++
++ my $function_like_macro = qr/
++ (
++ QT_FORWARD_DECLARE_CLASS |
++ QT_FORWARD_DECLARE_STRUCT |
++ Q_PROPERTY |
++ Q_DECLARE_PUBLIC |
++ Q_DECLARE_PRIVATE |
++ Q_DECLARE_PRIVATE_D |
++ Q_DISABLE_COPY |
++ Q_DISABLE_COPY_MOVE |
++ QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_MOVE_AND_SWAP |
++ QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_PURE_SWAP
++ )\([^)]*\);
++ /x;
++
++ my $merge;
++ my $new_file;
++ my $foreign;
++ my $is_bin;
++ my $maybe_bin;
++ my $is_special;
++ my $need_hash_bang;
++ my $size;
++ my $check_gen = 0;
++ my $crlf_fail;
++ my $in_plus;
++ my $conflict_fail;
++ my $tabs_check;
++ my $ws_check;
++ my $allow_2spaces;
++ my $tsv_check;
++ my $eof_check;
++ my $ctlkw_check;
++ my $apple_check;
++ my $notobjc_check;
++ my $stealth_check;
++ my $attribution_changed;
++ open DIFF, "git diff-tree --minimal --no-commit-id --diff-filter=ACMR --ignore-submodules " .
++ "--src-prefix=\@old\@/ --dst-prefix=\@new\@/ --full-index -r -U100000 --cc -C -l1000 " .
++ "--root ".$sha1." |" or die "cannot run git: $!";
++ binmode(DIFF, ":utf8");
++ while (<DIFF>) {
++ if (/^-/) {
++ if ($mixws_check) {
++ /^--- / and next;
++ push @deli, substr($_, 1);
++ $chunk = 1;
++ }
++ $in_plus = 0;
++ next;
++ }
++ if ($lineno < 50) {
++ if ($no_copyright && /Copyright/) {
++ $no_copyright = 0;
++ }
++ if ($check_gen && /All changes made in this file will be lost|This file (?:is|was) (?:auto(?:matically |-)?)?generated|DO NOT (?:EDIT|CHANGE)|DO NOT delete this file|[Gg]enerated by|uicgenerated|produced by gperf|made by GNU Bison/) {
++ complain("Adding generated file (inferred from line ".($lineno + 1).")", "generated")
++ if ($new_file && !defined($cfg{generated}));
++ $ws_check = 0;
++ $check_gen = 0;
++ }
++ }
++ if (/^\+/) {
++ if (/^\+\+\+ /) {
++ # This indicates a text file; binary files have "Binary files ... and ... differ" instead.
++ $maybe_bin = 0;
++
++ if ($file =~ /\.(prl|la|pc|ilk)$/i) {
++ complain("Adding build artifact", "generated") if ($new_file && !defined($cfg{generated}));
++ $ws_check = 0;
++ } else {
++ if ($file !~ /\.qmltypes$/i) {
++ $check_gen = 1;
++ }
++ if ($file =~ m,^tests/.*/(perf|test)?data[/_-],) {
++ $ws_check = $eof_check = $apple_check = $no_copyright = 0;
++ # Additional logic for test data goes here...
++ } elsif ($file =~ /(~|\.(old|bak))$/i) {
++ complain("Adding backup file", "backup") if ($new_file && !defined($cfg{backup}));
++ $ws_check = 0;
++ } elsif ($file =~ /\.(hpp|ch|h\+\+|hh|hxx)$/i) {
++ complain("Adding header file with a non-.h extension", "extension") if ($new_file && !$foreign && !defined($cfg{extension}));
++ }
++ }
++ next;
++ }
++ next if ($is_bin);
++ $lineno++;
++ if ($merge) {
++ # Consider only lines which are new relative to both parents, i.e., were added during the merge.
++ s/^\+\+// or next;
++ } else {
++ $_ = substr($_, 1);
++ if ($mixws_check) {
++ push @addi, $_;
++ $chunk = 1;
++ }
++ }
++ if ($lineno == 1 && $need_hash_bang) {
++ if (/^#! ?\//) {
++ $need_hash_bang = 0;
++ } else {
++ # Can still be valid if run via the right interpreter, so level -1:
++ complain("Executable file with no initial #! line", "", -1);
++ }
++ }
++ $in_plus = 1;
++ if (!$crlf_fail && /\r\n$/) {
++ $crlf_fail = 1;
++ if ($lineno != 1) {
++ # A stray CRLF somewhere in the middle.
++ complain_ln("CRLF line endings", "crlf");
++ } else {
++ # Assume that the whole file is affected.
++ complain("CRLF line endings", "crlf");
++ }
++ }
++ if (!$conflict_fail && /^(?:[<>=]){7}( |$)/) {
++ complain_ln("Unresolved merge conflict", "conflict");
++ $conflict_fail = 1;
++ }
++ if ($ws_check) {
++ if ($tsv_check) {
++ styleFail("Mixing spaces with TABs") if (/^ +\t|\t +/);
++ } else {
++ if ($allow_2spaces) {
++ s/(?<=\S) (?=\r?\n$)//;
++ }
++ styleFail("Trailing whitespace") if (s/[ \t]+\r?\n$//);
++ styleFail("Space indent followed by a TAB character") if (/^ +\t/);
++ styleFail("TAB character in non-leading whitespace") if (/\S *\t/);
++ if ($tabs_check) {
++ styleFail("Leading tabs") if (/^\t+/);
++ if ($ctlkw_check) {
++ styleFail("Flow control keywords must be followed by a single space")
++ if (/\b(if|if constexpr|elsif|for|foreach|Q_FOREACH|while|do|switch)(| +)\(/);
++ }
++ }
++ }
++ }
++ check_spelling() if ($spell_check);
++ if ($apple_check) {
++ check_apple_terminology();
++ if ($notobjc_check && /\b__OBJC__\b/) {
++ complain_ln("__OBJC__ will never be defined for non-Objective-C/C++ source files", "objc");
++ }
++ }
++ check_invisible_unicode() if ($stealth_check);
++ if ($clike && /\bQ_CLANG_QDOC\b/) {
++ complain_ln("Using deprecated define Q_CLANG_QDOC; use Q_QDOC instead", "qdoc");
++ }
++ if ($clike && /\bqWait\s*\(.*?\)/) {
++ complain_ln("Usage of qWait() is discouraged", "", -1);
++ }
++ # Check for Unicode Bi-directional Override exploit characters.
++ # NB: data has been UTF-8 encoded by the time it gets here,
++ # but retain Unicode form of it for (a) ease of reading (b)
++ # maybe perl will change encoding...
++ if (/\xe2(\x80[\xaa-\xae]|\x81[\xa6-\xa9])/ || /[\x{202A}-\x{202E}\x{2066}-\x{2069}]/) {
++ complain_ln("Unicode bi-directional override characters", "bidi", 1)
++ }
++ # Don't grumble about the #if-ery around deprecations (or the #defines
++ # of the deprecation macros), only about the deprecations themselves.
++ if (!/^\h*#/ && /\bQ(T|_DECL_ENUMERATOR)_DEPRECATED_/) {
++ complain_ln("Deprecation adjustments already submitted?", "", -1);
++ }
++ # Check for extraneous semicolon after macros.
++ if ($_ =~ $object_like_macro || $_ =~ $function_like_macro) {
++ styleFail("Semicolon after $1");
++ }
++ } else {
++ flushChunk() if ($chunk);
++ if (/^ /) {
++ $lineno++ if (!$merge || !/^ -/);
++ $in_plus = 0;
++ next;
++ }
++ if ($eof_check && !$is_special && $in_plus && /^\\ No newline/) {
++ complain("No newline at end of file", "fileend");
++ next;
++ }
++ if ($merge ? /^\@\@\@ -\S+ -\S+ \+(\d+)/ : /^\@\@ -\S+ \+(\d+)/) {
++ $lineno = $1 - 1;
++ next;
++ }
++ if (/^diff /) {
++ flushFile();
++ if (/^diff --git \@old\@\/.+ \@new\@\/(.+)$/) {
++ $merge = 0;
++ } elsif (/^diff --cc (.+)$/) {
++ $merge = 1;
++ } else {
++ print STDERR "Warning: cannot parse diff header: ".$_;
++ next;
++ }
++ $file = $1;
++ #print "*** got file ".$file.".\n";
++ if ($file =~ /qt_attribution\.json$/) {
++ $attribution_changed = 1;
++ if (!$changelog3rdparty) {
++ complain("qt_attribution.json changed without [ChangeLog][Third-Party Code] in the commit message", "changelog");
++ }
++ }
++ $clike = ($file =~ /\.(c|cc|cpp|c\+\+|cxx|qdoc|m|mm|h|hpp|hxx|cs|java|js|qs|qml|g|y|ypp|pl|glsl)$/i);
++ $qmake = ($file =~ /\.pr[filo]$/i);
++ $is_bin = ($file =~ /\.(ps|pdf)$/);
++ $foreign = $is_bin || ($file =~ /(^|\/)3rdparty\//) || ($file =~ /^LICENSES\//);
++ $new_file = 0;
++ $maybe_bin = 0;
++ $is_special = 0;
++ $need_hash_bang = 0;
++ $crlf_fail = $is_bin || defined($cfg{crlf});
++ $in_plus = 0;
++ $mixws_check = !$merge && !$foreign && $clike && !defined($cfg{mixws});
++ $ws_check = !defined($cfg{style}) && !$foreign && ($file !~ /\.(ts|diff|patch)$|^\.gitmodules$/);
++ $tsv_check = $ws_check && ($file =~ /((^|\/)objects\.map$|\.tsv$)/);
++ $tabs_check = $ws_check && !$tsv_check && !defined($cfg{tabs})
++ && ($file !~ /((^|\/)Makefile\b|debian[.\/]rules|\.(pbxproj|plist(\.[^.\/]+)?|def|mk|spec|changes|[xn]ib|storyboardc?|go|svg|ui[ap])(?:\.in)?$)/);
++ $allow_2spaces = $ws_check && ($file =~ /\.md$/);
++ $ctlkw_check = $tabs_check && $clike;
++ $eof_check = !defined($cfg{fileend}) && !$foreign
++ && ($file !~ /(\.plist(\.[^.\/]+)?|\.xcassets\/.*\.json)$/); # Xcode consistently forgets the trailing newline
++ # .ts files usually contain languages other than English
++ $spell_check = !defined($cfg{spell}) && !$foreign && ($file !~ /\.ts$/i);
++ $apple_check = !$foreign && ($file !~ /\.ts$/i);
++ $notobjc_check = $apple_check && ($file =~ /\.(c|cc|cpp|c\+\+|cxx)$/i);
++ # Invisible Unicode detection is always enabled. File-type exclusions
++ # can be added here if needed (e.g., for Unicode test data files).
++ $stealth_check = 1;
++ $conflict_fail = $is_bin || defined($cfg{conflict});
++ $braces = 0;
++ $check_gen = 0;
++ $no_copyright = 0;
++ next;
++ }
++ if ($maybe_bin && /^Binary files /) {
++ if ($new_file) {
++ if (!defined($cfg{generated}) && ($file =~ /\.(obj|o|lib|a|dll|so|exe|exp|qm|pdb|idb|suo)$/i || isExe($maybe_bin))) {
++ complain("Adding build artifact", "generated");
++ }
++ } else {
++ if (!defined($cfg{giant}) && $size > (2 << 20)) {
++ complain("Changing huge binary file (".formatSize($size)." > 2MiB)", "giant", 1);
++ }
++ }
++ if ($file =~ /\.png$/i
++ # If identify is available, check for faults:
++ && haveIdentify() && dodgyPng($sha1, $file)
++ # Only complain if the issue is new, of course:
++ && ($new_file || !dodgyPng("${sha1}^", $file))) {
++ complain("Defect in PNG format, see: identify $file", "cruft");
++ }
++ next;
++ }
++ if (/^new(?: file)? mode (\d+)$/) {
++ my $text = $1;
++ if ($text eq "160000" || $text eq "120000") {
++ $is_special = 1;
++ next;
++ }
++ my $perm = oct($text);
++ # Executable or set*id:
++ if ($perm & 07000) {
++ # set*id; git discards it - but asking for it is misguided.
++ complain("Adding file with set*id permissions", "permissions", 1);
++ } elsif ($perm & 0111) {
++ if ($file =~ /\.(py|pl|sh|bash|php|cgi|js)$/
++ # i.e. known script suffix; or no suffix at all:
++ || $file !~ m,[^/.]\.[^/.]+$,) {
++ $need_hash_bang = 1;
++ } elsif ($file !~ /\.(exe|bat|cmd|ps1)$/) {
++ # Should not be executable.
++ complain("Adding implausibly executable file", "permissions");
++ }
++ }
++ }
++ if (!$is_special && /^index ([\w,]+)\.\.(\w+)(?! 160000)( |$)/) {
++ my ($old_trees, $new_tree) = ($1, $2);
++ #print "*** got index $old_trees $new_tree.\n";
++ $size = `git cat-file -s $new_tree`;
++ my $issrc = $clike || ($file =~ /\.(s|asm|pas|l|m4|bat|cmd|sh|py|php|qdoc(conf)?|json)$/i);
++ if ($old_trees =~ /^0{40}(,0{40})*$/) {
++ #print "*** adding ".$file.".\n";
++ if (!$conflict_fail && $file =~ /\.(BACKUP|BASE|LOCAL|REMOTE)\.[^\/]+$/) {
++ complain("Adding temporary file from merge conflict resolution", "conflict", 1);
++ $conflict_fail = 1;
++ }
++ if (!defined($cfg{alien}) && $file =~ /\.(sln|vcproj|vcxproj|user)$/i) {
++ complain("Warning: Adding alien build system file", "alien");
++ }
++ if ($size > (2 << 20)) {
++ if (!defined($cfg{giant})) {
++ complain("Adding huge file (".formatSize($size)." > 2MiB)", "giant", 1);
++ }
++ } elsif ($size >= 51200 && !$issrc && !defined($cfg{size})) {
++ complain("Warning: Adding big file (".formatSize($size)." >= 50KiB)", "size");
++ }
++ $size = 0;
++ $new_file = 1;
++ $no_copyright = $issrc && $file !~ /\.(qdocconf|json)$/i && $file !~ /(^|\/)3rdparty\//;
++ } elsif ($size > 20000 && !$issrc && !defined($cfg{size})) {
++ my $old_size = 0;
++ for my $old_tree (split(/,/, $old_trees)) {
++ my $osz = `git cat-file -s $old_tree`;
++ $old_size = $osz if ($osz > $old_size);
++ }
++ if ($size > $old_size * 3 / 2) {
++ complain("Warning: Increasing file size by more than 50% (".
++ formatSize($old_size)." => ".formatSize($size).")", "size");
++ }
++ }
++ $maybe_bin = $new_tree;
++ next;
++ }
++ }
++ }
++ close DIFF;
++ printerr;
++ flushFile();
++
++ if ($changelog3rdparty && !$attribution_changed) {
++ $file = "!!!!!";
++ do_complain($changelog3rdparty,
++ "[ChangeLog][Third-Party Code] entry without change to qt_attribution.json",
++ "changelog");
++ }
++
++ if ($mixws_check) {
++ flushChunk() if ($chunk);
++ if ($nonws and $ws) {
++ my @extras;
++ for (sort keys %ws_lines) {
++ $file = $_;
++ if ($gerrit_rest) {
++ do_complain($_, "WS-only change", "", -1) foreach (@{$ws_lines{$file}});
++ } else {
++ push @extras, "WS-only in ".$file.": ".join(", ", @{$ws_lines{$file}});
++ }
++ }
++ $file = "~~~~~";
++ do_complain(1e9, "Mixing whitespace-only changes with other changes", "", -1, @extras);
++ }
++ }
++
++ if ($gerrit_rest) {
++ my %gerrit_review;
++ my @cover_texts;
++ foreach my $fn (sort keys %complaints) {
++ my $file;
++ if (length($fn) && ($fn ne "~~~~~")) {
++ $file = ($fn eq "!!!!!") ? "/COMMIT_MSG" : $fn;
++ my @comments = ();
++ foreach my $ln (sort { $a <=> $b } keys %{$complaints{$fn}}) {
++ my @texts;
++ push @texts, @$_ foreach (@{$complaints{$fn}{$ln}});
++ my %comment;
++ $comment{line} = int($ln) if ($ln && $ln < 1e9);
++ $comment{message} = join "\n\n", @texts;
++ push @comments, \%comment;
++ }
++ $gerrit_review{comments}{$file} = \@comments;
++ } else {
++ foreach my $ln (sort { $a <=> $b } keys %{$complaints{$fn}}) {
++ push @cover_texts, @$_ foreach (@{$complaints{$fn}{$ln}});
++ }
++ }
++ }
++ if (%complaints) {
++ push @cover_texts, $_ for (sort keys %footnotes);
++ push @cover_texts, "See http://wiki.qt.io/Early_Warning_System for explanations.";
++ $gerrit_review{message} = join "\n\n", @cover_texts;
++ }
++ $gerrit_review{labels}{'Sanity-Review'} = (!$fail ? 1 : -$fail);
++ require JSON;
++ print JSON::encode_json(\%gerrit_review)."\n";
++ exit(0);
++ }
++
++ if (%complaints) {
++ $summary =~ s/^(.{50}).{5,}$/$1\[...]/;
++ print "***\n*** Suspicious changes in commit ".$sha1." (".$summary."):\n";
++ my ($lpfx, $elpfx) = ("*** ", "***\n");
++ foreach my $fn (sort keys %complaints) {
++ my $fpfx;
++ if (length($fn) && ($fn ne "~~~~~")) {
++ $file = ($fn eq "!!!!!") ? "commit message" : $fn;
++ print $elpfx.$lpfx.$file.":\n";
++ $fpfx = $lpfx." - ";
++ } else {
++ print $elpfx;
++ $fpfx = $lpfx."- ";
++ }
++ foreach my $ln (sort { $a <=> $b } keys %{$complaints{$fn}}) {
++ my $pfx = $fpfx;
++ $pfx .= "$ln: " if ($ln > 0 && $ln < 1e9);
++ foreach my $ent (@{$complaints{$fn}{$ln}}) {
++ my @ry = @$ent;
++ print $pfx.(shift @ry)."\n";
++ print $lpfx." $_\n" for (@ry);
++ }
++ }
++ }
++ if (%footnotes) {
++ print $elpfx;
++ print $lpfx.$_."\n" for (sort keys %footnotes);
++ }
++ print $elpfx.$lpfx."See http://wiki.qt.io/Early_Warning_System for explanations.\n";
++ }
++ exit($fail);