diff --git a/commands/command_unlock.go b/commands/command_unlock.go index 95dc1822..b32d511d 100644 --- a/commands/command_unlock.go +++ b/commands/command_unlock.go @@ -4,6 +4,8 @@ import ( "encoding/json" "os" + "github.com/git-lfs/git-lfs/git" + "github.com/git-lfs/git-lfs/locking" "github.com/spf13/cobra" ) @@ -31,11 +33,16 @@ func unlockCommand(cmd *cobra.Command, args []string) { Exit("Unable to determine path: %v", err.Error()) } + unlockCheckFileStatus(path) + err = lockClient.UnlockFile(path, unlockCmdFlags.Force) if err != nil { Exit("Unable to unlock: %v", err.Error()) } } else if unlockCmdFlags.Id != "" { + + unlockCheckFileStatusById(unlockCmdFlags.Id, lockClient) + err := lockClient.UnlockFileById(unlockCmdFlags.Id, unlockCmdFlags.Force) if err != nil { Exit("Unable to unlock %v: %v", unlockCmdFlags.Id, err.Error()) @@ -55,6 +62,41 @@ func unlockCommand(cmd *cobra.Command, args []string) { Print("'%s' was unlocked", args[0]) } +func unlockCheckFileStatus(path string) { + modified, err := git.IsFileModified(path) + + if err != nil { + Exit(err.Error()) + } + + if modified { + if unlockCmdFlags.Force { + // Only a warning + Error("Warning: unlocking with uncommitted changes because --force") + } else { + Exit("Cannot unlock file with uncommitted changes") + } + + } +} + +func unlockCheckFileStatusById(id string, lockClient *locking.Client) { + // Get the path so we can check the status + filter := map[string]string{"id": id} + // try local cache first + locks, _ := lockClient.SearchLocks(filter, 0, true) + if len(locks) == 0 { + // Fall back on calling server + locks, _ = lockClient.SearchLocks(filter, 0, false) + } + + if len(locks) > 0 { + unlockCheckFileStatus(locks[0].Path) + } + + // Don't block if we can't determine the path, may be cleaning up old data +} + func init() { if !isCommandEnabled(cfg, "locks") { return diff --git a/git/git.go b/git/git.go index 335a3bae..137f4814 100644 --- a/git/git.go +++ b/git/git.go @@ -1092,3 +1092,43 @@ func GetFilesChanged(from, to string) ([]string, error) { return files, err } + +// IsFileModified returns whether the filepath specified is modified according +// to `git status`. A file is modified if it has uncommitted changes in the +// working copy or the index. This includes being untracked. +func IsFileModified(filepath string) (bool, error) { + + args := []string{ + "-c", "core.quotepath=false", // handle special chars in filenames + "status", + "--porcelain", + "--", // separator in case filename ambiguous + filepath, + } + cmd := subprocess.ExecCommand("git", args...) + outp, err := cmd.StdoutPipe() + if err != nil { + return false, fmt.Errorf("Failed to call git status: %v", err) + } + if err := cmd.Start(); err != nil { + return false, fmt.Errorf("Failed to start git status: %v", err) + } + scanner := bufio.NewScanner(outp) + for scanner.Scan() { + line := scanner.Text() + // Porcelain format is " " + // Where = index status, = working copy status + if len(line) > 3 { + // Double-check even though should be only match + if strings.TrimSpace(line[3:]) == filepath { + return true, nil + } + } + + } + if err := cmd.Wait(); err != nil { + return false, fmt.Errorf("Git status failed: %v", err) + } + + return false, nil +} diff --git a/test/test-unlock.sh b/test/test-unlock.sh index d7418aa5..3a22ba3c 100755 --- a/test/test-unlock.sh +++ b/test/test-unlock.sh @@ -72,3 +72,52 @@ begin_test "unlocking a lock without sufficient info" assert_server_lock "$reponame" "$id" ) end_test + +begin_test "unlocking a lock while uncommitted" +( + set -e + + reponame="unlock_modified" + setup_remote_repo_with_file "$reponame" "f.dat" + + GITLFSLOCKSENABLED=1 git lfs lock "f.dat" | tee lock.log + + id=$(grep -oh "\((.*)\)" lock.log | tr -d "()") + assert_server_lock "$reponame" "$id" + + echo "\nSomething" >> f.dat + + GITLFSLOCKSENABLED=1 git lfs unlock "f.dat" 2>&1 | tee unlock.log + [ ${PIPESTATUS[0]} -ne "0" ] + + grep "Cannot unlock file with uncommitted changes" unlock.log + + assert_server_lock "$reponame" "$id" + + # should allow after discard + git checkout f.dat + GITLFSLOCKSENABLED=1 git lfs unlock "f.dat" 2>&1 | tee unlock.log + refute_server_lock "$reponame" "$id" +) +end_test + +begin_test "unlocking a lock while uncommitted with --force" +( + set -e + + reponame="unlock_modified_force" + setup_remote_repo_with_file "$reponame" "g.dat" + + GITLFSLOCKSENABLED=1 git lfs lock "g.dat" | tee lock.log + + id=$(grep -oh "\((.*)\)" lock.log | tr -d "()") + assert_server_lock "$reponame" "$id" + + echo "\nSomething" >> g.dat + + # should allow with --force + GITLFSLOCKSENABLED=1 git lfs unlock --force "g.dat" 2>&1 | tee unlock.log + grep "Warning: unlocking with uncommitted changes" unlock.log + refute_server_lock "$reponame" "$id" +) +end_test