Can git push to the current branch of a remote repository?
Asked Answered
git
S

3

3

From https://mcmap.net/q/12459/-will-the-working-directory-be-updated-when-the-current-branch-is-updated

When you push to a checked out branch of a remote repository, you usually get a warning and Git will not allow you to do so.

But I get an implication that git can push to the current branch of a remote repository, from Version Control with Git, by Loeliger, 2ed, especially the texts in bold:

The push operation can update the repository state, including the HEAD commit. That is, even though the developer at the remote end has done nothing, the branch refs and HEAD might change, becoming out of sync with the checked out files and index.

A developer who is actively working in a repository into which an asynchronous push happens will not see the push. But a subsequent commit by that developer will occur on an unexpected HEAD, creating an odd history. A forced push will lose pushed commits from the other developer. The developer at that repository also may find herself unable to reconcile her history with either an upstream repository or a downstream clone because they are no longer simple fast-forwards as they should be. And she won’t know why: the repository has silently changed out from underneath her. Cats and dogs will live together. It’ll be bad.

As a result, you are encouraged to push only into a bare repository. This is not a hard- and-fast rule, but it’s a good guide to the average developer and is considered a best practice. There are a few instances and use cases where you might want to push into a development repository, but you should fully understand its implications. When you do want to push into a development repository, you may want to follow one of two basic approaches.

In the first scenario, you really do want to have a working directory with a branch checked out in the receiving repository. You may know, for example, that no other developer will ever be doing active development there and therefore there is no one who might be blind sided by silent changes being pushed into his repository.

In this case, you may want to enable a hook in the receiving repository to perform a checkout of some branch, perhaps the one just pushed, into the working directory as well. To verify that the receiving repository is in a sane state prior to having an automatic checkout, the hook should ensure that the nonbare repository’s working directory contains no edits or modified files and that its index has no files in the staged but uncommitted state when the push happens. When these conditions are not met, you run the risk of losing those edits or changes as the checkout overwrites them.

There is another scenario where pushing into a nonbare repository can work reasonably well. By agreement, each developer who pushes changes must push to a non–checked out branch that is considered simply a receiving branch. A developer never pushes to a branch that is expected to be checked out. It is up to some developer in particular to manage what branch is checked out and when. Perhaps that person is responsible for handling the receiving branches and merging them into a master branch before it is checked out.

Does it imply that git can push to the current branch of a remote repository? (My guess is yes, but I am not sure)

Does the paragraph before the last (the paragraph recommending using a hook that checkout the branch updated by push) assume that the branch to push to is not the current branch in remote repository? (My thought is that checking out the branch to push to implies the branch to push to isn't the current branch, but the last paragraph points out "a different scenario" of pushing to a non–checked out branch, implying to me that the previous paragraph is about pushing to a checked out branch i.e. the current branch)

Sharpen answered 3/1, 2016 at 7:3 Comment(3)
It is possible to do such a push; it's up to the remote (via that remote's config entries) what to allow. Hence the "usually" in the SO answer you quoted. (Don't have time to go into detail.)Eugenaeugene
Thanks. In he paragraph before the last, it recommends using a hook that checkout the branch updated by push. Does it assume that the branch to push to is the current branch in remote repository? My guess is no. But if yes, does it mean checking out a branch which is current i.e. checked out?Sharpen
I'm not sure that the second-to-last quoted paragraph is a good idea at all. I did work with a setup that did something like that and it has certain pitfalls...Eugenaeugene
W
7

Since Git 2.3+, you can configure the receiving end to "have a working directory with a branch checked out in the receiving repository."

Specifically: "push-to-checkout" (git 2.4, May 2015), which improves on "push-to-deploy" (Git 2.3, February 2015).

See "Deploy a project using Git push" for a concrete example.

  • First: push-to-deploy (git 2.3):

you can push changes directly to the repository on your server. Provided no local modifications have been made on the server, any changes to the server's current branch will be checked out automatically. Instant deploy!

To use this feature, you have to first enable it in the Git repository on your server by running

$ git config receive.denyCurrentBranch updateInstead
  • Then push-to-checkout hook (git 2.4)

There is now a push-to-checkout hook, which can be installed on the server to customize exactly what happens when a user pushes to the checked-out branch.
For example, by default such a push fails if there have been any changes to the working tree on the server. The push-to-checkout hook could instead try to merge any server-side edits with the new branch contents, or it could unconditionally overwrite any local changes with a pristine copy of the pushed branch contents.

In short, you can push directly to a checked out branch.
See commit 4d7a5ce for the caveats:

  • A change only to the working tree but not to the index is still a change to be protected;

  • An untracked file in the working tree that would be overwritten by a push-to-deploy needs to be protected;

  • A change that happens to make a file identical to what is being pushed is still a change to be protected (i.e. the feature's cleanliness requirement is more strict than that of checkout).

Windmill answered 3/1, 2016 at 9:41 Comment(0)
E
3

I'll take a stab at an answer, at this point, but I don't want to get bogged down in the details of what the book you're quoting says.

I think the way to view this is from the "receiving side", i.e., to think about what happens when you're an ordinary person using git and someone does a push into your repository (vs what you usually do, which is to fetch from theirs). Git provides a lot of mechanism for this, and a default policy that works ok, which is: "just say no". :-) Newer versions of git (2.3+) have added a few more policies that provide safety while allowing some of these pushes; whether you would call them "right" or "perfect" is more a matter of opinion. (Consider what happens if someone starts editing on your non-bare "deployed" host and then, say, falls asleep, so that no one else can push to it because it now "looks dirty" to git.)

Remember first that a repository can be set to "bare", which tells git not to look for a work-tree. (You can convert a non-bare repo to bare, or vice versa, but in my experience most people goof something up in the process. Using git clone --bare sets up a bare clone initially, and avoids creating a work-tree, which means there's no possibility for confusion or error here.1) Given a repository that has no work-tree, no one ever goes into its work-tree and does any work, and the "push screws up in-progress work" case is impossible.

With that in mind, let's look at what happens when we're the receiver of a push, and we have a non-bare repository, which has an actual work-tree. Let's also keep two more items in mind:

  • Because we have a work-tree, we also have an index file (.git/index) that is acting in its usual dual role of "place to stage the next commit" and "cache to speed up git status and similar operations".

  • We also have a "current" branch, as stored in the HEAD file,2 which we can look at directly, or use git symbolic-ref HEAD to read (the latter is the formally approved method). If the current branch is br, the HEAD file contains a single line reading ref: refs/heads/br, for instance, and git symbolic-ref HEAD prints refs/heads/br.

    (If we're in "detached HEAD" mode, the HEAD file contains a raw SHA-1, rather than ref: refs/heads/branch. In this case, receiving a push won't screw up in-progress work, so we can safely ignore this case.)

Here's the underlying mechanism behind receiving a push:

  1. As the receiver, we start by receiving objects to add to our repository.3 We add all those objects, even if we'll wind up rejecting one or more reference updates.

  2. Now we get a list of proposals, whose general form looks like: "please set refs/heads/X to 1234567...", "forcefully set refs/heads/Y to fedcba9...", and so on. These correspond to the refspecs whoever started up their push is using. (If the supplied SHA-1 is all-zeros, their git is asking us to delete these references.)

    We take each of these reference update requests into consideration, partly one at a time and partly in whole gulps, applying the sub-rules listed below. For updates that pass, we set the supplied reference to the supplied SHA-1 and tell the other git "ok, done"; for updates that fail, we tell the other git "no, rejected" and supply a bit more "reason" message. (We also pass the stderr output, and sometimes stdout output, of hooks on to the other git. There's a small protocol to tell him which pieces are our own answers and which are just passed-on output, so that his git knows which updates we accepted.)

  3. Once we're all done, we run a "post-receive hook" and pass it the successful updates (in the same form as for the pre-receive hook, but eliminating rejected updates).

Now let's cover (lightly) the rules used to accept or reject individual updates and/or the updates-as-a-whole. These are not necessarily in the actual internal order (and I'm going to ignore a few special cases like receive.shallowUpdates for instance):

  • Some updates must pass various built-in tests, most commonly the "fast forward" test, and sometimes a "never change this" test. Exactly which refs get tested in which way depends on our version of git, our configuration, and the force flag for this update. For details on these, see the git config documentation, paying particular attention to receive.denyDeletes and receive.denyNonFastForwards, and note that git used to apply fast-forward rules to tag updates (but not tag deletes) until git 1.8.2, when tags were changed to "never change" (but still allow deletes unless receive.denyDeletes is set).

  • The entire set of updates is sent to the pre-receive hook (on its standard input, as a series of lines). They're augmented with one more bit of information first: the current SHA-1 associated with each reference, or all-zeros if we don't already have that reference. If that hook exits non-zero, the entire set of updates—the whole push—is rejected. (If the hook does not exist, we consider this test to have passed.)

  • Each individual update is sent to the update hook (as arguments). If that hook exits non-zero, this particular update is rejected, but we go on to verify the others. (As before, if the hook does not exist, the test is an automatic pass.)

  • Finally, we have the "non bare repository" rules, which are the ones you care about here, and I will separate out into their own section.

Updating a non-bare repository: what's allowed and why

(I see VonC beat me to this, but I'll go ahead with details.)

The new configuration entries or values in git 1.6.6, git 2.3, and git 2.4 are:

  • receive.denyDeleteCurrent: this option was actually introduced in git 1.6.2 but didn't really do anything until git 1.6.6. Before then, receiving a push-to-delete of whatever the current branch was, deleted the current branch's reference. When it did so, HEAD was left pointing to a non-existent branch (to fix it you had to use the plumbing tools or modify the file directly). In git 1.6.6, this stopped being allowed by default. (I haven't tested to see if the "bad HEAD" thing still happens.)

  • receive.denyCurrentBranch: this also went in during 1.6.2 and was enabled (i.e., the default "refuse" action went live) in 1.6.6. However, it grew the new 'updateInstead' value in git 2.3.

Note that both of these are specific to "the current branch", i.e., the (single) reference refs/heads/br that HEAD refers to. And again, they apply only when core.bare is not set. In this case, there is a work-tree and it's full of files that relate, in some way, to whatever SHA-1 is filed away in refs/heads/br. There is also (probably4) an index file, which may or may not have things add-ed, rm-ed, and be holding merge state if you're in the middle of a conflicted merge.

Suppose that, via receive.denyCurrentBranch, you allow someone's git push to change your repository's stored SHA-1 for refs/heads/br. Suppose further that you don't have any deployment hooks set up, and are not using the new (2.3+) features. Then, in this case, if someone else alters your refs/heads/br, your own index and work-tree remain entirely unchanged. For concreteness, let's say that br used to point to commit 2222222... and someone else—Bob, for instance—has just successfully pushed and changed it to 3333333....

If you now finish your own editing / merging / whatever, git add the results as usual, and run git commit, git will make a new commit from your current index, which consists of "everything from commit 2222222... except for your git add and git rms". The stuff Bob did, which is in 3333333..., is not in your index. The new commit that git makes, though, will have 3333333... as its parent, while using the contents taken from your index, which is based on 2222222.... The effect is that your commit reverts all Bob's changes while adding all your changes: diffing your new commit against 2222222... will show you what you did, while diffing your new commit against its parent will show you backing out all Bob's work while keeping your own work.

If you do have a hook that does some deploying, the contents of your index and/or work-tree will depend on exactly what that hook does. For instance, if it does a git checkout -f, everything Bob changed will replace what you've put in the index and work-tree.

Neither of these results is ever what anyone actually wants.

The new updateInstead setting is closer to what people sometimes want: before allowing the reference update (Bob's change of refs/heads/br from 2222222... to 3333333...), git checks whether your index and work-tree match commit 2222222.... If they do,5 git allows Bob's push and applies that update to your index and work-tree, as if you had spotted Bob's push somehow and done git checkout br, or anything equivalent to bring everything up to date.

There's still some potential danger here. For instance, suppose you've opened up README to work on it. You've spent a while chasing down some reference URLs and typed them in, in your editor, but not written the result anywhere. Meanwhile, Bob has made a fix to README and he runs his git push. Your git sees that your work-tree is "clean" and that the update is "safe", so it updates your README.

Depending on how clever your editor is, when you go to write out your README, you may overwrite Bob's changes, or your editor might say "hey, README changed, I'll grab the new one" and lose your work, etc. One might argue that this is bad behavior by that editor (and I'd buy that argument), but it's still a potential problem—and it's not limited to editors; you might be running some slow computational process that writes files that you keep source-controlled, which could produce the same sorts of issues.

Git doesn't try to decide how to handle all this. Git just gives you configuration options (more mechanisms) and leaves the final policy up to you. I'd say git's defaults here are correct; the fancier updateInstead mode is not the default, because the "right" policy is unclear.


1There are still other possible errors, depending on whether you want group write mode and sharing for simple ssh pushes. At a previous workplace we eventually instituted a policy of configuring push-able repositories with a script: you would set up what you wanted to see in it as a private repo, then you run the script yourself or have an admin run it, giving it a URL for your private repo, to create the public-and-shared repo by cloning. After that we didn't care what you did with that private repo, but the main point here is that we would clone with --bare rather than having to have someone—usually me—go fix up all the broken bits. :-)

2Even a bare repository has a HEAD file and hence has a current branch. It also has an index, but with no work-dir the index is usually irrelevant. (Some deployment scripts wind up using the bare repo's index, which leads to bugs in some of those deployment scripts, but that's another question entirely.) The current branch is slightly relevant: it affects which branch someone else's git clone will check out for them at the end of the clone process, provided they did not specify a particular branch name.

3Usually we get these as a "thin pack", which is to say, a pack that can delta-compress against objects we already have. In order for that to happen, there's a step before the "receive objects" step, where we tell the sender what SHA-1s we have. You can see what we tell the sender by, on the sender, using git ls-remote. There's also some early protocol negotiation steps. These matter for lower level details, but not for the process described above.

4You can remove .git/index and git will re-construct it later when it needs it. I don't particularly recommend removing it, but the effect is to lose all stored-up git add and git rms, along with any merge information if you're in the middle of a merge.

5And additional tests pass (see VonC's answer). Some of those additional tests weren't in the initial stab at updateInstead mode and, I think, were found the hard way.

Eugenaeugene answered 3/1, 2016 at 10:39 Comment(1)
Instructive and detailed as usual. +1Windmill
B
2

Does it imply that git can push to the current branch of a remote repository?
(My guess is yes, but I am not sure)

It depends on your git version.
Prior to version 2 you had to explicitly tell git to which branch to push to, but from version 2.X it was changed.

All the details are here:

Git v2.0 Release Notes

Backward compatibility notes

When git push [$there] does not say what to push, we have used the
traditional "matching" semantics so far (all your branches were sent
to the remote as long as there already are branches of the same name over there).

In Git 2.0, the default is now the "simple" semantics,
which pushes:

  • only the current branch to the branch with the same name, and only when the current branch is set to integrate with that remote branch, if you are pushing to the same remote as you fetch from; or

  • only the current branch to the branch with the same name, if you are pushing to a remote that is not where you usually fetch from.

You can use the configuration variable "push.default" to change this. If you are an old-timer who wants to keep using the "matching" semantics, you can set the variable to "matching", for example. Read the documentation for other possibilities.


Setting upstream branches (remote branch for given branch)

You can set for each branch the default (fetch) remote branch which the branch will be "talking" to (pull/push).

The settings are defined in the .git/config file per branch.
You can also change it manually with the following command:

git branch --set-upstream-to=upstream/foo
Blueing answered 3/1, 2016 at 8:18 Comment(1)
He's asking about what happens on the receiving side, not the sending side.Eugenaeugene

© 2022 - 2024 — McMap. All rights reserved.