gitflow-manager 1.1.4__tar.gz → 2.0.0__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.
- {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/PKG-INFO +6 -1
- gitflow_manager-2.0.0/gitflow_manager/__init__.py +1 -0
- {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager/git_hosts.py +165 -68
- {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager/gitflow_manager.py +69 -43
- {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager/start.py +44 -23
- {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager.egg-info/PKG-INFO +6 -1
- {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager.egg-info/requires.txt +6 -0
- {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/pyproject.toml +2 -2
- gitflow_manager-1.1.4/gitflow_manager/__init__.py +0 -1
- {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/README.md +0 -0
- {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager/changelog.py +0 -0
- {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager/errors.py +0 -0
- {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager/file_utils.py +0 -0
- {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager/run.py +0 -0
- {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager.egg-info/SOURCES.txt +0 -0
- {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager.egg-info/dependency_links.txt +0 -0
- {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager.egg-info/entry_points.txt +0 -0
- {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager.egg-info/top_level.txt +0 -0
- {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitflow_manager
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: This package contains the Anacision Git Manager.
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -9,6 +9,11 @@ Requires-Dist: pystache>=0.6.0
|
|
|
9
9
|
Requires-Dist: pyyaml>=6.0.2
|
|
10
10
|
Requires-Dist: requests>=2.27.1
|
|
11
11
|
Requires-Dist: tomlkit>=0.13.3
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pre-commit>=4.0.1; extra == "dev"
|
|
14
|
+
Requires-Dist: ruff==0.6.9; extra == "dev"
|
|
15
|
+
Requires-Dist: setuptools>=75.3.0; extra == "dev"
|
|
16
|
+
Requires-Dist: twine>=6.0.1; extra == "dev"
|
|
12
17
|
|
|
13
18
|
# Anacision Gitflow Manager
|
|
14
19
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__="2.0.0"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import base64
|
|
1
2
|
import json
|
|
2
3
|
import warnings
|
|
3
4
|
from abc import ABC, abstractmethod
|
|
@@ -52,7 +53,13 @@ class GitHost(ABC):
|
|
|
52
53
|
pass
|
|
53
54
|
|
|
54
55
|
@abstractmethod
|
|
55
|
-
def create_merge_request(
|
|
56
|
+
def create_merge_request(
|
|
57
|
+
self,
|
|
58
|
+
source_branch: str,
|
|
59
|
+
target_branch: str,
|
|
60
|
+
description: str,
|
|
61
|
+
reviewer_ids: list[int] | None = None,
|
|
62
|
+
) -> None:
|
|
56
63
|
"""Create a merge request between branches."""
|
|
57
64
|
pass
|
|
58
65
|
|
|
@@ -115,6 +122,39 @@ class GitHost(ABC):
|
|
|
115
122
|
"""
|
|
116
123
|
return self.git_obj.execute(["git", "rev-parse", "--abbrev-ref", "HEAD"])
|
|
117
124
|
|
|
125
|
+
def _build_basic_auth_header(self, username: str) -> str:
|
|
126
|
+
"""
|
|
127
|
+
Build an HTTP Basic auth header for Git-over-HTTPS operations.
|
|
128
|
+
|
|
129
|
+
:param username: Username part for HTTP Basic authentication
|
|
130
|
+
:return: Authorization header value
|
|
131
|
+
"""
|
|
132
|
+
raw_auth = f"{username}:{self.token}".encode("utf-8")
|
|
133
|
+
encoded_auth = base64.b64encode(raw_auth).decode("ascii")
|
|
134
|
+
return f"Authorization: Basic {encoded_auth}"
|
|
135
|
+
|
|
136
|
+
def _execute_git_with_https_auth(self, git_args: list[str], https_url: str, username: str) -> str:
|
|
137
|
+
"""
|
|
138
|
+
Execute a git command against origin using an in-memory HTTPS URL and auth header.
|
|
139
|
+
|
|
140
|
+
:param git_args: Arguments after the git executable
|
|
141
|
+
:param https_url: HTTPS repository URL for origin
|
|
142
|
+
:param username: Username part for HTTP Basic authentication
|
|
143
|
+
:return: Command output
|
|
144
|
+
"""
|
|
145
|
+
auth_header = self._build_basic_auth_header(username=username)
|
|
146
|
+
command = [
|
|
147
|
+
"git",
|
|
148
|
+
"-c",
|
|
149
|
+
f"remote.origin.url={https_url}",
|
|
150
|
+
"-c",
|
|
151
|
+
f"remote.origin.pushurl={https_url}",
|
|
152
|
+
"-c",
|
|
153
|
+
f"http.extraHeader={auth_header}",
|
|
154
|
+
*git_args,
|
|
155
|
+
]
|
|
156
|
+
return self.git_obj.execute(command)
|
|
157
|
+
|
|
118
158
|
|
|
119
159
|
class BitBucket(GitHost):
|
|
120
160
|
"""
|
|
@@ -145,52 +185,40 @@ class BitBucket(GitHost):
|
|
|
145
185
|
info = self._create_request(
|
|
146
186
|
f"{self.git_host_rest_url}/api/1.0/projects/{self.project_id}/repos/{self.repo_id}"
|
|
147
187
|
).json()
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
# set origin temporarily to https url
|
|
152
|
-
url = url.replace("https://", f"https://oauth2:{self.token}@")
|
|
153
|
-
self.git_obj.execute(["git", "remote", "set-url", "origin", url])
|
|
154
|
-
# necessary as some configs have an extra push url (and we cannot check if it exists...)
|
|
155
|
-
self.git_obj.execute(["git", "remote", "set-url", "--push", "origin", url])
|
|
156
|
-
else:
|
|
157
|
-
ssh_url = False
|
|
188
|
+
clone_links = info["links"]["clone"]
|
|
189
|
+
https_link = next((l for l in clone_links if l["name"] in {"http","https"}), clone_links[0])
|
|
190
|
+
url = https_link["href"]
|
|
158
191
|
try:
|
|
159
192
|
if method == "push":
|
|
160
|
-
self.
|
|
193
|
+
self._execute_git_with_https_auth(
|
|
194
|
+
git_args=["push", "--set-upstream", "origin", branch],
|
|
195
|
+
https_url=url,
|
|
196
|
+
username="oauth2",
|
|
197
|
+
)
|
|
161
198
|
if tag is not None:
|
|
162
|
-
self.
|
|
199
|
+
self._execute_git_with_https_auth(
|
|
200
|
+
git_args=["push", "origin", f"refs/tags/{tag}"],
|
|
201
|
+
https_url=url,
|
|
202
|
+
username="oauth2",
|
|
203
|
+
)
|
|
163
204
|
|
|
164
205
|
else:
|
|
165
|
-
self.
|
|
206
|
+
self._execute_git_with_https_auth(
|
|
207
|
+
git_args=["pull", "origin", branch],
|
|
208
|
+
https_url=url,
|
|
209
|
+
username="oauth2",
|
|
210
|
+
)
|
|
166
211
|
except GitCommandError as e:
|
|
167
212
|
raise Exception(
|
|
168
213
|
f"Trying to communicate via https failed due to:\n{str(e)}\n\n" f"Please pull/push manually!!!"
|
|
169
214
|
)
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
"git",
|
|
178
|
-
"remote",
|
|
179
|
-
"set-url",
|
|
180
|
-
"origin",
|
|
181
|
-
[link["href"] for link in info["links"]["clone"] if link["name"] == "ssh"][0],
|
|
182
|
-
]
|
|
183
|
-
)
|
|
184
|
-
self.git_obj.execute(
|
|
185
|
-
[
|
|
186
|
-
"git",
|
|
187
|
-
"remote",
|
|
188
|
-
"set-url",
|
|
189
|
-
"--push",
|
|
190
|
-
"origin",
|
|
191
|
-
[link["href"] for link in info["links"]["clone"] if link["name"] == "ssh"][0],
|
|
192
|
-
]
|
|
193
|
-
)
|
|
215
|
+
|
|
216
|
+
self._execute_git_with_https_auth(
|
|
217
|
+
git_args=["fetch", "origin"],
|
|
218
|
+
https_url=url,
|
|
219
|
+
username="oauth2",
|
|
220
|
+
)
|
|
221
|
+
self.git_obj.execute(["git", "branch", "--set-upstream-to", f"origin/{branch}"])
|
|
194
222
|
|
|
195
223
|
def git_remote(self, method, branch="HEAD", tag=None):
|
|
196
224
|
"""
|
|
@@ -261,7 +289,7 @@ class BitBucket(GitHost):
|
|
|
261
289
|
except Exception as e:
|
|
262
290
|
return f"Could not identify protected branches: {e}"
|
|
263
291
|
|
|
264
|
-
def create_merge_request(self, source_branch, target_branch, description):
|
|
292
|
+
def create_merge_request(self, source_branch, target_branch, description, reviewer_ids=None):
|
|
265
293
|
"""
|
|
266
294
|
Create a merge request in a Bitbucket repository.
|
|
267
295
|
|
|
@@ -350,43 +378,39 @@ class GitLab(GitHost):
|
|
|
350
378
|
if "error" in info:
|
|
351
379
|
raise AssertionError(f"Error when accessing Gitlab API: {info['error_description']}")
|
|
352
380
|
url = info["http_url_to_repo"]
|
|
353
|
-
|
|
354
|
-
url_replaced = False
|
|
355
|
-
if "https://" not in origin_url:
|
|
356
|
-
url_replaced = True
|
|
357
|
-
# set origin temporarily to https url
|
|
358
|
-
url = url.replace("https://", f"https://oauth2:{self.token}@")
|
|
359
|
-
self.git_obj.execute(["git", "remote", "set-url", "origin", url])
|
|
360
|
-
# necessary as some configs have an extra push url (and we cannot check if it exists...)
|
|
361
|
-
self.git_obj.execute(["git", "remote", "set-url", "--push", "origin", url])
|
|
362
|
-
else:
|
|
363
|
-
user_name = self._create_request(f"{self.git_host_rest_url}/user").json()["username"]
|
|
364
|
-
url = url.replace("https://", f"https://{user_name}:{self.token}@")
|
|
365
|
-
if url != origin_url:
|
|
366
|
-
url_replaced = True
|
|
367
|
-
self.git_obj.execute(["git", "remote", "set-url", "origin", url])
|
|
368
|
-
# necessary as some configs have an extra push url (and we cannot check if it exists...)
|
|
369
|
-
self.git_obj.execute(["git", "remote", "set-url", "--push", "origin", url])
|
|
381
|
+
user_name = self._create_request(f"{self.git_host_rest_url}/user").json().get("username", "oauth2")
|
|
370
382
|
|
|
371
383
|
try:
|
|
372
384
|
if method == "push":
|
|
373
|
-
self.
|
|
385
|
+
self._execute_git_with_https_auth(
|
|
386
|
+
git_args=["push", "--set-upstream", "origin", branch],
|
|
387
|
+
https_url=url,
|
|
388
|
+
username=user_name,
|
|
389
|
+
)
|
|
374
390
|
if tag is not None:
|
|
375
|
-
self.
|
|
391
|
+
self._execute_git_with_https_auth(
|
|
392
|
+
git_args=["push", "origin", f"refs/tags/{tag}"],
|
|
393
|
+
https_url=url,
|
|
394
|
+
username=user_name,
|
|
395
|
+
)
|
|
376
396
|
|
|
377
397
|
else:
|
|
378
|
-
self.
|
|
398
|
+
self._execute_git_with_https_auth(
|
|
399
|
+
git_args=["pull", "origin", branch],
|
|
400
|
+
https_url=url,
|
|
401
|
+
username=user_name,
|
|
402
|
+
)
|
|
379
403
|
except GitCommandError as e:
|
|
380
404
|
raise Exception(
|
|
381
405
|
f"Trying to communicate via https failed due to:\n{str(e)}\n\n" f"Please pull/push manually!!!"
|
|
382
406
|
)
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
407
|
+
|
|
408
|
+
self._execute_git_with_https_auth(
|
|
409
|
+
git_args=["fetch", "origin"],
|
|
410
|
+
https_url=url,
|
|
411
|
+
username=user_name,
|
|
412
|
+
)
|
|
413
|
+
self.git_obj.execute(["git", "branch", "--set-upstream-to", f"origin/{branch}"])
|
|
390
414
|
|
|
391
415
|
def git_remote(self, method, branch="HEAD", tag=None):
|
|
392
416
|
"""
|
|
@@ -424,23 +448,94 @@ class GitLab(GitHost):
|
|
|
424
448
|
|
|
425
449
|
:return: A list of names for the protected branches in the repository.
|
|
426
450
|
"""
|
|
427
|
-
response = self._create_request(
|
|
451
|
+
response = self._create_request(
|
|
452
|
+
f"{self.git_host_rest_url}/projects/{self.project_id}/protected_branches"
|
|
453
|
+
).json()
|
|
428
454
|
protected = []
|
|
429
455
|
for br in response:
|
|
430
456
|
if br["push_access_levels"][0]["access_level"] == 0:
|
|
431
457
|
protected.append(br["name"])
|
|
432
458
|
return protected
|
|
433
459
|
|
|
434
|
-
def
|
|
460
|
+
def _current_user_id(self) -> int | None:
|
|
461
|
+
"""
|
|
462
|
+
Return GitLab user ID of the authenticated user.
|
|
463
|
+
Returns None if a project access token is used.
|
|
464
|
+
"""
|
|
465
|
+
user_data = self._create_request(f"{self.git_host_rest_url}/user").json()
|
|
466
|
+
username = user_data.get("username", "")
|
|
467
|
+
|
|
468
|
+
# detect project bot users by naming pattern
|
|
469
|
+
if username.startswith("project_") or "_bot" in username:
|
|
470
|
+
return None
|
|
471
|
+
|
|
472
|
+
return user_data["id"]
|
|
473
|
+
|
|
474
|
+
def _user_id_by_username(self, username: str) -> int | None:
|
|
475
|
+
"""
|
|
476
|
+
Return the GitLab user ID for a given username.
|
|
477
|
+
Returns None if the user does not exist.
|
|
478
|
+
"""
|
|
479
|
+
res = self._create_request(f"{self.git_host_rest_url}/users", params={"username": username}).json()
|
|
480
|
+
return res[0]["id"] if res else None
|
|
481
|
+
|
|
482
|
+
def _reviewer_ids(self, reviewers: list[str]) -> tuple[list[int], list[str]]:
|
|
483
|
+
"""
|
|
484
|
+
Convert GitLab usernames to user IDs.
|
|
485
|
+
Returns (valid ids, invalid usernames).
|
|
486
|
+
"""
|
|
487
|
+
ids: list[int] = []
|
|
488
|
+
invalid: list[str] = []
|
|
489
|
+
for username in reviewers:
|
|
490
|
+
uid = self._user_id_by_username(username)
|
|
491
|
+
if uid is None:
|
|
492
|
+
invalid.append(username)
|
|
493
|
+
else:
|
|
494
|
+
ids.append(uid)
|
|
495
|
+
return ids, invalid
|
|
496
|
+
|
|
497
|
+
def _prompt_reviewer_ids(self) -> list[int]:
|
|
498
|
+
"""
|
|
499
|
+
Prompt for reviewer usernames and resolve them to GitLab user IDs.
|
|
500
|
+
Returns a list of user IDs, or [] if user skips (presses Enter).
|
|
501
|
+
"""
|
|
502
|
+
reviewer_id_set: set[int] = set()
|
|
503
|
+
invalid: list[str] = []
|
|
504
|
+
|
|
505
|
+
rev = input("Enter reviewer usernames (without @, comma-separated), or leave empty to skip:\n").strip()
|
|
506
|
+
if rev:
|
|
507
|
+
reviewers = [r.strip() for r in rev.split(",") if r.strip()]
|
|
508
|
+
ids, invalid = self._reviewer_ids(reviewers)
|
|
509
|
+
reviewer_id_set.update(ids)
|
|
510
|
+
|
|
511
|
+
while invalid:
|
|
512
|
+
print(f'Invalid usernames: {", ".join(invalid)}')
|
|
513
|
+
rev = input("Re-enter the invalid usernames, or press Enter to continue with valid reviewers:\n").strip()
|
|
514
|
+
if not rev:
|
|
515
|
+
break
|
|
516
|
+
|
|
517
|
+
retry = [r.strip() for r in rev.split(",") if r.strip()]
|
|
518
|
+
ids, invalid = self._reviewer_ids(retry)
|
|
519
|
+
reviewer_id_set.update(ids)
|
|
520
|
+
|
|
521
|
+
return list(reviewer_id_set)
|
|
522
|
+
|
|
523
|
+
def create_merge_request(self, source_branch, target_branch, description, reviewer_ids=None):
|
|
435
524
|
"""
|
|
436
525
|
Create a merge request in a GitLab repository.
|
|
437
526
|
|
|
438
527
|
:param source_branch: The branch to be merged into the target branch.
|
|
439
528
|
:param target_branch: The branch into which the source branch will be merged.
|
|
440
529
|
:param description: A description for the merge request.
|
|
530
|
+
:param reviewer_ids: Optional list of reviewer ids.
|
|
441
531
|
"""
|
|
442
532
|
# make sure source branch is up to date on origin
|
|
443
533
|
self.git_push(source_branch)
|
|
534
|
+
|
|
535
|
+
# normalize empty list → None for GitLab API
|
|
536
|
+
if reviewer_ids == []:
|
|
537
|
+
reviewer_ids = None
|
|
538
|
+
|
|
444
539
|
# create merge information
|
|
445
540
|
merge_data = {
|
|
446
541
|
"id": self.project_id,
|
|
@@ -449,6 +544,8 @@ class GitLab(GitHost):
|
|
|
449
544
|
"title": f"Merge {source_branch} into {target_branch}",
|
|
450
545
|
"description": description,
|
|
451
546
|
"remove_source_branch": False,
|
|
547
|
+
"assignee_id": self._current_user_id(),
|
|
548
|
+
"reviewer_ids": reviewer_ids,
|
|
452
549
|
}
|
|
453
550
|
|
|
454
551
|
response = self._create_request(
|
|
@@ -46,8 +46,9 @@ class GitFlowManager:
|
|
|
46
46
|
self.check_docs = check_docs
|
|
47
47
|
self.dynamic_version = dynamic_version
|
|
48
48
|
self.layout = project_layout
|
|
49
|
-
self.package_name, self.package_version = self.get_package_info(
|
|
50
|
-
|
|
49
|
+
self.package_name, self.package_version = self.get_package_info(
|
|
50
|
+
layout=project_layout, dynamic_version=dynamic_version
|
|
51
|
+
)
|
|
51
52
|
self.steps_to_do = []
|
|
52
53
|
self.steps_done = []
|
|
53
54
|
self.files_to_commit = []
|
|
@@ -75,12 +76,26 @@ class GitFlowManager:
|
|
|
75
76
|
if match:
|
|
76
77
|
package_version = match.group(1)
|
|
77
78
|
else:
|
|
78
|
-
raise FileNotFoundError(
|
|
79
|
-
|
|
79
|
+
raise FileNotFoundError(
|
|
80
|
+
f"Found no version in init file {path_to_init}. If your project has no dynamic"
|
|
81
|
+
f"version, please set 'dynamic_version' to false in '.gitflow_manager.yml'."
|
|
82
|
+
)
|
|
80
83
|
else:
|
|
81
84
|
package_version = pyproject_data["project"]["version"]
|
|
82
85
|
return package_name, package_version
|
|
83
86
|
|
|
87
|
+
def assert_branch_available(self, branch_name: str):
|
|
88
|
+
"""
|
|
89
|
+
Raise if branch_name already exists locally or on origin.
|
|
90
|
+
|
|
91
|
+
:param branch_name: Name of the branch to check
|
|
92
|
+
"""
|
|
93
|
+
if self.git.execute(["git", "branch", "--list", branch_name]).strip():
|
|
94
|
+
raise GitFlowManagerError(f'Branch "{branch_name}" already exists locally.')
|
|
95
|
+
|
|
96
|
+
if self.git.execute(["git", "ls-remote", "--heads", "origin", branch_name]).strip():
|
|
97
|
+
raise GitFlowManagerError(f'Branch "{branch_name}" already exists on origin.')
|
|
98
|
+
|
|
84
99
|
def branch_from_main(self, chglog, user_name, app_specific_changes, app_specific_text):
|
|
85
100
|
"""
|
|
86
101
|
Create a new branch from the main branch, update version and changelog, and push new branch to origin
|
|
@@ -106,6 +121,7 @@ class GitFlowManager:
|
|
|
106
121
|
package_version_parsed = [int(x) for x in self.package_version.split(".")]
|
|
107
122
|
new_package_version = self.increase_version(package_version_parsed, "hotfix")
|
|
108
123
|
branch_name = f"hotfix/v{new_package_version}"
|
|
124
|
+
self.assert_branch_available(branch_name)
|
|
109
125
|
self.steps_to_do.extend([f'Checkout to "{branch_name}"'])
|
|
110
126
|
# create the new hotfix branch and checkout
|
|
111
127
|
self.git.checkout("HEAD", b=branch_name) # create a new branch
|
|
@@ -179,6 +195,7 @@ class GitFlowManager:
|
|
|
179
195
|
print("Only letters and underscore is allowed!")
|
|
180
196
|
|
|
181
197
|
branch_name = f"{branch_type}/{new_branch}"
|
|
198
|
+
self.assert_branch_available(branch_name)
|
|
182
199
|
self.steps_to_do.extend([f'Checkout to "{branch_name}"'])
|
|
183
200
|
# create the new release branch and checkout
|
|
184
201
|
self.git.checkout("HEAD", b=branch_name) # create a new branch
|
|
@@ -234,9 +251,7 @@ class GitFlowManager:
|
|
|
234
251
|
sys.exit()
|
|
235
252
|
|
|
236
253
|
if answer == "0":
|
|
237
|
-
self.edit_change_log_info(
|
|
238
|
-
chglog, user_name, app_specific_changes, app_specific_text
|
|
239
|
-
)
|
|
254
|
+
self.edit_change_log_info(chglog, user_name, app_specific_changes, app_specific_text)
|
|
240
255
|
else:
|
|
241
256
|
# fill steps to do list
|
|
242
257
|
self.steps_to_do.append("Do a sanity push of your branch to origin beforehand")
|
|
@@ -244,9 +259,16 @@ class GitFlowManager:
|
|
|
244
259
|
f"Merge branch into dev & {self.main_branch_name} (directly+pushing or via merge request)"
|
|
245
260
|
)
|
|
246
261
|
branches_to_merge = ["dev"]
|
|
262
|
+
descr = None
|
|
263
|
+
reviewer_ids = None
|
|
247
264
|
|
|
248
265
|
if answer == "2":
|
|
249
266
|
branches_to_merge.append(self.main_branch_name)
|
|
267
|
+
descr = input(
|
|
268
|
+
f"Enter description for merge into dev and {self.main_branch_name} "
|
|
269
|
+
f"(adding all git commit messages if nothing entered)\n"
|
|
270
|
+
).strip()
|
|
271
|
+
reviewer_ids = self.git_host._prompt_reviewer_ids()
|
|
250
272
|
# only do the commit check if there will be merge into the main branch
|
|
251
273
|
self.commit_check(branches_to_merge[-1])
|
|
252
274
|
last_main_commit = self.git.execute(["git", "rev-parse", f"origin/{branches_to_merge[-1]}"])
|
|
@@ -261,10 +283,10 @@ class GitFlowManager:
|
|
|
261
283
|
self.steps_to_do.pop(0)
|
|
262
284
|
|
|
263
285
|
protected_branches = self.git_host.get_protected_branches()
|
|
264
|
-
|
|
286
|
+
|
|
265
287
|
for main_branch in branches_to_merge:
|
|
266
288
|
if main_branch in protected_branches:
|
|
267
|
-
self.request_merge(main_branch, last_main_commit, descr)
|
|
289
|
+
self.request_merge(main_branch, last_main_commit, descr, reviewer_ids)
|
|
268
290
|
|
|
269
291
|
else:
|
|
270
292
|
self.merge_branch(main_branch)
|
|
@@ -293,9 +315,7 @@ class GitFlowManager:
|
|
|
293
315
|
sys.exit()
|
|
294
316
|
|
|
295
317
|
if answer == "0":
|
|
296
|
-
self.edit_change_log_info(
|
|
297
|
-
chglog, user_name, app_specific_changes, app_specific_text
|
|
298
|
-
)
|
|
318
|
+
self.edit_change_log_info(chglog, user_name, app_specific_changes, app_specific_text)
|
|
299
319
|
else:
|
|
300
320
|
print("******Checking latest commit of dev...******")
|
|
301
321
|
main_branch = "dev"
|
|
@@ -378,8 +398,10 @@ class GitFlowManager:
|
|
|
378
398
|
init_text = read_file(path_to_init)
|
|
379
399
|
init_text_new = init_text.replace(old_version, new_version)
|
|
380
400
|
if init_text == init_text_new:
|
|
381
|
-
warnings.warn(
|
|
382
|
-
|
|
401
|
+
warnings.warn(
|
|
402
|
+
f"Failed to replace old version string with new one in {path_to_init}. Probably it does not"
|
|
403
|
+
f"contain a version string."
|
|
404
|
+
)
|
|
383
405
|
else:
|
|
384
406
|
write_to_file(path_to_init, init_text_new)
|
|
385
407
|
|
|
@@ -521,21 +543,19 @@ class GitFlowManager:
|
|
|
521
543
|
print(self.git.status())
|
|
522
544
|
|
|
523
545
|
try:
|
|
524
|
-
self.git.commit(f
|
|
546
|
+
self.git.commit(f"-m {message}")
|
|
525
547
|
except GitCommandError:
|
|
526
548
|
print("******Try committing changes again after pre-commit.******")
|
|
527
549
|
self.git.add(self.files_to_commit)
|
|
528
|
-
self.git.commit(f
|
|
550
|
+
self.git.commit(f"-m {message}")
|
|
529
551
|
|
|
530
552
|
self.steps_done.append(self.steps_to_do.pop(0))
|
|
531
|
-
print(
|
|
553
|
+
print("******Pushing now...******")
|
|
532
554
|
self.git_host.git_push(branch_name)
|
|
533
555
|
self.files_to_commit = []
|
|
534
556
|
self.steps_done.append(self.steps_to_do.pop(0))
|
|
535
557
|
|
|
536
|
-
def edit_change_log_info(
|
|
537
|
-
self, chglog, user_name, app_specific_changes, app_specific_text
|
|
538
|
-
):
|
|
558
|
+
def edit_change_log_info(self, chglog, user_name, app_specific_changes, app_specific_text):
|
|
539
559
|
"""
|
|
540
560
|
Edit change log information or regenerate change log.
|
|
541
561
|
:param chglog:
|
|
@@ -555,9 +575,7 @@ class GitFlowManager:
|
|
|
555
575
|
sys.exit()
|
|
556
576
|
|
|
557
577
|
if answer == "0":
|
|
558
|
-
self.add_change_log_info(
|
|
559
|
-
chglog, user_name, app_specific_changes, app_specific_text
|
|
560
|
-
)
|
|
578
|
+
self.add_change_log_info(chglog, user_name, app_specific_changes, app_specific_text)
|
|
561
579
|
else:
|
|
562
580
|
self.steps_to_do.append("Create new change_log.rst")
|
|
563
581
|
print("******Regenerating change log...******")
|
|
@@ -575,9 +593,7 @@ class GitFlowManager:
|
|
|
575
593
|
:return:
|
|
576
594
|
"""
|
|
577
595
|
# fill steps to do list
|
|
578
|
-
self.steps_to_do.append(
|
|
579
|
-
"Log changes to change_log.json and create new change_log.rst"
|
|
580
|
-
)
|
|
596
|
+
self.steps_to_do.append("Log changes to change_log.json and create new change_log.rst")
|
|
581
597
|
if "bugfix/" in self.branch_of_interest or "hotfix/" in self.branch_of_interest:
|
|
582
598
|
ask_sections = [Sections.fixed]
|
|
583
599
|
else:
|
|
@@ -625,7 +641,9 @@ class GitFlowManager:
|
|
|
625
641
|
# find most recent commit of main branch/dev
|
|
626
642
|
last_main_commit = self.git.execute(["git", "rev-parse", f"origin/{branch_to_merge}"])
|
|
627
643
|
if last_commit == last_main_commit:
|
|
628
|
-
raise GitFlowManagerError(
|
|
644
|
+
raise GitFlowManagerError(
|
|
645
|
+
f"{branch_to_merge} is already up to date with your branch {self.branch_of_interest}"
|
|
646
|
+
)
|
|
629
647
|
self.steps_done.append(self.steps_to_do.pop(0))
|
|
630
648
|
if (
|
|
631
649
|
common_commit != last_main_commit
|
|
@@ -672,13 +690,18 @@ class GitFlowManager:
|
|
|
672
690
|
print(f"Answer was {answer}, aborting...")
|
|
673
691
|
sys.exit()
|
|
674
692
|
|
|
675
|
-
def request_merge(self, branch_to_merge, last_main_commit, descr):
|
|
693
|
+
def request_merge(self, branch_to_merge, last_main_commit, descr, reviewer_ids=None):
|
|
676
694
|
"""
|
|
677
695
|
For protected branches, create a merge request
|
|
678
696
|
|
|
679
697
|
:param branch_to_merge: Branch to merge into, main branch or dev
|
|
680
698
|
:param last_main_commit: Last commit of the branch to merge into
|
|
681
|
-
:param descr: Description for the merge request
|
|
699
|
+
:param descr: Description for the merge request.
|
|
700
|
+
None prompts the user for input.
|
|
701
|
+
"" skips the prompt and auto-generates from commit history.
|
|
702
|
+
:param reviewer_ids: Optional list of GitLab reviewer user IDs.
|
|
703
|
+
None prompts the user for reviewer usernames.
|
|
704
|
+
[] skips the prompt and creates the merge request without reviewers.
|
|
682
705
|
"""
|
|
683
706
|
# fill steps to do list
|
|
684
707
|
self.steps_to_do.append(f"Create merge request for branch {self.branch_of_interest} into {branch_to_merge}")
|
|
@@ -688,25 +711,28 @@ class GitFlowManager:
|
|
|
688
711
|
f"Enter some description for the merge to {branch_to_merge} "
|
|
689
712
|
f"(adding all git commit messages if nothing entered)\n"
|
|
690
713
|
)
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
714
|
+
if not descr.strip(): # auto-generate if empty or whitespace-only
|
|
715
|
+
commit_history = self.git.execute(
|
|
716
|
+
[
|
|
717
|
+
"git",
|
|
718
|
+
"log",
|
|
719
|
+
'--pretty=format:"%h%x09%an:%x09%s"',
|
|
720
|
+
f"{last_main_commit}..HEAD",
|
|
721
|
+
]
|
|
722
|
+
)
|
|
723
|
+
commit_history = "* " + "* ".join(commit_history.splitlines(True))
|
|
724
|
+
descr = (
|
|
725
|
+
f"Merge of {self.branch_of_interest} into {branch_to_merge}.\n\n" f"Git history:\n\n{commit_history}"
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
if reviewer_ids is None:
|
|
729
|
+
reviewer_ids = self.git_host._prompt_reviewer_ids() # only implemented for GitLab
|
|
705
730
|
|
|
706
731
|
self.git_host.create_merge_request(
|
|
707
732
|
source_branch=self.branch_of_interest,
|
|
708
733
|
target_branch=branch_to_merge,
|
|
709
734
|
description=descr,
|
|
735
|
+
reviewer_ids=reviewer_ids,
|
|
710
736
|
)
|
|
711
737
|
self.steps_done.append(self.steps_to_do.pop(0))
|
|
712
738
|
|
|
@@ -3,15 +3,22 @@ from pathlib import Path
|
|
|
3
3
|
from urllib.parse import urlparse
|
|
4
4
|
import requests
|
|
5
5
|
import yaml
|
|
6
|
+
from git import Repo
|
|
6
7
|
|
|
7
8
|
from gitflow_manager.run import run
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def get_project_details(repo_url, git_host_token):
|
|
11
12
|
"""Get the details of the repository: url, project id, repository name"""
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
|
|
14
|
+
if repo_url.startswith("git@"):
|
|
15
|
+
host, path = repo_url.split(":", 1)
|
|
16
|
+
host = host.split("@", 1)[1]
|
|
17
|
+
else:
|
|
18
|
+
parsed = urlparse(repo_url)
|
|
19
|
+
host = parsed.netloc
|
|
20
|
+
path = parsed.path
|
|
21
|
+
path = path.strip("/").removesuffix(".git")
|
|
15
22
|
|
|
16
23
|
namespace, repo_name = path.split("/", 1)
|
|
17
24
|
gitlab_api_url = f"https://{host}/api/v4"
|
|
@@ -46,12 +53,20 @@ def init(config_file: Path):
|
|
|
46
53
|
"""Function to initialize GitFlowManager"""
|
|
47
54
|
|
|
48
55
|
# Check if config exists
|
|
49
|
-
if
|
|
50
|
-
|
|
56
|
+
if (
|
|
57
|
+
not config_file.is_file()
|
|
58
|
+
or input(
|
|
59
|
+
"Configuration already existing in .gitflow_manager.yml. " "Use existing configuration (y/n)?\n"
|
|
60
|
+
).lower()
|
|
61
|
+
== "n"
|
|
62
|
+
):
|
|
51
63
|
# create new config file and use new information
|
|
52
|
-
repo_url =
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
repo_url = Repo(".").remotes.origin.url
|
|
65
|
+
print(f"Using origin URL from current repository:\n{repo_url}")
|
|
66
|
+
|
|
67
|
+
git_host_token = input(
|
|
68
|
+
"Please enter a project or personal token, with which you can access that repository " "(read/write):\n"
|
|
69
|
+
)
|
|
55
70
|
git_host_rest_url, project_id, repo_name = get_project_details(repo_url, git_host_token=git_host_token)
|
|
56
71
|
project_layout = input("Please enter the layout of your project (e.g., 'flat' or 'src'):\n")
|
|
57
72
|
dynamic_version = input("Is the versioning of your project dynamic (y/n)?\n")
|
|
@@ -62,16 +77,19 @@ def init(config_file: Path):
|
|
|
62
77
|
use_ssh = use_ssh.lower() == "y"
|
|
63
78
|
|
|
64
79
|
# Save config
|
|
65
|
-
save_config(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
save_config(
|
|
81
|
+
config_file,
|
|
82
|
+
{
|
|
83
|
+
"git_host_rest_url": git_host_rest_url,
|
|
84
|
+
"git_host_token": git_host_token,
|
|
85
|
+
"project_id": project_id,
|
|
86
|
+
"repo_name": repo_name,
|
|
87
|
+
"project_layout": project_layout,
|
|
88
|
+
"dynamic_version": dynamic_version,
|
|
89
|
+
"check_docs": check_docs,
|
|
90
|
+
"use_ssh": use_ssh,
|
|
91
|
+
},
|
|
92
|
+
)
|
|
75
93
|
|
|
76
94
|
ignore_file = Path(".gitignore")
|
|
77
95
|
|
|
@@ -103,7 +121,8 @@ def main():
|
|
|
103
121
|
if config_file.is_file():
|
|
104
122
|
config = load_config(config_file)
|
|
105
123
|
# use config file based on user choice during init
|
|
106
|
-
run(
|
|
124
|
+
run(
|
|
125
|
+
config["project_id"],
|
|
107
126
|
config["repo_name"],
|
|
108
127
|
folder=str(Path.cwd()), # Get the current project directory,
|
|
109
128
|
git_host_rest_url=config["git_host_rest_url"],
|
|
@@ -111,11 +130,13 @@ def main():
|
|
|
111
130
|
layout=config["project_layout"],
|
|
112
131
|
dynamic_version=config["dynamic_version"],
|
|
113
132
|
check_docs=config["check_docs"],
|
|
114
|
-
use_ssh=config["use_ssh"]
|
|
115
|
-
|
|
133
|
+
use_ssh=config["use_ssh"],
|
|
134
|
+
)
|
|
116
135
|
else:
|
|
117
|
-
raise FileNotFoundError(
|
|
118
|
-
|
|
136
|
+
raise FileNotFoundError(
|
|
137
|
+
"Git manager config file '.gitflow_manager.yml' not found in the current directory. "
|
|
138
|
+
"Either you are in the wrong directory, or you have not run 'gfm --init', yet."
|
|
139
|
+
)
|
|
119
140
|
|
|
120
141
|
|
|
121
142
|
if __name__ == "__main__":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitflow_manager
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: This package contains the Anacision Git Manager.
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -9,6 +9,11 @@ Requires-Dist: pystache>=0.6.0
|
|
|
9
9
|
Requires-Dist: pyyaml>=6.0.2
|
|
10
10
|
Requires-Dist: requests>=2.27.1
|
|
11
11
|
Requires-Dist: tomlkit>=0.13.3
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pre-commit>=4.0.1; extra == "dev"
|
|
14
|
+
Requires-Dist: ruff==0.6.9; extra == "dev"
|
|
15
|
+
Requires-Dist: setuptools>=75.3.0; extra == "dev"
|
|
16
|
+
Requires-Dist: twine>=6.0.1; extra == "dev"
|
|
12
17
|
|
|
13
18
|
# Anacision Gitflow Manager
|
|
14
19
|
|
|
@@ -15,7 +15,7 @@ dependencies = [
|
|
|
15
15
|
[project.scripts]
|
|
16
16
|
gfm = "gitflow_manager.start:main"
|
|
17
17
|
|
|
18
|
-
[
|
|
18
|
+
[project.optional-dependencies]
|
|
19
19
|
dev = [
|
|
20
20
|
"pre-commit>=4.0.1",
|
|
21
21
|
"ruff==0.6.9",
|
|
@@ -32,7 +32,7 @@ version = { attr = "gitflow_manager.__version__" }
|
|
|
32
32
|
|
|
33
33
|
[tool.ruff]
|
|
34
34
|
line-length = 120
|
|
35
|
-
target-version="
|
|
35
|
+
target-version="2.0.0"
|
|
36
36
|
|
|
37
37
|
[tool.ruff.lint]
|
|
38
38
|
select = [
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__="1.1.4"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|