sir-haver
sir-haver

Reputation: 3602

Why does Git pushes files I resolved when pulling?

I just 'git pull' to receive the latest version of the files from the remote repo, and some newer files created conflicts that I had to resolve. In all conflicts I chose to accept the remote repo version so those files are exactly the same as the remote.

Now I would like to push the repo because of changes made to another file, but Git pushes all the files that had conflicts even though they are exactly the same as the remote repo. Why does it happen? Is there a way to avoid this?

Upvotes: 0

Views: 73

Answers (3)

Ajay poly
Ajay poly

Reputation: 73

If you haven't set rebase=true in .gitconfig, please set it up like this:

[pull]
    rebase = true

When you have conflicts you should resolve it and force push it:

git push -f

Upvotes: 0

torek
torek

Reputation: 489628

This is indeed one reason people use rebase.

Remember that each Git commit:

  • is numbered: it has a raw hash ID like 4af7188bc97f70277d0f10d56d5373022b1fa385, unique to that one particular commit;
  • is completely read-only: no part of 4af7blahblah can ever change;
  • is mostly permanent: once you have 4af7blahblah you will still have it, but more interesting will be whether or not you can find it and whether you see it;
  • contains both a full snapshot of every file and some metadata; and
  • is found, in Git, by starting with a name—a branch or tag name for instance—that holds the hash ID of the latest commit and then working backwards.

That last bullet point is important: you find your commits using a branch name. Every Git repository has its own private branch names. You'll have your Git show some other Git software some of your branch names now and then, and they'll show you theirs now and then, but each repository has its own branch names. That's why, when you git clone from GitHub or wherever, you get a bunch of origin/* names. Those are your copies of their branch names.

(Compare: the name "Paul" is very common, and you can't assume that two people both named "Paul" are in fact the same. You need to qualify it: "Paul Aberly" vs "Paul Brumblefee", or on this case, "branch feature/foo in repository A" vs "branch feature/foo in repository B".)

When you have been working for a while in some branch of your own, other people have been working in their branches, in their repositories. Eventually they add new commits to the clone over on GitHub or wherever. You have, in your repository, at this point:

          I--J   <-- your-feature (HEAD)
         /
...--G--H   <-- origin/master

where each uppercase letter stands in for a commit. You've made two new commits I and J.

But now you run git fetch—the first step of a git pull command—to fetch any new commits from origin, and there are some on their master, which is your origin/master. So your Git adds those to your own repository and updates your origin/master to remember them:

          I--J   <-- your-feature (HEAD)
         /
...--G--H
         \
          K--L   <-- origin/master

When you ran git pull, you actually told your Git to run two Git commands:

  1. git fetch, which did the above;
  2. a second command of your choice (which you must choose before you enter the git pull command): if you don't choose one, you get a default git merge.

This second command has to combine the work you did with the work they did. This work-combining process requires making a new commit, so let's draw that in:

          I--J
         /    \
...--G--H      M   <-- your-feature (HEAD)
         \    /
          K--L   <-- origin/master

Your new commit M connects your work, in commits I-J, to their work, in commits K-L, through the common starting point, commit H. (Git is all about the commits. The names are just tricks we use to find commits, without having to type in commit hash IDs.)

To make your work fit in with their work, commit M modifies your code in J to match their changes in L. That is, to your changes, Git has added their changes. Or you can look at it another way: to make their work fit in with your work, commit M modifies their code in L to match your changes in J.

You mention that there were conflicts:

In all conflicts I chose to accept the remote repo version so those files are exactly the same as the remote.

Blindly accepting their version is rarely right, but if you examined these overlapping (colliding) changes closely and determined that your own change was no longer relevant and should be thrown away, then that was the right thing to do.

The right thing to do is to look carefully and determine what the right final file version should look like. Sometimes that means "use their version, throw out all my work", sometimes it means "use my version, throw out all their work" and sometimes it means "take this part of my work, that part of their work, and use this new third thing I just came up with too". This requires careful judgement and usually careful testing as well. You cannot omit this step or use a blanket rule like "theirs is always better" unless you have very strong tests. Whatever you put in here, Git is going to believe that this is the correct result, so make sure it is the correct result!

In any case, commit M makes changes to the work you did so that you have whatever snapshot went into new commit M. It's therefore part of your branch and must be included in the commits you send as part of your Pull Request. But you do have one other option.

The other option: regretting and taking back your original commits

Let's go back to the pre-git merge picture, after git fetch but before the second command that git pull ran:

          I--J   <-- your-feature (HEAD)
         /
...--G--H
         \
          K--L   <-- origin/master

If you used git fetch instead of git pull, Git would stop here, and you could now inspect things. You could run a test merge if you like:

git merge origin/master

and if there are merge conflicts, you could inspect those conflicts and say to yourself: Oh, hey, look, the problem is that the changes that I made in commit I are now useless! They fixed the problem a different way. If only I had not made commit I after all, I could just use the changes from commit J.

This is where git rebase comes in. With git rebase, we say to ourselves:

  • I made a bunch of commits, to do a bunch of work.
  • Some parts of this work are good, and some parts of this are bad.
  • I'd like to redo my work, and make new and improved commits and forget the original commits.

This is what git rebase does. The rebase command comes in a dizzying variety of flavors, e.g., git rebase --interactive and git rebase --autosquash and other fancy modes, but fundamentally it's about saying to yourself that you want to replace your old-and-lousy commits with new-and-improved commits.

Because Git is about commits and commits, with their unique hash IDs, get shared, you should be careful only to do this with commits that aren't already shared, in general. (In some very specific cases, you can do it with commits that are shared, as long as the other people sharing them are aware of what you're doing.)

To use git rebase is tricky, but knowing that it's going to copy commits, one commit at a time, helps. Here's that diagram yet again:

          I--J   <-- your-feature (HEAD)
         /
...--G--H
         \
          K--L   <-- origin/master

Suppose we now make a new branch, new-and-improved, pointing at commit L, and switch to it:

git switch -c new-and-improved --no-track origin/master

for instance (the --no-track is to keep from setting origin/master as the upstream of this new branch):

          I--J   <-- your-feature
         /
...--G--H
         \
          K--L   <-- new-and-improved (HEAD), origin/master

Now we copy part of commit I, or perhaps if commit I is entirely bad, deliberately don't copy it at all. Let's say we copy just one of the changes, so that we get a new commit that's like I but smaller:

          I--J   <-- your-feature
         /
...--G--H
         \
          K--L   <-- origin/master
              \
               i   <-- new-and-improved (HEAD)

We drew it as a lowercase i because it's so much smaller than the original I, but is otherwise a lot like I. We'll even re-use the commit message, perhaps.

Then we copy commit J wholesale, without much change, to make a new commit we'll call J' to show how much it is like the original J:

          I--J   <-- your-feature
         /
...--G--H
         \
          K--L   <-- origin/master
              \
               i--J'  <-- new-and-improved (HEAD)

Now we come to the last trick that git rebase does for us. It grabs the name your-feature—the branch you were on when you started all this—and makes that name point to commit J', where you are now: The temporary branch disappears and we have:

          I--J   ???
         /
...--G--H
         \
          K--L   <-- origin/master
              \
               i--J'  <-- your-feature (HEAD)

If nobody else has commits I and J, the only way you can find them now is if you memorized their hash IDs. You won't see them! Nobody else can see them either, because you're the only one who has these two commits.

So if the final result, after rebasing, is all good, this is what you want to send to GitHub to make into your Pull Request on GitHub. You can now:

git push origin your-feature

(or whatever name it has) and make the PR from there. Nobody has to know about your original I-J commits.

If you ever sent I-J to some other Git repository, make sure no one else is using them before you do this! It's very hard to get rid of a commit once it spreads: they're like viruses, infecting Git repositories. When two Git repositories meet up and one has some name or names that locate these commits, the one that has the commits can send them (if in "send commits" mode) to the one that doesn't, and then the commits have been copied and can spread from that repository to more repositories.

Other than this caveat about being careful not to spread bad commits like they were COVID, rebase is generally pretty nice. But it can be hard to use: be very good at regular git merge before you start in on git rebase, because every commit you copy is a mini-git merge of sorts.

Upvotes: 2

Jens Schauder
Jens Schauder

Reputation: 82008

Even if you accepted the remote version you still created a merge commit which basically contains the information that the changes you made are integrated in the branch. The merge commit will have two parents: the commit you pulled and your local one.

This new commit needs pushing.

You'll see the commit when you inspect the log using git log or your preferred visual tool for inspecting the commit history.

Upvotes: 1

Related Questions