Merge, update, and pull Git branches without using

2018-12-31 17:12发布

问题:

I work on a project that has 2 branches, A and B. I typically work on branch A, and merge stuff from branch B. For the merging, I would typically do:

git merge origin/branchB

However, I would also like to keep a local copy of branch B, as I may occasionally check out the branch without first merging with my branch A. For this, I would do:

git checkout branchB
git pull
git checkout branchA

Is there a way to do the above in one command, and without having to switch branch back and forth? Should I be using git update-ref for that? How?

回答1:

The Short Answer

As long as you\'re doing a fast-forward merge, then you can simply use

git fetch <remote> <sourceBranch>:<destinationBranch>

Examples:

# Merge local branch foo into local branch master,
# without having to checkout master first.
# Here `.` means to use the local repository as the \"remote\":
git fetch . foo:master

# Merge remote branch origin/foo into local branch foo,
# without having to checkout foo first:
git fetch origin foo:foo

While Amber\'s answer will also work in fast-forward cases, using git fetch in this way instead is a little safer than just force-moving the branch reference, since git fetch will automatically prevent accidental non-fast-forwards as long as you don\'t use + in the refspec.

The Long Answer

You cannot merge a branch B into branch A without checking out A first if it would result in a non-fast-forward merge. This is because a working copy is needed to resolve any potential conflicts.

However, in the case of fast-forward merges, this is possible, because such merges can never result in conflicts, by definition. To do this without checking out a branch first, you can use git fetch with a refspec.

Here\'s an example of updating master (disallowing non-fast-forward changes) if you have another branch feature checked out:

git fetch upstream master:master

This use-case is so common, that you\'ll probably want to make an alias for it in your git configuration file, like this one:

[alias]
    sync = !sh -c \'git checkout --quiet HEAD; git fetch upstream master:master; git checkout --quiet -\'

What this alias does is the following:

  1. git checkout HEAD: this puts your working copy into a detached-head state. This is useful if you want to update master while you happen to have it checked-out. I think it was necessary to do with because otherwise the branch reference for master won\'t move, but I don\'t remember if that\'s really right off-the-top of my head.

  2. git fetch upstream master:master: this fast-forwards your local master to the same place as upstream/master.

  3. git checkout - checks out your previously checked-out branch (that\'s what the - does in this case).

The syntax of git fetch for (non-)fast-forward merges

If you want the fetch command to fail if the update is non-fast-forward, then you simply use a refspec of the form

git fetch <remote> <remoteBranch>:<localBranch>

If you want to allow non-fast-forward updates, then you add a + to the front of the refspec:

git fetch <remote> +<remoteBranch>:<localBranch>

Note that you can pass your local repo as the \"remote\" parameter using .:

git fetch . <sourceBranch>:<destinationBranch>

The Documentation

From the git fetch documentation that explains this syntax (emphasis mine):

<refspec>

The format of a <refspec> parameter is an optional plus +, followed by the source ref <src>, followed by a colon :, followed by the destination ref <dst>.

The remote ref that matches <src> is fetched, and if <dst> is not empty string, the local ref that matches it is fast-forwarded using <src>. If the optional plus + is used, the local ref is updated even if it does not result in a fast-forward update.

See Also

  1. Git checkout and merge without touching working tree

  2. Merging without changing the working directory



回答2:

No, there is not. A checkout of the target branch is necessary to allow you to resolve conflicts, among other things (if Git is unable to automatically merge them).

However, if the merge is one that would be fast-forward, you don\'t need to check out the target branch, because you don\'t actually need to merge anything - all you have to do is update the branch to point to the new head ref. You can do this with git branch -f:

git branch -f branch-b branch-a

Will update branch-b to point to the head of branch-a.

The -f option stands for --force, which means you must be careful when using it. Don\'t use it unless you are sure you the merge will be fast-forward.



回答3:

As Amber said, fast-forward merges are the only case in which you could conceivably do this. Any other merge conceivably needs to go through the whole three-way merge, applying patches, resolving conflicts deal - and that means there need to be files around.

I happen to have a script around I use for exactly this: doing fast-forward merges without touching the work tree (unless you\'re merging into HEAD). It\'s a little long, because it\'s at least a bit robust - it checks to make sure that the merge would be a fast-forward, then performs it without checking out the branch, but producing the same results as if you had - you see the diff --stat summary of changes, and the entry in the reflog is exactly like a fast forward merge, instead of the \"reset\" one you get if you use branch -f. If you name it git-merge-ff and drop it in your bin directory, you can call it as a git command: git merge-ff.

#!/bin/bash

_usage() {
    echo \"Usage: git merge-ff <branch> <committish-to-merge>\" 1>&2
    exit 1
}

_merge_ff() {
    branch=\"$1\"
    commit=\"$2\"

    branch_orig_hash=\"$(git show-ref -s --verify refs/heads/$branch 2> /dev/null)\"
    if [ $? -ne 0 ]; then
        echo \"Error: unknown branch $branch\" 1>&2
        _usage
    fi

    commit_orig_hash=\"$(git rev-parse --verify $commit 2> /dev/null)\"
    if [ $? -ne 0 ]; then
        echo \"Error: unknown revision $commit\" 1>&2
        _usage
    fi

    if [ \"$(git symbolic-ref HEAD)\" = \"refs/heads/$branch\" ]; then
        git merge $quiet --ff-only \"$commit\"
    else
        if [ \"$(git merge-base $branch_orig_hash $commit_orig_hash)\" != \"$branch_orig_hash\" ]; then
            echo \"Error: merging $commit into $branch would not be a fast-forward\" 1>&2
            exit 1
        fi
        echo \"Updating ${branch_orig_hash:0:7}..${commit_orig_hash:0:7}\"
        if git update-ref -m \"merge $commit: Fast forward\" \"refs/heads/$branch\" \"$commit_orig_hash\" \"$branch_orig_hash\"; then
            if [ -z $quiet ]; then
                echo \"Fast forward\"
                git diff --stat \"$branch@{1}\" \"$branch\"
            fi
        else
            echo \"Error: fast forward using update-ref failed\" 1>&2
        fi
    fi
}

while getopts \"q\" opt; do
    case $opt in
        q ) quiet=\"-q\";;
        * ) ;;
    esac
done
shift $((OPTIND-1))

case $# in
    2 ) _merge_ff \"$1\" \"$2\";;
    * ) _usage
esac

P.S. If anyone sees any issues with that script, please comment! It was a write-and-forget job, but I\'d be happy to improve it.



回答4:

You can only do this if the merge is a fast-forward. If it\'s not, then git needs to have the files checked out so it can merge them!

To do it for a fast-forward only:

git fetch <branch that would be pulled for branchB>
git update-ref -m \"merge <commit>: Fast forward\" refs/heads/<branch> <commit>

where <commit> is the fetched commit, the one you want to fast-forward to. This is basically like using git branch -f to move the branch, except it also records it in the reflog as if you actually did the merge.

Please, please, please don\'t do this for something that\'s not a fast-forward, or you\'ll just be resetting your branch to the other commit. (To check, see if git merge-base <branch> <commit> gives the branch\'s SHA1.)



回答5:

Another, admittedly pretty brute way is to just re-create the branch:

git fetch remote
git branch -f localbranch remote/remotebranch

This throws away the local outdated branch and re-creates one with the same name, so use with care ...



回答6:

In your case you can use

git fetch origin branchB:branchB

which does what you want (assuming the merge is fast-forward). If the branch can\'t be updated because it requires a non-fast-forward merge, then this fails safely with a message.

This form of fetch has some more useful options too:

git fetch <remote> <sourceBranch>:<destinationBranch>

Note that <remote> can be a local repository, and <sourceBranch> can be a tracking branch. So you can update a local branch, even if it\'s not checked out, without accessing the network.

Currently, my upstream server access is via a slow VPN, so I periodically connect, git fetch to update all remotes, and then disconnect. Then if, say, the remote master has changed, I can do

git fetch . remotes/origin/master:master

to safely bring my local master up to date, even if I currently have some other branch checked out. No network access required.



回答7:

You can clone the repo and do the merge in the new repo. On the same filesystem, this will hardlink rather than copy most of the data. Finish by pulling the results into the original repo.



回答8:

Enter git-forward-merge:

Without needing to checkout destination, git-forward-merge <source> <destination> merges source into destination branch.

https://github.com/schuyler1d/git-forward-merge

Only works for automatic merges, if there are conflicts you need to use the regular merge.



回答9:

For many cases (such as merging), you can just use the remote branch without having to update the local tracking branch. Adding a message in the reflog sounds like overkill and will stop it being quicker. To make it easier to recover, add the following into your git config

[core]
    logallrefupdates=true

Then type

git reflog show mybranch

to see the recent history for your branch



回答10:

I wrote a shell function for a similar use case I encounter daily on projects. This is basically a shortcut for keeping local branches up to date with a common branch like develop before opening a PR, etc.

Posting this even though you don\'t want to use checkout, in case others don\'t mind that constraint.

glmh (\"git pull and merge here\") will automatically checkout branchB, pull the latest, re-checkout branchA, and merge branchB.

Doesn\'t address the need to keep a local copy of branchA, but could easily be modified to do so by adding a step before checking out branchB. Something like...

git branch ${branchA}-no-branchB ${branchA}

For simple fast-forward merges, this skips to the commit message prompt.

For non fast-forward merges, this places your branch in the conflict resolution state (you likely need to intervene).

To setup, add to .bashrc or .zshrc, etc:

glmh() {
    branchB=$1
    [ $# -eq 0 ] && { branchB=\"develop\" }
    branchA=\"$(git branch | grep \'*\' | sed \'s/* //g\')\"
    git checkout ${branchB} && git pull
    git checkout ${branchA} && git merge ${branchB} 
}

Usage:

# No argument given, will assume \"develop\"
> glmh

# Pass an argument to pull and merge a specific branch
> glmh your-other-branch

Note: This is not robust enough to hand-off of args beyond branch name to git merge