detached d5769b9

Merge pull request #69 from zerone0x/fix/issue-68-mcp-bridge-cwd

fix: run MCP servers from project directory
commit
d5769b973437e6068ae94bd1327f278a052d22d2
author
Patric Eckhart · · 2026-07-08 05:14 UTC
committer
GitHub · · 2026-07-08 05:14 UTC
merge of
2bf9508 + 29ef336 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
refs
tag: v0.2.66
5 files · +51 -16
M examples/extensions/mcp-bridge/bridge.go +5 -3
@@ -32,7 +32,8 @@ type toolMapping struct {
3232
3333 // bridge connects MCP servers to zot's extension protocol.
3434 type bridge struct {
35- e *ext.Extension
35+ e *ext.Extension
36+ cwd string
3637 // servers is written once in loadServers before any goroutine
3738 // starts, and read-only afterwards — that invariant is what makes
3839 // the lock-free reads in handleToolCall and the idle reaper safe.
@@ -45,9 +46,10 @@ type bridge struct {
4546 }
4647
4748 // newBridge creates a new MCP→zot bridge.
48-func newBridge(e *ext.Extension, logger *log.Logger) *bridge {
49+func newBridge(e *ext.Extension, cwd string, logger *log.Logger) *bridge {
4950 return &bridge{
5051 e: e,
52+ cwd: cwd,
5153 servers: make(map[string]*managedServer),
5254 mapping: make(map[string]toolMapping),
5355 logger: logger,
@@ -78,7 +80,7 @@ func toolName(serverName, mcpToolName string) string {
7880 // loadServers reads the config and creates managed servers.
7981 func (b *bridge) loadServers(cfg Config) {
8082 for name, srvCfg := range cfg.MCPServers {
81- srv := newManagedServer(name, srvCfg, b.logger)
83+ srv := newManagedServer(name, srvCfg, b.cwd, b.logger)
8284 b.servers[name] = srv
8385 }
8486 }
@@ -32,7 +32,8 @@ type toolMapping struct {
32 32
33 // bridge connects MCP servers to zot's extension protocol.33 // bridge connects MCP servers to zot's extension protocol.
34 type bridge struct {34 type bridge struct {
35- e *ext.Extension35+ e *ext.Extension
36+ cwd string
36 // servers is written once in loadServers before any goroutine37 // servers is written once in loadServers before any goroutine
37 // starts, and read-only afterwards — that invariant is what makes38 // starts, and read-only afterwards — that invariant is what makes
38 // the lock-free reads in handleToolCall and the idle reaper safe.39 // the lock-free reads in handleToolCall and the idle reaper safe.
@@ -45,9 +46,10 @@ type bridge struct {
45 }46 }
46 47
47 // newBridge creates a new MCP→zot bridge.48 // newBridge creates a new MCP→zot bridge.
48-func newBridge(e *ext.Extension, logger *log.Logger) *bridge {49+func newBridge(e *ext.Extension, cwd string, logger *log.Logger) *bridge {
49 return &bridge{50 return &bridge{
50 e: e,51 e: e,
52+ cwd: cwd,
51 servers: make(map[string]*managedServer),53 servers: make(map[string]*managedServer),
52 mapping: make(map[string]toolMapping),54 mapping: make(map[string]toolMapping),
53 logger: logger,55 logger: logger,
@@ -78,7 +80,7 @@ func toolName(serverName, mcpToolName string) string {
78 // loadServers reads the config and creates managed servers.80 // loadServers reads the config and creates managed servers.
79 func (b *bridge) loadServers(cfg Config) {81 func (b *bridge) loadServers(cfg Config) {
80 for name, srvCfg := range cfg.MCPServers {82 for name, srvCfg := range cfg.MCPServers {
81- srv := newManagedServer(name, srvCfg, b.logger)83+ srv := newManagedServer(name, srvCfg, b.cwd, b.logger)
82 b.servers[name] = srv84 b.servers[name] = srv
83 }85 }
84 }86 }
M examples/extensions/mcp-bridge/bridge_test.go +12 -0
@@ -12,6 +12,7 @@ import (
1212
1313 func testBridge() *bridge {
1414 return &bridge{
15+ cwd: "/project",
1516 servers: map[string]*managedServer{},
1617 mapping: map[string]toolMapping{},
1718 logger: log.New(io.Discard, "", 0),
@@ -30,6 +31,7 @@ func TestHandleToolCallStartFailureIsFriendlyError(t *testing.T) {
3031 b := testBridge()
3132 b.servers["bogus"] = newManagedServer("bogus",
3233 ServerConfig{Transport: "no-such-transport", ConnectTimeout: 1, RequestTimeout: 1},
34+ "/project",
3335 log.New(io.Discard, "", 0))
3436 b.mapping["mcp__bogus__t"] = toolMapping{serverName: "bogus", mcpTool: "t"}
3537
@@ -39,6 +41,16 @@ func TestHandleToolCallStartFailureIsFriendlyError(t *testing.T) {
3941 }
4042 }
4143
44+func TestLoadServersPassesProjectCWD(t *testing.T) {
45+ b := testBridge()
46+ b.loadServers(Config{MCPServers: map[string]ServerConfig{
47+ "fs": {Transport: "stdio", Command: "dummy"},
48+ }})
49+ if got := b.servers["fs"].cwd; got != "/project" {
50+ t.Fatalf("managed server cwd = %q, want /project", got)
51+ }
52+}
53+
4254 func TestClassifyTimeoutUsesErrorsIs(t *testing.T) {
4355 wrapped := errors.Join(errors.New("call failed"), context.DeadlineExceeded)
4456 if !errors.Is(wrapped, context.DeadlineExceeded) {
@@ -12,6 +12,7 @@ import (
12 12
13 func testBridge() *bridge {13 func testBridge() *bridge {
14 return &bridge{14 return &bridge{
15+ cwd: "/project",
15 servers: map[string]*managedServer{},16 servers: map[string]*managedServer{},
16 mapping: map[string]toolMapping{},17 mapping: map[string]toolMapping{},
17 logger: log.New(io.Discard, "", 0),18 logger: log.New(io.Discard, "", 0),
@@ -30,6 +31,7 @@ func TestHandleToolCallStartFailureIsFriendlyError(t *testing.T) {
30 b := testBridge()31 b := testBridge()
31 b.servers["bogus"] = newManagedServer("bogus",32 b.servers["bogus"] = newManagedServer("bogus",
32 ServerConfig{Transport: "no-such-transport", ConnectTimeout: 1, RequestTimeout: 1},33 ServerConfig{Transport: "no-such-transport", ConnectTimeout: 1, RequestTimeout: 1},
34+ "/project",
33 log.New(io.Discard, "", 0))35 log.New(io.Discard, "", 0))
34 b.mapping["mcp__bogus__t"] = toolMapping{serverName: "bogus", mcpTool: "t"}36 b.mapping["mcp__bogus__t"] = toolMapping{serverName: "bogus", mcpTool: "t"}
35 37
@@ -39,6 +41,16 @@ func TestHandleToolCallStartFailureIsFriendlyError(t *testing.T) {
39 }41 }
40 }42 }
41 43
44+func TestLoadServersPassesProjectCWD(t *testing.T) {
45+ b := testBridge()
46+ b.loadServers(Config{MCPServers: map[string]ServerConfig{
47+ "fs": {Transport: "stdio", Command: "dummy"},
48+ }})
49+ if got := b.servers["fs"].cwd; got != "/project" {
50+ t.Fatalf("managed server cwd = %q, want /project", got)
51+ }
52+}
53+
42 func TestClassifyTimeoutUsesErrorsIs(t *testing.T) {54 func TestClassifyTimeoutUsesErrorsIs(t *testing.T) {
43 wrapped := errors.Join(errors.New("call failed"), context.DeadlineExceeded)55 wrapped := errors.Join(errors.New("call failed"), context.DeadlineExceeded)
44 if !errors.Is(wrapped, context.DeadlineExceeded) {56 if !errors.Is(wrapped, context.DeadlineExceeded) {
M examples/extensions/mcp-bridge/main.go +1 -1
@@ -76,7 +76,7 @@ func main() {
7676
7777 logger.Printf("found %d MCP server(s)", len(cfg.MCPServers))
7878
79- b = newBridge(e, logger)
79+ b = newBridge(e, cwd, logger)
8080 b.loadServers(cfg)
8181
8282 cachePath := toolCachePath()
@@ -76,7 +76,7 @@ func main() {
76 76
77 logger.Printf("found %d MCP server(s)", len(cfg.MCPServers))77 logger.Printf("found %d MCP server(s)", len(cfg.MCPServers))
78 78
79- b = newBridge(e, logger)79+ b = newBridge(e, cwd, logger)
80 b.loadServers(cfg)80 b.loadServers(cfg)
81 81
82 cachePath := toolCachePath()82 cachePath := toolCachePath()
M examples/extensions/mcp-bridge/server.go +24 -3
@@ -16,6 +16,8 @@ import (
1616 "fmt"
1717 "io"
1818 "log"
19+ "os"
20+ "os/exec"
1921 "strings"
2022 "sync"
2123 "time"
@@ -54,6 +56,7 @@ func (s serverState) String() string {
5456 type managedServer struct {
5557 name string
5658 config ServerConfig
59+ cwd string
5760 logger *log.Logger
5861
5962 mu sync.Mutex
@@ -66,10 +69,11 @@ type managedServer struct {
6669 }
6770
6871 // newManagedServer creates a new server wrapper.
69-func newManagedServer(name string, cfg ServerConfig, logger *log.Logger) *managedServer {
72+func newManagedServer(name string, cfg ServerConfig, cwd string, logger *log.Logger) *managedServer {
7073 return &managedServer{
7174 name: name,
7275 config: cfg,
76+ cwd: cwd,
7377 logger: logger,
7478 state: stateStopped,
7579 lastUsed: time.Now(),
@@ -186,8 +190,25 @@ func (s *managedServer) startStdio() (*client.Client, error) {
186190 env = append(env, fmt.Sprintf("%s=%s", k, v))
187191 }
188192
189- // Create stdio client (this spawns the subprocess)
190- c, err := client.NewStdioMCPClient(s.config.Command, env, s.config.Args...)
193+ // Create stdio client (this spawns the subprocess). Run stdio MCP
194+ // servers from the zot session's project directory, not the installed
195+ // extension directory. Many MCP servers use their process cwd as their
196+ // project root when answering relative-path or directory-listing
197+ // requests, so inheriting mcp-bridge's cwd makes them operate on the
198+ // extension install instead of the user's project.
199+ c, err := client.NewStdioMCPClientWithOptions(
200+ s.config.Command,
201+ env,
202+ s.config.Args,
203+ transport.WithCommandFunc(func(ctx context.Context, command string, env []string, args []string) (*exec.Cmd, error) {
204+ cmd := exec.CommandContext(ctx, command, args...)
205+ cmd.Env = append(os.Environ(), env...)
206+ if s.cwd != "" {
207+ cmd.Dir = s.cwd
208+ }
209+ return cmd, nil
210+ }),
211+ )
191212 if err != nil {
192213 return nil, fmt.Errorf("spawn %s: %w", s.config.Command, err)
193214 }
@@ -16,6 +16,8 @@ import (
16 "fmt"16 "fmt"
17 "io"17 "io"
18 "log"18 "log"
19+ "os"
20+ "os/exec"
19 "strings"21 "strings"
20 "sync"22 "sync"
21 "time"23 "time"
@@ -54,6 +56,7 @@ func (s serverState) String() string {
54 type managedServer struct {56 type managedServer struct {
55 name string57 name string
56 config ServerConfig58 config ServerConfig
59+ cwd string
57 logger *log.Logger60 logger *log.Logger
58 61
59 mu sync.Mutex62 mu sync.Mutex
@@ -66,10 +69,11 @@ type managedServer struct {
66 }69 }
67 70
68 // newManagedServer creates a new server wrapper.71 // newManagedServer creates a new server wrapper.
69-func newManagedServer(name string, cfg ServerConfig, logger *log.Logger) *managedServer {72+func newManagedServer(name string, cfg ServerConfig, cwd string, logger *log.Logger) *managedServer {
70 return &managedServer{73 return &managedServer{
71 name: name,74 name: name,
72 config: cfg,75 config: cfg,
76+ cwd: cwd,
73 logger: logger,77 logger: logger,
74 state: stateStopped,78 state: stateStopped,
75 lastUsed: time.Now(),79 lastUsed: time.Now(),
@@ -186,8 +190,25 @@ func (s *managedServer) startStdio() (*client.Client, error) {
186 env = append(env, fmt.Sprintf("%s=%s", k, v))190 env = append(env, fmt.Sprintf("%s=%s", k, v))
187 }191 }
188 192
189- // Create stdio client (this spawns the subprocess)193+ // Create stdio client (this spawns the subprocess). Run stdio MCP
190- c, err := client.NewStdioMCPClient(s.config.Command, env, s.config.Args...)194+ // servers from the zot session's project directory, not the installed
195+ // extension directory. Many MCP servers use their process cwd as their
196+ // project root when answering relative-path or directory-listing
197+ // requests, so inheriting mcp-bridge's cwd makes them operate on the
198+ // extension install instead of the user's project.
199+ c, err := client.NewStdioMCPClientWithOptions(
200+ s.config.Command,
201+ env,
202+ s.config.Args,
203+ transport.WithCommandFunc(func(ctx context.Context, command string, env []string, args []string) (*exec.Cmd, error) {
204+ cmd := exec.CommandContext(ctx, command, args...)
205+ cmd.Env = append(os.Environ(), env...)
206+ if s.cwd != "" {
207+ cmd.Dir = s.cwd
208+ }
209+ return cmd, nil
210+ }),
211+ )
191 if err != nil {212 if err != nil {
192 return nil, fmt.Errorf("spawn %s: %w", s.config.Command, err)213 return nil, fmt.Errorf("spawn %s: %w", s.config.Command, err)
193 }214 }
M examples/extensions/mcp-bridge/status_test.go +9 -9
@@ -16,7 +16,7 @@ import (
1616 )
1717
1818 func TestManagedServerStatusCompact(t *testing.T) {
19- s := newManagedServer("grep", ServerConfig{}, log.New(io.Discard, "", 0))
19+ s := newManagedServer("grep", ServerConfig{}, "", log.New(io.Discard, "", 0))
2020
2121 s.state = stateReady
2222 s.tools = []mcp.Tool{{Name: "searchGitHub"}}
@@ -38,10 +38,10 @@ func TestManagedServerStatusCompact(t *testing.T) {
3838
3939 func TestFormatStatusSummaryCompact(t *testing.T) {
4040 b := &bridge{servers: map[string]*managedServer{}}
41- b.servers["grep"] = newManagedServer("grep", ServerConfig{}, log.New(io.Discard, "", 0))
41+ b.servers["grep"] = newManagedServer("grep", ServerConfig{}, "", log.New(io.Discard, "", 0))
4242 b.servers["grep"].state = stateReady
4343 b.servers["grep"].tools = []mcp.Tool{{Name: "searchGitHub"}}
44- b.servers["n8n-mcp"] = newManagedServer("n8n-mcp", ServerConfig{}, log.New(io.Discard, "", 0))
44+ b.servers["n8n-mcp"] = newManagedServer("n8n-mcp", ServerConfig{}, "", log.New(io.Discard, "", 0))
4545 b.servers["n8n-mcp"].state = stateReady
4646 b.servers["n8n-mcp"].tools = make([]mcp.Tool, 26)
4747
@@ -54,7 +54,7 @@ func TestFormatStatusSummaryCompact(t *testing.T) {
5454 }
5555
5656 func TestManagedServerStopBeforeStartDoesNotPanic(t *testing.T) {
57- s := newManagedServer("never-started", ServerConfig{}, log.New(io.Discard, "", 0))
57+ s := newManagedServer("never-started", ServerConfig{}, "", log.New(io.Discard, "", 0))
5858 s.stop()
5959 if got, want := s.state, stateStopped; got != want {
6060 t.Fatalf("state = %s, want %s", got, want)
@@ -98,10 +98,10 @@ func TestCompactErrShortensConnectionFailures(t *testing.T) {
9898
9999 func TestNotifyLevelWarnOnPartialFailure(t *testing.T) {
100100 b := &bridge{servers: map[string]*managedServer{}}
101- b.servers["grep"] = newManagedServer("grep", ServerConfig{}, log.New(io.Discard, "", 0))
101+ b.servers["grep"] = newManagedServer("grep", ServerConfig{}, "", log.New(io.Discard, "", 0))
102102 b.servers["grep"].state = stateReady
103103 b.servers["grep"].tools = []mcp.Tool{{Name: "searchGitHub"}}
104- b.servers["broken"] = newManagedServer("broken", ServerConfig{}, log.New(io.Discard, "", 0))
104+ b.servers["broken"] = newManagedServer("broken", ServerConfig{}, "", log.New(io.Discard, "", 0))
105105 b.servers["broken"].state = stateError
106106 b.servers["broken"].startErr = errors.New("authorization required\nmore detail")
107107
@@ -114,7 +114,7 @@ func TestNotifyLevelWarnOnPartialFailure(t *testing.T) {
114114 }
115115
116116 func TestStopDuringStartDiscardsStaleResult(t *testing.T) {
117- s := newManagedServer("racy", ServerConfig{}, log.New(io.Discard, "", 0))
117+ s := newManagedServer("racy", ServerConfig{}, "", log.New(io.Discard, "", 0))
118118
119119 // Simulate the window inside start(): state is starting, gen recorded.
120120 s.mu.Lock()
@@ -142,7 +142,7 @@ func TestStopDuringStartDiscardsStaleResult(t *testing.T) {
142142 }
143143
144144 func TestCallToolNilClientReturnsErrorNotPanic(t *testing.T) {
145- s := newManagedServer("bogus", ServerConfig{Transport: "no-such-transport", RequestTimeout: 1, ConnectTimeout: 1}, log.New(io.Discard, "", 0))
145+ s := newManagedServer("bogus", ServerConfig{Transport: "no-such-transport", RequestTimeout: 1, ConnectTimeout: 1}, "", log.New(io.Discard, "", 0))
146146 _, err := s.callTool(context.Background(), "anything", nil)
147147 if err == nil {
148148 t.Fatal("expected error from unknown transport")
@@ -151,7 +151,7 @@ func TestCallToolNilClientReturnsErrorNotPanic(t *testing.T) {
151151
152152 func TestPipeStderrKeepsLinesIntactAcrossChunks(t *testing.T) {
153153 var buf bytes.Buffer
154- s := newManagedServer("srv", ServerConfig{}, log.New(&buf, "", 0))
154+ s := newManagedServer("srv", ServerConfig{}, "", log.New(&buf, "", 0))
155155
156156 // iotest.OneByteReader forces 1-byte reads: the old 4096-byte
157157 // chunking code would log every byte as its own "line".
@@ -16,7 +16,7 @@ import (
16 )16 )
17 17
18 func TestManagedServerStatusCompact(t *testing.T) {18 func TestManagedServerStatusCompact(t *testing.T) {
19- s := newManagedServer("grep", ServerConfig{}, log.New(io.Discard, "", 0))19+ s := newManagedServer("grep", ServerConfig{}, "", log.New(io.Discard, "", 0))
20 20
21 s.state = stateReady21 s.state = stateReady
22 s.tools = []mcp.Tool{{Name: "searchGitHub"}}22 s.tools = []mcp.Tool{{Name: "searchGitHub"}}
@@ -38,10 +38,10 @@ func TestManagedServerStatusCompact(t *testing.T) {
38 38
39 func TestFormatStatusSummaryCompact(t *testing.T) {39 func TestFormatStatusSummaryCompact(t *testing.T) {
40 b := &bridge{servers: map[string]*managedServer{}}40 b := &bridge{servers: map[string]*managedServer{}}
41- b.servers["grep"] = newManagedServer("grep", ServerConfig{}, log.New(io.Discard, "", 0))41+ b.servers["grep"] = newManagedServer("grep", ServerConfig{}, "", log.New(io.Discard, "", 0))
42 b.servers["grep"].state = stateReady42 b.servers["grep"].state = stateReady
43 b.servers["grep"].tools = []mcp.Tool{{Name: "searchGitHub"}}43 b.servers["grep"].tools = []mcp.Tool{{Name: "searchGitHub"}}
44- b.servers["n8n-mcp"] = newManagedServer("n8n-mcp", ServerConfig{}, log.New(io.Discard, "", 0))44+ b.servers["n8n-mcp"] = newManagedServer("n8n-mcp", ServerConfig{}, "", log.New(io.Discard, "", 0))
45 b.servers["n8n-mcp"].state = stateReady45 b.servers["n8n-mcp"].state = stateReady
46 b.servers["n8n-mcp"].tools = make([]mcp.Tool, 26)46 b.servers["n8n-mcp"].tools = make([]mcp.Tool, 26)
47 47
@@ -54,7 +54,7 @@ func TestFormatStatusSummaryCompact(t *testing.T) {
54 }54 }
55 55
56 func TestManagedServerStopBeforeStartDoesNotPanic(t *testing.T) {56 func TestManagedServerStopBeforeStartDoesNotPanic(t *testing.T) {
57- s := newManagedServer("never-started", ServerConfig{}, log.New(io.Discard, "", 0))57+ s := newManagedServer("never-started", ServerConfig{}, "", log.New(io.Discard, "", 0))
58 s.stop()58 s.stop()
59 if got, want := s.state, stateStopped; got != want {59 if got, want := s.state, stateStopped; got != want {
60 t.Fatalf("state = %s, want %s", got, want)60 t.Fatalf("state = %s, want %s", got, want)
@@ -98,10 +98,10 @@ func TestCompactErrShortensConnectionFailures(t *testing.T) {
98 98
99 func TestNotifyLevelWarnOnPartialFailure(t *testing.T) {99 func TestNotifyLevelWarnOnPartialFailure(t *testing.T) {
100 b := &bridge{servers: map[string]*managedServer{}}100 b := &bridge{servers: map[string]*managedServer{}}
101- b.servers["grep"] = newManagedServer("grep", ServerConfig{}, log.New(io.Discard, "", 0))101+ b.servers["grep"] = newManagedServer("grep", ServerConfig{}, "", log.New(io.Discard, "", 0))
102 b.servers["grep"].state = stateReady102 b.servers["grep"].state = stateReady
103 b.servers["grep"].tools = []mcp.Tool{{Name: "searchGitHub"}}103 b.servers["grep"].tools = []mcp.Tool{{Name: "searchGitHub"}}
104- b.servers["broken"] = newManagedServer("broken", ServerConfig{}, log.New(io.Discard, "", 0))104+ b.servers["broken"] = newManagedServer("broken", ServerConfig{}, "", log.New(io.Discard, "", 0))
105 b.servers["broken"].state = stateError105 b.servers["broken"].state = stateError
106 b.servers["broken"].startErr = errors.New("authorization required\nmore detail")106 b.servers["broken"].startErr = errors.New("authorization required\nmore detail")
107 107
@@ -114,7 +114,7 @@ func TestNotifyLevelWarnOnPartialFailure(t *testing.T) {
114 }114 }
115 115
116 func TestStopDuringStartDiscardsStaleResult(t *testing.T) {116 func TestStopDuringStartDiscardsStaleResult(t *testing.T) {
117- s := newManagedServer("racy", ServerConfig{}, log.New(io.Discard, "", 0))117+ s := newManagedServer("racy", ServerConfig{}, "", log.New(io.Discard, "", 0))
118 118
119 // Simulate the window inside start(): state is starting, gen recorded.119 // Simulate the window inside start(): state is starting, gen recorded.
120 s.mu.Lock()120 s.mu.Lock()
@@ -142,7 +142,7 @@ func TestStopDuringStartDiscardsStaleResult(t *testing.T) {
142 }142 }
143 143
144 func TestCallToolNilClientReturnsErrorNotPanic(t *testing.T) {144 func TestCallToolNilClientReturnsErrorNotPanic(t *testing.T) {
145- s := newManagedServer("bogus", ServerConfig{Transport: "no-such-transport", RequestTimeout: 1, ConnectTimeout: 1}, log.New(io.Discard, "", 0))145+ s := newManagedServer("bogus", ServerConfig{Transport: "no-such-transport", RequestTimeout: 1, ConnectTimeout: 1}, "", log.New(io.Discard, "", 0))
146 _, err := s.callTool(context.Background(), "anything", nil)146 _, err := s.callTool(context.Background(), "anything", nil)
147 if err == nil {147 if err == nil {
148 t.Fatal("expected error from unknown transport")148 t.Fatal("expected error from unknown transport")
@@ -151,7 +151,7 @@ func TestCallToolNilClientReturnsErrorNotPanic(t *testing.T) {
151 151
152 func TestPipeStderrKeepsLinesIntactAcrossChunks(t *testing.T) {152 func TestPipeStderrKeepsLinesIntactAcrossChunks(t *testing.T) {
153 var buf bytes.Buffer153 var buf bytes.Buffer
154- s := newManagedServer("srv", ServerConfig{}, log.New(&buf, "", 0))154+ s := newManagedServer("srv", ServerConfig{}, "", log.New(&buf, "", 0))
155 155
156 // iotest.OneByteReader forces 1-byte reads: the old 4096-byte156 // iotest.OneByteReader forces 1-byte reads: the old 4096-byte
157 // chunking code would log every byte as its own "line".157 // chunking code would log every byte as its own "line".

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 476

↑↓ moveenter openesc close