From 30d48ecc09e34d3c5a4fc009cafa5b4a6ff635ae Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Tue, 13 Oct 2026 06:48:12 +0000 Subject: [PATCH 1/5] Fix Workspace Git metadata detection for linked worktrees --- .../bundles/linked-worktree-git-metadata.md | 1 + libs/git/info.go | 49 +++++++++-- libs/git/info_test.go | 88 +++++++++++++++++++ 3 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 .nextchanges/bundles/linked-worktree-git-metadata.md create mode 100644 libs/git/info_test.go diff --git a/.nextchanges/bundles/linked-worktree-git-metadata.md b/.nextchanges/bundles/linked-worktree-git-metadata.md new file mode 100644 index 00000000000..cca0f9eb6ea --- /dev/null +++ b/.nextchanges/bundles/linked-worktree-git-metadata.md @@ -0,0 +1 @@ +* Fix bundle Git branch detection for linked worktrees on Databricks Runtime. diff --git a/libs/git/info.go b/libs/git/info.go index 5d1d810dbc0..050690cd69f 100644 --- a/libs/git/info.go +++ b/libs/git/info.go @@ -5,7 +5,9 @@ import ( "errors" "io/fs" "net/http" + "os" "path" + "path/filepath" "strings" "github.com/databricks/cli/libs/auth" @@ -41,7 +43,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 +54,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 +69,45 @@ 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 != "" { + return info, err + } + + // Linked worktrees lack Workspace Git metadata but have a local .git file. + localPath := path + localInfo, err := os.Stat(path) + if errors.Is(err, fs.ErrNotExist) { + return info, nil + } + if err != nil { + return info, err + } + if !localInfo.IsDir() { + localPath = filepath.Dir(path) + } + + root, err := folders.FindDirWithLeaf(localPath, GitDirectoryName) + if errors.Is(err, fs.ErrNotExist) { + return info, nil + } + if err != nil { + return info, err + } + gitFile, err := os.Lstat(filepath.Join(root, GitDirectoryName)) + if err != nil { + return info, err + } + if !gitFile.Mode().IsRegular() { + return info, nil + } + return fetchRepositoryInfoDotGit(ctx, root) +} + +// 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..c3ffc25d165 --- /dev/null +++ b/libs/git/info_test.go @@ -0,0 +1,88 @@ +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, + } + testutil.WriteFile(t, filepath.Join(dir, ".git", "refs", "heads", want.CurrentBranch), want.LatestCommit) + 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: "file", path: 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}, + {name: "no git metadata", path: t.TempDir()}, + {name: "missing path", path: filepath.Join(worktree, "missing")}, + {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) + }) + } +} From c769f098950f503cf0a70615d6ca74d54a82adb0 Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Tue, 13 Oct 2026 06:50:26 +0000 Subject: [PATCH 2/5] Link changelog entry to PR 7033 --- .nextchanges/bundles/linked-worktree-git-metadata.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nextchanges/bundles/linked-worktree-git-metadata.md b/.nextchanges/bundles/linked-worktree-git-metadata.md index cca0f9eb6ea..b9753bf8195 100644 --- a/.nextchanges/bundles/linked-worktree-git-metadata.md +++ b/.nextchanges/bundles/linked-worktree-git-metadata.md @@ -1 +1 @@ -* Fix bundle Git branch detection for linked worktrees on Databricks Runtime. +* Fix bundle Git branch detection for linked worktrees on Databricks Runtime. ([#7033](https://github.com/databricks/cli/pull/7033)) From da89aa7634f67cf133580e4146cbc4bbe0da78f8 Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Tue, 13 Oct 2026 07:26:53 +0000 Subject: [PATCH 3/5] Remove changelog entry for Workspace worktree fix --- .nextchanges/bundles/linked-worktree-git-metadata.md | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .nextchanges/bundles/linked-worktree-git-metadata.md diff --git a/.nextchanges/bundles/linked-worktree-git-metadata.md b/.nextchanges/bundles/linked-worktree-git-metadata.md deleted file mode 100644 index b9753bf8195..00000000000 --- a/.nextchanges/bundles/linked-worktree-git-metadata.md +++ /dev/null @@ -1 +0,0 @@ -* Fix bundle Git branch detection for linked worktrees on Databricks Runtime. ([#7033](https://github.com/databricks/cli/pull/7033)) From 38e3e5ee66127613db32a2b1f22dd9a955e025a0 Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Tue, 13 Oct 2026 12:17:37 +0000 Subject: [PATCH 4/5] Fall back to local Git metadata in Workspace worktrees --- libs/git/info.go | 42 +++++++++++------------------------------- libs/git/info_test.go | 16 +++++++++++++--- 2 files changed, 24 insertions(+), 34 deletions(-) diff --git a/libs/git/info.go b/libs/git/info.go index 050690cd69f..14917bd6a43 100644 --- a/libs/git/info.go +++ b/libs/git/info.go @@ -71,38 +71,11 @@ func FetchRepositoryInfo(ctx context.Context, path string, w *databricks.Workspa func fetchRepositoryInfoFromWorkspace(ctx context.Context, path string, w *databricks.WorkspaceClient) (RepositoryInfo, error) { info, err := FetchRepositoryInfoAPI(ctx, path, w) - if err != nil || info.WorktreeRoot != "" { - return info, err + if err == nil && info.WorktreeRoot == "" { + // Fall back to Git information from .git for unsupported Git folders like worktree folders + return fetchRepositoryInfoDotGit(ctx, path) } - - // Linked worktrees lack Workspace Git metadata but have a local .git file. - localPath := path - localInfo, err := os.Stat(path) - if errors.Is(err, fs.ErrNotExist) { - return info, nil - } - if err != nil { - return info, err - } - if !localInfo.IsDir() { - localPath = filepath.Dir(path) - } - - root, err := folders.FindDirWithLeaf(localPath, GitDirectoryName) - if errors.Is(err, fs.ErrNotExist) { - return info, nil - } - if err != nil { - return info, err - } - gitFile, err := os.Lstat(filepath.Join(root, GitDirectoryName)) - if err != nil { - return info, err - } - if !gitFile.Mode().IsRegular() { - return info, nil - } - return fetchRepositoryInfoDotGit(ctx, root) + return info, err } // FetchRepositoryInfoAPI reads Workspace Git metadata, the primary source on a Runtime. @@ -185,6 +158,13 @@ func ensureWorkspacePrefix(p string) string { func fetchRepositoryInfoDotGit(ctx context.Context, path string) (RepositoryInfo, error) { result := RepositoryInfo{} + localInfo, err := os.Stat(path) + if err != nil { + return result, err + } + if !localInfo.IsDir() { + path = filepath.Dir(path) + } rootDir, err := folders.FindDirWithLeaf(path, GitDirectoryName) if rootDir == "" { diff --git a/libs/git/info_test.go b/libs/git/info_test.go index c3ffc25d165..f4b2ab2f08f 100644 --- a/libs/git/info_test.go +++ b/libs/git/info_test.go @@ -27,7 +27,11 @@ func TestFetchRepositoryInfoFromWorkspace(t *testing.T) { 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") @@ -53,9 +57,9 @@ func TestFetchRepositoryInfoFromWorkspace(t *testing.T) { CurrentBranch: "api-main", WorktreeRoot: "/Workspace/Users/me/api", }, }, - {name: "git directory", path: dir}, - {name: "no git metadata", path: t.TempDir()}, - {name: "missing path", path: filepath.Join(worktree, "missing")}, + {name: "git directory", path: dir, want: gitDirectoryWant}, + {name: "no git metadata", path: noGitRoot, wantErr: fs.ErrNotExist}, + {name: "missing path", path: filepath.Join(worktree, "missing"), wantErr: fs.ErrNotExist}, {name: "malformed gitfile", path: malformed, want: RepositoryInfo{WorktreeRoot: malformed}}, { name: "permission denied", path: worktree, status: http.StatusForbidden, @@ -86,3 +90,9 @@ func TestFetchRepositoryInfoFromWorkspace(t *testing.T) { }) } } + +func TestFetchRepositoryInfoMissingLocalPath(t *testing.T) { + got, err := FetchRepositoryInfo(t.Context(), filepath.Join(t.TempDir(), "missing"), nil) + require.NoError(t, err) + assert.Equal(t, RepositoryInfo{}, got) +} From 7194af3323d244f010a31e09fd678f74cfeac992 Mon Sep 17 00:00:00 2001 From: Lennart Kats Date: Tue, 13 Oct 2026 12:52:01 +0000 Subject: [PATCH 5/5] Keep local Git reader unchanged for Workspace fallback --- libs/git/info.go | 9 --------- libs/git/info_test.go | 8 -------- 2 files changed, 17 deletions(-) diff --git a/libs/git/info.go b/libs/git/info.go index 14917bd6a43..03f64cb43d7 100644 --- a/libs/git/info.go +++ b/libs/git/info.go @@ -5,9 +5,7 @@ import ( "errors" "io/fs" "net/http" - "os" "path" - "path/filepath" "strings" "github.com/databricks/cli/libs/auth" @@ -158,13 +156,6 @@ func ensureWorkspacePrefix(p string) string { func fetchRepositoryInfoDotGit(ctx context.Context, path string) (RepositoryInfo, error) { result := RepositoryInfo{} - localInfo, err := os.Stat(path) - if err != nil { - return result, err - } - if !localInfo.IsDir() { - path = filepath.Dir(path) - } rootDir, err := folders.FindDirWithLeaf(path, GitDirectoryName) if rootDir == "" { diff --git a/libs/git/info_test.go b/libs/git/info_test.go index f4b2ab2f08f..973ba69ddef 100644 --- a/libs/git/info_test.go +++ b/libs/git/info_test.go @@ -48,7 +48,6 @@ func TestFetchRepositoryInfoFromWorkspace(t *testing.T) { }{ {name: "worktree", path: worktree, want: want}, {name: "subdirectory", path: filepath.Dir(file), want: want}, - {name: "file", path: 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"}}`, @@ -59,7 +58,6 @@ func TestFetchRepositoryInfoFromWorkspace(t *testing.T) { }, {name: "git directory", path: dir, want: gitDirectoryWant}, {name: "no git metadata", path: noGitRoot, wantErr: fs.ErrNotExist}, - {name: "missing path", path: filepath.Join(worktree, "missing"), wantErr: fs.ErrNotExist}, {name: "malformed gitfile", path: malformed, want: RepositoryInfo{WorktreeRoot: malformed}}, { name: "permission denied", path: worktree, status: http.StatusForbidden, @@ -90,9 +88,3 @@ func TestFetchRepositoryInfoFromWorkspace(t *testing.T) { }) } } - -func TestFetchRepositoryInfoMissingLocalPath(t *testing.T) { - got, err := FetchRepositoryInfo(t.Context(), filepath.Join(t.TempDir(), "missing"), nil) - require.NoError(t, err) - assert.Equal(t, RepositoryInfo{}, got) -}