main 6af0a61

exec.go RawHistoryFolder

1//nolint:errcheck
2package system
3
4import (
5	"bufio"
6	"fmt"
7	"os"
8	"os/exec"
9	"strings"
10	"sync"
11)
12
13// CommandResult holds the result of a command execution
14type CommandResult struct {
15	ExitCode int
16	Output   string
17	Error    error
18}
19
20// RunConfig configures how a command should be executed
21type RunConfig struct {
22	Command     string
23	Args        []string
24	StreamToLog bool
25	LogPath     string
26	WorkingDir  string
27	Env         []string
28}
29
30// Run executes a command with the given configuration
31func Run(cfg RunConfig) CommandResult {
32	cmd := exec.Command(cfg.Command, cfg.Args...)
33
34	if cfg.WorkingDir != "" {
35		cmd.Dir = cfg.WorkingDir
36	}
37
38	if len(cfg.Env) > 0 {
39		cmd.Env = append(os.Environ(), cfg.Env...)
40	}
41
42	// Create pipes for stdout and stderr
43	stdout, err := cmd.StdoutPipe()
44	if err != nil {
45		return CommandResult{
46			ExitCode: -1,
47			Error:    fmt.Errorf("failed to create stdout pipe: %w", err),
48		}
49	}
50
51	stderr, err := cmd.StderrPipe()
52	if err != nil {
53		return CommandResult{
54			ExitCode: -1,
55			Error:    fmt.Errorf("failed to create stderr pipe: %w", err),
56		}
57	}
58
59	// Start the command
60	if err := cmd.Start(); err != nil {
61		return CommandResult{
62			ExitCode: -1,
63			Error:    fmt.Errorf("failed to start command: %w", err),
64		}
65	}
66
67	var wg sync.WaitGroup
68	var output strings.Builder
69	var logFile *os.File
70
71	// Open log file if streaming is enabled
72	if cfg.StreamToLog && cfg.LogPath != "" {
73		logFile, err = os.OpenFile(cfg.LogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
74		if err != nil {
75			// Continue without logging if we can't open the file
76			fmt.Fprintf(os.Stderr, "Warning: failed to open log file: %v\n", err)
77		} else {
78			defer logFile.Close()
79		}
80	}
81
82	// Process stdout
83	wg.Add(1)
84	go func() {
85		defer wg.Done()
86		scanner := bufio.NewScanner(stdout)
87		for scanner.Scan() {
88			line := scanner.Text()
89			output.WriteString(line + "\n")
90			if logFile != nil {
91				if _, err := fmt.Fprintln(logFile, line); err != nil {
92					fmt.Fprintf(os.Stderr, "failed to write log file: %v\n", err)
93				}
94				if err := logFile.Sync(); err != nil {
95					fmt.Fprintf(os.Stderr, "failed to sync log file: %v\n", err)
96				} // Flush to disk immediately
97			}
98		}
99	}()
100
101	// Process stderr
102	wg.Add(1)
103	go func() {
104		defer wg.Done()
105		scanner := bufio.NewScanner(stderr)
106		for scanner.Scan() {
107			line := scanner.Text()
108			output.WriteString(line + "\n")
109			if logFile != nil {
110				if _, err := fmt.Fprintln(logFile, line); err != nil {
111					fmt.Fprintf(os.Stderr, "failed to write log file: %v\n", err)
112				}
113				if err := logFile.Sync(); err != nil {
114					fmt.Fprintf(os.Stderr, "failed to sync log file: %v\n", err)
115				} // Flush to disk immediately
116			}
117		}
118	}()
119
120	// Wait for output processing to complete
121	wg.Wait()
122
123	// Wait for command to finish
124	err = cmd.Wait()
125
126	exitCode := 0
127	if err != nil {
128		if exitError, ok := err.(*exec.ExitError); ok {
129			exitCode = exitError.ExitCode()
130		} else {
131			exitCode = -1
132		}
133	}
134
135	return CommandResult{
136		ExitCode: exitCode,
137		Output:   output.String(),
138		Error:    err,
139	}
140}
141
142// RunSimple is a convenience function for simple command execution
143func RunSimple(command string, args ...string) CommandResult {
144	return Run(RunConfig{
145		Command: command,
146		Args:    args,
147	})
148}
149
150// RunLogged runs a command and streams output to the log file
151func RunLogged(logPath, command string, args ...string) CommandResult {
152	return Run(RunConfig{
153		Command:     command,
154		Args:        args,
155		StreamToLog: true,
156		LogPath:     logPath,
157	})
158}

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 0

    ↑↓ moveenter openesc close