restore: take tree-ish from --source option instead
[gitweb.git] / builtin / checkout.c
index 4989ca73a31ffe3640ef6b3ec03376fcf4c33fd2..5aba345712d765cf8fbdce0f60fe02ab9a0b291f 100644 (file)
 #include "tree.h"
 #include "tree-walk.h"
 #include "unpack-trees.h"
+#include "wt-status.h"
 #include "xdiff-interface.h"
 
-static int checkout_optimize_new_branch;
-
 static const char * const checkout_usage[] = {
        N_("git checkout [<options>] <branch>"),
        N_("git checkout [<options>] [<branch>] -- <file>..."),
@@ -39,12 +38,18 @@ static const char * const switch_branch_usage[] = {
        NULL,
 };
 
+static const char * const restore_usage[] = {
+       N_("git restore [<options>] [--source=<branch>] <file>..."),
+       NULL,
+};
+
 struct checkout_opts {
        int patch_mode;
        int quiet;
        int merge;
        int force;
        int force_detach;
+       int implicit_detach;
        int writeout_stage;
        int overwrite_ignore;
        int ignore_skipworktree;
@@ -52,12 +57,14 @@ struct checkout_opts {
        int show_progress;
        int count_checkout_paths;
        int overlay_mode;
-       int no_dwim_new_local_branch;
-
-       /*
-        * If new checkout options are added, skip_merge_working_tree
-        * should be updated accordingly.
-        */
+       int dwim_new_local_branch;
+       int discard_changes;
+       int accept_ref;
+       int accept_pathspec;
+       int switch_branch_doing_nothing_is_ok;
+       int only_merge_on_switching_branches;
+       int can_switch_when_in_progress;
+       int orphan_from_empty_tree;
 
        const char *new_branch;
        const char *new_branch_force;
@@ -70,6 +77,7 @@ struct checkout_opts {
        int branch_exists;
        const char *prefix;
        struct pathspec pathspec;
+       const char *from_treeish;
        struct tree *source_tree;
 };
 
@@ -561,112 +569,6 @@ static void setup_branch_path(struct branch_info *branch)
        branch->path = strbuf_detach(&buf, NULL);
 }
 
-/*
- * Skip merging the trees, updating the index and working directory if and
- * only if we are creating a new branch via "git checkout -b <new_branch>."
- */
-static int skip_merge_working_tree(const struct checkout_opts *opts,
-       const struct branch_info *old_branch_info,
-       const struct branch_info *new_branch_info)
-{
-       /*
-        * Do the merge if sparse checkout is on and the user has not opted in
-        * to the optimized behavior
-        */
-       if (core_apply_sparse_checkout && !checkout_optimize_new_branch)
-               return 0;
-
-       /*
-        * We must do the merge if we are actually moving to a new commit.
-        */
-       if (!old_branch_info->commit || !new_branch_info->commit ||
-               !oideq(&old_branch_info->commit->object.oid,
-                      &new_branch_info->commit->object.oid))
-               return 0;
-
-       /*
-        * opts->patch_mode cannot be used with switching branches so is
-        * not tested here
-        */
-
-       /*
-        * opts->quiet only impacts output so doesn't require a merge
-        */
-
-       /*
-        * Honor the explicit request for a three-way merge or to throw away
-        * local changes
-        */
-       if (opts->merge || opts->force)
-               return 0;
-
-       /*
-        * --detach is documented as "updating the index and the files in the
-        * working tree" but this optimization skips those steps so fall through
-        * to the regular code path.
-        */
-       if (opts->force_detach)
-               return 0;
-
-       /*
-        * opts->writeout_stage cannot be used with switching branches so is
-        * not tested here
-        */
-
-       /*
-        * Honor the explicit ignore requests
-        */
-       if (!opts->overwrite_ignore || opts->ignore_skipworktree ||
-               opts->ignore_other_worktrees)
-               return 0;
-
-       /*
-        * opts->show_progress only impacts output so doesn't require a merge
-        */
-
-       /*
-        * opts->overlay_mode cannot be used with switching branches so is
-        * not tested here
-        */
-
-       /*
-        * If we aren't creating a new branch any changes or updates will
-        * happen in the existing branch.  Since that could only be updating
-        * the index and working directory, we don't want to skip those steps
-        * or we've defeated any purpose in running the command.
-        */
-       if (!opts->new_branch)
-               return 0;
-
-       /*
-        * new_branch_force is defined to "create/reset and checkout a branch"
-        * so needs to go through the merge to do the reset
-        */
-       if (opts->new_branch_force)
-               return 0;
-
-       /*
-        * A new orphaned branch requrires the index and the working tree to be
-        * adjusted to <start_point>
-        */
-       if (opts->new_orphan_branch)
-               return 0;
-
-       /*
-        * Remaining variables are not checkout options but used to track state
-        */
-
-        /*
-         * Do the merge if this is the initial checkout. We cannot use
-         * is_cache_unborn() here because the index hasn't been loaded yet
-         * so cache_nr and timestamp.sec are always zero.
-         */
-       if (!file_exists(get_index_file()))
-               return 0;
-
-       return 1;
-}
-
 static int merge_working_tree(const struct checkout_opts *opts,
                              struct branch_info *old_branch_info,
                              struct branch_info *new_branch_info,
@@ -674,15 +576,21 @@ static int merge_working_tree(const struct checkout_opts *opts,
 {
        int ret;
        struct lock_file lock_file = LOCK_INIT;
+       struct tree *new_tree;
 
        hold_locked_index(&lock_file, LOCK_DIE_ON_ERROR);
        if (read_cache_preload(NULL) < 0)
                return error(_("index file corrupt"));
 
        resolve_undo_clear();
-       if (opts->force) {
-               ret = reset_tree(get_commit_tree(new_branch_info->commit),
-                                opts, 1, writeout_error);
+       if (opts->new_orphan_branch && opts->orphan_from_empty_tree) {
+               if (new_branch_info->commit)
+                       BUG("'switch --orphan' should never accept a commit as starting point");
+               new_tree = parse_tree_indirect(the_hash_algo->empty_tree);
+       } else
+               new_tree = get_commit_tree(new_branch_info->commit);
+       if (opts->discard_changes) {
+               ret = reset_tree(new_tree, opts, 1, writeout_error);
                if (ret)
                        return ret;
        } else {
@@ -720,7 +628,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
                                           &old_branch_info->commit->object.oid :
                                           the_hash_algo->empty_tree);
                init_tree_desc(&trees[0], tree->buffer, tree->size);
-               tree = parse_tree_indirect(&new_branch_info->commit->object.oid);
+               parse_tree(new_tree);
+               tree = new_tree;
                init_tree_desc(&trees[1], tree->buffer, tree->size);
 
                ret = unpack_trees(2, trees, &topts);
@@ -769,7 +678,7 @@ static int merge_working_tree(const struct checkout_opts *opts,
                        o.verbosity = 0;
                        work = write_tree_from_memory(&o);
 
-                       ret = reset_tree(get_commit_tree(new_branch_info->commit),
+                       ret = reset_tree(new_tree,
                                         opts, 1,
                                         writeout_error);
                        if (ret)
@@ -778,13 +687,13 @@ static int merge_working_tree(const struct checkout_opts *opts,
                        o.branch1 = new_branch_info->name;
                        o.branch2 = "local";
                        ret = merge_trees(&o,
-                                         get_commit_tree(new_branch_info->commit),
+                                         new_tree,
                                          work,
                                          get_commit_tree(old_branch_info->commit),
                                          &result);
                        if (ret < 0)
                                exit(128);
-                       ret = reset_tree(get_commit_tree(new_branch_info->commit),
+                       ret = reset_tree(new_tree,
                                         opts, 0,
                                         writeout_error);
                        strbuf_release(&o.obuf);
@@ -802,7 +711,7 @@ static int merge_working_tree(const struct checkout_opts *opts,
        if (write_locked_index(&the_index, &lock_file, COMMIT_LOCK))
                die(_("unable to write new index file"));
 
-       if (!opts->force && !opts->quiet)
+       if (!opts->discard_changes && !opts->quiet && new_branch_info->commit)
                show_local_changes(&new_branch_info->commit->object, &opts->diff_options);
 
        return 0;
@@ -1003,7 +912,10 @@ static void orphaned_commit_warning(struct commit *old_commit, struct commit *ne
        add_pending_object(&revs, object, oid_to_hex(&object->oid));
 
        for_each_ref(add_pending_uninteresting_ref, &revs);
-       add_pending_oid(&revs, "HEAD", &new_commit->object.oid, UNINTERESTING);
+       if (new_commit)
+               add_pending_oid(&revs, "HEAD",
+                               &new_commit->object.oid,
+                               UNINTERESTING);
 
        if (prepare_revision_walk(&revs))
                die(_("internal error in revision walk"));
@@ -1024,6 +936,7 @@ static int switch_branches(const struct checkout_opts *opts,
        void *path_to_free;
        struct object_id rev;
        int flag, writeout_error = 0;
+       int do_merge = 1;
 
        trace2_cmd_mode("branch");
 
@@ -1037,22 +950,26 @@ static int switch_branches(const struct checkout_opts *opts,
        if (old_branch_info.path)
                skip_prefix(old_branch_info.path, "refs/heads/", &old_branch_info.name);
 
+       if (opts->new_orphan_branch && opts->orphan_from_empty_tree) {
+               if (new_branch_info->name)
+                       BUG("'switch --orphan' should never accept a commit as starting point");
+               new_branch_info->commit = NULL;
+               new_branch_info->name = "(empty)";
+               do_merge = 1;
+       }
+
        if (!new_branch_info->name) {
                new_branch_info->name = "HEAD";
                new_branch_info->commit = old_branch_info.commit;
                if (!new_branch_info->commit)
                        die(_("You are on a branch yet to be born"));
                parse_commit_or_die(new_branch_info->commit);
+
+               if (opts->only_merge_on_switching_branches)
+                       do_merge = 0;
        }
 
-       /* optimize the "checkout -b <new_branch> path */
-       if (skip_merge_working_tree(opts, &old_branch_info, new_branch_info)) {
-               if (!checkout_optimize_new_branch && !opts->quiet) {
-                       if (read_cache_preload(NULL) < 0)
-                               return error(_("index file corrupt"));
-                       show_local_changes(&new_branch_info->commit->object, &opts->diff_options);
-               }
-       } else {
+       if (do_merge) {
                ret = merge_working_tree(opts, &old_branch_info, new_branch_info, &writeout_error);
                if (ret) {
                        free(path_to_free);
@@ -1072,11 +989,6 @@ static int switch_branches(const struct checkout_opts *opts,
 
 static int git_checkout_config(const char *var, const char *value, void *cb)
 {
-       if (!strcmp(var, "checkout.optimizenewbranch")) {
-               checkout_optimize_new_branch = git_config_bool(var, value);
-               return 0;
-       }
-
        if (!strcmp(var, "diff.ignoresubmodules")) {
                struct checkout_opts *opts = cb;
                handle_ignore_submodules_arg(&opts->diff_options, value);
@@ -1175,10 +1087,16 @@ static int parse_branchname_arg(int argc, const char **argv,
        if (!argc)
                return 0;
 
+       if (!opts->accept_pathspec) {
+               if (argc > 1)
+                       die(_("only one reference expected"));
+               has_dash_dash = 1; /* helps disambiguate */
+       }
+
        arg = argv[0];
        dash_dash_pos = -1;
        for (i = 0; i < argc; i++) {
-               if (!strcmp(argv[i], "--")) {
+               if (opts->accept_pathspec && !strcmp(argv[i], "--")) {
                        dash_dash_pos = i;
                        break;
                }
@@ -1212,11 +1130,12 @@ static int parse_branchname_arg(int argc, const char **argv,
                        recover_with_dwim = 0;
 
                /*
-                * Accept "git checkout foo" and "git checkout foo --"
-                * as candidates for dwim.
+                * Accept "git checkout foo", "git checkout foo --"
+                * and "git switch foo" as candidates for dwim.
                 */
                if (!(argc == 1 && !has_dash_dash) &&
-                   !(argc == 2 && has_dash_dash))
+                   !(argc == 2 && has_dash_dash) &&
+                   opts->accept_pathspec)
                        recover_with_dwim = 0;
 
                if (recover_with_dwim) {
@@ -1261,7 +1180,7 @@ static int parse_branchname_arg(int argc, const char **argv,
                 */
                if (argc)
                        verify_non_filename(opts->prefix, arg);
-       } else {
+       } else if (opts->accept_pathspec) {
                argcount++;
                argv++;
                argc--;
@@ -1288,6 +1207,62 @@ static int switch_unborn_to_new_branch(const struct checkout_opts *opts)
        return status;
 }
 
+static void die_expecting_a_branch(const struct branch_info *branch_info)
+{
+       struct object_id oid;
+       char *to_free;
+
+       if (dwim_ref(branch_info->name, strlen(branch_info->name), &oid, &to_free) == 1) {
+               const char *ref = to_free;
+
+               if (skip_prefix(ref, "refs/tags/", &ref))
+                       die(_("a branch is expected, got tag '%s'"), ref);
+               if (skip_prefix(ref, "refs/remotes/", &ref))
+                       die(_("a branch is expected, got remote branch '%s'"), ref);
+               die(_("a branch is expected, got '%s'"), ref);
+       }
+       if (branch_info->commit)
+               die(_("a branch is expected, got commit '%s'"), branch_info->name);
+       /*
+        * This case should never happen because we already die() on
+        * non-commit, but just in case.
+        */
+       die(_("a branch is expected, got '%s'"), branch_info->name);
+}
+
+static void die_if_some_operation_in_progress(void)
+{
+       struct wt_status_state state;
+
+       memset(&state, 0, sizeof(state));
+       wt_status_get_state(the_repository, &state, 0);
+
+       if (state.merge_in_progress)
+               die(_("cannot switch branch while merging\n"
+                     "Consider \"git merge --quit\" "
+                     "or \"git worktree add\"."));
+       if (state.am_in_progress)
+               die(_("cannot switch branch in the middle of an am session\n"
+                     "Consider \"git am --quit\" "
+                     "or \"git worktree add\"."));
+       if (state.rebase_interactive_in_progress || state.rebase_in_progress)
+               die(_("cannot switch branch while rebasing\n"
+                     "Consider \"git rebase --quit\" "
+                     "or \"git worktree add\"."));
+       if (state.cherry_pick_in_progress)
+               die(_("cannot switch branch while cherry-picking\n"
+                     "Consider \"git cherry-pick --quit\" "
+                     "or \"git worktree add\"."));
+       if (state.revert_in_progress)
+               die(_("cannot switch branch while reverting\n"
+                     "Consider \"git revert --quit\" "
+                     "or \"git worktree add\"."));
+       if (state.bisect_in_progress)
+               die(_("cannot switch branch while bisecting\n"
+                     "Consider \"git bisect reset HEAD\" "
+                     "or \"git worktree add\"."));
+}
+
 static int checkout_branch(struct checkout_opts *opts,
                           struct branch_info *new_branch_info)
 {
@@ -1309,6 +1284,9 @@ static int checkout_branch(struct checkout_opts *opts,
        if (opts->force && opts->merge)
                die(_("'%s' cannot be used with '%s'"), "-f", "-m");
 
+       if (opts->discard_changes && opts->merge)
+               die(_("'%s' cannot be used with '%s'"), "--discard-changes", "--merge");
+
        if (opts->force_detach && opts->new_branch)
                die(_("'%s' cannot be used with '%s'"),
                    "--detach", "-b/-B/--orphan");
@@ -1316,6 +1294,8 @@ static int checkout_branch(struct checkout_opts *opts,
        if (opts->new_orphan_branch) {
                if (opts->track != BRANCH_TRACK_UNSPECIFIED)
                        die(_("'%s' cannot be used with '%s'"), "--orphan", "-t");
+               if (opts->orphan_from_empty_tree && new_branch_info->name)
+                       die(_("'%s' cannot take <start-point>"), "--orphan");
        } else if (opts->force_detach) {
                if (opts->track != BRANCH_TRACK_UNSPECIFIED)
                        die(_("'%s' cannot be used with '%s'"), "--detach", "-t");
@@ -1326,6 +1306,23 @@ static int checkout_branch(struct checkout_opts *opts,
                die(_("Cannot switch branch to a non-commit '%s'"),
                    new_branch_info->name);
 
+       if (!opts->switch_branch_doing_nothing_is_ok &&
+           !new_branch_info->name &&
+           !opts->new_branch &&
+           !opts->force_detach)
+               die(_("missing branch or commit argument"));
+
+       if (!opts->implicit_detach &&
+           !opts->force_detach &&
+           !opts->new_branch &&
+           !opts->new_branch_force &&
+           new_branch_info->name &&
+           !new_branch_info->path)
+               die_expecting_a_branch(new_branch_info);
+
+       if (!opts->can_switch_when_in_progress)
+               die_if_some_operation_in_progress();
+
        if (new_branch_info->path && !opts->force_detach && !opts->new_branch &&
            !opts->ignore_other_worktrees) {
                int flag;
@@ -1368,24 +1365,17 @@ static struct option *add_common_options(struct checkout_opts *opts,
        return newopts;
 }
 
-static struct option *add_switch_branch_options(struct checkout_opts *opts,
-                                               struct option *prevopts)
+static struct option *add_common_switch_branch_options(
+       struct checkout_opts *opts, struct option *prevopts)
 {
        struct option options[] = {
-               OPT_STRING('b', NULL, &opts->new_branch, N_("branch"),
-                          N_("create and checkout a new branch")),
-               OPT_STRING('B', NULL, &opts->new_branch_force, N_("branch"),
-                          N_("create/reset and checkout a branch")),
-               OPT_BOOL('l', NULL, &opts->new_branch_log, N_("create reflog for new branch")),
-               OPT_BOOL(0, "detach", &opts->force_detach, N_("detach HEAD at named commit")),
+               OPT_BOOL('d', "detach", &opts->force_detach, N_("detach HEAD at named commit")),
                OPT_SET_INT('t', "track",  &opts->track, N_("set upstream info for new branch"),
                        BRANCH_TRACK_EXPLICIT),
                OPT_STRING(0, "orphan", &opts->new_orphan_branch, N_("new-branch"), N_("new unparented branch")),
                OPT_BOOL_F(0, "overwrite-ignore", &opts->overwrite_ignore,
                           N_("update ignored files (default)"),
                           PARSE_OPT_NOCOMPLETE),
-               OPT_BOOL(0, "no-guess", &opts->no_dwim_new_local_branch,
-                        N_("second guess 'git checkout <no-such-branch>'")),
                OPT_BOOL(0, "ignore-other-worktrees", &opts->ignore_other_worktrees,
                         N_("do not check if another worktree is holding the given ref")),
                OPT_END()
@@ -1422,7 +1412,7 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
 {
        struct branch_info new_branch_info;
        int dwim_remotes_matched = 0;
-       int dwim_new_local_branch;
+       int parseopt_flags = 0;
 
        memset(&new_branch_info, 0, sizeof(new_branch_info));
        opts->overwrite_ignore = 1;
@@ -1434,10 +1424,14 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
 
        opts->track = BRANCH_TRACK_UNSPECIFIED;
 
-       argc = parse_options(argc, argv, prefix, options, usagestr,
-                            PARSE_OPT_KEEP_DASHDASH);
+       if (!opts->accept_pathspec && !opts->accept_ref)
+               BUG("make up your mind, you need to take _something_");
+       if (opts->accept_pathspec && opts->accept_ref)
+               parseopt_flags = PARSE_OPT_KEEP_DASHDASH;
+
+       argc = parse_options(argc, argv, prefix, options,
+                            usagestr, parseopt_flags);
 
-       dwim_new_local_branch = !opts->no_dwim_new_local_branch;
        if (opts->show_progress < 0) {
                if (opts->quiet)
                        opts->show_progress = 0;
@@ -1449,6 +1443,8 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
                opts->merge = 1; /* implied */
                git_xmerge_config("merge.conflictstyle", opts->conflict_style, NULL);
        }
+       if (opts->force)
+               opts->discard_changes = 1;
 
        if ((!!opts->new_branch + !!opts->new_branch_force + !!opts->new_orphan_branch) > 1)
                die(_("-b, -B and --orphan are mutually exclusive"));
@@ -1493,11 +1489,11 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
         * including "last branch" syntax and DWIM-ery for names of
         * remote branches, erroring out for invalid or ambiguous cases.
         */
-       if (argc) {
+       if (argc && opts->accept_ref) {
                struct object_id rev;
                int dwim_ok =
                        !opts->patch_mode &&
-                       dwim_new_local_branch &&
+                       opts->dwim_new_local_branch &&
                        opts->track == BRANCH_TRACK_UNSPECIFIED &&
                        !opts->new_branch;
                int n = parse_branchname_arg(argc, argv, dwim_ok,
@@ -1505,6 +1501,18 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
                                             &dwim_remotes_matched);
                argv += n;
                argc -= n;
+       } else if (!opts->accept_ref && opts->from_treeish) {
+               struct object_id rev;
+
+               if (get_oid_mb(opts->from_treeish, &rev))
+                       die(_("could not resolve %s"), opts->from_treeish);
+
+               setup_new_branch_info_and_source_tree(&new_branch_info,
+                                                     opts, &rev,
+                                                     opts->from_treeish);
+
+               if (!opts->source_tree)
+                       die(_("reference is not a tree: %s"), opts->from_treeish);
        }
 
        if (argc) {
@@ -1571,15 +1579,32 @@ static int checkout_main(int argc, const char **argv, const char *prefix,
 int cmd_checkout(int argc, const char **argv, const char *prefix)
 {
        struct checkout_opts opts;
-       struct option *options = NULL;
+       struct option *options;
+       struct option checkout_options[] = {
+               OPT_STRING('b', NULL, &opts.new_branch, N_("branch"),
+                          N_("create and checkout a new branch")),
+               OPT_STRING('B', NULL, &opts.new_branch_force, N_("branch"),
+                          N_("create/reset and checkout a branch")),
+               OPT_BOOL('l', NULL, &opts.new_branch_log, N_("create reflog for new branch")),
+               OPT_BOOL(0, "guess", &opts.dwim_new_local_branch,
+                        N_("second guess 'git checkout <no-such-branch>' (default)")),
+               OPT_END()
+       };
        int ret;
 
        memset(&opts, 0, sizeof(opts));
-       opts.no_dwim_new_local_branch = 0;
-
-       options = parse_options_dup(options);
+       opts.dwim_new_local_branch = 1;
+       opts.switch_branch_doing_nothing_is_ok = 1;
+       opts.only_merge_on_switching_branches = 0;
+       opts.accept_ref = 1;
+       opts.accept_pathspec = 1;
+       opts.implicit_detach = 1;
+       opts.can_switch_when_in_progress = 1;
+       opts.orphan_from_empty_tree = 0;
+
+       options = parse_options_dup(checkout_options);
        options = add_common_options(&opts, options);
-       options = add_switch_branch_options(&opts, options);
+       options = add_common_switch_branch_options(&opts, options);
        options = add_checkout_path_options(&opts, options);
 
        ret = checkout_main(argc, argv, prefix, &opts,
@@ -1592,17 +1617,60 @@ int cmd_switch(int argc, const char **argv, const char *prefix)
 {
        struct checkout_opts opts;
        struct option *options = NULL;
+       struct option switch_options[] = {
+               OPT_STRING('c', "create", &opts.new_branch, N_("branch"),
+                          N_("create and switch to a new branch")),
+               OPT_STRING('C', "force-create", &opts.new_branch_force, N_("branch"),
+                          N_("create/reset and switch to a branch")),
+               OPT_BOOL(0, "guess", &opts.dwim_new_local_branch,
+                        N_("second guess 'git switch <no-such-branch>'")),
+               OPT_BOOL(0, "discard-changes", &opts.discard_changes,
+                        N_("throw away local modifications")),
+               OPT_END()
+       };
        int ret;
 
        memset(&opts, 0, sizeof(opts));
-       opts.no_dwim_new_local_branch = 0;
-
-       options = parse_options_dup(options);
+       opts.dwim_new_local_branch = 1;
+       opts.accept_ref = 1;
+       opts.accept_pathspec = 0;
+       opts.switch_branch_doing_nothing_is_ok = 0;
+       opts.only_merge_on_switching_branches = 1;
+       opts.implicit_detach = 0;
+       opts.can_switch_when_in_progress = 0;
+       opts.orphan_from_empty_tree = 1;
+
+       options = parse_options_dup(switch_options);
        options = add_common_options(&opts, options);
-       options = add_switch_branch_options(&opts, options);
+       options = add_common_switch_branch_options(&opts, options);
 
        ret = checkout_main(argc, argv, prefix, &opts,
                            options, switch_branch_usage);
        FREE_AND_NULL(options);
        return ret;
 }
+
+int cmd_restore(int argc, const char **argv, const char *prefix)
+{
+       struct checkout_opts opts;
+       struct option *options;
+       struct option restore_options[] = {
+               OPT_STRING('s', "source", &opts.from_treeish, "<tree-ish>",
+                          N_("where the checkout from")),
+               OPT_END()
+       };
+       int ret;
+
+       memset(&opts, 0, sizeof(opts));
+       opts.accept_ref = 0;
+       opts.accept_pathspec = 1;
+
+       options = parse_options_dup(restore_options);
+       options = add_common_options(&opts, options);
+       options = add_checkout_path_options(&opts, options);
+
+       ret = checkout_main(argc, argv, prefix, &opts,
+                           options, restore_usage);
+       FREE_AND_NULL(options);
+       return ret;
+}