hatch_ci.script

src/hatch_ci/script.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
"""create a beta branch or release a beta branch

This script will either create a new beta branch:

     hatch-ci make-beta ./src/package_name/__init__.py

Or will release the beta branch and will move inot the next minor

    hatch-ci {major|minor|micro} ./src/package_name/__init__.py

"""
from __future__ import annotations

import argparse
import logging
import re
import sys
from pathlib import Path

from . import cli, scm, tools

log = logging.getLogger(__name__)


def add_arguments(parser: argparse.ArgumentParser):
    parser.add_argument("--master", help="the 'master' branch")
    parser.add_argument(
        "-w",
        "--workdir",
        help="git working dir",
        default=Path("."),
        type=Path,
    )
    parser.add_argument("mode", choices=["micro", "minor", "major", "make-beta"])
    parser.add_argument("initfile", metavar="__init__.py", type=Path)


def process_options(
    options: argparse.Namespace, error: cli.ErrorFn
) -> argparse.Namespace:
    try:
        options.repo = repo = scm.GitRepo(options.workdir)
        repo.status()
    except scm.GitError:
        error(
            "no git directory",
            "It looks the repository is not a git repo",
            hint="init the git directory",
        )
    log.info("working dir set to '%s'", options.workdir)
    try:
        branch = repo.head.shorthand
        log.info("current branch set to '%s'", branch)
    except scm.GitError:
        error(
            "invalid git repository",
            """
              It looks the repository doesn't have any branch,
              you should:
                git checkout --orphan <branch-name>
              """,
            hint="create a git branch",
        )
    return options


@cli.cli(add_arguments, process_options, __doc__)
def main(options) -> None:
    # master branch
    master = options.master or (
        options.repo.config["init.defaultbranch"]
        if "init.defaultbranch" in options.repo.config
        else "master"
    )
    current = options.repo.head.shorthand
    log.info("current branch '%s'", current)

    if options.repo.status(untracked_files="no", ignored=False):
        options.error(f"modified files in {options.repo.workdir}")
    if not options.initfile.exists():
        options.error(f"cannot find version file {options.initfile}")

    version = tools.get_module_var(options.initfile, "__version__")
    log.info("got version %s for branch '%s'", version, options.repo.head.shorthand)
    if not version:
        raise tools.InvalidVersionError(f"cannot find a version in {options.initfile}")

    # fetching all remotes
    options.repo(["fetch", "--all"])

    if options.mode == "make-beta":
        if options.repo.head.name != f"refs/heads/{master}":
            options.error(
                f"wrong branch '{options.repo.head.name}', expected '{master}'"
            )

        for branch in [*options.repo.branches.local, *options.repo.branches.remote]:
            if not branch.endswith(f"beta/{version}"):
                continue
            options.error(f"branch '{branch}' already present")
        log.info("creating branch '%s'", f"/beta/{version}")
        options.repo.branch(f"beta/{version}", master)
        options.repo(["checkout", current])
        print(  # noqa: T201
            tools.indent(
                f"""
        The release branch beta/{version} has been created.

        To complete the release:
            git push origin beta/{version}

        To revert this beta branch:
            git branch -D beta/{version}
        """
            ),
            file=sys.stderr,
        )
    elif options.mode in {"micro", "minor", "major"}:
        # we need to be in the beta/N.M.O branch
        expr = re.compile(r"refs/heads/beta/(?P<beta>\d+([.]\d+)*)$")
        if not (match := expr.search(options.repo.head.name)):
            options.error(
                f"wrong branch '{options.repo.head.shorthand}'",
                f"expected to be in 'beta/{version}' branch",
                f"git checkout beta/{version}",
            )
            return
        local = match.group("beta")
        if local != version:
            options.error(f"wrong version file {version=} != {local}")

        # create an empty commit to mark the release
        options.repo(["commit", "--allow-empty", "-m", f"released {version}"])

        # tag
        options.repo(["tag", "-a", f"release/{version}", "-m", f"released {version}"])

        # switch to master (and incorporate the commit message)
        options.repo(["checkout", master])
        options.repo(["merge", f"beta/{version}"])

        # bump version
        new_version = tools.bump_version(version, options.mode)
        tools.set_module_var(options.initfile, "__version__", new_version)

        # commit
        options.repo.commit(
            options.initfile, f"version bump {version} -> {new_version}"
        )

        print(  # noqa: T201
            tools.indent(
                f"""
        The release is almost complete.

        To complete the release:
            git push origin release/{version}
            git push origin master

        To revert this release:
            git reset --hard HEAD~1
            git tag -d release/{version}
        """
            ),
            file=sys.stderr,
        )
    else:
        options.error(f"unsupported mode {options.mode=}")
        raise RuntimeError(f"unsupported mode {options.mode=}")


if __name__ == "__main__":
    main()