Hi.

I'm sending the gcc-changelog relates scripts which should be added to contrib
folder. The patch contains:
- git_check_commit.py - checking script that verifies git message format
- git_update_version.py - a replacement of 
maintainer-scripts/update_version_git which
bumps DATESTAMP and generates ChangeLog entries (for now into ChangeLog.test 
files)
- git_commit.py, git_email.py and git_repository.py - helper classes

I also added a new git.config alias: 'gcc-verify' which can be used in the 
following
way:

$ git gcc-verify HEAD~2..HEAD -p -n
Checking 0e4009e9d523270e26856d2441c1be3d8119a477
OK
@@CL contrib
2020-05-13  Martin Liska  <mli...@suse.cz>

        * gcc-changelog/git_check_commit.py: New file.
        * gcc-changelog/git_commit.py: New file.
        * gcc-changelog/git_email.py: New file.
        * gcc-changelog/git_repository.py: New file.
        * gcc-changelog/git_update_version.py: New file.
        * gcc-git-customization.sh: Add gcc-verify alias.
@@CL
Checking 18edc195442291525e04f0fa4d5ef972155117da
OK
@@CL gcc
2020-05-13  Jakub Jelinek  <ja...@redhat.com>

        PR debug/95080
        * cfgrtl.c (purge_dead_edges): Skip over debug and note insns even
        if the last insn is a note.
@@CL gcc/testsuite
2020-05-13  Jakub Jelinek  <ja...@redhat.com>

        PR debug/95080
        * g++.dg/opt/pr95080.C: New test.
@@CL

Note the -n option which disables _strict mode_ (modification of both ChangeLog
and another files).

The second part is git hook that will reject all commits for release and master 
branches.
that violate ChangeLog format. Right now, strict mode is disabled in the hooks.

What's still missing to be done is format of Revert and Backport commits.
I suggest to use native 'git revert XYZ' and 'git cherry-pick -x XYZ'.
Doing that the commit messages will provide link to original commit and the 
script
can later append corresponding 'Backported ..' or 'Reverted' line.

Thoughts?
Martin
>From 0e4009e9d523270e26856d2441c1be3d8119a477 Mon Sep 17 00:00:00 2001
From: Martin Liska <mli...@suse.cz>
Date: Wed, 13 May 2020 12:22:39 +0200
Subject: [PATCH] Add gcc-changelog related scripts.

contrib/ChangeLog:

2020-05-13  Martin Liska  <mli...@suse.cz>

	* gcc-changelog/git_check_commit.py: New file.
	* gcc-changelog/git_commit.py: New file.
	* gcc-changelog/git_email.py: New file.
	* gcc-changelog/git_repository.py: New file.
	* gcc-changelog/git_update_version.py: New file.
	* gcc-git-customization.sh: Add gcc-verify alias.
---
 contrib/gcc-changelog/git_check_commit.py   |  49 ++
 contrib/gcc-changelog/git_commit.py         | 536 ++++++++++++++++++++
 contrib/gcc-changelog/git_email.py          |  92 ++++
 contrib/gcc-changelog/git_repository.py     |  60 +++
 contrib/gcc-changelog/git_update_version.py | 105 ++++
 contrib/gcc-git-customization.sh            |   2 +
 6 files changed, 844 insertions(+)
 create mode 100755 contrib/gcc-changelog/git_check_commit.py
 create mode 100755 contrib/gcc-changelog/git_commit.py
 create mode 100755 contrib/gcc-changelog/git_email.py
 create mode 100755 contrib/gcc-changelog/git_repository.py
 create mode 100755 contrib/gcc-changelog/git_update_version.py

diff --git a/contrib/gcc-changelog/git_check_commit.py b/contrib/gcc-changelog/git_check_commit.py
new file mode 100755
index 00000000000..b2d1d08a242
--- /dev/null
+++ b/contrib/gcc-changelog/git_check_commit.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+#
+# This file is part of GCC.
+#
+# GCC is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation; either version 3, or (at your option) any later
+# version.
+#
+# GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GCC; see the file COPYING3.  If not see
+# <http://www.gnu.org/licenses/>.  */
+
+import argparse
+
+from git_repository import parse_git_revisions
+
+parser = argparse.ArgumentParser(description='Check git ChangeLog format '
+                                 'of a commit')
+parser.add_argument('revisions',
+                    help='Git revisions (e.g. hash~5..hash or just hash)')
+parser.add_argument('-g', '--git-path', default='.',
+                    help='Path to git repository')
+parser.add_argument('-p', '--print-changelog', action='store_true',
+                    help='Print final changelog entires')
+parser.add_argument('-n', '--allow-non-strict-mode', action='store_true',
+                    help='Allow non-strict mode (change in both ChangeLog and '
+                    'other files.')
+args = parser.parse_args()
+
+retval = 0
+for git_commit in parse_git_revisions(args.git_path, args.revisions,
+                                      not args.allow_non_strict_mode):
+    print('Checking %s' % git_commit.hexsha)
+    if git_commit.success:
+        print('OK')
+        if args.print_changelog:
+            git_commit.print_output()
+    else:
+        for error in git_commit.errors:
+            print('ERR: %s' % error)
+        retval = 1
+
+exit(retval)
diff --git a/contrib/gcc-changelog/git_commit.py b/contrib/gcc-changelog/git_commit.py
new file mode 100755
index 00000000000..a434f949ee6
--- /dev/null
+++ b/contrib/gcc-changelog/git_commit.py
@@ -0,0 +1,536 @@
+#!/usr/bin/env python3
+#
+# This file is part of GCC.
+#
+# GCC is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation; either version 3, or (at your option) any later
+# version.
+#
+# GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GCC; see the file COPYING3.  If not see
+# <http://www.gnu.org/licenses/>.  */
+
+import os
+import re
+
+changelog_locations = set([
+    'config',
+    'contrib',
+    'contrib/header-tools',
+    'contrib/reghunt',
+    'contrib/regression',
+    'fixincludes',
+    'gcc/ada',
+    'gcc/analyzer',
+    'gcc/brig',
+    'gcc/c',
+    'gcc/c-family',
+    'gcc',
+    'gcc/cp',
+    'gcc/d',
+    'gcc/fortran',
+    'gcc/go',
+    'gcc/jit',
+    'gcc/lto',
+    'gcc/objc',
+    'gcc/objcp',
+    'gcc/po',
+    'gcc/testsuite',
+    'gnattools',
+    'gotools',
+    'include',
+    'intl',
+    'libada',
+    'libatomic',
+    'libbacktrace',
+    'libcc1',
+    'libcpp',
+    'libcpp/po',
+    'libdecnumber',
+    'libffi',
+    'libgcc',
+    'libgcc/config/avr/libf7',
+    'libgcc/config/libbid',
+    'libgfortran',
+    'libgomp',
+    'libhsail-rt',
+    'libiberty',
+    'libitm',
+    'libobjc',
+    'liboffloadmic',
+    'libphobos',
+    'libquadmath',
+    'libsanitizer',
+    'libssp',
+    'libstdc++-v3',
+    'libvtv',
+    'lto-plugin',
+    'maintainer-scripts',
+    'zlib'])
+
+bug_components = set([
+    'ada',
+    'analyzer',
+    'boehm-gc',
+    'bootstrap',
+    'c',
+    'c++',
+    'd',
+    'debug',
+    'demangler',
+    'driver',
+    'fastjar',
+    'fortran',
+    'gcov-profile',
+    'go',
+    'hsa',
+    'inline-asm',
+    'ipa',
+    'java',
+    'jit',
+    'libbacktrace',
+    'libf2c',
+    'libffi',
+    'libfortran',
+    'libgcc',
+    'libgcj',
+    'libgomp',
+    'libitm',
+    'libobjc',
+    'libquadmath',
+    'libstdc++',
+    'lto',
+    'middle-end',
+    'modula2',
+    'objc',
+    'objc++',
+    'other',
+    'pch',
+    'pending',
+    'plugins',
+    'preprocessor',
+    'regression',
+    'rtl-optimization',
+    'sanitizer',
+    'spam',
+    'target',
+    'testsuite',
+    'translation',
+    'tree-optimization',
+    'web'])
+
+ignored_prefixes = [
+    'gcc/d/dmd/',
+    'gcc/go/frontend/',
+    'libgo/',
+    'libphobos/libdruntime',
+    'libphobos/src/',
+    'libsanitizer/',
+    ]
+
+misc_files = [
+    'gcc/DATESTAMP',
+    'gcc/BASE-VER',
+    'gcc/DEV-PHASE'
+    ]
+
+author_line_regex = \
+        re.compile(r'^(?P<datetime>\d{4}-\d{2}-\d{2})\ {2}(?P<name>.*  <.*>)')
+additional_author_regex = re.compile(r'^\t(?P<spaces>\ *)?(?P<name>.*  <.*>)')
+changelog_regex = re.compile(r'^([a-z0-9+-/]*)/ChangeLog:?')
+pr_regex = re.compile(r'\tPR (?P<component>[a-z+-]+\/)?([0-9]+)$')
+star_prefix_regex = re.compile(r'\t\*(?P<spaces>\ *)(?P<content>.*)')
+
+LINE_LIMIT = 100
+TAB_WIDTH = 8
+CO_AUTHORED_BY_PREFIX = 'co-authored-by: '
+
+
+class Error:
+    def __init__(self, message, line=None):
+        self.message = message
+        self.line = line
+
+    def __repr__(self):
+        s = self.message
+        if self.line:
+            s += ':"%s"' % self.line
+        return s
+
+
+class ChangeLogEntry:
+    def __init__(self, folder, authors, prs):
+        self.folder = folder
+        # Python2 has not 'copy' function
+        self.author_lines = list(authors)
+        self.initial_prs = list(prs)
+        self.prs = list(prs)
+        self.lines = []
+
+    @property
+    def files(self):
+        files = []
+        for line in self.lines:
+            m = star_prefix_regex.match(line)
+            if m:
+                line = m.group('content')
+                if '(' in line:
+                    line = line[:line.index('(')]
+                if ':' in line:
+                    line = line[:line.index(':')]
+                for file in line.split(','):
+                    file = file.strip()
+                    if file:
+                        files.append(file)
+        return files
+
+    @property
+    def datetime(self):
+        for author in self.author_lines:
+            if author[1]:
+                return author[1]
+        return None
+
+    @property
+    def authors(self):
+        return [author_line[0] for author_line in self.author_lines]
+
+    @property
+    def is_empty(self):
+        return not self.lines and self.prs == self.initial_prs
+
+
+class GitCommit:
+    def __init__(self, hexsha, date, author, body, modified_files,
+                 strict=True):
+        self.hexsha = hexsha
+        self.lines = body
+        self.modified_files = modified_files
+        self.message = None
+        self.changes = None
+        self.changelog_entries = []
+        self.errors = []
+        self.date = date
+        self.author = author
+        self.top_level_authors = []
+        self.co_authors = []
+        self.top_level_prs = []
+
+        project_files = [f for f in self.modified_files
+                         if self.is_changelog_filename(f[0])
+                         or f[0] in misc_files]
+        if len(project_files) == len(self.modified_files):
+            # All modified files are only MISC files
+            return
+        elif project_files and strict:
+            self.errors.append(Error('ChangeLog, DATESTAMP, BASE-VER and '
+                                     'DEV-PHASE updates should be done '
+                                     'separately from normal commits'))
+            return
+
+        self.parse_lines()
+        if self.changes:
+            self.parse_changelog()
+            self.deduce_changelog_locations()
+            if not self.errors:
+                self.check_mentioned_files()
+                self.check_for_correct_changelog()
+
+    @property
+    def success(self):
+        return not self.errors
+
+    @property
+    def new_files(self):
+        return [x[0] for x in self.modified_files if x[1] == 'A']
+
+    @classmethod
+    def is_changelog_filename(cls, path):
+        return path.endswith('/ChangeLog') or path == 'ChangeLog'
+
+    @classmethod
+    def find_changelog_location(cls, name):
+        if name.startswith('\t'):
+            name = name[1:]
+        if name.endswith(':'):
+            name = name[:-1]
+        if name.endswith('/'):
+            name = name[:-1]
+        return name if name in changelog_locations else None
+
+    @classmethod
+    def format_git_author(cls, author):
+        assert '<' in author
+        return author.replace('<', ' <')
+
+    @classmethod
+    def parse_git_name_status(cls, string):
+        modified_files = []
+        for entry in string.split('\n'):
+            parts = entry.split('\t')
+            t = parts[0]
+            if t == 'A' or t == 'D' or t == 'M':
+                modified_files.append((parts[1], t))
+            elif t == 'R':
+                modified_files.append((parts[1], 'D'))
+                modified_files.append((parts[2], 'A'))
+        return modified_files
+
+    def parse_lines(self):
+        body = self.lines
+
+        for i, b in enumerate(body):
+            if not b:
+                continue
+            if (changelog_regex.match(b) or self.find_changelog_location(b)
+                    or star_prefix_regex.match(b) or pr_regex.match(b)
+                    or author_line_regex.match(b)):
+                self.changes = body[i:]
+                return
+        self.errors.append(Error('cannot find a ChangeLog location in '
+                                 'message'))
+
+    def parse_changelog(self):
+        last_entry = None
+        will_deduce = False
+        for line in self.changes:
+            if not line:
+                if last_entry and will_deduce:
+                    last_entry = None
+                continue
+            if line != line.rstrip():
+                self.errors.append(Error('trailing whitespace', line))
+            if len(line.replace('\t', ' ' * TAB_WIDTH)) > LINE_LIMIT:
+                self.errors.append(Error('line limit exceeds %d characters'
+                                         % LINE_LIMIT, line))
+            m = changelog_regex.match(line)
+            if m:
+                last_entry = ChangeLogEntry(m.group(1), self.top_level_authors,
+                                            self.top_level_prs)
+                self.changelog_entries.append(last_entry)
+            elif self.find_changelog_location(line):
+                last_entry = ChangeLogEntry(self.find_changelog_location(line),
+                                            self.top_level_authors,
+                                            self.top_level_prs)
+                self.changelog_entries.append(last_entry)
+            else:
+                author_tuple = None
+                pr_line = None
+                if author_line_regex.match(line):
+                    m = author_line_regex.match(line)
+                    author_tuple = (m.group('name'), m.group('datetime'))
+                elif additional_author_regex.match(line):
+                    m = additional_author_regex.match(line)
+                    if len(m.group('spaces')) != 4:
+                        msg = 'additional author must prepend with tab ' \
+                              'and 4 spaces'
+                        self.errors.append(Error(msg, line))
+                    else:
+                        author_tuple = (m.group('name'), None)
+                elif pr_regex.match(line):
+                    component = pr_regex.match(line).group('component')
+                    if not component:
+                        self.errors.append(Error('missing PR component', line))
+                        continue
+                    elif not component[:-1] in bug_components:
+                        self.errors.append(Error('invalid PR component', line))
+                        continue
+                    else:
+                        pr_line = line.lstrip()
+
+                if line.lower().startswith(CO_AUTHORED_BY_PREFIX):
+                    name = line[len(CO_AUTHORED_BY_PREFIX):]
+                    author = self.format_git_author(name)
+                    self.co_authors.append(author)
+                    continue
+
+                # ChangeLog name will be deduced later
+                if not last_entry:
+                    if author_tuple:
+                        self.top_level_authors.append(author_tuple)
+                        continue
+                    elif pr_line:
+                        # append to top_level_prs only when we haven't met
+                        # a ChangeLog entry
+                        if (pr_line not in self.top_level_prs
+                                and not self.changelog_entries):
+                            self.top_level_prs.append(pr_line)
+                        continue
+                    else:
+                        last_entry = ChangeLogEntry(None,
+                                                    self.top_level_authors,
+                                                    self.top_level_prs)
+                        self.changelog_entries.append(last_entry)
+                        will_deduce = True
+                elif author_tuple:
+                    last_entry.author_lines.append(author_tuple)
+                    continue
+
+                if not line.startswith('\t'):
+                    err = Error('line should start with a tab', line)
+                    self.errors.append(err)
+                elif pr_line:
+                    last_entry.prs.append(pr_line)
+                else:
+                    m = star_prefix_regex.match(line)
+                    if m:
+                        if len(m.group('spaces')) != 1:
+                            err = Error('one space should follow asterisk',
+                                        line)
+                            self.errors.append(err)
+                        else:
+                            last_entry.lines.append(line)
+                    else:
+                        if last_entry.is_empty:
+                            msg = 'first line should start with a tab, ' \
+                                  'asterisk and space'
+                            self.errors.append(Error(msg, line))
+                        else:
+                            last_entry.lines.append(line)
+
+    def get_file_changelog_location(self, changelog_file):
+        for file in self.modified_files:
+            if file[0] == changelog_file:
+                # root ChangeLog file
+                return ''
+            index = file[0].find('/' + changelog_file)
+            if index != -1:
+                return file[0][:index]
+        return None
+
+    def deduce_changelog_locations(self):
+        for entry in self.changelog_entries:
+            if not entry.folder:
+                changelog = None
+                for file in entry.files:
+                    location = self.get_file_changelog_location(file)
+                    if (location == ''
+                       or (location and location in changelog_locations)):
+                        if changelog and changelog != location:
+                            msg = 'could not deduce ChangeLog file, ' \
+                                  'not unique location'
+                            self.errors.append(Error(msg))
+                            return
+                        changelog = location
+                if changelog is not None:
+                    entry.folder = changelog
+                else:
+                    msg = 'could not deduce ChangeLog file'
+                    self.errors.append(Error(msg))
+
+    @classmethod
+    def in_ignored_location(cls, path):
+        for ignored in ignored_prefixes:
+            if path.startswith(ignored):
+                return True
+        return False
+
+    @classmethod
+    def get_changelog_by_path(cls, path):
+        components = path.split('/')
+        while components:
+            if '/'.join(components) in changelog_locations:
+                break
+            components = components[:-1]
+        return '/'.join(components)
+
+    def check_mentioned_files(self):
+        folder_count = len([x.folder for x in self.changelog_entries])
+        assert folder_count == len(self.changelog_entries)
+
+        mentioned_files = set()
+        for entry in self.changelog_entries:
+            if not entry.files:
+                msg = 'ChangeLog must contain a file entry'
+                self.errors.append(Error(msg, entry.folder))
+            assert not entry.folder.endswith('/')
+            for file in entry.files:
+                if not self.is_changelog_filename(file):
+                    mentioned_files.add(os.path.join(entry.folder, file))
+
+        cand = [x[0] for x in self.modified_files
+                if not self.is_changelog_filename(x[0])]
+        changed_files = set(cand)
+        for file in sorted(mentioned_files - changed_files):
+            self.errors.append(Error('file not changed in a patch', file))
+        for file in sorted(changed_files - mentioned_files):
+            if not self.in_ignored_location(file):
+                if file in self.new_files:
+                    changelog_location = self.get_changelog_by_path(file)
+                    # Python2: we cannot use next(filter(...))
+                    entries = filter(lambda x: x.folder == changelog_location,
+                                     self.changelog_entries)
+                    entries = list(entries)
+                    entry = entries[0] if entries else None
+                    if not entry:
+                        prs = self.top_level_prs
+                        if not prs:
+                            # if all ChangeLog entries have identical PRs
+                            # then use them
+                            prs = self.changelog_entries[0].prs
+                            for entry in self.changelog_entries:
+                                if entry.prs != prs:
+                                    prs = []
+                                    break
+                        entry = ChangeLogEntry(changelog_location,
+                                               self.top_level_authors,
+                                               prs)
+                        self.changelog_entries.append(entry)
+                    # strip prefix of the file
+                    assert file.startswith(entry.folder)
+                    file = file[len(entry.folder):].lstrip('/')
+                    entry.lines.append('\t* %s: New file.' % file)
+                else:
+                    msg = 'changed file not mentioned in a ChangeLog'
+                    self.errors.append(Error(msg, file))
+
+    def check_for_correct_changelog(self):
+        for entry in self.changelog_entries:
+            for file in entry.files:
+                full_path = os.path.join(entry.folder, file)
+                changelog_location = self.get_changelog_by_path(full_path)
+                if changelog_location != entry.folder:
+                    msg = 'wrong ChangeLog location "%s", should be "%s"'
+                    err = Error(msg % (entry.folder, changelog_location), file)
+                    self.errors.append(err)
+
+    def to_changelog_entries(self):
+        for entry in self.changelog_entries:
+            output = ''
+            timestamp = entry.datetime
+            if not timestamp:
+                timestamp = self.date.strftime('%Y-%m-%d')
+            authors = entry.authors if entry.authors else [self.author]
+            # add Co-Authored-By authors to all ChangeLog entries
+            for author in self.co_authors:
+                if author not in authors:
+                    authors.append(author)
+
+            for i, author in enumerate(authors):
+                if i == 0:
+                    output += '%s  %s\n' % (timestamp, author)
+                else:
+                    output += '\t    %s\n' % author
+            output += '\n'
+            for pr in entry.prs:
+                output += '\t%s\n' % pr
+            for line in entry.lines:
+                output += line + '\n'
+            yield (entry.folder, output.rstrip())
+
+    def print_output(self):
+        for entry, output in self.to_changelog_entries():
+            print('@@CL %s' % entry)
+            print(output)
+        print('@@CL')
+
+    def print_errors(self):
+        print('Errors:')
+        for error in self.errors:
+            print(error)
diff --git a/contrib/gcc-changelog/git_email.py b/contrib/gcc-changelog/git_email.py
new file mode 100755
index 00000000000..e1d6b70e80c
--- /dev/null
+++ b/contrib/gcc-changelog/git_email.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+#
+# This file is part of GCC.
+#
+# GCC is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation; either version 3, or (at your option) any later
+# version.
+#
+# GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GCC; see the file COPYING3.  If not see
+# <http://www.gnu.org/licenses/>.  */
+
+import os
+import sys
+from itertools import takewhile
+
+from dateutil.parser import parse
+
+from git_commit import GitCommit
+
+from unidiff import PatchSet
+
+DATE_PREFIX = 'Date: '
+FROM_PREFIX = 'From: '
+
+
+class GitEmail(GitCommit):
+    def __init__(self, filename, strict=False):
+        self.filename = filename
+        diff = PatchSet.from_filename(filename)
+        date = None
+        author = None
+
+        lines = open(self.filename).read().splitlines()
+        lines = list(takewhile(lambda line: line != '---', lines))
+        for line in lines:
+            if line.startswith(DATE_PREFIX):
+                date = parse(line[len(DATE_PREFIX):])
+            elif line.startswith(FROM_PREFIX):
+                author = GitCommit.format_git_author(line[len(FROM_PREFIX):])
+        header = list(takewhile(lambda line: line != '', lines))
+        body = lines[len(header) + 1:]
+
+        modified_files = []
+        for f in diff:
+            if f.is_added_file:
+                t = 'A'
+            elif f.is_removed_file:
+                t = 'D'
+            else:
+                t = 'M'
+            modified_files.append((f.path, t))
+        super().__init__(None, date, author, body, modified_files,
+                         strict=strict)
+
+
+if __name__ == '__main__':
+    if len(sys.argv) == 1:
+        allfiles = []
+        for root, _dirs, files in os.walk('patches'):
+            for f in files:
+                full = os.path.join(root, f)
+                allfiles.append(full)
+
+        success = 0
+        for full in sorted(allfiles):
+            email = GitEmail(full, False)
+            print(email.filename)
+            if email.success:
+                success += 1
+                print('  OK')
+            else:
+                for error in email.errors:
+                    print('  ERR: %s' % error)
+
+        print()
+        print('Successfully parsed: %d/%d' % (success, len(allfiles)))
+    else:
+        email = GitEmail(sys.argv[1], False)
+        if email.success:
+            print('OK')
+            email.print_output()
+        else:
+            if not email.lines:
+                print('Error: patch contains no parsed lines', file=sys.stderr)
+            email.print_errors()
diff --git a/contrib/gcc-changelog/git_repository.py b/contrib/gcc-changelog/git_repository.py
new file mode 100755
index 00000000000..0473fe73fba
--- /dev/null
+++ b/contrib/gcc-changelog/git_repository.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python3
+#
+# This file is part of GCC.
+#
+# GCC is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation; either version 3, or (at your option) any later
+# version.
+#
+# GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GCC; see the file COPYING3.  If not see
+# <http://www.gnu.org/licenses/>.  */
+
+from datetime import datetime
+
+try:
+    from git import Repo
+except ImportError:
+    print('Cannot import GitPython package, please install the package:')
+    print('  Fedora, openSUSE: python3-GitPython')
+    print('  Debian, Ubuntu: python3-git')
+    exit(1)
+
+from git_commit import GitCommit
+
+
+def parse_git_revisions(repo_path, revisions, strict=False):
+    repo = Repo(repo_path)
+
+    parsed_commits = []
+    if '..' in revisions:
+        commits = list(repo.iter_commits(revisions))
+    else:
+        commits = [repo.commit(revisions)]
+
+    for commit in commits:
+        diff = repo.commit(commit.hexsha + '~').diff(commit.hexsha)
+
+        modified_files = []
+        for file in diff:
+            if file.new_file:
+                t = 'A'
+            elif file.deleted_file:
+                t = 'D'
+            else:
+                t = 'M'
+            modified_files.append((file.b_path, t))
+
+        date = datetime.utcfromtimestamp(commit.committed_date)
+        author = '%s  <%s>' % (commit.author.name, commit.author.email)
+        git_commit = GitCommit(commit.hexsha, date, author,
+                               commit.message.split('\n'), modified_files,
+                               strict=strict)
+        parsed_commits.append(git_commit)
+    return parsed_commits
diff --git a/contrib/gcc-changelog/git_update_version.py b/contrib/gcc-changelog/git_update_version.py
new file mode 100755
index 00000000000..c66b4d68e68
--- /dev/null
+++ b/contrib/gcc-changelog/git_update_version.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python3
+#
+# This file is part of GCC.
+#
+# GCC is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation; either version 3, or (at your option) any later
+# version.
+#
+# GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GCC; see the file COPYING3.  If not see
+# <http://www.gnu.org/licenses/>.  */
+
+import argparse
+import datetime
+import os
+
+from git import Repo
+
+from git_repository import parse_git_revisions
+
+# TODO: remove sparta suffix
+current_timestamp = datetime.datetime.now().strftime('%Y%m%d sparta\n')
+
+
+def read_timestamp(path):
+    return open(path).read()
+
+
+def prepend_to_changelog_files(repo, folder, git_commit):
+    if not git_commit.success:
+        for error in git_commit.errors:
+            print(error)
+        # TODO: add error message
+        return
+    for entry, output in git_commit.to_changelog_entries():
+        # TODO
+        full_path = os.path.join(folder, entry, 'ChangeLog.test')
+        print('writting to %s' % full_path)
+        if os.path.exists(full_path):
+            content = open(full_path).read()
+        else:
+            content = ''
+        with open(full_path, 'w+') as f:
+            f.write(output)
+            if content:
+                f.write('\n\n')
+                f.write(content)
+        repo.git.add(full_path)
+
+
+active_refs = ['master', 'releases/gcc-8', 'releases/gcc-9', 'releases/gcc-10']
+
+parser = argparse.ArgumentParser(description='Update DATESTAMP and generate '
+                                 'ChangeLog entries')
+parser.add_argument('-g', '--git-path', default='.',
+                    help='Path to git repository')
+args = parser.parse_args()
+
+repo = Repo(args.git_path)
+origin = repo.remotes['origin']
+
+for ref in origin.refs:
+    assert ref.name.startswith('origin/')
+    name = ref.name[len('origin/'):]
+    if name in active_refs:
+        if name in repo.branches:
+            branch = repo.branches[name]
+        else:
+            branch = repo.create_head(name, ref).set_tracking_branch(ref)
+        origin.pull(rebase=True)
+        branch.checkout()
+        print('=== Working on: %s ===' % branch)
+        assert not repo.index.diff(None)
+        commit = branch.commit
+        commit_count = 1
+        while commit:
+            if (commit.author.email == 'gccad...@gcc.gnu.org'
+                    and commit.message.strip() == 'Daily bump.'):
+                break
+            commit = commit.parents[0]
+            commit_count += 1
+
+        print('%d revisions since last Daily bump' % commit_count)
+        datestamp_path = os.path.join(args.git_path, 'gcc/DATESTAMP')
+        if read_timestamp(datestamp_path) != current_timestamp:
+            print('DATESTAMP will be changed:')
+            # TODO: set strict=True after testing period
+            commits = parse_git_revisions(args.git_path, '%s..HEAD'
+                                          % commit.hexsha, strict=False)
+            for git_commit in commits:
+                prepend_to_changelog_files(repo, args.git_path, git_commit)
+            # update timestamp
+            with open(datestamp_path, 'w+') as f:
+                f.write(current_timestamp)
+            repo.git.add(datestamp_path)
+            repo.index.commit('Daily bump.')
+            # TODO: push the repository
+        else:
+            print('DATESTAMP unchanged')
diff --git a/contrib/gcc-git-customization.sh b/contrib/gcc-git-customization.sh
index a932bf8c06a..77c7d0ff4fb 100755
--- a/contrib/gcc-git-customization.sh
+++ b/contrib/gcc-git-customization.sh
@@ -25,6 +25,8 @@ git config alias.svn-rev '!f() { rev=$1; shift; git log --all --grep="^From-SVN:
 git config alias.gcc-descr \!"f() { if test \${1:-no} = --full; then c=\${2:-master}; r=\$(git describe --all --abbrev=40 --match 'basepoints/gcc-[0-9]*' \$c | sed -n 's,^\\(tags/\\)\\?basepoints/gcc-,r,p'); expr match \${r:-no} '^r[0-9]\\+\$' >/dev/null && r=\${r}-0-g\$(git rev-parse \${2:-master}); else c=\${1:-master}; r=\$(git describe --all --match 'basepoints/gcc-[0-9]*' \$c | sed -n 's,^\\(tags/\\)\\?basepoints/gcc-\\([0-9]\\+\\)-\\([0-9]\\+\\)-g[0-9a-f]*\$,r\\2-\\3,p;s,^\\(tags/\\)\\?basepoints/gcc-\\([0-9]\\+\\)\$,r\\2-0,p'); fi; if test -n \$r; then o=\$(git config --get gcc-config.upstream); rr=\$(echo \$r | sed -n 's,^r\\([0-9]\\+\\)-[0-9]\\+\\(-g[0-9a-f]\\+\\)\\?\$,\\1,p'); if git rev-parse --verify --quiet \${o:-origin}/releases/gcc-\$rr >/dev/null; then m=releases/gcc-\$rr; else m=master; fi; git merge-base --is-ancestor \$c \${o:-origin}/\$m && \echo \${r}; fi; }; f"
 git config alias.gcc-undescr \!"f() { o=\$(git config --get gcc-config.upstream); r=\$(echo \$1 | sed -n 's,^r\\([0-9]\\+\\)-[0-9]\\+\$,\\1,p'); n=\$(echo \$1 | sed -n 's,^r[0-9]\\+-\\([0-9]\\+\\)\$,\\1,p'); test -z \$r && echo Invalid id \$1 && exit 1; h=\$(git rev-parse --verify --quiet \${o:-origin}/releases/gcc-\$r); test -z \$h && h=\$(git rev-parse --verify --quiet \${o:-origin}/master); p=\$(git describe --all --match 'basepoints/gcc-'\$r \$h | sed -n 's,^\\(tags/\\)\\?basepoints/gcc-[0-9]\\+-\\([0-9]\\+\\)-g[0-9a-f]*\$,\\2,p;s,^\\(tags/\\)\\?basepoints/gcc-[0-9]\\+\$,0,p'); git rev-parse --verify \$h~\$(expr \$p - \$n); }; f"
 
+git config alias.gcc-verify '!f() { ./contrib/gcc-changelog/git_check_commit.py $@; } ; f'
+
 # Make diff on MD files use "(define" as a function marker.
 # Use this in conjunction with a .gitattributes file containing
 # *.md    diff=md
-- 
2.26.2

>From 6367837722c62930c9e00e9aa82073d24813a4c1 Mon Sep 17 00:00:00 2001
From: Martin Liska <mli...@suse.cz>
Date: Mon, 11 May 2020 15:24:16 +0200
Subject: [PATCH] Add gcc-changelog hook for commit message format.

---
 hooks/git_commit.py        | 535 +++++++++++++++++++++++++++++++++++++
 hooks/pre_commit_checks.py |  29 +-
 hooks/updates/__init__.py  |   2 +-
 3 files changed, 563 insertions(+), 3 deletions(-)
 create mode 100755 hooks/git_commit.py

diff --git a/hooks/git_commit.py b/hooks/git_commit.py
new file mode 100755
index 0000000..5214cc3
--- /dev/null
+++ b/hooks/git_commit.py
@@ -0,0 +1,535 @@
+#!/usr/bin/env python3
+#
+# This file is part of GCC.
+#
+# GCC is free software; you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation; either version 3, or (at your option) any later
+# version.
+#
+# GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+# for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with GCC; see the file COPYING3.  If not see
+# <http://www.gnu.org/licenses/>.  */
+
+import os
+import re
+
+changelog_locations = set([
+    'config',
+    'contrib',
+    'contrib/header-tools',
+    'contrib/reghunt',
+    'contrib/regression',
+    'fixincludes',
+    'gcc/ada',
+    'gcc/analyzer',
+    'gcc/brig',
+    'gcc/c',
+    'gcc/c-family',
+    'gcc',
+    'gcc/cp',
+    'gcc/d',
+    'gcc/fortran',
+    'gcc/go',
+    'gcc/jit',
+    'gcc/lto',
+    'gcc/objc',
+    'gcc/objcp',
+    'gcc/po',
+    'gcc/testsuite',
+    'gnattools',
+    'gotools',
+    'include',
+    'intl',
+    'libada',
+    'libatomic',
+    'libbacktrace',
+    'libcc1',
+    'libcpp',
+    'libcpp/po',
+    'libdecnumber',
+    'libffi',
+    'libgcc',
+    'libgcc/config/avr/libf7',
+    'libgcc/config/libbid',
+    'libgfortran',
+    'libgomp',
+    'libhsail-rt',
+    'libiberty',
+    'libitm',
+    'libobjc',
+    'liboffloadmic',
+    'libphobos',
+    'libquadmath',
+    'libsanitizer',
+    'libssp',
+    'libstdc++-v3',
+    'libvtv',
+    'lto-plugin',
+    'maintainer-scripts',
+    'zlib'])
+
+bug_components = set([
+    'ada',
+    'analyzer',
+    'boehm-gc',
+    'bootstrap',
+    'c',
+    'c++',
+    'd',
+    'debug',
+    'demangler',
+    'driver',
+    'fastjar',
+    'fortran',
+    'gcov-profile',
+    'go',
+    'hsa',
+    'inline-asm',
+    'ipa',
+    'java',
+    'jit',
+    'libbacktrace',
+    'libf2c',
+    'libffi',
+    'libfortran',
+    'libgcc',
+    'libgcj',
+    'libgomp',
+    'libitm',
+    'libobjc',
+    'libquadmath',
+    'libstdc++',
+    'lto',
+    'middle-end',
+    'modula2',
+    'objc',
+    'objc++',
+    'other',
+    'pch',
+    'pending',
+    'plugins',
+    'preprocessor',
+    'regression',
+    'rtl-optimization',
+    'sanitizer',
+    'spam',
+    'target',
+    'testsuite',
+    'translation',
+    'tree-optimization',
+    'web'])
+
+ignored_prefixes = [
+    'gcc/d/dmd/',
+    'gcc/go/frontend/',
+    'libgo/',
+    'libphobos/libdruntime',
+    'libphobos/src/',
+    'libsanitizer/',
+    ]
+
+misc_files = [
+    'gcc/DATESTAMP',
+    'gcc/BASE-VER',
+    'gcc/DEV-PHASE'
+    ]
+
+author_line_regex = \
+        re.compile(r'^(?P<datetime>\d{4}-\d{2}-\d{2})\ {2}(?P<name>.*  <.*>)')
+additional_author_regex = re.compile(r'^\t(?P<spaces>\ *)?(?P<name>.*  <.*>)')
+changelog_regex = re.compile(r'^([a-z0-9+-/]*)/ChangeLog:?')
+pr_regex = re.compile(r'\tPR (?P<component>[a-z+-]+\/)?([0-9]+)$')
+star_prefix_regex = re.compile(r'\t\*(?P<spaces>\ *)(?P<content>.*)')
+
+LINE_LIMIT = 100
+TAB_WIDTH = 8
+CO_AUTHORED_BY_PREFIX = 'co-authored-by: '
+
+
+class Error:
+    def __init__(self, message, line=None):
+        self.message = message
+        self.line = line
+
+    def __repr__(self):
+        s = self.message
+        if self.line:
+            s += ':"%s"' % self.line
+        return s
+
+
+class ChangeLogEntry:
+    def __init__(self, folder, authors, prs):
+        self.folder = folder
+        # Python2 has not 'copy' function
+        self.author_lines = list(authors)
+        self.initial_prs = list(prs)
+        self.prs = list(prs)
+        self.lines = []
+
+    @property
+    def files(self):
+        files = []
+        for line in self.lines:
+            m = star_prefix_regex.match(line)
+            if m:
+                line = m.group('content')
+                if '(' in line:
+                    line = line[:line.index('(')]
+                if ':' in line:
+                    line = line[:line.index(':')]
+                for file in line.split(','):
+                    file = file.strip()
+                    if file:
+                        files.append(file)
+        return files
+
+    @property
+    def datetime(self):
+        for author in self.author_lines:
+            if author[1]:
+                return author[1]
+        return None
+
+    @property
+    def authors(self):
+        return [author_line[0] for author_line in self.author_lines]
+
+    @property
+    def is_empty(self):
+        return not self.lines and self.prs == self.initial_prs
+
+
+class GitCommit:
+    def __init__(self, hexsha, date, author, body, modified_files,
+                 strict=True):
+        self.hexsha = hexsha
+        self.lines = body
+        self.modified_files = modified_files
+        self.message = None
+        self.changes = None
+        self.changelog_entries = []
+        self.errors = []
+        self.date = date
+        self.author = author
+        self.top_level_authors = []
+        self.co_authors = []
+        self.top_level_prs = []
+
+        project_files = [f for f in self.modified_files
+                         if self.is_changelog_filename(f[0])
+                         or f[0] in misc_files]
+        if len(project_files) == len(self.modified_files):
+            # All modified files are only MISC files
+            return
+        elif project_files and strict:
+            self.errors.append(Error('ChangeLog, DATESTAMP, BASE-VER and '
+                                     'DEV-PHASE updates should be done '
+                                     'separately from normal commits'))
+            return
+
+        self.parse_lines()
+        if self.changes:
+            self.parse_changelog()
+            self.deduce_changelog_locations()
+            if not self.errors:
+                self.check_mentioned_files()
+                self.check_for_correct_changelog()
+
+    @property
+    def success(self):
+        return not self.errors
+
+    @property
+    def new_files(self):
+        return [x[0] for x in self.modified_files if x[1] == 'A']
+
+    @classmethod
+    def is_changelog_filename(cls, path):
+        return path.endswith('/ChangeLog') or path == 'ChangeLog'
+
+    @classmethod
+    def find_changelog_location(cls, name):
+        if name.startswith('\t'):
+            name = name[1:]
+        if name.endswith(':'):
+            name = name[:-1]
+        if name.endswith('/'):
+            name = name[:-1]
+        return name if name in changelog_locations else None
+
+    @classmethod
+    def format_git_author(cls, author):
+        assert '<' in author
+        return author.replace('<', ' <')
+
+    @classmethod
+    def parse_git_name_status(cls, string):
+        modified_files = []
+        for entry in string.split('\n'):
+            parts = entry.split('\t')
+            t = parts[0]
+            if t == 'A' or t == 'D' or t == 'M':
+                modified_files.append((parts[1], t))
+            elif t == 'R':
+                modified_files.append((parts[1], 'D'))
+                modified_files.append((parts[2], 'A'))
+        return modified_files
+
+    def parse_lines(self):
+        body = self.lines
+
+        for i, b in enumerate(body):
+            if not b:
+                continue
+            if (changelog_regex.match(b) or self.find_changelog_location(b)
+                    or star_prefix_regex.match(b) or pr_regex.match(b)
+                    or author_line_regex.match(b)):
+                self.changes = body[i:]
+                return
+        self.errors.append(Error('cannot find a ChangeLog location in '
+                                 'message'))
+
+    def parse_changelog(self):
+        last_entry = None
+        will_deduce = False
+        for line in self.changes:
+            if not line:
+                if last_entry and will_deduce:
+                    last_entry = None
+                continue
+            if line != line.rstrip():
+                self.errors.append(Error('trailing whitespace', line))
+            if len(line.replace('\t', ' ' * TAB_WIDTH)) > LINE_LIMIT:
+                self.errors.append(Error('line limit exceeds %d characters'
+                                         % LINE_LIMIT, line))
+            m = changelog_regex.match(line)
+            if m:
+                last_entry = ChangeLogEntry(m.group(1), self.top_level_authors,
+                                            self.top_level_prs)
+                self.changelog_entries.append(last_entry)
+            elif self.find_changelog_location(line):
+                last_entry = ChangeLogEntry(self.find_changelog_location(line),
+                                            self.top_level_authors,
+                                            self.top_level_prs)
+                self.changelog_entries.append(last_entry)
+            else:
+                author_tuple = None
+                pr_line = None
+                if author_line_regex.match(line):
+                    m = author_line_regex.match(line)
+                    author_tuple = (m.group('name'), m.group('datetime'))
+                elif additional_author_regex.match(line):
+                    m = additional_author_regex.match(line)
+                    if len(m.group('spaces')) != 4:
+                        msg = 'additional author must prepend with tab ' \
+                              'and 4 spaces'
+                        self.errors.append(Error(msg, line))
+                    else:
+                        author_tuple = (m.group('name'), None)
+                elif pr_regex.match(line):
+                    component = pr_regex.match(line).group('component')
+                    if not component:
+                        self.errors.append(Error('missing PR component', line))
+                        continue
+                    elif not component[:-1] in bug_components:
+                        self.errors.append(Error('invalid PR component', line))
+                        continue
+                    else:
+                        pr_line = line.lstrip()
+
+                if line.lower().startswith(CO_AUTHORED_BY_PREFIX):
+                    name = line[len(CO_AUTHORED_BY_PREFIX):]
+                    author = self.format_git_author(name)
+                    self.co_authors.append(author)
+                    continue
+
+                # ChangeLog name will be deduced later
+                if not last_entry:
+                    if author_tuple:
+                        self.top_level_authors.append(author_tuple)
+                        continue
+                    elif pr_line:
+                        # append to top_level_prs only when we haven't met
+                        # a ChangeLog entry
+                        if (pr_line not in self.top_level_prs
+                                and not self.changelog_entries):
+                            self.top_level_prs.append(pr_line)
+                        continue
+                    else:
+                        last_entry = ChangeLogEntry(None,
+                                                    self.top_level_authors,
+                                                    self.top_level_prs)
+                        self.changelog_entries.append(last_entry)
+                        will_deduce = True
+                elif author_tuple:
+                    last_entry.author_lines.append(author_tuple)
+                    continue
+
+                if not line.startswith('\t'):
+                    err = Error('line should start with a tab', line)
+                    self.errors.append(err)
+                elif pr_line:
+                    last_entry.prs.append(pr_line)
+                else:
+                    m = star_prefix_regex.match(line)
+                    if m:
+                        if len(m.group('spaces')) != 1:
+                            err = Error('one space should follow asterisk',
+                                        line)
+                            self.errors.append(err)
+                        else:
+                            last_entry.lines.append(line)
+                    else:
+                        if last_entry.is_empty:
+                            msg = 'first line should start with a tab, ' \
+                                  'asterisk and space'
+                            self.errors.append(Error(msg, line))
+                        else:
+                            last_entry.lines.append(line)
+
+    def get_file_changelog_location(self, changelog_file):
+        for file in self.modified_files:
+            if file[0] == changelog_file:
+                # root ChangeLog file
+                return ''
+            index = file[0].find('/' + changelog_file)
+            if index != -1:
+                return file[0][:index]
+        return None
+
+    def deduce_changelog_locations(self):
+        for entry in self.changelog_entries:
+            if not entry.folder:
+                changelog = None
+                for file in entry.files:
+                    location = self.get_file_changelog_location(file)
+                    if (location == ''
+                       or (location and location in changelog_locations)):
+                        if changelog and changelog != location:
+                            msg = 'could not deduce ChangeLog file, ' \
+                                  'not unique location'
+                            self.errors.append(Error(msg))
+                            return
+                        changelog = location
+                if changelog is not None:
+                    entry.folder = changelog
+                else:
+                    msg = 'could not deduce ChangeLog file'
+                    self.errors.append(Error(msg))
+
+    @classmethod
+    def in_ignored_location(cls, path):
+        for ignored in ignored_prefixes:
+            if path.startswith(ignored):
+                return True
+        return False
+
+    @classmethod
+    def get_changelog_by_path(cls, path):
+        components = path.split('/')
+        while components:
+            if '/'.join(components) in changelog_locations:
+                break
+            components = components[:-1]
+        return '/'.join(components)
+
+    def check_mentioned_files(self):
+        folder_count = len([x.folder for x in self.changelog_entries])
+        assert folder_count == len(self.changelog_entries)
+
+        mentioned_files = set()
+        for entry in self.changelog_entries:
+            if not entry.files:
+                msg = 'ChangeLog must contain a file entry'
+                self.errors.append(Error(msg, entry.folder))
+            assert not entry.folder.endswith('/')
+            for file in entry.files:
+                if not self.is_changelog_filename(file):
+                    mentioned_files.add(os.path.join(entry.folder, file))
+
+        cand = [x[0] for x in self.modified_files
+                if not self.is_changelog_filename(x[0])]
+        changed_files = set(cand)
+        for file in sorted(mentioned_files - changed_files):
+            self.errors.append(Error('file not changed in a patch', file))
+        for file in sorted(changed_files - mentioned_files):
+            if not self.in_ignored_location(file):
+                if file in self.new_files:
+                    changelog_location = self.get_changelog_by_path(file)
+                    # Python2: we cannot use next(filter(...))
+                    entries = filter(lambda x: x.folder == changelog_location,
+                                     self.changelog_entries)
+                    entries = list(entries)
+                    entry = entries[0] if entries else None
+                    if not entry:
+                        prs = self.top_level_prs
+                        if not prs:
+                            # if all ChangeLog entries have identical PRs
+                            # then use them
+                            prs = self.changelog_entries[0].prs
+                            for entry in self.changelog_entries:
+                                if entry.prs != prs:
+                                    prs = []
+                                    break
+                        entry = ChangeLogEntry(changelog_location,
+                                               self.top_level_authors,
+                                               prs)
+                        self.changelog_entries.append(entry)
+                    # strip prefix of the file
+                    assert file.startswith(entry.folder)
+                    file = file[len(entry.folder):].lstrip('/')
+                    entry.lines.append('\t* %s: New file.' % file)
+                else:
+                    msg = 'changed file not mentioned in a ChangeLog'
+                    self.errors.append(Error(msg, file))
+
+    def check_for_correct_changelog(self):
+        for entry in self.changelog_entries:
+            for file in entry.files:
+                full_path = os.path.join(entry.folder, file)
+                changelog_location = self.get_changelog_by_path(full_path)
+                if changelog_location != entry.folder:
+                    msg = 'wrong ChangeLog location "%s", should be "%s"'
+                    err = Error(msg % (entry.folder, changelog_location), file)
+                    self.errors.append(err)
+
+    def to_changelog_entries(self):
+        for entry in self.changelog_entries:
+            output = ''
+            timestamp = entry.datetime
+            if not timestamp:
+                timestamp = self.date.strftime('%Y-%m-%d')
+            authors = entry.authors if entry.authors else [self.author]
+            # add Co-Authored-By authors to all ChangeLog entries
+            for author in self.co_authors:
+                if author not in authors:
+                    authors.append(author)
+
+            for i, author in enumerate(authors):
+                if i == 0:
+                    output += '%s  %s\n' % (timestamp, author)
+                else:
+                    output += '\t    %s\n' % author
+            output += '\n'
+            for pr in entry.prs:
+                output += '\t%s\n' % pr
+            for line in entry.lines:
+                output += line + '\n'
+            yield (entry.folder, output.rstrip())
+
+    def print_output(self):
+        for entry, output in self.to_changelog_entries():
+            print('------ %s/ChangeLog ------ ' % entry)
+            print(output)
+
+    def print_errors(self):
+        print('Errors:')
+        for error in self.errors:
+            print(error)
diff --git a/hooks/pre_commit_checks.py b/hooks/pre_commit_checks.py
index b3546ba..dbdc269 100644
--- a/hooks/pre_commit_checks.py
+++ b/hooks/pre_commit_checks.py
@@ -3,6 +3,9 @@ from pipes import quote
 import re
 from subprocess import Popen, PIPE, STDOUT
 
+from git_commit import GitCommit
+from datetime import datetime
+
 from config import git_config
 from errors import InvalidUpdate
 from git import git, diff_tree, file_exists
@@ -326,8 +329,27 @@ def check_missing_ticket_number(rev, raw_rh):
         'Subject: %s' % raw_rh[0],
         ])
 
+def verify_changelog_format(rev, raw_body):
+    author = git.show(rev, format='%ae  <%aN>', no_patch=True)
+    committed_date = git.show(rev, format='%ct', no_patch=True)
+    changed_files = git.diff('%s~..%s' % (rev, rev), name_status=True)
+
+    date = datetime.utcfromtimestamp(int(committed_date))
+    # TODO: enable strict mode
+    git_commit = GitCommit(date, author, raw_body,
+                           GitCommit.parse_git_name_status(changed_files),
+                           strict=False)
 
-def check_revision_history(rev):
+    if git_commit.success:
+        # OK
+        pass
+    else:
+        message = 'ChangeLog format failed:\n'
+        for error in git_commit.errors:
+            message += 'ERR: %s\n' % error
+        raise InvalidUpdate(message)
+
+def check_revision_history(rev, project_name, short_ref_name):
     """Apply pre-commit checks to the commit's revision history.
 
     Raise InvalidUpdate if one or more style violation are detected.
@@ -370,7 +392,10 @@ def check_revision_history(rev):
                 'from SVN, not in new git commits.  If this is a cherry-pick '
                 'of a commit done in SVN, remove the From-SVN: line from '
                 'the commit message before pushing.')
-
+    if (len(raw_body) >= 1
+        and (short_ref_name == 'master'
+             or short_ref_name.startswith('releases/gcc-'))):
+        verify_changelog_format(rev, raw_body)
 
 def check_filename_collisions(rev):
     """raise InvalidUpdate if the name of two files only differ in casing.
diff --git a/hooks/updates/__init__.py b/hooks/updates/__init__.py
index 25bfb18..e47145f 100644
--- a/hooks/updates/__init__.py
+++ b/hooks/updates/__init__.py
@@ -263,7 +263,7 @@ class AbstractUpdate(object):
         if do_rh_style_checks:
             for commit in added:
                 if not commit.pre_existing_p:
-                    check_revision_history(commit.rev)
+                    check_revision_history(commit.rev, self.email_info.project_name, self.short_ref_name)
 
         reject_merge_commits = (
             self.search_config_option_list('hooks.reject-merge-commits')
-- 
2.26.2

Reply via email to