From d38f1641fc33535aa3c92cf6d3a30334324d3488 Mon Sep 17 00:00:00 2001 From: Felipe Contreras Date: Fri, 9 May 2014 13:45:25 -0500 Subject: [PATCH] Add new `git update` tool Signed-off-by: Felipe Contreras --- .gitignore | 1 + Documentation/config.txt | 27 ++++ Documentation/git-update.txt | 156 ++++++++++++++++++ Makefile | 1 + git-update.sh | 301 +++++++++++++++++++++++++++++++++++ t/t5580-update.sh | 144 +++++++++++++++++ 6 files changed, 630 insertions(+) create mode 100644 Documentation/git-update.txt create mode 100755 git-update.sh create mode 100755 t/t5580-update.sh diff --git a/.gitignore b/.gitignore index 5087ce1eb7..7881d62632 100644 --- a/.gitignore +++ b/.gitignore @@ -161,6 +161,7 @@ /git-tag /git-unpack-file /git-unpack-objects +/git-update /git-update-index /git-update-ref /git-update-server-info diff --git a/Documentation/config.txt b/Documentation/config.txt index 2cd6bdd7d2..0a157ef56e 100644 --- a/Documentation/config.txt +++ b/Documentation/config.txt @@ -885,6 +885,19 @@ When the value is `interactive`, the rebase is run in interactive mode. it unless you understand the implications (see linkgit:git-rebase[1] for details). +branch..updatemode:: + When `git update` is run, this determines if it would either merge or + rebase the fetched branch. The possible values are 'merge', + 'rebase', and 'rebase-preserve'. + If 'ff-only' is specified, the merge will only succeed if it's + fast-forward. ++ + When 'rebase-preserve', also pass '--preserve-merges' along to + 'git rebase' so that locally committed merge commits will not be + flattened by running `git update`. ++ + See "update.mode" for doing this in a non branch-specific manner. + branch..description:: Branch description, can be edited with `git branch --edit-description`. Branch description is @@ -2217,6 +2230,20 @@ When the value is `interactive`, the rebase is run in interactive mode. it unless you understand the implications (see linkgit:git-rebase[1] for details). +update.mode:: + When `git update` is run, this determines if it would either merge or + rebase the fetched branch. The possible values are 'merge', + 'rebase', 'ff-only', and 'rebase-preserve'. + If 'ff-only' is specified, the merge will only succeed if it's + fast-forward. ++ + When 'rebase-preserve', also pass '--preserve-merges' along to + 'git rebase' so that locally committed merge commits will not be + flattened by running `git update`. ++ + See "branch..updatemode" for doing this in a branch-specific + manner. + pull.octopus:: The default merge strategy to use when pulling multiple branches at once. diff --git a/Documentation/git-update.txt b/Documentation/git-update.txt new file mode 100644 index 0000000000..1e58f55a1b --- /dev/null +++ b/Documentation/git-update.txt @@ -0,0 +1,156 @@ +git-update(1) +============= + +NAME +---- +git-update - update the current branch to the latest remote status + +SYNOPSIS +-------- +[verse] +'git update' [options] + +DESCRIPTION +----------- + +Incorporates changes from a remote repository into the current +branch. + +`git update` runs `git fetch` and then tries to update the current branch to +the latest status. If you don't have any extra changes, the update operation is +straight-forward, but if you do have them, extra actions are necessary. + +Assume the following history exists and the current branch is "master": + +------------ + A---B---C origin/master + / + D---E master +------------ + +Then `git update` will merge in a fast-foward way up to the new master. + +------------ + D---E---A---B---C master, origin/master +------------ + +owever, a non-fast-foward case looks very different. + +------------ + A---B---C origin/master + / + D---E---F---G master +------------ + +This divergence means 'master' cannot be fast-forwarded to 'origin/master' and +that is named a "non-fast-forward". By default `git update` will warn about +these situations, however, most likely you would want a merge, which you can +force with `git update --merge`. This will result in a new commit that combines +the tips of both branches ('C' and 'G'). See linkgit:git-merge[1] for more details. + +------------ + origin/master + | + A---B---C---H master + / / + D---E---F------G +------------ + +You can choose to do a rebase instead by specifying the '--rebase' option, +which is slightly more complicated, but results in a cleaner history. In this +mode the commits that are not present in the remote branch are replayed on top +of it, which results in newer commits. See linkgit:git-rebase[1] for more +details. + +------------ + F---G master + / + D---E---A---B---C origin/master +------------ + +You should make sure you don't have any uncommitted changes before running +`git update`. It is generally best to get any local changes in working order +before pulling or stash them away with linkgit:git-stash[1]. + +The remote branch +----------------- + +By default `git update` will try to use the 'origin' remote and a branch with +the same name as the current branch. So if you are currently in the 'topic' +branch, `git update` will try to incorporate the changes from 'origin/topic'. + +If you have configured an upstream tracking branch (e.g. git branch +--set-upstream-to), then that branch will be used instead of the default. To +find out the upstream of your current branch, you can run +`git name-rev @{upstream}`. + +OPTIONS +------- + +-q:: +--quiet:: + Be quiet. + +-v:: +--verbose:: + Be verbose. + +--[no-]recurse-submodules[=yes|on-demand|no]:: + This option controls if new commits of all populated submodules should + be fetched too (see linkgit:git-config[1] and linkgit:gitmodules[5]). + That might be necessary to get the data needed for merging submodule + commits. Notice that the result of a merge will not be checked out in + the submodule, "git submodule update" has to be called afterwards to + bring the work tree up to date with the merge result. + +-r:: +--rebase[=false|true|preserve]:: + When true, rebase the current branch on top of the remote + branch after fetching. ++ +When preserve, also rebase the current branch on top of the upstream +branch, but pass `--preserve-merges` along to `git rebase` so that +locally created merge commits will not be flattened. ++ +When false, merge the current branch into the upstream branch. ++ +See `update.mode`, `branch..updatemode` and `branch.autosetuprebase` in +linkgit:git-config[1] if you want to make `git update` always use +`--rebase`. ++ +[NOTE] +This is a potentially _dangerous_ mode of operation. +It rewrites history, which does not bode well when you +published that history already. Do *not* use this option +unless you have read linkgit:git-rebase[1] carefully. + +-m:: +--merge:: + Force a merge. ++ +See `update.mode`, `branch..updatemode` in linkgit:git-config[1] if you want +to make `git update` always use `--merge`. + +Options related to merging +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:git-pull: 1 +include::merge-options.txt[] +include::merge-strategies.txt[] + +BUGS +---- +Using --recurse-submodules can only fetch new commits in already checked +out submodules right now. When e.g. upstream added a new submodule in the +just fetched commits of the superproject the submodule itself can not be +fetched, making it impossible to check out that submodule later without +having to do a fetch again. This is expected to be fixed in a future Git +version. + +SEE ALSO +-------- +linkgit:git-fetch[1], linkgit:git-merge[1], linkgit:git-rebase[1], linkgit:git-config[1] + +GIT +--- +Part of the linkgit:git[1] suite diff --git a/Makefile b/Makefile index 2742a6977c..d765841da1 100644 --- a/Makefile +++ b/Makefile @@ -504,6 +504,7 @@ SCRIPT_SH += git-remote-testgit.sh SCRIPT_SH += git-request-pull.sh SCRIPT_SH += git-stash.sh SCRIPT_SH += git-submodule.sh +SCRIPT_SH += git-update.sh SCRIPT_SH += git-web--browse.sh SCRIPT_LIB += git-mergetool--lib diff --git a/git-update.sh b/git-update.sh new file mode 100755 index 0000000000..73b1bc4c82 --- /dev/null +++ b/git-update.sh @@ -0,0 +1,301 @@ +#!/bin/sh +# +# Copyright (C) 2014 Felipe Contreras +# Copyright (C) 2005 Junio C Hamano +# +# Fetch upstream and update the current branch. + +USAGE='[-n | --no-stat] [--[no-]commit] [--[no-]ff] [--[no-]rebase|--rebase=preserve] [-s strategy]...' +LONG_USAGE='Fetch upstram and update the current branch.' +SUBDIRECTORY_OK=Yes +OPTIONS_SPEC= +. git-sh-setup +. git-sh-i18n +set_reflog_action "update${1+ $*}" +require_work_tree_exists +cd_to_toplevel + + +warn () { + printf >&2 'warning: %s\n' "$*" +} + +die_conflict () { + git diff-index --cached --name-status -r --ignore-submodules HEAD -- + die "$(gettext "Update is not possible because you have instaged files.")" +} + +die_merge () { + die "$(gettext "You have not concluded your merge (MERGE_HEAD exists).")" +} + +test -z "$(git ls-files -u)" || die_conflict +test -f "$GIT_DIR/MERGE_HEAD" && die_merge + +bool_or_string_config () { + git config --bool "$1" 2>/dev/null || git config "$1" +} + +strategy_args= diffstat= no_commit= no_ff= ff_only= +log_arg= verbosity= progress= recurse_submodules= verify_signatures= +merge_args= edit= rebase_args= +curr_branch=$(git symbolic-ref -q HEAD) +curr_branch_short="${curr_branch#refs/heads/}" +mode=$(git config branch.${curr_branch_short}.updatemode) +if test -z "$mode" +then + mode=$(git config update.mode) +fi +case "$mode" in +merge|rebase|ff-only|'') + ;; +rebase-preserve) + mode="rebase" + rebase_args="--preserve-merges" + ;; +*) + echo "Invalid value for 'mode'" + usage + exit 1 + ;; +esac +# compatibility with pull configuration +if test -z "$mode" +then + rebase=$(bool_or_string_config branch.$curr_branch_short.rebase) + if test -z "$rebase" + then + rebase=$(bool_or_string_config pull.rebase) + fi +fi +test -z "$mode" && mode=ff-only +dry_run= +while : +do + case "$1" in + -q|--quiet) + verbosity="$verbosity -q" ;; + -v|--verbose) + verbosity="$verbosity -v" ;; + --progress) + progress=--progress ;; + --no-progress) + progress=--no-progress ;; + -n|--no-stat|--no-summary) + diffstat=--no-stat ;; + --stat|--summary) + diffstat=--stat ;; + --log|--no-log) + log_arg=$1 ;; + -e|--edit) + edit=--edit ;; + --no-edit) + edit=--no-edit ;; + --no-commit) + no_commit=--no-commit ;; + --commit) + no_commit=--commit ;; + --ff) + no_ff=--ff ;; + --no-ff) + no_ff=--no-ff ;; + --ff-only) + ff_only=--ff-only ;; + -s=*|--strategy=*|-s|--strategy) + case "$#,$1" in + *,*=*) + strategy=`expr "z$1" : 'z-[^=]*=\(.*\)'` ;; + 1,*) + usage ;; + *) + strategy="$2" + shift ;; + esac + strategy_args="${strategy_args}-s $strategy " + ;; + -X*) + case "$#,$1" in + 1,-X) + usage ;; + *,-X) + xx="-X $(git rev-parse --sq-quote "$2")" + shift ;; + *,*) + xx=$(git rev-parse --sq-quote "$1") ;; + esac + merge_args="$merge_args$xx " + ;; + -r=*|--rebase=*) + rebase="${1#*=}" + ;; + -r|--rebase) + mode=rebase + ;; + -m|--merge) + mode=merge + ;; + --recurse-submodules) + recurse_submodules=--recurse-submodules + ;; + --recurse-submodules=*) + recurse_submodules="$1" + ;; + --no-recurse-submodules) + recurse_submodules=--no-recurse-submodules + ;; + --verify-signatures) + verify_signatures=--verify-signatures + ;; + --no-verify-signatures) + verify_signatures=--no-verify-signatures + ;; + --d|--dry-run) + dry_run=--dry-run + ;; + -h|--help-all) + usage + ;; + *) + # Pass thru anything that may be meant for fetch. + break + ;; + esac + shift +done + +if test -n "$rebase" +then + case "$rebase" in + true) + mode="rebase" + ;; + false) + mode="merge" + ;; + preserve) + mode="rebase" + rebase_args=--preserve-merges + ;; + *) + echo "Invalid value for --rebase, should be true, false, or preserve" + usage + exit 1 + ;; + esac +fi + +test -z "$curr_branch" && + die "$(gettext "You are not currently on a branch.")" + +get_remote_branch () { + local ref + + ref=$(git rev-parse --symbolic-full-name $1) + + git config --get-regexp 'remote.*.fetch' | while read key value + do + remote=${key#remote.} + remote=${remote%.fetch} + right=${value#*:} + case $ref in + $right) + prefix=${right%\*} + branch=${ref#$prefix} + echo "remote=$remote branch=$branch" + break + ;; + esac + done +} + +case $# in +0) + branch=$(git config "branch.$curr_branch_short.merge") + remote=$(git config "branch.$curr_branch_short.remote") + + test -z "$branch" && branch=$curr_branch + test -z "$remote" && remote="origin" + ;; +1) + branch=$1 + remote=. + + eval $(get_remote_branch $1) + ;; +*) + usage + exit 1 + ;; +esac + +branch="${branch#refs/heads/}" + +test "$mode" = rebase && { + require_clean_work_tree "update with rebase" "Please commit or stash them." + oldremoteref=$(git merge-base --fork-point $branch $curr_branch 2>/dev/null) +} +orig_head=$(git rev-parse -q --verify HEAD) + +if test $remote = "." +then + extra=--quiet +fi + +git fetch $verbosity $progress $dry_run $recurse_submodules $remote $branch $extra || exit 1 + +test -z "$dry_run" || exit 0 + +merge_head=$(sed -e '/ not-for-merge /d' -e 's/ .*//' "$GIT_DIR"/FETCH_HEAD) + +test -z "$merge_head" && + die "$(gettext "Couldnot fetch branch '${branch#refs/heads/}'.")" + +if test "$mode" = 'ff-only' && test -z "$no_ff$ff_only" +then + # check if a non-fast-forward merge would be needed + if ! git merge-base --is-ancestor "$orig_head" "$merge_head" && + ! git merge-base --is-ancestor "$merge_head" "$orig_head" + then + die "$(gettext "The update was not fast-forward, please either merge or rebase. +If unsure, run 'git update --merge'.")" + fi + ff_only=--ff-only +fi + +if test "$mode" = rebase +then + o=$(git show-branch --merge-base $curr_branch $merge_head $oldremoteref) + test "$oldremoteref" = "$o" && unset oldremoteref +fi + +build_msg () { + if test "$curr_branch_short" = "$branch" + then + echo "Update branch '$curr_branch_short'" + else + msg="Merge" + msg="${msg} branch '$curr_branch_short'" + test "$branch" != "master" && msg="${msg} into $branch" + echo "$msg" + fi +} + +fix_msg () { + build_msg + sed -e '1 d' +} + +case "$mode" in +rebase) + eval="git-rebase $diffstat $strategy_args $merge_args $rebase_args $verbosity" + eval="$eval --onto $merge_head ${oldremoteref:-$merge_head}" + eval "exec $eval" + ;; +*) + merge_msg=$(git fmt-merge-msg $log_arg < "$GIT_DIR"/FETCH_HEAD | fix_msg) || exit + eval="git-merge $diffstat $no_commit $verify_signatures $edit $no_ff $ff_only" + eval="$eval $log_arg $strategy_args $merge_args $verbosity $progress" + eval="$eval --reverse-parents -m \"\$merge_msg\" $merge_head" + eval "exec $eval" + ;; +esac diff --git a/t/t5580-update.sh b/t/t5580-update.sh new file mode 100755 index 0000000000..3591fc854d --- /dev/null +++ b/t/t5580-update.sh @@ -0,0 +1,144 @@ +#!/bin/sh + +test_description='update' + +. ./test-lib.sh + +check () { + git rev-parse -q --verify $1 > expected && + git rev-parse -q --verify $2 > actual && + test_cmp expected actual +} + +check_msg () { + if test "$1" = "$2" + then + echo "Update branch '$1'" > expected + else + test "$2" != "master" && into=" into $2" + echo "Merge branch '$1'${into}" > expected + fi && + git log -1 --format=%s > actual && + test_cmp expected actual +} + +test_expect_success 'setup' ' + echo one > file && + git add file && + git commit -a -m one && + echo two > file && + git commit -a -m two && + git clone . remote && + git remote add origin remote +' + +test_expect_success 'basic update' ' + test_when_finished "rm -rf test" && + ( + git clone . test && + cd test && + git reset --hard @^ && + git update && + check master origin/master + ) +' + +test_expect_success 'basic update without upstream' ' + test_when_finished "rm -rf test" && + ( + git clone . test && + cd test && + git reset --hard @^ && + git branch --unset-upstream && + git update && + check master origin/master + ) +' + +test_expect_success 'basic update no-ff' ' + test_when_finished "rm -rf test" && + ( + git clone . test && + cd test && + git reset --hard @^ && + git update --no-ff && + check @^1 origin/master && + check_msg master master + ) +' + +test_expect_success 'git update non-fast-forward' ' + test_when_finished "rm -rf test" && + ( + git clone . test && + cd test && + git checkout -b other master^ && + >new && + git add new && + git commit -m new && + git checkout -b test -t other && + git reset --hard master && + test_must_fail git update && + check @ master + ) +' + +test_expect_success 'git update non-fast-forward with merge' ' + test_when_finished "rm -rf test" && + ( + git clone . test && + cd test && + git checkout -b other master^ && + >new && + git add new && + git commit -m new && + git checkout -b test -t other && + git reset --hard master && + git update --merge && + check @^2 master && + check @^1 other && + check_msg test other + ) +' + +test_expect_success 'git update non-fast-forward with rebase' ' + test_when_finished "rm -rf test" && + ( + git clone . test && + cd test && + git checkout -b other master^ && + >new && + git add new && + git commit -m new && + git checkout -b test -t other && + git reset --hard master && + git update --rebase && + check @^ other + ) +' + +test_expect_success 'git update with argument' ' + test_when_finished "rm -rf test" && + ( + git clone . test && + cd test && + git checkout -b test && + git reset --hard @^ && + git update master && + check test master + ) +' + +test_expect_success 'git update with remote argument' ' + test_when_finished "rm -rf test" && + ( + git clone . test && + cd test && + git checkout -b test && + git reset --hard @^ && + git update origin/master && + check test master + ) +' + +test_done -- 2.32.0.93.g670b81a890