Skip to content

Commit 16df442

Browse files
committed
Merge branch 'load-repo-info' (resolve #5)
2 parents bddf363 + 50a406b commit 16df442

File tree

9 files changed

+482
-168
lines changed

9 files changed

+482
-168
lines changed

src/labels/cli.py

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,27 @@
33
import sys
44
import typing
55

6+
import attr
67
import click
78
from requests.auth import HTTPBasicAuth
89

9-
from labels import __version__
10+
from labels import __version__, utils
1011
from labels.exceptions import LabelsException
11-
from labels.github import Client, Label
12+
from labels.github import Client, Label, Repository
1213
from labels.io import write_labels, read_labels
1314
from labels.log import create_logger
1415

1516
Labels_Dict = typing.Dict[str, Label]
1617

1718

19+
@attr.s(auto_attribs=True)
20+
class LabelsContext:
21+
"""Extra context for label CLI commands."""
22+
23+
client: Client
24+
repository: typing.Optional[Repository] = None
25+
26+
1827
@click.group()
1928
@click.pass_context
2029
@click.version_option(__version__, "-V", "--version", prog_name="labels")
@@ -45,13 +54,49 @@ def labels(ctx, username: str, token: str, verbose: bool) -> None:
4554
else:
4655
logger.setLevel(logging.INFO)
4756

48-
ctx.obj = Client(HTTPBasicAuth(username, token))
57+
ctx.obj = LabelsContext(Client(HTTPBasicAuth(username, token)))
58+
59+
60+
@click.pass_obj
61+
def default_owner(labels_context: LabelsContext) -> str:
62+
"""Load repository owner information from the local working tree."""
63+
if labels_context.repository is None:
64+
repository = utils.load_repository_info()
65+
if repository is None:
66+
raise click.BadParameter("Unable to load repository information.")
67+
labels_context.repository = repository
68+
return labels_context.repository.owner
69+
70+
71+
@click.pass_obj
72+
def default_repo(labels_context: LabelsContext) -> str:
73+
"""Load repository name information from the local working tree."""
74+
if labels_context.repository is None:
75+
repository = utils.load_repository_info()
76+
if repository is None:
77+
raise click.BadParameter("Unable to load repository information.")
78+
labels_context.repository = repository
79+
return labels_context.repository.name
4980

5081

5182
@labels.command("fetch")
5283
@click.pass_obj
53-
@click.option("-o", "--owner", help="GitHub owner name", type=str, required=True)
54-
@click.option("-r", "--repo", help="GitHub repository name", type=str, required=True)
84+
@click.option(
85+
"-o",
86+
"--owner",
87+
help="GitHub owner name",
88+
type=str,
89+
default=default_owner,
90+
required=True,
91+
)
92+
@click.option(
93+
"-r",
94+
"--repo",
95+
help="GitHub repository name",
96+
type=str,
97+
default=default_repo,
98+
required=True,
99+
)
55100
@click.option(
56101
"-f",
57102
"--filename",
@@ -60,13 +105,16 @@ def labels(ctx, username: str, token: str, verbose: bool) -> None:
60105
type=click.Path(),
61106
required=True,
62107
)
63-
def fetch_cmd(client: Client, owner: str, repo: str, filename: str) -> None:
108+
def fetch_cmd(context: LabelsContext, owner: str, repo: str, filename: str) -> None:
64109
"""Fetch labels for a GitHub repository.
65110
66111
This will write the labels information to disk to the specified filename.
67112
"""
113+
114+
repository = Repository(owner, repo)
115+
68116
try:
69-
labels = client.list_labels(owner, repo)
117+
labels = context.client.list_labels(repository)
70118
except LabelsException as exc:
71119
click.echo(str(exc))
72120
sys.exit(1)
@@ -79,8 +127,22 @@ def fetch_cmd(client: Client, owner: str, repo: str, filename: str) -> None:
79127

80128
@labels.command("sync")
81129
@click.pass_obj
82-
@click.option("-o", "--owner", help="GitHub owner name", type=str, required=True)
83-
@click.option("-r", "--repo", help="GitHub repository name", type=str, required=True)
130+
@click.option(
131+
"-o",
132+
"--owner",
133+
help="GitHub owner name",
134+
type=str,
135+
default=default_owner,
136+
required=True,
137+
)
138+
@click.option(
139+
"-r",
140+
"--repo",
141+
help="GitHub repository name",
142+
type=str,
143+
default=default_repo,
144+
required=True,
145+
)
84146
@click.option("-n", "--dryrun", help="Do not modify remote labels", is_flag=True)
85147
@click.option(
86148
"-f",
@@ -91,7 +153,7 @@ def fetch_cmd(client: Client, owner: str, repo: str, filename: str) -> None:
91153
required=True,
92154
)
93155
def sync_cmd(
94-
client: Client, owner: str, repo: str, filename: str, dryrun: bool
156+
context: LabelsContext, owner: str, repo: str, filename: str, dryrun: bool
95157
) -> None:
96158
"""Sync labels with a GitHub repository.
97159
@@ -105,8 +167,10 @@ def sync_cmd(
105167

106168
local_labels = read_labels(filename)
107169

170+
repository = Repository(owner, repo)
171+
108172
try:
109-
remote_labels = {l.name: l for l in client.list_labels(owner, repo)}
173+
remote_labels = {l.name: l for l in context.client.list_labels(repository)}
110174
except LabelsException as exc:
111175
click.echo(str(exc), err=True)
112176
sys.exit(1)
@@ -152,21 +216,21 @@ def sync_cmd(
152216

153217
for name in labels_to_delete.keys():
154218
try:
155-
client.delete_label(owner, repo, name=name)
219+
context.client.delete_label(repository, name=name)
156220
except LabelsException as exc:
157221
click.echo(str(exc), err=True)
158222
failures.append(name)
159223

160224
for name, label in labels_to_update.items():
161225
try:
162-
client.edit_label(owner, repo, name=name, label=label)
226+
context.client.edit_label(repository, name=name, label=label)
163227
except LabelsException as exc:
164228
click.echo(str(exc), err=True)
165229
failures.append(name)
166230

167231
for name, label in labels_to_create.items():
168232
try:
169-
client.create_label(owner, repo, label=label)
233+
context.client.create_label(repository, label=label)
170234
except LabelsException as exc:
171235
click.echo(str(exc), err=True)
172236
failures.append(name)

src/labels/github.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
from labels.exceptions import GitHubException
88

99

10+
@attr.s(auto_attribs=True, frozen=True)
11+
class Repository:
12+
"""Represents a GitHub repository."""
13+
14+
owner: str
15+
name: str
16+
17+
1018
def not_read_only(attr: attr.Attribute, value: typing.Any) -> bool:
1119
"""Filter for attr that checks for a leading underscore."""
1220
return not attr.name.startswith("_")
@@ -48,17 +56,17 @@ def __init__(
4856
self.session = requests.Session()
4957
self.session.auth = auth
5058

51-
def list_labels(self, owner: str, repo: str) -> typing.List[Label]:
59+
def list_labels(self, repo: Repository) -> typing.List[Label]:
5260
"""Return the list of Labels from the repository.
5361
5462
GitHub API docs:
5563
https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository
5664
"""
5765
logger = logging.getLogger("labels")
58-
logger.debug(f"Requesting labels for {owner}/{repo}")
66+
logger.debug(f"Requesting labels for {repo.owner}/{repo.name}")
5967

6068
response = self.session.get(
61-
f"{self.base_url}/repos/{owner}/{repo}/labels",
69+
f"{self.base_url}/repos/{repo.owner}/{repo.name}/labels",
6270
headers={"Accept": "application/vnd.github.symmetra-preview+json"},
6371
)
6472

@@ -71,17 +79,17 @@ def list_labels(self, owner: str, repo: str) -> typing.List[Label]:
7179

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

74-
def get_label(self, owner: str, repo: str, *, name: str) -> Label:
82+
def get_label(self, repo: Repository, *, name: str) -> Label:
7583
"""Return a single Label from the repository.
7684
7785
GitHub API docs:
7886
https://developer.github.com/v3/issues/labels/#get-a-single-label
7987
"""
8088
logger = logging.getLogger("labels")
81-
logger.debug(f"Requesting label '{name}' for {owner}/{repo}")
89+
logger.debug(f"Requesting label '{name}' for {repo.owner}/{repo.name}")
8290

8391
response = self.session.get(
84-
f"{self.base_url}/repos/{owner}/{repo}/labels/{name}",
92+
f"{self.base_url}/repos/{repo.owner}/{repo.name}/labels/{name}",
8593
headers={"Accept": "application/vnd.github.symmetra-preview+json"},
8694
)
8795

@@ -94,17 +102,17 @@ def get_label(self, owner: str, repo: str, *, name: str) -> Label:
94102

95103
return Label(**response.json())
96104

97-
def create_label(self, owner: str, repo: str, *, label: Label) -> Label:
105+
def create_label(self, repo: Repository, *, label: Label) -> Label:
98106
"""Create a new Label for the repository.
99107
100108
GitHub API docs:
101109
https://developer.github.com/v3/issues/labels/#create-a-label
102110
"""
103111
logger = logging.getLogger("labels")
104-
logger.debug(f"Creating label '{label.name}' for {owner}/{repo}")
112+
logger.debug(f"Creating label '{label.name}' for {repo.owner}/{repo.name}")
105113

106114
response = self.session.post(
107-
f"{self.base_url}/repos/{owner}/{repo}/labels",
115+
f"{self.base_url}/repos/{repo.owner}/{repo.name}/labels",
108116
headers={"Accept": "application/vnd.github.symmetra-preview+json"},
109117
json=label.params_dict,
110118
)
@@ -118,17 +126,17 @@ def create_label(self, owner: str, repo: str, *, label: Label) -> Label:
118126

119127
return Label(**response.json())
120128

121-
def edit_label(self, owner: str, repo: str, *, name: str, label: Label) -> Label:
129+
def edit_label(self, repo: Repository, *, name: str, label: Label) -> Label:
122130
"""Update a GitHub issue label.
123131
124132
GitHub API docs:
125133
https://developer.github.com/v3/issues/labels/#update-a-label
126134
"""
127135
logger = logging.getLogger("labels")
128-
logger.debug(f"Editing label '{name}' for {owner}/{repo}")
136+
logger.debug(f"Editing label '{name}' for {repo.owner}/{repo.name}")
129137

130138
response = self.session.patch(
131-
f"{self.base_url}/repos/{owner}/{repo}/labels/{name}",
139+
f"{self.base_url}/repos/{repo.owner}/{repo.name}/labels/{name}",
132140
headers={"Accept": "application/vnd.github.symmetra-preview+json"},
133141
json=label.params_dict,
134142
)
@@ -142,17 +150,17 @@ def edit_label(self, owner: str, repo: str, *, name: str, label: Label) -> Label
142150

143151
return Label(**response.json())
144152

145-
def delete_label(self, owner: str, repo: str, *, name: str) -> None:
153+
def delete_label(self, repo: Repository, *, name: str) -> None:
146154
"""Delete a GitHub issue label.
147155
148156
GitHub API docs:
149157
https://developer.github.com/v3/issues/labels/#delete-a-label
150158
"""
151159
logger = logging.getLogger("labels")
152-
logger.debug(f"Deleting label '{name}' for {owner}/{repo}")
160+
logger.debug(f"Deleting label '{name}' for {repo.owner}/{repo.name}")
153161

154162
response = self.session.delete(
155-
f"{self.base_url}/repos/{owner}/{repo}/labels/{name}"
163+
f"{self.base_url}/repos/{repo.owner}/{repo.name}/labels/{name}"
156164
)
157165

158166
if response.status_code != 204:

src/labels/log.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ class ContextFilter(logging.Filter):
77
"""Logging filter to add the click command to the record."""
88

99
def filter(self, record: logging.LogRecord) -> bool:
10-
ctx = click.get_current_context()
10+
ctx = click.get_current_context(silent=True)
11+
if not ctx:
12+
return False
13+
1114
setattr(record, "cmd", ctx.command.name)
1215
return True
1316

src/labels/utils.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import re
2+
import shlex
3+
import subprocess
4+
import typing
5+
6+
7+
from labels.github import Repository
8+
9+
10+
REMOTE_REGEX = re.compile(
11+
r"^(https|git)(:\/\/|@)github\.com[\/:](?P<owner>[^\/:]+)\/(?P<name>.+).git$"
12+
)
13+
14+
15+
def load_repository_info(remote_name: str = "origin") -> typing.Optional[Repository]:
16+
"""Load repository information from the local working tree.
17+
18+
HTTPS url format -> 'https://github.com/owner/name.git'
19+
SSH url format -> '[email protected]:owner/name.git'
20+
"""
21+
22+
proc = subprocess.run(
23+
shlex.split(f"git remote get-url {remote_name}"),
24+
stdout=subprocess.PIPE,
25+
encoding="utf-8",
26+
)
27+
28+
if proc.returncode != 0:
29+
return None
30+
31+
match = REMOTE_REGEX.match(proc.stdout.strip())
32+
33+
if match is None:
34+
return None
35+
36+
return Repository(owner=match.group("owner"), name=match.group("name"))

0 commit comments

Comments
 (0)