worktree add: extend DWIM to infer --orphan

Extend DWIM to try to infer `--orphan` when in an empty repository. i.e. a repository with an invalid/unborn HEAD, no local branches, and if `--guess-remote` is used then no remote branches. This behavior is equivalent to `git switch -c` or `git checkout -b` in an empty repository. Also warn the user (overriden with `-f`/`--force`) when they likely intend to checkout a remote branch to the worktree but have not yet fetched from the remote. i.e. when using `--guess-remote` and there is a remote but no local or remote refs. Current Behavior: % git --no-pager branch --list --remotes % git remote origin % git workree add ../main hint: If you meant to create a worktree containing a new orphan branch [...] hint: Disable this message with "git config advice.worktreeAddOrphan false" fatal: invalid reference: HEAD % git workree add --guess-remote ../main hint: If you meant to create a worktree containing a new orphan branch [...] hint: Disable this message with "git config advice.worktreeAddOrphan false" fatal: invalid reference: HEAD % git fetch --quiet % git --no-pager branch --list --remotes origin/HEAD -> origin/main origin/main % git workree add --guess-remote ../main Preparing worktree (new branch 'main') branch 'main' set up to track 'origin/main'. HEAD is now at dadc8e6dac commit message % New Behavior: % git --no-pager branch --list --remotes % git remote origin % git workree add ../main No possible source branch, inferring '--orphan' Preparing worktree (new branch 'main') % git worktree remove ../main % git workree add --guess-remote ../main fatal: No local or remote refs exist despite at least one remote present, stopping; use 'add -f' to overide or fetch a remote first % git workree add --guess-remote -f ../main No possible source branch, inferring '--orphan' Preparing worktree (new branch 'main') % git worktree remove ../main % git fetch --quiet % git --no-pager branch --list --remotes origin/HEAD -> origin/main origin/main % git workree add --guess-remote ../main Preparing worktree (new branch 'main') branch 'main' set up to track 'origin/main'. HEAD is now at dadc8e6dac commit message % Signed-off-by: Jacob Abel <jacobabel@nullpo.dev> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Jacob Abel committed May 17, 2023 at 21:48 UTC 128e5496b325640f0a09cc1d5b1e346c069b410f
3 files changed +449 -1
Documentation/git-worktree.txt
+10
@@ -95,6 +95,16 @@ exist, a new branch based on `HEAD` is automatically created as if
95 `-b <branch>` was given. If `<branch>` does exist, it will be checked out
96 in the new worktree, if it's not checked out anywhere else, otherwise the
97 command will refuse to create the worktree (unless `--force` is used).
98 ++
99 +If `<commit-ish>` is omitted, neither `--detach`, or `--orphan` is
100 +used, and there are no valid local branches (or remote branches if
101 +`--guess-remote` is specified) then, as a convenience, the new worktree is
102 +associated with a new orphan branch named `<branch>` (after
103 +`$(basename <path>)` if neither `-b` or `-B` is used) as if `--orphan` was
104 +passed to the command. In the event the repository has a remote and
105 +`--guess-remote` is used, but no remote or local branches exist, then the
106 +command fails with a warning reminding the user to fetch from their remote
107 +first (or override by using `-f/--force`).
108
109 list::
110
builtin/worktree.c
+113 -1
@@ -12,6 +12,7 @@
12 #include "strvec.h"
13 #include "branch.h"
14 #include "refs.h"
15 +#include "remote.h"
16 #include "run-command.h"
17 #include "hook.h"
18 #include "sigchain.h"
@@ -40,6 +41,9 @@
41 #define BUILTIN_WORKTREE_UNLOCK_USAGE \
42 N_("git worktree unlock <worktree>")
43
44 +#define WORKTREE_ADD_DWIM_ORPHAN_INFER_TEXT \
45 + _("No possible source branch, inferring '--orphan'")
46 +
47 #define WORKTREE_ADD_ORPHAN_WITH_DASH_B_HINT_TEXT \
48 _("If you meant to create a worktree containing a new orphan branch\n" \
49 "(branch with no commits) for this repository, you can do so\n" \
@@ -613,6 +617,107 @@ static void print_preparing_worktree_line(int detach,
617 }
618 }
619
620 +/**
621 + * Callback to short circuit iteration over refs on the first reference
622 + * corresponding to a valid oid.
623 + *
624 + * Returns 0 on failure and non-zero on success.
625 + */
626 +static int first_valid_ref(const char *refname,
627 + const struct object_id *oid,
628 + int flags,
629 + void *cb_data)
630 +{
631 + return 1;
632 +}
633 +
634 +/**
635 + * Verifies HEAD and determines whether there exist any valid local references.
636 + *
637 + * - Checks whether HEAD points to a valid reference.
638 + *
639 + * - Checks whether any valid local branches exist.
640 + *
641 + * Returns 1 if any of the previous checks are true, otherwise returns 0.
642 + */
643 +static int can_use_local_refs(const struct add_opts *opts)
644 +{
645 + if (head_ref(first_valid_ref, NULL)) {
646 + return 1;
647 + } else if (for_each_branch_ref(first_valid_ref, NULL)) {
648 + return 1;
649 + }
650 + return 0;
651 +}
652 +
653 +/**
654 + * Reports whether the necessary flags were set and whether the repository has
655 + * remote references to attempt DWIM tracking of upstream branches.
656 + *
657 + * 1. Checks that `--guess-remote` was used or `worktree.guessRemote = true`.
658 + *
659 + * 2. Checks whether any valid remote branches exist.
660 + *
661 + * 3. Checks that there exists at least one remote and emits a warning/error
662 + * if both checks 1. and 2. are false (can be bypassed with `--force`).
663 + *
664 + * Returns 1 if checks 1. and 2. are true, otherwise 0.
665 + */
666 +static int can_use_remote_refs(const struct add_opts *opts)
667 +{
668 + if (!guess_remote) {
669 + if (!opts->quiet)
670 + fprintf_ln(stderr, WORKTREE_ADD_DWIM_ORPHAN_INFER_TEXT);
671 + return 0;
672 + } else if (for_each_remote_ref(first_valid_ref, NULL)) {
673 + return 1;
674 + } else if (!opts->force && remote_get(NULL)) {
675 + die(_("No local or remote refs exist despite at least one remote\n"
676 + "present, stopping; use 'add -f' to overide or fetch a remote first"));
677 + } else if (!opts->quiet) {
678 + fprintf_ln(stderr, WORKTREE_ADD_DWIM_ORPHAN_INFER_TEXT);
679 + }
680 + return 0;
681 +}
682 +
683 +/**
684 + * Determines whether `--orphan` should be inferred in the evaluation of
685 + * `worktree add path/` or `worktree add -b branch path/` and emits an error
686 + * if the supplied arguments would produce an illegal combination when the
687 + * `--orphan` flag is included.
688 + *
689 + * `opts` and `opt_track` contain the other options & flags supplied to the
690 + * command.
691 + *
692 + * remote determines whether to check `can_use_remote_refs()` or not. This
693 + * is primarily to differentiate between the basic `add` DWIM and `add -b`.
694 + *
695 + * Returns 1 when inferring `--orphan`, 0 otherwise, and emits an error when
696 + * `--orphan` is inferred but doing so produces an illegal combination of
697 + * options and flags. Additionally produces an error when remote refs are
698 + * checked and the repo is in a state that looks like the user added a remote
699 + * but forgot to fetch (and did not override the warning with -f).
700 + */
701 +static int dwim_orphan(const struct add_opts *opts, int opt_track, int remote)
702 +{
703 + if (can_use_local_refs(opts)) {
704 + return 0;
705 + } else if (remote && can_use_remote_refs(opts)) {
706 + return 0;
707 + } else if (!opts->quiet) {
708 + fprintf_ln(stderr, WORKTREE_ADD_DWIM_ORPHAN_INFER_TEXT);
709 + }
710 +
711 + if (opt_track) {
712 + die(_("'%s' and '%s' cannot be used together"), "--orphan",
713 + "--track");
714 + } else if (!opts->checkout) {
715 + die(_("'%s' and '%s' cannot be used together"), "--orphan",
716 + "--no-checkout");
717 + }
718 + return 1;
719 +}
720 +
721 static const char *dwim_branch(const char *path, const char **new_branch)
722 {
723 int n;
@@ -723,12 +828,19 @@ static int add(int ac, const char **av, const char *prefix)
828 int n;
829 const char *s = worktree_basename(path, &n);
830 new_branch = xstrndup(s, n);
726 - } else if (new_branch || opts.detach || opts.orphan) {
831 + } else if (opts.orphan || opts.detach) {
832 // No-op
833 + } else if (ac < 2 && new_branch) {
834 + // DWIM: Infer --orphan when repo has no refs.
835 + opts.orphan = dwim_orphan(&opts, !!opt_track, 0);
836 } else if (ac < 2) {
837 + // DWIM: Guess branch name from path.
838 const char *s = dwim_branch(path, &new_branch);
839 if (s)
840 branch = s;
841 +
842 + // DWIM: Infer --orphan when repo has no refs.
843 + opts.orphan = (!s) && dwim_orphan(&opts, !!opt_track, 1);
844 } else if (ac == 2) {
845 struct object_id oid;
846 struct commit *commit;
t/t2400-worktree-add.sh
+326
@@ -705,6 +705,332 @@ test_expect_success 'git worktree --no-guess-remote option overrides config' '
705 )
706 '
707
708 +test_dwim_orphan () {
709 + local info_text="No possible source branch, inferring '--orphan'" &&
710 + local fetch_error_text="fatal: No local or remote refs exist despite at least one remote" &&
711 + local orphan_hint="hint: If you meant to create a worktree containing a new orphan branch" &&
712 + local invalid_ref_regex="^fatal: invalid reference:\s\+.*" &&
713 + local bad_combo_regex="^fatal: '[a-z-]\+' and '[a-z-]\+' cannot be used together" &&
714 +
715 + local git_ns="repo" &&
716 + local dashc_args="-C $git_ns" &&
717 + local use_cd=0 &&
718 +
719 + local bad_head=0 &&
720 + local empty_repo=1 &&
721 + local local_ref=0 &&
722 + local use_quiet=0 &&
723 + local remote=0 &&
724 + local remote_ref=0 &&
725 + local use_new_branch=0 &&
726 +
727 + local outcome="$1" &&
728 + local outcome_text &&
729 + local success &&
730 + shift &&
731 + local args="" &&
732 + local context="" &&
733 + case "$outcome" in
734 + "infer")
735 + success=1 &&
736 + outcome_text='"add" DWIM infer --orphan'
737 + ;;
738 + "no_infer")
739 + success=1 &&
740 + outcome_text='"add" DWIM doesnt infer --orphan'
741 + ;;
742 + "fetch_error")
743 + success=0 &&
744 + outcome_text='"add" error need fetch'
745 + ;;
746 + "fatal_orphan_bad_combo")
747 + success=0 &&
748 + outcome_text='"add" error inferred "--orphan" gives illegal opts combo'
749 + ;;
750 + *)
751 + echo "test_dwim_orphan(): invalid outcome: '$outcome'" >&2 &&
752 + return 1
753 + ;;
754 + esac &&
755 + while [ $# -gt 0 ]
756 + do
757 + case "$1" in
758 + # How and from where to create the worktree
759 + "-C_repo")
760 + use_cd=0 &&
761 + git_ns="repo" &&
762 + dashc_args="-C $git_ns" &&
763 + context="$context, 'git -C repo'"
764 + ;;
765 + "-C_wt")
766 + use_cd=0 &&
767 + git_ns="wt" &&
768 + dashc_args="-C $git_ns" &&
769 + context="$context, 'git -C wt'"
770 + ;;
771 + "cd_repo")
772 + use_cd=1 &&
773 + git_ns="repo" &&
774 + dashc_args="" &&
775 + context="$context, 'cd repo && git'"
776 + ;;
777 + "cd_wt")
778 + use_cd=1 &&
779 + git_ns="wt" &&
780 + dashc_args="" &&
781 + context="$context, 'cd wt && git'"
782 + ;;
783 +
784 + # Bypass the "pull first" warning
785 + "force")
786 + args="$args --force" &&
787 + context="$context, --force"
788 + ;;
789 +
790 + # Try to use remote refs when DWIM
791 + "guess_remote")
792 + args="$args --guess-remote" &&
793 + context="$context, --guess-remote"
794 + ;;
795 + "no_guess_remote")
796 + args="$args --no-guess-remote" &&
797 + context="$context, --no-guess-remote"
798 + ;;
799 +
800 + # Whether there is at least one local branch present
801 + "local_ref")
802 + empty_repo=0 &&
803 + local_ref=1 &&
804 + context="$context, >=1 local branches"
805 + ;;
806 + "no_local_ref")
807 + empty_repo=0 &&
808 + context="$context, 0 local branches"
809 + ;;
810 +
811 + # Whether the HEAD points at a valid ref (skip this opt when no refs)
812 + "good_head")
813 + # requires: local_ref
814 + context="$context, valid HEAD"
815 + ;;
816 + "bad_head")
817 + bad_head=1 &&
818 + context="$context, invalid (or orphan) HEAD"
819 + ;;
820 +
821 + # Whether the code path is tested with the base add command or -b
822 + "no_-b")
823 + use_new_branch=0 &&
824 + context="$context, no --branch"
825 + ;;
826 + "-b")
827 + use_new_branch=1 &&
828 + context="$context, --branch"
829 + ;;
830 +
831 + # Whether to check that all output is suppressed (except errors)
832 + # or that the output is as expected
833 + "quiet")
834 + use_quiet=1 &&
835 + args="$args --quiet" &&
836 + context="$context, --quiet"
837 + ;;
838 + "no_quiet")
839 + use_quiet=0 &&
840 + context="$context, no --quiet (expect output)"
841 + ;;
842 +
843 + # Whether there is at least one remote attached to the repo
844 + "remote")
845 + empty_repo=0 &&
846 + remote=1 &&
847 + context="$context, >=1 remotes"
848 + ;;
849 + "no_remote")
850 + empty_repo=0 &&
851 + remote=0 &&
852 + context="$context, 0 remotes"
853 + ;;
854 +
855 + # Whether there is at least one valid remote ref
856 + "remote_ref")
857 + # requires: remote
858 + empty_repo=0 &&
859 + remote_ref=1 &&
860 + context="$context, >=1 fetched remote branches"
861 + ;;
862 + "no_remote_ref")
863 + empty_repo=0 &&
864 + remote_ref=0 &&
865 + context="$context, 0 fetched remote branches"
866 + ;;
867 +
868 + # Options or flags that become illegal when --orphan is inferred
869 + "no_checkout")
870 + args="$args --no-checkout" &&
871 + context="$context, --no-checkout"
872 + ;;
873 + "track")
874 + args="$args --track" &&
875 + context="$context, --track"
876 + ;;
877 +
878 + # All other options are illegal
879 + *)
880 + echo "test_dwim_orphan(): invalid arg: '$1'" >&2 &&
881 + return 1
882 + ;;
883 + esac &&
884 + shift
885 + done &&
886 + context="${context#', '}" &&
887 + if [ $use_new_branch -eq 1 ]
888 + then
889 + args="$args -b foo"
890 + else
891 + context="DWIM (no --branch), $context"
892 + fi &&
893 + if [ $empty_repo -eq 1 ]
894 + then
895 + context="empty repo, $context"
896 + fi &&
897 + args="$args ../foo" &&
898 + context="${context%', '}" &&
899 + test_expect_success "$outcome_text w/ $context" '
900 + test_when_finished "rm -rf repo" &&
901 + git init repo &&
902 + if [ $local_ref -eq 1 ] && [ "$git_ns" = "repo" ]
903 + then
904 + (cd repo && test_commit commit) &&
905 + if [ $bad_head -eq 1 ]
906 + then
907 + git -C repo symbolic-ref HEAD refs/heads/badbranch
908 + fi
909 + elif [ $local_ref -eq 1 ] && [ "$git_ns" = "wt" ]
910 + then
911 + test_when_finished "git -C repo worktree remove -f ../wt" &&
912 + git -C repo worktree add --orphan -b main ../wt &&
913 + (cd wt && test_commit commit) &&
914 + if [ $bad_head -eq 1 ]
915 + then
916 + git -C wt symbolic-ref HEAD refs/heads/badbranch
917 + fi
918 + elif [ $local_ref -eq 0 ] && [ "$git_ns" = "wt" ]
919 + then
920 + test_when_finished "git -C repo worktree remove -f ../wt" &&
921 + git -C repo worktree add --orphan -b orphanbranch ../wt
922 + fi &&
923 +
924 + if [ $remote -eq 1 ]
925 + then
926 + test_when_finished "rm -rf upstream" &&
927 + git init upstream &&
928 + (cd upstream && test_commit commit) &&
929 + git -C upstream switch -c foo &&
930 + git -C repo remote add upstream ../upstream
931 + fi &&
932 +
933 + if [ $remote_ref -eq 1 ]
934 + then
935 + git -C repo fetch
936 + fi &&
937 + if [ $success -eq 1 ]
938 + then
939 + test_when_finished git -C repo worktree remove ../foo
940 + fi &&
941 + (
942 + if [ $use_cd -eq 1 ]
943 + then
944 + cd $git_ns
945 + fi &&
946 + if [ "$outcome" = "infer" ]
947 + then
948 + git $dashc_args worktree add $args 2>actual &&
949 + if [ $use_quiet -eq 1 ]
950 + then
951 + test_must_be_empty actual
952 + else
953 + grep "$info_text" actual
954 + fi
955 + elif [ "$outcome" = "no_infer" ]
956 + then
957 + git $dashc_args worktree add $args 2>actual &&
958 + if [ $use_quiet -eq 1 ]
959 + then
960 + test_must_be_empty actual
961 + else
962 + ! grep "$info_text" actual
963 + fi
964 + elif [ "$outcome" = "fetch_error" ]
965 + then
966 + test_must_fail git $dashc_args worktree add $args 2>actual &&
967 + grep "$fetch_error_text" actual
968 + elif [ "$outcome" = "fatal_orphan_bad_combo" ]
969 + then
970 + test_must_fail git $dashc_args worktree add $args 2>actual &&
971 + if [ $use_quiet -eq 1 ]
972 + then
973 + ! grep "$info_text" actual
974 + else
975 + grep "$info_text" actual
976 + fi &&
977 + grep "$bad_combo_regex" actual
978 + elif [ "$outcome" = "warn_bad_head" ]
979 + then
980 + test_must_fail git $dashc_args worktree add $args 2>actual &&
981 + if [ $use_quiet -eq 1 ]
982 + then
983 + grep "$invalid_ref_regex" actual &&
984 + ! grep "$orphan_hint" actual
985 + else
986 + headpath=$(git $dashc_args rev-parse --sq --path-format=absolute --git-path HEAD) &&
987 + headcontents=$(cat "$headpath") &&
988 + grep "HEAD points to an invalid (or orphaned) reference" actual &&
989 + grep "HEAD path:\s*.$headpath." actual &&
990 + grep "HEAD contents:\s*.$headcontents." actual &&
991 + grep "$orphan_hint" actual &&
992 + ! grep "$info_text" actual
993 + fi &&
994 + grep "$invalid_ref_regex" actual
995 + else
996 + # Unreachable
997 + false
998 + fi
999 + ) &&
1000 + if [ $success -ne 1 ]
1001 + then
1002 + test_path_is_missing foo
1003 + fi
1004 + '
1005 +}
1006 +
1007 +for quiet_mode in "no_quiet" "quiet"
1008 +do
1009 + for changedir_type in "cd_repo" "cd_wt" "-C_repo" "-C_wt"
1010 + do
1011 + dwim_test_args="$quiet_mode $changedir_type"
1012 + test_dwim_orphan 'infer' $dwim_test_args no_-b
1013 + test_dwim_orphan 'no_infer' $dwim_test_args no_-b local_ref good_head
1014 + test_dwim_orphan 'infer' $dwim_test_args no_-b no_local_ref no_remote no_remote_ref no_guess_remote
1015 + test_dwim_orphan 'infer' $dwim_test_args no_-b no_local_ref remote no_remote_ref no_guess_remote
1016 + test_dwim_orphan 'fetch_error' $dwim_test_args no_-b no_local_ref remote no_remote_ref guess_remote
1017 + test_dwim_orphan 'infer' $dwim_test_args no_-b no_local_ref remote no_remote_ref guess_remote force
1018 + test_dwim_orphan 'no_infer' $dwim_test_args no_-b no_local_ref remote remote_ref guess_remote
1019 +
1020 + test_dwim_orphan 'infer' $dwim_test_args -b
1021 + test_dwim_orphan 'no_infer' $dwim_test_args -b local_ref good_head
1022 + test_dwim_orphan 'infer' $dwim_test_args -b no_local_ref no_remote no_remote_ref no_guess_remote
1023 + test_dwim_orphan 'infer' $dwim_test_args -b no_local_ref remote no_remote_ref no_guess_remote
1024 + test_dwim_orphan 'infer' $dwim_test_args -b no_local_ref remote no_remote_ref guess_remote
1025 + test_dwim_orphan 'infer' $dwim_test_args -b no_local_ref remote remote_ref guess_remote
1026 + done
1027 +
1028 + test_dwim_orphan 'fatal_orphan_bad_combo' $quiet_mode no_-b no_checkout
1029 + test_dwim_orphan 'fatal_orphan_bad_combo' $quiet_mode no_-b track
1030 + test_dwim_orphan 'fatal_orphan_bad_combo' $quiet_mode -b no_checkout
1031 + test_dwim_orphan 'fatal_orphan_bad_combo' $quiet_mode -b track
1032 +done
1033 +
1034 post_checkout_hook () {
1035 test_when_finished "rm -rf .git/hooks" &&
1036 mkdir .git/hooks &&