Is using Git equivalent to the Mercurial Push command?

advertisements

I've been using TortoiseHg/Mercurial for many years and am now using Git for first time with SourceTree and TortoiseGit GUI. From this link I understand that PUSH is different on Git from Mercurial. Mercurial PUSH only affects the remote repository but doesn't touch the workspace. PUSH with Git on the other hand not only updates the repository but also the workspace (and index which Mercurial doesn't have).

My question is does Git have the equivalent of Mercurial PUSH? Can I PUSH to a remote repository and leave the workspace and index as it is? I know Git FETCH is the equivalent of Mercurial PULL in that neither of them modify the local workspace. Effectively I'm looking for a reverse FETCH for Git. Is that possible or must I always open the remote repository and do a FETCH instead?


Edit: TL;DR summary:

  • When using Git, you can push to a repository humans use, that has a work-tree. You just should not push to the current branch within that repository, at least, not without special considerations. So Git blocks this by default.

  • Mercurial allows you to push to such repositories, including to the "current branch"—this phrase means something different in Mercurial—because in Mercurial, what Git would call a "detached HEAD" is a normal and usable state. This can be done in Git, it's just not very practical.

  • Therefore, Git offers, and you should generally use, bare repositories as push targets. These have no work-tree and therefore Git allows pushes to them, even to the "current branch" (Git's notion, rather than Mercurial's).

Original complete reply below.


As MrTux said, this claim is wrong:

PUSH with Git on the other hand not only updates the repository but also the workspace (and index which Mercurial doesn't have).

While pushing updates the repository (adding new commits as usual), it does not—at least, not by default—update the index, nor the work-tree. (It can be configured to do so in versions of Git starting from Git 2.4.0.)

The problem here is a mismatch between some fairly deep philosophical notions. The answer to this:

My question is does Git have the equivalent of Mercurial PUSH? Can I PUSH to a remote repository and leave the workspace and index as it is?

is "yes, using detached HEAD", but that answer is not as useful as you might think.

In Mercurial, the current revision (known as ., vs the name HEAD in Git) is checked-out, and possibly modified, in much the same way as it would be in Git if you were using a Git "detached HEAD" (though with some important differences). The index itself—the thing that exists and is exposed in Git, but does not exist in that form and is well-hidden in Mercurial—is not directly relevant.

The problem is that in Mercurial, the current branch is well-separated from the current revision. In Git, the two are deeply intertwingled. Clearly the current revision is on some branch: all revisions are on some branch, in Mercurial. But in Mercurial, all revisions are on exactly one branch. In Git, a revision—a commit—may be on many branches simultaneously: none, one, two, ten, or thousands. While the commit is unchanging and permanent (in both Git and Mercurial), the set of branches that contain the commit is fixed and unchanging in Mercurial, but totally fluid in Git.

Git (deliberately1) discards the idea that a branch name is anything more than a temporary, ephemeral label. The use of a branch name is just to identify one specific commit, for right now, mostly for humans to use, but also for use during git push and git fetch.

Therefore, since HEAD in Git contains the branch name—let's call this cb for Current Branch—and the branch name contains the commit ID, we will get in terrible trouble if we change the name-to-ID mapping in an unexpected manner. If receiving a push updates cb, while we are (through HEAD) using it as our current revision, we will get confused: we will believe that our index and work-tree apply to the commit to which cb now points, instead of the commit to which cb used to point before the update.

Again, this is not a problem in Mercurial, because in Mercurial, the system knows which is the current revision. Pushing new revisions (commits) into the branch does not affect our current revision, and in fact, if we make a new commit without updating, we just get a new commit that is a new head (lowercase, not Git's all-caps HEAD thing). This new head will have no name, unless we're using bookmarks, but having heads with no name is an ordinary state in Mercurial. We merely run hg heads to find these unnamed heads, and hg update -r to those revisions to work on them (such as to merge them, usually with other heads).

In Git, you can get precisely this state: it's that "detached HEAD". A detached HEAD contains a commit ID directly, rather than a branch name. Now that Git won't lose the current revision (or rather, commit hash ID), it's safe to push to this repository.

The problem is that when you have a detached HEAD, while you can make new commits, they're being remembered only by the name HEAD. They are on no branches. (If you prefer, you can call HEAD an "anonymous branch". It has the name HEAD but HEAD is not actually a branch name, as all branch names semi-secretly start with refs/heads/, and HEAD does not.) To make Git remember it more permanently, you must give this thing a branch name:

git checkout -b newbranch

which changes HEAD from "detached" to "attached", to the new branch newbranch. The branch name newbranch now stores the commit hash ID and the name HEAD now stores the branch-name newbranch.

But now you can't push to newbranch!

So as long as HEAD is not "detached", there is some branch you cannot, or at least should not, push to ... and for HEAD to work well, it must eventually be re-attached to a branch, maybe a new branch.

Note that you can push to any other branch. All the other branches are not in the index and not in the work-tree.

The new capability in Git 2.4.0 and later is that you can set receive.denyCurrentBranch to updateInstead. When it is set this way, a Git on the receiving end of git push checks:

  • Is the push going to the branch that is the current branch? That is, does HEAD have the name of a branch in it, and if so, is the push going to update this branch name?
  • If not (and all other tests pass), allow the update.
  • If so, check receive.denyCurrentBranch:
    • true or refuse: reject the push
    • false or ignore: accept the push, leaving the index and work-tree alone, relying on whoever is using this repository to know what to do
    • warn: accept the push, printing a warning to whoever is doing the push (ideally this is the same person who will know what to do, otherwise the warning is useless).
    • updateInstead: check one more thing (see below).

In the updateInstead mode, things get complicated. See the description in the git config documentation and the description of the push-to-checkout hook in the githooks documentation. The short version is that if the index and work-tree are clean, the push will generally be allowed and the receiving repository will be updated to the new branch-tip.

In general, though, your best bet is the much simpler rule: just don't do it.2 Don't push to a repository that has humans working in it. It's a bad idea. Push only to repositories that never have humans working in them. Configuring a Git repository as "bare" guarantees that no human can work in it, as it has no work-tree.


1This is either a wonderful idea, or a terrible idea, or perhaps both at the same time. Either way, it's radically different.

2This might be called the anti-Nike rule. :-)