git-lfs/locking/lockable.go

209 lines
6.9 KiB
Go
Raw Normal View History

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
}