diff --git a/libs/git/info.go b/libs/git/info.go index 5d1d810dbc0..03f64cb43d7 100644 --- a/libs/git/info.go +++ b/libs/git/info.go @@ -41,7 +41,7 @@ type response struct { GitInfo *gitInfo `json:"git_info,omitempty"` } -// Fetch repository information either by quering .git or by fetching it from API (for dabs-in-workspace case). +// FetchRepositoryInfo reads Workspace Git metadata or a local .git checkout. // - In case we could not find git repository (including when the path does not exist), all string fields of RepositoryInfo will be "" and err will be nil. // - If there were any errors when trying to determine git root (e.g. API call returned an error or there were permission issues // reading the file system), all strings fields of RepositoryInfo will be "" and err will be non-nil. @@ -52,7 +52,7 @@ func FetchRepositoryInfo(ctx context.Context, path string, w *databricks.Workspa var info RepositoryInfo var err error if strings.HasPrefix(path, "/Workspace/") && dbr.RunsOnRuntime(ctx) { - info, err = FetchRepositoryInfoAPI(ctx, path, w) + info, err = fetchRepositoryInfoFromWorkspace(ctx, path, w) } else { info, err = fetchRepositoryInfoDotGit(ctx, path) } @@ -67,10 +67,18 @@ func FetchRepositoryInfo(ctx context.Context, path string, w *databricks.Workspa return info, err } -// FetchRepositoryInfoAPI reads the metadata from the workspace API, which is what -// FetchRepositoryInfo does on a Databricks Runtime. Exported so that -// `bundle debug fetch-repository-info --workspace-api` can reach this path -// off-runtime; the product calls it through FetchRepositoryInfo. +func fetchRepositoryInfoFromWorkspace(ctx context.Context, path string, w *databricks.WorkspaceClient) (RepositoryInfo, error) { + info, err := FetchRepositoryInfoAPI(ctx, path, w) + if err == nil && info.WorktreeRoot == "" { + // Fall back to Git information from .git for unsupported Git folders like worktree folders + return fetchRepositoryInfoDotGit(ctx, path) + } + return info, err +} + +// FetchRepositoryInfoAPI reads Workspace Git metadata, the primary source on a Runtime. +// It is exported so `bundle debug fetch-repository-info --workspace-api` can use this backend +// off Runtime; normal commands call FetchRepositoryInfo. // // A path that does not exist is reported as fs.ErrNotExist; callers that treat // that as "no repository here" must normalize it the way FetchRepositoryInfo does. diff --git a/libs/git/info_test.go b/libs/git/info_test.go new file mode 100644 index 00000000000..973ba69ddef --- /dev/null +++ b/libs/git/info_test.go @@ -0,0 +1,90 @@ +package git + +import ( + "io/fs" + "net/http" + "net/url" + "path/filepath" + "strings" + "testing" + + "github.com/databricks/cli/internal/testutil" + "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/apierr" + "github.com/databricks/databricks-sdk-go/qa" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFetchRepositoryInfoFromWorkspace(t *testing.T) { + dir := setupWorktree(t) + worktree := filepath.Join(dir, "my_worktree") + writeGitDir(t, dir, "gitdir: ../.git/worktrees/my_worktree") + writeGitCommonDir(t, dir, "../..") + want := RepositoryInfo{ + OriginURL: "https://example.test/repo.git", + LatestCommit: strings.Repeat("1", 40), + CurrentBranch: "projects/task", + WorktreeRoot: worktree, + } + gitDirectoryWant := want + gitDirectoryWant.WorktreeRoot = dir + noGitRoot := filepath.VolumeName(dir) + string(filepath.Separator) + testutil.WriteFile(t, filepath.Join(dir, ".git", "refs", "heads", want.CurrentBranch), want.LatestCommit) + testutil.WriteFile(t, filepath.Join(dir, ".git", "HEAD"), "ref: refs/heads/"+want.CurrentBranch) + testutil.WriteFile(t, filepath.Join(dir, ".git", "worktrees", "my_worktree", "HEAD"), "ref: refs/heads/"+want.CurrentBranch) + testutil.WriteFile(t, filepath.Join(dir, ".git", "config"), "[remote \"origin\"]\n\turl = "+want.OriginURL) + file := testutil.Touch(t, worktree, "subdir", "README.md") + malformed := t.TempDir() + testutil.WriteFile(t, filepath.Join(malformed, ".git"), "invalid gitfile") + + for _, tc := range []struct { + name string + path string + body string + status int + want RepositoryInfo + wantErr error + }{ + {name: "worktree", path: worktree, want: want}, + {name: "subdirectory", path: filepath.Dir(file), want: want}, + { + name: "workspace metadata takes precedence", path: worktree, + body: `{"git_info":{"branch":"api-main","head_commit_id":"2222222222222222222222222222222222222222","path":"/Users/me/api","url":"https://example.test/api.git"}}`, + want: RepositoryInfo{ + OriginURL: "https://example.test/api.git", LatestCommit: strings.Repeat("2", 40), + CurrentBranch: "api-main", WorktreeRoot: "/Workspace/Users/me/api", + }, + }, + {name: "git directory", path: dir, want: gitDirectoryWant}, + {name: "no git metadata", path: noGitRoot, wantErr: fs.ErrNotExist}, + {name: "malformed gitfile", path: malformed, want: RepositoryInfo{WorktreeRoot: malformed}}, + { + name: "permission denied", path: worktree, status: http.StatusForbidden, + body: `{"error_code":"PERMISSION_DENIED","message":"denied"}`, wantErr: apierr.ErrPermissionDenied, + }, + { + name: "workspace path missing", path: worktree, status: http.StatusNotFound, + body: `{"error_code":"RESOURCE_DOES_NOT_EXIST","message":"missing"}`, wantErr: fs.ErrNotExist, + }, + } { + t.Run(tc.name, func(t *testing.T) { + body := tc.body + if body == "" { + body = `{}` + } + cfg, server := qa.HTTPFixtures{{ + Method: "GET", + Resource: "/api/2.0/workspace/get-status?path=" + url.QueryEscape(tc.path) + "&return_git_info=true", + Response: body, + Status: tc.status, + }}.Config(t) + t.Cleanup(server.Close) + w := databricks.Must(databricks.NewWorkspaceClient((*databricks.Config)(cfg))) + + got, err := fetchRepositoryInfoFromWorkspace(t.Context(), tc.path, w) + require.ErrorIs(t, err, tc.wantErr) + assert.Equal(t, tc.want, got) + }) + } +}