github.com/peterbe/gg2 is a command line app that compiles to a single binary called gg. It's for working with git branches (and pull requests) fast(er). The code itself is written in TypeScript and I've blogged about it here.

At the time of writing, the test suite is not complete. It only tests the following commands:

  • gg --version
  • gg --help
  • gg start
  • gg commit

These are easiest because they don't involve actually network talking to GitHub's API. Anyway, they are interactive. There are some pure TypeScript unit tests already but what I wanted was a decent end-to-end test suite that makes sure the compiled binary works. Somewhat confusingly, the test suite of choice is Bun's testing framework, using the $ Shell API. It's essentially bash but controlled from a regular TypeScript function.

Tests are run with bun test which runs all suites. You can do rapid development with bun test --watch. The test depends on there existing a gg executable just like it would be on your command line. In GitHub Actions, to make that available, it gets put into the $PATH using:


bun run build:linux-x64
mv out/gg-linux-x64 /home/runner/.local/bin/gg
echo "/home/runner/.local/bin" >> $GITHUB_PATH
gg --version

Now, here's a snippet of the test suite:


test("version", async () => {
  const output = await $`gg --version`.text()
  expect(/\d+\.\d+\d+/.test(output)).toBe(true)
})

That particular test doesn't involve actually using git in a real repo. Here's an (abbreviated) snippet of a test that involves actually using git and gg together:


test("create branch and commit", async () => {
  await $`git init --initial-branch=main`.cwd(tempDir)
  await $`git config --global init.defaultBranch main`.cwd(tempDir)

  await $`git config --global user.email "you@example.com"`.cwd(tempDir)
  await $`git config --global user.name "Your Name"`.cwd(tempDir)
  await $`echo "# My Project" > README.md`.cwd(tempDir)
  await $`git add README.md`.cwd(tempDir)

  await $`echo "This is my new branch" | gg start`.cwd(tempDir)
  const branchName = await $`git branch --show-current`.cwd(tempDir).text()
  expect(branchName.trim()).toBe("this-is-my-new-branch")
})

That tempDir comes from a beforeEach callback:


tempDir = await mkdtemp(join(tmpdir(), "gg-test"))

It works. It's fast. It's easy to debug. If something goes wrong, I can go in to the afterEach callback and comment out the deletion of the temporary directory. Then, I can manually go in to that directory to poke:


afterEach(async () => {
- await rm(tempDir, { recursive: true })
+ // await rm(tempDir, { recursive: true })
+ console.log("TEMP DIR WAS", tempDir)
})

Note that the gg start command uses import { input } from "@inquirer/prompts" to prompt the user for input. That can be satisfied using the pipe as seen in...


This is my new branch" | gg start

I haven't tested more complex inputs like the import { select } from "@inquirer/prompts" yet.

Next challenge is going to be; how do you test commands line gg pr which interacts with the GitHub API. Remains to be figured out!

Comments

Your email will never ever be published.

Previous:
How to count the number of non-blank lines with Bash September 3, 2025 Linux, Bash, macOS
Related by category:
Always run biome migrate after upgrading biome August 16, 2025 Bun
Video to Screenshots app June 21, 2025 Bun
Parse a CSV file with Bun September 13, 2023 Bun
gg2 - a new CLI for helping me manage git branches August 6, 2025 Bun
Related by keyword:
gg2 - a new CLI for helping me manage git branches August 6, 2025 JavaScript, Bun, macOS
gg commit with suggested --no-verify August 29, 2025 Bun
How to unset aliases set by Oh My Zsh June 14, 2018 Linux, macOS
gg - A prototype to rule Git, GitHub and Bugzilla May 6, 2016 Python, Web development