git – How to Prevent ‘git pull –rebase’ from Overriding Local Commits When Push and Pull Target Different Repositories

git

Description

I have a simple setup where my local repository has a single main branch.

That main branch is configured to:

  • Always pull (rebase) changes from main branch on my public GitHub repository.
  • Always push changes to main branch on my private company GitHub repository.

Why?

This setup prevents me from:

  • Leaking any company data by mistake.
  • Polluting personal configuration files which I use everywhere.

Issue

For some reason, with this setup, git pull --rebase is always overriding my local commits with the ones pulled from personal public repository. It correctly fetches the changes on local remote-tracking branch, but then it doesn't actually rebase the corresponding branch. It just overrides it.

I would like to know if this is a git bug or if I'm doing something wrong?

Minimal reproduction steps

I have managed to reproduce the issue with local repositories, doing the following:

mkdir /tmp/repo_1 /tmp/repo_2

# Initialize first repository with a single commit that creates file "a".
git -C /tmp/repo_1 init --quiet
touch /tmp/repo_1/a
git -C /tmp/repo_1 add a
git -C /tmp/repo_1 commit --quiet --message "create file a"

# Initialize second repository which will be used for pushing.
git -C /tmp/repo_2 init --bare --quiet

# Initialize local repository and configure it to pull from "repo_1" and push to "repo_2"
git clone /tmp/repo_1 /tmp/local --quiet
git -C /tmp/local remote set-url --push origin /tmp/repo_2

# Make second commit in a local repository (first one was cloned from "repo_1") that creates file "b".
touch /tmp/local/b
git -C /tmp/local add b
git -C /tmp/local commit --quiet --message "create file b"

# Push both commits to "repo_2".
git -C /tmp/local push --quiet

# Pull --rebase changes from "repo_1" and notice that second commit is now missing.
echo "Local commits before pull --rebase:"
git -C /tmp/local log --oneline
echo
git -C /tmp/local pull --rebase --quiet
echo "Local commits after pull --rebase:"
git -C /tmp/local log --oneline

Note that replacing last paragraph with manual fetch + rebase commands yields the expected result:

# Do manual fetch + rebase and notice that it works as expected.
echo "Local commits before fetch + rebase:"
git -C /tmp/local log --oneline
echo
git -C /tmp/local fetch --quiet
git -C /tmp/local rebase --quiet origin/master
echo "Local commits after fetch + rebase:"
git -C /tmp/local log --oneline

Git version: 2.45.2

Best Answer

I decided to write a more direct answer instead of simply pointing at the references in my comment above.

# Push both commits to "repo_2".
git -C /tmp/local push --quiet

When OP pushes implicitly to repo_2, they advance the remote branch 'main' on repo_2 and the local remote tracking branch 'origin/main'. By design, 'repo_1' does not get the commits and the remote branch 'main' on repo_1 stays the same.

$ git -C /tmp/repo_1 log --oneline 
2b75abd (HEAD -> main) create file a

$ git -C /tmp/repo_2 log --oneline
f74a217 (HEAD -> main) create file b
2b75abd create file a

$ git -C /tmp/local log --oneline
f74a217 (HEAD -> main, origin/main, origin/HEAD) create file b
2b75abd create file a

At this point, if we were to do a 'git fetch' (implicitly from repo_1), we would see a message about a forced update on the remote tracking branch 'origin/main' because it would be forced backward to the original commit (commit message 'create file a'). However, a manual rebase of the local 'main' onto the remote tracking branch 'origin/main' at this point would have the desired effect. However, instead of doing the manual fetch and rebase, assume we instead use 'git pull --rebase'

# Pull --rebase changes from "repo_1" and notice that second commit is now missing.

'git pull --rebase' has special logic intended to recognize when the remote branch has been rewritten (e.g. rebased). This is mentioned in the 'git pull' help '--rebase' option:

If there is a remote-tracking branch corresponding to the upstream branch and the upstream branch was rebased since last fetched, the rebase uses that information to avoid rebasing non-local changes.

In this case, the special logic is triggered by the fact that the remote branch 'main' on repo_1 ('create file a'), which would normally be in sync with or ahead of the remote tracking branch, is instead behind the remote tracking branch 'origin/main' ('create file b').

This special logic attempts to identify the 'locally created' commits vs old commits that seemingly used to be on the remote but were replaced by a rewrite/rebase. It does this by looking at the remote tracking branch 'origin/main' which points at commit 'create file b' and it sees no commits after that. This appears to indicate that the remote branch 'main' on repo_1 was purposely rebased/rewritten to remove the 'create file b' commit, so origin/main is reset to commit 'create file a'. Since the logic identified no local commits to rebase, 'main' is also reset to 'origin/main'.

$ git -C /tmp/repo_1 log --oneline 
2b75abd (HEAD -> main) create file a

$ git -C /tmp/local log --oneline
f74a217 (HEAD -> main, origin/main, origin/HEAD) create file b
2b75abd create file a

$ git -C /tmp/local pull          
From /tmp/repo_1
 + f74a217...2b75abd main       -> origin/main  (forced update)
Successfully rebased and updated refs/heads/main.

$ git -C /tmp/local log --oneline
2b75abd (HEAD -> main, origin/main, origin/HEAD) create file a

I think the usual use case for specifying a different push remote is for when pulling and pushing from a remote must use different protocols to access the same repo. I'm not sure if there are any other gotchas in git that would pop up when the URLs for pulling and pushing are actually pointing at different repos that aren't guaranteed to be in sync.

Another way to accomplish what OP is doing is to use 2 separate remotes for repo_1 and repo_2 and then disable pushes to repo_1 either at the server or at the client (maybe by setting the push URL for repo_1 to a bogus value). 'git log --all --decorate' would then reflect the status of each remote accurately. One could then always pull from both repos (git fetch --all --prune) and then ensure local branches are created from and linked to remote tracking branches for repo_2, e.g. git checkout --track repo_1/feature; git push -u repo2 feature. One could then pull in commits from repo_1 branches into a local branch, e.g. 'git merge --ff-only repo_1/main'.


More Info: