git-tide 0.1.0__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,162 @@
1
+ Metadata-Version: 2.1
2
+ Name: git-tide
3
+ Version: 0.1.2
4
+ Summary: CLI for automated gitflow-style branching
5
+ Author: Chad Dombrova
6
+ Classifier: Programming Language :: Python :: 2
7
+ Classifier: Programming Language :: Python :: 2.7
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.4
10
+ Classifier: Programming Language :: Python :: 3.5
11
+ Classifier: Programming Language :: Python :: 3.6
12
+ Classifier: Programming Language :: Python :: 3.7
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Provides-Extra: init
19
+ Requires-Dist: click (==8.1.7)
20
+ Requires-Dist: commitizen (==3.16.0)
21
+ Requires-Dist: python-gitlab (==4.8.0) ; extra == "init"
22
+ Requires-Dist: tomli (>=2.0.1,<3.0.0) ; python_version < "3.11"
23
+ Description-Content-Type: text/markdown
24
+
25
+
26
+ # Git Tide: Automated gitflow merge cycles with semantic versioning
27
+
28
+ `tide` simplifies [gitflow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) processes with automatic tagging and hot-fixing and promotion.
29
+
30
+ ## Features
31
+
32
+ - Automatic cascading merging of hotfixes from stable branches into experimental ones, e.g. alpha, beta, rc (this is the "ebb tide")
33
+ - - Automated "promotion" of experimental branches forward to their next branch. e.g. alpha to beta, beta to rc, and so on. (this is the "flood tide" or "flow")
34
+ - Uses [`commitizen`](https://github.com/commitizen-tools/commitizen) to create semver tags
35
+ - Avoids merge conflicts between branches by using tags to store and query the latest version
36
+
37
+
38
+ ### Example flow
39
+
40
+ Here is an overview of the rules that `tide` applies during automation:
41
+
42
+ > NOTE: this document and repository use **develop**, **staging**, and **master** for it's gitflow branches
43
+ > but this behavior is configurable in the **pyproject.toml** and **.gitlab-ci.yml** files.
44
+
45
+ - New features should start as merge requests made against the `develop` branch (this is the default branch)
46
+ - Commits that are merged to `develop` auto-generate a tag with a beta suffix: e.g. `1.0.0b1`
47
+ - Commits that are merged to `staging` auto-generate a tag with a release candidate suffix: e.g. `1.0.0rc1`
48
+ - Hotfixes that are added to `master` automatically merge to `staging`
49
+ - Hotfixes that are added to `staging` automatically merge to `develop`
50
+ - Promoting the release candidate to a new official release can be done two ways:
51
+ - Every pipeline on the `staging` branch has a `promotion` job that can be manually run
52
+ - A scheduled pipeline can be setup to do the same
53
+ - Either way, when the `promotion` job runs it will do 3 things:
54
+ - Move the `master` branch to the tip of `staging` and create a new release, stripping off the `rc` suffix
55
+ - Move the `staging` branch to the tip of `develop` and create a new release, converting `b` to `rc`
56
+ - Restart `develop`, incrementing the minor version, e.g. from `1.1.0b4` to `1.2.0b0`.
57
+
58
+ ### Sample development cycle
59
+
60
+ Below is the Git commit graph demonstrating a sample development cycle using this project's branching strategy,
61
+ including tags and hotfix propagation.
62
+
63
+ ```plaintext
64
+ * auto-hotfix into develop: staging: add hotfix 2 - (HEAD -> develop, tag: 1.2.0rc0, tag: 1.2.0b2, staging)
65
+ |\
66
+ | * staging: add hotfix 2 - (tag: 1.1.0rc2, tag: 1.1.0, master)
67
+ * | auto-hotfix into develop: master: add hotfix - (tag: 1.2.0b1)
68
+ |\|
69
+ | * auto-hotfix into staging: master: add hotfix - (tag: 1.1.0rc1)
70
+ | |\
71
+ | | * master: add hotfix - (tag: 1.0.1)
72
+ * | | develop: add beta feature 2 - (tag: 1.2.0b0)
73
+ |/ /
74
+ * / develop: add feature 1 - (tag: 1.1.0rc0, tag: 1.1.0b0)
75
+ |/
76
+ * master: initial state - (tag: 1.0.0)
77
+
78
+ chronological tag order:
79
+ 1.2.0rc0 promoting develop to staging!
80
+ 1.1.0 promoting staging to master!
81
+ 1.2.0b2 auto-hotfix into develop: staging: add hotfix 2
82
+ 1.1.0rc2 staging: add hotfix 2
83
+ 1.2.0b1 auto-hotfix into develop: master: add hotfix
84
+ 1.1.0rc1 auto-hotfix into staging: master: add hotfix
85
+ 1.0.1 master: add hotfix
86
+ 1.2.0b0 develop: add beta feature 2
87
+ 1.1.0rc0 promoting develop to staging!
88
+ 1.1.0b0 develop: add feature 1
89
+ 1.0.0 master: initial state
90
+ ```
91
+
92
+ ## Setting up a repo
93
+
94
+ In order for `tide` to work its magic, the Gitlab repo needs to be properly configured:
95
+
96
+ - Add a `[tool].tide` section to your `pyproject.toml` file to define the gitflow branch names and prerelease version you wish to use.
97
+ ```toml
98
+ [tool.tide]
99
+ branches.beta = "develop"
100
+ branches.rc = "staging"
101
+ branches.stable = "master"
102
+ ```
103
+ - Create a [Project Access Token](https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html) with the appropriate scope for being able to push tags, changes and create cicd variables.
104
+ - Run `tide init --access-token='YOUR_ACCESS_TOKEN'`
105
+ - Copy and modify the `.gitlab-ci.yml` file ensuring the branch variables match the ones in the `pyproject.toml` (TODO: automated generation of `.gitlab-ci.yml` in `tide init`)
106
+
107
+ ## Development
108
+
109
+ ### Setting up your local development environment
110
+
111
+ - clone the repository using either https or ssh depending on your preference and then run the following commands:
112
+ ```bash
113
+ python -m venv venv
114
+ .\venv\Scripts\Activate
115
+ pip install -r requirements.txt
116
+ pre-commit install
117
+ ```
118
+
119
+ ### Local development info, tips, and tricks
120
+
121
+ `nox` is the primary interface for what we'll be doing day to day and under the hood.
122
+
123
+ Use `nox --list` to see the tasks that are available.
124
+
125
+ ### Running the unit tests
126
+
127
+ ```bash
128
+ nox -s unit_tests
129
+ ```
130
+
131
+ The primary test `tests/test_unit.py::test_dev_cycle` can be run in several modes:
132
+ - local: simulated using local repos
133
+ - remote: integration testing using real gitlab repos
134
+ - gitlab-ci-local: simulated using local repos, but using the `.gitlab-ci.yml` file for entry-point and env var control. Requires installation of https://github.com/firecow/gitlab-ci-local
135
+
136
+ Here's how you run it in remote mode:
137
+ ```bash
138
+ EXEC_MODE="remote" ACCESS_TOKEN="your-access-token-here" nox -s unit_tests -- tests/test_unit.py::test_dev_cycle -vv -s
139
+ ```
140
+
141
+ Here's how you run it using `gitlab-ci-local`:
142
+ ```bash
143
+ EXEC_MODE="gitlab-ci-local" nox -s unit_tests -- tests/test_unit.py::test_dev_cycle -vv -s
144
+ ```
145
+
146
+ ### Serving Documentation on GitLab Pages
147
+
148
+ Our project leverages GitLab Pages to host and serve the documentation generated by MkDocs. This setup ensures that all team members and stakeholders have access to the most current documentation relevant to the entire project.
149
+
150
+ #### How It Works
151
+
152
+ - **Unified Documentation:** We maintain a single, comprehensive documentation system that reflects the latest changes in our codebase.
153
+
154
+ - **Automated Builds:** The documentation is automatically built and updated whenever changes are made. This ensures that our documentation is always up-to-date with the latest codebase.
155
+
156
+ #### Benefits
157
+
158
+ - **Simplified Access:** Provides a single, consistent source of documentation for all stakeholders, simplifying access and reducing potential confusion.
159
+ - **Immediate Updates:** Changes to the documentation are immediately reflected upon updates, ensuring that the documentation always matches the current state of the project.
160
+
161
+ This streamlined approach to documentation allows our team to efficiently maintain and update project details, ensuring consistent and accessible information for everyone involved.
162
+
@@ -0,0 +1,137 @@
1
+
2
+ # Git Tide: Automated gitflow merge cycles with semantic versioning
3
+
4
+ `tide` simplifies [gitflow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow) processes with automatic tagging and hot-fixing and promotion.
5
+
6
+ ## Features
7
+
8
+ - Automatic cascading merging of hotfixes from stable branches into experimental ones, e.g. alpha, beta, rc (this is the "ebb tide")
9
+ - - Automated "promotion" of experimental branches forward to their next branch. e.g. alpha to beta, beta to rc, and so on. (this is the "flood tide" or "flow")
10
+ - Uses [`commitizen`](https://github.com/commitizen-tools/commitizen) to create semver tags
11
+ - Avoids merge conflicts between branches by using tags to store and query the latest version
12
+
13
+
14
+ ### Example flow
15
+
16
+ Here is an overview of the rules that `tide` applies during automation:
17
+
18
+ > NOTE: this document and repository use **develop**, **staging**, and **master** for it's gitflow branches
19
+ > but this behavior is configurable in the **pyproject.toml** and **.gitlab-ci.yml** files.
20
+
21
+ - New features should start as merge requests made against the `develop` branch (this is the default branch)
22
+ - Commits that are merged to `develop` auto-generate a tag with a beta suffix: e.g. `1.0.0b1`
23
+ - Commits that are merged to `staging` auto-generate a tag with a release candidate suffix: e.g. `1.0.0rc1`
24
+ - Hotfixes that are added to `master` automatically merge to `staging`
25
+ - Hotfixes that are added to `staging` automatically merge to `develop`
26
+ - Promoting the release candidate to a new official release can be done two ways:
27
+ - Every pipeline on the `staging` branch has a `promotion` job that can be manually run
28
+ - A scheduled pipeline can be setup to do the same
29
+ - Either way, when the `promotion` job runs it will do 3 things:
30
+ - Move the `master` branch to the tip of `staging` and create a new release, stripping off the `rc` suffix
31
+ - Move the `staging` branch to the tip of `develop` and create a new release, converting `b` to `rc`
32
+ - Restart `develop`, incrementing the minor version, e.g. from `1.1.0b4` to `1.2.0b0`.
33
+
34
+ ### Sample development cycle
35
+
36
+ Below is the Git commit graph demonstrating a sample development cycle using this project's branching strategy,
37
+ including tags and hotfix propagation.
38
+
39
+ ```plaintext
40
+ * auto-hotfix into develop: staging: add hotfix 2 - (HEAD -> develop, tag: 1.2.0rc0, tag: 1.2.0b2, staging)
41
+ |\
42
+ | * staging: add hotfix 2 - (tag: 1.1.0rc2, tag: 1.1.0, master)
43
+ * | auto-hotfix into develop: master: add hotfix - (tag: 1.2.0b1)
44
+ |\|
45
+ | * auto-hotfix into staging: master: add hotfix - (tag: 1.1.0rc1)
46
+ | |\
47
+ | | * master: add hotfix - (tag: 1.0.1)
48
+ * | | develop: add beta feature 2 - (tag: 1.2.0b0)
49
+ |/ /
50
+ * / develop: add feature 1 - (tag: 1.1.0rc0, tag: 1.1.0b0)
51
+ |/
52
+ * master: initial state - (tag: 1.0.0)
53
+
54
+ chronological tag order:
55
+ 1.2.0rc0 promoting develop to staging!
56
+ 1.1.0 promoting staging to master!
57
+ 1.2.0b2 auto-hotfix into develop: staging: add hotfix 2
58
+ 1.1.0rc2 staging: add hotfix 2
59
+ 1.2.0b1 auto-hotfix into develop: master: add hotfix
60
+ 1.1.0rc1 auto-hotfix into staging: master: add hotfix
61
+ 1.0.1 master: add hotfix
62
+ 1.2.0b0 develop: add beta feature 2
63
+ 1.1.0rc0 promoting develop to staging!
64
+ 1.1.0b0 develop: add feature 1
65
+ 1.0.0 master: initial state
66
+ ```
67
+
68
+ ## Setting up a repo
69
+
70
+ In order for `tide` to work its magic, the Gitlab repo needs to be properly configured:
71
+
72
+ - Add a `[tool].tide` section to your `pyproject.toml` file to define the gitflow branch names and prerelease version you wish to use.
73
+ ```toml
74
+ [tool.tide]
75
+ branches.beta = "develop"
76
+ branches.rc = "staging"
77
+ branches.stable = "master"
78
+ ```
79
+ - Create a [Project Access Token](https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html) with the appropriate scope for being able to push tags, changes and create cicd variables.
80
+ - Run `tide init --access-token='YOUR_ACCESS_TOKEN'`
81
+ - Copy and modify the `.gitlab-ci.yml` file ensuring the branch variables match the ones in the `pyproject.toml` (TODO: automated generation of `.gitlab-ci.yml` in `tide init`)
82
+
83
+ ## Development
84
+
85
+ ### Setting up your local development environment
86
+
87
+ - clone the repository using either https or ssh depending on your preference and then run the following commands:
88
+ ```bash
89
+ python -m venv venv
90
+ .\venv\Scripts\Activate
91
+ pip install -r requirements.txt
92
+ pre-commit install
93
+ ```
94
+
95
+ ### Local development info, tips, and tricks
96
+
97
+ `nox` is the primary interface for what we'll be doing day to day and under the hood.
98
+
99
+ Use `nox --list` to see the tasks that are available.
100
+
101
+ ### Running the unit tests
102
+
103
+ ```bash
104
+ nox -s unit_tests
105
+ ```
106
+
107
+ The primary test `tests/test_unit.py::test_dev_cycle` can be run in several modes:
108
+ - local: simulated using local repos
109
+ - remote: integration testing using real gitlab repos
110
+ - gitlab-ci-local: simulated using local repos, but using the `.gitlab-ci.yml` file for entry-point and env var control. Requires installation of https://github.com/firecow/gitlab-ci-local
111
+
112
+ Here's how you run it in remote mode:
113
+ ```bash
114
+ EXEC_MODE="remote" ACCESS_TOKEN="your-access-token-here" nox -s unit_tests -- tests/test_unit.py::test_dev_cycle -vv -s
115
+ ```
116
+
117
+ Here's how you run it using `gitlab-ci-local`:
118
+ ```bash
119
+ EXEC_MODE="gitlab-ci-local" nox -s unit_tests -- tests/test_unit.py::test_dev_cycle -vv -s
120
+ ```
121
+
122
+ ### Serving Documentation on GitLab Pages
123
+
124
+ Our project leverages GitLab Pages to host and serve the documentation generated by MkDocs. This setup ensures that all team members and stakeholders have access to the most current documentation relevant to the entire project.
125
+
126
+ #### How It Works
127
+
128
+ - **Unified Documentation:** We maintain a single, comprehensive documentation system that reflects the latest changes in our codebase.
129
+
130
+ - **Automated Builds:** The documentation is automatically built and updated whenever changes are made. This ensures that our documentation is always up-to-date with the latest codebase.
131
+
132
+ #### Benefits
133
+
134
+ - **Simplified Access:** Provides a single, consistent source of documentation for all stakeholders, simplifying access and reducing potential confusion.
135
+ - **Immediate Updates:** Changes to the documentation are immediately reflected upon updates, ensuring that the documentation always matches the current state of the project.
136
+
137
+ This streamlined approach to documentation allows our team to efficiently maintain and update project details, ensuring consistent and accessible information for everyone involved.
@@ -6,9 +6,10 @@ name = "git-tide"
6
6
  packages = [
7
7
  { include = "tide", from = "src" },
8
8
  ]
9
- version = "0.1.0"
9
+ version = "0.1.2"
10
10
  description = "CLI for automated gitflow-style branching"
11
11
  authors = ["Chad Dombrova", "Matt Collie"]
12
+ readme = "README.md"
12
13
 
13
14
  [tool.poetry.dependencies]
14
15
  commitizen = "3.16.0"
@@ -20,7 +21,7 @@ python-gitlab = { version = "4.8.0", optional = true }
20
21
  init = ["python-gitlab"]
21
22
 
22
23
  [tool.poetry.scripts]
23
- tide = "tide.core:main"
24
+ tide = "tide.cli:main"
24
25
 
25
26
  [build-system]
26
27
  requires = ["poetry-core"]
@@ -1,4 +1,4 @@
1
- from .core import main
1
+ from .cli import main
2
2
 
3
3
  if __name__ == "__main__":
4
4
  main()
@@ -0,0 +1,331 @@
1
+ from __future__ import absolute_import, print_function, annotations
2
+ import click
3
+ import time
4
+ import os
5
+ import subprocess
6
+ import re
7
+ from pathlib import Path
8
+
9
+ from .core import (
10
+ is_url,
11
+ cz,
12
+ get_tag_for_branch,
13
+ get_modified_projects,
14
+ get_projects,
15
+ load_config,
16
+ promote as _promote,
17
+ Config,
18
+ Runtime,
19
+ Backend,
20
+ GitlabBackend,
21
+ TestGitlabBackend,
22
+ GitlabRuntime,
23
+ TestGitlabRuntime,
24
+ LocalRuntime,
25
+ ENVVAR_PREFIX,
26
+ HOTFIX_MESSAGE,
27
+ )
28
+ from .gitutils import git, checkout
29
+
30
+ CONFIG: Config
31
+ CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
32
+
33
+
34
+ def set_config(config: Config) -> Config:
35
+ """
36
+ Set the global configuration object.
37
+ """
38
+ global CONFIG
39
+ CONFIG = config
40
+ return config
41
+
42
+
43
+ def get_backend(url: str | None = None) -> Backend:
44
+ """
45
+ Return a Backend corresponding to where the current python process is pushing/pulling.
46
+ """
47
+ if os.environ.get("GITLAB_CI", "false") == "true" or (url and "gitlab" in url):
48
+ return GitlabBackend(CONFIG)
49
+ else:
50
+ return TestGitlabBackend(CONFIG)
51
+
52
+
53
+ def get_runtime() -> Runtime:
54
+ """
55
+ Return a Runtime corresponding to where the current python process is *running*
56
+ """
57
+ print(os.environ.get("GITLAB_CI"))
58
+ if os.environ.get("GITLAB_CI", "false") == "true":
59
+ return GitlabRuntime(CONFIG)
60
+ # gitlab-ci-local and our unittests set this to false as an inidicator that
61
+ # we are testing gitlab, but not IN gitlab.
62
+ elif os.environ.get("GITLAB_CI") == "false":
63
+ return TestGitlabRuntime(CONFIG)
64
+ else:
65
+ return LocalRuntime(CONFIG)
66
+
67
+
68
+ @click.group(context_settings=CONTEXT_SETTINGS)
69
+ @click.option("--config", "-c", "config_path", metavar="CONFIG", type=str)
70
+ @click.option("--verbose", "-v", is_flag=True, default=False)
71
+ def cli(config_path: str, verbose: bool) -> None:
72
+ set_config(load_config(config_path, verbose))
73
+
74
+
75
+ @cli.command()
76
+ @click.option(
77
+ "--access-token",
78
+ required=True,
79
+ metavar="TOKEN",
80
+ help="Security token used to authenticate with the remote",
81
+ )
82
+ @click.option(
83
+ "--remote",
84
+ default="origin",
85
+ metavar="REMOTE",
86
+ show_default=True,
87
+ help=(
88
+ "The name of the git remote in the current git repo "
89
+ "(e.g. configured using `git remote`) or the remote URL. "
90
+ "Providing a URL implies --no-local"
91
+ ),
92
+ )
93
+ @click.option(
94
+ "--save-token/--no-save-token",
95
+ default=True,
96
+ help="Whether to save the access token into the remote as a reusable "
97
+ "variable. If this is disabled, you must configure the ACCESS_TOKEN "
98
+ "manually.",
99
+ )
100
+ @click.option(
101
+ "--init-local/--no-local",
102
+ default=True,
103
+ help="Whether to initialize the local git repo",
104
+ )
105
+ @click.option(
106
+ "--init-remote/--no-remote",
107
+ default=True,
108
+ help="Whether to initialize the remote git repo (i.e. Gitlab)",
109
+ )
110
+ def init(
111
+ access_token: str,
112
+ remote: str,
113
+ save_token: bool,
114
+ init_local: bool,
115
+ init_remote: bool,
116
+ ) -> None:
117
+ """
118
+ Initialize the current git repo and its associated Gitlab project for use with tide.
119
+
120
+ This command must be run from a git repo, and the repo must have the Gitlab project setup
121
+ as a remote, either by being cloned from it, or via `git remote add`.
122
+ """
123
+ import tempfile
124
+
125
+ if is_url(remote):
126
+ init_local = False
127
+ remote_url = remote
128
+ else:
129
+ # FIXME: print a user friendly error if we're not in a git repo.
130
+ # FIXME: handle remote not setup correctly
131
+ remote_url = git("remote", "get-url", remote, capture=True)
132
+
133
+ backend = get_backend(remote_url)
134
+
135
+ if init_local:
136
+ backend.init_local_repo(remote)
137
+ else:
138
+ # we may still need to create branches in the remote, so create a dummy clone
139
+ with tempfile.TemporaryDirectory() as tmpdir:
140
+ git("clone", f"--branch={CONFIG.stable}", remote_url, tmpdir)
141
+ pwd = os.getcwd()
142
+ try:
143
+ os.chdir(tmpdir)
144
+ backend.init_local_repo("origin")
145
+ finally:
146
+ os.chdir(pwd)
147
+
148
+ # FIXME: add pyproject.toml section? check if it exists? Hard to do automatically,
149
+ # because in a monorepo there could be many.
150
+ # FIXME: create a stub gitlab-ci.yml file if it doesn't exist?
151
+ if init_remote:
152
+ backend.init_remote_repo(remote_url, access_token, save_token)
153
+
154
+
155
+ @cli.command()
156
+ @click.option(
157
+ "--annotation",
158
+ default="automatic change detected",
159
+ show_default=True,
160
+ help="Message to pass for tag annotations.",
161
+ )
162
+ @click.option(
163
+ "--base-rev",
164
+ metavar="SHA",
165
+ help="The Git revision to compare against when identifying changed files.",
166
+ )
167
+ def autotag(annotation: str, base_rev: str | None) -> None:
168
+ """
169
+ Tag the current branch with a new version number and push the tag to the remote repository.
170
+ """
171
+ runtime = get_runtime()
172
+ branch = runtime.current_branch()
173
+ remote = runtime.get_remote()
174
+
175
+ backend = get_backend()
176
+
177
+ if not base_rev:
178
+ base_rev = runtime.get_base_rev()
179
+
180
+ projects = get_modified_projects(base_rev, verbose=CONFIG.verbose)
181
+ if projects:
182
+ for project_folder, _ in projects:
183
+ # Auto-tag
184
+ tag = get_tag_for_branch(CONFIG, remote, branch, project_folder)
185
+
186
+ # NOTE: this delay is necessary to create stable sorting of tags
187
+ # because git's time resolution is 1s (same as unix timestamp).
188
+ # https://stackoverflow.com/questions/28237043/what-is-the-resolution-of-gits-commit-date-or-author-date-timestamps
189
+ time.sleep(1.1)
190
+
191
+ click.echo(f"Creating new tag: {tag} on branch: {branch} {time.time()}")
192
+ git("tag", "-a", tag, "-m", annotation)
193
+
194
+ # FIXME: we may want to push all tags at once
195
+ click.echo(f"Pushing {tag} to remote")
196
+ backend.push(remote, tag)
197
+ else:
198
+ click.echo("No tags generated!", err=True)
199
+
200
+
201
+ @cli.command()
202
+ def hotfix() -> None:
203
+ """
204
+ Merge hotfixes from a feature branch back to upstream branches.
205
+ """
206
+ runtime = get_runtime()
207
+ branch = runtime.current_branch()
208
+ remote = runtime.get_remote()
209
+ upstream_branch = CONFIG.get_upstream_branch(branch)
210
+ if not upstream_branch:
211
+ click.echo(f"No branch upstream from {branch}. Skipping auto-merge")
212
+ return
213
+
214
+ # Auto-merge
215
+ # Record the current state
216
+ message = git("log", "--pretty=format: %s", "-1", capture=True)
217
+ # strip out previous Automerge formatting
218
+ match = re.match(
219
+ HOTFIX_MESSAGE.format(upstream_branch="[^:]+", message="(.*)$"), message
220
+ )
221
+ if match:
222
+ message = match.groups()[0]
223
+
224
+ git("checkout", "-B", f"{branch}_temp")
225
+
226
+ # Fetch the upstream branch
227
+ git("fetch", remote, upstream_branch)
228
+
229
+ checkout(remote, upstream_branch, create=True)
230
+
231
+ msg = HOTFIX_MESSAGE.format(upstream_branch=upstream_branch, message=message)
232
+ click.echo(msg)
233
+
234
+ try:
235
+ git("merge", f"{branch}_temp", "-m", msg)
236
+ except subprocess.CalledProcessError:
237
+ click.echo("Conflicts:", err=True)
238
+ git("diff", "--name-only", "--diff-filter=U")
239
+ raise
240
+
241
+ # this will trigger a full pipeline for upstream_branch, and potentially another auto-merge
242
+ click.echo(f"Pushing {upstream_branch} to {remote}")
243
+ variables = {
244
+ f"{ENVVAR_PREFIX}_AUTOTAG_ANNOTATION": msg,
245
+ }
246
+ get_backend().push(remote, upstream_branch, variables=variables)
247
+
248
+
249
+ @cli.command()
250
+ def promote() -> None:
251
+ """
252
+ Promote changes through the branch hierarchy.
253
+
254
+ e.g. from alpha -> beta -> rc -> stable.
255
+ """
256
+ backend = get_backend()
257
+ runtime = get_runtime()
258
+ remote = runtime.get_remote()
259
+ if CONFIG.verbose:
260
+ click.echo(f"remote = {remote}")
261
+ _promote(CONFIG, backend, runtime)
262
+
263
+
264
+ @cli.command
265
+ @click.option("--modified", "-m", is_flag=True, default=False)
266
+ # FIXME: add output format
267
+ # FIXME: add option to write to file
268
+ def projects(modified: bool) -> None:
269
+ """
270
+ List project paths within the repo.
271
+
272
+ A project is a folder with a pyproject.toml file with a `tool.commitizen` section.
273
+ """
274
+ if modified:
275
+ runtime = get_runtime()
276
+ projects_ = get_modified_projects(
277
+ runtime.get_base_rev(), verbose=CONFIG.verbose
278
+ )
279
+ else:
280
+ projects_ = get_projects()
281
+
282
+ for project_dir, project_name in projects_:
283
+ if project_name is None:
284
+ project_name = "[unset]"
285
+ click.echo(f"{project_name} = {project_dir}")
286
+
287
+
288
+ @cli.command("get-tag")
289
+ @click.option(
290
+ "--path",
291
+ default=".",
292
+ type=click.Path(exists=True, file_okay=False),
293
+ show_default=True,
294
+ help="Folder within the repository. A pyproject.toml should reside in the "
295
+ "specified directory",
296
+ )
297
+ @click.option(
298
+ "--branch",
299
+ default=None,
300
+ help=(
301
+ "The release branch to use to determine the next version. "
302
+ "Defaults to the current branch."
303
+ ),
304
+ )
305
+ @click.option(
306
+ "--remote",
307
+ default="origin",
308
+ show_default=True,
309
+ help="The git remote to use to when determining the next version.",
310
+ )
311
+ @click.option("--next", "-n", is_flag=True, default=False)
312
+ def get_tag(path: str, branch: str | None, remote: str, next: bool) -> None:
313
+ """
314
+ Get the next tag.
315
+ """
316
+ if next:
317
+ if branch is None:
318
+ runtime = get_runtime()
319
+ branch = runtime.current_branch()
320
+ click.echo(get_tag_for_branch(CONFIG, remote, branch, Path(path)))
321
+ else:
322
+ click.echo(cz("version", "--project", folder=path))
323
+
324
+
325
+ def main() -> None:
326
+ import shutil
327
+
328
+ return cli(
329
+ auto_envvar_prefix=ENVVAR_PREFIX,
330
+ max_content_width=shutil.get_terminal_size().columns,
331
+ )
@@ -3,7 +3,6 @@ from __future__ import absolute_import, print_function, annotations
3
3
  import os
4
4
  import re
5
5
  import subprocess
6
- import time
7
6
  import json
8
7
 
9
8
  import click
@@ -20,7 +19,7 @@ from abc import abstractmethod
20
19
  from enum import Enum
21
20
  from typing import TYPE_CHECKING, cast
22
21
 
23
- from .gitutils import git, get_tags, branch_exists, checkout, current_rev, join, GitRepo
22
+ from .gitutils import git, checkout, get_tags, branch_exists, join, current_rev, GitRepo
24
23
 
25
24
  if TYPE_CHECKING:
26
25
  import gitlab.v4.objects
@@ -30,9 +29,7 @@ TOOL_NAME = "tide"
30
29
  ENVVAR_PREFIX = TOOL_NAME.upper()
31
30
  PROMOTION_BASE_MSG = "promotion base"
32
31
  HERE = os.path.dirname(__file__)
33
- CONFIG: Config
34
32
  GITLAB_REMOTE = "gitlab_origin"
35
- CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
36
33
 
37
34
  # FIXME: add these to config file
38
35
  HOTFIX_MESSAGE = "auto-hotfix into {upstream_branch}: {message}"
@@ -101,15 +98,6 @@ def load_config(path: str | None = None, verbose: bool = False) -> Config:
101
98
  return config
102
99
 
103
100
 
104
- def set_config(config: Config) -> Config:
105
- """
106
- Set the global configuration object.
107
- """
108
- global CONFIG
109
- CONFIG = config
110
- return config
111
-
112
-
113
101
  @dataclass
114
102
  class Config:
115
103
  stable: str = "master"
@@ -122,6 +110,29 @@ class Config:
122
110
  branch_to_release_id: dict[str, ReleaseID] = field(default_factory=dict)
123
111
  verbose: bool = False
124
112
 
113
+ def get_upstream_branch(self, branch: str) -> str | None:
114
+ """
115
+ Determine the upstream branch for a given branch name based on configuration.
116
+
117
+ Args:
118
+ branch: The name of the branch for which to find the upstream branch.
119
+
120
+ Returns:
121
+ The name of the upstream branch, or None if there is no upstream branch.
122
+
123
+ Raises:
124
+ ClickException: If the branch is not found in the configuration.
125
+ """
126
+ try:
127
+ index = self.branches.index(branch)
128
+ except ValueError:
129
+ raise click.ClickException(f"Invalid branch: {branch}")
130
+
131
+ if index > 0:
132
+ return self.branches[index - 1]
133
+ else:
134
+ return None
135
+
125
136
  def most_experimental_branch(self) -> str | None:
126
137
  """
127
138
  Return the most experimental branch.
@@ -134,36 +145,14 @@ class Config:
134
145
  return self.branches[0]
135
146
 
136
147
 
137
- def get_backend(url: str | None = None) -> Backend:
138
- """
139
- Return a Backend corresponding to where the current python process is pushing/pulling.
140
- """
141
- if os.environ.get("GITLAB_CI", "false") == "true" or (url and "gitlab" in url):
142
- return GitlabBackend()
143
- else:
144
- return TestGitlabBackend()
145
-
146
-
147
- def get_runtime() -> Runtime:
148
- """
149
- Return a Runtime corresponding to where the current python process is *running*
150
- """
151
- print(os.environ.get("GITLAB_CI"))
152
- if os.environ.get("GITLAB_CI", "false") == "true":
153
- return GitlabRuntime()
154
- # gitlab-ci-local and our unittests set this to false as an inidicator that
155
- # we are testing gitlab, but not IN gitlab.
156
- elif os.environ.get("GITLAB_CI") == "false":
157
- return TestGitlabRuntime()
158
- else:
159
- return LocalRuntime()
160
-
161
-
162
148
  class Runtime:
163
149
  """
164
150
  Interact with a git repo that is local to the current process
165
151
  """
166
152
 
153
+ def __init__(self, config: Config):
154
+ self.config = config
155
+
167
156
  @abstractmethod
168
157
  def current_branch(self) -> str:
169
158
  """
@@ -206,6 +195,9 @@ class Backend:
206
195
  Interact with a remote git backend.
207
196
  """
208
197
 
198
+ def __init__(self, config: Config):
199
+ self.config = config
200
+
209
201
  def push(
210
202
  self, *args: str, variables: dict[str, str] | None = None, skip_ci: bool = False
211
203
  ) -> None:
@@ -230,24 +222,24 @@ class Backend:
230
222
  remote_name: name of the git remote, used to query the url
231
223
  """
232
224
  # initialize the local repo
233
- git("fetch", remote_name, quiet=CONFIG.verbose)
225
+ git("fetch", remote_name, quiet=self.config.verbose)
234
226
 
235
- if CONFIG.verbose:
227
+ if self.config.verbose:
236
228
  git("branch", "-la")
237
229
  git("remote", "-v")
238
230
 
239
231
  # setup branches.
240
232
  # loop from stable to pre-release branches, bc we set all release_id branches to
241
233
  # the location of stable
242
- for branch in reversed(CONFIG.branches):
234
+ for branch in reversed(self.config.branches):
243
235
  if branch_exists(branch):
244
- if branch != CONFIG.stable:
236
+ if branch != self.config.stable:
245
237
  click.echo(
246
238
  f"{branch} already exists. This can potentially cause problems",
247
239
  err=True,
248
240
  )
249
241
  else:
250
- git("branch", "-f", branch, CONFIG.stable)
242
+ git("branch", "-f", branch, self.config.stable)
251
243
 
252
244
  remote_branch = f"{remote_name}/{branch}"
253
245
  if branch_exists(remote_branch):
@@ -380,7 +372,7 @@ class GitlabBackend(Backend):
380
372
  # FIXME: validate that ACCESS_TOKEN has been set at the project or group level
381
373
  pass
382
374
 
383
- for branch in CONFIG.branches:
375
+ for branch in self.config.branches:
384
376
  try:
385
377
  p_branch = project.protectedbranches.get(branch)
386
378
  except gitlab.exceptions.GitlabGetError:
@@ -397,7 +389,7 @@ class GitlabBackend(Backend):
397
389
  p_branch.save()
398
390
  click.echo("Setup protected branches")
399
391
 
400
- default_branch = CONFIG.most_experimental_branch() or CONFIG.stable
392
+ default_branch = self.config.most_experimental_branch() or self.config.stable
401
393
  gl.projects.update(project.id, {"default_branch": default_branch})
402
394
 
403
395
  if not self._find_promote_job(project):
@@ -405,7 +397,7 @@ class GitlabBackend(Backend):
405
397
  # commit pushed
406
398
  schedule = project.pipelineschedules.create(
407
399
  {
408
- "ref": CONFIG.stable,
400
+ "ref": self.config.stable,
409
401
  "description": self.PROMOTION_SCHEDULED_JOB_NAME,
410
402
  "cron": "6 6 * * 4",
411
403
  "active": False,
@@ -477,41 +469,15 @@ def cz(*args: str, folder: str | Path | None = None) -> str:
477
469
  """
478
470
  Run commitizen in a subprocess.
479
471
  """
480
- if CONFIG.verbose:
481
- click.echo(f"running {args} in {folder}")
482
472
  output = subprocess.check_output(
483
473
  ["cz"] + list(args),
484
474
  text=True,
485
475
  cwd=folder,
486
476
  )
487
- return output
488
-
489
-
490
- def get_upstream_branch(branch: str) -> str | None:
491
- """
492
- Determine the upstream branch for a given branch name based on configuration.
477
+ return output.strip()
493
478
 
494
- Args:
495
- branch: The name of the branch for which to find the upstream branch.
496
479
 
497
- Returns:
498
- The name of the upstream branch, or None if there is no upstream branch.
499
-
500
- Raises:
501
- ClickException: If the branch is not found in the configuration.
502
- """
503
- try:
504
- index = CONFIG.branches.index(branch)
505
- except ValueError:
506
- raise click.ClickException(f"Invalid branch: {branch}")
507
-
508
- if index > 0:
509
- return CONFIG.branches[index - 1]
510
- else:
511
- return None
512
-
513
-
514
- def is_pending_bump(remote: str, branch: str, folder: Path) -> bool:
480
+ def is_pending_bump(config: Config, remote: str, branch: str, folder: Path) -> bool:
515
481
  """
516
482
  Return whether the given branch and folder combination are awaiting a minor bump.
517
483
 
@@ -526,13 +492,13 @@ def is_pending_bump(remote: str, branch: str, folder: Path) -> bool:
526
492
  from commitizen.config import read_cfg
527
493
  from commitizen.providers import ScmProvider
528
494
 
529
- if branch != CONFIG.most_experimental_branch():
495
+ if branch != config.most_experimental_branch():
530
496
  return False
531
497
 
532
498
  # Find the closest promotion note to the current branch
533
499
  promotion_rev = get_promotion_marker(remote)
534
500
  if promotion_rev is None:
535
- if CONFIG.verbose:
501
+ if config.verbose:
536
502
  click.echo("No promote marker found", err=True)
537
503
  return True
538
504
 
@@ -543,7 +509,7 @@ def is_pending_bump(remote: str, branch: str, folder: Path) -> bool:
543
509
  # config file for us (note: it supports more than just pyproject.toml).
544
510
  provider = ScmProvider(read_cfg())
545
511
  matcher = provider._tag_format_matcher()
546
- if CONFIG.verbose:
512
+ if config.verbose:
547
513
  click.echo(f"Found promotion base rev: {promotion_rev}")
548
514
  # List any tags for this project folder between this branch and the promotion note
549
515
  all_tags = get_tags(end_rev=promotion_rev)
@@ -593,7 +559,7 @@ def set_promotion_marker(remote: str, branch: str) -> None:
593
559
  git("push", remote, "refs/notes/*")
594
560
 
595
561
 
596
- def get_tag_for_branch(remote: str, branch: str, folder: Path) -> str:
562
+ def get_tag_for_branch(config: Config, remote: str, branch: str, folder: Path) -> str:
597
563
  """
598
564
  Determine the appropriate new tag for a given branch based on the latest changes.
599
565
 
@@ -612,17 +578,17 @@ def get_tag_for_branch(remote: str, branch: str, folder: Path) -> str:
612
578
  RuntimeError: If the command does not generate an output or fails to determine the tag.
613
579
  """
614
580
  try:
615
- release_id = CONFIG.branch_to_release_id[branch]
581
+ release_id = config.branch_to_release_id[branch]
616
582
  except KeyError:
617
583
  raise click.ClickException(
618
584
  f"{branch} is not a valid release branch. "
619
- f"Must be one of {', '.join(CONFIG.branches)}"
585
+ f"Must be one of {', '.join(config.branches)}"
620
586
  )
621
587
 
622
588
  increment = "patch"
623
589
 
624
590
  # Only apply minor increment the most experimental branch
625
- if is_pending_bump(remote, branch, folder):
591
+ if is_pending_bump(config, remote, branch, folder):
626
592
  increment = "minor"
627
593
 
628
594
  args = [f"--increment={increment}"]
@@ -667,7 +633,9 @@ def get_projects() -> list[tuple[Path, str | None]]:
667
633
  return sorted(results)
668
634
 
669
635
 
670
- def get_modified_projects(base_rev: str | None = None) -> list[tuple[Path, str | None]]:
636
+ def get_modified_projects(
637
+ base_rev: str, verbose: bool = False
638
+ ) -> list[tuple[Path, str | None]]:
671
639
  """Get the list of projects with changes files.
672
640
 
673
641
  A project is defined as a folder with a pyproject.toml file with a `tool.commitizen` section.
@@ -675,15 +643,11 @@ def get_modified_projects(base_rev: str | None = None) -> list[tuple[Path, str |
675
643
  Args:
676
644
  base_rev: The Git revision to compare against when identifying changed files
677
645
  """
678
- if not base_rev:
679
- runtime = get_runtime()
680
- base_rev = runtime.get_base_rev()
681
-
682
646
  # Compare the current commit with the branch you want to merge with:
683
647
  # FIXME: do not included deleted files
684
648
  output = git("diff-tree", "--name-only", "-r", base_rev, "HEAD", capture=True)
685
649
  all_files = output.splitlines()
686
- if CONFIG.verbose:
650
+ if verbose:
687
651
  if all_files:
688
652
  click.echo(f"Modified files between {base_rev} and HEAD:")
689
653
  for path in all_files:
@@ -704,195 +668,14 @@ def get_modified_projects(base_rev: str | None = None) -> list[tuple[Path, str |
704
668
  return [(project_dir, projects[project_dir]) for project_dir in sorted(results)]
705
669
 
706
670
 
707
- @click.group(context_settings=CONTEXT_SETTINGS)
708
- @click.option("--config", "-c", "config_path", metavar="CONFIG", type=str)
709
- @click.option("--verbose", "-v", is_flag=True, default=False)
710
- def cli(config_path: str, verbose: bool) -> None:
711
- set_config(load_config(config_path, verbose))
712
-
713
-
714
- @cli.command()
715
- @click.option(
716
- "--access-token",
717
- required=True,
718
- metavar="TOKEN",
719
- help="Security token used to authenticate with the remote",
720
- )
721
- @click.option(
722
- "--remote",
723
- default="origin",
724
- metavar="REMOTE",
725
- show_default=True,
726
- help=(
727
- "The name of the git remote in the current git repo "
728
- "(e.g. configured using `git remote`) or the remote URL. "
729
- "Providing a URL implies --no-local"
730
- ),
731
- )
732
- @click.option(
733
- "--save-token/--no-save-token",
734
- default=True,
735
- help="Whether to save the access token into the remote as a reusable "
736
- "variable. If this is disabled, you must configure the ACCESS_TOKEN "
737
- "manually.",
738
- )
739
- @click.option(
740
- "--init-local/--no-local",
741
- default=True,
742
- help="Whether to initialize the local git repo",
743
- )
744
- @click.option(
745
- "--init-remote/--no-remote",
746
- default=True,
747
- help="Whether to initialize the remote git repo (i.e. Gitlab)",
748
- )
749
- def init(
750
- access_token: str,
751
- remote: str,
752
- save_token: bool,
753
- init_local: bool,
754
- init_remote: bool,
755
- ) -> None:
756
- """
757
- Initialize the current git repo and its associated Gitlab project for use with tide.
758
-
759
- This command must be run from a git repo, and the repo must have the Gitlab project setup
760
- as a remote, either by being cloned from it, or via `git remote add`.
761
- """
762
- import tempfile
763
-
764
- if is_url(remote):
765
- init_local = False
766
- remote_url = remote
767
- else:
768
- # FIXME: print a user friendly error if we're not in a git repo.
769
- # FIXME: handle remote not setup correctly
770
- remote_url = git("remote", "get-url", remote, capture=True)
771
-
772
- backend = get_backend(remote_url)
773
-
774
- if init_local:
775
- backend.init_local_repo(remote)
776
- else:
777
- # we may still need to create branches in the remote, so create a dummy clone
778
- with tempfile.TemporaryDirectory() as tmpdir:
779
- git("clone", f"--branch={CONFIG.stable}", remote_url, tmpdir)
780
- pwd = os.getcwd()
781
- try:
782
- os.chdir(tmpdir)
783
- backend.init_local_repo("origin")
784
- finally:
785
- os.chdir(pwd)
786
-
787
- # FIXME: add pyproject.toml section? check if it exists? Hard to do automatically,
788
- # because in a monorepo there could be many.
789
- # FIXME: create a stub gitlab-ci.yml file if it doesn't exist?
790
- if init_remote:
791
- backend.init_remote_repo(remote_url, access_token, save_token)
792
-
793
-
794
- @cli.command()
795
- @click.option(
796
- "--annotation",
797
- default="automatic change detected",
798
- show_default=True,
799
- help="Message to pass for tag annotations.",
800
- )
801
- @click.option(
802
- "--base-rev",
803
- metavar="SHA",
804
- help="The Git revision to compare against when identifying changed files.",
805
- )
806
- def autotag(annotation: str, base_rev: str | None) -> None:
807
- """
808
- Tag the current branch with a new version number and push the tag to the remote repository.
809
- """
810
- runtime = get_runtime()
811
- branch = runtime.current_branch()
812
- remote = runtime.get_remote()
813
-
814
- backend = get_backend()
815
-
816
- projects = get_modified_projects(base_rev)
817
- if projects:
818
- for project_folder, _ in projects:
819
- # Auto-tag
820
- tag = get_tag_for_branch(remote, branch, project_folder)
821
-
822
- # NOTE: this delay is necessary to create stable sorting of tags
823
- # because git's time resolution is 1s (same as unix timestamp).
824
- # https://stackoverflow.com/questions/28237043/what-is-the-resolution-of-gits-commit-date-or-author-date-timestamps
825
- time.sleep(1.1)
826
-
827
- click.echo(f"Creating new tag: {tag} on branch: {branch} {time.time()}")
828
- git("tag", "-a", tag, "-m", annotation)
829
-
830
- # FIXME: we may want to push all tags at once
831
- click.echo(f"Pushing {tag} to remote")
832
- backend.push(remote, tag)
833
- else:
834
- click.echo("No tags generated!", err=True)
835
-
836
-
837
- @cli.command()
838
- def hotfix() -> None:
839
- """
840
- Merge hotfixes from a feature branch back to upstream branches.
841
- """
842
- runtime = get_runtime()
843
- branch = runtime.current_branch()
844
- remote = runtime.get_remote()
845
- upstream_branch = get_upstream_branch(branch)
846
- if not upstream_branch:
847
- click.echo(f"No branch upstream from {branch}. Skipping auto-merge")
848
- return
849
-
850
- # Auto-merge
851
- # Record the current state
852
- message = git("log", "--pretty=format: %s", "-1", capture=True)
853
- # strip out previous Automerge formatting
854
- match = re.match(
855
- HOTFIX_MESSAGE.format(upstream_branch="[^:]+", message="(.*)$"), message
856
- )
857
- if match:
858
- message = match.groups()[0]
859
-
860
- git("checkout", "-B", f"{branch}_temp")
861
-
862
- # Fetch the upstream branch
863
- git("fetch", remote, upstream_branch)
864
-
865
- checkout(remote, upstream_branch, create=True)
866
-
867
- msg = HOTFIX_MESSAGE.format(upstream_branch=upstream_branch, message=message)
868
- click.echo(msg)
869
-
870
- try:
871
- git("merge", f"{branch}_temp", "-m", msg)
872
- except subprocess.CalledProcessError:
873
- click.echo("Conflicts:", err=True)
874
- git("diff", "--name-only", "--diff-filter=U")
875
- raise
876
-
877
- # this will trigger a full pipeline for upstream_branch, and potentially another auto-merge
878
- click.echo(f"Pushing {upstream_branch} to {remote}")
879
- variables = {
880
- f"{ENVVAR_PREFIX}_AUTOTAG_ANNOTATION": msg,
881
- }
882
- get_backend().push(remote, upstream_branch, variables=variables)
883
-
884
-
885
- @cli.command()
886
- def promote() -> None:
671
+ def promote(config: Config, backend: Backend, runtime: Runtime) -> None:
887
672
  """
888
673
  Promote changes through the branch hierarchy.
889
674
 
890
675
  e.g. from alpha -> beta -> rc -> stable.
891
676
  """
892
- backend = get_backend()
893
- runtime = get_runtime()
894
677
  remote = runtime.get_remote()
895
- if CONFIG.verbose:
678
+ if config.verbose:
896
679
  click.echo(f"remote = {remote}")
897
680
 
898
681
  local_output = []
@@ -905,8 +688,8 @@ def promote() -> None:
905
688
 
906
689
  The branch is left checked out.
907
690
  """
908
- upstream_branch = get_upstream_branch(branch)
909
- release_id = CONFIG.branch_to_release_id[branch]
691
+ upstream_branch = config.get_upstream_branch(branch)
692
+ release_id = config.branch_to_release_id[branch]
910
693
  log_msg = log_msg_template.format(
911
694
  branch=branch, upstream_branch=upstream_branch, release_id=release_id.value
912
695
  )
@@ -950,10 +733,10 @@ def promote() -> None:
950
733
  # Promotion time!
951
734
 
952
735
  # loop from stable to most experimental
953
- for branch in reversed(CONFIG.branches):
736
+ for branch in reversed(config.branches):
954
737
  # It doesn't matter what the active branch is when promote is run: the next thing
955
738
  # that we do is checkout CONFIG.stable.
956
- if branch == CONFIG.most_experimental_branch():
739
+ if branch == config.most_experimental_branch():
957
740
  msg = PROMOTION_CYCLE_START_MESSAGE
958
741
  else:
959
742
  msg = PROMOTION_MESSAGE
@@ -968,70 +751,6 @@ def promote() -> None:
968
751
 
969
752
  # Note: we do not make a tag on our cycle-start branch at this time. Instead, we wait
970
753
  # for the first commit on the branch to do so.
971
- experimental_branch = CONFIG.most_experimental_branch()
754
+ experimental_branch = config.most_experimental_branch()
972
755
  if experimental_branch:
973
756
  set_promotion_marker(remote, experimental_branch)
974
-
975
-
976
- @cli.command
977
- @click.option("--modified", "-m", is_flag=True, default=False)
978
- # FIXME: add output format
979
- # FIXME: add option to write to file
980
- def projects(modified: bool) -> None:
981
- """
982
- List project paths within the repo.
983
-
984
- A project is a folder with a pyproject.toml file with a `tool.commitizen` section.
985
- """
986
-
987
- if modified:
988
- projects_ = get_modified_projects()
989
- else:
990
- projects_ = get_projects()
991
-
992
- for project_dir, project_name in projects_:
993
- if project_name is None:
994
- project_name = "[unset]"
995
- click.echo(f"{project_name} = {project_dir}")
996
-
997
-
998
- @cli.command(name="next-version")
999
- @click.option(
1000
- "--path",
1001
- default=".",
1002
- type=click.Path(exists=True, file_okay=False),
1003
- show_default=True,
1004
- help="Folder within the repository. A pyproject.toml should reside in the "
1005
- "specified directory",
1006
- )
1007
- @click.option(
1008
- "--branch",
1009
- default=None,
1010
- help=(
1011
- "The release branch to use to determine the next version. "
1012
- "Defaults to the current branch."
1013
- ),
1014
- )
1015
- @click.option(
1016
- "--origin",
1017
- default="origin",
1018
- show_default=True,
1019
- help="The git remote to use to when determining the next version.",
1020
- )
1021
- def next_tag(path: str, branch: str | None, remote: str) -> None:
1022
- """
1023
- Get the next tag.
1024
- """
1025
- if branch is None:
1026
- runtime = get_runtime()
1027
- branch = runtime.current_branch()
1028
- click.echo(get_tag_for_branch(remote, branch, Path(path)))
1029
-
1030
-
1031
- def main() -> None:
1032
- import shutil
1033
-
1034
- return cli(
1035
- auto_envvar_prefix=ENVVAR_PREFIX,
1036
- max_content_width=shutil.get_terminal_size().columns,
1037
- )
git_tide-0.1.0/PKG-INFO DELETED
@@ -1,22 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: git-tide
3
- Version: 0.1.0
4
- Summary: CLI for automated gitflow-style branching
5
- Author: Chad Dombrova
6
- Classifier: Programming Language :: Python :: 2
7
- Classifier: Programming Language :: Python :: 2.7
8
- Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.4
10
- Classifier: Programming Language :: Python :: 3.5
11
- Classifier: Programming Language :: Python :: 3.6
12
- Classifier: Programming Language :: Python :: 3.7
13
- Classifier: Programming Language :: Python :: 3.8
14
- Classifier: Programming Language :: Python :: 3.9
15
- Classifier: Programming Language :: Python :: 3.10
16
- Classifier: Programming Language :: Python :: 3.11
17
- Classifier: Programming Language :: Python :: 3.12
18
- Provides-Extra: init
19
- Requires-Dist: click (==8.1.7)
20
- Requires-Dist: commitizen (==3.16.0)
21
- Requires-Dist: python-gitlab (==4.8.0) ; extra == "init"
22
- Requires-Dist: tomli (>=2.0.1,<3.0.0) ; python_version < "3.11"
File without changes
File without changes