question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

How do I fetch all commits only in the PR branch

See original GitHub issue

How do I tell actions/checkout to fetch all the commits up to the fork point? I want all the commits up to the fork point from the base branch. I don’t see any examples on how to do this in the README. I’d assume this is a common need.

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:5
  • Comments:11

github_iconTop GitHub Comments

3reactions
polarathenecommented, Jul 2, 2022

I want all the commits up to the fork point from the base branch.

You can use the github context to get how many commits belong to the PR, then fetch that depth.


Example - Fetch enough commits from PR & base branch (eg: master)

To minimize commits fetched to compare two branches, a git fetch to the other branch (eg: master / main) will identify a common commit between the two branches and retrieve roughly only the commits needed to support that minimal history.

If the local commit history doesn’t already have a commit from the 2nd branch being fetched, the full history (or whatever depth is requested) is fetched for that branch instead. This usually requires fetching one additional commit (the one you branched from) for the 1st branch (eg: PR branch).

Example for the PR branch to pull enough commit history to include a commit the other branch also has:

- name: 'PR commits + 1'
  run: echo "PR_FETCH_DEPTH=$(( ${{ github.event.pull_request.commits }} + 1 ))" >> "${GITHUB_ENV}"

- name: 'Checkout PR branch and all PR commits'
  uses: actions/checkout@v3
    with:
      ref: ${{ github.event.pull_request.head.ref }}
      fetch-depth: ${{ env.PR_FETCH_DEPTH }}

- name: 'Fetch the other branch with enough history for a common merge-base commit'
  run: |
    git fetch origin ${{ github.event.pull_request.base.ref }}

More Info

It’s important to set the ref to the PR head ref as above, since the default ref of this action is one extra “merge-commit” (the PR into the base branch), which will not only offset your fetch-depth by 1 additional commit needed, but possibly cause other issues (eg: with git fetch on the base branch, and trying to get commit history that can successfully derive a merge-base).

NOTE: Merge-commits (those from the base branch into the PR branch, not the action default test merge-commit ref) contribute to the total commits from github context, and the fetch depth range would not only get N commits from your branch but also N commits associated to the history of merge commits.

You may not need the extra commit - If you have a merge commit from the base branch, then your local history will have a commit prior to the merge that it can probably use instead. Otherwise fetching an extra commit should ensure a common ancestor.

EDIT: Fetching to a common ancestor may fail when you have commits belonging to the base branch in local history which have not been merged into the PR branch. This problem occurs with the default ref (the generated test merge-commit of the PR branch into the base branch) with a fetch-depth of 2 or more.


An earlier attempt for fetch that didn't work out

Originally I was suggesting a way to provide a more specific commit for fetch to negotiate with, but I think it usually won’t make much of a difference vs default git fetch? I also ran into scenarios where it broke, keeping for reference here as it may be helpful to others and not common to find online.

# Get the oldest commit in the branch which should have no parents,
# `--first-parent` only follows first parent encountered (should be our PR branch commits, not commits associated to merge commits from base branch):
FIRST_COMMIT_IN_BRANCH=$(git rev-list --first-parent --max-parents=0 --max-count=1 HEAD)

# Use that commit hash to lookup the next parent (that we don't have in history):
PARENT_COMMIT_HASH=$(git cat-file -p "${FIRST_COMMIT_IN_BRANCH}" | sed -n 's/^parent //p')

# Use that parent commit as a hint for fetch auto-depth, otherwise default fetch increments by the 
# fibonacci sequence until a commit from the base branch is found that also exists in our local commit history:
git fetch --negotiation-tip "${PARENT_COMMIT_HASH}" origin ${{ github.event.pull_request.base.ref }}

# Note this hint seems to fail if you already have a newer commit in local history from the base branch, 
# Such as when the default merge-commit ref with fetch depth of 2 or more pulls in base branch commits.
# Thus not always reliable to establish a merge-base...
#
# It will also fail if the root commit (first commit in repo) was the oldest commit found for FIRST_COMMIT_IN_BRANCH,
# Due to the PARENT_COMMIT_HASH not being possible to resolve.
# That can happen with too many merge commits of base branch to PR branch, as it bumps up the github context
# for commit count, fetching more history than needed (those merge commits don't seem to count for history depth)

Otherwise I found with a merge-commit, the fetch depth for +1 commits of your PR commit count could result in history being more than the expected number of commits fetched (the depth is technically correct from the commit chain associated to the merge commit).

With that lengthy command, you shouldn’t need to request one more commit. But I’ve since found it unreliable (explained in comments for snippet).

2reactions
polarathenecommented, Jul 3, 2022

(Improvements welcome)

@jbreckmckye if you’re interested in a file-name diff (specifically files added/changed, ignoring others like renames or deletions), here’s a few examples.

This is nice and small, and should be fine AFAIK, see commented version below for more details:

- name: 'Checkout PR branch (with test merge-commit)'
  uses: actions/checkout@v3
    with:
      fetch-depth: 2
- name: 'Get a list of changed files to process'
  run: git diff-tree --no-commit-id --name-only --diff-filter 'AM' -r HEAD^1 HEAD

Variants and details

I’ve collapsed the original content (still useful maybe if you want to understand why a --merge-base is important), as I have curated a better source of this information here.

Click to view more info
- name: 'Checkout PR branch'
  uses: actions/checkout@v3
    with:
      ref: ${{ github.event.pull_request.head.ref }}

- name: 'Get a list of changed files to process'
  run: |
    # Fetch enough history for a common merge-base commit
    git fetch origin ${{ github.event.pull_request.head.ref }} --depth $(( ${{ github.event.pull_request.commits }} + 1 ))
    git fetch origin ${{ github.event.pull_request.base.ref }}

    # Show only files from the PR with content filtered by specific git status (Added or Modified):
    git diff-tree --name-only --diff-filter 'AM' -r \
      --merge-base origin/${{ github.event.pull_request.base.ref }} ${{ github.event.pull_request.head.ref }}

The --merge-base option will find a common ancestor like git merge-base, and use that commit as the “before” reference to diff against, replacing the base branch ref value (thus: --merge-base <common ancestor commit> <PR head commit>)

If your action(s) involved use actions/checkout prior to this point, careful of the default “test merge-commit” ref being in the local history still. That can cause git fetch origin ${{ github.event.pull_request.base.ref }} to not fetch extra history needed for a merge-base commit, assuming that ref had a fetch-depth of 2 or more (which will fetch commits from both base and PR branches at that depth).

If you do need to avoid that failure scenario, you can lookup the date of the commit you branched from the base branch, and request all commits since then:

# This should get the oldest commit in the local fetched history (which may not be the branched base commit):
BRANCHED_FROM_COMMIT=$( git rev-list --first-parent --max-parents=0 --max-count=1 ${{ github.event.pull_request.head.ref }} )
UNIX_TIMESTAMP=$( git log --format=%ct "${BRANCHED_FROM_COMMIT}" )

# Get all commits since that commit for the base branch (eg: master):
git fetch --shallow-since "${UNIX_TIMESTAMP}" origin ${{ github.event.pull_request.base.ref }}

If you only need the diff from the PR, you may not need the potentially lengthy commit history from the above approach and can instead use either of these:

- name: 'Checkout PR branch (with test merge-commit)'
  uses: actions/checkout@v3
    with:
      # Merge commit + two commits (1 from each branch, the 2nd depth level)
      fetch-depth: 2

# Show only files from the PR with content filtered by specific git status (Added or Modified)
# HEAD^1 is the base branch commit, HEAD is the merge commit, no merge-base needed
- name: 'Get a list of changed files to process'
  run: git diff-tree --no-commit-id --name-only --diff-filter 'AM' -r HEAD^1 HEAD

Instead of using HEAD, you can use the actual default ref, and set a --merge-base, but since it’s effectively on the same branch (base) to compare, there isn’t much benefit from doing so, this is just more verbose for the sake of it (but useful if you need/prefer to use refs instead of HEAD or commit hashes):

- name: 'Checkout PR branch (with test merge-commit)'
  uses: actions/checkout@v3
    with:
      # Merge commit + two commits (1 from each branch, the 2nd depth level)
      fetch-depth: 2

- name: 'Get a list of changed files to process'
  env:
    # This is the default ref value, not exactly the same value as `github.ref` context:
    PR_REF: refs/remotes/pull/${{ github.event.pull_request.number }}/merge
  run: |
    # No extra commits need to be fetched, the base branch ref will be set to HEAD^1 commit
    git fetch origin ${{ github.event.pull_request.base.ref }}

    # Show only files from the PR with content filtered by specific git status (Added or Modified)
    git diff-tree --name-only --diff-filter 'AM' -r \
      --merge-base origin/${{ github.event.pull_request.base.ref }} ${PR_REF}

If you tried to use --merge-base against the other commit (from the PR) belonging to the merge commit, then it’d fail to derive until both branches fetch enough commits into local history, as explained earlier with --shallow-since approach.

WARNING: If you removed the --merge-base and changed ${PR_REF} to the PR commit, you’d get a diff between those two commits, which isn’t that useful (eg: If the base branch has deleted a file since, but the PR hasn’t, then that file is shown in part of the diff as “Added”, which is wrong).

For those unfamiliar with why --merge-base is important, that’s why, but a non-issue AFAIK when comparing with the “test merge commit” that Github provides and fetches by default.

Read more comments on GitHub >

github_iconTop Results From Across the Web

git - All Commits From Every Branch Displayed During Pull ...
Before pushing your branch from which you want to make a pull request, first: fetch from the original repo (the one with the...
Read more >
git-pull Documentation - Git
Then " git pull " will fetch and replay the changes from the remote master branch since it diverged from the local master...
Read more >
Git Fetch | Atlassian Git Tutorial
The git fetch command downloads commits, files, and refs from a remote repository into a local repo. Learn about additional uses and see...
Read more >
Pull changes to your local Git repo - Azure - Microsoft Learn
Git pull performs a fetch and then a merge or rebase to integrate fetched commits into your current local branch. Visual Studio uses...
Read more >
Git - Pull Specific Commit - Unfuddle Support
If you want to bring that specific COMMIT_ID to your local branch, you may either use git-cherry-pick to bring only that commit over,...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found