🚀

Github CLIを活用して無敵のエンジニアになる

2023/08/27に公開2

みなさん Github CLI 使ってますか?
私は PR 建てたりとかなんでも CLI で済ませたいので毎日使っています。
https://cli.github.com/

Github CLI は開発速度が早くガンガン機能が追加されるので、意外と高機能でいろんなことができます。
今回は便利なGithub CLI の使い方を紹介します。

install

公式の README に従ってください。
https://github.com/cli/cli

だいたいの package manager に対応しているのでえらい。
macOS であれば homebrew を使うのが楽です。

$ brew install gh

コマンド名が gh なので、以降は gh cli と呼びます。

認証を通す

Github のアカウントで CLI の認証を通す必要があります。

$ gh auth login

だけでブラウザ経由の認証が行われますが、この場合権限が多少弱いので利用できる機能に制限があります。

それで特に困らなければ問題ないですが、私は必要な権限を振った Private Access Token を生成して Token 経由で認証しています。

$ gh auth login --with-token < GITHUB_TOKEN_FILE_PATH

これで gh cli を利用できるようになりました。ここからは gh cli の使い方を紹介します。

🐣 基本: 今いるリポジトリの内容を確認する

リポジトリ操作を通してgh cliの基本的な使い方を紹介します。

cli で確認する

$ gh repo view

で、今いるリポジトリの内容を cli 上で確認することができます。グラフィカルでうれしいね。

ブラウザで確認する

この際、

$ gh repo view --web

のように --web 引数を付与することで、ブラウザでこのリポジトリを閲覧することもできます

メタデータも確認する

--json パラメータを指定することで、表示されていないメタデータも含めた JSON 形式でリポジトリ情報を取得できるようになります。

引数にはカンマ区切りでほしいフィールド名の string を指定する必要があり、利用できるフィールドは

$ gh pr view --json

などすると確認できます。

現時点で利用できるフィールドをすべて指定するとこんな感じです。
この際、最初の cli の認証方法によっては利用できないフィールドがあるかもしれません。その場合は必要な権限を持ったトークン認証に切り替えるか、権限の足りないフィールドを削除してください。

$ gh repo view --json 'assignableUsers,codeOfConduct,contactLinks,createdAt,defaultBranchRef,deleteBranchOnMerge,description,diskUsage,forkCount,fundingLinks,hasIssuesEnabled,hasProjectsEnabled,hasWikiEnabled,homepageUrl,id,isArchived,isBlankIssuesEnabled,isEmpty,isFork,isInOrganization,isMirror,isPrivate,isSecurityPolicyEnabled,isTemplate,isUserConfigurationRepository,issueTemplates,issues,labels,languages,latestRelease,licenseInfo,mentionableUsers,mergeCommitAllowed,milestones,mirrorUrl,name,nameWithOwner,openGraphImageUrl,owner,parent,primaryLanguage,projects,pullRequestTemplates,pullRequests,pushedAt,rebaseMergeAllowed,repositoryTopics,securityPolicyUrl,squashMergeAllowed,sshUrl,stargazerCount,templateRepository,updatedAt,url,usesCustomOpenGraphImage,viewerCanAdminister,viewerDefaultCommitEmail,viewerDefaultMergeMethod,viewerHasStarred,viewerPermission,viewerPossibleCommitEmails,viewerSubscription,watchers'

レスポンスはこんな感じの JSON です。長いので折りたたみました。

JSON
{
  "assignableUsers": [
    {
      "id": "MDQ6VXNlcjIwODk3NDM=",
      "login": "andyfeller",
      "name": "Andy Feller"
    },
    {
      "id": "MDQ6VXNlcjgwNDIwMjYw",
      "login": "cliAutomation",
      "name": "CLI Automation"
    },
    { "id": "MDQ6VXNlcjU0NDcxMTg=", "login": "cmbrose", "name": "Caleb Brose" },
    {
      "id": "MDQ6VXNlcjIyMDM3NzY5",
      "login": "dmgardiner25",
      "name": "David Gardiner"
    },
    { "id": "MDQ6VXNlcjE1OTY3Njg3", "login": "eljog", "name": "Eljo George" },
    {
      "id": "MDQ6VXNlcjIzMzYyNTM5",
      "login": "GrantBirki",
      "name": "Grant Birkinbine"
    },
    { "id": "MDQ6VXNlcjc0NjAyMA==", "login": "jkeech", "name": "John Keech" },
    {
      "id": "MDQ6VXNlcjIzMjQ2NTk0",
      "login": "joshspicer",
      "name": "Josh Spicer"
    },
    { "id": "MDQ6VXNlcjY4ODkwMDg=", "login": "jtpetty", "name": "Tommy Petty" },
    {
      "id": "MDQ6VXNlcjExNDkyNDY=",
      "login": "mntlty",
      "name": "Ariel Deitcher"
    },
    { "id": "MDQ6VXNlcjc5Njk3Nzk=", "login": "samcoe", "name": "Sam Coe" },
    {
      "id": "MDQ6VXNlcjgwMTMwMTgy",
      "login": "thomas-sickert",
      "name": "Thomas Sickert"
    },
    {
      "id": "MDQ6VXNlcjE2MTE1MTA=",
      "login": "williammartin",
      "name": "William Martin"
    }
  ],
  "codeOfConduct": {
    "key": "contributor_covenant",
    "name": "Contributor Covenant",
    "url": "https://github.com/cli/cli/blob/trunk/.github/CODE-OF-CONDUCT.md"
  },
  "contactLinks": [
    {
      "about": "For general-purpose questions and answers, see the Discussions section.",
      "name": "Ask a question on how to use GitHub CLI",
      "url": "https://github.com/cli/cli/discussions"
    },
    {
      "about": "Please check out the GitHub community forum for discussions about the GitHub API.",
      "name": "Ask a question about the GitHub API",
      "url": "https://github.community/c/github-ecosystem/37"
    }
  ],
  "createdAt": "2019-10-03T15:24:53Z",
  "defaultBranchRef": { "name": "trunk" },
  "deleteBranchOnMerge": true,
  "description": "GitHub’s official command line tool",
  "diskUsage": 27405,
  "forkCount": 5399,
  "fundingLinks": [],
  "hasIssuesEnabled": true,
  "hasProjectsEnabled": true,
  "hasWikiEnabled": false,
  "homepageUrl": "https://cli.github.com",
  "id": "MDEwOlJlcG9zaXRvcnkyMTI2MTMwNDk=",
  "isArchived": false,
  "isBlankIssuesEnabled": true,
  "isEmpty": false,
  "isFork": false,
  "isInOrganization": true,
  "isMirror": false,
  "isPrivate": false,
  "isSecurityPolicyEnabled": true,
  "isTemplate": false,
  "isUserConfigurationRepository": false,
  "issueTemplates": [
    {
      "name": "🐛 Bug report",
      "title": "",
      "body": "### Describe the bug\n\nA clear and concise description of what the bug is. Include version by typing `gh --version`.\n\n### Steps to reproduce the behavior\n\n1. Type this '...'\n2. View the output '....'\n3. See error\n\n### Expected vs actual behavior\n\nA clear and concise description of what you expected to happen and what actually happened.\n\n### Logs\n\nPaste the activity from your command line. Redact if needed.\n",
      "about": "Report a bug or unexpected behavior while using GitHub CLI"
    },
    {
      "name": "📣 Feedback",
      "title": "",
      "body": "# CLI Feedback\n\nYou can use this template to give us structured feedback or just wipe it and leave us a note. Thank you!\n\n## What have you loved?\n\n_eg \"the nice colors\"_\n\n## What was confusing or gave you pause?\n\n_eg \"it did something unexpected\"_\n\n## Are there features you'd like to see added?\n\n_eg \"gh cli needs mini-games\"_\n\n## Anything else?\n\n_eg \"have a nice day\"_\n",
      "about": "Give us general feedback about the GitHub CLI"
    },
    {
      "name": "⭐ Submit a request",
      "title": "",
      "body": "### Describe the feature or problem you’d like to solve\n\nA clear and concise description of what the feature or problem is.\n\n### Proposed solution\n\nHow will it benefit CLI and its users?\n\n### Additional context\n\nAdd any other context like screenshots or mockups are helpful, if applicable.\n",
      "about": "Surface a feature or problem that you think should be solved"
    }
  ],
  "issues": { "totalCount": 427 },
  "labels": [
    {
      "id": "MDU6TGFiZWwxNTkzNDg0NjIw",
      "name": "bug",
      "description": "Something isn't working",
      "color": "d73a4a"
    },
    {
      "id": "MDU6TGFiZWwxNjA4MjgzNzU0",
      "name": "tracking issue",
      "description": "",
      "color": "ffa8da"
    },
    {
      "id": "MDU6TGFiZWwxNjc0OTI5MjQ0",
      "name": "blocked",
      "description": "",
      "color": "ff5451"
    },
    {
      "id": "MDU6TGFiZWwxNjgzODYyMjQ5",
      "name": "needs-design",
      "description": "An engineering task needs design to proceed",
      "color": "2db0bf"
    },
    {
      "id": "MDU6TGFiZWwxNjk4MDc2MTcz",
      "name": "enhancement",
      "description": "a request to improve CLI",
      "color": "0dd8ac"
    },
    {
      "id": "MDU6TGFiZWwxNzAwMTQ3NzMz",
      "name": "windows",
      "description": "",
      "color": "0258e2"
    },
    {
      "id": "MDU6TGFiZWwxNzI2NTkzODkw",
      "name": "needs-user-input",
      "description": "",
      "color": "830eb5"
    },
    {
      "id": "MDU6TGFiZWwxODE1OTc2ODgz",
      "name": "linux",
      "description": "",
      "color": "3eddb8"
    },
    {
      "id": "MDU6TGFiZWwxODE1OTc4NDIw",
      "name": "tech debt",
      "description": "A chore that addresses technical debt",
      "color": "c9f26a"
    },
    {
      "id": "MDU6TGFiZWwxODI1MDM2NTcx",
      "name": "packaging",
      "description": "",
      "color": "56efa8"
    },
    {
      "id": "MDU6TGFiZWwxODI1MDc1NjA1",
      "name": "p2",
      "description": "Affects more than a few users but doesn't prevent core functions",
      "color": "FFA501"
    },
    {
      "id": "MDU6TGFiZWwxODI1MDc2ODcz",
      "name": "p3",
      "description": "Affects a small number of users or is largely cosmetic",
      "color": "eded04"
    },
    {
      "id": "MDU6TGFiZWwxODI1MDc5MTM0",
      "name": "p1",
      "description": "Affects a large population and inhibits work",
      "color": "e00808"
    },
    {
      "id": "MDU6TGFiZWwxODI5MzE4Mjc5",
      "name": "docs",
      "description": "",
      "color": "6cafc9"
    },
    {
      "id": "MDU6TGFiZWwxODQ2MDgzNzk3",
      "name": "needs-investigation",
      "description": "CLI team needs to investigate",
      "color": "d45bd8"
    },
    {
      "id": "MDU6TGFiZWwxODQ2MDg2NjIx",
      "name": "help wanted",
      "description": "Contributions welcome",
      "color": "98f9f0"
    },
    {
      "id": "MDU6TGFiZWwyMDU4NDg1NzU0",
      "name": "config",
      "description": "",
      "color": "f98d75"
    },
    {
      "id": "MDU6TGFiZWwyMDYxMDM5MTY4",
      "name": "checks",
      "description": "",
      "color": "0e92d3"
    },
    {
      "id": "MDU6TGFiZWwyMjExMjMxODE1",
      "name": "auth",
      "description": "related to tokens, authentication state, or oauth",
      "color": "00517a"
    },
    {
      "id": "MDU6TGFiZWwyMjg3MzQwODg2",
      "name": "hackday",
      "description": "PRs that came out of a Hack Day",
      "color": "3ed1ac"
    },
    {
      "id": "MDU6TGFiZWwyMzQxMjgyODAx",
      "name": "accessibility",
      "description": "",
      "color": "9ce9f4"
    },
    {
      "id": "MDU6TGFiZWwyMzUxMDY4MjU0",
      "name": "feedback",
      "description": "",
      "color": "d4c5f9"
    },
    {
      "id": "MDU6TGFiZWwyMzUzNjE5NDY5",
      "name": "actions",
      "description": "",
      "color": "f9e98b"
    },
    {
      "id": "MDU6TGFiZWwyMzg1ODU4OTA5",
      "name": "core",
      "description": "This issue is not accepting PRs from outside contributors",
      "color": "e25944"
    },
    {
      "id": "MDU6TGFiZWwyNDEzNDMxODc2",
      "name": "good first issue",
      "description": "",
      "color": "d0f9a4"
    },
    {
      "id": "MDU6TGFiZWwzMTYyODgzNTQy",
      "name": "extensions",
      "description": "",
      "color": "DAB5B8"
    },
    {
      "id": "LA_kwDODKw3uc7M9vi_",
      "name": "dependencies",
      "description": "Pull requests that update a dependency file",
      "color": "0366d6"
    },
    {
      "id": "LA_kwDODKw3uc7QD3p7",
      "name": "needs-triage",
      "description": "needs to be reviewed",
      "color": "D6393F"
    },
    {
      "id": "LA_kwDODKw3uc7QP8m9",
      "name": "hacktoberfest-accepted",
      "description": "",
      "color": "D93F0B"
    },
    {
      "id": "LA_kwDODKw3uc7SU-oz",
      "name": "discuss",
      "description": "Feature changes that require discussion primarily among the GitHub CLI team",
      "color": "C9DC07"
    },
    {
      "id": "LA_kwDODKw3uc7VXsHA",
      "name": "platform",
      "description": "Problems with the GitHub platform rather than the CLI client",
      "color": "6E4A8E"
    },
    {
      "id": "LA_kwDODKw3uc7Vepyn",
      "name": "external",
      "description": "pull request originating outside of the CLI core team",
      "color": "2700AF"
    },
    {
      "id": "LA_kwDODKw3uc7hiAL3",
      "name": "extension idea",
      "description": "An idea that could make a good GitHub CLI extension",
      "color": "257FDD"
    },
    {
      "id": "LA_kwDODKw3uc75wEcN",
      "name": "go",
      "description": "Pull requests that update Go code",
      "color": "16e2e2"
    },
    {
      "id": "LA_kwDODKw3uc8AAAABE1uvIQ",
      "name": "codespaces",
      "description": "",
      "color": "DFA997"
    },
    {
      "id": "LA_kwDODKw3uc8AAAABGFkABw",
      "name": "github_actions",
      "description": "Pull requests that update GitHub Actions code",
      "color": "000000"
    }
  ],
  "languages": [
    { "size": 3697021, "node": { "name": "Go" } },
    { "size": 3024, "node": { "name": "Makefile" } },
    { "size": 7459, "node": { "name": "Shell" } },
    { "size": 264, "node": { "name": "Batchfile" } }
  ],
  "latestRelease": {
    "name": "GitHub CLI 2.33.0",
    "tagName": "v2.33.0",
    "url": "https://github.com/cli/cli/releases/tag/v2.33.0",
    "publishedAt": "2023-08-21T18:02:45Z"
  },
  "licenseInfo": { "key": "mit", "name": "MIT License", "nickname": "" },
  "mentionableUsers": [
    {
      "id": "MDQ6VXNlcjU5Ng==",
      "login": "probablycorey",
      "name": "Corey Johnson"
    },
    {
      "id": "MDQ6VXNlcjY5MA==",
      "login": "matschaffer",
      "name": "Mat Schaffer"
    },
    { "id": "MDQ6VXNlcjgzNg==", "login": "cdb", "name": "Cameron Booth" },
    { "id": "MDQ6VXNlcjg4Nw==", "login": "mislav", "name": "Mislav Marohnić" },
    {
      "id": "MDQ6VXNlcjYwNTk=",
      "login": "brettbuddin",
      "name": "Brett Buddin"
    },
    {
      "id": "MDQ6VXNlcjk5NTU=",
      "login": "Sixeight",
      "name": "Tomohiro Nishimura"
    },
    { "id": "MDQ6VXNlcjEzNzYw", "login": "joshaber", "name": "Josh Abernathy" },
    { "id": "MDQ6VXNlcjE4NzQx", "login": "silby", "name": "" },
    { "id": "MDQ6VXNlcjE5NzE0", "login": "azu", "name": "azu" },
    {
      "id": "MDQ6VXNlcjIwNTcw",
      "login": "srivatsn",
      "name": "Srivatsn Narayanan"
    },
    {
      "id": "MDQ6VXNlcjIyMzQ4",
      "login": "veverkap",
      "name": "Patrick Veverka"
    },
    { "id": "MDQ6VXNlcjMxNTk3", "login": "plu", "name": "Johannes Plunien" },
    {
      "id": "MDQ6VXNlcjM1ODYx",
      "login": "georgebrock",
      "name": "George Brocklehurst"
    },
    {
      "id": "MDQ6VXNlcjQ2Nzc1",
      "login": "mjpieters",
      "name": "Martijn Pieters"
    },
    { "id": "MDQ6VXNlcjU2OTE5", "login": "ptxmac", "name": "Peter Kristensen" },
    { "id": "MDQ6VXNlcjU3NjQw", "login": "andrewhsu", "name": "Andrew Hsu" },
    { "id": "MDQ6VXNlcjYzMjQx", "login": "ajstiles", "name": "Adam Stiles" },
    {
      "id": "MDQ6VXNlcjYzODc2",
      "login": "ahmedelgabri",
      "name": "Ahmed El Gabri"
    },
    { "id": "MDQ6VXNlcjY5NTk1", "login": "beret", "name": "" },
    { "id": "MDQ6VXNlcjc0NTgz", "login": "inktomi", "name": "Matthew Runo" },
    {
      "id": "MDQ6VXNlcjc4NTM0",
      "login": "TobiX",
      "name": "Tobias Gruetzmacher"
    },
    {
      "id": "MDQ6VXNlcjgzNDgz",
      "login": "graphaelli",
      "name": "Gil Raphaelli"
    },
    { "id": "MDQ6VXNlcjk4NDgy", "login": "vilmibm", "name": "Nate Smith" },
    {
      "id": "MDQ6VXNlcjExNDA5Nw==",
      "login": "0atman",
      "name": "Tristram Oaten"
    },
    {
      "id": "MDQ6VXNlcjExNDYwMQ==",
      "login": "seanbright",
      "name": "Sean Bright"
    },
    {
      "id": "MDQ6VXNlcjExODIwMg==",
      "login": "torgeir",
      "name": "Torgeir Thoresen"
    },
    {
      "id": "MDQ6VXNlcjEyMTI5OQ==",
      "login": "sparr",
      "name": "Clarence \"Sparr\" Risher"
    },
    {
      "id": "MDQ6VXNlcjEyMTMyMg==",
      "login": "leereilly",
      "name": "Lee Reilly"
    },
    {
      "id": "MDQ6VXNlcjEyNzc5MA==",
      "login": "dscho",
      "name": "Johannes Schindelin"
    },
    { "id": "MDQ6VXNlcjEzODU5Nw==", "login": "mmrwoods", "name": "Mark Woods" },
    {
      "id": "MDQ6VXNlcjE0NjM3OA==",
      "login": "muesli",
      "name": "Christian Muehlhaeuser"
    },
    {
      "id": "MDQ6VXNlcjE1MDg1NQ==",
      "login": "iloveitaly",
      "name": "Michael Bianco"
    },
    {
      "id": "MDQ6VXNlcjE1MjU2OQ==",
      "login": "pete-woods",
      "name": "Pete Woods"
    },
    {
      "id": "MDQ6VXNlcjE1Nzg5Mg==",
      "login": "michaelld",
      "name": "Michael Dickens"
    },
    {
      "id": "MDQ6VXNlcjE2NjE3NQ==",
      "login": "thefotios",
      "name": "Fotios Lindiakos"
    },
    { "id": "MDQ6VXNlcjE5MzkzNg==", "login": "simi", "name": "Josef Šimánek" },
    {
      "id": "MDQ6VXNlcjIwMzM2Ng==",
      "login": "sgerrand",
      "name": "Sasha Gerrand"
    },
    { "id": "MDQ6VXNlcjIwNTM3NQ==", "login": "danburzo", "name": "Dan Burzo" },
    {
      "id": "MDQ6VXNlcjIwOTQ3Nw==",
      "login": "markphelps",
      "name": "Mark Phelps"
    },
    {
      "id": "MDQ6VXNlcjIxMDc1NQ==",
      "login": "kevinbluer",
      "name": "Kevin Bluer"
    },
    {
      "id": "MDQ6VXNlcjIyNzQ0Mg==",
      "login": "rex4539",
      "name": "Dimitris Apostolou"
    },
    {
      "id": "MDQ6VXNlcjI0Mjk4OA==",
      "login": "bak1an",
      "name": "Anton Baklanov"
    },
    {
      "id": "MDQ6VXNlcjI0Mzk5NQ==",
      "login": "rneatherway",
      "name": "Robin Neatherway"
    },
    {
      "id": "MDQ6VXNlcjI0NTg3OQ==",
      "login": "twelvelabs",
      "name": "Skip Baney"
    },
    {
      "id": "MDQ6VXNlcjI2NjI3MQ==",
      "login": "SiarheiFedartsou",
      "name": "Siarhei Fedartsou"
    },
    {
      "id": "MDQ6VXNlcjMxODIwOA==",
      "login": "koddsson",
      "name": "Kristján Oddsson"
    },
    {
      "id": "MDQ6VXNlcjMyNTM4NA==",
      "login": "MikeRogers0",
      "name": "Mike Rogers"
    },
    { "id": "MDQ6VXNlcjMyODYwMg==", "login": "pabs3", "name": "Paul Wise" },
    {
      "id": "MDQ6VXNlcjMzNjQ0Nw==",
      "login": "MaxHorstmann",
      "name": "Max Horstmann"
    },
    {
      "id": "MDQ6VXNlcjMzOTA4MA==",
      "login": "josebalius",
      "name": "Jose Garcia"
    },
    {
      "id": "MDQ6VXNlcjM0ODc5Mw==",
      "login": "alefranz",
      "name": "Alessio Franceschelli"
    },
    { "id": "MDQ6VXNlcjM1NTAzMw==", "login": "issyl0", "name": "Issy Long" },
    {
      "id": "MDQ6VXNlcjM2Mzc2NA==",
      "login": "rmw",
      "name": "Rebecca Miller-Webster"
    },
    { "id": "MDQ6VXNlcjM4ODA3NA==", "login": "dbruns", "name": "Daniel Bruns" },
    { "id": "MDQ6VXNlcjQwNjkzNw==", "login": "lumaxis", "name": "Lukas Spieß" },
    {
      "id": "MDQ6VXNlcjQxNDQwMg==",
      "login": "marckhouzam",
      "name": "Marc Khouzam"
    },
    {
      "id": "MDQ6VXNlcjQyNjk4MA==",
      "login": "ismaell",
      "name": "Ismael Luceno"
    },
    {
      "id": "MDQ6VXNlcjQyODM4NA==",
      "login": "antleblanc",
      "name": "Antoine Leblanc"
    },
    {
      "id": "MDQ6VXNlcjQyODQ3OA==",
      "login": "arkentos",
      "name": "Anowar Islam"
    },
    {
      "id": "MDQ6VXNlcjQzNDI1NA==",
      "login": "pborzenkov",
      "name": "Pavel Borzenkov"
    },
    {
      "id": "MDQ6VXNlcjQ1ODEwOA==",
      "login": "jonlorusso",
      "name": "Jon Lorusso"
    },
    {
      "id": "MDQ6VXNlcjQ3MjgzMA==",
      "login": "callumacrae",
      "name": "Callum Macrae"
    },
    {
      "id": "MDQ6VXNlcjQ3ODIzNw==",
      "login": "waldyrious",
      "name": "Waldir Pimenta"
    },
    {
      "id": "MDQ6VXNlcjQ4MTE4NQ==",
      "login": "spenrose",
      "name": "Scott Penrose"
    },
    {
      "id": "MDQ6VXNlcjQ4NzE2OA==",
      "login": "tgyurci",
      "name": "Teubel György"
    },
    {
      "id": "MDQ6VXNlcjUyNjMwNw==",
      "login": "endorama",
      "name": "Edoardo Tenani"
    },
    {
      "id": "MDQ6VXNlcjUyNzcxMw==",
      "login": "azchohfi",
      "name": "Alexandre Zollinger Chohfi"
    },
    {
      "id": "MDQ6VXNlcjUzOTcwOA==",
      "login": "tklauser",
      "name": "Tobias Klauser"
    },
    {
      "id": "MDQ6VXNlcjU0MjEwOA==",
      "login": "lukekarrys",
      "name": "Luke Karrys"
    },
    {
      "id": "MDQ6VXNlcjU1NTAwOQ==",
      "login": "GreenRecycleBin",
      "name": "Daniel Le"
    },
    { "id": "MDQ6VXNlcjU2Mjc5NQ==", "login": "castaneai", "name": "castaneai" },
    {
      "id": "MDQ6VXNlcjU3MDYwOA==",
      "login": "Crunch09",
      "name": "Florian Thomas"
    },
    {
      "id": "MDQ6VXNlcjU4NDYyMw==",
      "login": "victorhsn",
      "name": "Victor Hugo"
    },
    { "id": "MDQ6VXNlcjYwOTQ1Mg==", "login": "wilhelmeek", "name": "wilhelm" },
    {
      "id": "MDQ6VXNlcjY1MzI4OA==",
      "login": "SubOptimal",
      "name": "Frank Dietrich"
    },
    {
      "id": "MDQ6VXNlcjY4Mzk4OA==",
      "login": "Raffo",
      "name": "Raffaele Di Fazio"
    },
    {
      "id": "MDQ6VXNlcjY5NzMzOA==",
      "login": "0robustus1",
      "name": "Tim Reddehase"
    },
    {
      "id": "MDQ6VXNlcjcxMzUyNQ==",
      "login": "inverse",
      "name": "Malachi Soord"
    },
    {
      "id": "MDQ6VXNlcjcyNTQ0OA==",
      "login": "macmacbr",
      "name": "Marco Carvalho"
    },
    {
      "id": "MDQ6VXNlcjcyNTg0Mg==",
      "login": "Xerkus",
      "name": "Aleksei Khudiakov"
    },
    { "id": "MDQ6VXNlcjc0NjAyMA==", "login": "jkeech", "name": "John Keech" },
    { "id": "MDQ6VXNlcjc0NjIzMA==", "login": "nilsvu", "name": "Nils Vu" },
    { "id": "MDQ6VXNlcjc3MTEzNA==", "login": "vaindil", "name": "" },
    {
      "id": "MDQ6VXNlcjgxNjE1MA==",
      "login": "dhleong",
      "name": "Daniel Leong"
    },
    {
      "id": "MDQ6VXNlcjgzMzA0Mw==",
      "login": "BjoernAkAManf",
      "name": "Björn Heinrichs"
    },
    {
      "id": "MDQ6VXNlcjg0OTUwMg==",
      "login": "emecas",
      "name": "Emerson Castaneda"
    },
    {
      "id": "MDQ6VXNlcjg2MTA0NA==",
      "login": "browniebroke",
      "name": "Bruno Alla"
    },
    { "id": "MDQ6VXNlcjg2MTk3NQ==", "login": "Dasio", "name": "Dávid Mikuš" },
    {
      "id": "MDQ6VXNlcjg2NjMzMA==",
      "login": "bsiegert",
      "name": "Benny Siegert"
    },
    {
      "id": "MDQ6VXNlcjg2Nzc0Ng==",
      "login": "bigkevmcd",
      "name": "Kevin McDermott"
    },
    { "id": "MDQ6VXNlcjg5MTIwMg==", "login": "zxaos", "name": "Matt Bond" },
    {
      "id": "MDQ6VXNlcjkwMzM2NQ==",
      "login": "greggroth",
      "name": "Greggory Rothmeier"
    },
    { "id": "MDQ6VXNlcjkyMTcyOA==", "login": "znull", "name": "Jason Lunz" },
    {
      "id": "MDQ6VXNlcjkyMjk4OA==",
      "login": "white-hat",
      "name": "Ilya Trushchenko"
    },
    {
      "id": "MDQ6VXNlcjkzNjQyMQ==",
      "login": "henvic",
      "name": "Henrique Vicente"
    },
    {
      "id": "MDQ6VXNlcjk2NDkxMg==",
      "login": "outofambit",
      "name": "evelyn masso"
    },
    { "id": "MDQ6VXNlcjk4NzYzOA==", "login": "y-yagi", "name": "y-yagi" },
    {
      "id": "MDQ6VXNlcjk5Mjg3OA==",
      "login": "signalwerk",
      "name": "Stefan Huber"
    },
    {
      "id": "MDQ6VXNlcjEwNDI5NDY=",
      "login": "Foxboron",
      "name": "Morten Linderud"
    },
    {
      "id": "MDQ6VXNlcjEwNTQwNDE=",
      "login": "RasmusWL",
      "name": "Rasmus Wriedt Larsen"
    }
  ],
  "mergeCommitAllowed": true,
  "milestones": [],
  "mirrorUrl": "",
  "name": "cli",
  "nameWithOwner": "cli/cli",
  "openGraphImageUrl": "https://repository-images.githubusercontent.com/212613049/d51e7480-f84b-11ea-84d5-4ef1c4d2a5bc",
  "owner": { "id": "MDEyOk9yZ2FuaXphdGlvbjU5NzA0NzEx", "login": "cli" },
  "parent": null,
  "primaryLanguage": { "name": "Go" },
  "projects": [],
  "pullRequestTemplates": [
    {
      "filename": "PULL_REQUEST_TEMPLATE.md",
      "body": "<!--\n  Thank you for contributing to GitHub CLI!\n  To reference an open issue, please write this in your description: `Fixes #NUMBER`\n-->\n"
    }
  ],
  "pullRequests": { "totalCount": 33 },
  "pushedAt": "2023-08-26T19:09:00Z",
  "rebaseMergeAllowed": true,
  "repositoryTopics": [
    { "name": "github-api-v4" },
    { "name": "cli" },
    { "name": "git" },
    { "name": "golang" }
  ],
  "securityPolicyUrl": "https://github.com/cli/cli/security/policy",
  "squashMergeAllowed": true,
  "sshUrl": "git@github.com:cli/cli.git",
  "stargazerCount": 33079,
  "templateRepository": null,
  "updatedAt": "2023-08-27T05:44:44Z",
  "url": "https://github.com/cli/cli",
  "usesCustomOpenGraphImage": true,
  "viewerCanAdminister": false,
  "viewerDefaultCommitEmail": "",
  "viewerDefaultMergeMethod": "MERGE",
  "viewerHasStarred": false,
  "viewerPermission": "READ",
  "viewerPossibleCommitEmails": [""],
  "viewerSubscription": "UNSUBSCRIBED",
  "watchers": { "totalCount": 845 }
}

欲しいメタデータだけに絞って確認する

実際はこんなに情報はいらないので、欲しいフィールドだけに絞って実行してみます。

$ gh repo view --json 'defaultBranchRef,forkCount,homepageUrl,isPrivate,latestRelease,nameWithOwner,primaryLanguage'

レスポンスはこんな JSON になります。

{
  "defaultBranchRef": {
    "name": "trunk"
  },
  "forkCount": 5399,
  "homepageUrl": "https://cli.github.com",
  "isPrivate": false,
  "latestRelease": {
    "name": "GitHub CLI 2.33.0",
    "tagName": "v2.33.0",
    "url": "https://github.com/cli/cli/releases/tag/v2.33.0",
    "publishedAt": "2023-08-21T18:02:45Z"
  },
  "nameWithOwner": "cli/cli",
  "primaryLanguage": {
    "name": "Go"
  }
}

🐔 応用: PR を整理して把握する

ここまでは gh cli の基本的な使い方でした。

以降は PR の情報整理と合わせて gh と別のツールを組み合わせて強力に活用する方法などを紹介します。

いま出ている PR を把握する

$ gh pr list

で、現在 Open になっている PR を確認することができます。グラフィカルでうれしいね。

なお、 --limit オプションで取得する上限数をデフォルトの 30 件から変更が可能です。

クエリで PR を絞り込む

gh cli はクエリで絞り込み検索ができるオプションが用意されています。
例えば、 --state オプションを付与することでPR のステータスを指定することができます。

こんな感じでクローズ済みの PR を確認することができます。

$ gh pr list --state 'closed'

他に使えるオプションは --help を確認してください。

PR のメタデータを任意の形式の JSON に整形して取得する

素のgh pr listの出力だけだと情報がちょっと物足りないので、--json オプションを使って json でメタデータを取得し jq コマンドを使って情報を整形した上で表示してみます。

$ gh pr list --json 'author,baseRefName,changedFiles,deletions,headRefName,number,title,url' \
      | jq -r '[.[] | { number: .number, title: .title, author: .author.login, base: .baseRefName, head: .headRefName, changedFiles: .changedFiles, deletions: .deletions, url: .url }]'

こんな感じに、欲しい情報だけに絞ってデータを得ることができました。

PR の情報をわかりやすく可視化する

正直 JSON で返ってきても全然見やすくありません。なので、先程のクエリの結果をグラフィカルに整形してパッと見て把握できるようにします。

今回は可視化に tty-table を利用します。
tty-table は結構細かくカスタマイズができ、利用が簡単なので気に入っています。
https://www.npmjs.com/package/tty-table

こういった情報を可視化するのに好みのツールがある方はそれを利用すればよいです。

tty-table で表形式にして表示する

tty-table は JSON 入力をそのまま受け入れることができるので、jq でネストのない配列形式に加工した JSON をそのまま pipe で食わせるだけで機能します。

$ gh pr list --json 'author,baseRefName,changedFiles,deletions,headRefName,number,title,url' \
      | jq -r '[.[] | { number: .number, title: .title, author: .author.login, base: .baseRefName, head: .headRefName, changedFiles: .changedFiles, deletions: .deletions, url: .url }]' \
      | tty-table --format json

これだけでもかなり見やすくなりましたが、まだ改善の余地がありますね。

ヘッダー行を付与する

tty-table はヘッダー定義ファイルを引き渡すことでテーブルをカスタマイズできます。
定義ファイルは静的なものであれば JSON で定義できますし、JS で書くことでロジックを組み込むことも可能です。

今回はこんな定義を作成して、 $HOME/.config/tty-table/github-pr.json に保存しました

[
  {
    "alias": "#",
    "color": "green",
    "width": 6
  },
  {
    "alias": "title",
    "align": "left",
    "color": "yellow",
    "width": 50,
    "truncate": "..."
  },
  {
    "alias": "author",
    "align": "left",
    "width": 15,
    "truncate": "..."
  },
  {
    "alias": "base",
    "align": "left",
    "width": 10,
    "truncate": "..."
  },
  {
    "alias": "head",
    "align": "left",
    "width": 20,
    "truncate": "..."
  },
  {
    "alias": "CHF",
    "align": "right"
  },
  {
    "alias": "DEL",
    "align": "right"
  },
  {
    "alias": "url",
    "color": "blue",
    "width": 40
  }
]

これを tty-table の引数に取って実行します。

$ gh pr list --json 'author,baseRefName,changedFiles,deletions,headRefName,number,title,url' \
      | jq -r '[.[] | { number: .number, title: .title, author: .author.login, base: .baseRefName, head: .headRefName, changedFiles: .changedFiles, deletions: .deletions, url: .url }]' \
      | tty-table --format json --header $HOME/.config/tty-table/github-pr.json

よいですね。かなり見やすくなりました。

🐲 最終奥義

ここまでの手段を組み合わせて便利に利用する例です。
テーブル表示での可視化についてはここまでで定型化できているので、あとは gh の検索クエリを工夫することで色々なパターンで利用することができます。

自分の PR を一覧表示する

gh pr list--author '@me' を付与することで、自分の建てた PR だけを取得することが可能です。

$ gh pr list --json 'author,baseRefName,changedFiles,deletions,headRefName,number,title,url' --author '@me' \
      | jq -r '[.[] | { number: .number, title: .title, author: .author.login, base: .baseRefName, head: .headRefName, changedFiles: .changedFiles, deletions: .deletions, url: .url }]' \
      | tty-table --format json --header $HOME/.config/tty-table/github-pr.json

自分からの Review を待っている PR を一覧表示する

gh pr list--search 'user-review-requested:@me' を付与することで、自分からの Review を待っている PR を一覧表示できます。ちなみに「自分もしくは自分の所属チームからの」であれば review-requested:@me です。

$ gh pr list --json 'author,baseRefName,changedFiles,deletions,headRefName,number,title,url' --search 'user-review-requested:@me' \
      | jq -r '[.[] | { number: .number, title: .title, author: .author.login, base: .baseRefName, head: .headRefName, changedFiles: .changedFiles, deletions: .deletions, url: .url }]' \
      | tty-table --format json --header $HOME/.config/tty-table/github-pr.json

自分の作業に関連しそうな PR を検索する

gh pr list--state 'all' を付与することで状態問わずすべての PR を対象にし、 --search で任意の単語を検索することでヒットした PR を一覧表示できます。
また、先のとおり --search 上でも github の検索クエリが利用できるので gh cli のオプションだけで実現できない複雑な検索も可能です。

$ gh pr list --json 'author,baseRefName,changedFiles,deletions,headRefName,number,title,url' --state 'all' --search '検索クエリ' \
      | jq -r '[.[] | { number: .number, title: .title, author: .author.login, base: .baseRefName, head: .headRefName, changedFiles: .changedFiles, deletions: .deletions, url: .url }]' \
      | tty-table --format json --header $HOME/.config/tty-table/github-pr.json

まとめ

私は「今どの PR レビューしないと何だっけ...自分の Review 待ち PR はどうなってるんだっけ...」となることが多いのでこれらの手段で把握できるようにしています。
shell の機能で alias や abbr にしておいたり、gh cli 部分のオプションを自由に引き渡せるように script 化しておくなどすると利便性が高まるのでおすすめです。

gh cli は非常に強力で、アイディア次第でここで紹介した以上にいろいろな使い方ができますのでぜひ試してみてください。


おまけ: Review しないとな PR 全部まとめてブラウザで開く

$ gh pr list --json 'url' --search 'user-review-requested:@me' --jq '.[] | .url' | xargs open


(gh cli のリポジトリには私の Review を待っている PR なんてないので空検索しています)

Review を貯めすぎるとブラウザが大爆発するようになるので気をつけてください。こまめに見よう。

paiza

Discussion

s10akirs10akir

2023-08-29 追記

しばらく使っていて、tty-tableが大きなJSONをparseできないことがあることに気づきました。
どうやらtty-tableのバグっぽかったので本家にPRを出しています。取り込まれるのを期待。

https://github.com/tecfu/tty-table/pull/96