Skip to content

Commit 8479e44

Browse files
committed
Implement merge_label, fix computing of labels_to_merge
- Fix order of operations (merge, update, delete). This implementation assumes that when an old label is merged to another label, the old label should be deleted. This should normally be what is intended, and avoids ambiguity (e.g. people start adding the old label again by force of habit).
1 parent 8f61323 commit 8479e44

File tree

2 files changed

+104
-12
lines changed

2 files changed

+104
-12
lines changed

src/labels/cli.py

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,9 @@ def sync_cmd(
164164
On success this will also update the local labels file, so that section
165165
names match the `name` parameter.
166166
"""
167-
labels_to_delete = {}
167+
labels_to_merge = {}
168168
labels_to_update = {}
169+
labels_to_delete = {}
169170
labels_to_create = {}
170171
labels_to_ignore = {}
171172

@@ -187,7 +188,11 @@ def sync_cmd(
187188
if local_label.params_dict == remote_label.params_dict:
188189
labels_to_ignore[remote_name] = local_label
189190
else:
190-
labels_to_update[remote_name] = local_label
191+
if (remote_name != local_label.name) and (local_label.name in remote_labels):
192+
# There is already a label with this name
193+
labels_to_merge[remote_name] = local_label.name
194+
else:
195+
labels_to_update[remote_name] = local_label
191196
else:
192197
if remote_name == local_label.name:
193198
labels_to_create[local_label.name] = local_label
@@ -212,18 +217,20 @@ def sync_cmd(
212217
if dryrun:
213218
# Do not modify remote labels, but only print info
214219
dryrun_echo(
215-
labels_to_delete, labels_to_update, labels_to_create, labels_to_ignore
220+
labels_to_merge, labels_to_update, labels_to_delete, labels_to_create, labels_to_ignore
216221
)
217-
sys.exit(0)
222+
# sys.exit(0)
223+
return
218224

219225
failures = []
220226

221-
for name in labels_to_delete.keys():
227+
# Merge has to occur before update and delete
228+
for old_label, new_label in labels_to_merge.items():
222229
try:
223-
context.client.delete_label(repository, name=name)
230+
context.client.merge_label(repository, old_label=old_label, new_label=new_label)
224231
except LabelsException as exc:
225232
click.echo(str(exc), err=True)
226-
failures.append(name)
233+
failures.append(old_label)
227234

228235
for name, label in labels_to_update.items():
229236
try:
@@ -232,6 +239,13 @@ def sync_cmd(
232239
click.echo(str(exc), err=True)
233240
failures.append(name)
234241

242+
for name in labels_to_delete.keys():
243+
try:
244+
context.client.delete_label(repository, name=name)
245+
except LabelsException as exc:
246+
click.echo(str(exc), err=True)
247+
failures.append(name)
248+
235249
for name, label in labels_to_create.items():
236250
try:
237251
context.client.create_label(repository, label=label)
@@ -253,23 +267,29 @@ def sync_cmd(
253267

254268

255269
def dryrun_echo(
256-
labels_to_delete: Labels_Dict,
270+
labels_to_merge: dict,
257271
labels_to_update: Labels_Dict,
272+
labels_to_delete: Labels_Dict,
258273
labels_to_create: Labels_Dict,
259274
labels_to_ignore: Labels_Dict,
260275
) -> None:
261276
"""Print information about how labels would be updated on sync."""
262277

263-
if labels_to_delete:
264-
click.echo(f"This would delete the following labels:")
265-
for name in labels_to_delete:
266-
click.echo(f" - {name}")
278+
if labels_to_merge:
279+
click.echo(f"This would merge the following labels:")
280+
for name in labels_to_merge:
281+
click.echo(f" - {', '.join([' to '.join((old, new)) for old, new in labels_to_merge.items()])}")
267282

268283
if labels_to_update:
269284
click.echo(f"This would update the following labels:")
270285
for name in labels_to_update:
271286
click.echo(f" - {name}")
272287

288+
if labels_to_delete:
289+
click.echo(f"This would delete the following labels:")
290+
for name in labels_to_delete:
291+
click.echo(f" - {name}")
292+
273293
if labels_to_create:
274294
click.echo(f"This would create the following labels:")
275295
for name in labels_to_create:

src/labels/github.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,78 @@ def edit_label(self, repo: Repository, *, name: str, label: Label) -> Label:
170170

171171
return Label(**response.json())
172172

173+
def merge_label(self, repo: Repository, *, old_label: str, new_label: str) -> None:
174+
"""Merge a GitHub issue label to an existing label.
175+
176+
- Add the target label to all issues with the old label.
177+
- The old label will be deleted while processing labels to delete.
178+
"""
179+
logger = logging.getLogger("labels")
180+
logger.debug(f"Requesting issues for label {old_label} in {repo.owner}/{repo.name}")
181+
182+
response = self.session.get(
183+
f"{self.base_url}/search/issues?q=label:{old_label}+repo:{repo.owner}/{repo.name}",
184+
headers={"Accept": "application/vnd.github.symmetra-preview+json"},
185+
)
186+
187+
if response.status_code != 200:
188+
raise GitHubException(
189+
f"Error retrieving issues for label {old_label} in {repo.owner}/{repo.name}: "
190+
f"{response.status_code} - "
191+
f"{response.reason}"
192+
)
193+
194+
json = response.json()
195+
196+
next_page = response.links.get('next', None)
197+
while next_page:
198+
logger.debug(f"Requesting {next_page}")
199+
response = self.session.get(
200+
next_page['url'],
201+
headers={"Accept": "application/vnd.github.symmetra-preview+json"},
202+
)
203+
204+
if response.status_code != 200:
205+
raise GitHubException(
206+
f"Error retrieving next page of issues for label {old_label}: "
207+
f"{response.status_code} - "
208+
f"{response.reason}"
209+
)
210+
211+
json.extend(response.json())
212+
next_page = response.links.get('next', None)
213+
214+
for issue in json['items']:
215+
response = self.session.get(
216+
f"{self.base_url}/repos/{repo.owner}/{repo.name}/issues/{issue['number']}/labels",
217+
headers={"Accept": "application/vnd.github.symmetra-preview+json"},
218+
)
219+
220+
if response.status_code != 200:
221+
raise GitHubException(
222+
f"Error retrieving labels for {repo.owner}/{repo.name}/issue/{issue['number']}: "
223+
f"{response.status_code} - "
224+
f"{response.reason}"
225+
)
226+
227+
labels = [l['name'] for l in response.json()]
228+
229+
if new_label not in labels:
230+
breakpoint()
231+
response = self.session.post(
232+
f"{self.base_url}/repos/{repo.owner}/{repo.name}/issues/{issue['number']}/labels",
233+
headers={"Accept": "application/vnd.github.symmetra-preview+json"},
234+
json={'labels': [f"{new_label}"]},
235+
)
236+
if response.status_code != 200:
237+
raise GitHubException(
238+
f"Error adding '{new_label}' for issue {repo.owner}/{repo.name}/issues/{issue['number']}: "
239+
f"{response.status_code} - "
240+
f"{response.reason}"
241+
)
242+
logger.debug(f"Added label '{new_label}' to {repo.owner}/{repo.name}/issue/{issue['number']}")
243+
244+
173245
def delete_label(self, repo: Repository, *, name: str) -> None:
174246
"""Delete a GitHub issue label.
175247

0 commit comments

Comments
 (0)