2016-05-18 10:43:42 +00:00
|
|
|
// Package tools contains other helper functions too small to justify their own package
|
|
|
|
// NOTE: Subject to change, do not rely on this package from outside git-lfs source
|
2016-05-13 16:38:06 +00:00
|
|
|
package tools
|
|
|
|
|
|
|
|
import (
|
2016-07-12 10:43:54 +00:00
|
|
|
"encoding/hex"
|
2016-05-25 10:28:24 +00:00
|
|
|
"fmt"
|
2016-07-12 10:43:54 +00:00
|
|
|
"io"
|
2019-09-23 19:33:31 +00:00
|
|
|
"io/ioutil"
|
2016-05-13 16:38:06 +00:00
|
|
|
"os"
|
2018-08-30 19:29:44 +00:00
|
|
|
"os/user"
|
2016-05-13 16:38:06 +00:00
|
|
|
"path/filepath"
|
2017-10-17 15:14:48 +00:00
|
|
|
"runtime"
|
|
|
|
"strconv"
|
2016-05-31 15:48:09 +00:00
|
|
|
"strings"
|
2016-10-31 15:39:52 +00:00
|
|
|
"sync"
|
2017-10-17 15:14:48 +00:00
|
|
|
"sync/atomic"
|
2016-05-13 16:38:06 +00:00
|
|
|
|
2018-08-30 19:29:44 +00:00
|
|
|
"github.com/git-lfs/git-lfs/errors"
|
2016-11-21 23:53:19 +00:00
|
|
|
"github.com/git-lfs/git-lfs/filepathfilter"
|
|
|
|
)
|
2016-10-31 12:45:12 +00:00
|
|
|
|
2016-05-13 16:38:06 +00:00
|
|
|
// FileOrDirExists determines if a file/dir exists, returns IsDir() results too.
|
|
|
|
func FileOrDirExists(path string) (exists bool, isDir bool) {
|
|
|
|
fi, err := os.Stat(path)
|
|
|
|
if err != nil {
|
|
|
|
return false, false
|
|
|
|
} else {
|
|
|
|
return true, fi.IsDir()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// FileExists determines if a file (NOT dir) exists.
|
|
|
|
func FileExists(path string) bool {
|
|
|
|
ret, isDir := FileOrDirExists(path)
|
|
|
|
return ret && !isDir
|
|
|
|
}
|
|
|
|
|
|
|
|
// DirExists determines if a dir (NOT file) exists.
|
|
|
|
func DirExists(path string) bool {
|
|
|
|
ret, isDir := FileOrDirExists(path)
|
|
|
|
return ret && isDir
|
|
|
|
}
|
|
|
|
|
|
|
|
// FileExistsOfSize determines if a file exists and is of a specific size.
|
|
|
|
func FileExistsOfSize(path string, sz int64) bool {
|
|
|
|
fi, err := os.Stat(path)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return !fi.IsDir() && fi.Size() == sz
|
|
|
|
}
|
|
|
|
|
|
|
|
// ResolveSymlinks ensures that if the path supplied is a symlink, it is
|
|
|
|
// resolved to the actual concrete path
|
|
|
|
func ResolveSymlinks(path string) string {
|
|
|
|
if len(path) == 0 {
|
|
|
|
return path
|
|
|
|
}
|
|
|
|
|
|
|
|
if resolved, err := filepath.EvalSymlinks(path); err == nil {
|
|
|
|
return resolved
|
|
|
|
}
|
|
|
|
return path
|
|
|
|
}
|
2016-05-25 10:28:24 +00:00
|
|
|
|
|
|
|
// RenameFileCopyPermissions moves srcfile to destfile, replacing destfile if
|
|
|
|
// necessary and also copying the permissions of destfile if it already exists
|
|
|
|
func RenameFileCopyPermissions(srcfile, destfile string) error {
|
|
|
|
info, err := os.Stat(destfile)
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
// no original file
|
|
|
|
} else if err != nil {
|
|
|
|
return err
|
|
|
|
} else {
|
|
|
|
if err := os.Chmod(srcfile, info.Mode()); err != nil {
|
|
|
|
return fmt.Errorf("can't set filemode on file %q: %v", srcfile, err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := os.Rename(srcfile, destfile); err != nil {
|
|
|
|
return fmt.Errorf("cannot replace %q with %q: %v", destfile, srcfile, err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2016-06-02 09:04:40 +00:00
|
|
|
|
2016-05-31 15:48:09 +00:00
|
|
|
// CleanPaths splits the given `paths` argument by the delimiter argument, and
|
2016-07-15 13:20:53 +00:00
|
|
|
// then "cleans" that path according to the path.Clean function (see
|
|
|
|
// https://golang.org/pkg/path#Clean).
|
|
|
|
// Note always cleans to '/' path separators regardless of platform (git friendly)
|
2016-05-31 15:48:09 +00:00
|
|
|
func CleanPaths(paths, delim string) (cleaned []string) {
|
|
|
|
// If paths is an empty string, splitting it will yield [""], which will
|
2016-07-15 13:20:53 +00:00
|
|
|
// become the path ".". To avoid this, bail out if trimmed paths
|
2016-05-31 15:48:09 +00:00
|
|
|
// argument is empty.
|
|
|
|
if paths = strings.TrimSpace(paths); len(paths) == 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, part := range strings.Split(paths, delim) {
|
|
|
|
part = strings.TrimSpace(part)
|
|
|
|
|
2018-02-15 01:39:20 +00:00
|
|
|
// Remove trailing `/` or `\`, but only the first one.
|
|
|
|
for _, sep := range []string{`/`, `\`} {
|
|
|
|
if strings.HasSuffix(part, sep) {
|
|
|
|
part = strings.TrimSuffix(part, sep)
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
cleaned = append(cleaned, part)
|
2016-05-31 15:48:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return cleaned
|
|
|
|
}
|
|
|
|
|
2018-12-04 17:35:27 +00:00
|
|
|
// repositoryPermissionFetcher is an interface that matches the configuration
|
|
|
|
// object and can be used to fetch repository permissions.
|
|
|
|
type repositoryPermissionFetcher interface {
|
|
|
|
RepositoryPermissions(executable bool) os.FileMode
|
|
|
|
}
|
|
|
|
|
|
|
|
// MkdirAll makes a directory and any intervening directories with the
|
|
|
|
// permissions specified by the core.sharedRepository setting.
|
|
|
|
func MkdirAll(path string, config repositoryPermissionFetcher) error {
|
|
|
|
umask := 0777 & ^config.RepositoryPermissions(true)
|
|
|
|
return doWithUmask(int(umask), func() error {
|
|
|
|
return os.MkdirAll(path, config.RepositoryPermissions(true))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2018-08-30 19:29:44 +00:00
|
|
|
var (
|
|
|
|
// currentUser is a wrapper over user.Current(), but instead uses the
|
|
|
|
// value of os.Getenv("HOME") for the returned *user.User's "HomeDir"
|
|
|
|
// member.
|
|
|
|
currentUser func() (*user.User, error) = func() (*user.User, error) {
|
|
|
|
u, err := user.Current()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
u.HomeDir = os.Getenv("HOME")
|
|
|
|
return u, nil
|
|
|
|
}
|
2018-11-26 20:49:25 +00:00
|
|
|
lookupUser func(who string) (*user.User, error) = user.Lookup
|
|
|
|
lookupConfigHome func() string = func() string {
|
|
|
|
return os.Getenv("XDG_CONFIG_HOME")
|
|
|
|
}
|
2018-08-30 19:29:44 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// ExpandPath returns a copy of path with any references to the current user's
|
|
|
|
// home directory (spelled "~"), or a named user's home directory (spelled
|
|
|
|
// "~user") in the path, sanitized to the calling filesystem's path separator
|
|
|
|
// preference.
|
|
|
|
//
|
|
|
|
// If the "expand" argument is given as true, the resolved path to the named
|
|
|
|
// user's home directory will expanded with filepath.EvalSymlinks.
|
|
|
|
//
|
|
|
|
// If either the current or named user does not have a home directory, an error
|
|
|
|
// will be returned.
|
|
|
|
//
|
|
|
|
// Otherwise, the error returned will be nil, and the string returned will be
|
|
|
|
// the expanded path.
|
|
|
|
func ExpandPath(path string, expand bool) (string, error) {
|
|
|
|
if len(path) == 0 || path[0] != '~' {
|
|
|
|
return path, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var username string
|
|
|
|
if slash := strings.Index(path[1:], "/"); slash > -1 {
|
|
|
|
username = path[1 : slash+1]
|
|
|
|
} else {
|
|
|
|
username = path[1:]
|
|
|
|
}
|
|
|
|
|
|
|
|
var (
|
|
|
|
who *user.User
|
|
|
|
err error
|
|
|
|
)
|
|
|
|
if len(username) == 0 {
|
|
|
|
who, err = currentUser()
|
|
|
|
} else {
|
|
|
|
who, err = lookupUser(username)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return "", errors.Wrapf(err, "could not find user %s", username)
|
|
|
|
}
|
|
|
|
|
|
|
|
homedir := who.HomeDir
|
|
|
|
if expand {
|
|
|
|
homedir, err = filepath.EvalSymlinks(homedir)
|
|
|
|
if err != nil {
|
|
|
|
return "", errors.Wrapf(err, "cannot eval symlinks for %s", homedir)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return filepath.Join(homedir, path[len(username)+1:]), nil
|
|
|
|
}
|
|
|
|
|
2018-11-26 20:49:25 +00:00
|
|
|
// ExpandConfigPath returns a copy of path expanded as with ExpandPath. If the
|
|
|
|
// path is empty, the default path is looked up inside $XDG_CONFIG_HOME, or
|
|
|
|
// ~/.config if that is not set.
|
|
|
|
func ExpandConfigPath(path, defaultPath string) (string, error) {
|
|
|
|
if path != "" {
|
|
|
|
return ExpandPath(path, false)
|
|
|
|
}
|
|
|
|
|
|
|
|
configHome := lookupConfigHome()
|
|
|
|
if configHome != "" {
|
|
|
|
return filepath.Join(configHome, defaultPath), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return ExpandPath(fmt.Sprintf("~/.config/%s", defaultPath), false)
|
|
|
|
}
|
|
|
|
|
2016-07-12 10:43:54 +00:00
|
|
|
// VerifyFileHash reads a file and verifies whether the SHA is correct
|
|
|
|
// Returns an error if there is a problem
|
|
|
|
func VerifyFileHash(oid, path string) error {
|
|
|
|
f, err := os.Open(path)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
h := NewLfsContentHash()
|
|
|
|
_, err = io.Copy(h, f)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
calcOid := hex.EncodeToString(h.Sum(nil))
|
|
|
|
if calcOid != oid {
|
2019-10-22 07:15:05 +00:00
|
|
|
return fmt.Errorf("file %q has an invalid hash %s, expected %s", path, calcOid, oid)
|
2016-07-12 10:43:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2016-10-31 12:45:12 +00:00
|
|
|
|
2016-11-23 20:33:07 +00:00
|
|
|
// FastWalkCallback is the signature for the callback given to FastWalkGitRepo()
|
2016-11-23 20:28:11 +00:00
|
|
|
type FastWalkCallback func(parentDir string, info os.FileInfo, err error)
|
2016-10-31 12:46:49 +00:00
|
|
|
|
2019-09-17 19:20:04 +00:00
|
|
|
// FastWalkDir is a more optimal implementation of filepath.Walk for a Git
|
2016-11-23 20:33:07 +00:00
|
|
|
// repo. The callback guaranteed to be called sequentially. The function returns
|
|
|
|
// once all files and errors have triggered callbacks.
|
2016-11-08 09:58:07 +00:00
|
|
|
// It differs in the following ways:
|
|
|
|
// * Uses goroutines to parallelise large dirs and descent into subdirs
|
|
|
|
// * Does not provide sorted output; parents will always be before children but
|
2016-11-23 20:33:07 +00:00
|
|
|
// there are no other guarantees. Use parentDir argument in the callback to
|
|
|
|
// determine absolute path rather than tracking it yourself
|
2016-11-08 09:58:07 +00:00
|
|
|
// * Automatically ignores any .git directories
|
2017-05-17 23:00:51 +00:00
|
|
|
//
|
|
|
|
// rootDir - Absolute path to the top of the repository working directory
|
2019-09-17 19:20:04 +00:00
|
|
|
func FastWalkDir(rootDir string, cb FastWalkCallback) {
|
|
|
|
fastWalkCallback(fastWalkWithExcludeFiles(rootDir), cb)
|
2018-08-20 13:33:56 +00:00
|
|
|
}
|
|
|
|
|
2018-08-20 13:00:31 +00:00
|
|
|
// fastWalkCallback calls the FastWalkCallback "cb" for all files found by the
|
|
|
|
// given *fastWalker, "walker".
|
|
|
|
func fastWalkCallback(walker *fastWalker, cb FastWalkCallback) {
|
2017-10-17 15:06:30 +00:00
|
|
|
for file := range walker.ch {
|
2016-11-29 15:14:35 +00:00
|
|
|
cb(file.ParentDir, file.Info, file.Err)
|
2016-11-22 22:41:24 +00:00
|
|
|
}
|
2016-10-31 12:46:49 +00:00
|
|
|
}
|
|
|
|
|
2016-11-23 20:33:07 +00:00
|
|
|
// Returned from FastWalk with parent directory context
|
|
|
|
// This is needed because FastWalk can provide paths out of order so the
|
|
|
|
// parent dir cannot be implied
|
|
|
|
type fastWalkInfo struct {
|
|
|
|
ParentDir string
|
|
|
|
Info os.FileInfo
|
2016-11-29 15:14:35 +00:00
|
|
|
Err error
|
2016-11-23 20:33:07 +00:00
|
|
|
}
|
|
|
|
|
2017-10-17 15:06:30 +00:00
|
|
|
type fastWalker struct {
|
2019-10-02 19:16:59 +00:00
|
|
|
rootDir string
|
|
|
|
ch chan fastWalkInfo
|
|
|
|
limit int32
|
|
|
|
cur *int32
|
|
|
|
wg *sync.WaitGroup
|
2017-10-17 15:06:30 +00:00
|
|
|
}
|
|
|
|
|
2016-11-22 22:28:16 +00:00
|
|
|
// fastWalkWithExcludeFiles walks the contents of a dir, respecting
|
2019-09-17 19:20:04 +00:00
|
|
|
// include/exclude patterns.
|
2017-05-17 23:00:51 +00:00
|
|
|
//
|
|
|
|
// rootDir - Absolute path to the top of the repository working directory
|
2019-09-17 19:20:04 +00:00
|
|
|
func fastWalkWithExcludeFiles(rootDir string) *fastWalker {
|
2017-10-17 15:06:30 +00:00
|
|
|
excludePaths := []filepathfilter.Pattern{
|
|
|
|
filepathfilter.NewPattern(".git"),
|
2018-02-15 01:39:20 +00:00
|
|
|
filepathfilter.NewPattern("**/.git"),
|
2017-10-17 15:06:30 +00:00
|
|
|
}
|
2016-10-31 15:39:52 +00:00
|
|
|
|
2017-10-17 15:14:48 +00:00
|
|
|
limit, _ := strconv.Atoi(os.Getenv("LFS_FASTWALK_LIMIT"))
|
|
|
|
if limit < 1 {
|
|
|
|
limit = runtime.GOMAXPROCS(-1) * 20
|
|
|
|
}
|
|
|
|
|
|
|
|
c := int32(0)
|
2017-10-17 15:06:30 +00:00
|
|
|
w := &fastWalker{
|
2019-10-02 19:16:59 +00:00
|
|
|
rootDir: rootDir,
|
|
|
|
limit: int32(limit),
|
|
|
|
cur: &c,
|
|
|
|
ch: make(chan fastWalkInfo, 256),
|
|
|
|
wg: &sync.WaitGroup{},
|
2016-10-31 15:39:52 +00:00
|
|
|
}
|
|
|
|
|
2017-10-17 15:06:30 +00:00
|
|
|
go func() {
|
|
|
|
dirFi, err := os.Stat(w.rootDir)
|
|
|
|
if err != nil {
|
|
|
|
w.ch <- fastWalkInfo{Err: err}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
w.Walk(true, "", dirFi, excludePaths)
|
|
|
|
w.Wait()
|
|
|
|
}()
|
|
|
|
return w
|
2016-10-31 15:39:52 +00:00
|
|
|
}
|
|
|
|
|
2019-09-17 19:20:04 +00:00
|
|
|
// Walk is the main recursive implementation of fast walk. Sends the file/dir
|
|
|
|
// and any contents to the channel so long as it passes the include/exclude
|
|
|
|
// filter. Increments waitg.Add(1) for each new goroutine launched internally
|
2017-05-17 23:00:51 +00:00
|
|
|
//
|
|
|
|
// workDir - Relative path inside the repository
|
2017-10-17 15:06:30 +00:00
|
|
|
func (w *fastWalker) Walk(isRoot bool, workDir string, itemFi os.FileInfo,
|
|
|
|
excludePaths []filepathfilter.Pattern) {
|
2016-10-31 12:46:49 +00:00
|
|
|
|
2017-05-17 23:00:51 +00:00
|
|
|
var fullPath string // Absolute path to the current file or dir
|
|
|
|
var parentWorkDir string // Absolute path to the workDir inside the repository
|
|
|
|
if isRoot {
|
2017-10-17 15:06:30 +00:00
|
|
|
fullPath = w.rootDir
|
2017-05-17 23:00:51 +00:00
|
|
|
} else {
|
2018-02-15 01:39:20 +00:00
|
|
|
parentWorkDir = join(w.rootDir, workDir)
|
|
|
|
fullPath = join(parentWorkDir, itemFi.Name())
|
2017-05-17 23:00:51 +00:00
|
|
|
}
|
2016-10-31 15:39:52 +00:00
|
|
|
|
2019-06-14 17:15:16 +00:00
|
|
|
if !isRoot && itemFi.IsDir() {
|
|
|
|
// If this directory has a .git directory or file in it, then
|
|
|
|
// this is a submodule, and we should not recurse into it.
|
|
|
|
_, err := os.Stat(filepath.Join(fullPath, ".git"))
|
|
|
|
if err == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-15 01:39:20 +00:00
|
|
|
workPath := join(workDir, itemFi.Name())
|
2017-05-17 23:00:51 +00:00
|
|
|
if !filepathfilter.NewFromPatterns(nil, excludePaths).Allows(workPath) {
|
2016-10-31 12:46:49 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-10-17 15:06:30 +00:00
|
|
|
w.ch <- fastWalkInfo{ParentDir: parentWorkDir, Info: itemFi}
|
2016-10-31 12:46:49 +00:00
|
|
|
|
|
|
|
if !itemFi.IsDir() {
|
|
|
|
// Nothing more to do if this is not a dir
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-05-17 23:00:51 +00:00
|
|
|
var childWorkDir string
|
|
|
|
if !isRoot {
|
2018-02-15 01:39:20 +00:00
|
|
|
childWorkDir = join(workDir, itemFi.Name())
|
2017-05-17 23:00:51 +00:00
|
|
|
}
|
|
|
|
|
2016-10-31 12:46:49 +00:00
|
|
|
// The absolute optimal way to scan would be File.Readdirnames but we
|
|
|
|
// still need the Stat() to know whether something is a dir, so use
|
|
|
|
// File.Readdir instead. Means we can provide os.FileInfo to callers like
|
|
|
|
// filepath.Walk as a bonus.
|
|
|
|
df, err := os.Open(fullPath)
|
|
|
|
if err != nil {
|
2017-10-17 15:06:30 +00:00
|
|
|
w.ch <- fastWalkInfo{Err: err}
|
2016-10-31 12:46:49 +00:00
|
|
|
return
|
|
|
|
}
|
2016-11-21 23:53:19 +00:00
|
|
|
|
2016-11-08 09:57:39 +00:00
|
|
|
// The number of items in a dir we process in each goroutine
|
2016-10-31 15:39:52 +00:00
|
|
|
jobSize := 100
|
2016-10-31 12:46:49 +00:00
|
|
|
for children, err := df.Readdir(jobSize); err == nil; children, err = df.Readdir(jobSize) {
|
2016-10-31 15:39:52 +00:00
|
|
|
// Parallelise all dirs, and chop large dirs into batches
|
2017-10-17 15:06:30 +00:00
|
|
|
w.walk(children, func(subitems []os.FileInfo) {
|
2016-10-31 15:39:52 +00:00
|
|
|
for _, childFi := range subitems {
|
2017-10-17 15:06:30 +00:00
|
|
|
w.Walk(false, childWorkDir, childFi, excludePaths)
|
2016-10-31 12:46:49 +00:00
|
|
|
}
|
2017-10-17 15:06:30 +00:00
|
|
|
})
|
2016-10-31 12:46:49 +00:00
|
|
|
}
|
2017-10-17 15:06:30 +00:00
|
|
|
|
|
|
|
df.Close()
|
2016-10-31 15:39:52 +00:00
|
|
|
if err != nil && err != io.EOF {
|
2017-10-17 15:06:30 +00:00
|
|
|
w.ch <- fastWalkInfo{Err: err}
|
2016-10-31 12:46:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-17 15:06:30 +00:00
|
|
|
func (w *fastWalker) walk(children []os.FileInfo, fn func([]os.FileInfo)) {
|
2017-10-17 15:14:48 +00:00
|
|
|
cur := atomic.AddInt32(w.cur, 1)
|
|
|
|
if cur > w.limit {
|
|
|
|
fn(children)
|
|
|
|
atomic.AddInt32(w.cur, -1)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-10-17 15:06:30 +00:00
|
|
|
w.wg.Add(1)
|
|
|
|
go func() {
|
|
|
|
fn(children)
|
|
|
|
w.wg.Done()
|
2017-10-17 15:14:48 +00:00
|
|
|
atomic.AddInt32(w.cur, -1)
|
2017-10-17 15:06:30 +00:00
|
|
|
}()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *fastWalker) Wait() {
|
|
|
|
w.wg.Wait()
|
|
|
|
close(w.ch)
|
|
|
|
}
|
|
|
|
|
2018-02-15 01:39:20 +00:00
|
|
|
func join(paths ...string) string {
|
|
|
|
ne := make([]string, 0, len(paths))
|
|
|
|
|
|
|
|
for _, p := range paths {
|
|
|
|
if len(p) > 0 {
|
|
|
|
ne = append(ne, p)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return strings.Join(ne, "/")
|
|
|
|
}
|
|
|
|
|
2016-12-19 12:39:51 +00:00
|
|
|
// SetFileWriteFlag changes write permissions on a file
|
|
|
|
// Used to make a file read-only or not. When writeEnabled = false, the write
|
|
|
|
// bit is removed for all roles. When writeEnabled = true, the behaviour is
|
|
|
|
// different per platform:
|
|
|
|
// On Mac & Linux, the write bit is set only on the owner as per default umask.
|
|
|
|
// All other bits are unaffected.
|
|
|
|
// On Windows, all the write bits are set since Windows doesn't support Unix permissions.
|
|
|
|
func SetFileWriteFlag(path string, writeEnabled bool) error {
|
|
|
|
stat, err := os.Stat(path)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
mode := uint32(stat.Mode())
|
|
|
|
|
|
|
|
if (writeEnabled && (mode&0200) > 0) ||
|
|
|
|
(!writeEnabled && (mode&0222) == 0) {
|
|
|
|
// no change needed
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if writeEnabled {
|
|
|
|
mode = mode | 0200 // set owner write only
|
|
|
|
// Go's own Chmod makes Windows set all though
|
|
|
|
} else {
|
|
|
|
mode = mode &^ 0222 // disable all write
|
|
|
|
}
|
|
|
|
return os.Chmod(path, os.FileMode(mode))
|
|
|
|
}
|
2018-12-04 17:27:25 +00:00
|
|
|
|
2019-09-23 19:33:31 +00:00
|
|
|
// TempFile creates a temporary file in specified directory with proper permissions for the repository.
|
|
|
|
// On success, it returns an open, non-nil *os.File, and the caller is responsible
|
|
|
|
// for closing and/or removing it. On failure, the temporary file is
|
|
|
|
// automatically cleaned up and an error returned.
|
|
|
|
//
|
|
|
|
// This function is designed to handle only temporary files that will be renamed
|
|
|
|
// into place later somewhere within the Git repository.
|
|
|
|
func TempFile(dir, pattern string, cfg repositoryPermissionFetcher) (*os.File, error) {
|
|
|
|
tmp, err := ioutil.TempFile(dir, pattern)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
perms := cfg.RepositoryPermissions(false)
|
|
|
|
err = os.Chmod(tmp.Name(), perms)
|
|
|
|
if err != nil {
|
|
|
|
tmp.Close()
|
|
|
|
os.Remove(tmp.Name())
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return tmp, nil
|
|
|
|
}
|
|
|
|
|
2018-12-04 17:27:25 +00:00
|
|
|
// ExecutablePermissions takes a set of Unix permissions (which may or may not
|
|
|
|
// have the executable bits set) and maps them into a set of permissions in
|
|
|
|
// which the executable bits are set, using the same technique as Git does.
|
|
|
|
func ExecutablePermissions(perms os.FileMode) os.FileMode {
|
|
|
|
// Copy read bits to executable bits.
|
|
|
|
return perms | ((perms & 0444) >> 2)
|
|
|
|
}
|