diff --git a/internal/cli/repo_clone.go b/internal/cli/repo_clone.go index ac00186..877551d 100644 --- a/internal/cli/repo_clone.go +++ b/internal/cli/repo_clone.go @@ -9,15 +9,18 @@ import ( func newRepoCloneCommand(service *app.Service) *cobra.Command { return &cobra.Command{ - Use: "clone [directory]", + Use: "clone [directory]", Short: "Clone a Tangled repository", Long: `Clone a Tangled repository via SSH into a local directory. -The default destination is the repository name.`, +The default destination is the repository name. If only a repository name is +given, the authenticated user's handle is used. + +Run "tg auth login" first when using the repository-only form.`, Args: cobra.RangeArgs(1, 2), RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - target, err := app.ParseTarget(args[0]) + target, err := resolveCloneTarget(ctx, args[0], service) if err != nil { return err } diff --git a/internal/cli/target.go b/internal/cli/target.go index 81bfce3..cf15b45 100644 --- a/internal/cli/target.go +++ b/internal/cli/target.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "strings" "github.com/alyraffauf/tg/internal/app" ) @@ -47,3 +48,20 @@ func resolveHandleOrSelf(ctx context.Context, args []string, service accountHand } return service.HandleOrSelf(ctx, "") } + +// resolveCloneTarget accepts either a complete handle/repo target or a repo +// name owned by the authenticated user. +func resolveCloneTarget(ctx context.Context, arg string, service accountHandleResolver) (app.Target, error) { + if strings.Contains(arg, "/") { + return app.ParseTarget(arg) + } + if arg == "" { + return app.Target{}, fmt.Errorf("expected repo or handle/repo, got %q", arg) + } + + handle, err := service.HandleOrSelf(ctx, "") + if err != nil { + return app.Target{}, err + } + return app.Target{Handle: handle, Repo: arg}, nil +} diff --git a/internal/cli/target_resolution_test.go b/internal/cli/target_resolution_test.go index 358bba4..3261930 100644 --- a/internal/cli/target_resolution_test.go +++ b/internal/cli/target_resolution_test.go @@ -4,6 +4,8 @@ import ( "context" "errors" "testing" + + "github.com/alyraffauf/tg/internal/app" ) func TestResolveHandleOrSelf(t *testing.T) { @@ -57,6 +59,60 @@ func TestResolveHandleOrSelf(t *testing.T) { } } +func TestResolveCloneTarget(t *testing.T) { + tests := []struct { + name string + arg string + resolver fakeAccountHandleResolver + want app.Target + wantCalls int + wantError bool + }{ + { + name: "explicit handle and repo", + arg: "other.test/tg", + want: app.Target{Handle: "other.test", Repo: "tg"}, + }, + { + name: "authenticated user repo", + arg: "tg", + resolver: fakeAccountHandleResolver{ + handle: "self.test", + }, + want: app.Target{Handle: "self.test", Repo: "tg"}, + wantCalls: 1, + }, + { + name: "authentication failure", + arg: "tg", + resolver: fakeAccountHandleResolver{ + err: errors.New("not logged in"), + }, + wantCalls: 1, + wantError: true, + }, + { + name: "empty repo", + wantError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := resolveCloneTarget(context.Background(), tt.arg, &tt.resolver) + if (err != nil) != tt.wantError { + t.Fatalf("resolveCloneTarget() error = %v, want error %t", err, tt.wantError) + } + if got != tt.want { + t.Fatalf("resolveCloneTarget() = %+v, want %+v", got, tt.want) + } + if tt.resolver.calls != tt.wantCalls { + t.Fatalf("HandleOrSelf() calls = %d, want %d", tt.resolver.calls, tt.wantCalls) + } + }) + } +} + type fakeAccountHandleResolver struct { handle string err error