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