Merge branch 'jc/pull-rebase-ff'
authorJunio C Hamano <gitster@pobox.com>
Mon, 19 Dec 2016 22:45:38 +0000 (14:45 -0800)
committerJunio C Hamano <gitster@pobox.com>
Mon, 19 Dec 2016 22:45:38 +0000 (14:45 -0800)
"git pull --rebase", when there is no new commits on our side since
we forked from the upstream, should be able to fast-forward without
invoking "git rebase", but it didn't.

* jc/pull-rebase-ff:
pull: fast-forward "pull --rebase=true"

1  2 
builtin/pull.c
t/t5520-pull.sh
diff --combined builtin/pull.c
index d6e46ee6d0054c152d721e92e354795c6f6002f8,2a41d415b2df1e789f8cf466306e92ec242923c1..3ecb881b0bcacbf1a453bf9a6cb95ae00cdecb1d
  #include "revision.h"
  #include "tempfile.h"
  #include "lockfile.h"
 +#include "wt-status.h"
  
  enum rebase_type {
        REBASE_INVALID = -1,
        REBASE_FALSE = 0,
        REBASE_TRUE,
 -      REBASE_PRESERVE
 +      REBASE_PRESERVE,
 +      REBASE_INTERACTIVE
  };
  
  /**
@@@ -44,8 -42,6 +44,8 @@@ static enum rebase_type parse_config_re
                return REBASE_TRUE;
        else if (!strcmp(value, "preserve"))
                return REBASE_PRESERVE;
 +      else if (!strcmp(value, "interactive"))
 +              return REBASE_INTERACTIVE;
  
        if (fatal)
                die(_("Invalid value for %s: %s"), key, value);
@@@ -87,12 -83,9 +87,12 @@@ static char *opt_commit
  static char *opt_edit;
  static char *opt_ff;
  static char *opt_verify_signatures;
 +static int opt_autostash = -1;
 +static int config_autostash;
  static struct argv_array opt_strategies = ARGV_ARRAY_INIT;
  static struct argv_array opt_strategy_opts = ARGV_ARRAY_INIT;
  static char *opt_gpg_sign;
 +static int opt_allow_unrelated_histories;
  
  /* Options passed to git-fetch */
  static char *opt_all;
@@@ -102,7 -95,6 +102,7 @@@ static int opt_force
  static char *opt_tags;
  static char *opt_prune;
  static char *opt_recurse_submodules;
 +static char *max_children;
  static int opt_dry_run;
  static char *opt_keep;
  static char *opt_depth;
@@@ -120,7 -112,7 +120,7 @@@ static struct option pull_options[] = 
        /* Options passed to git-merge or git-rebase */
        OPT_GROUP(N_("Options related to merging")),
        { OPTION_CALLBACK, 'r', "rebase", &opt_rebase,
 -        "false|true|preserve",
 +        "false|true|preserve|interactive",
          N_("incorporate changes by rebasing rather than merging"),
          PARSE_OPT_OPTARG, parse_opt_rebase },
        OPT_PASSTHRU('n', NULL, &opt_diffstat, NULL,
        OPT_PASSTHRU(0, "verify-signatures", &opt_verify_signatures, NULL,
                N_("verify that the named commit has a valid GPG signature"),
                PARSE_OPT_NOARG),
 +      OPT_BOOL(0, "autostash", &opt_autostash,
 +              N_("automatically stash/stash pop before and after rebase")),
        OPT_PASSTHRU_ARGV('s', "strategy", &opt_strategies, N_("strategy"),
                N_("merge strategy to use"),
                0),
        OPT_PASSTHRU('S', "gpg-sign", &opt_gpg_sign, N_("key-id"),
                N_("GPG sign commit"),
                PARSE_OPT_OPTARG),
 +      OPT_SET_INT(0, "allow-unrelated-histories",
 +                  &opt_allow_unrelated_histories,
 +                  N_("allow merging unrelated histories"), 1),
  
        /* Options passed to git-fetch */
        OPT_GROUP(N_("Options related to fetching")),
                N_("on-demand"),
                N_("control recursive fetching of submodules"),
                PARSE_OPT_OPTARG),
 +      OPT_PASSTHRU('j', "jobs", &max_children, N_("n"),
 +              N_("number of submodules pulled in parallel"),
 +              PARSE_OPT_OPTARG),
        OPT_BOOL(0, "dry-run", &opt_dry_run,
                N_("dry run")),
        OPT_PASSTHRU('k', "keep", &opt_keep, NULL,
@@@ -315,15 -299,70 +315,15 @@@ static enum rebase_type config_get_reba
  }
  
  /**
 - * Returns 1 if there are unstaged changes, 0 otherwise.
 - */
 -static int has_unstaged_changes(const char *prefix)
 -{
 -      struct rev_info rev_info;
 -      int result;
 -
 -      init_revisions(&rev_info, prefix);
 -      DIFF_OPT_SET(&rev_info.diffopt, IGNORE_SUBMODULES);
 -      DIFF_OPT_SET(&rev_info.diffopt, QUICK);
 -      diff_setup_done(&rev_info.diffopt);
 -      result = run_diff_files(&rev_info, 0);
 -      return diff_result_code(&rev_info.diffopt, result);
 -}
 -
 -/**
 - * Returns 1 if there are uncommitted changes, 0 otherwise.
 + * Read config variables.
   */
 -static int has_uncommitted_changes(const char *prefix)
 +static int git_pull_config(const char *var, const char *value, void *cb)
  {
 -      struct rev_info rev_info;
 -      int result;
 -
 -      if (is_cache_unborn())
 +      if (!strcmp(var, "rebase.autostash")) {
 +              config_autostash = git_config_bool(var, value);
                return 0;
 -
 -      init_revisions(&rev_info, prefix);
 -      DIFF_OPT_SET(&rev_info.diffopt, IGNORE_SUBMODULES);
 -      DIFF_OPT_SET(&rev_info.diffopt, QUICK);
 -      add_head_to_pending(&rev_info);
 -      diff_setup_done(&rev_info.diffopt);
 -      result = run_diff_index(&rev_info, 1);
 -      return diff_result_code(&rev_info.diffopt, result);
 -}
 -
 -/**
 - * If the work tree has unstaged or uncommitted changes, dies with the
 - * appropriate message.
 - */
 -static void die_on_unclean_work_tree(const char *prefix)
 -{
 -      struct lock_file *lock_file = xcalloc(1, sizeof(*lock_file));
 -      int do_die = 0;
 -
 -      hold_locked_index(lock_file, 0);
 -      refresh_cache(REFRESH_QUIET);
 -      update_index_if_able(&the_index, lock_file);
 -      rollback_lock_file(lock_file);
 -
 -      if (has_unstaged_changes(prefix)) {
 -              error(_("Cannot pull with rebase: You have unstaged changes."));
 -              do_die = 1;
 -      }
 -
 -      if (has_uncommitted_changes(prefix)) {
 -              if (do_die)
 -                      error(_("Additionally, your index contains uncommitted changes."));
 -              else
 -                      error(_("Cannot pull with rebase: Your index contains uncommitted changes."));
 -              do_die = 1;
        }
 -
 -      if (do_die)
 -              exit(1);
 +      return git_default_config(var, value, cb);
  }
  
  /**
@@@ -339,7 -378,7 +339,7 @@@ static void get_merge_heads(struct sha1
  
        if (!(fp = fopen(filename, "r")))
                die_errno(_("could not open '%s' for reading"), filename);
 -      while (strbuf_getline(&sb, fp, '\n') != EOF) {
 +      while (strbuf_getline_lf(&sb, fp) != EOF) {
                if (get_sha1_hex(sb.buf, sha1))
                        continue;  /* invalid line: does not start with SHA1 */
                if (starts_with(sb.buf + GIT_SHA1_HEXSZ, "\tnot-for-merge\t"))
@@@ -412,13 -451,13 +412,13 @@@ static void NORETURN die_no_merge_candi
                        fprintf_ln(stderr, _("Please specify which branch you want to merge with."));
                fprintf_ln(stderr, _("See git-pull(1) for details."));
                fprintf(stderr, "\n");
 -              fprintf_ln(stderr, "    git pull <remote> <branch>");
 +              fprintf_ln(stderr, "    git pull %s %s", _("<remote>"), _("<branch>"));
                fprintf(stderr, "\n");
        } else if (!curr_branch->merge_nr) {
                const char *remote_name = NULL;
  
                if (for_each_remote(get_only_remote, &remote_name) || !remote_name)
 -                      remote_name = "<remote>";
 +                      remote_name = _("<remote>");
  
                fprintf_ln(stderr, _("There is no tracking information for the current branch."));
                if (opt_rebase)
                        fprintf_ln(stderr, _("Please specify which branch you want to merge with."));
                fprintf_ln(stderr, _("See git-pull(1) for details."));
                fprintf(stderr, "\n");
 -              fprintf_ln(stderr, "    git pull <remote> <branch>");
 +              fprintf_ln(stderr, "    git pull %s %s", _("<remote>"), _("<branch>"));
                fprintf(stderr, "\n");
 -              fprintf_ln(stderr, _("If you wish to set tracking information for this branch you can do so with:\n"
 -                              "\n"
 -                              "    git branch --set-upstream-to=%s/<branch> %s\n"),
 -                              remote_name, curr_branch->name);
 +              fprintf_ln(stderr, _("If you wish to set tracking information for this branch you can do so with:"));
 +              fprintf(stderr, "\n");
 +              fprintf_ln(stderr, "    git branch --set-upstream-to=%s/%s %s\n",
 +                              remote_name, _("<branch>"), curr_branch->name);
        } else
                fprintf_ln(stderr, _("Your configuration specifies to merge with the ref '%s'\n"
                        "from the remote, but no such ref was fetched."),
@@@ -486,8 -525,6 +486,8 @@@ static int run_fetch(const char *repo, 
                argv_array_push(&args, opt_prune);
        if (opt_recurse_submodules)
                argv_array_push(&args, opt_recurse_submodules);
 +      if (max_children)
 +              argv_array_push(&args, max_children);
        if (opt_dry_run)
                argv_array_push(&args, "--dry-run");
        if (opt_keep)
@@@ -566,8 -603,6 +566,8 @@@ static int run_merge(void
        argv_array_pushv(&args, opt_strategy_opts.argv);
        if (opt_gpg_sign)
                argv_array_push(&args, opt_gpg_sign);
 +      if (opt_allow_unrelated_histories > 0)
 +              argv_array_push(&args, "--allow-unrelated-histories");
  
        argv_array_push(&args, "FETCH_HEAD");
        ret = run_command_v_opt(args.argv, RUN_GIT_CMD);
@@@ -708,7 -743,7 +708,7 @@@ static int get_octopus_merge_base(unsig
        if (!result)
                return 1;
  
 -      hashcpy(merge_base, result->item->object.sha1);
 +      hashcpy(merge_base, result->item->object.oid.hash);
        return 0;
  }
  
@@@ -737,21 -772,12 +737,21 @@@ static int run_rebase(const unsigned ch
        /* Options passed to git-rebase */
        if (opt_rebase == REBASE_PRESERVE)
                argv_array_push(&args, "--preserve-merges");
 +      else if (opt_rebase == REBASE_INTERACTIVE)
 +              argv_array_push(&args, "--interactive");
        if (opt_diffstat)
                argv_array_push(&args, opt_diffstat);
        argv_array_pushv(&args, opt_strategies.argv);
        argv_array_pushv(&args, opt_strategy_opts.argv);
        if (opt_gpg_sign)
                argv_array_push(&args, opt_gpg_sign);
 +      if (opt_autostash == 0)
 +              argv_array_push(&args, "--no-autostash");
 +      else if (opt_autostash == 1)
 +              argv_array_push(&args, "--autostash");
 +      if (opt_verify_signatures &&
 +          !strcmp(opt_verify_signatures, "--verify-signatures"))
 +              warning(_("ignoring --verify-signatures for rebase"));
  
        argv_array_push(&args, "--onto");
        argv_array_push(&args, sha1_to_hex(merge_head));
@@@ -786,10 -812,10 +786,10 @@@ int cmd_pull(int argc, const char **arg
        if (opt_rebase < 0)
                opt_rebase = config_get_rebase();
  
 -      git_config(git_default_config, NULL);
 +      git_config(git_pull_config, NULL);
  
        if (read_cache_unmerged())
 -              die_resolve_conflict("Pull");
 +              die_resolve_conflict("pull");
  
        if (file_exists(git_path("MERGE_HEAD")))
                die_conclude_merge();
        if (get_sha1("HEAD", orig_head))
                hashclr(orig_head);
  
 +      if (!opt_rebase && opt_autostash != -1)
 +              die(_("--[no-]autostash option is only valid with --rebase."));
 +
        if (opt_rebase) {
 -              int autostash = 0;
 +              int autostash = config_autostash;
 +              if (opt_autostash != -1)
 +                      autostash = opt_autostash;
  
                if (is_null_sha1(orig_head) && !is_cache_unborn())
                        die(_("Updating an unborn branch with changes added to the index."));
  
 -              git_config_get_bool("rebase.autostash", &autostash);
                if (!autostash)
 -                      die_on_unclean_work_tree(prefix);
 +                      require_clean_work_tree(N_("pull with rebase"),
 +                              _("please commit or stash them."), 1, 0);
  
                if (get_rebase_fork_point(rebase_fork_point, repo, *refspecs))
                        hashclr(rebase_fork_point);
                if (merge_heads.nr > 1)
                        die(_("Cannot merge multiple branches into empty head."));
                return pull_into_void(*merge_heads.sha1, curr_head);
-       } else if (opt_rebase) {
-               if (merge_heads.nr > 1)
-                       die(_("Cannot rebase onto multiple branches."));
+       }
+       if (opt_rebase && merge_heads.nr > 1)
+               die(_("Cannot rebase onto multiple branches."));
+       if (opt_rebase) {
+               struct commit_list *list = NULL;
+               struct commit *merge_head, *head;
+               head = lookup_commit_reference(orig_head);
+               commit_list_insert(head, &list);
+               merge_head = lookup_commit_reference(merge_heads.sha1[0]);
+               if (is_descendant_of(merge_head, list)) {
+                       /* we can fast-forward this without invoking rebase */
+                       opt_ff = "--ff-only";
+                       return run_merge();
+               }
                return run_rebase(curr_head, *merge_heads.sha1, rebase_fork_point);
-       } else
+       } else {
                return run_merge();
+       }
  }
diff --combined t/t5520-pull.sh
index 551844584fc7b8ff71667dd61fcba62889ded69f,7887b6d97b762c3792b05257528ae38e6289bb99..17f4d0fe4e7244cb58eaedcf41bcaa9c7b157bb4
@@@ -9,24 -9,6 +9,24 @@@ modify () 
        mv "$2.x" "$2"
  }
  
 +test_pull_autostash () {
 +      git reset --hard before-rebase &&
 +      echo dirty >new_file &&
 +      git add new_file &&
 +      git pull "$@" . copy &&
 +      test_cmp_rev HEAD^ copy &&
 +      test "$(cat new_file)" = dirty &&
 +      test "$(cat file)" = "modified again"
 +}
 +
 +test_pull_autostash_fail () {
 +      git reset --hard before-rebase &&
 +      echo dirty >new_file &&
 +      git add new_file &&
 +      test_must_fail git pull "$@" . copy 2>err &&
 +      test_i18ngrep "uncommitted changes." err
 +}
 +
  test_expect_success setup '
        echo file >file &&
        git add file &&
@@@ -211,7 -193,7 +211,7 @@@ test_expect_success 'fail if the index 
        test -n "$(git ls-files -u)" &&
        cp file expected &&
        test_must_fail git pull . second 2>err &&
 -      test_i18ngrep "Pull is not possible because you have unmerged files" err &&
 +      test_i18ngrep "Pulling is not possible because you have unmerged files." err &&
        test_cmp expected file &&
        git add file &&
        test -z "$(git ls-files -u)" &&
@@@ -255,38 -237,23 +255,55 @@@ test_expect_success '--rebase' 
        test new = "$(git show HEAD:file2)"
  '
  
+ test_expect_success '--rebase fast forward' '
+       git reset --hard before-rebase &&
+       git checkout -b ff &&
+       echo another modification >file &&
+       git commit -m third file &&
+       git checkout to-rebase &&
+       git pull --rebase . ff &&
+       test "$(git rev-parse HEAD)" = "$(git rev-parse ff)" &&
+       # The above only validates the result.  Did we actually bypass rebase?
+       git reflog -1 >reflog.actual &&
+       sed "s/^[0-9a-f][0-9a-f]*/OBJID/" reflog.actual >reflog.fuzzy &&
+       echo "OBJID HEAD@{0}: pull --rebase . ff: Fast-forward" >reflog.expected &&
+       test_cmp reflog.expected reflog.fuzzy
+ '
 +test_expect_success '--rebase with conflicts shows advice' '
 +      test_when_finished "git rebase --abort; git checkout -f to-rebase" &&
 +      git checkout -b seq &&
 +      test_seq 5 >seq.txt &&
 +      git add seq.txt &&
 +      test_tick &&
 +      git commit -m "Add seq.txt" &&
 +      echo 6 >>seq.txt &&
 +      test_tick &&
 +      git commit -m "Append to seq.txt" seq.txt &&
 +      git checkout -b with-conflicts HEAD^ &&
 +      echo conflicting >>seq.txt &&
 +      test_tick &&
 +      git commit -m "Create conflict" seq.txt &&
 +      test_must_fail git pull --rebase . seq 2>err >out &&
 +      test_i18ngrep "When you have resolved this problem" out
 +'
 +
 +test_expect_success 'failed --rebase shows advice' '
 +      test_when_finished "git rebase --abort; git checkout -f to-rebase" &&
 +      git checkout -b diverging &&
 +      test_commit attributes .gitattributes "* text=auto" attrs &&
 +      sha1="$(printf "1\\r\\n" | git hash-object -w --stdin)" &&
 +      git update-index --cacheinfo 0644 $sha1 file &&
 +      git commit -m v1-with-cr &&
 +      # force checkout because `git reset --hard` will not leave clean `file`
 +      git checkout -f -b fails-to-rebase HEAD^ &&
 +      test_commit v2-without-cr file "2" file2-lf &&
 +      test_must_fail git pull --rebase . diverging 2>err >out &&
 +      test_i18ngrep "When you have resolved this problem" out
 +'
 +
  test_expect_success '--rebase fails with multiple branches' '
        git reset --hard before-rebase &&
        test_must_fail git pull --rebase . copy master 2>err &&
  
  test_expect_success 'pull --rebase succeeds with dirty working directory and rebase.autostash set' '
        test_config rebase.autostash true &&
 -      git reset --hard before-rebase &&
 -      echo dirty >new_file &&
 -      git add new_file &&
 -      git pull --rebase . copy &&
 -      test_cmp_rev HEAD^ copy &&
 -      test "$(cat new_file)" = dirty &&
 -      test "$(cat file)" = "modified again"
 +      test_pull_autostash --rebase
 +'
 +
 +test_expect_success 'pull --rebase --autostash & rebase.autostash=true' '
 +      test_config rebase.autostash true &&
 +      test_pull_autostash --rebase --autostash
 +'
 +
 +test_expect_success 'pull --rebase --autostash & rebase.autostash=false' '
 +      test_config rebase.autostash false &&
 +      test_pull_autostash --rebase --autostash
  '
  
 +test_expect_success 'pull --rebase --autostash & rebase.autostash unset' '
 +      test_unconfig rebase.autostash &&
 +      test_pull_autostash --rebase --autostash
 +'
 +
 +test_expect_success 'pull --rebase --no-autostash & rebase.autostash=true' '
 +      test_config rebase.autostash true &&
 +      test_pull_autostash_fail --rebase --no-autostash
 +'
 +
 +test_expect_success 'pull --rebase --no-autostash & rebase.autostash=false' '
 +      test_config rebase.autostash false &&
 +      test_pull_autostash_fail --rebase --no-autostash
 +'
 +
 +test_expect_success 'pull --rebase --no-autostash & rebase.autostash unset' '
 +      test_unconfig rebase.autostash &&
 +      test_pull_autostash_fail --rebase --no-autostash
 +'
 +
 +for i in --autostash --no-autostash
 +do
 +      test_expect_success "pull $i (without --rebase) is illegal" '
 +              test_must_fail git pull $i . copy 2>err &&
 +              test_i18ngrep "only valid with --rebase" err
 +      '
 +done
 +
  test_expect_success 'pull.rebase' '
        git reset --hard before-rebase &&
        test_config pull.rebase true &&
        test new = "$(git show HEAD:file2)"
  '
  
 +test_expect_success 'pull --autostash & pull.rebase=true' '
 +      test_config pull.rebase true &&
 +      test_pull_autostash --autostash
 +'
 +
 +test_expect_success 'pull --no-autostash & pull.rebase=true' '
 +      test_config pull.rebase true &&
 +      test_pull_autostash_fail --no-autostash
 +'
 +
  test_expect_success 'branch.to-rebase.rebase' '
        git reset --hard before-rebase &&
        test_config branch.to-rebase.rebase true &&
@@@ -373,22 -298,6 +390,22 @@@ test_expect_success 'branch.to-rebase.r
        test new = "$(git show HEAD:file2)"
  '
  
 +test_expect_success "pull --rebase warns on --verify-signatures" '
 +      git reset --hard before-rebase &&
 +      git pull --rebase --verify-signatures . copy 2>err &&
 +      test "$(git rev-parse HEAD^)" = "$(git rev-parse copy)" &&
 +      test new = "$(git show HEAD:file2)" &&
 +      test_i18ngrep "ignoring --verify-signatures for rebase" err
 +'
 +
 +test_expect_success "pull --rebase does not warn on --no-verify-signatures" '
 +      git reset --hard before-rebase &&
 +      git pull --rebase --no-verify-signatures . copy 2>err &&
 +      test "$(git rev-parse HEAD^)" = "$(git rev-parse copy)" &&
 +      test new = "$(git show HEAD:file2)" &&
 +      test_i18ngrep ! "verify-signatures" err
 +'
 +
  # add a feature branch, keep-merge, that is merged into master, so the
  # test can try preserving the merge commit (or not) with various
  # --rebase flags/pull.rebase settings.
@@@ -434,16 -343,6 +451,16 @@@ test_expect_success 'pull.rebase=preser
        test "$(git rev-parse HEAD^2)" = "$(git rev-parse keep-merge)"
  '
  
 +test_expect_success 'pull.rebase=interactive' '
 +      write_script "$TRASH_DIRECTORY/fake-editor" <<-\EOF &&
 +      echo I was here >fake.out &&
 +      false
 +      EOF
 +      test_set_editor "$TRASH_DIRECTORY/fake-editor" &&
 +      test_must_fail git pull --rebase=interactive . copy &&
 +      test "I was here" = "$(cat fake.out)"
 +'
 +
  test_expect_success 'pull.rebase=invalid fails' '
        git reset --hard before-preserve-rebase &&
        test_config pull.rebase invalid &&