mirror of https://github.com/databricks/cli.git
281 lines
8.0 KiB
Go
281 lines
8.0 KiB
Go
package git
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"net/url"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/databricks/cli/libs/vfs"
|
|
)
|
|
|
|
const gitIgnoreFileName = ".gitignore"
|
|
|
|
var GitDirectoryName = ".git"
|
|
|
|
// Repository represents a Git repository or a directory
|
|
// that could later be initialized as Git repository.
|
|
type Repository struct {
|
|
// real indicates if this is a real repository or a non-Git
|
|
// directory where we process .gitignore files.
|
|
real bool
|
|
|
|
// rootDir is the path to the root of the repository checkout.
|
|
// This can be either the main repository checkout or a worktree checkout.
|
|
// For more information about worktrees, see: https://git-scm.com/docs/git-worktree#_description.
|
|
rootDir vfs.Path
|
|
|
|
// gitDir is the equivalent of $GIT_DIR and points to the
|
|
// `.git` directory of a repository or a worktree directory.
|
|
// See https://git-scm.com/docs/git-worktree#_details for more information.
|
|
gitDir vfs.Path
|
|
|
|
// gitCommonDir is the equivalent of $GIT_COMMON_DIR and points to the
|
|
// `.git` directory of the main working tree (common between worktrees).
|
|
// This is equivalent to [gitDir] if this is the main working tree.
|
|
// See https://git-scm.com/docs/git-worktree#_details for more information.
|
|
gitCommonDir vfs.Path
|
|
|
|
// ignore contains a list of ignore patterns indexed by the
|
|
// path prefix relative to the repository root.
|
|
//
|
|
// Example prefixes: ".", "foo/bar"
|
|
//
|
|
// Note: prefixes use the forward slash instead of the
|
|
// OS-specific path separator. This matches Git convention.
|
|
ignore map[string][]ignoreRules
|
|
|
|
// config contains a merged view of the user specific and the repository
|
|
// specific git configuration loaded from .git/config files.
|
|
//
|
|
// Also see: https://git-scm.com/docs/git-config.
|
|
config *config
|
|
}
|
|
|
|
// Root returns the absolute path to the repository root.
|
|
func (r *Repository) Root() string {
|
|
return r.rootDir.Native()
|
|
}
|
|
|
|
func (r *Repository) CurrentBranch() (string, error) {
|
|
ref, err := LoadReferenceFile(r.gitDir, "HEAD")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if ref == nil {
|
|
return "", nil
|
|
}
|
|
|
|
// case: when a git object like commit,tag or remote branch is checked out
|
|
if ref.Type == ReferenceTypeSHA1 {
|
|
return "", nil
|
|
}
|
|
return ref.CurrentBranch()
|
|
}
|
|
|
|
func (r *Repository) LatestCommit() (string, error) {
|
|
ref, err := LoadReferenceFile(r.gitDir, "HEAD")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if ref == nil {
|
|
// return empty string when head file does not exist
|
|
return "", nil
|
|
}
|
|
|
|
// case: when a git object like commit,tag or remote branch is checked out
|
|
if ref.Type == ReferenceTypeSHA1 {
|
|
return ref.Content, nil
|
|
}
|
|
|
|
// Read reference from $GIT_DIR/HEAD
|
|
branchHeadPath, err := ref.ResolvePath()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
branchHeadRef, err := LoadReferenceFile(r.gitCommonDir, branchHeadPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if branchHeadRef == nil {
|
|
// return empty string when head file does not exist
|
|
return "", nil
|
|
}
|
|
if branchHeadRef.Type != ReferenceTypeSHA1 {
|
|
return "", fmt.Errorf("git reference at %s was expected to be a SHA-1 commit id", branchHeadPath)
|
|
}
|
|
return branchHeadRef.Content, nil
|
|
}
|
|
|
|
// return origin url if it's defined, otherwise an empty string
|
|
func (r *Repository) OriginUrl() string {
|
|
rawUrl := r.config.variables["remote.origin.url"]
|
|
|
|
// Remove username and password from the URL.
|
|
parsedUrl, err := url.Parse(rawUrl)
|
|
if err != nil {
|
|
// Git supports https URLs and non standard URLs like "ssh://" or "file://".
|
|
// Parsing these URLs is not supported by the Go standard library. In case
|
|
// of an error, we return the raw URL. This is okay because for ssh URLs
|
|
// because passwords cannot be included in the URL.
|
|
return rawUrl
|
|
}
|
|
// Setting User to nil removes the username and password from the URL when
|
|
// .String() is called.
|
|
// See: https://pkg.go.dev/net/url#URL.String
|
|
parsedUrl.User = nil
|
|
return parsedUrl.String()
|
|
}
|
|
|
|
// loadConfig loads and combines user specific and repository specific configuration files.
|
|
func (r *Repository) loadConfig() error {
|
|
config, err := globalGitConfig()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to load user specific gitconfig: %w", err)
|
|
}
|
|
err = config.loadFile(r.gitCommonDir, "config")
|
|
if err != nil {
|
|
return fmt.Errorf("unable to load repository specific gitconfig: %w", err)
|
|
}
|
|
r.config = config
|
|
return nil
|
|
}
|
|
|
|
// getIgnoreRules returns a slice of [ignoreRules] that apply
|
|
// for the specified prefix. The prefix must be cleaned by the caller.
|
|
// It lazily initializes an entry for the specified prefix if it
|
|
// doesn't yet exist.
|
|
func (r *Repository) getIgnoreRules(prefix string) []ignoreRules {
|
|
fs, ok := r.ignore[prefix]
|
|
if ok {
|
|
return fs
|
|
}
|
|
|
|
r.ignore[prefix] = append(r.ignore[prefix], newIgnoreFile(r.rootDir, path.Join(prefix, gitIgnoreFileName)))
|
|
return r.ignore[prefix]
|
|
}
|
|
|
|
// taintIgnoreRules taints all ignore rules such that the underlying files
|
|
// are checked for modification next time they are needed.
|
|
func (r *Repository) taintIgnoreRules() {
|
|
for _, fs := range r.ignore {
|
|
for _, f := range fs {
|
|
f.Taint()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ignore computes whether to ignore the specified path.
|
|
// The specified path is relative to the repository root path.
|
|
func (r *Repository) Ignore(relPath string) (bool, error) {
|
|
parts := strings.Split(relPath, "/")
|
|
|
|
// Retain trailing slash for directory patterns.
|
|
// We know a trailing slash was present if the last element
|
|
// after splitting is an empty string.
|
|
trailingSlash := ""
|
|
if parts[len(parts)-1] == "" {
|
|
parts = parts[:len(parts)-1]
|
|
trailingSlash = "/"
|
|
}
|
|
|
|
// Never ignore the root path (an unnamed path)
|
|
if len(parts) == 1 && parts[0] == "." {
|
|
return false, nil
|
|
}
|
|
|
|
// Walk over path prefixes to check applicable gitignore files.
|
|
for i := range parts {
|
|
prefix := path.Clean(strings.Join(parts[:i], "/"))
|
|
suffix := path.Clean(strings.Join(parts[i:], "/")) + trailingSlash
|
|
|
|
// For this prefix (e.g. ".", or "dir1/dir2") we check if the
|
|
// suffix is matched in the respective gitignore files.
|
|
for _, rules := range r.getIgnoreRules(prefix) {
|
|
match, err := rules.MatchesPath(suffix)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if match {
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func NewRepository(path vfs.Path) (*Repository, error) {
|
|
real := true
|
|
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.
|
|
real = false
|
|
rootDir = path
|
|
}
|
|
|
|
// 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)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
repo := &Repository{
|
|
real: real,
|
|
rootDir: rootDir,
|
|
gitDir: gitDir,
|
|
gitCommonDir: gitCommonDir,
|
|
ignore: make(map[string][]ignoreRules),
|
|
}
|
|
|
|
err = repo.loadConfig()
|
|
if err != nil {
|
|
// Error doesn't need to be rewrapped.
|
|
return nil, err
|
|
}
|
|
|
|
coreExcludesPath, err := repo.config.coreExcludesFile()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to access core excludes file: %w", err)
|
|
}
|
|
|
|
// Load global excludes on this machine.
|
|
// This is by definition a local path so we create a new [vfs.Path] instance.
|
|
coreExcludes := newStringIgnoreRules([]string{})
|
|
if coreExcludesPath != "" {
|
|
dir := filepath.Dir(coreExcludesPath)
|
|
base := filepath.Base(coreExcludesPath)
|
|
coreExcludes = newIgnoreFile(vfs.MustNew(dir), base)
|
|
}
|
|
|
|
// Initialize root ignore rules.
|
|
// These are special and not lazily initialized because:
|
|
// 1) we include a hardcoded ignore pattern
|
|
// 2) we include a gitignore file at a non-standard path
|
|
repo.ignore["."] = []ignoreRules{
|
|
coreExcludes,
|
|
// Always ignore root .git directory.
|
|
newStringIgnoreRules([]string{
|
|
".git",
|
|
}),
|
|
// Load repository-wide exclude file.
|
|
newIgnoreFile(repo.gitCommonDir, "info/exclude"),
|
|
// Load root gitignore file.
|
|
newIgnoreFile(repo.rootDir, ".gitignore"),
|
|
}
|
|
|
|
return repo, nil
|
|
}
|
|
|
|
func (r *Repository) addIgnoreRule(rule ignoreRules) {
|
|
r.ignore["."] = append(r.ignore["."], rule)
|
|
}
|