Skip to content

Merge labels #16

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 11 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ export LABELS_USERNAME="<GITHUB_USERNAME>"
export LABELS_TOKEN="<GITHUB_TOKEN>"
```

You can override one or both of these values manually using the following CLI
options:

```text
-u, --username TEXT GitHub username
-t, --token TEXT GitHub access token
```

## Usage

Once you've installed **labels** and set up the environment variables, you're
Expand Down Expand Up @@ -118,6 +126,8 @@ You can make the following changes to labels for your repo:
labels file 🗑
- You can **edit** a label by changing the value for one or more parameters for
that label 🎨
- You can **merge** one label to another by setting the ``name`` of a label to
that of an existing label. The merged label will be deleted.
- You can **create** a new label by adding a new section with your desired
parameters 📝

Expand All @@ -137,15 +147,18 @@ labels sync -n -o hackebrot -r pytest-emoji
```

```text
This would delete the following labels:
- dependencies
This would merge the following labels:
- dependencies to dependency
This would update the following labels:
- bug
- good first issue
This would delete the following labels:
- dependencies
This would create the following labels:
- duplicate
This would NOT modify the following labels:
- code quality
- dependency
- docs
```

Expand Down
51 changes: 39 additions & 12 deletions src/labels/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,9 @@ def sync_cmd(
On success this will also update the local labels file, so that section
names match the `name` parameter.
"""
labels_to_delete = {}
labels_to_merge = {}
labels_to_update = {}
labels_to_delete = {}
labels_to_create = {}
labels_to_ignore = {}

Expand All @@ -187,7 +188,12 @@ def sync_cmd(
if local_label.params_dict == remote_label.params_dict:
labels_to_ignore[remote_name] = local_label
else:
labels_to_update[remote_name] = local_label
if ((remote_name != local_label.name)
and (local_label.name in remote_labels)):
# There is already a label with this name
labels_to_merge[remote_name] = local_label.name
else:
labels_to_update[remote_name] = local_label
else:
if remote_name == local_label.name:
labels_to_create[local_label.name] = local_label
Expand All @@ -212,18 +218,25 @@ def sync_cmd(
if dryrun:
# Do not modify remote labels, but only print info
dryrun_echo(
labels_to_delete, labels_to_update, labels_to_create, labels_to_ignore
labels_to_merge,
labels_to_update,
labels_to_delete,
labels_to_create,
labels_to_ignore
)
sys.exit(0)
# sys.exit(0)
return

failures = []

for name in labels_to_delete.keys():
# Merge has to occur before update and delete
for old_label, new_label in labels_to_merge.items():
try:
context.client.delete_label(repository, name=name)
context.client.merge_label(
repository, old_label=old_label, new_label=new_label)
except LabelsException as exc:
click.echo(str(exc), err=True)
failures.append(name)
failures.append(old_label)

for name, label in labels_to_update.items():
try:
Expand All @@ -232,6 +245,13 @@ def sync_cmd(
click.echo(str(exc), err=True)
failures.append(name)

for name in labels_to_delete.keys():
try:
context.client.delete_label(repository, name=name)
except LabelsException as exc:
click.echo(str(exc), err=True)
failures.append(name)

for name, label in labels_to_create.items():
try:
context.client.create_label(repository, label=label)
Expand All @@ -253,23 +273,30 @@ def sync_cmd(


def dryrun_echo(
labels_to_delete: Labels_Dict,
labels_to_merge: dict,
labels_to_update: Labels_Dict,
labels_to_delete: Labels_Dict,
labels_to_create: Labels_Dict,
labels_to_ignore: Labels_Dict,
) -> None:
"""Print information about how labels would be updated on sync."""

if labels_to_delete:
click.echo(f"This would delete the following labels:")
for name in labels_to_delete:
click.echo(f" - {name}")
if labels_to_merge:
click.echo(f"This would merge the following labels:")
for name in labels_to_merge:
click.echo(f"""\
- {', '.join([' to '.join((old, new)) for old, new in labels_to_merge.items()])}""")

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

if labels_to_delete:
click.echo(f"This would delete the following labels:")
for name in labels_to_delete:
click.echo(f" - {name}")

if labels_to_create:
click.echo(f"This would create the following labels:")
for name in labels_to_create:
Expand Down
92 changes: 91 additions & 1 deletion src/labels/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,27 @@ def list_labels(self, repo: Repository) -> typing.List[Label]:
f"{response.reason}"
)

return [Label(**data) for data in response.json()]
json = response.json()

next_page = response.links.get('next', None)
while next_page:
logger.debug(f"Requesting {next_page}")
response = self.session.get(
next_page['url'],
headers={"Accept": "application/vnd.github.symmetra-preview+json"},
)

if response.status_code != 200:
raise GitHubException(
f"Error retrieving next page of labels: "
f"{response.status_code} - "
f"{response.reason}"
)

json.extend(response.json())
next_page = response.links.get('next', None)

return [Label(**data) for data in json]

def get_label(self, repo: Repository, *, name: str) -> Label:
"""Return a single Label from the repository.
Expand Down Expand Up @@ -150,6 +170,76 @@ def edit_label(self, repo: Repository, *, name: str, label: Label) -> Label:

return Label(**response.json())

def merge_label(self, repo: Repository, *, old_label: str, new_label: str) -> None:
"""Merge a GitHub issue label to an existing label.

- Add the target label to all issues with the old label.
- The old label will be deleted while processing labels to delete.
"""
logger = logging.getLogger("labels")
logger.debug(f"Requesting issues for label {old_label} in {repo.owner}/{repo.name}") # noqa: E501

response = self.session.get(
f"{self.base_url}/search/issues?q=label:{old_label}+repo:{repo.owner}/{repo.name}", # noqa: E501
headers={"Accept": "application/vnd.github.symmetra-preview+json"},
)

if response.status_code != 200:
raise GitHubException(
f"Error retrieving issues for label {old_label} in {repo.owner}/{repo.name}: " # noqa: E501
f"{response.status_code} - "
f"{response.reason}"
)

json = response.json()

next_page = response.links.get('next', None)
while next_page:
logger.debug(f"Requesting {next_page}")
response = self.session.get(
next_page['url'],
headers={"Accept": "application/vnd.github.symmetra-preview+json"},
)

if response.status_code != 200:
raise GitHubException(
f"Error retrieving next page of issues for label {old_label}: "
f"{response.status_code} - "
f"{response.reason}"
)

json.extend(response.json())
next_page = response.links.get('next', None)

for issue in json['items']:
response = self.session.get(
f"{self.base_url}/repos/{repo.owner}/{repo.name}/issues/{issue['number']}/labels", # noqa: E501
headers={"Accept": "application/vnd.github.symmetra-preview+json"},
)

if response.status_code != 200:
raise GitHubException(
f"Error retrieving labels for {repo.owner}/{repo.name}/issue/{issue['number']}: " # noqa: E501
f"{response.status_code} - "
f"{response.reason}"
)

labels = [l['name'] for l in response.json()]

if new_label not in labels:
response = self.session.post(
f"{self.base_url}/repos/{repo.owner}/{repo.name}/issues/{issue['number']}/labels", # noqa: E501
headers={"Accept": "application/vnd.github.symmetra-preview+json"},
json={'labels': [f"{new_label}"]},
)
if response.status_code != 200:
raise GitHubException(
f"Error adding '{new_label}' for issue {repo.owner}/{repo.name}/issues/{issue['number']}: " # noqa: E501
f"{response.status_code} - "
f"{response.reason}"
)
logger.debug(f"Added label '{new_label}' to {repo.owner}/{repo.name}/issue/{issue['number']}") # noqa: E501

def delete_label(self, repo: Repository, *, name: str) -> None:
"""Delete a GitHub issue label.

Expand Down
4 changes: 2 additions & 2 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,10 +194,10 @@ def test_sync_dryrun(
assert result.exit_code == 0

output = (
"This would delete the following labels:\n"
" - infra\n"
"This would update the following labels:\n"
" - bug\n"
"This would delete the following labels:\n"
" - infra\n"
"This would create the following labels:\n"
" - dependencies\n"
"This would NOT modify the following labels:\n"
Expand Down