Add the ability to change lockable attribute to git lfs track
This commit is contained in:
parent
08e3e5b40e
commit
98919456b8
@ -2,19 +2,18 @@ package commands
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rubyist/tracerx"
|
|
||||||
|
|
||||||
"github.com/git-lfs/git-lfs/config"
|
"github.com/git-lfs/git-lfs/config"
|
||||||
"github.com/git-lfs/git-lfs/git"
|
"github.com/git-lfs/git-lfs/git"
|
||||||
"github.com/git-lfs/git-lfs/lfs"
|
"github.com/git-lfs/git-lfs/lfs"
|
||||||
"github.com/git-lfs/git-lfs/tools"
|
"github.com/git-lfs/git-lfs/locking"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -23,6 +22,8 @@ var (
|
|||||||
".git", ".lfs",
|
".git", ".lfs",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trackLockableFlag bool
|
||||||
|
trackNotLockableFlag bool
|
||||||
trackVerboseLoggingFlag bool
|
trackVerboseLoggingFlag bool
|
||||||
trackDryRunFlag bool
|
trackDryRunFlag bool
|
||||||
)
|
)
|
||||||
@ -41,50 +42,110 @@ func trackCommand(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lfs.InstallHooks(false)
|
lfs.InstallHooks(false)
|
||||||
knownPatterns := findPatterns()
|
knownPatterns := git.GetAttributePaths(config.LocalWorkingDir, config.LocalGitDir)
|
||||||
|
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
Print("Listing tracked patterns")
|
Print("Listing tracked patterns")
|
||||||
for _, t := range knownPatterns {
|
for _, t := range knownPatterns {
|
||||||
Print(" %s (%s)", t.Pattern, t.Source)
|
if t.Lockable {
|
||||||
|
Print(" %s [lockable] (%s)", t.Path, t.Source)
|
||||||
|
} else {
|
||||||
|
Print(" %s (%s)", t.Path, t.Source)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
addTrailingLinebreak := needsTrailingLinebreak(".gitattributes")
|
|
||||||
attributesFile, err := os.OpenFile(".gitattributes", os.O_RDWR|os.O_APPEND|os.O_CREATE, 0660)
|
|
||||||
if err != nil {
|
|
||||||
Print("Error opening .gitattributes file")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer attributesFile.Close()
|
|
||||||
|
|
||||||
if addTrailingLinebreak {
|
|
||||||
if _, werr := attributesFile.WriteString("\n"); werr != nil {
|
|
||||||
Print("Error writing to .gitattributes")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wd, _ := os.Getwd()
|
wd, _ := os.Getwd()
|
||||||
relpath, err := filepath.Rel(config.LocalWorkingDir, wd)
|
relpath, err := filepath.Rel(config.LocalWorkingDir, wd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Exit("Current directory %q outside of git working directory %q.", wd, config.LocalWorkingDir)
|
Exit("Current directory %q outside of git working directory %q.", wd, config.LocalWorkingDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
changedAttribLines := make(map[string]string)
|
||||||
|
var readOnlyPatterns []string
|
||||||
|
var writeablePatterns []string
|
||||||
ArgsLoop:
|
ArgsLoop:
|
||||||
for _, unsanitizedPattern := range args {
|
for _, unsanitizedPattern := range args {
|
||||||
pattern := cleanRootPath(unsanitizedPattern)
|
pattern := cleanRootPath(unsanitizedPattern)
|
||||||
for _, known := range knownPatterns {
|
for _, known := range knownPatterns {
|
||||||
if known.Pattern == filepath.Join(relpath, pattern) {
|
if known.Path == filepath.Join(relpath, pattern) &&
|
||||||
|
((trackLockableFlag && known.Lockable) || // enabling lockable & already lockable (no change)
|
||||||
|
(trackNotLockableFlag && !known.Lockable) || // disabling lockable & not lockable (no change)
|
||||||
|
(!trackLockableFlag && !trackNotLockableFlag)) { // leave lockable as-is in all cases
|
||||||
Print("%s already supported", pattern)
|
Print("%s already supported", pattern)
|
||||||
continue ArgsLoop
|
continue ArgsLoop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure any existing git tracked files have their timestamp updated
|
// Generate the new / changed attrib line for merging
|
||||||
// so they will now show as modifed
|
encodedArg := strings.Replace(pattern, " ", "[[:space:]]", -1)
|
||||||
// note this is relative to current dir which is how we write .gitattributes
|
lockableArg := ""
|
||||||
// deliberately not done in parallel as a chan because we'll be marking modified
|
if trackLockableFlag { // no need to test trackNotLockableFlag, if we got here we're disabling
|
||||||
|
lockableArg = " " + git.LockableAttrib
|
||||||
|
}
|
||||||
|
|
||||||
|
changedAttribLines[pattern] = fmt.Sprintf("%s filter=lfs diff=lfs merge=lfs -text%v\n", encodedArg, lockableArg)
|
||||||
|
|
||||||
|
if trackLockableFlag {
|
||||||
|
readOnlyPatterns = append(readOnlyPatterns, pattern)
|
||||||
|
} else {
|
||||||
|
writeablePatterns = append(writeablePatterns, pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
Print("Tracking %s", pattern)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now read the whole local attributes file and iterate over the contents,
|
||||||
|
// replacing any lines where the values have changed, and appending new lines
|
||||||
|
// change this:
|
||||||
|
|
||||||
|
attribContents, err := ioutil.ReadFile(".gitattributes")
|
||||||
|
// it's fine for file to not exist
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
Print("Error reading .gitattributes file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Re-generate the file with merge of old contents and new (to deal with changes)
|
||||||
|
attributesFile, err := os.OpenFile(".gitattributes", os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0660)
|
||||||
|
if err != nil {
|
||||||
|
Print("Error opening .gitattributes file")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer attributesFile.Close()
|
||||||
|
|
||||||
|
if len(attribContents) > 0 {
|
||||||
|
scanner := bufio.NewScanner(bytes.NewReader(attribContents))
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
pattern := fields[0]
|
||||||
|
if newline, ok := changedAttribLines[pattern]; ok {
|
||||||
|
// Replace this line (newline already embedded)
|
||||||
|
attributesFile.WriteString(newline)
|
||||||
|
// Remove from map so we know we don't have to add it to the end
|
||||||
|
delete(changedAttribLines, pattern)
|
||||||
|
} else {
|
||||||
|
// Write line unchanged (replace newline)
|
||||||
|
attributesFile.WriteString(line + "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our method of writing also made sure there's always a newline at end
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any items left in the map, write new lines at the end of the file
|
||||||
|
// Note this is only new patterns, not ones which changed locking flags
|
||||||
|
for pattern, newline := range changedAttribLines {
|
||||||
|
// Newline already embedded
|
||||||
|
attributesFile.WriteString(newline)
|
||||||
|
|
||||||
|
// Also, for any new patterns we've added, make sure any existing git
|
||||||
|
// tracked files have their timestamp updated so they will now show as
|
||||||
|
// modifed note this is relative to current dir which is how we write
|
||||||
|
// .gitattributes deliberately not done in parallel as a chan because
|
||||||
|
// we'll be marking modified
|
||||||
//
|
//
|
||||||
// NOTE: `git ls-files` does not do well with leading slashes.
|
// NOTE: `git ls-files` does not do well with leading slashes.
|
||||||
// Since all `git-lfs track` calls are relative to the root of
|
// Since all `git-lfs track` calls are relative to the root of
|
||||||
@ -114,16 +175,6 @@ ArgsLoop:
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !trackDryRunFlag {
|
|
||||||
encodedArg := strings.Replace(pattern, " ", "[[:space:]]", -1)
|
|
||||||
_, err := attributesFile.WriteString(fmt.Sprintf("%s filter=lfs diff=lfs merge=lfs -text\n", encodedArg))
|
|
||||||
if err != nil {
|
|
||||||
Print("Error adding pattern %s", pattern)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Print("Tracking %s", pattern)
|
|
||||||
|
|
||||||
for _, f := range gittracked {
|
for _, f := range gittracked {
|
||||||
if trackVerboseLoggingFlag || trackDryRunFlag {
|
if trackVerboseLoggingFlag || trackDryRunFlag {
|
||||||
Print("Git LFS: touching %s", f)
|
Print("Git LFS: touching %s", f)
|
||||||
@ -139,85 +190,16 @@ ArgsLoop:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
// now flip read-only mode based on lockable / not lockable changes
|
||||||
|
lockClient, err := locking.NewClient(cfg)
|
||||||
type mediaPattern struct {
|
|
||||||
Pattern string
|
|
||||||
Source string
|
|
||||||
}
|
|
||||||
|
|
||||||
func findPatterns() []mediaPattern {
|
|
||||||
var patterns []mediaPattern
|
|
||||||
|
|
||||||
for _, path := range findAttributeFiles() {
|
|
||||||
attributes, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(attributes)
|
|
||||||
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
if strings.Contains(line, "filter=lfs") {
|
|
||||||
fields := strings.Fields(line)
|
|
||||||
relfile, _ := filepath.Rel(config.LocalWorkingDir, path)
|
|
||||||
pattern := fields[0]
|
|
||||||
if reldir := filepath.Dir(relfile); len(reldir) > 0 {
|
|
||||||
pattern = filepath.Join(reldir, pattern)
|
|
||||||
}
|
|
||||||
|
|
||||||
patterns = append(patterns, mediaPattern{Pattern: pattern, Source: relfile})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return patterns
|
|
||||||
}
|
|
||||||
|
|
||||||
func findAttributeFiles() []string {
|
|
||||||
var paths []string
|
|
||||||
|
|
||||||
repoAttributes := filepath.Join(config.LocalGitDir, "info", "attributes")
|
|
||||||
if info, err := os.Stat(repoAttributes); err == nil && !info.IsDir() {
|
|
||||||
paths = append(paths, repoAttributes)
|
|
||||||
}
|
|
||||||
|
|
||||||
tools.FastWalkGitRepo(config.LocalWorkingDir, func(parentDir string, info os.FileInfo, err error) {
|
|
||||||
if err != nil {
|
|
||||||
tracerx.Printf("Error finding .gitattributes: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.IsDir() || info.Name() != ".gitattributes" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
paths = append(paths, filepath.Join(parentDir, info.Name()))
|
|
||||||
})
|
|
||||||
|
|
||||||
return paths
|
|
||||||
}
|
|
||||||
|
|
||||||
func needsTrailingLinebreak(filename string) bool {
|
|
||||||
file, err := os.Open(filename)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
LoggedError(err, "Error changing lockable file permissions")
|
||||||
}
|
} else {
|
||||||
defer file.Close()
|
err = lockClient.FixFileWriteFlagsInDir(relpath, readOnlyPatterns, writeablePatterns, true)
|
||||||
|
if err != nil {
|
||||||
buf := make([]byte, 16384)
|
LoggedError(err, "Error changing lockable file permissions")
|
||||||
bytesRead := 0
|
|
||||||
for {
|
|
||||||
n, err := file.Read(buf)
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
} else if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
bytesRead = n
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return !strings.HasSuffix(string(buf[0:bytesRead]), "\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// blocklistItem returns the name of the blocklist item preventing the given
|
// blocklistItem returns the name of the blocklist item preventing the given
|
||||||
@ -236,6 +218,8 @@ func blocklistItem(name string) string {
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RegisterCommand("track", trackCommand, func(cmd *cobra.Command) {
|
RegisterCommand("track", trackCommand, func(cmd *cobra.Command) {
|
||||||
|
cmd.Flags().BoolVarP(&trackLockableFlag, "lockable", "l", false, "make pattern lockable, i.e. read-only unless locked")
|
||||||
|
cmd.Flags().BoolVarP(&trackNotLockableFlag, "not-lockable", "", false, "remove lockable attribute from pattern")
|
||||||
cmd.Flags().BoolVarP(&trackVerboseLoggingFlag, "verbose", "v", false, "log which files are being tracked and modified")
|
cmd.Flags().BoolVarP(&trackVerboseLoggingFlag, "verbose", "v", false, "log which files are being tracked and modified")
|
||||||
cmd.Flags().BoolVarP(&trackDryRunFlag, "dry-run", "d", false, "preview results of running `git lfs track`")
|
cmd.Flags().BoolVarP(&trackDryRunFlag, "dry-run", "d", false, "preview results of running `git lfs track`")
|
||||||
})
|
})
|
||||||
|
@ -31,6 +31,14 @@ to match paths.
|
|||||||
|
|
||||||
Disabled by default.
|
Disabled by default.
|
||||||
|
|
||||||
|
* `--lockable` `-l`
|
||||||
|
Make the paths 'lockable', meaning they should be locked to edit them, and
|
||||||
|
will be made read-only in the working copy when not locked.
|
||||||
|
|
||||||
|
* `--not-lockable`
|
||||||
|
Remove the lockable flag from the paths so they are no longer read-only unless
|
||||||
|
locked.
|
||||||
|
|
||||||
## EXAMPLES
|
## EXAMPLES
|
||||||
|
|
||||||
* List the patterns that Git LFS is currently tracking:
|
* List the patterns that Git LFS is currently tracking:
|
||||||
@ -41,6 +49,10 @@ to match paths.
|
|||||||
|
|
||||||
`git lfs track '*.gif'`
|
`git lfs track '*.gif'`
|
||||||
|
|
||||||
|
* Configure Git LFS to track PSD files and make them read-only unless locked:
|
||||||
|
|
||||||
|
`git lfs track --lockable '*.psd'`
|
||||||
|
|
||||||
## SEE ALSO
|
## SEE ALSO
|
||||||
|
|
||||||
git-lfs-untrack(1), git-lfs-install(1), gitattributes(5).
|
git-lfs-untrack(1), git-lfs-install(1), gitattributes(5).
|
||||||
|
@ -292,3 +292,136 @@ begin_test "track blocklisted files with glob"
|
|||||||
grep "Pattern .git\* matches forbidden file" track.log
|
grep "Pattern .git\* matches forbidden file" track.log
|
||||||
)
|
)
|
||||||
end_test
|
end_test
|
||||||
|
|
||||||
|
begin_test "track lockable"
|
||||||
|
(
|
||||||
|
set -e
|
||||||
|
|
||||||
|
repo="track_lockable"
|
||||||
|
mkdir "$repo"
|
||||||
|
cd "$repo"
|
||||||
|
git init
|
||||||
|
|
||||||
|
# track *.jpg once, lockable
|
||||||
|
git lfs track --lockable "*.jpg" | grep "Tracking \*.jpg"
|
||||||
|
numjpg=$(grep -e "\*.jpg.*lockable" .gitattributes | wc -l)
|
||||||
|
if [ "$(printf "%d" "$numjpg")" != "1" ]; then
|
||||||
|
echo "wrong number of lockable jpgs"
|
||||||
|
cat .gitattributes
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# track *.jpg again, don't change anything. Should retain lockable
|
||||||
|
git lfs track "*.jpg" | grep "*.jpg already supported"
|
||||||
|
numjpg=$(grep -e "\*.jpg.*lockable" .gitattributes | wc -l)
|
||||||
|
if [ "$(printf "%d" "$numjpg")" != "1" ]; then
|
||||||
|
echo "wrong number of lockable jpgs"
|
||||||
|
cat .gitattributes
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# track *.png once, not lockable yet
|
||||||
|
git lfs track "*.png" | grep "Tracking \*.png"
|
||||||
|
numpng=$(grep -e "\*.png" .gitattributes | wc -l)
|
||||||
|
numpnglockable=$(grep -e "\*.png.*lockable" .gitattributes | wc -l)
|
||||||
|
if [ "$(printf "%d" "$numpng")" != "1" ]; then
|
||||||
|
echo "wrong number of pngs"
|
||||||
|
cat .gitattributes
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$(printf "%d" "$numpnglockable")" != "0" ]; then
|
||||||
|
echo "wrong number of lockable pngs"
|
||||||
|
cat .gitattributes
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# track png again, enable lockable, should replace
|
||||||
|
git lfs track --lockable "*.png" | grep "Tracking \*.png"
|
||||||
|
numpng=$(grep -e "\*.png" .gitattributes | wc -l)
|
||||||
|
numpnglockable=$(grep -e "\*.png.*lockable" .gitattributes | wc -l)
|
||||||
|
if [ "$(printf "%d" "$numpng")" != "1" ]; then
|
||||||
|
echo "wrong number of pngs"
|
||||||
|
cat .gitattributes
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$(printf "%d" "$numpnglockable")" != "1" ]; then
|
||||||
|
echo "wrong number of lockable pngs"
|
||||||
|
cat .gitattributes
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# track png again, disable lockable, should replace
|
||||||
|
git lfs track --not-lockable "*.png" | grep "Tracking \*.png"
|
||||||
|
numpng=$(grep -e "\*.png" .gitattributes | wc -l)
|
||||||
|
numpnglockable=$(grep -e "\*.png.*lockable" .gitattributes | wc -l)
|
||||||
|
if [ "$(printf "%d" "$numpng")" != "1" ]; then
|
||||||
|
echo "wrong number of pngs"
|
||||||
|
cat .gitattributes
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$(printf "%d" "$numpnglockable")" != "0" ]; then
|
||||||
|
echo "wrong number of lockable pngs"
|
||||||
|
cat .gitattributes
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# check output reflects lockable
|
||||||
|
out=$(git lfs track)
|
||||||
|
echo "$out" | grep "Listing tracked patterns"
|
||||||
|
echo "$out" | grep "*.jpg \[lockable\] (.gitattributes)"
|
||||||
|
echo "$out" | grep "*.png (.gitattributes)"
|
||||||
|
)
|
||||||
|
end_test
|
||||||
|
|
||||||
|
begin_test "track lockable read-only/read-write"
|
||||||
|
(
|
||||||
|
set -e
|
||||||
|
|
||||||
|
repo="track_lockable_ro_rw"
|
||||||
|
mkdir "$repo"
|
||||||
|
cd "$repo"
|
||||||
|
git init
|
||||||
|
|
||||||
|
echo "blah blah" > test.bin
|
||||||
|
echo "foo bar" > test.dat
|
||||||
|
mkdir subfolder
|
||||||
|
echo "sub blah blah" > subfolder/test.bin
|
||||||
|
echo "sub foo bar" > subfolder/test.dat
|
||||||
|
# should start writeable
|
||||||
|
[ -w test.bin ]
|
||||||
|
[ -w test.dat ]
|
||||||
|
[ -w subfolder/test.bin ]
|
||||||
|
[ -w subfolder/test.dat ]
|
||||||
|
|
||||||
|
# track *.bin, not lockable yet
|
||||||
|
git lfs track "*.bin" | grep "Tracking \*.bin"
|
||||||
|
# track *.dat, lockable immediately
|
||||||
|
git lfs track --lockable "*.dat" | grep "Tracking \*.dat"
|
||||||
|
|
||||||
|
# bin should remain writeable, dat should have been made read-only
|
||||||
|
[ -w test.bin ]
|
||||||
|
[ ! -w test.dat ]
|
||||||
|
[ -w subfolder/test.bin ]
|
||||||
|
[ ! -w subfolder/test.dat ]
|
||||||
|
|
||||||
|
git add .gitattributes test.bin test.dat
|
||||||
|
git commit -m "First commit"
|
||||||
|
|
||||||
|
# bin should still be writeable
|
||||||
|
[ -w test.bin ]
|
||||||
|
[ -w subfolder/test.bin ]
|
||||||
|
# now make bin lockable
|
||||||
|
git lfs track --lockable "*.bin" | grep "Tracking \*.bin"
|
||||||
|
# bin should now be read-only
|
||||||
|
[ ! -w test.bin ]
|
||||||
|
[ ! -w subfolder/test.bin ]
|
||||||
|
|
||||||
|
# remove lockable again
|
||||||
|
git lfs track --not-lockable "*.bin" | grep "Tracking \*.bin"
|
||||||
|
# bin should now be writeable again
|
||||||
|
[ -w test.bin ]
|
||||||
|
[ -w subfolder/test.bin ]
|
||||||
|
|
||||||
|
)
|
||||||
|
end_test
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user