]>
git.ipfire.org Git - thirdparty/gcc.git/blob - contrib/gcc-changelog/git_commit.py
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([
132 'libphobos/libdruntime',
143 author_line_regex
= \
144 re
.compile(r
'^(?P<datetime>\d{4}-\d{2}-\d{2})\ {2}(?P<name>.* <.*>)')
145 additional_author_regex
= re
.compile(r
'^\t(?P<spaces>\ *)?(?P<name>.* <.*>)')
146 changelog_regex
= re
.compile(r
'^([a-z0-9+-/]*)/ChangeLog:?')
147 pr_regex
= re
.compile(r
'\tPR (?P<component>[a-z+-]+\/)?([0-9]+)$')
148 star_prefix_regex
= re
.compile(r
'\t\*(?P<spaces>\ *)(?P<content>.*)')
152 CO_AUTHORED_BY_PREFIX
= 'co-authored-by: '
153 CHERRY_PICK_PREFIX
= '(cherry picked from commit '
157 def __init__(self
, message
, line
=None):
158 self
.message
= message
164 s
+= ':"%s"' % self
.line
168 class ChangeLogEntry
:
169 def __init__(self
, folder
, authors
, prs
):
171 # Python2 has not 'copy' function
172 self
.author_lines
= list(authors
)
173 self
.initial_prs
= list(prs
)
180 for line
in self
.lines
:
181 m
= star_prefix_regex
.match(line
)
183 line
= m
.group('content')
185 line
= line
[:line
.index('(')]
187 line
= line
[:line
.index(':')]
188 for file in line
.split(','):
196 for author
in self
.author_lines
:
203 return [author_line
[0] for author_line
in self
.author_lines
]
207 return not self
.lines
and self
.prs
== self
.initial_prs
211 def __init__(self
, hexsha
, date
, author
, body
, modified_files
,
215 self
.modified_files
= modified_files
218 self
.changelog_entries
= []
222 self
.top_level_authors
= []
224 self
.top_level_prs
= []
226 project_files
= [f
for f
in self
.modified_files
227 if self
.is_changelog_filename(f
[0])
228 or f
[0] in misc_files
]
229 if len(project_files
) == len(self
.modified_files
):
230 # All modified files are only MISC files
232 elif project_files
and strict
:
233 self
.errors
.append(Error('ChangeLog, DATESTAMP, BASE-VER and '
234 'DEV-PHASE updates should be done '
235 'separately from normal commits'))
240 self
.parse_changelog()
241 self
.deduce_changelog_locations()
243 self
.check_mentioned_files()
244 self
.check_for_correct_changelog()
248 return not self
.errors
252 return [x
[0] for x
in self
.modified_files
if x
[1] == 'A']
255 def is_changelog_filename(cls
, path
):
256 return path
.endswith('/ChangeLog') or path
== 'ChangeLog'
259 def find_changelog_location(cls
, name
):
260 if name
.startswith('\t'):
262 if name
.endswith(':'):
264 if name
.endswith('/'):
266 return name
if name
in changelog_locations
else None
269 def format_git_author(cls
, author
):
271 return author
.replace('<', ' <')
274 def parse_git_name_status(cls
, string
):
276 for entry
in string
.split('\n'):
277 parts
= entry
.split('\t')
279 if t
== 'A' or t
== 'D' or t
== 'M':
280 modified_files
.append((parts
[1], t
))
282 modified_files
.append((parts
[1], 'D'))
283 modified_files
.append((parts
[2], 'A'))
284 return modified_files
286 def parse_lines(self
):
289 for i
, b
in enumerate(body
):
292 if (changelog_regex
.match(b
) or self
.find_changelog_location(b
)
293 or star_prefix_regex
.match(b
) or pr_regex
.match(b
)
294 or author_line_regex
.match(b
)):
295 self
.changes
= body
[i
:]
297 self
.errors
.append(Error('cannot find a ChangeLog location in '
300 def parse_changelog(self
):
303 for line
in self
.changes
:
305 if last_entry
and will_deduce
:
308 if line
!= line
.rstrip():
309 self
.errors
.append(Error('trailing whitespace', line
))
310 if len(line
.replace('\t', ' ' * TAB_WIDTH
)) > LINE_LIMIT
:
311 self
.errors
.append(Error('line limit exceeds %d characters'
313 m
= changelog_regex
.match(line
)
315 last_entry
= ChangeLogEntry(m
.group(1), self
.top_level_authors
,
317 self
.changelog_entries
.append(last_entry
)
318 elif self
.find_changelog_location(line
):
319 last_entry
= ChangeLogEntry(self
.find_changelog_location(line
),
320 self
.top_level_authors
,
322 self
.changelog_entries
.append(last_entry
)
326 if author_line_regex
.match(line
):
327 m
= author_line_regex
.match(line
)
328 author_tuple
= (m
.group('name'), m
.group('datetime'))
329 elif additional_author_regex
.match(line
):
330 m
= additional_author_regex
.match(line
)
331 if len(m
.group('spaces')) != 4:
332 msg
= 'additional author must prepend with tab ' \
334 self
.errors
.append(Error(msg
, line
))
336 author_tuple
= (m
.group('name'), None)
337 elif pr_regex
.match(line
):
338 component
= pr_regex
.match(line
).group('component')
340 self
.errors
.append(Error('missing PR component', line
))
342 elif not component
[:-1] in bug_components
:
343 self
.errors
.append(Error('invalid PR component', line
))
346 pr_line
= line
.lstrip()
348 if line
.lower().startswith(CO_AUTHORED_BY_PREFIX
):
349 name
= line
[len(CO_AUTHORED_BY_PREFIX
):]
350 author
= self
.format_git_author(name
)
351 self
.co_authors
.append(author
)
353 elif line
.startswith(CHERRY_PICK_PREFIX
):
356 # ChangeLog name will be deduced later
359 self
.top_level_authors
.append(author_tuple
)
362 # append to top_level_prs only when we haven't met
364 if (pr_line
not in self
.top_level_prs
365 and not self
.changelog_entries
):
366 self
.top_level_prs
.append(pr_line
)
369 last_entry
= ChangeLogEntry(None,
370 self
.top_level_authors
,
372 self
.changelog_entries
.append(last_entry
)
375 last_entry
.author_lines
.append(author_tuple
)
378 if not line
.startswith('\t'):
379 err
= Error('line should start with a tab', line
)
380 self
.errors
.append(err
)
382 last_entry
.prs
.append(pr_line
)
384 m
= star_prefix_regex
.match(line
)
386 if len(m
.group('spaces')) != 1:
387 err
= Error('one space should follow asterisk',
389 self
.errors
.append(err
)
391 last_entry
.lines
.append(line
)
393 if last_entry
.is_empty
:
394 msg
= 'first line should start with a tab, ' \
396 self
.errors
.append(Error(msg
, line
))
398 last_entry
.lines
.append(line
)
400 def get_file_changelog_location(self
, changelog_file
):
401 for file in self
.modified_files
:
402 if file[0] == changelog_file
:
403 # root ChangeLog file
405 index
= file[0].find('/' + changelog_file
)
407 return file[0][:index
]
410 def deduce_changelog_locations(self
):
411 for entry
in self
.changelog_entries
:
414 for file in entry
.files
:
415 location
= self
.get_file_changelog_location(file)
417 or (location
and location
in changelog_locations
)):
418 if changelog
and changelog
!= location
:
419 msg
= 'could not deduce ChangeLog file, ' \
420 'not unique location'
421 self
.errors
.append(Error(msg
))
424 if changelog
is not None:
425 entry
.folder
= changelog
427 msg
= 'could not deduce ChangeLog file'
428 self
.errors
.append(Error(msg
))
431 def in_ignored_location(cls
, path
):
432 for ignored
in ignored_prefixes
:
433 if path
.startswith(ignored
):
438 def get_changelog_by_path(cls
, path
):
439 components
= path
.split('/')
441 if '/'.join(components
) in changelog_locations
:
443 components
= components
[:-1]
444 return '/'.join(components
)
446 def check_mentioned_files(self
):
447 folder_count
= len([x
.folder
for x
in self
.changelog_entries
])
448 assert folder_count
== len(self
.changelog_entries
)
450 mentioned_files
= set()
451 for entry
in self
.changelog_entries
:
453 msg
= 'ChangeLog must contain a file entry'
454 self
.errors
.append(Error(msg
, entry
.folder
))
455 assert not entry
.folder
.endswith('/')
456 for file in entry
.files
:
457 if not self
.is_changelog_filename(file):
458 mentioned_files
.add(os
.path
.join(entry
.folder
, file))
460 cand
= [x
[0] for x
in self
.modified_files
461 if not self
.is_changelog_filename(x
[0])]
462 changed_files
= set(cand
)
463 for file in sorted(mentioned_files
- changed_files
):
464 self
.errors
.append(Error('file not changed in a patch', file))
465 for file in sorted(changed_files
- mentioned_files
):
466 if not self
.in_ignored_location(file):
467 if file in self
.new_files
:
468 changelog_location
= self
.get_changelog_by_path(file)
469 # Python2: we cannot use next(filter(...))
470 entries
= filter(lambda x
: x
.folder
== changelog_location
,
471 self
.changelog_entries
)
472 entries
= list(entries
)
473 entry
= entries
[0] if entries
else None
475 prs
= self
.top_level_prs
477 # if all ChangeLog entries have identical PRs
479 prs
= self
.changelog_entries
[0].prs
480 for entry
in self
.changelog_entries
:
484 entry
= ChangeLogEntry(changelog_location
,
485 self
.top_level_authors
,
487 self
.changelog_entries
.append(entry
)
488 # strip prefix of the file
489 assert file.startswith(entry
.folder
)
490 file = file[len(entry
.folder
):].lstrip('/')
491 entry
.lines
.append('\t* %s: New file.' % file)
493 msg
= 'changed file not mentioned in a ChangeLog'
494 self
.errors
.append(Error(msg
, file))
496 def check_for_correct_changelog(self
):
497 for entry
in self
.changelog_entries
:
498 for file in entry
.files
:
499 full_path
= os
.path
.join(entry
.folder
, file)
500 changelog_location
= self
.get_changelog_by_path(full_path
)
501 if changelog_location
!= entry
.folder
:
502 msg
= 'wrong ChangeLog location "%s", should be "%s"'
503 err
= Error(msg
% (entry
.folder
, changelog_location
), file)
504 self
.errors
.append(err
)
506 def to_changelog_entries(self
, use_commit_ts
=False):
507 for entry
in self
.changelog_entries
:
509 timestamp
= entry
.datetime
510 if not timestamp
or use_commit_ts
:
511 timestamp
= self
.date
.strftime('%Y-%m-%d')
512 authors
= entry
.authors
if entry
.authors
else [self
.author
]
513 # add Co-Authored-By authors to all ChangeLog entries
514 for author
in self
.co_authors
:
515 if author
not in authors
:
516 authors
.append(author
)
518 for i
, author
in enumerate(authors
):
520 output
+= '%s %s\n' % (timestamp
, author
)
522 output
+= '\t %s\n' % author
525 output
+= '\t%s\n' % pr
526 for line
in entry
.lines
:
527 output
+= line
+ '\n'
528 yield (entry
.folder
, output
.rstrip())
530 def print_output(self
):
531 for entry
, output
in self
.to_changelog_entries():
532 print('------ %s/ChangeLog ------ ' % entry
)
535 def print_errors(self
):
537 for error
in self
.errors
: