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}