4d003ccf496a433108f2b345cc8bfb61fbeda0c5
3 # This file is part of GCC.
5 # GCC is free software; you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation; either version 3, or (at your option) any later
10 # GCC is distributed in the hope that it will be useful, but WITHOUT ANY
11 # WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
15 # You should have received a copy of the GNU General Public License
16 # along with GCC; see the file COPYING3. If not see
17 # <http://www.gnu.org/licenses/>. */
22 changelog_locations
= set([
25 'contrib/header-tools',
58 'libgcc/config/avr/libf7',
59 'libgcc/config/libbid',
77 bug_components
= set([
130 'gcc/go/gofrontend/',
131 'gcc/testsuite/gdc.test/',
132 'gcc/testsuite/go.test/test/',
134 'libphobos/libdruntime/',
139 wildcard_prefixes
= [
141 'libstdc++-v3/doc/html/'
150 author_line_regex
= \
151 re
.compile(r
'^(?P<datetime>\d{4}-\d{2}-\d{2})\ {2}(?P<name>.* <.*>)')
152 additional_author_regex
= re
.compile(r
'^\t(?P<spaces>\ *)?(?P<name>.* <.*>)')
153 changelog_regex
= re
.compile(r
'^(?:[fF]or +)?([a-z0-9+-/]*)ChangeLog:?')
154 pr_regex
= re
.compile(r
'\tPR (?P<component>[a-z+-]+\/)?([0-9]+)$')
155 dr_regex
= re
.compile(r
'\tDR ([0-9]+)$')
156 star_prefix_regex
= re
.compile(r
'\t\*(?P<spaces>\ *)(?P<content>.*)')
160 CO_AUTHORED_BY_PREFIX
= 'co-authored-by: '
161 CHERRY_PICK_PREFIX
= '(cherry picked from commit '
162 REVERT_PREFIX
= 'This reverts commit '
164 REVIEW_PREFIXES
= ('reviewed-by: ', 'reviewed-on: ', 'signed-off-by: ',
165 'acked-by: ', 'tested-by: ', 'reported-by: ',
167 DATE_FORMAT
= '%Y-%m-%d'
171 def __init__(self
, message
, line
=None):
172 self
.message
= message
178 s
+= ':"%s"' % self
.line
182 class ChangeLogEntry
:
183 def __init__(self
, folder
, authors
, prs
):
185 # The 'list.copy()' function is not available before Python 3.3
186 self
.author_lines
= list(authors
)
187 self
.initial_prs
= list(prs
)
191 self
.file_patterns
= []
193 def parse_file_names(self
):
194 # Whether the content currently processed is between a star prefix the
195 # end of the file list: a colon or an open paren.
198 for line
in self
.lines
:
199 # If this line matches the star prefix, start the location
200 # processing on the information that follows the star.
201 m
= star_prefix_regex
.match(line
)
204 line
= m
.group('content')
207 # Strip everything that is not a filename in "line": entities
208 # "(NAME)", entry text (the colon, if present, and anything
211 line
= line
[:line
.index('(')]
214 line
= line
[:line
.index(':')]
217 # At this point, all that's left is a list of filenames
218 # separated by commas and whitespaces.
219 for file in line
.split(','):
222 if file.endswith('*'):
223 self
.file_patterns
.append(file[:-1])
225 self
.files
.append(file)
229 for author
in self
.author_lines
:
236 return [author_line
[0] for author_line
in self
.author_lines
]
240 return not self
.lines
and self
.prs
== self
.initial_prs
242 def contains_author(self
, author
):
243 for author_lines
in self
.author_lines
:
244 if author_lines
[0] == author
:
250 def __init__(self
, hexsha
, date
, author
, lines
, modified_files
):
255 self
.modified_files
= modified_files
259 def __init__(self
, info
, strict
=True, commit_to_info_hook
=None):
260 self
.original_info
= info
264 self
.changelog_entries
= []
266 self
.top_level_authors
= []
268 self
.top_level_prs
= []
269 self
.cherry_pick_commit
= None
270 self
.revert_commit
= None
271 self
.commit_to_info_hook
= commit_to_info_hook
273 # Identify first if the commit is a Revert commit
274 for line
in self
.info
.lines
:
275 if line
.startswith(REVERT_PREFIX
):
276 self
.revert_commit
= line
[len(REVERT_PREFIX
):].rstrip('.')
278 if self
.revert_commit
:
279 self
.info
= self
.commit_to_info_hook(self
.revert_commit
)
281 project_files
= [f
for f
in self
.info
.modified_files
282 if self
.is_changelog_filename(f
[0])
283 or f
[0] in misc_files
]
284 ignored_files
= [f
for f
in self
.info
.modified_files
285 if self
.in_ignored_location(f
[0])]
286 if len(project_files
) == len(self
.info
.modified_files
):
287 # All modified files are only MISC files
289 elif project_files
and strict
:
290 self
.errors
.append(Error('ChangeLog, DATESTAMP, BASE-VER and '
291 'DEV-PHASE updates should be done '
292 'separately from normal commits'))
295 all_are_ignored
= (len(project_files
) + len(ignored_files
)
296 == len(self
.info
.modified_files
))
297 self
.parse_lines(all_are_ignored
)
299 self
.parse_changelog()
300 self
.parse_file_names()
301 self
.check_for_empty_description()
302 self
.deduce_changelog_locations()
303 self
.check_file_patterns()
305 self
.check_mentioned_files()
306 self
.check_for_correct_changelog()
310 return not self
.errors
314 return [x
[0] for x
in self
.info
.modified_files
if x
[1] == 'A']
317 def is_changelog_filename(cls
, path
):
318 return path
.endswith('/ChangeLog') or path
== 'ChangeLog'
321 def find_changelog_location(cls
, name
):
322 if name
.startswith('\t'):
324 if name
.endswith(':'):
326 if name
.endswith('/'):
328 return name
if name
in changelog_locations
else None
331 def format_git_author(cls
, author
):
333 return author
.replace('<', ' <')
336 def parse_git_name_status(cls
, string
):
338 for entry
in string
.split('\n'):
339 parts
= entry
.split('\t')
341 if t
== 'A' or t
== 'D' or t
== 'M':
342 modified_files
.append((parts
[1], t
))
343 elif t
.startswith('R'):
344 modified_files
.append((parts
[1], 'D'))
345 modified_files
.append((parts
[2], 'A'))
346 return modified_files
348 def parse_lines(self
, all_are_ignored
):
349 body
= self
.info
.lines
351 for i
, b
in enumerate(body
):
354 if (changelog_regex
.match(b
) or self
.find_changelog_location(b
)
355 or star_prefix_regex
.match(b
) or pr_regex
.match(b
)
356 or dr_regex
.match(b
) or author_line_regex
.match(b
)):
357 self
.changes
= body
[i
:]
359 if not all_are_ignored
:
360 self
.errors
.append(Error('cannot find a ChangeLog location in '
363 def parse_changelog(self
):
366 for line
in self
.changes
:
368 if last_entry
and will_deduce
:
371 if line
!= line
.rstrip():
372 self
.errors
.append(Error('trailing whitespace', line
))
373 if len(line
.replace('\t', ' ' * TAB_WIDTH
)) > LINE_LIMIT
:
374 self
.errors
.append(Error('line exceeds %d character limit'
376 m
= changelog_regex
.match(line
)
378 last_entry
= ChangeLogEntry(m
.group(1).rstrip('/'),
379 self
.top_level_authors
,
381 self
.changelog_entries
.append(last_entry
)
382 elif self
.find_changelog_location(line
):
383 last_entry
= ChangeLogEntry(self
.find_changelog_location(line
),
384 self
.top_level_authors
,
386 self
.changelog_entries
.append(last_entry
)
390 if author_line_regex
.match(line
):
391 m
= author_line_regex
.match(line
)
392 author_tuple
= (m
.group('name'), m
.group('datetime'))
393 elif additional_author_regex
.match(line
):
394 m
= additional_author_regex
.match(line
)
395 if len(m
.group('spaces')) != 4:
396 msg
= 'additional author must be indented with '\
397 'one tab and four spaces'
398 self
.errors
.append(Error(msg
, line
))
400 author_tuple
= (m
.group('name'), None)
401 elif pr_regex
.match(line
):
402 component
= pr_regex
.match(line
).group('component')
404 self
.errors
.append(Error('missing PR component', line
))
406 elif not component
[:-1] in bug_components
:
407 self
.errors
.append(Error('invalid PR component', line
))
410 pr_line
= line
.lstrip()
411 elif dr_regex
.match(line
):
412 pr_line
= line
.lstrip()
414 lowered_line
= line
.lower()
415 if lowered_line
.startswith(CO_AUTHORED_BY_PREFIX
):
416 name
= line
[len(CO_AUTHORED_BY_PREFIX
):]
417 author
= self
.format_git_author(name
)
418 self
.co_authors
.append(author
)
420 elif lowered_line
.startswith(REVIEW_PREFIXES
):
422 elif line
.startswith(CHERRY_PICK_PREFIX
):
423 commit
= line
[len(CHERRY_PICK_PREFIX
):].rstrip(')')
424 self
.cherry_pick_commit
= commit
427 # ChangeLog name will be deduced later
430 self
.top_level_authors
.append(author_tuple
)
433 # append to top_level_prs only when we haven't met
435 if (pr_line
not in self
.top_level_prs
436 and not self
.changelog_entries
):
437 self
.top_level_prs
.append(pr_line
)
440 last_entry
= ChangeLogEntry(None,
441 self
.top_level_authors
,
443 self
.changelog_entries
.append(last_entry
)
446 if not last_entry
.contains_author(author_tuple
[0]):
447 last_entry
.author_lines
.append(author_tuple
)
450 if not line
.startswith('\t'):
451 err
= Error('line should start with a tab', line
)
452 self
.errors
.append(err
)
454 last_entry
.prs
.append(pr_line
)
456 m
= star_prefix_regex
.match(line
)
458 if len(m
.group('spaces')) != 1:
459 msg
= 'one space should follow asterisk'
460 self
.errors
.append(Error(msg
, line
))
462 last_entry
.lines
.append(line
)
464 if last_entry
.is_empty
:
465 msg
= 'first line should start with a tab, ' \
466 'an asterisk and a space'
467 self
.errors
.append(Error(msg
, line
))
469 last_entry
.lines
.append(line
)
471 def parse_file_names(self
):
472 for entry
in self
.changelog_entries
:
473 entry
.parse_file_names()
475 def check_file_patterns(self
):
476 for entry
in self
.changelog_entries
:
477 for pattern
in entry
.file_patterns
:
478 name
= os
.path
.join(entry
.folder
, pattern
)
479 if name
not in wildcard_prefixes
:
480 msg
= 'unsupported wildcard prefix'
481 self
.errors
.append(Error(msg
, name
))
483 def check_for_empty_description(self
):
484 for entry
in self
.changelog_entries
:
485 for i
, line
in enumerate(entry
.lines
):
486 if (star_prefix_regex
.match(line
) and line
.endswith(':') and
487 (i
== len(entry
.lines
) - 1
488 or star_prefix_regex
.match(entry
.lines
[i
+ 1]))):
489 msg
= 'missing description of a change'
490 self
.errors
.append(Error(msg
, line
))
492 def get_file_changelog_location(self
, changelog_file
):
493 for file in self
.info
.modified_files
:
494 if file[0] == changelog_file
:
495 # root ChangeLog file
497 index
= file[0].find('/' + changelog_file
)
499 return file[0][:index
]
502 def deduce_changelog_locations(self
):
503 for entry
in self
.changelog_entries
:
506 for file in entry
.files
:
507 location
= self
.get_file_changelog_location(file)
509 or (location
and location
in changelog_locations
)):
510 if changelog
and changelog
!= location
:
511 msg
= 'could not deduce ChangeLog file, ' \
512 'not unique location'
513 self
.errors
.append(Error(msg
))
516 if changelog
is not None:
517 entry
.folder
= changelog
519 msg
= 'could not deduce ChangeLog file'
520 self
.errors
.append(Error(msg
))
523 def in_ignored_location(cls
, path
):
524 for ignored
in ignored_prefixes
:
525 if path
.startswith(ignored
):
530 def get_changelog_by_path(cls
, path
):
531 components
= path
.split('/')
533 if '/'.join(components
) in changelog_locations
:
535 components
= components
[:-1]
536 return '/'.join(components
)
538 def check_mentioned_files(self
):
539 folder_count
= len([x
.folder
for x
in self
.changelog_entries
])
540 assert folder_count
== len(self
.changelog_entries
)
542 mentioned_files
= set()
543 mentioned_patterns
= []
544 used_patterns
= set()
545 for entry
in self
.changelog_entries
:
547 msg
= 'no files mentioned for ChangeLog in directory'
548 self
.errors
.append(Error(msg
, entry
.folder
))
549 assert not entry
.folder
.endswith('/')
550 for file in entry
.files
:
551 if not self
.is_changelog_filename(file):
552 mentioned_files
.add(os
.path
.join(entry
.folder
, file))
553 for pattern
in entry
.file_patterns
:
554 mentioned_patterns
.append(os
.path
.join(entry
.folder
, pattern
))
556 cand
= [x
[0] for x
in self
.info
.modified_files
557 if not self
.is_changelog_filename(x
[0])]
558 changed_files
= set(cand
)
559 for file in sorted(mentioned_files
- changed_files
):
560 msg
= 'unchanged file mentioned in a ChangeLog'
561 self
.errors
.append(Error(msg
, file))
562 for file in sorted(changed_files
- mentioned_files
):
563 if not self
.in_ignored_location(file):
564 if file in self
.new_files
:
565 changelog_location
= self
.get_changelog_by_path(file)
566 # Python2: we cannot use next(filter(...))
567 entries
= filter(lambda x
: x
.folder
== changelog_location
,
568 self
.changelog_entries
)
569 entries
= list(entries
)
570 entry
= entries
[0] if entries
else None
572 prs
= self
.top_level_prs
574 # if all ChangeLog entries have identical PRs
576 prs
= self
.changelog_entries
[0].prs
577 for entry
in self
.changelog_entries
:
581 entry
= ChangeLogEntry(changelog_location
,
582 self
.top_level_authors
,
584 self
.changelog_entries
.append(entry
)
585 # strip prefix of the file
586 assert file.startswith(entry
.folder
)
587 file = file[len(entry
.folder
):].lstrip('/')
588 entry
.lines
.append('\t* %s: New file.' % file)
589 entry
.files
.append(file)
591 used_pattern
= [p
for p
in mentioned_patterns
592 if file.startswith(p
)]
593 used_pattern
= used_pattern
[0] if used_pattern
else None
595 used_patterns
.add(used_pattern
)
597 msg
= 'changed file not mentioned in a ChangeLog'
598 self
.errors
.append(Error(msg
, file))
600 for pattern
in mentioned_patterns
:
601 if pattern
not in used_patterns
:
602 error
= 'pattern doesn''t match any changed files'
603 self
.errors
.append(Error(error
, pattern
))
605 def check_for_correct_changelog(self
):
606 for entry
in self
.changelog_entries
:
607 for file in entry
.files
:
608 full_path
= os
.path
.join(entry
.folder
, file)
609 changelog_location
= self
.get_changelog_by_path(full_path
)
610 if changelog_location
!= entry
.folder
:
611 msg
= 'wrong ChangeLog location "%s", should be "%s"'
612 err
= Error(msg
% (entry
.folder
, changelog_location
), file)
613 self
.errors
.append(err
)
616 def format_authors_in_changelog(cls
, authors
, timestamp
, prefix
=''):
618 for i
, author
in enumerate(authors
):
620 output
+= '%s%s %s\n' % (prefix
, timestamp
, author
)
622 output
+= '%s\t %s\n' % (prefix
, author
)
626 def to_changelog_entries(self
, use_commit_ts
=False):
627 current_timestamp
= self
.info
.date
.strftime(DATE_FORMAT
)
628 for entry
in self
.changelog_entries
:
630 timestamp
= entry
.datetime
631 if self
.cherry_pick_commit
:
632 info
= self
.commit_to_info_hook(self
.cherry_pick_commit
)
633 # it can happen that it is a cherry-pick for a different
636 timestamp
= info
.date
.strftime(DATE_FORMAT
)
638 timestamp
= current_timestamp
639 elif self
.revert_commit
:
640 timestamp
= current_timestamp
641 orig_date
= self
.original_info
.date
642 current_timestamp
= orig_date
.strftime(DATE_FORMAT
)
643 elif not timestamp
or use_commit_ts
:
644 timestamp
= current_timestamp
645 authors
= entry
.authors
if entry
.authors
else [self
.info
.author
]
646 # add Co-Authored-By authors to all ChangeLog entries
647 for author
in self
.co_authors
:
648 if author
not in authors
:
649 authors
.append(author
)
651 if self
.cherry_pick_commit
or self
.revert_commit
:
652 output
+= self
.format_authors_in_changelog([self
.info
.author
],
654 if self
.cherry_pick_commit
:
655 output
+= '\tBackported from master:\n'
657 output
+= '\tRevert:\n'
658 output
+= self
.format_authors_in_changelog(authors
,
661 output
+= self
.format_authors_in_changelog(authors
, timestamp
)
663 output
+= '\t%s\n' % pr
664 for line
in entry
.lines
:
665 output
+= line
+ '\n'
666 yield (entry
.folder
, output
.rstrip())
668 def print_output(self
):
669 for entry
, output
in self
.to_changelog_entries():
670 print('------ %s/ChangeLog ------ ' % entry
)
673 def print_errors(self
):
675 for error
in self
.errors
: