detached 28f73e3

refactor(watch): drop the lost event flag and dead poll fallbacks

A lost watch is an empty event. Windows always uses its native backend,
unsupported systems get the poller from newNative, and ios relies on the
darwin build constraint. open_other.go leaves the regular-file check to
ReadFile.

Tests: change waits stay below the 2s retry unless a step relies on it,
the debounce test proves the window restarts, and the pre-cancelled
context test is removed.
commit
28f73e3d2683352e3193130afaadf9ea84a406d1
author
brice · · 2026-10-10 06:21 UTC
parent
946d18c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +67 -58
M open_other.go +0 -3
@@ -5,8 +5,5 @@ package kvconf
55 import "os"
66
77 func openNonblocking(path string) (*os.File, error) {
8- if info, err := os.Stat(path); err == nil && !info.Mode().IsRegular() {
9- return nil, &os.PathError{Op: "open", Path: path, Err: ErrNotRegular}
10- }
118 return os.Open(path)
129 }
@@ -5,8 +5,5 @@ package kvconf
5 import "os"5 import "os"
6 6
7 func openNonblocking(path string) (*os.File, error) {7 func openNonblocking(path string) (*os.File, error) {
8- if info, err := os.Stat(path); err == nil && !info.Mode().IsRegular() {
9- return nil, &os.PathError{Op: "open", Path: path, Err: ErrNotRegular}
10- }
11 return os.Open(path)8 return os.Open(path)
12 }9 }
M watch.go +3 -11
@@ -68,15 +68,13 @@ func Watch(ctx context.Context, path string, opts WatchOptions, out chan<- struc
6868 b = newPoller(opts.Poll)
6969 } else if b, err = newNative(); err != nil {
7070 return err
71- } else if b == nil {
72- b = newPoller(time.Second)
7371 }
7472 defer b.close() //nolint:errcheck // nothing to report once watching ends
7573 stop := context.AfterFunc(ctx, b.wake)
7674 defer stop()
7775
7876 w := &watcher{path: path, dir: opts.Dir, stamps: map[string]stamp{}}
79- missing, err := w.rewatch(b)
77+ missing, _, err := w.rewatchChanged(b)
8078 if err != nil {
8179 return err
8280 }
@@ -189,10 +187,9 @@ func (t *target) has(name string) bool {
189187
190188 // event is a change in a watched directory. An empty name means "something
191189 // changed in dir" (kqueue, polling, overflow); an empty dir means anything
192-// may have changed. lost means the watch itself vanished.
190+// may have changed, or a watch was lost.
193191 type event struct {
194192 dir, name string
195- lost bool
196193 // moved: the name was created, removed or renamed, so a link may now
197194 // point elsewhere. A plain write leaves links alone.
198195 moved bool
@@ -329,11 +326,6 @@ func (w *watcher) plan() (targets []*target, files []string) {
329326 return targets, files
330327 }
331328
332-func (w *watcher) rewatch(b backend) (missing bool, err error) {
333- missing, _, err = w.rewatchChanged(b)
334- return missing, err
335-}
336-
337329 // rewatchChanged installs the watches again and reports whether the watched
338330 // files changed meanwhile: a link was retargeted, a missing directory
339331 // appeared, or a file changed while it was not watched.
@@ -367,7 +359,7 @@ func sameTargets(a, b []*target) bool {
367359 func (w *watcher) classify(evs []event) (relevant, rewatch, recheck bool) {
368360 relinked := false
369361 for _, ev := range evs {
370- if ev.lost || ev.dir == "" {
362+ if ev.dir == "" {
371363 relevant, rewatch, recheck = true, true, true
372364 continue
373365 }
@@ -68,15 +68,13 @@ func Watch(ctx context.Context, path string, opts WatchOptions, out chan<- struc
68 b = newPoller(opts.Poll)68 b = newPoller(opts.Poll)
69 } else if b, err = newNative(); err != nil {69 } else if b, err = newNative(); err != nil {
70 return err70 return err
71- } else if b == nil {
72- b = newPoller(time.Second)
73 }71 }
74 defer b.close() //nolint:errcheck // nothing to report once watching ends72 defer b.close() //nolint:errcheck // nothing to report once watching ends
75 stop := context.AfterFunc(ctx, b.wake)73 stop := context.AfterFunc(ctx, b.wake)
76 defer stop()74 defer stop()
77 75
78 w := &watcher{path: path, dir: opts.Dir, stamps: map[string]stamp{}}76 w := &watcher{path: path, dir: opts.Dir, stamps: map[string]stamp{}}
79- missing, err := w.rewatch(b)77+ missing, _, err := w.rewatchChanged(b)
80 if err != nil {78 if err != nil {
81 return err79 return err
82 }80 }
@@ -189,10 +187,9 @@ func (t *target) has(name string) bool {
189 187
190 // event is a change in a watched directory. An empty name means "something188 // event is a change in a watched directory. An empty name means "something
191 // changed in dir" (kqueue, polling, overflow); an empty dir means anything189 // changed in dir" (kqueue, polling, overflow); an empty dir means anything
192-// may have changed. lost means the watch itself vanished.190+// may have changed, or a watch was lost.
193 type event struct {191 type event struct {
194 dir, name string192 dir, name string
195- lost bool
196 // moved: the name was created, removed or renamed, so a link may now193 // moved: the name was created, removed or renamed, so a link may now
197 // point elsewhere. A plain write leaves links alone.194 // point elsewhere. A plain write leaves links alone.
198 moved bool195 moved bool
@@ -329,11 +326,6 @@ func (w *watcher) plan() (targets []*target, files []string) {
329 return targets, files326 return targets, files
330 }327 }
331 328
332-func (w *watcher) rewatch(b backend) (missing bool, err error) {
333- missing, _, err = w.rewatchChanged(b)
334- return missing, err
335-}
336-
337 // rewatchChanged installs the watches again and reports whether the watched329 // rewatchChanged installs the watches again and reports whether the watched
338 // files changed meanwhile: a link was retargeted, a missing directory330 // files changed meanwhile: a link was retargeted, a missing directory
339 // appeared, or a file changed while it was not watched.331 // appeared, or a file changed while it was not watched.
@@ -367,7 +359,7 @@ func sameTargets(a, b []*target) bool {
367 func (w *watcher) classify(evs []event) (relevant, rewatch, recheck bool) {359 func (w *watcher) classify(evs []event) (relevant, rewatch, recheck bool) {
368 relinked := false360 relinked := false
369 for _, ev := range evs {361 for _, ev := range evs {
370- if ev.lost || ev.dir == "" {362+ if ev.dir == "" {
371 relevant, rewatch, recheck = true, true, true363 relevant, rewatch, recheck = true, true, true
372 continue364 continue
373 }365 }
M watch_kqueue.go +6 -3
@@ -1,4 +1,4 @@
1-//go:build darwin || ios || freebsd || netbsd || openbsd || dragonfly
1+//go:build darwin || freebsd || netbsd || openbsd || dragonfly
22
33 package kvconf
44
@@ -124,8 +124,11 @@ func (k *kqueue) wait(timeout time.Duration, evs []event) ([]event, error) {
124124 if !ok {
125125 continue
126126 }
127- lost := ev.Fflags&(syscall.NOTE_DELETE|syscall.NOTE_RENAME|syscall.NOTE_REVOKE) != 0
128- evs = append(evs, event{dir: dir, lost: lost})
127+ if ev.Fflags&(syscall.NOTE_DELETE|syscall.NOTE_RENAME|syscall.NOTE_REVOKE) != 0 {
128+ evs = append(evs, event{}) // a watch was lost
129+ continue
130+ }
131+ evs = append(evs, event{dir: dir})
129132 }
130133 return evs, nil
131134 }
@@ -1,4 +1,4 @@
1-//go:build darwin || ios || freebsd || netbsd || openbsd || dragonfly1+//go:build darwin || freebsd || netbsd || openbsd || dragonfly
2 2
3 package kvconf3 package kvconf
4 4
@@ -124,8 +124,11 @@ func (k *kqueue) wait(timeout time.Duration, evs []event) ([]event, error) {
124 if !ok {124 if !ok {
125 continue125 continue
126 }126 }
127- lost := ev.Fflags&(syscall.NOTE_DELETE|syscall.NOTE_RENAME|syscall.NOTE_REVOKE) != 0127+ if ev.Fflags&(syscall.NOTE_DELETE|syscall.NOTE_RENAME|syscall.NOTE_REVOKE) != 0 {
128- evs = append(evs, event{dir: dir, lost: lost})128+ evs = append(evs, event{}) // a watch was lost
129+ continue
130+ }
131+ evs = append(evs, event{dir: dir})
129 }132 }
130 return evs, nil133 return evs, nil
131 }134 }
M watch_kqueue_darwin.go +1 -1
@@ -1,4 +1,4 @@
1-//go:build darwin || ios
1+//go:build darwin
22
33 package kvconf
44
@@ -1,4 +1,4 @@
1-//go:build darwin || ios1+//go:build darwin
2 2
3 package kvconf3 package kvconf
4 4
M watch_linux.go +1 -1
@@ -123,7 +123,7 @@ func (in *inotify) wait(timeout time.Duration, evs []event) ([]event, error) {
123123 if in.wds[dir] == ev.Wd {
124124 delete(in.wds, dir)
125125 }
126- evs = append(evs, event{dir: dir, lost: true})
126+ evs = append(evs, event{})
127127 continue
128128 }
129129 for len(name) > 0 && name[len(name)-1] == 0 {
@@ -123,7 +123,7 @@ func (in *inotify) wait(timeout time.Duration, evs []event) ([]event, error) {
123 if in.wds[dir] == ev.Wd {123 if in.wds[dir] == ev.Wd {
124 delete(in.wds, dir)124 delete(in.wds, dir)
125 }125 }
126- evs = append(evs, event{dir: dir, lost: true})126+ evs = append(evs, event{})
127 continue127 continue
128 }128 }
129 for len(name) > 0 && name[len(name)-1] == 0 {129 for len(name) > 0 && name[len(name)-1] == 0 {
M watch_other.go +5 -3
@@ -1,6 +1,8 @@
1-//go:build !linux && !windows && !darwin && !ios && !freebsd && !netbsd && !openbsd && !dragonfly
1+//go:build !linux && !windows && !darwin && !freebsd && !netbsd && !openbsd && !dragonfly
22
33 package kvconf
44
5-// newNative returns nil: this OS has no native backend, Watch polls.
6-func newNative() (backend, error) { return nil, nil }
5+import "time"
6+
7+// newNative polls every second: this OS has no native backend.
8+func newNative() (backend, error) { return newPoller(time.Second), nil }
@@ -1,6 +1,8 @@
1-//go:build !linux && !windows && !darwin && !ios && !freebsd && !netbsd && !openbsd && !dragonfly1+//go:build !linux && !windows && !darwin && !freebsd && !netbsd && !openbsd && !dragonfly
2 2
3 package kvconf3 package kvconf
4 4
5-// newNative returns nil: this OS has no native backend, Watch polls.5+import "time"
6-func newNative() (backend, error) { return nil, nil }6+
7+// newNative polls every second: this OS has no native backend.
8+func newNative() (backend, error) { return newPoller(time.Second), nil }
M watch_test.go +47 -29
@@ -44,12 +44,23 @@ func (e *watchEnv) symlink(target, rel string) {
4444 }
4545 }
4646
47+// fastWait is how long a change that needs no retry may take. It stays below
48+// retryMissing, so a broken fast path is not hidden by the periodic look.
49+const fastWait = retryMissing - 500*time.Millisecond
50+
4751 func (e *watchEnv) expectChange(why string) {
52+ e.t.Helper()
53+ e.expectChangeWithin(why, fastWait)
54+}
55+
56+// expectChangeWithin is expectChange for a step that legitimately relies on
57+// the retry or recheck, which fires after retryMissing.
58+func (e *watchEnv) expectChangeWithin(why string, d time.Duration) {
4859 e.t.Helper()
4960 select {
5061 case <-e.out:
51- case <-time.After(5 * time.Second):
52- e.t.Fatalf("no change reported after %s", why)
62+ case <-time.After(d):
63+ e.t.Fatalf("no change reported within %v after %s", d, why)
5364 }
5465 }
5566
@@ -76,6 +87,14 @@ type backendCase struct {
7687 poll time.Duration
7788 }
7889
90+// slowWait is the deadline for a step that relies on the retry on this OS.
91+func slowWait() time.Duration {
92+ if runtime.GOOS == "windows" {
93+ return retryMissing + 3*time.Second
94+ }
95+ return fastWait
96+}
97+
7998 func backends() []backendCase {
8099 return []backendCase{{name: "native"}, {name: "poll", poll: 20 * time.Millisecond}}
81100 }
@@ -135,7 +154,8 @@ func TestWatch(t *testing.T) {
135154 rel: "a/b/config",
136155 steps: func(e *watchEnv) {
137156 e.write("a/b/config", "a = 1\n")
138- e.expectChange("the directories and file appeared")
157+ // Windows may only find the new directories on the retry.
158+ e.expectChangeWithin("the directories and file appeared", slowWait())
139159 e.settle()
140160 e.write("a/b/config", "a = 2\n")
141161 e.expectChange("a write once the directory exists")
@@ -147,10 +167,12 @@ func TestWatch(t *testing.T) {
147167 setup: func(e *watchEnv) { e.write("d/config", "a = 1\n") },
148168 steps: func(e *watchEnv) {
149169 must(e.t, os.RemoveAll(filepath.Join(e.dir, "d")))
150- e.expectChange("the directory removal")
170+ // Windows reports a removed directory only after the watch on
171+ // it is closed: the recheck finds it.
172+ e.expectChangeWithin("the directory removal", slowWait())
151173 e.settle()
152174 e.write("d/config", "a = 2\n")
153- e.expectChange("the recreated file")
175+ e.expectChangeWithin("the recreated file", slowWait())
154176 },
155177 },
156178 {
@@ -286,9 +308,17 @@ func TestWatch(t *testing.T) {
286308 }
287309 }
288310
311+// Every write restarts the debounce: a burst longer than the debounce is
312+// reported once, after its last write.
289313 func TestWatchDebounce(t *testing.T) {
314+ const (
315+ debounce = 200 * time.Millisecond
316+ writes = 10
317+ gap = 50 * time.Millisecond
318+ )
290319 for _, bc := range backends() {
291320 t.Run(bc.name, func(t *testing.T) {
321+ t.Parallel()
292322 dir := t.TempDir()
293323 path := filepath.Join(dir, "config")
294324 must(t, os.WriteFile(path, nil, 0o600))
@@ -297,15 +327,20 @@ func TestWatchDebounce(t *testing.T) {
297327 defer cancel()
298328 ready := make(chan struct{})
299329 go func() {
300- _ = Watch(ctx, path, WatchOptions{Poll: bc.poll, Debounce: 300 * time.Millisecond, Ready: ready}, out)
330+ _ = Watch(ctx, path, WatchOptions{Poll: bc.poll, Debounce: debounce, Ready: ready}, out)
301331 }()
302332 <-ready
303- for i := range 5 {
333+ var lastWrite time.Time
334+ for i := range writes {
335+ lastWrite = time.Now()
304336 must(t, os.WriteFile(path, []byte{byte('a' + i)}, 0o600))
305- time.Sleep(50 * time.Millisecond)
337+ time.Sleep(gap)
306338 }
307339 select {
308340 case <-out:
341+ if d := time.Since(lastWrite); d < debounce {
342+ t.Fatalf("reported %v after the last write, before the %v debounce", d, debounce)
343+ }
309344 case <-time.After(5 * time.Second):
310345 t.Fatal("no change reported")
311346 }
@@ -319,10 +354,12 @@ func TestWatchDebounce(t *testing.T) {
319354 }
320355
321356 // An atomic save is one change: nothing is reported again when the watcher
322-// looks at the files later.
357+// looks at the files later. This guards Windows, where the recheck after a
358+// rename would otherwise report the same save a second time.
323359 func TestWatchReportsASaveOnce(t *testing.T) {
324360 for _, bc := range backends() {
325361 t.Run(bc.name, func(t *testing.T) {
362+ t.Parallel()
326363 dir := t.TempDir()
327364 path := filepath.Join(dir, "config")
328365 must(t, os.WriteFile(path, []byte("a = 1\n"), 0o600))
@@ -337,7 +374,7 @@ func TestWatchReportsASaveOnce(t *testing.T) {
337374 must(t, WriteFile(path, []byte("a = 2\n"), 0o600))
338375 select {
339376 case <-out:
340- case <-time.After(5 * time.Second):
377+ case <-time.After(slowWait()):
341378 t.Fatal("no change reported")
342379 }
343380 select {
@@ -348,22 +385,3 @@ func TestWatchReportsASaveOnce(t *testing.T) {
348385 })
349386 }
350387 }
351-
352-func TestWatchCancelBeforeEvents(t *testing.T) {
353- for _, bc := range backends() {
354- t.Run(bc.name, func(t *testing.T) {
355- ctx, cancel := context.WithCancel(context.Background())
356- cancel()
357- done := make(chan error, 1)
358- go func() {
359- done <- Watch(ctx, filepath.Join(t.TempDir(), "config"), WatchOptions{Poll: bc.poll}, make(chan struct{}, 1))
360- }()
361- select {
362- case err := <-done:
363- must(t, err)
364- case <-time.After(5 * time.Second):
365- t.Fatal("Watch ignored a cancelled context")
366- }
367- })
368- }
369-}
@@ -44,12 +44,23 @@ func (e *watchEnv) symlink(target, rel string) {
44 }44 }
45 }45 }
46 46
47+// fastWait is how long a change that needs no retry may take. It stays below
48+// retryMissing, so a broken fast path is not hidden by the periodic look.
49+const fastWait = retryMissing - 500*time.Millisecond
50+
47 func (e *watchEnv) expectChange(why string) {51 func (e *watchEnv) expectChange(why string) {
52+ e.t.Helper()
53+ e.expectChangeWithin(why, fastWait)
54+}
55+
56+// expectChangeWithin is expectChange for a step that legitimately relies on
57+// the retry or recheck, which fires after retryMissing.
58+func (e *watchEnv) expectChangeWithin(why string, d time.Duration) {
48 e.t.Helper()59 e.t.Helper()
49 select {60 select {
50 case <-e.out:61 case <-e.out:
51- case <-time.After(5 * time.Second):62+ case <-time.After(d):
52- e.t.Fatalf("no change reported after %s", why)63+ e.t.Fatalf("no change reported within %v after %s", d, why)
53 }64 }
54 }65 }
55 66
@@ -76,6 +87,14 @@ type backendCase struct {
76 poll time.Duration87 poll time.Duration
77 }88 }
78 89
90+// slowWait is the deadline for a step that relies on the retry on this OS.
91+func slowWait() time.Duration {
92+ if runtime.GOOS == "windows" {
93+ return retryMissing + 3*time.Second
94+ }
95+ return fastWait
96+}
97+
79 func backends() []backendCase {98 func backends() []backendCase {
80 return []backendCase{{name: "native"}, {name: "poll", poll: 20 * time.Millisecond}}99 return []backendCase{{name: "native"}, {name: "poll", poll: 20 * time.Millisecond}}
81 }100 }
@@ -135,7 +154,8 @@ func TestWatch(t *testing.T) {
135 rel: "a/b/config",154 rel: "a/b/config",
136 steps: func(e *watchEnv) {155 steps: func(e *watchEnv) {
137 e.write("a/b/config", "a = 1\n")156 e.write("a/b/config", "a = 1\n")
138- e.expectChange("the directories and file appeared")157+ // Windows may only find the new directories on the retry.
158+ e.expectChangeWithin("the directories and file appeared", slowWait())
139 e.settle()159 e.settle()
140 e.write("a/b/config", "a = 2\n")160 e.write("a/b/config", "a = 2\n")
141 e.expectChange("a write once the directory exists")161 e.expectChange("a write once the directory exists")
@@ -147,10 +167,12 @@ func TestWatch(t *testing.T) {
147 setup: func(e *watchEnv) { e.write("d/config", "a = 1\n") },167 setup: func(e *watchEnv) { e.write("d/config", "a = 1\n") },
148 steps: func(e *watchEnv) {168 steps: func(e *watchEnv) {
149 must(e.t, os.RemoveAll(filepath.Join(e.dir, "d")))169 must(e.t, os.RemoveAll(filepath.Join(e.dir, "d")))
150- e.expectChange("the directory removal")170+ // Windows reports a removed directory only after the watch on
171+ // it is closed: the recheck finds it.
172+ e.expectChangeWithin("the directory removal", slowWait())
151 e.settle()173 e.settle()
152 e.write("d/config", "a = 2\n")174 e.write("d/config", "a = 2\n")
153- e.expectChange("the recreated file")175+ e.expectChangeWithin("the recreated file", slowWait())
154 },176 },
155 },177 },
156 {178 {
@@ -286,9 +308,17 @@ func TestWatch(t *testing.T) {
286 }308 }
287 }309 }
288 310
311+// Every write restarts the debounce: a burst longer than the debounce is
312+// reported once, after its last write.
289 func TestWatchDebounce(t *testing.T) {313 func TestWatchDebounce(t *testing.T) {
314+ const (
315+ debounce = 200 * time.Millisecond
316+ writes = 10
317+ gap = 50 * time.Millisecond
318+ )
290 for _, bc := range backends() {319 for _, bc := range backends() {
291 t.Run(bc.name, func(t *testing.T) {320 t.Run(bc.name, func(t *testing.T) {
321+ t.Parallel()
292 dir := t.TempDir()322 dir := t.TempDir()
293 path := filepath.Join(dir, "config")323 path := filepath.Join(dir, "config")
294 must(t, os.WriteFile(path, nil, 0o600))324 must(t, os.WriteFile(path, nil, 0o600))
@@ -297,15 +327,20 @@ func TestWatchDebounce(t *testing.T) {
297 defer cancel()327 defer cancel()
298 ready := make(chan struct{})328 ready := make(chan struct{})
299 go func() {329 go func() {
300- _ = Watch(ctx, path, WatchOptions{Poll: bc.poll, Debounce: 300 * time.Millisecond, Ready: ready}, out)330+ _ = Watch(ctx, path, WatchOptions{Poll: bc.poll, Debounce: debounce, Ready: ready}, out)
301 }()331 }()
302 <-ready332 <-ready
303- for i := range 5 {333+ var lastWrite time.Time
334+ for i := range writes {
335+ lastWrite = time.Now()
304 must(t, os.WriteFile(path, []byte{byte('a' + i)}, 0o600))336 must(t, os.WriteFile(path, []byte{byte('a' + i)}, 0o600))
305- time.Sleep(50 * time.Millisecond)337+ time.Sleep(gap)
306 }338 }
307 select {339 select {
308 case <-out:340 case <-out:
341+ if d := time.Since(lastWrite); d < debounce {
342+ t.Fatalf("reported %v after the last write, before the %v debounce", d, debounce)
343+ }
309 case <-time.After(5 * time.Second):344 case <-time.After(5 * time.Second):
310 t.Fatal("no change reported")345 t.Fatal("no change reported")
311 }346 }
@@ -319,10 +354,12 @@ func TestWatchDebounce(t *testing.T) {
319 }354 }
320 355
321 // An atomic save is one change: nothing is reported again when the watcher356 // An atomic save is one change: nothing is reported again when the watcher
322-// looks at the files later.357+// looks at the files later. This guards Windows, where the recheck after a
358+// rename would otherwise report the same save a second time.
323 func TestWatchReportsASaveOnce(t *testing.T) {359 func TestWatchReportsASaveOnce(t *testing.T) {
324 for _, bc := range backends() {360 for _, bc := range backends() {
325 t.Run(bc.name, func(t *testing.T) {361 t.Run(bc.name, func(t *testing.T) {
362+ t.Parallel()
326 dir := t.TempDir()363 dir := t.TempDir()
327 path := filepath.Join(dir, "config")364 path := filepath.Join(dir, "config")
328 must(t, os.WriteFile(path, []byte("a = 1\n"), 0o600))365 must(t, os.WriteFile(path, []byte("a = 1\n"), 0o600))
@@ -337,7 +374,7 @@ func TestWatchReportsASaveOnce(t *testing.T) {
337 must(t, WriteFile(path, []byte("a = 2\n"), 0o600))374 must(t, WriteFile(path, []byte("a = 2\n"), 0o600))
338 select {375 select {
339 case <-out:376 case <-out:
340- case <-time.After(5 * time.Second):377+ case <-time.After(slowWait()):
341 t.Fatal("no change reported")378 t.Fatal("no change reported")
342 }379 }
343 select {380 select {
@@ -348,22 +385,3 @@ func TestWatchReportsASaveOnce(t *testing.T) {
348 })385 })
349 }386 }
350 }387 }
351-
352-func TestWatchCancelBeforeEvents(t *testing.T) {
353- for _, bc := range backends() {
354- t.Run(bc.name, func(t *testing.T) {
355- ctx, cancel := context.WithCancel(context.Background())
356- cancel()
357- done := make(chan error, 1)
358- go func() {
359- done <- Watch(ctx, filepath.Join(t.TempDir(), "config"), WatchOptions{Poll: bc.poll}, make(chan struct{}, 1))
360- }()
361- select {
362- case err := <-done:
363- must(t, err)
364- case <-time.After(5 * time.Second):
365- t.Fatal("Watch ignored a cancelled context")
366- }
367- })
368- }
369-}
M watch_windows.go +4 -7
@@ -8,8 +8,8 @@ import (
88 "unsafe"
99 )
1010
11-// The standard syscall package lacks these, or declares them with a wrong
12-// signature on 64-bit Windows; they are loaded from kernel32 directly.
11+// These functions are missing from package syscall: they are loaded from
12+// kernel32 directly.
1313 var (
1414 kernel32 = syscall.NewLazyDLL("kernel32.dll")
1515 procCreateEventW = kernel32.NewProc("CreateEventW")
@@ -58,9 +58,6 @@ func createEvent() (syscall.Handle, error) {
5858 }
5959
6060 func newNative() (backend, error) {
61- if err := procWaitForMultipleObjects.Find(); err != nil {
62- return nil, nil // poll instead
63- }
6461 ev, err := createEvent()
6562 if err != nil {
6663 return nil, err
@@ -186,7 +183,7 @@ func (w *winWatch) wait(timeout time.Duration, evs []event) ([]event, error) {
186183 return evs, nil
187184 }
188185 // The directory was removed or became unreadable.
189- return append(evs, event{dir: d.dir, lost: true}), nil
186+ return append(evs, event{}), nil
190187 }
191188 if n == 0 {
192189 evs = append(evs, event{dir: d.dir}) // the buffer overflowed
@@ -203,7 +200,7 @@ func (w *winWatch) wait(timeout time.Duration, evs []event) ([]event, error) {
203200 off += info.NextEntryOffset
204201 }
205202 if err := d.read(); err != nil {
206- evs = append(evs, event{dir: d.dir, lost: true})
203+ evs = append(evs, event{})
207204 }
208205 return evs, nil
209206 }
@@ -8,8 +8,8 @@ import (
8 "unsafe"8 "unsafe"
9 )9 )
10 10
11-// The standard syscall package lacks these, or declares them with a wrong11+// These functions are missing from package syscall: they are loaded from
12-// signature on 64-bit Windows; they are loaded from kernel32 directly.12+// kernel32 directly.
13 var (13 var (
14 kernel32 = syscall.NewLazyDLL("kernel32.dll")14 kernel32 = syscall.NewLazyDLL("kernel32.dll")
15 procCreateEventW = kernel32.NewProc("CreateEventW")15 procCreateEventW = kernel32.NewProc("CreateEventW")
@@ -58,9 +58,6 @@ func createEvent() (syscall.Handle, error) {
58 }58 }
59 59
60 func newNative() (backend, error) {60 func newNative() (backend, error) {
61- if err := procWaitForMultipleObjects.Find(); err != nil {
62- return nil, nil // poll instead
63- }
64 ev, err := createEvent()61 ev, err := createEvent()
65 if err != nil {62 if err != nil {
66 return nil, err63 return nil, err
@@ -186,7 +183,7 @@ func (w *winWatch) wait(timeout time.Duration, evs []event) ([]event, error) {
186 return evs, nil183 return evs, nil
187 }184 }
188 // The directory was removed or became unreadable.185 // The directory was removed or became unreadable.
189- return append(evs, event{dir: d.dir, lost: true}), nil186+ return append(evs, event{}), nil
190 }187 }
191 if n == 0 {188 if n == 0 {
192 evs = append(evs, event{dir: d.dir}) // the buffer overflowed189 evs = append(evs, event{dir: d.dir}) // the buffer overflowed
@@ -203,7 +200,7 @@ func (w *winWatch) wait(timeout time.Duration, evs []event) ([]event, error) {
203 off += info.NextEntryOffset200 off += info.NextEntryOffset
204 }201 }
205 if err := d.read(); err != nil {202 if err := d.read(); err != nil {
206- evs = append(evs, event{dir: d.dir, lost: true})203+ evs = append(evs, event{})
207 }204 }
208 return evs, nil205 return evs, nil
209 }206 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 3

↑↓ moveenter openesc close