Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

### Bundles

* Fixed missing git information (origin URL, branch, commit) when deploying a bundle from a Git folder inside the workspace ([#5709](https://github.com/databricks/cli/pull/5709)).

### Dependency updates

### API Changes
34 changes: 27 additions & 7 deletions libs/git/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ type gitInfo struct {
HeadCommitID string `json:"head_commit_id"`
Path string `json:"path"`
URL string `json:"url"`
// ID of the git folder object. Some workspace git folders return only id+path
// from get-status (omitting branch/commit/url), so the id lets us recover the
// rest via the Repos API. See the fallback in fetchRepositoryInfoAPI.
ID int64 `json:"id"`
}

type response struct {
Expand Down Expand Up @@ -102,14 +106,30 @@ func fetchRepositoryInfoAPI(ctx context.Context, path string, w *databricks.Work

// Check if GitInfo is present and extract relevant fields
gi := response.GitInfo
if gi != nil {
fixedPath := ensureWorkspacePrefix(gi.Path)
result.OriginURL = gi.URL
result.LatestCommit = gi.HeadCommitID
result.CurrentBranch = gi.Branch
result.WorktreeRoot = fixedPath
} else {
if gi == nil {
log.Infof(ctx, "Failed to load git info from %s", apiEndpoint)
return result, nil
}

result.OriginURL = gi.URL
result.LatestCommit = gi.HeadCommitID
result.CurrentBranch = gi.Branch
result.WorktreeRoot = ensureWorkspacePrefix(gi.Path)

// Some workspace git folders return only id+path from get-status and omit the
// origin URL. When that happens, fetch the full provenance from the Repos API
// by id. Classic repos return the URL inline and skip this extra call.
if gi.ID != 0 && result.OriginURL == "" {
repo, err := w.Repos.GetByRepoId(ctx, gi.ID)
if err != nil {
// Best effort: WorktreeRoot is already set, so degrade to partial info
// rather than failing the deploy (see FetchRepositoryInfo's contract).
log.Debugf(ctx, "Failed to load git info from Repos API for id %d: %v", gi.ID, err)
return result, nil
}
result.OriginURL = repo.Url
result.LatestCommit = repo.HeadCommitId
result.CurrentBranch = repo.Branch
}

return result, nil
Expand Down
123 changes: 123 additions & 0 deletions libs/git/info_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package git

import (
"context"
"testing"

"github.com/databricks/cli/libs/dbr"
"github.com/databricks/cli/libs/testserver"
"github.com/databricks/databricks-sdk-go"
"github.com/databricks/databricks-sdk-go/service/workspace"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const (
// Bundle root passed to FetchRepositoryInfo: a subdirectory of the git folder.
testBundleRoot = "/Workspace/Users/test/bundle-examples/dabs_in_ws_bundle"
// Git folder path as get-status returns it (without the /Workspace prefix).
testGitFolderRaw = "/Users/test/bundle-examples"
// Expected worktree root after ensureWorkspacePrefix is applied.
testWorktreeRoot = "/Workspace/Users/test/bundle-examples"
testRepoID = int64(2884540697170475)
testOriginURL = "https://github.com/databricks/bundle-examples.git"
)

func newTestWorkspaceClient(t *testing.T, server *testserver.Server) *databricks.WorkspaceClient {
t.Helper()
w, err := databricks.NewWorkspaceClient(&databricks.Config{
Host: server.URL,
Token: "testtoken",
})
require.NoError(t, err)
return w
}

// runtimeContext forces the in-workspace API branch of FetchRepositoryInfo
// without needing a real /databricks directory on the test host.
func runtimeContext(t *testing.T) context.Context {
return dbr.MockRuntime(t.Context(), dbr.Environment{IsDbr: true, Version: "15.4"})
}

// New workspace git folders return only id+path from get-status; the missing
// branch/commit/url are recovered from the Repos API by id.
func TestFetchRepositoryInfoNewGitFolderFallsBackToReposAPI(t *testing.T) {
server := testserver.New(t)
server.Handle("GET", "/api/2.0/workspace/get-status", func(_ testserver.Request) any {
return testserver.Response{Body: map[string]any{
"git_info": map[string]any{
"id": testRepoID,
"path": testGitFolderRaw,
},
}}
})
server.Handle("GET", "/api/2.0/repos/{repo_id}", func(_ testserver.Request) any {
return testserver.Response{Body: workspace.GetRepoResponse{
Id: testRepoID,
Branch: "main",
HeadCommitId: "d53214abc",
Url: testOriginURL,
Provider: "gitHub",
Path: testGitFolderRaw,
}}
})

info, err := FetchRepositoryInfo(runtimeContext(t), testBundleRoot, newTestWorkspaceClient(t, server))
require.NoError(t, err)
assert.Equal(t, "main", info.CurrentBranch)
assert.Equal(t, "d53214abc", info.LatestCommit)
assert.Equal(t, testOriginURL, info.OriginURL)
assert.Equal(t, testWorktreeRoot, info.WorktreeRoot)
}

// Classic Repos return full git info inline from get-status, so the Repos API is
// not called.
func TestFetchRepositoryInfoClassicRepoSkipsReposAPI(t *testing.T) {
server := testserver.New(t)
server.Handle("GET", "/api/2.0/workspace/get-status", func(_ testserver.Request) any {
return testserver.Response{Body: map[string]any{
"git_info": map[string]any{
"id": testRepoID,
"path": testGitFolderRaw,
"branch": "main",
"head_commit_id": "abc123",
"url": testOriginURL,
},
}}
})
server.Handle("GET", "/api/2.0/repos/{repo_id}", func(_ testserver.Request) any {
t.Error("Repos API must not be called when get-status returns the URL inline")
return testserver.Response{StatusCode: 500}
})

info, err := FetchRepositoryInfo(runtimeContext(t), testBundleRoot, newTestWorkspaceClient(t, server))
require.NoError(t, err)
assert.Equal(t, "main", info.CurrentBranch)
assert.Equal(t, "abc123", info.LatestCommit)
assert.Equal(t, testOriginURL, info.OriginURL)
assert.Equal(t, testWorktreeRoot, info.WorktreeRoot)
}

// A failed Repos lookup must not fail the deploy: the worktree root stays set and
// the provenance fields stay empty, with no error.
func TestFetchRepositoryInfoReposLookupFailureDegradesGracefully(t *testing.T) {
server := testserver.New(t)
server.Handle("GET", "/api/2.0/workspace/get-status", func(_ testserver.Request) any {
return testserver.Response{Body: map[string]any{
"git_info": map[string]any{
"id": testRepoID,
"path": testGitFolderRaw,
},
}}
})
server.Handle("GET", "/api/2.0/repos/{repo_id}", func(_ testserver.Request) any {
return testserver.Response{StatusCode: 404, Body: map[string]string{"message": "not found"}}
})

info, err := FetchRepositoryInfo(runtimeContext(t), testBundleRoot, newTestWorkspaceClient(t, server))
require.NoError(t, err)
assert.Empty(t, info.CurrentBranch)
assert.Empty(t, info.LatestCommit)
assert.Empty(t, info.OriginURL)
assert.Equal(t, testWorktreeRoot, info.WorktreeRoot)
}
Loading