Movatterモバイル変換


[0]ホーム

URL:


Skip to contentSkip to sidebar
/Blog
Try GitHub CopilotAttend GitHub Universe

How the GitHub CLI can now enable triangular workflows

The GitHub CLI now supports common Git configurations for triangular workflows. Learn more about triangular workflows, how they work, and how to configure them for your Git workflows. Then, see how you can leverage these using the GitHub CLI.

|12 minutes
  • Share:

Most developers are familiar with the standard Git workflow. You create a branch, make changes, and push those changes back to the same branch on the main repository. Git calls this a centralized workflow. It’s straightforward and works well for many projects.

However, sometimes you might want to pull changes from a different branch directly into your feature branch to help you keep your branch updated without constantly needing to merge or rebase. However, you’ll still want to push local changes to your own branch. This is where triangular workflows come in.

It’s possible that some of you have already used triangular workflows, even without knowing it. When you fork a repo, contribute to your fork, then open a pull request back to the original repo, you’re working in a triangular workflow. While this can work seamlessly on github.com, the process hasn’t always been seamless with theGitHub CLI.

The GitHub CLI team has recently made improvements (released inv2.71.2) to better support these triangular workflows, ensuring that thegh pr commands work smoothly with your Git configurations. So, whether you’re working on a centralized workflow or a more complex triangular one, the GitHub CLI will be better equipped to handle your needs.

If you’re already familiar with how Git handles triangular workflows, feel free to skip ahead to learn about how to usegh pr commands with triangular workflows. Otherwise, let’s get into the details of how Git and the GitHub CLI have historically differed, and how four-and-a-half years after it was first requested, we have finally unlocked managing pull requests using triangular workflows in the GitHub CLI.

First, a lesson in Git fundamentals

To provide a framework for what we set out to do, it’s important to first understand some Git basics. Git, at its core, is a way to store and catalog changes on a repository and communicate those changes between copies of that repository. This workflow typically looks like the diagram below:

Figure 1: A typical git branch setup
Figure 1: A typical git branch setup

The building blocks of this diagram illustrate two important Git concepts you likely use every day, aref andpush/pull.

Refs

Aref is a reference to a repository and branch. It has two parts: theremote, usually a name likeorigin orupstream, and thebranch. If the remote is the local repository, it is blank. So, in the example above,origin/branch in the purple box is aremote ref, referring to a branch namedbranch on the repository nameorigin, whilebranch in the green box is alocal ref, referring to a branch namedbranch on the local machine.

While working with GitHub, the remote ref is usually the repository you are hosting on GitHub. In the diagram above, you can consider the purple box GitHub and the green box your local machine.

Pushing and pulling

Apush and apull refer to the same action, but from two different perspectives. Whether you are pushing or pulling is determined by whether you are sending or receiving the changes. I can push a commit to your repo, or you can pull that commit from my repo, and the references to that action would be the same.

To disambiguate this, we will refer to different refs as theheadRef orbaseRef, where theheadRef is sending the changes (pushing them) and thebaseRef is receiving the changes (pulling them).

Figure 2: Disambiguating headRef and baseRef for push/pull operations.
Figure 2: Disambiguating headRef and baseRef for push/pull operations

When dealing with a branch, we’ll often refer to the headRef of its pull operations as itspullRef and the baseRef of its push operations as itspushRef. That’s because, in these instances, the working branch is the pull’s baseRef and the push’s headRef, so they’re already disambiguated.

The@{push} revision syntax

Turns out, Git has a handy built-in tool for referring to the pushRef for a branch: the@{push} revision syntax. You can usually determine a branch’s pushRef by running the following command:

git rev-parse --abbrev-ref @{push}

This will result in a human-readable ref, likeorigin/branch, if one can be determined.

Pull Requests

On GitHub, apull request is a proposal to integrate changes from one ref to another. In particular, they act as a simple “pause” before performing the actual integration operation, often called amerge, when changes are being pushed from ref to another. This pause allows for humans (code reviews) and robots (GitHub Copilot reviews and GitHub Actions workflows) to check the code before the changes are integrated. The namepull request came from this language specifically: You are requesting that a ref pulls your changes into itself.

Figure 3: Demonstrating how GitHub Pull Requests correspond to pushing and pulling.
Figure 3: Demonstrating how GitHub Pull Requests correspond to pushing and pulling

Common Git workflows

Now that you understand the basics, let’s talk about the workflows we typically use with Git every day.

Acentralized workflow is how most folks interact with Git and GitHub. In this configuration, any given branch is pushing and pulling from a remote ref with the same branch name. For most of us, this type of configuration is set up by default when we clone a repo and push a branch. It is the situation shown in Figure 1.

In contrast, atriangular workflow pushes to and pulls fromdifferent refs. A common use case for this configuration is to pull directly from a remote repository’s default branch into your local feature branch, eliminating the need to run commands likegit rebase <default> orgit merge <default> on your feature branch to ensure the branch you’re working on is always up to date with the default branch. However, when pushing changes, this configuration will typically push to a remote ref with the same branch name as the feature branch.

Figure 4: juxtaposing centralized workflows from triangular workflows.
Figure 4: juxtaposing centralized workflows from triangular workflows.

We complete the triangle when considering pull requests: theheadRef is thepushRef for the local ref and thebaseRef is thepullRef for the local branch:

Figure 5: a triangular workflow
Figure 5: a triangular workflow

We can go one step further and set up triangular workflows usingdifferent remotes as well. This most commonly occurs when you’re developing on a fork. In this situation, you usually give the fork and source remotes different names. I’ll useorigin for the fork andupstream for the source, as these are common names used in these setups. This functions exactly the same as the triangular workflows above, but theremotes andbranches on thepushRef andpullRef are different:

Figure 6: juxtaposing triangular workflows and centralized workflows with different remotes such as with forks
Figure 6: juxtaposing triangular workflows and centralized workflows with different remotes such as with forks

Using a Git configuration file for triangular workflows

There are two primary ways that you can set up a triangular workflow using theGit configuration – typically defined in a `.git/config` or `.gitconfig` file. Before explaining these, let’s take a look at what the relevant bits of a typical configuration look like in a repo’s `.git/config` file for a centralized workflow:

[remote “origin”]     url = https://github.com/OWNER/REPO.git     fetch = +refs/heads/*:refs/remotes/origin/*  [branch “default”]    remote = origin      merge = refs/heads/default  [branch “branch”]    remote = origin     merge = refs/heads/branch

Figure 7: A typical Git configuration setup found in .git/config

The[remote “origin”] part is naming the Git repository located atgithub.com/OWNER/REPO.git toorigin, so we can reference it elsewhere by that name. We can see that reference being used in the specific[branch] configurations for both thedefault andbranch branches in theirremote keys. This key, in conjunction with the branch name, typically makes up the branch’spushRef: in this example, it isorigin/branch.

Theremote andmerge keys are combined to make up the branch’spullRef: in this example, it isorigin/branch.

Setting up a triangular branch workflow

The simplest way to assemble a triangular workflow is to set the branch’smerge key to a different branch name, like so:

[branch “branch”]    remote = origin    merge = refs/heads/default

Figure 8: a triangular branch’s Git configuration found in .git/config

This will result in the branchpullRef asorigin/default, butpushRef asorigin/branch, as shown in Figure 9.

Figure 9: A triangular branch workflow
Figure 9: A triangular branch workflow

Setting up a triangular fork workflow

Working with triangular forks requires a bit more customization than triangular branches because we are dealing with multiple remotes. Thus, our remotes in the Git config will look different than the one shown previously in Figure 7:

[remote “upstream”]    url = https://github.com/ORIGINALOWNER/REPO.git     fetch = +refs/heads/*:refs/remotes/upstream/* [remote “origin”]    url = https://github.com/FORKOWNER/REPO.git      fetch = +refs/heads/*:refs/remotes/origin/*

Figure 10: a Git configuration for a multi-remote Git setup found in .git/config

Upstream andorigin are the most common names used in this construction, so I’ve used them here, but they can be named anything you want1.

However, toggling a branch’sremote key betweenupstream andorigin won’t actually set up a triangular fork workflow—it will just set up a centralized workflow with either of those remotes, like the centralized workflow shown in Figure 6. Luckily, there are two common Git configuration options to change this behavior.

Setting a branch’spushremote

A branch’s configuration has a key calledpushremote that does exactly what the name suggests: configures the remote that the branch will push to. A triangular fork workflow config usingpushremote may look like this:

[branch “branch”]    remote = upstream      merge = refs/heads/default      pushremote = origin

Figure 11: a triangular fork’s Git config using pushremote found in .git/config

This assembles the triangular fork repo we see in Figure 12. ThepullRef isupstream/default, as determined by combining theremote andmerge keys, while thepushRef isorigin/branch, as determined by combining thepushremote key and the branch name.

Figure 12: A triangular fork workflow
Figure 12: A triangular fork workflow

Setting a repo’sremote.pushDefault

To configure all branches in a repository to have the same behavior as what you’re seeing in Figure 12, you can instead set the repository’spushDefault. The config for this is below:

[remote]     pushDefault = origin [branch “branch”]    remote = upstream     merge = refs/heads/default

Figure 13: a triangular fork’s Git config using remote.pushDefault found in .git/config

This assembles the same triangular fork repo as shown in Figure 12 above, however this time thepushRef is determined by combining theremote.pushDefault key and the branch name, resulting inorigin/branch.

When using the branch’spushremote and the repo’sremote.pushDefault keys together, Git will preferentially resolve the branch’s configuration over the repo’s, so the remote set onpushremote supersedes the remote set onremote.pushDefault.

Updating thegh pr command set to reflect Git

Previously, thegh pr command set did not resolvepushRefs andpullRefs in the same way that Git does. This was due to technical design decisions that made this change both difficult and complex. Instead of discussing that complexity—a big enough topic for a whole article in itself—I’m going to focus here on what you can nowdo with the updatedgh pr command set.

If you set up triangular Git workflows in the manner described above, we will automatically resolvegh pr commands in accordance with your Git configuration.

To be slightly more specific, when trying to resolve a pull request for a branch, the GitHub CLI will respect whatever@{push} resolves to first, if it resolves at all. Then it will fall back to respect a branch’spushremote, and if that isn’t set, finally look for a repo’sremote.pushDefault config settings.

What this means is that the CLI is assuming your branch’spullRef is the pull request’sbaseRef and the branch’spushRef is the pull requestsheadRef. In other words, if you’ve configuredgit pull andgit push to work, thengh pr commands should just work.2 The diagram below, a general version of Figure 5, demonstrates this nicely:

Figure 14: the triangular workflow supported by the GitHub CLI with respect to a branch’s pullRef and pushRef. This is the generalized version of Figure 5
Figure 14: the triangular workflow supported by the GitHub CLI with respect to a branch’s pullRef and pushRef. This is the generalized version of Figure 5

Conclusion

We’re constantly working to improve the GitHub CLI, and we’d like the behavior of the GitHub CLI to reasonably reflect the behavior of Git. This was a team effort—everyone contributed to understanding, reviewing, and testing the code to enable this enhancedgh pr command set functionality.

It also couldn’t have happened without the support of our contributors, so we extend our thanks to them:

CLI native support for triangular workflows was 4.5 years in the making, and we’re proud to have been able to provide this update for the community.

The GitHub CLI Team
@andyfeller,@babakks,@bagtoad,@jtmcg,@mxie,@RyanHecht, and@williammartin


  1. Some commands in gh are opinionated about remote names and will resolve remotes in this order: upstream, github, origin,<other remotes unstably sorted>. There is a convenience command you can run to supersede this:*gh repo set-default [<repository>]to override the default behavior above and preferentially resolve<repository>as the default remote repo. 
  2. If you find a git configuration that doesn’t work, please open an issue in the OSS repo so we can fix it. 

Written by

Tyler McGoffin

Tyler McGoffin

@jtmcg

Tyler is a Sr. Software Engineer on the GitHub CLI team. He has an eclectic background in scientific research, education, game design/development, and software. His favorite part about his current job is being an open source maintainer and interacting with the community.

More onGitHub CLI

Building a more accessible GitHub CLI

How do we translate web accessibility standards to command line applications? This is GitHub CLI’s journey toward making terminal experiences for all developers.

Exploring GitHub CLI: How to interact with GitHub's GraphQL API endpoint

Discover practical tips and tricks for forming effective GraphQL queries and mutations.

Related posts

GitHub Copilot

For the Love of Code: a summer hackathon for joyful, ridiculous, and wildly creative projects

That idea you’ve been sitting on? The domain you bought at 2AM? A silly or serious side project? This summer, we invite you to build it — for the joy, for the vibes, For the Love of Code 🧡

Git

Git security vulnerabilities announced

Today, the Git project released new versions to address seven security vulnerabilities that affect all prior versions of Git.

Git

Highlights from Git 2.50

The open source Git project just released Git 2.50. Here is GitHub’s look at some of the most interesting features and changes introduced since last time.

Explore more from GitHub

Docs

Docs

Everything you need to master GitHub, all in one place.

Go to Docs
GitHub

GitHub

Build what’s next on GitHub, the place for anyone from anywhere to build anything.

Start building
Customer stories

Customer stories

Meet the companies and engineering teams that build with GitHub.

Learn more
Enterprise content

Enterprise content

Executive insights, curated just for you

Get started

We do newsletters, too

Discover tips, technical guides, and best practices in our biweekly newsletter just for devs.


[8]ページ先頭

©2009-2025 Movatter.jp