arche / internal/cli/cmd_lock.go

commit 154431fd
  1package cli
  2
  3import (
  4	"fmt"
  5	"os"
  6	"time"
  7
  8	"arche/internal/store"
  9
 10	"github.com/spf13/cobra"
 11)
 12
 13var lockCmd = &cobra.Command{
 14	Use:   "lock",
 15	Short: "Manage exclusive file locks",
 16	Long: `Advisory exclusive locks for large binary files (Perforce-style).
 17
 18Locks are stored in store.db and transmitted during arche sync so that
 19all clones see who has a file checked out for editing.
 20
 21  arche lock add <paths...>     - acquire an exclusive lock
 22  arche lock remove <paths...>  - release a lock you hold
 23  arche lock list               - show all current locks`,
 24}
 25
 26var lockComment string
 27
 28var lockAddCmd = &cobra.Command{
 29	Use:     "add <path> [paths...]",
 30	Aliases: []string{"acquire"},
 31	Short:   "Acquire an exclusive lock on one or more files",
 32	Args:    cobra.MinimumNArgs(1),
 33	RunE: func(cmd *cobra.Command, args []string) error {
 34		r := openRepo()
 35		defer r.Close()
 36
 37		ls, ok := r.Store.(store.LockStore)
 38		if !ok {
 39			return fmt.Errorf("store does not support file locks")
 40		}
 41
 42		owner := lockOwner(r.Cfg.User.Name)
 43
 44		tx, err := r.Store.Begin()
 45		if err != nil {
 46			return err
 47		}
 48		for _, path := range args {
 49			if err := ls.AcquireLock(tx, path, owner, lockComment); err != nil {
 50				r.Store.Rollback(tx) //nolint:errcheck
 51				return err
 52			}
 53		}
 54		if err := r.Store.Commit(tx); err != nil {
 55			return err
 56		}
 57		for _, path := range args {
 58			fmt.Printf("locked  %s\n", path)
 59		}
 60		return nil
 61	},
 62}
 63
 64var lockRemoveCmd = &cobra.Command{
 65	Use:     "remove <path> [paths...]",
 66	Aliases: []string{"release", "unlock"},
 67	Short:   "Release a lock you hold",
 68	Args:    cobra.MinimumNArgs(1),
 69	RunE: func(cmd *cobra.Command, args []string) error {
 70		r := openRepo()
 71		defer r.Close()
 72
 73		ls, ok := r.Store.(store.LockStore)
 74		if !ok {
 75			return fmt.Errorf("store does not support file locks")
 76		}
 77
 78		owner := lockOwner(r.Cfg.User.Name)
 79
 80		tx, err := r.Store.Begin()
 81		if err != nil {
 82			return err
 83		}
 84		for _, path := range args {
 85			if err := ls.ReleaseLock(tx, path, owner); err != nil {
 86				r.Store.Rollback(tx) //nolint:errcheck
 87				return err
 88			}
 89		}
 90		if err := r.Store.Commit(tx); err != nil {
 91			return err
 92		}
 93		for _, path := range args {
 94			fmt.Printf("unlocked  %s\n", path)
 95		}
 96		return nil
 97	},
 98}
 99
100var lockStealFlag bool
101
102var lockSteelCmd = &cobra.Command{
103	Use:   "steal <path> [paths...]",
104	Short: "Force-release a lock held by another user (admin action)",
105	Args:  cobra.MinimumNArgs(1),
106	RunE: func(cmd *cobra.Command, args []string) error {
107		r := openRepo()
108		defer r.Close()
109
110		ls, ok := r.Store.(store.LockStore)
111		if !ok {
112			return fmt.Errorf("store does not support file locks")
113		}
114
115		tx, err := r.Store.Begin()
116		if err != nil {
117			return err
118		}
119		for _, path := range args {
120			lock, _ := ls.GetLock(path)
121			if err := ls.ReleaseLockAdmin(tx, path); err != nil {
122				r.Store.Rollback(tx) //nolint:errcheck
123				return err
124			}
125			if lock != nil {
126				fmt.Printf("stole lock on %s (was held by %s)\n", path, lock.Owner)
127			} else {
128				fmt.Printf("no lock on %s\n", path)
129			}
130		}
131		return r.Store.Commit(tx)
132	},
133}
134
135var lockListCmd = &cobra.Command{
136	Use:   "list",
137	Short: "List all current file locks",
138	Args:  cobra.NoArgs,
139	RunE: func(cmd *cobra.Command, args []string) error {
140		r := openRepo()
141		defer r.Close()
142
143		ls, ok := r.Store.(store.LockStore)
144		if !ok {
145			return fmt.Errorf("store does not support file locks")
146		}
147
148		locks, err := ls.ListLocks()
149		if err != nil {
150			return err
151		}
152		if len(locks) == 0 {
153			fmt.Println("No file locks.")
154			return nil
155		}
156		for _, l := range locks {
157			ts := time.Unix(l.AcquiredAt, 0).Format("2006-01-02 15:04")
158			if l.Comment != "" {
159				fmt.Printf("%-50s  %-30s  %s  %s\n", l.Path, l.Owner, ts, l.Comment)
160			} else {
161				fmt.Printf("%-50s  %-30s  %s\n", l.Path, l.Owner, ts)
162			}
163		}
164		return nil
165	},
166}
167
168func init() {
169	lockAddCmd.Flags().StringVarP(&lockComment, "comment", "c", "", "Optional comment for the lock")
170	lockCmd.AddCommand(lockAddCmd, lockRemoveCmd, lockSteelCmd, lockListCmd)
171}
172
173func lockOwner(userName string) string {
174	if userName == "" {
175		userName = "unknown"
176	}
177	host, err := os.Hostname()
178	if err != nil {
179		host = "localhost"
180	}
181	return userName + "@" + host
182}