Skip to content

Commit 8915b6a

Browse files
committed
Implement /gh search files
1 parent 5656f96 commit 8915b6a

File tree

6 files changed

+144
-1
lines changed

6 files changed

+144
-1
lines changed

bot/pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ dependencies = [
1515
"sqlmodel>=0.0.19",
1616
"psycopg2-binary>=2.9.9",
1717
"githubkit[auth-app]>=0.11.8",
18+
"pfzy>=0.3.4",
19+
"more-itertools>=10.5.0",
1820
]
1921

2022
[tool.rye]

bot/src/ghutils/cogs/app_commands/github.py

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,22 @@
44
import textwrap
55
import uuid
66
from datetime import datetime
7+
from pathlib import Path
78
from typing import Any
89

10+
import pfzy
911
from discord import Color, Embed, Interaction, app_commands
12+
from discord.app_commands import Range
1013
from discord.ext.commands import GroupCog
1114
from discord.ui import Button, View
1215
from githubkit import GitHub
1316
from githubkit.exception import GitHubException
1417
from githubkit.rest import Issue, PullRequest, SimpleUser
18+
from more_itertools import consecutive_groups
19+
from yarl import URL
1520

16-
from ghutils.core.cog import GHUtilsCog
21+
from ghutils.core.cog import GHUtilsCog, SubGroup
22+
from ghutils.core.types import LoginState, NotLoggedInError
1723
from ghutils.db.models import (
1824
UserGitHubTokens,
1925
UserLogin,
@@ -244,6 +250,122 @@ async def status(
244250

245251
await respond_with_visibility(interaction, visibility, embed=embed)
246252

253+
class Search(SubGroup):
254+
"""Search for things on GitHub."""
255+
256+
@app_commands.command()
257+
async def files(
258+
self,
259+
interaction: Interaction,
260+
repo: FullRepositoryOption,
261+
query: Range[str, 1, 128],
262+
ref: Range[str, 1, 255] | None = None,
263+
exact: bool = False,
264+
limit: Range[int, 1, 25] = 5,
265+
visibility: MessageVisibility = "private",
266+
):
267+
"""Search for files in a repository by name.
268+
269+
Args:
270+
ref: Branch name, tag name, or commit to search in. Defaults to the
271+
default branch of the repo.
272+
exact: If true, use exact search; otherwise use fuzzy search.
273+
limit: Maximum number of results to show.
274+
"""
275+
276+
async with self.bot.github_app(interaction) as (github, state):
277+
if state != LoginState.LOGGED_IN:
278+
raise NotLoggedInError()
279+
280+
if ref is None:
281+
ref = repo.default_branch
282+
283+
tree = await gh_request(
284+
github.rest.git.async_get_tree(
285+
repo.owner.login,
286+
repo.name,
287+
ref,
288+
recursive="1",
289+
)
290+
)
291+
292+
sha = tree.sha[:12]
293+
tree_dict = {item.path: item for item in tree.tree if item.path}
294+
295+
matches = await pfzy.fuzzy_match(
296+
query,
297+
list(tree_dict.keys()),
298+
scorer=pfzy.substr_scorer if exact else pfzy.fzy_scorer,
299+
)
300+
301+
embed = (
302+
Embed(
303+
title="File search results",
304+
)
305+
.set_author(
306+
name=repo.full_name,
307+
url=repo.html_url,
308+
icon_url=repo.owner.avatar_url,
309+
)
310+
.set_footer(
311+
text=f"{repo.full_name}@{ref} • Total results: {len(matches)}",
312+
)
313+
)
314+
315+
# code search only works on the default branch
316+
# so don't add the link otherwise, since it won't be useful
317+
if ref == repo.default_branch:
318+
embed.url = str(
319+
URL("https://github.com/search").with_query(
320+
type="code",
321+
q=f'repo:{repo.full_name} path:"{query}"',
322+
)
323+
)
324+
325+
if matches:
326+
embed.color = Color.green()
327+
else:
328+
embed.description = "⚠️ No matches found."
329+
embed.color = Color.red()
330+
331+
size = 0
332+
for match in matches[:limit]:
333+
path: str = match["value"]
334+
indices: list[int] = match["indices"]
335+
336+
item = tree_dict[path]
337+
338+
icon = "📁" if item.type == "tree" else "📄"
339+
url = f"https://github.com/{repo.full_name}/{item.type}/{sha}/{item.path}"
340+
341+
parts = list[str]()
342+
index = 0
343+
for group in consecutive_groups(indices):
344+
group = list(group)
345+
parts += [
346+
# everything before the start of the group
347+
path[index : group[0]],
348+
"**",
349+
# everything in the group
350+
path[group[0] : group[-1] + 1],
351+
"**",
352+
]
353+
index = group[-1] + 1
354+
# everything after the last group
355+
parts.append(path[index:])
356+
highlighted_path = "".join(parts)
357+
358+
name = f"{icon} {Path(path).name}"
359+
value = f"[{highlighted_path}]({url})"
360+
361+
size += len(name) + len(value)
362+
if size > 5000:
363+
break
364+
365+
embed.add_field(name=name, value=value, inline=False)
366+
367+
await respond_with_visibility(interaction, visibility, embed=embed)
368+
247369

248370
def _discord_date(timestamp: int | float | datetime):
249371
match timestamp:

bot/src/ghutils/core/tree.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
TransformerError,
1111
)
1212

13+
from ghutils.core.types import NotLoggedInError
14+
1315

1416
class GHUtilsCommandTree(CommandTree):
1517
async def on_error(self, interaction: Interaction, error: AppCommandError):
@@ -35,6 +37,9 @@ async def on_error(self, interaction: Interaction, error: AppCommandError):
3537
value=str(value),
3638
inline=False,
3739
)
40+
case NotLoggedInError():
41+
embed.title = "Not logged in!"
42+
embed.description = "You must be logged in with GitHub to use this command. Use `/gh login` to log in, then try again."
3843
case _:
3944
await super().on_error(interaction, error)
4045
embed.title = "Command failed!"

bot/src/ghutils/core/types.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
from enum import Enum, auto
22

3+
from discord.app_commands import AppCommandError
4+
35

46
class LoginState(Enum):
57
LOGGED_IN = auto()
68
LOGGED_OUT = auto()
79
EXPIRED = auto()
10+
11+
12+
class NotLoggedInError(AppCommandError):
13+
pass

requirements-dev.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ markupsafe==2.1.5
122122
# via jinja2
123123
mdurl==0.1.2
124124
# via markdown-it-py
125+
more-itertools==10.5.0
126+
# via ghutils-bot
125127
multidict==6.0.5
126128
# via aiohttp
127129
# via yarl
@@ -131,6 +133,8 @@ object-ci @ git+https://github.com/object-Object/ci@d49aad2be03e0c6995be20d505d3
131133
# via ghutils-infrastructure
132134
orjson==3.10.5
133135
# via fastapi
136+
pfzy==0.3.4
137+
# via ghutils-bot
134138
platformdirs==4.2.2
135139
# via virtualenv
136140
pre-commit==3.7.1

requirements.lock

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,13 +114,17 @@ markupsafe==2.1.5
114114
# via jinja2
115115
mdurl==0.1.2
116116
# via markdown-it-py
117+
more-itertools==10.5.0
118+
# via ghutils-bot
117119
multidict==6.0.5
118120
# via aiohttp
119121
# via yarl
120122
object-ci @ git+https://github.com/object-Object/ci@d49aad2be03e0c6995be20d505d32aa54a6b7f89
121123
# via ghutils-infrastructure
122124
orjson==3.10.5
123125
# via fastapi
126+
pfzy==0.3.4
127+
# via ghutils-bot
124128
psycopg2-binary==2.9.9
125129
# via ghutils-bot
126130
publication==0.0.3

0 commit comments

Comments
 (0)