210 lines
6.9 KiB
Go
210 lines
6.9 KiB
Go
package locking
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/git-lfs/git-lfs/errors"
|
|
"github.com/git-lfs/git-lfs/filepathfilter"
|
|
"github.com/git-lfs/git-lfs/git"
|
|
"github.com/git-lfs/git-lfs/tools"
|
|
)
|
|
|
|
// GetLockablePatterns returns a list of patterns in .gitattributes which are
|
|
// marked as lockable
|
|
func (c *Client) GetLockablePatterns() []string {
|
|
c.ensureLockablesLoaded()
|
|
return c.lockablePatterns
|
|
}
|
|
|
|
// getLockableFilter returns the internal filter used to check if a file is lockable
|
|
func (c *Client) getLockableFilter() *filepathfilter.Filter {
|
|
c.ensureLockablesLoaded()
|
|
return c.lockableFilter
|
|
}
|
|
|
|
func (c *Client) ensureLockablesLoaded() {
|
|
c.lockableMutex.Lock()
|
|
defer c.lockableMutex.Unlock()
|
|
|
|
// Only load once
|
|
if c.lockablePatterns == nil {
|
|
c.refreshLockablePatterns()
|
|
}
|
|
}
|
|
|
|
// Internal function to repopulate lockable patterns
|
|
// You must have locked the c.lockableMutex in the caller
|
|
func (c *Client) refreshLockablePatterns() {
|
|
|
|
paths := git.GetAttributePaths(c.LocalWorkingDir, c.LocalGitDir)
|
|
// Always make non-nil even if empty
|
|
c.lockablePatterns = make([]string, 0, len(paths))
|
|
for _, p := range paths {
|
|
if p.Lockable {
|
|
c.lockablePatterns = append(c.lockablePatterns, p.Path)
|
|
}
|
|
}
|
|
c.lockableFilter = filepathfilter.New(c.lockablePatterns, nil)
|
|
}
|
|
|
|
// IsFileLockable returns whether a specific file path is marked as Lockable,
|
|
// ie has the 'lockable' attribute in .gitattributes
|
|
// Lockable patterns are cached once for performance, unless you call RefreshLockablePatterns
|
|
// path should be relative to repository root
|
|
func (c *Client) IsFileLockable(path string) bool {
|
|
return c.getLockableFilter().Allows(path)
|
|
}
|
|
|
|
// FixAllLockableFileWriteFlags recursively scans the repo looking for files which
|
|
// are lockable, and makes sure their write flags are set correctly based on
|
|
// whether they are currently locked or unlocked.
|
|
// Files which are unlocked are made read-only, files which are locked are made
|
|
// writeable.
|
|
// This function can be used after a clone or checkout to ensure that file
|
|
// state correctly reflects the locking state
|
|
func (c *Client) FixAllLockableFileWriteFlags() error {
|
|
return c.fixFileWriteFlags(c.LocalWorkingDir, c.LocalWorkingDir, c.getLockableFilter(), nil)
|
|
}
|
|
|
|
// FixFileWriteFlagsInDir scans dir (which can either be a relative dir
|
|
// from the root of the repo, or an absolute dir within the repo) looking for
|
|
// files to change permissions for.
|
|
// If lockablePatterns is non-nil, then any file matching those patterns will be
|
|
// checked to see if it is currently locked by the current committer, and if so
|
|
// it will be writeable, and if not locked it will be read-only.
|
|
// If unlockablePatterns is non-nil, then any file matching those patterns will
|
|
// be made writeable if it is not already. This can be used to reset files to
|
|
// writeable when their 'lockable' attribute is turned off.
|
|
func (c *Client) FixFileWriteFlagsInDir(dir string, lockablePatterns, unlockablePatterns []string) error {
|
|
|
|
// early-out if no patterns
|
|
if len(lockablePatterns) == 0 && len(unlockablePatterns) == 0 {
|
|
return nil
|
|
}
|
|
|
|
absPath := dir
|
|
if !filepath.IsAbs(dir) {
|
|
absPath = filepath.Join(c.LocalWorkingDir, dir)
|
|
}
|
|
stat, err := os.Stat(absPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !stat.IsDir() {
|
|
return fmt.Errorf("%q is not a valid directory", dir)
|
|
}
|
|
|
|
var lockableFilter *filepathfilter.Filter
|
|
var unlockableFilter *filepathfilter.Filter
|
|
if lockablePatterns != nil {
|
|
lockableFilter = filepathfilter.New(lockablePatterns, nil)
|
|
}
|
|
if unlockablePatterns != nil {
|
|
unlockableFilter = filepathfilter.New(unlockablePatterns, nil)
|
|
}
|
|
|
|
return c.fixFileWriteFlags(absPath, c.LocalWorkingDir, lockableFilter, unlockableFilter)
|
|
}
|
|
|
|
// Internal implementation of fixing file write flags with precompiled filters
|
|
func (c *Client) fixFileWriteFlags(absPath, workingDir string, lockable, unlockable *filepathfilter.Filter) error {
|
|
|
|
var errs []error
|
|
var errMux sync.Mutex
|
|
|
|
addErr := func(err error) {
|
|
errMux.Lock()
|
|
defer errMux.Unlock()
|
|
|
|
errs = append(errs, err)
|
|
}
|
|
|
|
tools.FastWalkGitRepo(absPath, func(parentDir string, fi os.FileInfo, err error) {
|
|
if err != nil {
|
|
addErr(err)
|
|
return
|
|
}
|
|
// Skip dirs, we only need to check files
|
|
if fi.IsDir() {
|
|
return
|
|
}
|
|
abschild := filepath.Join(parentDir, fi.Name())
|
|
|
|
// This is a file, get relative to repo root
|
|
relpath, err := filepath.Rel(workingDir, abschild)
|
|
if err != nil {
|
|
addErr(err)
|
|
return
|
|
}
|
|
|
|
err = c.fixSingleFileWriteFlags(relpath, lockable, unlockable)
|
|
if err != nil {
|
|
addErr(err)
|
|
}
|
|
|
|
})
|
|
return errors.Combine(errs)
|
|
}
|
|
|
|
// FixLockableFileWriteFlags checks each file in the provided list, and for
|
|
// those which are lockable, makes sure their write flags are set correctly
|
|
// based on whether they are currently locked or unlocked. Files which are
|
|
// unlocked are made read-only, files which are locked are made writeable.
|
|
// Files which are not lockable are ignored.
|
|
// This function can be used after a clone or checkout to ensure that file
|
|
// state correctly reflects the locking state, and is more efficient than
|
|
// FixAllLockableFileWriteFlags when you know which files changed
|
|
func (c *Client) FixLockableFileWriteFlags(files []string) error {
|
|
// early-out if no lockable patterns
|
|
if len(c.GetLockablePatterns()) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var errs []error
|
|
for _, f := range files {
|
|
err := c.fixSingleFileWriteFlags(f, c.getLockableFilter(), nil)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
|
|
return errors.Combine(errs)
|
|
}
|
|
|
|
// fixSingleFileWriteFlags fixes write flags on a single file
|
|
// If lockablePatterns is non-nil, then any file matching those patterns will be
|
|
// checked to see if it is currently locked by the current committer, and if so
|
|
// it will be writeable, and if not locked it will be read-only.
|
|
// If unlockablePatterns is non-nil, then any file matching those patterns will
|
|
// be made writeable if it is not already. This can be used to reset files to
|
|
// writeable when their 'lockable' attribute is turned off.
|
|
func (c *Client) fixSingleFileWriteFlags(file string, lockable, unlockable *filepathfilter.Filter) error {
|
|
// Convert to git-style forward slash separators if necessary
|
|
// Necessary to match attributes
|
|
if filepath.Separator == '\\' {
|
|
file = strings.Replace(file, "\\", "/", -1)
|
|
}
|
|
if lockable != nil && lockable.Allows(file) {
|
|
// Lockable files are writeable only if they're currently locked
|
|
err := tools.SetFileWriteFlag(file, c.IsFileLockedByCurrentCommitter(file))
|
|
// Ignore not exist errors
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
} else if unlockable != nil && unlockable.Allows(file) {
|
|
// Unlockable files are always writeable
|
|
// We only check files which match the incoming patterns to avoid
|
|
// checking every file in the system all the time, and only do it
|
|
// when a file has had its lockable attribute removed
|
|
err := tools.SetFileWriteFlag(file, true)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|