Merge branch 'tg/checkout-no-overlay'
authorJunio C Hamano <gitster@pobox.com>
Thu, 7 Mar 2019 00:59:51 +0000 (09:59 +0900)
committerJunio C Hamano <gitster@pobox.com>
Thu, 7 Mar 2019 00:59:51 +0000 (09:59 +0900)
"git checkout --no-overlay" can be used to trigger a new mode of
checking out paths out of the tree-ish, that allows paths that
match the pathspec that are in the current index and working tree
and are not in the tree-ish.

* tg/checkout-no-overlay:
revert "checkout: introduce checkout.overlayMode config"
checkout: introduce checkout.overlayMode config
checkout: introduce --{,no-}overlay option
checkout: factor out mark_cache_entry_for_checkout function
checkout: clarify comment
read-cache: add invalidate parameter to remove_marked_cache_entries
entry: support CE_WT_REMOVE flag in checkout_entry
entry: factor out unlink_entry function
move worktree tests to t24*

1  2 
Documentation/git-checkout.txt
builtin/checkout.c
cache.h
entry.c
read-cache.c
t/t2403-worktree-move.sh
t/t9902-completion.sh
unpack-trees.c
index 8c3d4128c2871274967cd30029e2390e48d0dcc1,24e52b01e1897528780efed7ccb4490b897ec670..f179b43732aa2a7e211a95889847294890c90d38
@@@ -276,10 -279,13 +279,17 @@@ Note that this option uses the no overl
        Just like linkgit:git-submodule[1], this will detach the
        submodules HEAD.
  
 +--no-guess::
 +      Do not attempt to create a branch if a remote tracking branch
 +      of the same name exists.
 +
+ --[no-]overlay::
+       In the default overlay mode, `git checkout` never
+       removes files from the index or the working tree.  When
+       specifying `--no-overlay`, files that appear in the index and
+       working tree, but not in <tree-ish> are removed, to make them
+       match <tree-ish> exactly.
  <branch>::
        Branch to checkout; if it refers to a branch (i.e., a name that,
        when prepended with "refs/heads/", is a valid ref), then that
index 24b8593b938b67baf4a662f2463efdd35562873a,0c5fe948ef58901870949532b1a4bfc2b1870452..bea08ef959189cff99e34170a2999a52953a3020
@@@ -45,7 -44,7 +45,8 @@@ struct checkout_opts 
        int ignore_skipworktree;
        int ignore_other_worktrees;
        int show_progress;
 +      int count_checkout_paths;
+       int overlay_mode;
        /*
         * If new checkout options are added, skip_merge_working_tree
         * should be updated accordingly.
@@@ -168,15 -169,18 +172,20 @@@ static int check_stages(unsigned stages
  }
  
  static int checkout_stage(int stage, const struct cache_entry *ce, int pos,
-                         const struct checkout *state, int *nr_checkouts)
 -                        const struct checkout *state, int overlay_mode)
++                        const struct checkout *state, int *nr_checkouts,
++                        int overlay_mode)
  {
        while (pos < active_nr &&
               !strcmp(active_cache[pos]->name, ce->name)) {
                if (ce_stage(active_cache[pos]) == stage)
 -                      return checkout_entry(active_cache[pos], state, NULL);
 +                      return checkout_entry(active_cache[pos], state,
 +                                            NULL, nr_checkouts);
                pos++;
        }
+       if (!overlay_mode) {
+               unlink_entry(ce);
+               return 0;
+       }
        if (stage == 2)
                return error(_("path '%s' does not have our version"), ce->name);
        else
@@@ -381,36 -414,15 +421,39 @@@ static int checkout_paths(const struct 
                                continue;
                        }
                        if (opts->writeout_stage)
 -                              errs |= checkout_stage(opts->writeout_stage, ce, pos, &state, opts->overlay_mode);
 +                              errs |= checkout_stage(opts->writeout_stage,
 +                                                     ce, pos,
-                                                      &state, &nr_checkouts);
++                                                     &state,
++                                                     &nr_checkouts, opts->overlay_mode);
                        else if (opts->merge)
 -                              errs |= checkout_merged(pos, &state);
 +                              errs |= checkout_merged(pos, &state,
 +                                                      &nr_unmerged);
                        pos = skip_same_name(ce, pos) - 1;
                }
        }
 -      errs |= finish_delayed_checkout(&state);
+       remove_marked_cache_entries(&the_index, 1);
+       remove_scheduled_dirs();
 +      errs |= finish_delayed_checkout(&state, &nr_checkouts);
 +
 +      if (opts->count_checkout_paths) {
 +              if (nr_unmerged)
 +                      fprintf_ln(stderr, Q_("Recreated %d merge conflict",
 +                                            "Recreated %d merge conflicts",
 +                                            nr_unmerged),
 +                                 nr_unmerged);
 +              if (opts->source_tree)
 +                      fprintf_ln(stderr, Q_("Updated %d path from %s",
 +                                            "Updated %d paths from %s",
 +                                            nr_checkouts),
 +                                 nr_checkouts,
 +                                 find_unique_abbrev(&opts->source_tree->object.oid,
 +                                                    DEFAULT_ABBREV));
 +              else if (!nr_unmerged || nr_checkouts)
 +                      fprintf_ln(stderr, Q_("Updated %d path from the index",
 +                                            "Updated %d paths from the index",
 +                                            nr_checkouts),
 +                                 nr_checkouts);
 +      }
  
        if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK))
                die(_("unable to write new index file"));
diff --cc cache.h
index 27fe635f622c49e2a21ffcb91d3113d3b3fbad8c,1deee48f5bff74226eb4f1ed21bc607a98d85b9e..473fa1eff10acf6f6e8357c6dabff6289883cdd0
+++ b/cache.h
@@@ -1566,9 -1539,14 +1566,14 @@@ struct checkout 
  #define CHECKOUT_INIT { NULL, "" }
  
  #define TEMPORARY_FILENAME_LENGTH 25
 -extern int checkout_entry(struct cache_entry *ce, const struct checkout *state, char *topath);
 +extern int checkout_entry(struct cache_entry *ce, const struct checkout *state, char *topath, int *nr_checkouts);
  extern void enable_delayed_checkout(struct checkout *state);
 -extern int finish_delayed_checkout(struct checkout *state);
 +extern int finish_delayed_checkout(struct checkout *state, int *nr_checkouts);
+ /*
+  * Unlink the last component and schedule the leading directories for
+  * removal, such that empty directories get removed.
+  */
+ extern void unlink_entry(const struct cache_entry *ce);
  
  struct cache_def {
        struct strbuf path;
diff --cc entry.c
index 6fd72b30c8768677f044923ef5e715949d32fdae,3d3701e7ae148b623e353ad2874e9dce28cca32b..0e4f2f29101f913d0db2c401851f279eac496f02
+++ b/entry.c
@@@ -506,7 -517,20 +517,22 @@@ int checkout_entry(struct cache_entry *
                return 0;
  
        create_directories(path.buf, path.len, state);
 +      if (nr_checkouts)
 +              (*nr_checkouts)++;
        return write_entry(ce, path.buf, state, 0);
  }
+ void unlink_entry(const struct cache_entry *ce)
+ {
+       const struct submodule *sub = submodule_from_ce(ce);
+       if (sub) {
+               /* state.force is set at the caller. */
+               submodule_move_head(ce->name, "HEAD", NULL,
+                                   SUBMODULE_MOVE_HEAD_FORCE);
+       }
+       if (!check_leading_path(ce->name, ce_namelen(ce)))
+               return;
+       if (remove_or_warn(ce->ce_mode, ce->name))
+               return;
+       schedule_dir_for_removal(ce->name, ce_namelen(ce));
+ }
diff --cc read-cache.c
Simple merge
index 0000000000000000000000000000000000000000,33c033773367a135d4cb7eb23f9e9d3131197174..939d18d7286c1be1e58a698e9164fda8e24c654a
mode 000000,100755..100755
--- /dev/null
@@@ -1,0 -1,188 +1,225 @@@
+ #!/bin/sh
+ test_description='test git worktree move, remove, lock and unlock'
+ . ./test-lib.sh
+ test_expect_success 'setup' '
+       test_commit init &&
+       git worktree add source &&
+       git worktree list --porcelain >out &&
+       grep "^worktree" out >actual &&
+       cat <<-EOF >expected &&
+       worktree $(pwd)
+       worktree $(pwd)/source
+       EOF
+       test_cmp expected actual
+ '
+ test_expect_success 'lock main worktree' '
+       test_must_fail git worktree lock .
+ '
+ test_expect_success 'lock linked worktree' '
+       git worktree lock --reason hahaha source &&
+       echo hahaha >expected &&
+       test_cmp expected .git/worktrees/source/locked
+ '
+ test_expect_success 'lock linked worktree from another worktree' '
+       rm .git/worktrees/source/locked &&
+       git worktree add elsewhere &&
+       git -C elsewhere worktree lock --reason hahaha ../source &&
+       echo hahaha >expected &&
+       test_cmp expected .git/worktrees/source/locked
+ '
+ test_expect_success 'lock worktree twice' '
+       test_must_fail git worktree lock source &&
+       echo hahaha >expected &&
+       test_cmp expected .git/worktrees/source/locked
+ '
+ test_expect_success 'lock worktree twice (from the locked worktree)' '
+       test_must_fail git -C source worktree lock . &&
+       echo hahaha >expected &&
+       test_cmp expected .git/worktrees/source/locked
+ '
+ test_expect_success 'unlock main worktree' '
+       test_must_fail git worktree unlock .
+ '
+ test_expect_success 'unlock linked worktree' '
+       git worktree unlock source &&
+       test_path_is_missing .git/worktrees/source/locked
+ '
+ test_expect_success 'unlock worktree twice' '
+       test_must_fail git worktree unlock source &&
+       test_path_is_missing .git/worktrees/source/locked
+ '
+ test_expect_success 'move non-worktree' '
+       mkdir abc &&
+       test_must_fail git worktree move abc def
+ '
+ test_expect_success 'move locked worktree' '
+       git worktree lock source &&
+       test_when_finished "git worktree unlock source" &&
+       test_must_fail git worktree move source destination
+ '
+ test_expect_success 'move worktree' '
+       git worktree move source destination &&
+       test_path_is_missing source &&
+       git worktree list --porcelain >out &&
+       grep "^worktree.*/destination$" out &&
+       ! grep "^worktree.*/source$" out &&
+       git -C destination log --format=%s >actual2 &&
+       echo init >expected2 &&
+       test_cmp expected2 actual2
+ '
+ test_expect_success 'move main worktree' '
+       test_must_fail git worktree move . def
+ '
+ test_expect_success 'move worktree to another dir' '
+       mkdir some-dir &&
+       git worktree move destination some-dir &&
+       test_when_finished "git worktree move some-dir/destination destination" &&
+       test_path_is_missing destination &&
+       git worktree list --porcelain >out &&
+       grep "^worktree.*/some-dir/destination$" out &&
+       git -C some-dir/destination log --format=%s >actual2 &&
+       echo init >expected2 &&
+       test_cmp expected2 actual2
+ '
+ test_expect_success 'move locked worktree (force)' '
+       test_when_finished "
+               git worktree unlock flump || :
+               git worktree remove flump || :
+               git worktree unlock ploof || :
+               git worktree remove ploof || :
+               " &&
+       git worktree add --detach flump &&
+       git worktree lock flump &&
+       test_must_fail git worktree move flump ploof" &&
+       test_must_fail git worktree move --force flump ploof" &&
+       git worktree move --force --force flump ploof
+ '
++test_expect_success 'move a repo with uninitialized submodule' '
++      git init withsub &&
++      (
++              cd withsub &&
++              test_commit initial &&
++              git submodule add "$PWD"/.git sub &&
++              git commit -m withsub &&
++              git worktree add second HEAD &&
++              git worktree move second third
++      )
++'
++
++test_expect_success 'not move a repo with initialized submodule' '
++      (
++              cd withsub &&
++              git -C third submodule update &&
++              test_must_fail git worktree move third forth
++      )
++'
++
+ test_expect_success 'remove main worktree' '
+       test_must_fail git worktree remove .
+ '
+ test_expect_success 'remove locked worktree' '
+       git worktree lock destination &&
+       test_when_finished "git worktree unlock destination" &&
+       test_must_fail git worktree remove destination
+ '
+ test_expect_success 'remove worktree with dirty tracked file' '
+       echo dirty >>destination/init.t &&
+       test_when_finished "git -C destination checkout init.t" &&
+       test_must_fail git worktree remove destination
+ '
+ test_expect_success 'remove worktree with untracked file' '
+       : >destination/untracked &&
+       test_must_fail git worktree remove destination
+ '
+ test_expect_success 'force remove worktree with untracked file' '
+       git worktree remove --force destination &&
+       test_path_is_missing destination
+ '
+ test_expect_success 'remove missing worktree' '
+       git worktree add to-be-gone &&
+       test -d .git/worktrees/to-be-gone &&
+       mv to-be-gone gone &&
+       git worktree remove to-be-gone &&
+       test_path_is_missing .git/worktrees/to-be-gone
+ '
+ test_expect_success 'NOT remove missing-but-locked worktree' '
+       git worktree add gone-but-locked &&
+       git worktree lock gone-but-locked &&
+       test -d .git/worktrees/gone-but-locked &&
+       mv gone-but-locked really-gone-now &&
+       test_must_fail git worktree remove gone-but-locked &&
+       test_path_is_dir .git/worktrees/gone-but-locked
+ '
+ test_expect_success 'proper error when worktree not found' '
+       for i in noodle noodle/bork
+       do
+               test_must_fail git worktree lock $i 2>err &&
+               test_i18ngrep "not a working tree" err || return 1
+       done
+ '
+ test_expect_success 'remove locked worktree (force)' '
+       git worktree add --detach gumby &&
+       test_when_finished "git worktree remove gumby || :" &&
+       git worktree lock gumby &&
+       test_when_finished "git worktree unlock gumby || :" &&
+       test_must_fail git worktree remove gumby &&
+       test_must_fail git worktree remove --force gumby &&
+       git worktree remove --force --force gumby
+ '
+ test_expect_success 'remove cleans up .git/worktrees when empty' '
+       git init moog &&
+       (
+               cd moog &&
+               test_commit bim &&
+               git worktree add --detach goom &&
+               test_path_exists .git/worktrees &&
+               git worktree remove goom &&
+               test_path_is_missing .git/worktrees
+       )
+ '
++test_expect_success 'remove a repo with uninitialized submodule' '
++      (
++              cd withsub &&
++              git worktree add to-remove HEAD &&
++              git worktree remove to-remove
++      )
++'
++
++test_expect_success 'not remove a repo with initialized submodule' '
++      (
++              cd withsub &&
++              git worktree add to-remove HEAD &&
++              git -C to-remove submodule update &&
++              test_must_fail git worktree remove to-remove
++      )
++'
++
+ test_done
index 3a2c6326d83b760194c600e2ccde619438200508,5758fffa0dd19cf1a8095060d3dacb5b80b34216..f5e21bf970f4dc44c7fa30d54134a15cf343ffc1
@@@ -1434,9 -1434,9 +1434,10 @@@ test_expect_success 'double dash "git c
        --ignore-other-worktrees Z
        --recurse-submodules Z
        --progress Z
 -      --no-quiet Z
 +      --guess Z
 +      --no-guess Z
        --no-... Z
+       --overlay Z
        EOF
  '
  
diff --cc unpack-trees.c
Simple merge