github.com/psexton/git-lfs@v2.1.1-0.20170517224304-289a18b2bc53+incompatible/locking/lockable.go (about)

     1  package locking
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"path/filepath"
     7  	"strings"
     8  	"sync"
     9  
    10  	"github.com/git-lfs/git-lfs/errors"
    11  	"github.com/git-lfs/git-lfs/filepathfilter"
    12  	"github.com/git-lfs/git-lfs/git"
    13  	"github.com/git-lfs/git-lfs/tools"
    14  )
    15  
    16  // GetLockablePatterns returns a list of patterns in .gitattributes which are
    17  // marked as lockable
    18  func (c *Client) GetLockablePatterns() []string {
    19  	c.ensureLockablesLoaded()
    20  	return c.lockablePatterns
    21  }
    22  
    23  // getLockableFilter returns the internal filter used to check if a file is lockable
    24  func (c *Client) getLockableFilter() *filepathfilter.Filter {
    25  	c.ensureLockablesLoaded()
    26  	return c.lockableFilter
    27  }
    28  
    29  func (c *Client) ensureLockablesLoaded() {
    30  	c.lockableMutex.Lock()
    31  	defer c.lockableMutex.Unlock()
    32  
    33  	// Only load once
    34  	if c.lockablePatterns == nil {
    35  		c.refreshLockablePatterns()
    36  	}
    37  }
    38  
    39  // Internal function to repopulate lockable patterns
    40  // You must have locked the c.lockableMutex in the caller
    41  func (c *Client) refreshLockablePatterns() {
    42  
    43  	paths := git.GetAttributePaths(c.LocalWorkingDir, c.LocalGitDir)
    44  	// Always make non-nil even if empty
    45  	c.lockablePatterns = make([]string, 0, len(paths))
    46  	for _, p := range paths {
    47  		if p.Lockable {
    48  			c.lockablePatterns = append(c.lockablePatterns, p.Path)
    49  		}
    50  	}
    51  	c.lockableFilter = filepathfilter.New(c.lockablePatterns, nil)
    52  }
    53  
    54  // IsFileLockable returns whether a specific file path is marked as Lockable,
    55  // ie has the 'lockable' attribute in .gitattributes
    56  // Lockable patterns are cached once for performance, unless you call RefreshLockablePatterns
    57  // path should be relative to repository root
    58  func (c *Client) IsFileLockable(path string) bool {
    59  	return c.getLockableFilter().Allows(path)
    60  }
    61  
    62  // FixAllLockableFileWriteFlags recursively scans the repo looking for files which
    63  // are lockable, and makes sure their write flags are set correctly based on
    64  // whether they are currently locked or unlocked.
    65  // Files which are unlocked are made read-only, files which are locked are made
    66  // writeable.
    67  // This function can be used after a clone or checkout to ensure that file
    68  // state correctly reflects the locking state
    69  func (c *Client) FixAllLockableFileWriteFlags() error {
    70  	return c.fixFileWriteFlags(c.LocalWorkingDir, c.LocalWorkingDir, c.getLockableFilter(), nil)
    71  }
    72  
    73  // FixFileWriteFlagsInDir scans dir (which can either be a relative dir
    74  // from the root of the repo, or an absolute dir within the repo) looking for
    75  // files to change permissions for.
    76  // If lockablePatterns is non-nil, then any file matching those patterns will be
    77  // checked to see if it is currently locked by the current committer, and if so
    78  // it will be writeable, and if not locked it will be read-only.
    79  // If unlockablePatterns is non-nil, then any file matching those patterns will
    80  // be made writeable if it is not already. This can be used to reset files to
    81  // writeable when their 'lockable' attribute is turned off.
    82  func (c *Client) FixFileWriteFlagsInDir(dir string, lockablePatterns, unlockablePatterns []string) error {
    83  
    84  	// early-out if no patterns
    85  	if len(lockablePatterns) == 0 && len(unlockablePatterns) == 0 {
    86  		return nil
    87  	}
    88  
    89  	absPath := dir
    90  	if !filepath.IsAbs(dir) {
    91  		absPath = filepath.Join(c.LocalWorkingDir, dir)
    92  	}
    93  	stat, err := os.Stat(absPath)
    94  	if err != nil {
    95  		return err
    96  	}
    97  	if !stat.IsDir() {
    98  		return fmt.Errorf("%q is not a valid directory", dir)
    99  	}
   100  
   101  	var lockableFilter *filepathfilter.Filter
   102  	var unlockableFilter *filepathfilter.Filter
   103  	if lockablePatterns != nil {
   104  		lockableFilter = filepathfilter.New(lockablePatterns, nil)
   105  	}
   106  	if unlockablePatterns != nil {
   107  		unlockableFilter = filepathfilter.New(unlockablePatterns, nil)
   108  	}
   109  
   110  	return c.fixFileWriteFlags(absPath, c.LocalWorkingDir, lockableFilter, unlockableFilter)
   111  }
   112  
   113  // Internal implementation of fixing file write flags with precompiled filters
   114  func (c *Client) fixFileWriteFlags(absPath, workingDir string, lockable, unlockable *filepathfilter.Filter) error {
   115  
   116  	var errs []error
   117  	var errMux sync.Mutex
   118  
   119  	addErr := func(err error) {
   120  		errMux.Lock()
   121  		defer errMux.Unlock()
   122  
   123  		errs = append(errs, err)
   124  	}
   125  
   126  	tools.FastWalkGitRepo(absPath, func(parentDir string, fi os.FileInfo, err error) {
   127  		if err != nil {
   128  			addErr(err)
   129  			return
   130  		}
   131  		// Skip dirs, we only need to check files
   132  		if fi.IsDir() {
   133  			return
   134  		}
   135  		abschild := filepath.Join(parentDir, fi.Name())
   136  
   137  		// This is a file, get relative to repo root
   138  		relpath, err := filepath.Rel(workingDir, abschild)
   139  		if err != nil {
   140  			addErr(err)
   141  			return
   142  		}
   143  
   144  		err = c.fixSingleFileWriteFlags(relpath, lockable, unlockable)
   145  		if err != nil {
   146  			addErr(err)
   147  		}
   148  
   149  	})
   150  	return errors.Combine(errs)
   151  }
   152  
   153  // FixLockableFileWriteFlags checks each file in the provided list, and for
   154  // those which are lockable, makes sure their write flags are set correctly
   155  // based on whether they are currently locked or unlocked. Files which are
   156  // unlocked are made read-only, files which are locked are made writeable.
   157  // Files which are not lockable are ignored.
   158  // This function can be used after a clone or checkout to ensure that file
   159  // state correctly reflects the locking state, and is more efficient than
   160  // FixAllLockableFileWriteFlags when you know which files changed
   161  func (c *Client) FixLockableFileWriteFlags(files []string) error {
   162  	// early-out if no lockable patterns
   163  	if len(c.GetLockablePatterns()) == 0 {
   164  		return nil
   165  	}
   166  
   167  	var errs []error
   168  	for _, f := range files {
   169  		err := c.fixSingleFileWriteFlags(f, c.getLockableFilter(), nil)
   170  		if err != nil {
   171  			errs = append(errs, err)
   172  		}
   173  	}
   174  
   175  	return errors.Combine(errs)
   176  }
   177  
   178  // fixSingleFileWriteFlags fixes write flags on a single file
   179  // If lockablePatterns is non-nil, then any file matching those patterns will be
   180  // checked to see if it is currently locked by the current committer, and if so
   181  // it will be writeable, and if not locked it will be read-only.
   182  // If unlockablePatterns is non-nil, then any file matching those patterns will
   183  // be made writeable if it is not already. This can be used to reset files to
   184  // writeable when their 'lockable' attribute is turned off.
   185  func (c *Client) fixSingleFileWriteFlags(file string, lockable, unlockable *filepathfilter.Filter) error {
   186  	// Convert to git-style forward slash separators if necessary
   187  	// Necessary to match attributes
   188  	if filepath.Separator == '\\' {
   189  		file = strings.Replace(file, "\\", "/", -1)
   190  	}
   191  	if lockable != nil && lockable.Allows(file) {
   192  		// Lockable files are writeable only if they're currently locked
   193  		err := tools.SetFileWriteFlag(file, c.IsFileLockedByCurrentCommitter(file))
   194  		// Ignore not exist errors
   195  		if err != nil && !os.IsNotExist(err) {
   196  			return err
   197  		}
   198  	} else if unlockable != nil && unlockable.Allows(file) {
   199  		// Unlockable files are always writeable
   200  		// We only check files which match the incoming patterns to avoid
   201  		// checking every file in the system all the time, and only do it
   202  		// when a file has had its lockable attribute removed
   203  		err := tools.SetFileWriteFlag(file, true)
   204  		if err != nil && !os.IsNotExist(err) {
   205  			return err
   206  		}
   207  	}
   208  	return nil
   209  }