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.
- git_tide-0.1.2/PKG-INFO +162 -0
- git_tide-0.1.2/README.md +137 -0
- {git_tide-0.1.0 → git_tide-0.1.2}/pyproject.toml +3 -2
- {git_tide-0.1.0 → git_tide-0.1.2}/src/tide/__main__.py +1 -1
- git_tide-0.1.2/src/tide/cli.py +331 -0
- {git_tide-0.1.0 → git_tide-0.1.2}/src/tide/core.py +58 -339
- git_tide-0.1.0/PKG-INFO +0 -22
- {git_tide-0.1.0 → git_tide-0.1.2}/src/tide/__init__.py +0 -0
- {git_tide-0.1.0 → git_tide-0.1.2}/src/tide/gitutils.py +0 -0
git_tide-0.1.2/PKG-INFO
ADDED
|
@@ -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
|
+
|
git_tide-0.1.2/README.md
ADDED
|
@@ -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.
|
|
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.
|
|
24
|
+
tide = "tide.cli:main"
|
|
24
25
|
|
|
25
26
|
[build-system]
|
|
26
27
|
requires = ["poetry-core"]
|
|
@@ -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,
|
|
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=
|
|
225
|
+
git("fetch", remote_name, quiet=self.config.verbose)
|
|
234
226
|
|
|
235
|
-
if
|
|
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(
|
|
234
|
+
for branch in reversed(self.config.branches):
|
|
243
235
|
if branch_exists(branch):
|
|
244
|
-
if branch !=
|
|
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,
|
|
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
|
|
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 =
|
|
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":
|
|
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
|
-
|
|
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 !=
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
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 ==
|
|
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 =
|
|
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
|