16from collections.abc 
import Callable, Iterable
 
   17from difflib 
import unified_diff
 
   18from subprocess 
import run
 
   19from typing 
import Union
 
   23GITLAB_TOKEN = os.environ.get(
"GITLAB_TOKEN")
 
   28    Convert a version in format "vXrY" or "X.Y" in the pair ("X.Y", "vXrY"). 
   30    >>> normalize_version("v37r0") 
   32    >>> normalize_version("37.0.1") 
   36    numbers = re.findall(
r"\d+", version)
 
   39        "".join(
"{}{}".
format(*pair) 
for pair 
in zip(
"vrpt", numbers)),
 
 
   45    Helper to carry the allowed fields for formatting replacement strings. 
   47    >>> f = Fields("v37r1", datetime.date(2023, 9, 25)) 
   49    Fields('37.1', datetime.date(2023, 9, 25)) 
   51    {'cmake_version': '37.1', 'tag_version': 'v37r1', 'date': datetime.date(2023, 9, 25)} 
   54    def __init__(self, version: str, date: datetime.date):
 
   57            cmake_version=cmake_version,
 
   58            tag_version=tag_version,
 
 
   64            f
"Fields({repr(self._data['cmake_version'])}, {repr(self._data['date'])})" 
 
 
   74    Helper to replace lines with patterns or applying functions. 
   76    >>> r = ReplacementRule(r"^version: ", "version: {cmake_version}") 
   77    >>> f = Fields("v1r1", datetime.date(2023, 9, 25)) 
   78    >>> r("nothing to change\\n", f) 
   79    'nothing to change\\n' 
   80    >>> r("version: 1.0\\n", f) 
   86        pattern: Union[str, re.Pattern],
 
   87        replace: Union[str, Callable[[str, Fields], str]],
 
   90        if isinstance(replace, str):
 
   91            replace = f
"{replace.rstrip()}\n" 
   92            self.
replace = 
lambda _line, fields: replace.format(**fields.data)
 
 
   96    def __call__(self, line: str, fields: Fields) -> str:
 
   98            return self.
replace(line, fields)
 
 
 
  104        self, filename: str, rules: Iterable[Union[ReplacementRule, tuple[str, str]]]
 
  108            r 
if isinstance(r, ReplacementRule) 
else ReplacementRule(*r) 
for r 
in rules
 
 
  112        for rule 
in self.
rules:
 
  113            line = rule(line, fields)
 
 
  116    def __call__(self, fields: Fields) -> tuple[str, list[str], list[str]]:
 
 
 
  124    Special updater to fill draft changelog entry. 
  127        [
"git", 
"describe", 
"--tags", 
"--abbrev=0"], capture_output=
True, text=
True 
  132        [
"git", 
"log", 
"--first-parent", 
"--format=%s<=>%b|", f
"{latest_tag}.."],
 
  137    changes_txt = 
" ".join(changes_txt.strip().rstrip(
"|").splitlines())
 
  140        changes_txt.replace(
"Closes #", 
"gaudi/Gaudi#")
 
  141        .replace(
"See merge request ", 
"")
 
  146        f
"- {msg.strip()} ({', '.join(refs.split())})\n" 
  148        else f
"- {msg.strip()}\n" 
  149        for change 
in changes
 
  150        for msg, refs 
in ([change.split(
"<=>", 1)] 
if "<=>" in change 
else [])
 
  153    contributors = sorted(
 
  157                [
"git", 
"log", 
"--format=%an", f
"{latest_tag}...HEAD"],
 
  160            ).stdout.splitlines()
 
  164    filename = 
"CHANGELOG.md" 
  165    with open(filename) 
as f:
 
  167    for idx, line 
in enumerate(old):
 
  168        if line.startswith(
"## ["):
 
  174            "## [{tag_version}](https://gitlab.cern.ch/gaudi/Gaudi/-/releases/{tag_version}) - {date}\n".
format(
 
  177            "\nA special thanks to all the people that contributed to this release:\n",
 
  178            ",\n".join(contributors),
 
  187    data.extend([
"\n", 
"\n"])
 
  188    data.extend(old[idx:])
 
  190    return filename, old, data
 
 
  195        from requests 
import get
 
  198            "https://gitlab.cern.ch/api/v4/users",
 
  199            headers={
"PRIVATE-TOKEN": GITLAB_TOKEN},
 
  200            params={
"search": name},
 
  203            return f
'@{users[0]["username"]}' 
  208@click.argument("version", type=str) 
  211    type=click.DateTime((
"%Y-%m-%d",)),
 
  213    default=datetime.datetime.now(),
 
  220    help=
"only show what would change, but do not modify the files",
 
 
  224    Helper to easily update the project version number in all needed files. 
  226    fields = 
Fields(version, date.date())
 
  228        "Bumping version to {cmake_version} (tag: {tag_version})".
format(**fields.data)
 
  234            [(
r"^project\(Gaudi VERSION", 
"project(Gaudi VERSION {cmake_version}")],
 
  239                (
r"^version: ", 
"version: {tag_version}"),
 
  240                (
r"^date-released: ", 
"date-released: '{date}'"),
 
  244            "docs/source/conf.py",
 
  246                (
r"^version = ", 
'version = "{cmake_version}"'),
 
  247                (
r"^release = ", 
'release = "{tag_version}"'),
 
  252        filename, old, new = updater(fields)
 
  256                sys.stdout.writelines(
 
  260                        fromfile=f
"a/{filename}",
 
  261                        tofile=f
"b/{filename}",
 
  265                click.echo(f
"updated {filename}")
 
  266                with open(filename, 
"w") 
as f:
 
 
  270if __name__ == 
"__main__":
 
GAUDI_API std::string format(const char *,...)
MsgStream format utility "a la sprintf(...)".
__init__(self, str version, datetime.date date)
str _apply_rules(self, str line, Fields fields)
__init__(self, str filename, Iterable[Union[ReplacementRule, tuple[str, str]]] rules)
tuple[str, list[str], list[str]] __call__(self, Fields fields)
__init__(self, Union[str, re.Pattern] pattern, Union[str, Callable[[str, Fields], str]] replace)
str __call__(self, str line, Fields fields)
str contributor_handle(str name)
tuple[str, str] normalize_version(str version)
tuple[str, list[str], list[str]] update_changelog(Fields fields)