git-lfs/commands/command_lock.go
brian m. carlson 95d1c1f737
commands: canonicalize paths properly on Windows
When we form a lock path, we take the working directory and resolve all
symlinks in it, and then append the name of the file specified on the
command line.  However, the working directory we get need not be
canonicalized for the system: it may contain lowercase drive paths (on
Windows) or components that differ in case or normalization from the
canonical form written on the file system.

We then made the path relative to the root of the repository by using
the strings module to pull of the prefix of the Git repository, which
will always be canonicalized.  If the working directory was
noncanonical, the paths would end up just concatenated together, leading
to failures down the line.  On Windows, this could lead to multiple
drive components, and hence a syntax error when trying to use the path.

Use the filepath.Rel function that's built into Go to help us make the
path relative in a way that is appropriate for the operating system.
Convert the path to slashes so that we store paths using slashes and
expose only slashes to the remote API.
2018-09-21 17:52:53 +00:00

112 lines
2.6 KiB
Go

package commands
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/git-lfs/git-lfs/errors"
"github.com/git-lfs/git-lfs/git"
"github.com/spf13/cobra"
)
var (
lockRemote string
lockRemoteHelp = "specify which remote to use when interacting with locks"
)
func lockCommand(cmd *cobra.Command, args []string) {
if len(args) == 0 {
Print("Usage: git lfs lock <path>")
return
}
path, err := lockPath(args[0])
if err != nil {
Exit(err.Error())
}
if len(lockRemote) > 0 {
cfg.SetRemote(lockRemote)
}
refUpdate := git.NewRefUpdate(cfg.Git, cfg.PushRemote(), cfg.CurrentRef(), nil)
lockClient := newLockClient()
lockClient.RemoteRef = refUpdate.Right()
defer lockClient.Close()
lock, err := lockClient.LockFile(path)
if err != nil {
Exit("Lock failed: %v", errors.Cause(err))
}
if locksCmdFlags.JSON {
if err := json.NewEncoder(os.Stdout).Encode(lock); err != nil {
Error(err.Error())
}
return
}
Print("Locked %s", path)
}
// lockPaths relativizes the given filepath such that it is relative to the root
// path of the repository it is contained within, taking into account the
// working directory of the caller.
//
// lockPaths also respects different filesystem directory separators, so that a
// Windows path of "\foo\bar" will be normalized to "foo/bar".
//
// If the root directory, working directory, or file cannot be
// determined/opened, an error will be returned. If the file in question is
// actually a directory, an error will be returned. Otherwise, the cleaned path
// will be returned.
//
// For example:
// - Working directory: /code/foo/bar/
// - Repository root: /code/foo/
// - File to lock: ./baz
// - Resolved path bar/baz
func lockPath(file string) (string, error) {
repo, err := git.RootDir()
if err != nil {
return "", err
}
wd, err := os.Getwd()
if err != nil {
return "", err
}
wd, err = filepath.EvalSymlinks(wd)
if err != nil {
return "", errors.Wrapf(err,
"could not follow symlinks for %s", wd)
}
abs := filepath.Join(wd, file)
path, err := filepath.Rel(repo, abs)
if err != nil {
return "", err
}
path = filepath.ToSlash(path)
if stat, err := os.Stat(abs); err != nil {
return "", err
} else {
if stat.IsDir() {
return path, fmt.Errorf("lfs: cannot lock directory: %s", file)
}
return filepath.ToSlash(path), nil
}
}
func init() {
RegisterCommand("lock", lockCommand, func(cmd *cobra.Command) {
cmd.Flags().StringVarP(&lockRemote, "remote", "r", "", lockRemoteHelp)
cmd.Flags().BoolVarP(&locksCmdFlags.JSON, "json", "", false, "print output in json")
})
}