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.
Files changed (19) hide show
  1. {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/PKG-INFO +6 -1
  2. gitflow_manager-2.0.0/gitflow_manager/__init__.py +1 -0
  3. {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager/git_hosts.py +165 -68
  4. {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager/gitflow_manager.py +69 -43
  5. {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager/start.py +44 -23
  6. {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager.egg-info/PKG-INFO +6 -1
  7. {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager.egg-info/requires.txt +6 -0
  8. {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/pyproject.toml +2 -2
  9. gitflow_manager-1.1.4/gitflow_manager/__init__.py +0 -1
  10. {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/README.md +0 -0
  11. {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager/changelog.py +0 -0
  12. {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager/errors.py +0 -0
  13. {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager/file_utils.py +0 -0
  14. {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager/run.py +0 -0
  15. {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager.egg-info/SOURCES.txt +0 -0
  16. {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager.egg-info/dependency_links.txt +0 -0
  17. {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager.egg-info/entry_points.txt +0 -0
  18. {gitflow_manager-1.1.4 → gitflow_manager-2.0.0}/gitflow_manager.egg-info/top_level.txt +0 -0
  19. {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: 1.1.4
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(self, source_branch: str, target_branch: str, description: str) -> None:
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
- url = info["links"]["clone"][1]["href"]
149
- if url != self.git_obj.execute(["git", "remote", "get-url", "origin"]):
150
- ssh_url = True
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.git_obj.execute(["git", "push", "--set-upstream", "origin", branch])
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.git_obj.execute(["git", "push", "origin", f"refs/tags/{tag}"])
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.git_obj.execute(["git", "pull", "origin", branch])
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
- finally:
171
- self.git_obj.fetch("origin")
172
- self.git_obj.execute(["git", "branch", "--set-upstream-to", f"origin/{branch}"])
173
- if ssh_url:
174
- # make sure remote tracking is set back to the default
175
- self.git_obj.execute(
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
- origin_url = self.git_obj.execute(["git", "remote", "get-url", "origin"])
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.git_obj.execute(["git", "push", "--set-upstream", "origin", branch])
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.git_obj.execute(["git", "push", "origin", f"refs/tags/{tag}"])
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.git_obj.execute(["git", "pull", "origin", branch])
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
- finally:
384
- self.git_obj.fetch("origin")
385
- self.git_obj.execute(["git", "branch", "--set-upstream-to", f"origin/{branch}"])
386
- if url_replaced:
387
- # make sure remote tracking is set back to the default
388
- self.git_obj.execute(["git", "remote", "set-url", "origin", origin_url])
389
- self.git_obj.execute(["git", "remote", "set-url", "--push", "origin", origin_url])
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(f"{self.git_host_rest_url}/projects/{self.project_id}/protected_branches").json()
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 create_merge_request(self, source_branch, target_branch, description):
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(layout=project_layout,
50
- dynamic_version=dynamic_version)
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(f"Found no version in init file {path_to_init}. If your project has no dynamic"
79
- f"version, please set 'dynamic_version' to false in '.gitflow_manager.yml'.")
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
- descr = None
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(f"Failed to replace old version string with new one in {path_to_init}. Probably it does not"
382
- f"contain a version string.")
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'-m {message}')
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'-m {message}')
550
+ self.git.commit(f"-m {message}")
529
551
 
530
552
  self.steps_done.append(self.steps_to_do.pop(0))
531
- print(f"******Pushing now...******")
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(f"{branch_to_merge} is already up to date with your branch {self.branch_of_interest}")
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
- if len(descr) <= 1:
692
- commit_history = self.git.execute(
693
- [
694
- "git",
695
- "log",
696
- '--pretty=format:"%h%x09%an:%x09%s"',
697
- f"{last_main_commit}..HEAD",
698
- ]
699
- )
700
- commit_history = "* " + "* ".join(commit_history.splitlines(True))
701
- descr = (
702
- f"Merge of {self.branch_of_interest} into {branch_to_merge}.\n\n"
703
- f"Git history:\n\n{commit_history}"
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
- parsed_url = urlparse(repo_url)
13
- host = parsed_url.netloc
14
- path = parsed_url.path.strip("/").removesuffix(".git")
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 not config_file.is_file() or input("Configuration already existing in .gitflow_manager.yml. "
50
- "Use existing configuration (y/n)?\n").lower() == "n":
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 = input("Please enter the URL of your repository:\n")
53
- git_host_token = input("Please enter a project or personal token, with which you can access that repository "
54
- "(read/write):\n")
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(config_file, {
66
- "git_host_rest_url": git_host_rest_url,
67
- "git_host_token": git_host_token,
68
- "project_id": project_id,
69
- "repo_name": repo_name,
70
- "project_layout": project_layout,
71
- "dynamic_version": dynamic_version,
72
- "check_docs": check_docs,
73
- "use_ssh": use_ssh
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(config["project_id"],
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("Git manager config file '.gitflow_manager.yml' not found in the current directory. "
118
- "Either you are in the wrong directory, or you have not run 'gfm --init', yet.")
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: 1.1.4
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
 
@@ -3,3 +3,9 @@ pystache>=0.6.0
3
3
  pyyaml>=6.0.2
4
4
  requests>=2.27.1
5
5
  tomlkit>=0.13.3
6
+
7
+ [dev]
8
+ pre-commit>=4.0.1
9
+ ruff==0.6.9
10
+ setuptools>=75.3.0
11
+ twine>=6.0.1
@@ -15,7 +15,7 @@ dependencies = [
15
15
  [project.scripts]
16
16
  gfm = "gitflow_manager.start:main"
17
17
 
18
- [dependency-groups]
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="1.1.4"
35
+ target-version="2.0.0"
36
36
 
37
37
  [tool.ruff.lint]
38
38
  select = [
@@ -1 +0,0 @@
1
- __version__="1.1.4"