๐Ÿ™ Browse GitHub repos and perform actions on them ๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ’ป

https://user-images.githubusercontent.com/2759340/190688251-8a7a0033-9cce-4dc2-bafe-9c02d06e9f00.mov

Install on Script Kit


Lets you browse your GitHub repos and perform actions on them such as coping/opening the URL or listing the issues/pulls/branches. This script could be improved way more so feel free to leave a comment below with some suggestions.


// Name: GitHub Repos
// Description: List GitHub repositories and perform actions on them
// Author: Vogelino
// Twitter: @soyvogelino
import "@johnlindquist/kit";
const { Octokit } = await npm("octokit");
const shodwon = await npm("showdown");
const converter = new shodwon.Converter({
ghMentions: true,
emoji: true,
});
converter.setFlavor("github");
interface RawRepositoryType {
id: string;
name: string;
html_url: string;
full_name: string;
visibility: string[];
description: string;
homepage?: string;
owner: {
login: string;
};
open_issues_count: number;
}
interface RawIssueType {
id: string;
title: string;
body: string;
labels: {
name: string;
}[];
number: string;
user: {
login: string;
};
html_url: string;
}
interface RawBranchName {
id: string;
name: string;
protected: boolean;
}
interface OptionType<ValueType = unknown> {
name: string;
descritpion?: string;
preview?: string;
value: ValueType;
}
const auth = await env(`GITHUB_ACCESS_TOKEN`, "Enter your GitHub access token");
const octokit = new Octokit({ auth });
const {
data: { login },
} = await octokit.rest.users.getAuthenticated();
const universalOptions = [
{
name: "Browse",
description: "Open the url in your default browser",
value: "browse",
},
{
name: "Copy URL",
description: "Copy the URL to the clipboard",
value: "copy",
},
];
const mapRawRepo = (repo: RawRepositoryType) => ({
name: repo.full_name,
description: [
repo.visibility[0].toUpperCase() + repo.visibility.slice(1),
repo.description,
repo.homepage,
]
.filter(Boolean)
.join(" ยท "),
value: repo,
});
const mapReposResponse = (response: { data: RawRepositoryType[] }) =>
(response.data || []).map(mapRawRepo);
async function fetchAllRepos() {
return await octokit.paginate(
octokit.rest.repos.listForAuthenticatedUser,
{ sort: "updated", per_page: 100 },
mapReposResponse
);
}
async function fetchRecentRepos() {
const res = await octokit.request("GET /user/repos", {
sort: "updated",
per_page: 50,
});
return res.data;
}
async function fetchOwnerRepos() {
const res = await octokit.request("GET /user/repos", {
sort: "updated",
per_page: 50,
affiliation: "owner",
});
return res.data;
}
function createIssuePullHandler(key: "pull" | "issue") {
return async (repo: RawRepositoryType) => {
const res = await octokit.request(`GET /repos/{owner}/{repo}/${key}s`, {
owner: repo.owner.login,
repo: repo.name,
pulls: key === "pull",
});
let { data: items } = res as { data: RawIssueType[] };
if (key === "issue") {
const pullsRes = (await octokit.request(
`GET /repos/{owner}/{repo}/pulls`,
{
owner: repo.owner.login,
repo: repo.name,
}
)) as { data: RawIssueType[] };
items = items.filter(
({ number }) => !pullsRes.data.find((p) => p.number === number)
);
}
if (items.length === 0) {
await div(`<div class="p-4 bg-white">No ${key}s</div>`);
return;
}
const itemSelected = await arg(
`Search for ${key}s`,
items.map((i) => ({
name: i.title,
description: [
`#${i.number}`,
i.user.login,
i.labels.map((l) => l.name).join(", "),
]
.filter(Boolean)
.join(" ยท "),
preview: `
<style>p,h1,h2,h3 { margin-bottom: 8px; }</style>
<div class="p-4 bg-white">
<h1>${i.title}</h1>
<small>By @${i.user.login} ยท ${i.labels
.map((l) => l.name)
.join(", ")}</small>
<hr/><br />
${i?.body ? converter.makeHtml(i.body) : "โ€“"}
</div>
`,
value: i,
}))
);
const action = await arg("Select an action to perform", [
...universalOptions,
{
name: `Copy ${key} number`,
description: `Copy ${key} number to clipboard for reference elswhere (eg. branch-name)`,
value: "number",
},
{
name: `Close ${key}`,
description: `Close ${key}`,
value: "close",
},
]);
switch (action) {
case "browse":
await browse(itemSelected.html_url);
exit();
case "copy":
await copy(itemSelected.html_url);
exit();
case "number":
await copy(`${itemSelected.number}`);
exit();
case "close":
await await octokit.request(
`PATCH /repos/{owner}/{repo}/${key}s/{${key}_number}`,
{
owner: repo.owner.login,
repo: repo.name,
[`${key}_number`]: itemSelected.number,
state: "closed",
}
);
break;
}
notify(`${action} successful`);
const nextHanlder = createIssuePullHandler(key);
await nextHanlder(repo);
};
}
async function getAllBranches(repo: RawRepositoryType) {
const { data } = await octokit.request(`GET /repos/{owner}/{repo}/branches`, {
owner: repo.owner.login,
repo: repo.name,
});
const items = data as RawBranchName[];
if (items.length === 0) {
await div(`<div class="p-4 bg-white">No branches</div>`);
return;
}
const branchSelected = await arg(
`Search for branches`,
items.map((b) => ({
name: b.name,
description: b.protected ? "Protected" : undefined,
value: b,
}))
);
const action = await arg("Select an action to perform", [
{
name: `Copy name`,
description: `Copy name to clipboard for reference elswhere (eg. in issue)`,
value: "name",
},
{
name: `Rename`,
value: "rename",
},
]);
switch (action) {
case "name":
await copy(branchSelected.name);
exit();
case "rename":
const newName = await arg(
`What should the new name be? (was ${branchSelected.name})`
);
await octokit.request(
`POST /repos/{owner}/{repo}/branches/{branch}/rename`,
{
owner: repo.owner.login,
repo: repo.name,
branch: branchSelected.name,
new_name: newName,
}
);
break;
}
notify(`${action} successful`);
await getAllBranches(repo);
}
const issueHandler = createIssuePullHandler(`issue`);
const pullHandler = createIssuePullHandler(`pull`);
function getTabHandler(getter: () => Promise<OptionType<RawRepositoryType>[]>) {
return async function handler() {
const repos = await getter();
const repoSelected = await arg(`Hello ${login}. Search for a repo`, repos);
if (repos.length === 0) {
await div(`<div class="p-4 bg-white">No repos</div>`);
await handler();
}
const action = await arg(
"Select an action to perform",
[
...universalOptions,
repoSelected.open_issues_count > 0 && {
name: "Recent issues",
description: "List top 10 most recent open issues",
value: "issues",
},
{
name: "Pull Requests",
description: "List top 10 most recent PRs",
value: "prs",
},
{
name: "Branches",
description: "List branches",
value: "branches",
},
].filter(Boolean)
);
switch (action) {
case "browse":
await browse(repoSelected.html_url);
exit();
case "copy":
await copy(repoSelected.html_url);
exit();
case "issues":
await issueHandler(repoSelected);
break;
case "prs":
await pullHandler(repoSelected);
break;
case "branches":
await getAllBranches(repoSelected);
break;
}
notify(`${action} successful`);
await handler();
};
}
const recentTab = getTabHandler(fetchRecentRepos);
onTab("Recent", recentTab);
onTab("Owner", getTabHandler(fetchOwnerRepos));
onTab("All", getTabHandler(fetchAllRepos));
await recentTab();