| #!/usr/bin/env python3 | |
| # Copyright (C) 2020-2026 Free Software Foundation, Inc. | |
| # | |
| # 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 logging | |
| import os | |
| import re | |
| from git import Repo | |
| from git_repository import parse_git_revisions | |
| current_timestamp = datetime.datetime.now().strftime('%Y%m%d\n') | |
| # Skip the following commits, they cannot be correctly processed | |
| ignored_commits = { | |
| '843b49269eaca82860ab4171f9644b5c411f05fc', | |
| '5c0785d62ca44b9073e543b2b3dbb04f0aca83af', | |
| 'ea3a617121455260ffff00a3b3744548ea45b6e1', | |
| 'c2eee09d92ddf1b676efd756e9f82c12f3e0a767'} | |
| FORMAT = '%(asctime)s:%(levelname)s:%(name)s:%(message)s' | |
| logging.basicConfig(level=logging.INFO, format=FORMAT, | |
| handlers=[ | |
| logging.FileHandler('/tmp/git_update_version.txt'), | |
| logging.StreamHandler() | |
| ]) | |
| def read_timestamp(path): | |
| with open(path) as f: | |
| return f.read() | |
| def prepend_to_changelog_files(repo, folder, git_commit, add_to_git): | |
| if not git_commit.success: | |
| logging.info(f"While processing {git_commit.info.hexsha}:") | |
| for error in git_commit.errors: | |
| logging.info(error) | |
| raise AssertionError() | |
| for entry, output in git_commit.to_changelog_entries(use_commit_ts=True): | |
| full_path = os.path.join(folder, entry, 'ChangeLog') | |
| logging.info('writing to %s' % full_path) | |
| if os.path.exists(full_path): | |
| with open(full_path) as f: | |
| content = f.read() | |
| else: | |
| content = '' | |
| with open(full_path, 'w+') as f: | |
| f.write(output) | |
| if content: | |
| f.write('\n\n') | |
| f.write(content) | |
| if add_to_git: | |
| repo.git.add(full_path) | |
| active_refs = ['master', | |
| 'releases/gcc-14', 'releases/gcc-15', 'releases/gcc-16'] | |
| parser = argparse.ArgumentParser(description='Update DATESTAMP and generate ' | |
| 'ChangeLog entries') | |
| parser.add_argument('-g', '--git-path', default='.', | |
| help='Path to git repository') | |
| parser.add_argument('-p', '--push', action='store_true', | |
| help='Push updated active branches') | |
| parser.add_argument('-d', '--dry-mode', | |
| help='Generate patch for ChangeLog entries and do it' | |
| ' even if DATESTAMP is unchanged; folder argument' | |
| ' is expected') | |
| parser.add_argument('-c', '--current', action='store_true', | |
| help='Modify current branch (--push argument is ignored)') | |
| parser.add_argument('-i', '--ignore', action='append', | |
| help='list of commits to ignore') | |
| args = parser.parse_args() | |
| repo = Repo(args.git_path) | |
| origin = repo.remotes['origin'] | |
| def update_current_branch(ref_name=None): | |
| commit = repo.head.commit | |
| commit_count = 1 | |
| while commit: | |
| if (commit.author.email == 'gccadmin@gcc.gnu.org' | |
| and commit.message.strip() == 'Daily bump.'): | |
| break | |
| # We support merge commits but only with 2 parensts | |
| assert len(commit.parents) <= 2 | |
| commit = commit.parents[-1] | |
| commit_count += 1 | |
| logging.info('%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 | |
| or args.dry_mode or args.current): | |
| head = repo.head.commit | |
| # if HEAD is a merge commit, start with second parent | |
| # (branched that is being merged into the current one) | |
| assert len(head.parents) <= 2 | |
| if len(head.parents) == 2: | |
| head = head.parents[1] | |
| commits = parse_git_revisions(args.git_path, '%s..%s' | |
| % (commit.hexsha, head.hexsha), ref_name) | |
| commits = [c for c in commits if c.info.hexsha not in ignored_commits] | |
| for git_commit in reversed(commits): | |
| prepend_to_changelog_files(repo, args.git_path, git_commit, | |
| not args.dry_mode) | |
| if args.dry_mode: | |
| diff = repo.git.diff('HEAD') | |
| patch = os.path.join(args.dry_mode, | |
| branch.name.split('/')[-1] + '.patch') | |
| with open(patch, 'w+') as f: | |
| f.write(diff) | |
| logging.info('branch diff written to %s' % patch) | |
| repo.git.checkout(force=True) | |
| else: | |
| # update timestamp | |
| logging.info('DATESTAMP will be changed:') | |
| with open(datestamp_path, 'w+') as f: | |
| f.write(current_timestamp) | |
| repo.git.add(datestamp_path) | |
| if not args.current: | |
| repo.index.commit('Daily bump.') | |
| logging.info('commit is done') | |
| if args.push: | |
| try: | |
| repo.git.push('origin', branch) | |
| logging.info('branch is pushed') | |
| except Exception: | |
| logging.exception('git push failed') | |
| else: | |
| logging.info('DATESTAMP unchanged') | |
| if args.ignore is not None: | |
| for item in args.ignore: | |
| ignored_commits.update(set(i for i in re.split(r'\s*,\s*|\s+', item))) | |
| if args.current: | |
| logging.info('=== Working on the current branch ===') | |
| update_current_branch() | |
| else: | |
| 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) | |
| logging.info('=== Working on: %s ===' % branch) | |
| branch.checkout() | |
| origin.pull(rebase=True) | |
| logging.info('branch pulled and checked out') | |
| update_current_branch(name) | |
| assert not repo.index.diff(None) | |
| logging.info('branch is done') | |
| logging.info('') |