Configure target branches for pull requests
Azure DevOps Services
By default, Azure DevOps suggests the creation of new pull requests against the default branch. In a repository with multiple branches used for pull requests, repository owners may configure the list of pull request target branches so these suggestions select the proper target branch.
To enable this feature, create a file with name
.azuredevops/pull_request_targets.yml
in the repository's default branch.
This YAML file should contain a single list, titled pull_request_targets
,
containing the branch names or prefixes that match the candidate branches.
For example, consider these contents:
pull_request_targets:
- main
- release/*
- feature/*
This list of potential targets specifies main
as the target branch to
select first, but if a branch starting with release/
or feature/
is a
better choice, then that branch is chosen instead.
For more pull request guidelines and management considerations, see About pull requests.
When is this configuration used?
There are multiple entry points to using a dynamic target branch.
Pull Request Suggestions. When a user pushes a branch to Azure DevOps, their next visit to the Repos page may suggest creating a pull request from that branch. This "Create New Pull Request" button chooses the target branch dynamically.
Pull Request URL. When a user navigates directly to the pull request creation page using a
sourceRef
parameter but omitting thetargetRef
parameter, Azure DevOps selects a target branch based on this dynamic choice.
There's a capability for client tools to create pull requests using this dynamic choice, but those clients need to add an optional signal that the user didn't specify a target branch. Check your client tool of choice to see if the option is enabled.
What are good candidates for branch targets?
We recommend that the configured list of candidate branches includes only branches that are protected by pull request policies. Such branches are likely only changed by completing pull requests, which guarantees that the previous branch position is in the first-parent history of the tip commit. If a merge strategy is used, then the second parent represents the commits being introduced to the target branch by completing a pull request and the first parent is the previous tip.
How does Azure DevOps pick a branch?
Git doesn't track metadata around the creation of a branch. There's no exact way to determine what branch was used when creating a topic branch. Instead, Azure DevOps uses a heuristic based on the first-parent history of the branches.
Among the possible target branches, Azure DevOps selects the branch whose first-parent history intersects with the source branch's first-parent history the most.
Example: No merge commits
Consider the following branch structure, which is simplified more than normal as there are no merge commits. In this example, the entire history is represented by the first-parent history.
,-E---F <-- release/2024-September
/
A---B---C---D <--- main
\
`-G---H <--- feature/targets
\
`-I <--- topic
With this history and the sample pull_request_targets
list used
previously, we have three candidate target branches, in order of priority:
main
release/2024-September
feature/targets
The source branch, topic
, is then compared to these branches.
main
intersects withtopic
atB
, leavingG,I
intopic
and not inmain
.release/2024-September
intersects withtopic
atA
leavingB,G,I
intopic
and not inrelease/2024-September
.feature/targets
intersects withtopic
atG
, leavingI
intopic
and not infeature/targets
.
Therefore, in this example, the feature/targets
branch is chosen as the target
branch for a pull request with topic
as the source branch.
Example: Merge commits
In a more complicated example, where the feature/targets
branch was merged
into main
and merged main
into itself, the commit history has more cases to
consider:
,-E---F <-- release/2024-September
/
A---B---C---D---J---K <--- main
\ _/ \
\ / \
`G---H---L--\--M <--- feature/targets
\ \/
\
`I <--- topic
Here, the commit D
in main
represents a time where feature/targets
was
merged into main
. Commit M
represents a time where main
was merged into
feature/targets
. The link between commits M
and J
is drawn in a way to
emphasize that J
is the second parent of M
while L
is the first parent.
In this case, when you consider the full commit history, main
and
feature/targets
both intersect the history of topic
at G
. However, the
first parent history still demonstrates a preference for feature/targets
.
Breaking Ties
If two branches have the same first-parent history intersection, then
Azure Devops selects the branch that appears earlier in the
pull_request_targets
list. If multiple branches are still tied based on
the pull_request_targets
list due to a prefix match, then the earliest
in alphabetical order wins.
These kinds of ties are most often present when new candidate branches are created, such as the start of a new feature branch or the fork of a release branch.
,-E---F <-- release/2024-October
/
A---B---C---D <--- main
\
\
`G <--- topic
In this example, the release/2024-October
branch was created off of the
main
branch after topic
was branched off of main
. While this is intuitive
to a human reader, the order of the main
and release/*
categories in the
pull_request_targets
list indicates the preferred order to Azure DevOps.
What if Azure DevOps chooses the wrong target branch?
The pull request creation page has a selector for adjusting the target branch if the dynamic choice does not match expectations. The target branch can also be adjusted after the pull request is created.
More importantly, it may be valuable to understand why the heuristic may be selecting the "wrong" target branch.
This heuristic relies on some assumptions about how the target branches and the source branch were created. Here are some potential reasons why the heuristic does not work:
The target branches are not protected by pull request policies. If the target branches can be pushed arbitrarily, then the first-parent history is not a reliable indicator of the previous location of that branch.
The source branch was created from a previous tip of a candidate branch. If the source branch chose an arbitrary commit in the history, then there's no guarantee about the first parent history it depended upon.
The source branch was advanced using
git commit
andgit merge
commands. Commands such asgit reset --hard
orgit rebase
may change the history of the branch in unpredictable ways.
If you disagree with the target branch chosen by this heuristic, then
consider updating the choice using
git rebase --onto <new-target> <old-target> <source>
. The git rebase
command
rewrites the first-parent history to make the heuristic choose the new
target.
One common mistake that users make when realizing that they're based on the
wrong branch is to use git merge
to bring the right branch into their history.
Merging doesn't change the first-parent history, and thus doesn't change the
choice for target branch.
How can I test this decision locally?
The heuristic used by Azure DevOps was contributed to the core Git client and is available in Git versions 2.47.0 and later.
To test this logic in your own repository, first run git fetch origin
to make
sure you have the latest version of the target branches. Then, run the following
git for-each-ref
command, adjusted to match your list of candidate branches:
$ git for-each-ref --format="%(is-base:HEAD) %(refname)" \
refs/remotes/origin/main \
"refs/remotes/origin/release/*" \
"refs/remotes/origin/feature/*"
refs/remotes/origin/main
refs/remotes/origin/release/2024-September
(HEAD) refs/remotes/origin/feature/targets
In this command, the HEAD
commit is used as the source and compares the
first-parent history of the target branches in the same way. While each
candidate branch is listed in the output, the string (HEAD)
indicates which of
the branches should be used as the target branch.