Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pull request for a branch contains commits from other branches

Say I have two branches, master and dev.

I created a branch from master, made changes and merged to master.

Now I created another branch from master, but pushed my changes to dev and am trying to merge them, but when I make the pull request, the first branch merged to master appears in my request. How can I avoid this?

(The problem is that I should have merged the first branch to dev not master but now I can't do that anymore, because the same changes were merged to dev with another, third branch.)

like image 288
Nahapet Avatar asked Aug 17 '17 07:08

Nahapet


People also ask

Why are there other commits in my pull request?

Normally, you would see other people commits when creating a PR, unless you pushed your fixes onto a common branch (like master). You should: create a new branch locally ( git switch -c newBranchForFix <oldSHA1> ) from a known starting point (before your current commits)

Can a pull request contain multiple commits?

A pull will pull all the commits, including their dependencies - it won't cherry-pick individual commits. So if you want to request that only your commits be pulled, and there are other people's commits in the same branch, you have to first separate your commits into a different branch. Yes, it will.

How do you raise a pull request from one branch to another?

Create a pull request Click Create. Select the source branch which is wanted to be merged. Select the target branch to which you want the changes to be merged. Give an appropriate subject line and description that will be used as a commit message for a merged pull request.


1 Answers

Note: if this is TL;DR, skip down to the last section, Drawing your pull request.

Let's draw what you already did. Remember, in Git, each commit "points back" to its parent commit, or for a merge commit, to both of its (two) parents; and a branch name like master or dev or newbranch simply points to one commit, with the commits themselves doing the pointing-back so as to represent a whole series of commits.

I have to make some assumptions here. Bitbucket (which I have not used since they converted from a Mercurial-only shop to Git) and GitHub (which I do use) are a little bit different, but since they both use Git underneath, have a lot of similarities. I am going to assume you're using what Bitbucket's web site calls a "forking workflow".

I am going to try to avoid making many other assumptions. That makes this answer very long and complicated. (It might have served you well to have provided more information in your question, so that the answer could be shorter.)

Drawing what you have, part 1

So, you started with:

... <-B <-C <-D   <-- master

where commit D points back to commit C, C points back to B, and so on, and master points to D. Note that each of these single uppercase letters stands in for an actual commit hash ID, one of those incomprehensible 1badc0ffee4deadcab... or whatever things. We use the letters here because they're simple and easy and comprehensible; Git uses the hashes because the letters run out after just 26 commits. :-) (And for other better reasons.)

We know commits always point backwards. They have to: you can't point forwards to a commit you haven't made yet! So let's not bother with the interior arrows. But the branch names move over time, so let's keep drawing their arrows:

...--B--C--D   <-- master

Now you created a new branch, which also points to D:

...--B--C--D   <-- master, newbranch (HEAD)

(Here's a quick exercise for you: which of these three commits are on master, and which are on newbranch? This is a little bit of a trick question, because the answer is that all three commits are on both branches.)

We've added this HEAD notation so that we can tell which branch you're "on", as in, git status says on branch newbranch. Now you make a new commit E. Git makes the current branch—the one marked HEAD—point to the new commit we just made, and has the new commit point back to the previous branch-tip:

...--B--C--D   <-- master
            \
             E   <-- newbranch (HEAD)

Let's make two more commits just for the heck of it:

...--B--C--D   <-- master
            \
             E--F--G   <-- newbranch (HEAD)

Now the name newbranch points to G, which points back to F, and so on.

Drawing what you have, part 2: merging newbranch

The next part is tricky. You did:

git checkout master; git merge newbranch

or maybe:

git checkout master; git merge --no-ff newbranch

but you don't say which of these you did. The git checkout will make the HEAD label attach to master. But, given the drawing we've drawn here, you can see that Git could just slide the name master forward, in the left-to-right direction that is the opposite of all the internal arrows in the branches, so that the name master points to G:

...--B--C--D
            \
             E--F--G   <-- master (HEAD), newbranch

Git calls this label-sliding operation a fast-forward. One critical thing about this is that a fast-forward operation does not make a merge commit. This makes it harder to see where master was before you ran git merge, because we can straighten out the kink in the drawing and get:

...--B--C--D--E--F--G   <-- master (HEAD), newbranch

Given that D is really 4384e3cde2ce8ecd194202e171ae16333d241326 or some such big ugly hash ID, you probably don't remember what it really was. How will you know that you merged exactly three commits, and that master used to be 4384e3cde2ce8ecd194202e171ae16333d241326?

You can use the --no-ff kind of merge to force Git to make a real merge even if it otherwise doesn't have to. This helps figure out what happened. Instead of just sliding the name master forward, Git will do this:

...--B--C--D---------H   <-- master (HEAD)
            \       /
             E--F--G   <-- newbranch

In other words, here Git makes a new merge commit H. The new commit points back to both D and G. This is in fact what makes it a merge commit: it points to two parents.

Why we're doing all this graph-drawing

The precise shape of the post-git merge-command graph doesn't affect your current problem. You would get this same thing with either of the two post-merge graphs. Still, it's important to note when merge commits go in, and what they do: they record this kind of actual history, for future exploration. And in either case, it's important to be able to draw the graph, to predict what will happen with a pull request. Both post-merge-command graphs result in a problem.

Sometimes, making this "merge bubble" commit H pointing back to both G and D* is just useless clutter. If there's no future need to remember that this group of commits came in all at once via merging, you can and probably should use the fast-forward merge. If you want to force a real merge, you now know that there is this --no-ff option to git merge to do that.

If you use GitHub to make merges (perhaps via pull requests), you can use GitHub's controls to decide whether to force real merges. These controls are different from Git's (naturally, as they are GUI clicky buttons in your browser). GitHub generally tries to hide these graphs from you, which I think is a mistake—the graph is crucial in determining what is actually possible and what will happen when you try various things. For instance, it shows, visually, why your pull request had the problem.

(I have not used Bitbucket's web interface but I assume it works much like GitHub's.)

Drawing your second set of changes

You now say:

Now I created another branch from master, but pushed my changes to dev and am trying to merge them, but when I make the pull request, that first branch merged to master appears in my request.

Let's assume you made a fast-forward merge and therefore have this in your Git repositories, to start with. I'll assume you've deleted newbranch or that we don't care about it, and that your new new branch, which you "pushed to dev", is called zaphod:

...--D--E--F--G   <-- master, zaphod (HEAD)

Now you make a few new commits (let's put in two):

...--D--E--F--G   <-- master
               \
                H--I   <-- zaphod (HEAD)

You then do something—it's not clear to me precisely what series of Git commands and/or Web and/or GUI clicky boxes—to get your Bitbucket repository to make the name dev also refer to commit I, or perhaps to a new merge commit J that points back to I.

Note that there are two Git repositories of your own involved at this point: one on your local computer, and one on Bitbucket. The interesting thing about Git hash IDs, which we've been drawing here as single uppercase letters, is that they are the same in all copies of every repository. (This is not true of branch names!) So over on Bitbucket, instead of zaphod, we have the name dev pointing to commit J. We also don't really need the HEAD notation since you don't commit directly on Bitbucket:

...--D--E--F--G   <-- master
               \
                H--I   <-- dev

Meanwhile, there's a third repository that we need to bring in. This third repository is the one you forked from, in your Bitbucket "forking workflow".

I will assume for the moment that this third repository started out looking like this:

...--B--C--D   <-- master, dev

(If this is not the case, it's your job to draw the appropriate portions of the graph that they do have in this third repository.)

Drawing your pull request

With all the above out of the way, let's look at what a pull request really is.

All that a pull request is, is a request (hence the name) that someone else add, to their repository, some set of commits from one of your repositories, using a name associated with your particular repository—such as your dev—and a name associated with their repository, often the same name again.

In this case, the repository you make the pull request from is the one you have stored on Bitbucket that is your fork of their repository. So that's why we need the drawing of the graph that's in your Bitbucket repository:

...--B--C--D--E--F--G   <-- master
                     \
                      H--I   <-- dev

We also need to know what they have in their repository. I've assumed this:

...--B--C--D   <-- master, dev

Note that your D and their D are literally the same commit.

What you are asking them to do is to set their dev to point to commit I.

For them to do that, they must take your commits E--F--G--H--I into their repository, resulting in:

...--B--C--D   <-- master
            \
             E--F--G--H--I   <-- dev

But that's not what you really want them to do right now. You want them to take some commit(s) that somewhat resemble H and I, and do this:

...--B--C--D   <-- master
            \
             H2--I2   <-- dev

To get that to happen, you must first make H2 and I2 yourself.

To make H2 and I2, you will need to copy your original commits H and I to new commits. You will need these new commits to come right after commit D. So now you need to locate their tip-of-dev commit.

I've been drawing this as commit D, but that might not be correct. I don't have your repositories and have had to make various guesses (including your chosen Bitbucket workflow).

If you have their Bitbucket repository set up as an upstream, you can run git fetch remote-name to get your own Git, on your local computer, to obtain their latest commit from their dev and record it as remote-name/dev. (Typically your Bitbucket fork would be named origin, so that origin/dev records your fork's dev in your repository on Bitbucket. You might name their Bitbucket repository upstream so that upstream/dev names the commit at the tip of their dev branch.)

Let's say, for concreteness, that their Bitbucket repository, which you can refer to by the name upstream from your local Git repository, actually has this in it:

...--B--C--D   <-- master
            \
             N--O   <-- dev

Here you'd run git fetch upstream, and now your repository—your local one, not your copy on Bitbucket yet—would have:

                      H--I   <-- zaphod (HEAD), origin/dev
                     /
...--B--C--D--E--F--G   <-- master
            \
             N--O   <-- upstream/dev

You then need to create your own branch that points to commit O. To do that locally, you can use git checkout with -b and --track:

$ git checkout -b for-upstream-dev --track upstream/dev

                      H--I   <-- zaphod, origin/dev
                     /
...--B--C--D--E--F--G   <-- master
            \
             N--O   <-- upstream/dev, for-upstream-dev (HEAD)

Now you can copy commits H and I to commits H2 and I2, using git cherry-pick:

 $ git cherry-pick zaphod~2..zaphod

                      H--I   <-- zaphod, origin/dev
                     /
...--B--C--D--E--F--G   <-- master
            \
             N--O   <-- upstream/dev
                 \
                  H2--I2   <-- for-upstream-dev (HEAD)

Once you have this set up in your local repository, you need to get your Bitbucket repository to set its dev to point to commit I2. To do that you must force-push. Normally, force-pushing is bad, as it tells another Git repository to lose some commits, but here you want your Bitbucket repository to lose its H--I commits. So:

$ git push --force origin for-upstream-dev:dev

would have your (local) Git call up the origin Git—that's your Bitbucket Git—and tell it: I give you my I2, and of course my H2 and O and N if you need them; now you set your dev to my I2!

Assuming they obey this command, Bitbucket will now change its copy of your fork to look like this:

...--B--C--D--E--F--G   <-- master
            \
             N--O--H2--I2   <-- dev

(And, when the origin Git accepts this push, your own Git moves your origin/dev to point to I2, to remember that origin now has its dev pointing to I2.)

Now compare this graph to the one in the upstream Bitbucket repository, the one your Git calls upstream:

...--B--C--D   <-- master
            \
             N--O   <-- dev

The difference between these two graphs is that you will ask them—your upstream—to take your origin's H2 and I2 and add those to their dev ... which is what you asked for in your question.

Note that your own (local) repository still has the complicated, tangled mess of branches named zaphod and so on. Once your upstream has accepted your changes to their dev—taken your H2 and I2—it's probably time to clean up your local repository, making it look at least a little bit, if not a lot, like your origin repository. To do that, you can delete your own zaphod branch entirely:

                      H--I   [abandoned]
                     /
...--B--C--D--E--F--G   <-- master
            \
             N--O   <-- upstream/dev (maybe)
                 \
                  H2--I2   <-- for-origin-dev, origin/dev, upstream/dev (maybe)

Note that once you drop a branch label, the only way to find the commits that you used to find from the branch label, is to use some other name (like the raw hash ID). If all names for some commit(s) are gone, so that there is no way to find them, they will eventually be garbage collected, i.e., tossed out entirely. This is what will happen to the original H--I, now that they are abandoned.

The (maybe)s above are because your own Git won't know that upstream has accepted your pull request until you run git fetch upstream to have your (local) Git call up the upstream Git and find its latest dev.

like image 65
torek Avatar answered Nov 15 '22 08:11

torek