Skip to content

Commit

Permalink
Properly read git metadata when inside Workspace
Browse files Browse the repository at this point in the history
Since there is no .git directory in Workspace file system, we need to make
an API call to fetch git checkout status (root of the repo, current branch, etc).
(api/2.0/workspace/get-status?return_git_info=true).

Refactor Repository to accept repository root rather than calculate it.
This helps, because Repository is currently created in multiple places and
finding the repository root is expensive.
  • Loading branch information
denik committed Dec 1, 2024
1 parent 84d535a commit 05ff508
Show file tree
Hide file tree
Showing 15 changed files with 217 additions and 83 deletions.
3 changes: 3 additions & 0 deletions bundle/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ type Bundle struct {
// Exclusively use this field for filesystem operations.
SyncRoot vfs.Path

// Path to root of git worktree
WorktreeRoot vfs.Path

// Config contains the bundle configuration.
// It is loaded from the bundle configuration files and mutators may update it.
Config config.Root
Expand Down
4 changes: 4 additions & 0 deletions bundle/bundle_read_only.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ func (r ReadOnlyBundle) SyncRoot() vfs.Path {
return r.b.SyncRoot
}

func (r ReadOnlyBundle) WorktreeRoot() vfs.Path {
return r.b.WorktreeRoot
}

func (r ReadOnlyBundle) WorkspaceClient() *databricks.WorkspaceClient {
return r.b.WorkspaceClient()
}
Expand Down
38 changes: 14 additions & 24 deletions bundle/config/mutator/load_git_details.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/libs/diag"
"github.com/databricks/cli/libs/git"
"github.com/databricks/cli/libs/log"
)

type loadGitDetails struct{}
Expand All @@ -21,50 +20,41 @@ func (m *loadGitDetails) Name() string {
}

func (m *loadGitDetails) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
// Load relevant git repository
repo, err := git.NewRepository(b.BundleRoot)
info, err := git.FetchRepositoryInfo(ctx, b.BundleRoot, b.WorkspaceClient())
if err != nil {
return diag.FromErr(err)
}

// Read branch name of current checkout
branch, err := repo.CurrentBranch()
if err == nil {
b.Config.Bundle.Git.ActualBranch = branch
if b.Config.Bundle.Git.Branch == "" {
// Only load branch if there's no user defined value
b.Config.Bundle.Git.Inferred = true
b.Config.Bundle.Git.Branch = branch
}
} else {
log.Warnf(ctx, "failed to load current branch: %s", err)
b.WorktreeRoot = info.WorktreeRoot

b.Config.Bundle.Git.ActualBranch = info.CurrentBranch
if b.Config.Bundle.Git.Branch == "" {
// Only load branch if there's no user defined value
b.Config.Bundle.Git.Inferred = true
b.Config.Bundle.Git.Branch = info.CurrentBranch
}

// load commit hash if undefined
if b.Config.Bundle.Git.Commit == "" {
commit, err := repo.LatestCommit()
if err != nil {
log.Warnf(ctx, "failed to load latest commit: %s", err)
} else {
b.Config.Bundle.Git.Commit = commit
}
b.Config.Bundle.Git.Commit = info.LatestCommit
}

// load origin url if undefined
if b.Config.Bundle.Git.OriginURL == "" {
remoteUrl := repo.OriginUrl()
b.Config.Bundle.Git.OriginURL = remoteUrl
b.Config.Bundle.Git.OriginURL = info.OriginURL
}

// Compute relative path of the bundle root from the Git repo root.
absBundlePath, err := filepath.Abs(b.BundleRootPath)
if err != nil {
return diag.FromErr(err)
}
// repo.Root() returns the absolute path of the repo
relBundlePath, err := filepath.Rel(repo.Root(), absBundlePath)

relBundlePath, err := filepath.Rel(info.WorktreeRoot.Native(), absBundlePath)
if err != nil {
return diag.FromErr(err)
}

b.Config.Bundle.Git.BundleRootPath = filepath.ToSlash(relBundlePath)
return nil
}
1 change: 1 addition & 0 deletions bundle/config/validate/files_to_sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func setupBundleForFilesToSyncTest(t *testing.T) *bundle.Bundle {
BundleRoot: vfs.MustNew(dir),
SyncRootPath: dir,
SyncRoot: vfs.MustNew(dir),
WorktreeRoot: vfs.MustNew(dir),
Config: config.Root{
Bundle: config.Bundle{
Target: "default",
Expand Down
9 changes: 5 additions & 4 deletions bundle/deploy/files/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,11 @@ func GetSyncOptions(ctx context.Context, rb bundle.ReadOnlyBundle) (*sync.SyncOp
}

opts := &sync.SyncOptions{
LocalRoot: rb.SyncRoot(),
Paths: rb.Config().Sync.Paths,
Include: includes,
Exclude: rb.Config().Sync.Exclude,
WorktreeRoot: rb.WorktreeRoot(),
LocalRoot: rb.SyncRoot(),
Paths: rb.Config().Sync.Paths,
Include: includes,
Exclude: rb.Config().Sync.Exclude,

RemotePath: rb.Config().Workspace.FilePath,
Host: rb.WorkspaceClient().Config.Host,
Expand Down
22 changes: 17 additions & 5 deletions cmd/sync/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/databricks/cli/bundle/deploy/files"
"github.com/databricks/cli/cmd/root"
"github.com/databricks/cli/libs/flags"
"github.com/databricks/cli/libs/git"
"github.com/databricks/cli/libs/sync"
"github.com/databricks/cli/libs/vfs"
"github.com/spf13/cobra"
Expand All @@ -37,6 +38,7 @@ func (f *syncFlags) syncOptionsFromBundle(cmd *cobra.Command, args []string, b *

opts.Full = f.full
opts.PollInterval = f.interval
opts.WorktreeRoot = b.WorktreeRoot
return opts, nil
}

Expand All @@ -60,11 +62,21 @@ func (f *syncFlags) syncOptionsFromArgs(cmd *cobra.Command, args []string) (*syn
}
}

ctx := cmd.Context()
client := root.WorkspaceClient(ctx)

localRoot := vfs.MustNew(args[0])
info, err := git.FetchRepositoryInfo(ctx, localRoot, client)
if err != nil {
return nil, err
}

opts := sync.SyncOptions{
LocalRoot: vfs.MustNew(args[0]),
Paths: []string{"."},
Include: nil,
Exclude: nil,
WorktreeRoot: info.WorktreeRoot,
LocalRoot: localRoot,
Paths: []string{"."},
Include: nil,
Exclude: nil,

RemotePath: args[1],
Full: f.full,
Expand All @@ -75,7 +87,7 @@ func (f *syncFlags) syncOptionsFromArgs(cmd *cobra.Command, args []string) (*syn
// The sync code will automatically create this directory if it doesn't
// exist and add it to the `.gitignore` file in the root.
SnapshotBasePath: filepath.Join(args[0], ".databricks"),
WorkspaceClient: root.WorkspaceClient(cmd.Context()),
WorkspaceClient: client,

OutputHandler: outputHandler,
}
Expand Down
10 changes: 7 additions & 3 deletions libs/git/fileset.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ type FileSet struct {
view *View
}

// NewFileSet returns [FileSet] for the Git repository located at `root`.
func NewFileSet(root vfs.Path, paths ...[]string) (*FileSet, error) {
// NewFileSet returns [FileSet] for the directory `root` which is contained within Git repository located at `worktreeRoot`.
func NewFileSet(worktreeRoot, root vfs.Path, paths ...[]string) (*FileSet, error) {
fs := fileset.New(root, paths...)
v, err := NewView(root)
v, err := NewView(worktreeRoot, root)
if err != nil {
return nil, err
}
Expand All @@ -27,6 +27,10 @@ func NewFileSet(root vfs.Path, paths ...[]string) (*FileSet, error) {
}, nil
}

func NewFileSetAtRoot(root vfs.Path, paths ...[]string) (*FileSet, error) {
return NewFileSet(root, root, paths...)
}

func (f *FileSet) IgnoreFile(file string) (bool, error) {
return f.view.IgnoreFile(file)
}
Expand Down
8 changes: 4 additions & 4 deletions libs/git/fileset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
)

func testFileSetAll(t *testing.T, root string) {
fileSet, err := NewFileSet(vfs.MustNew(root))
fileSet, err := NewFileSetAtRoot(vfs.MustNew(root))
require.NoError(t, err)
files, err := fileSet.Files()
require.NoError(t, err)
Expand All @@ -35,7 +35,7 @@ func TestFileSetNonCleanRoot(t *testing.T) {
// Test what happens if the root directory can be simplified.
// Path simplification is done by most filepath functions.
// This should yield the same result as above test.
fileSet, err := NewFileSet(vfs.MustNew("./testdata/../testdata"))
fileSet, err := NewFileSetAtRoot(vfs.MustNew("./testdata/../testdata"))
require.NoError(t, err)
files, err := fileSet.Files()
require.NoError(t, err)
Expand All @@ -44,7 +44,7 @@ func TestFileSetNonCleanRoot(t *testing.T) {

func TestFileSetAddsCacheDirToGitIgnore(t *testing.T) {
projectDir := t.TempDir()
fileSet, err := NewFileSet(vfs.MustNew(projectDir))
fileSet, err := NewFileSetAtRoot(vfs.MustNew(projectDir))
require.NoError(t, err)
fileSet.EnsureValidGitIgnoreExists()

Expand All @@ -59,7 +59,7 @@ func TestFileSetDoesNotCacheDirToGitIgnoreIfAlreadyPresent(t *testing.T) {
projectDir := t.TempDir()
gitIgnorePath := filepath.Join(projectDir, ".gitignore")

fileSet, err := NewFileSet(vfs.MustNew(projectDir))
fileSet, err := NewFileSetAtRoot(vfs.MustNew(projectDir))
require.NoError(t, err)
err = os.WriteFile(gitIgnorePath, []byte(".databricks"), 0o644)
require.NoError(t, err)
Expand Down
124 changes: 124 additions & 0 deletions libs/git/info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package git

import (
"context"
"errors"
"io/fs"
"net/http"
"strings"

"github.com/databricks/cli/libs/dbr"
"github.com/databricks/cli/libs/log"
"github.com/databricks/cli/libs/vfs"
"github.com/databricks/databricks-sdk-go"
"github.com/databricks/databricks-sdk-go/client"
)

type GitRepositoryInfo struct {
OriginURL string
LatestCommit string
CurrentBranch string
WorktreeRoot vfs.Path
}

type gitInfo struct {
Branch string `json:"branch"`
HeadCommitID string `json:"head_commit_id"`
Path string `json:"path"`
URL string `json:"url"`
}

type response struct {
GitInfo *gitInfo `json:"git_info,omitempty"`
}

func FetchRepositoryInfo(ctx context.Context, path vfs.Path, w *databricks.WorkspaceClient) (GitRepositoryInfo, error) {
if strings.HasPrefix(path.Native(), "/Workspace/") && dbr.RunsOnRuntime(ctx) {
return FetchRepositoryInfoAPI(ctx, path, w)
} else {
return FetchRepositoryInfoDotGit(ctx, path)
}
}

func FetchRepositoryInfoAPI(ctx context.Context, path vfs.Path, w *databricks.WorkspaceClient) (GitRepositoryInfo, error) {
apiClient, err := client.New(w.Config)
if err != nil {
return GitRepositoryInfo{}, err
}

var response response
const apiEndpoint = "/api/2.0/workspace/get-status"

err = apiClient.Do(
ctx,
http.MethodGet,
apiEndpoint,
nil,
map[string]string{
"path": path.Native(),
"return_git_info": "true",
},
&response,
)

if err != nil {
return GitRepositoryInfo{}, err
}

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

return GitRepositoryInfo{
WorktreeRoot: path,
}, nil
}

func fixResponsePath(path string) string {
if !strings.HasPrefix(path, "/Workspace/") {
return "/Workspace/" + path
}
return path
}

func FetchRepositoryInfoDotGit(ctx context.Context, path vfs.Path) (GitRepositoryInfo, error) {
rootDir, err := vfs.FindLeafInTree(path, GitDirectoryName)
if err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return GitRepositoryInfo{}, err
}
rootDir = path
}

repo, err := NewRepository(rootDir)
if err != nil {
return GitRepositoryInfo{}, err
}

branch, err := repo.CurrentBranch()
if err != nil {
return GitRepositoryInfo{}, nil
}

commit, err := repo.LatestCommit()
if err != nil {
return GitRepositoryInfo{}, nil
}

return GitRepositoryInfo{
OriginURL: repo.OriginUrl(),
LatestCommit: commit,
CurrentBranch: branch,
WorktreeRoot: rootDir,
}, nil
}
14 changes: 1 addition & 13 deletions libs/git/repository.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package git

import (
"errors"
"fmt"
"io/fs"
"net/url"
"path"
"path/filepath"
Expand Down Expand Up @@ -204,17 +202,7 @@ func (r *Repository) Ignore(relPath string) (bool, error) {
return false, nil
}

func NewRepository(path vfs.Path) (*Repository, error) {
rootDir, err := vfs.FindLeafInTree(path, GitDirectoryName)
if err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return nil, err
}
// Cannot find `.git` directory.
// Treat the specified path as a potential repository root checkout.
rootDir = path
}

func NewRepository(rootDir vfs.Path) (*Repository, error) {
// Derive $GIT_DIR and $GIT_COMMON_DIR paths if this is a real repository.
// If it isn't a real repository, they'll point to the (non-existent) `.git` directory.
gitDir, gitCommonDir, err := resolveGitDirs(rootDir)
Expand Down
Loading

0 comments on commit 05ff508

Please sign in to comment.