Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What happens when Git pull or push to different branch being in some other branch

I am familiar with GIT and using the same for versioning of my project.

I am having few queries which I would like to know. I googled but couldn't get the good answer.

So the thing is I am having master, module1, feature1 branches.

master
 ---------- 
         | module1
         ----------
                  | feature1
                  ------------

module1 is branched from master and feature1 is branched from module1.

Query 1 : Being in feature1 branch what if I work on few changes and commit and push the same to module1.

git add .
git commit -m "Changes of feature1"
git push origin module1 //Being in feature1 branch

What happens here to the code of feature1 to module1 branch and how module1 branch takes it.

My Understaing : As per my understanding the feature1 changes will be pushed to module1 branch. And later I realised that I should have pushed to feature1 branch then I will push the same to feature1 branch and will checkout to module1 branch and revert the code which I had pushed recently.

Query 2 : What if being in the feature1 branch and I pull the code of module1 in this branch by the following

git pull origin module1 //Being in feature1 branch

My Understanding : The changes of the module1 code will get merged to my feature1 branch and is same as the following in commands

git checkout moduel1
git pull origin module1
git checkout feature1
git merge module1

If there are any conflicts will be shown. Which I need to resolve.

Can anyone help me whether my understanding is correct or not. If not please help me to understand this concept properly. Thanks in advance.

like image 974
Channaveer Hakari Avatar asked Dec 21 '17 09:12

Channaveer Hakari


People also ask

Does git push affect other branches?

No, git push only pushes commits from current local branch to remote branch that you specified in command.

Does git pull affect all branches?

git pull is a Git command used to update the local version of a repository from a remote. It is one of the four commands that prompts network interaction by Git. By default, git pull does two things. Updates the remote tracking branches for all other branches.

What happens if you make a change on a branch you don't own?

When you change a file in your work-tree, nothing happens to the copy in the index. It still matches the copy in the commit you chose. You have to run git add to copy the file from the work-tree, to the index.

Can I push a branch from another branch?

Push Branch to Another Branch In some cases, you may want to push your changes to another branch on the remote repository. In order to push your branch to another remote branch, use the “git push” command and specify the remote name, the name of your local branch as the name of the remote branch.


1 Answers

You have, I think, a few broad misconceptions about branch names and the use of git pull. Let me split this into several parts, and give you this executive summary overview to start with:

  • push's counterpart is not pull, it's fetch;
  • git pull just runs git fetch followed by a second Git command, usually git merge, and I believe it's best for new Git users to avoid git pull, using instead the two separate commands;
  • while push and fetch use names for transferring hash IDs, it's the commits, identified by hash IDs, that matter; and
  • for git merge or git rebase, your current branch does matter. During push or fetch, the current branch does not matter, but if you use git pull, that runs git merge or git rebase, and now the current branch matters.

Now, let's dive into the details.

A branch name is only a pointer to a (single) commit

Git is all about commits. Git would, in a sense, "like it" if us mere humans did not need branch names, and just talked about commits by hash ID all the time. I might ask you if you are using commit 95ec6b1b3393eb6e26da40c565520a8db9796e9f, and you would say "yes" or "no, but I have that one" or "no, and I have not heard of that one yet".

You mention that:

module1 is branched from master and feature1 is branched from module1.

but in Git's eyes, branches don't branch from another branch. Instead, each commit links to a previous, or parent, commit. You drew this:

master
 ---------- 
         | module1
         ----------
                  | feature1
                  ------------

which suggests to me that you think of commits as belonging to (or being "on") only one branch. That's not how Git sees them, though. Instead, most commits are on many branches at the same time. For instance, consider a graph that we might draw like this:

          o--o--o   <-- br1
         /
...--o--o--o   <-- master
         \
          o--o   <-- br2

where each round o represents a commit. All the commits down the middle row are on master, but most of those commits are also on br1 and br2. The last (newest and right-most) commit on master is only on master; the rest are on the other branches as well.

This is all because, in Git, a branch name like master points only to one commit. The commit to which the name points is the one furthest to the right, if we draw the commit graph this way, left (earlier) to right (later). Git calls this the tip commit, or sometimes the head (note lowercase here) of the branch. To find the rest of the commits that you—or Git—can reach from this tip commit, Git will look up the commit by its big ugly hash ID, such as 95ec6b1.... Again, it's the hash ID that lets Git find the commit. The name just lets Git find this hash ID!

The commit itself stores a parent commit hash ID, so Git will look up the parent commit by its hash ID, finding the commit one step back. That commit has another parent ID, and so on. Walking backwards through this sequence of parent hash IDs, one commit at a time, from later to earlier, is how we see git log, for instance.

If you run:

git checkout br1

and then do some work and then run:

git add -a && git commit

and make a new commit—let's draw this as * instead of o so that we can see it—here's what happens to the branch name br1:

          o--o--o--*   <-- br1 (HEAD)
         /
...--o--o--o   <-- master
         \
          o--o   <-- br2

We draw this with the (HEAD) (note: all-uppercase) added to remember which name we gave to git checkout. The new commit * goes in the graph, and points backwards to whichever commit was the tip of br1. Meanwhile Git changes the name br1 so that it points to the new commit we just made. This is how branches grow, in Git: we add new commits to the graph, and Git updates the name that HEAD is attached-to.

Of course, it's no accident that if we start at the tips of both br1 and master and work backwards, we'll eventually come back to a single meeting-point commit. But this confluence of commits doesn't arise from the names. It doesn't matter how we choose to start from the tip commit of br1 and the tip commit of master; what matters is these two particular commits, and then each commit we find along the way.

The branch names, in other words, get us started in the commit graph. It's the commit graph that matters most. The names just serve as starting points!

The opposite of push is fetch

Everything above is about working within a single Git repository. But when we do work with Git, we work with more than one repository. In particular, we have our repository, where we do our own work, but then we often need to talk with another Git repository stored on some other machine, like those provided by GitHub, or by an employer or a friend or colleague.

This means that we want to share commits. As we saw above, Git is all about the commits and the commit graph—but we mere humans need names to get us started. This is where git fetch and its counterpart, git push, come in. Running either command connects our Git to some other Git.

Our Git has all the commits we have, some of which may be commits that we made. Some of our Git's commits might be commits we got from elsewhere. Similarly, their Git has all the commits they have, some of which are the same as commits we have. Some may be different commits. But in any case, all of these commits are identified by their unique hash IDs. These IDs are the same in every Git, all around the world, if they have the same commits. If they don't have our commits (because we just made them), our new commits have IDs that are different from every commit ID they have! (This seems like magic, but it's just cryptographic mathematics—some of which is similar to what's behind Bitcoin, for instance, though Git uses a weaker set of hashes.)

In the end, this means that each of these two Gits can tell which commits one of us has that the other doesn't, just by looking at these hash IDs. That's how our Git can give their commits we have that they don't—git push—or their Git can give our Git commits they have that we don't: git fetch.

Once they have sent commit objects (and other related Git objects needed to make those commits complete), the two Gits then need to set up names for any new tip commits. This is the second place that branch names start to matter.

The fetch direction is simpler. Your Git calls up some other Git. Usually we use the name origin to stand for some URL, where the other Git is listening for calls, so we run git fetch origin. Your Git calls up that Git, and asks it: What are your branch tip names? What hash IDs are those? Their Git tells your Git about its branch tips and hash IDs, and your Git either says: Ah, I have that hash ID or Hmm, I don't have that hash ID, please send me that commit, and by the way what's its parent hash ID because maybe I need that one too, and so on.

Eventually, your Git has all the commit and other hashed objects that they suggested. Now your Git takes their branch names, like their master, and saves those names. But you have your own branches. Your Git cannot save those names as your branches. It renames all those names. Their branches, like master, become your Git's remote-tracking names, like origin/master. Note that your Git is simply using your short-hand name, origin, plus a slash, in front of their branch names.

Once git fetch finishes, your Git now remembers where their Git's branch tips were, using your origin/* remote-tracking names. You have their commits, plus any required associated stuff so that you can check out those commits and get the files that go with them, but any new commits are only going to be found via these remote-tracking names. If they have, as branch tip commits, some of your older commits, you might already have other ways to find them.

The counterpart to git fetch is git push, but it's not completely symmetric. To do a git push to origin, for instance, you have your Git call up their Git as before, but this time, you want to send them things. The first part of this sending operation is to hand over any commits that you have, that they don't, that they will need. You identify these commits by having your git push take some extra arguments:

git push origin module1:module1

Note that I've put in the same name twice here. The name on the left, the module1:, is to find the specific commit hash that you want to send to them. That's the tip commit of your module1. The name on the right, the :module1 part, is the name you want them to use. (These names don't have to be the same! But it gets tricky if you use different names on each side, so avoid that if possible.)

When you run git fetch origin, you generally want to pick up everything they have that you don't. That's quite safe, because whatever tip commit they have as their master, your Git will call your origin/master. Whatever tip commit they have as their module1, your Git will call that your origin/module1. Your remote-tracking names are your own private entries, all reserved for that one remote named origin, and it's harmless, or even a good thing, to just get them all up to date right away.1

But git push doesn't work this way. You send them a commit hash ID, and then ask them to set their branch, master or module1 or feature1, to that hash ID. They have your Git send the commit object (and as many parent commits and other objects they need, all identified by hash IDs) if they don't already have that commit-ID, and then they evaluate for themselves whether they will let you set their branch name. Sometimes they will, and your push succeeds; sometimes they find a rule that says "don't allow this", and your push fails.

Note that their rules are up to them! You send a request ("please set your module1 to a9fc312..."); they can look at their current module1 branch hash ID, and the commit you've sent if it's new, and choose whether or not to take the request. You can use --force to send it as a command, but even if you do that, they can still choose whether to obey the command. All you can do is make a request (or forceful command) and see whether they accept it. But there's a standard rule that most Gits use most of the time: a request is allowed if the only thing it does is add new commits.

Look what happened when you added a commit to br1 above. The existing commits, which were all reachable from the original tip commit, were still reachable from the new tip commit. The new commit "grew the branch". It did not change any of the existing—no new commit can ever change any existing commit—but the new commit's parent was the old tip commit. If we start at the new tip and work backwards, as Git does, we arrive at the old tip.

The same rules work well with git push: if the request you send to the other Git will keep all its existing commits reachable from the new branch tip, the other Git will probably allow the request.

Note that we've been using git push origin module1:module1, but you suggested that we run:

git push origin module1 //Being in feature1 branch

It doesn't matter which branch we're in (or on, as git status would say on branch feature1). What matters here is the two module1:module1 parts to the git push command. That tells our Git: use our name module1 to find the commit to push, and ask their Git to set their module1 name.

We didn't give two parts. We only gave one part. For git push, if you only give one part, Git just assumes you mean "use that same name twice". So git push origin module1 means git push origin module1:module1, after all.

git pull is git fetch followed by a second Git command

You then asked about this:

git pull origin module1 //Being in feature1 branch

What git pull does is simple to describe:

  1. It runs git fetch with most of the arguments you pass it:

    git fetch origin module1
    
  2. If that works, it runs a second Git command, which you pick in advance. The default command to run is git merge. The exact arguments to this second command depend a little bit on what came in during the git fetch, but you still have to pick git merge vs git rebase before you get to see what came in.

We mentioned before that we normally just run git fetch origin, which brings over all the names origin has, and your Git then renames. If you run:

git fetch origin module1

this simply limits the fetch: it finds out what they have as their module1, brings over the commit(s) by ID if necessary, and then sets your origin/module1.2 It ignores all their other names. The module1 here is a lot like in git push, except that if you don't use two names—if you don't write module1:module1—your Git will just update your remote-tracking name. (And you should rarely if ever use two names separated by a colon here. It does work and can be used for some purposes, but you will need to learn more details.)

During the git fetch, it doesn't matter which branch you have checked out. But the second Git command is either git merge or git rebase, and for these two commands, it does matter which branch you have checked out.

The second command that git pull runs, runs without changing the current branch. Assuming the second branch is git merge, the arguments are:

  • the branch tip hash ID3 obtained from the remote Git, and
  • the message "merge branch 'name' of url"

This is a lot like running git merge origin/name, though the message is a bit different.

What git merge does with this is itself a bit complicated, since git merge sometimes does nothing, sometimes does a fast-forward instead of merging, sometimes does a real merge, and sometimes has merge conflicts that make the merge stop in the middle and get help from you. I'll leave all of this to other answers.

Let's summarize here:

  • Your git push and git fetch operations always transfer whole commits. They never work on individual files: they copy commits from one Git repository to another. The process of copying commits normally involves setting some name(s) in the receiving Git, so as to remember any adjusted tip commits. For the most part, these don't care about your current branch (though git push can be told to push the current branch by default: see push.default).

  • Your git pull simply runs two separate Git commands. If you're not already very familiar with both of those commands, I suggest you run each command separately instead.

  • Your git merge command is the most complicated one, and should be a separate question. If you don't think you ran a git merge command, see git pull above.


1The one drawback to getting everything is that it could take a long time, if there is a lot of "everything" and your net connection is slow. On the other hand, if you get everything today, you will have almost nothing to get tomorrow, so the next git fetch will go fast. If you selectively get just a little today, maybe tomorrow's git fetch will be slow.

2This assumes your Git version is at least 1.8.4. In Git before 1.8.4, git fetch origin module1 brings over their hash ID, but then throws away the name, failing to update your own origin/module1.

3If you run git pull origin name1 name2, Git passes multiple hash IDs to git merge, which produces what Git calls an octopus merge. This is almost never what you want, so avoid that. If you avoid git pull you won't make this particular mistake!

like image 194
torek Avatar answered Sep 23 '22 17:09

torek