unlock: allow locking multiple files

Since it's useful to be able to unlock multiple files, let's add this
functionality.  We iterate over each path provided and attempt to unlock
it.  If we're using JSON, we report all successful unlocking attempts at
the end; otherwise, we report all successful unlocking attempts as we
process them.  All errors are reported to standard error immediately,
and we additionally exit 2 on failure.  As a result of these changes,
avoid exiting early in various cases and instead return an error.

Note that we don't currently accept multiple --id arguments, although we
could in the future.

Note that this causes an incompatible change to the JSON output, since
we now always output an array of locks.
This commit is contained in:
brian m. carlson 2021-08-23 20:48:18 +00:00
parent a0aa09d1b6
commit 048c39834d
No known key found for this signature in database
GPG Key ID: 2D0C9BC12F82B3A1
2 changed files with 95 additions and 37 deletions

@ -2,6 +2,7 @@ package commands
import (
"encoding/json"
"fmt"
"os"
"github.com/git-lfs/git-lfs/v2/errors"
@ -26,13 +27,32 @@ type unlockFlags struct {
var unlockUsage = "Usage: git lfs unlock (--id my-lock-id | <path>)"
type unlockResponse struct {
Id string `json:"id,omitempty"`
Path string `json:"path,omitempty"`
Unlocked bool `json:"unlocked"`
Reason string `json:"reason,omitempty"`
}
func handleUnlockError(locks []unlockResponse, id string, path string, err error) []unlockResponse {
Error(err.Error())
if locksCmdFlags.JSON {
locks = append(locks, unlockResponse{
Id: id,
Path: path,
Unlocked: false,
Reason: err.Error(),
})
}
return locks
}
func unlockCommand(cmd *cobra.Command, args []string) {
hasPath := len(args) > 0
hasId := len(unlockCmdFlags.Id) > 0
if hasPath == hasId || len(args) > 1 {
if hasPath == hasId {
// If there is both an `--id` AND a `<path>`, or there is
// neither, or there are multiple paths, print the usage and
// quit.
// neither, print the usage and quit.
Exit(unlockUsage)
}
@ -45,26 +65,41 @@ func unlockCommand(cmd *cobra.Command, args []string) {
lockClient.RemoteRef = refUpdate.Right()
defer lockClient.Close()
locks := make([]unlockResponse, 0, len(args))
success := true
if hasPath {
path, err := lockPath(args[0])
if err != nil {
if !unlockCmdFlags.Force {
Exit("Unable to determine path: %v", err.Error())
for _, pathspec := range args {
path, err := lockPath(pathspec)
if err != nil {
if !unlockCmdFlags.Force {
locks = handleUnlockError(locks, "", path, fmt.Errorf("Unable to determine path: %v", err.Error()))
success = false
continue
}
path = pathspec
}
path = args[0]
}
// This call can early-out
unlockAbortIfFileModified(path)
if err := unlockAbortIfFileModified(path); err != nil {
locks = handleUnlockError(locks, "", path, err)
success = false
continue
}
err = lockClient.UnlockFile(path, unlockCmdFlags.Force)
if err != nil {
Exit("%s", errors.Cause(err))
}
err = lockClient.UnlockFile(path, unlockCmdFlags.Force)
if err != nil {
locks = handleUnlockError(locks, "", path, errors.Cause(err))
success = false
continue
}
if !locksCmdFlags.JSON {
Print("Unlocked %s", path)
return
if !locksCmdFlags.JSON {
Print("Unlocked %s", path)
continue
}
locks = append(locks, unlockResponse{
Path: path,
Unlocked: true,
})
}
} else if unlockCmdFlags.Id != "" {
// This call can early-out
@ -72,26 +107,26 @@ func unlockCommand(cmd *cobra.Command, args []string) {
err := lockClient.UnlockFileById(unlockCmdFlags.Id, unlockCmdFlags.Force)
if err != nil {
Exit("Unable to unlock %v: %v", unlockCmdFlags.Id, errors.Cause(err))
}
if !locksCmdFlags.JSON {
locks = handleUnlockError(locks, unlockCmdFlags.Id, "", fmt.Errorf("Unable to unlock %v: %v", unlockCmdFlags.Id, errors.Cause(err)))
success = false
} else if !locksCmdFlags.JSON {
Print("Unlocked Lock %s", unlockCmdFlags.Id)
return
}
} else {
Error(unlockUsage)
}
if err := json.NewEncoder(os.Stdout).Encode(struct {
Unlocked bool `json:"unlocked"`
}{true}); err != nil {
Error(err.Error())
if locksCmdFlags.JSON {
if err := json.NewEncoder(os.Stdout).Encode(locks); err != nil {
Error(err.Error())
}
}
if !success {
os.Exit(2)
}
return
}
func unlockAbortIfFileModified(path string) {
func unlockAbortIfFileModified(path string) error {
modified, err := git.IsFileModified(path)
if err != nil {
@ -102,9 +137,9 @@ func unlockAbortIfFileModified(path string) {
//
// Unlocking a files that does not exist with
// --force is OK.
return
return nil
}
Exit(err.Error())
return err
}
if modified {
@ -112,13 +147,14 @@ func unlockAbortIfFileModified(path string) {
// Only a warning
Error("Warning: unlocking with uncommitted changes because --force")
} else {
Exit("Cannot unlock file with uncommitted changes")
return fmt.Errorf("Cannot unlock file with uncommitted changes")
}
}
return nil
}
func unlockAbortIfFileModifiedById(id string, lockClient *locking.Client) {
func unlockAbortIfFileModifiedById(id string, lockClient *locking.Client) error {
// Get the path so we can check the status
filter := map[string]string{"id": id}
// try local cache first
@ -130,10 +166,10 @@ func unlockAbortIfFileModifiedById(id string, lockClient *locking.Client) {
if len(locks) == 0 {
// Don't block if we can't determine the path, may be cleaning up old data
return
return nil
}
unlockAbortIfFileModified(locks[0].Path)
return unlockAbortIfFileModified(locks[0].Path)
}
func init() {

@ -136,9 +136,31 @@ begin_test "unlock multiple files"
git lfs lock a.dat
git lfs lock b.dat
git lfs unlock *.dat >log 2>&1 && exit 1
git lfs unlock *.dat >log 2>&1
grep "Usage:" log && exit 1
true
)
end_test
grep "Usage:" log
begin_test "unlock multiple files (JSON)"
(
set -e
reponame="unlock-multiple-files-json"
setup_remote_repo "$reponame"
clone_repo "$reponame" "$reponame"
git lfs track "*.dat"
echo "a" > a.dat
echo "b" > b.dat
git add .gitattributes a.dat b.dat
git commit -m "add dat files"
git push origin main:other
git lfs lock a.dat
git lfs lock b.dat
git lfs unlock --json *.dat | tee lock.json
grep -F '[{"path":"a.dat","unlocked":true},{"path":"b.dat","unlocked":true}]' lock.json
)
end_test