Skip to content

Commit 6358076

Browse files
authored
hotfix: do not panic when out-of-repo directory (#5)
* hotfix: do not panic when out-of-repo directory * panic recovery
1 parent 1bcf40f commit 6358076

File tree

6 files changed

+259
-160
lines changed

6 files changed

+259
-160
lines changed

cmd/git-undo/main.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"fmt"
55
"os"
66

7-
gitundo "github.com/amberpixels/git-undo"
87
"github.com/amberpixels/git-undo/internal/app"
98
)
109

@@ -23,9 +22,7 @@ func main() {
2322
}
2423
}
2524

26-
application := app.New(".", version, verbose, dryRun)
27-
// Set embedded scripts from root package
28-
app.SetEmbeddedScripts(application, gitundo.GetUpdateScript(), gitundo.GetUninstallScript())
25+
application := app.New(version, verbose, dryRun)
2926

3027
if err := application.Run(os.Args[1:]); err != nil {
3128
_, _ = fmt.Fprintln(os.Stderr, redColor+"git-undo ❌: "+grayColor+err.Error()+resetColor)

internal/app/app.go

Lines changed: 45 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import (
44
"errors"
55
"fmt"
66
"os"
7-
"os/exec"
87
"strings"
98

9+
gitundo "github.com/amberpixels/git-undo"
1010
"github.com/amberpixels/git-undo/internal/git-undo/logging"
1111
"github.com/amberpixels/git-undo/internal/git-undo/undoer"
1212
"github.com/amberpixels/git-undo/internal/githelpers"
@@ -23,23 +23,16 @@ type GitHelper interface {
2323

2424
// App represents the main app.
2525
type App struct {
26-
verbose bool
27-
dryRun bool
28-
29-
git GitHelper
26+
verbose bool
27+
dryRun bool
28+
buildVersion string
3029

31-
lgr *logging.Logger
30+
dir string
3231

3332
// isInternalCall is a hack, so app works OK even without GIT_UNDO_INTERNAL_HOOK env variable.
3433
// So, we can run tests without setting env vars (but just via setting this flag).
34+
// Note: here it's read-only flag, and it's only set in export_test.go
3535
isInternalCall bool
36-
37-
// Embedded scripts for self-management
38-
updateScript string
39-
uninstallScript string
40-
41-
// Build-time version info
42-
buildVersion string
4336
}
4437

4538
// IsInternalCall checks if the hook is being called internally (either via test or zsh script).
@@ -53,20 +46,12 @@ func (a *App) IsInternalCall() bool {
5346
}
5447

5548
// New creates a new App instance.
56-
func New(repoDir string, version string, verbose, dryRun bool) *App {
57-
gitHelper := githelpers.NewGitHelper(repoDir)
58-
gitDir, err := gitHelper.GetRepoGitDir()
59-
if err != nil {
60-
fmt.Fprintf(os.Stderr, redColor+"git-undo ❌: "+grayColor+"failed to get repo git dir: %v"+resetColor+"\n", err)
61-
return nil
62-
}
63-
49+
func New(version string, verbose, dryRun bool) *App {
6450
return &App{
51+
dir: ".",
6552
buildVersion: version,
6653
verbose: verbose,
6754
dryRun: dryRun,
68-
git: gitHelper,
69-
lgr: logging.NewLogger(gitDir, gitHelper),
7055
}
7156
}
7257

@@ -84,77 +69,63 @@ func (a *App) logDebugf(format string, args ...interface{}) {
8469
return
8570
}
8671

87-
fmt.Fprintf(os.Stderr, yellowColor+"git-undo ⚙️: "+grayColor+format+resetColor+"\n", args...)
72+
_, _ = fmt.Fprintf(os.Stderr, yellowColor+"git-undo ⚙️: "+grayColor+format+resetColor+"\n", args...)
8873
}
8974

9075
// logWarnf writes error messages to stderr.
9176
func (a *App) logWarnf(format string, args ...interface{}) {
92-
fmt.Fprintf(os.Stderr, redColor+"git-undo ❌: "+grayColor+format+resetColor+"\n", args...)
77+
_, _ = fmt.Fprintf(os.Stderr, redColor+"git-undo ❌: "+grayColor+format+resetColor+"\n", args...)
9378
}
9479

9580
// Run executes the main app logic.
96-
func (a *App) Run(args []string) error {
81+
func (a *App) Run(args []string) (err error) {
9782
a.logDebugf("called in verbose mode")
9883

99-
// Handle version commands first (these don't require git repo)
100-
if len(args) >= 1 {
101-
firstArg := args[0]
102-
103-
// Handle version commands: version, --version, self-version
104-
if firstArg == "version" || firstArg == "--version" || firstArg == "self-version" {
105-
return a.cmdVersion()
84+
defer func() {
85+
if recovered := recover(); recovered != nil {
86+
a.logDebugf("git-undo panic recovery: %v", recovered)
87+
err = fmt.Errorf("unexpected internal failure")
10688
}
89+
}()
10790

108-
// Handle "self version"
109-
//nolint:goconst // we're fine with this for now
110-
if len(args) >= 2 && firstArg == "self" && args[1] == "version" {
111-
return a.cmdVersion()
112-
}
113-
}
91+
selfCtrl := NewSelfController(a.buildVersion, a.verbose).
92+
AddScript(CommandUpdate, gitundo.GetUpdateScript()).
93+
AddScript(CommandUninstall, gitundo.GetUninstallScript())
11494

115-
// Handle self-management commands (these don't require git repo)
116-
if len(args) >= 2 {
117-
firstArg := args[0]
118-
secondArg := args[1]
95+
if err := selfCtrl.HandleSelfCommand(args); err == nil {
96+
return nil
97+
} else if !errors.Is(err, ErrNotSelfCommand) {
98+
return err
99+
}
119100

120-
// Handle "self update" or "self-update"
121-
if (firstArg == "self" && secondArg == "update") || firstArg == "self-update" {
122-
return a.cmdSelfUpdate()
123-
}
101+
g := githelpers.NewGitHelper(a.dir)
124102

125-
// Handle "self uninstall" or "self-uninstall"
126-
if (firstArg == "self" && secondArg == "uninstall") || firstArg == "self-uninstall" {
127-
return a.cmdSelfUninstall()
128-
}
129-
} else if len(args) == 1 {
130-
// Handle single argument forms
131-
if args[0] == "self-update" {
132-
return a.cmdSelfUpdate()
133-
}
134-
if args[0] == "self-uninstall" {
135-
return a.cmdSelfUninstall()
136-
}
103+
gitDir, err := g.GetRepoGitDir()
104+
if err != nil {
105+
// Silently return for non-git repos when not using self commands
106+
a.logDebugf("not in a git repository, ignoring command%v: %s", args, err)
107+
return nil
137108
}
138109

139-
// Ensure we're inside a Git repository for other commands
140-
if _, err := a.git.GetRepoGitDir(); err != nil {
141-
return err
110+
lgr := logging.NewLogger(gitDir, g)
111+
if lgr == nil {
112+
return errors.New("failed to create git-undo logger")
142113
}
143114

144115
// Custom commands are --hook and --log
145116
for _, arg := range args {
146117
switch {
147118
case strings.HasPrefix(arg, "--hook"):
148-
return a.cmdHook(arg)
119+
return a.cmdHook(lgr, arg)
149120
case arg == "--log":
150-
return a.cmdLog()
121+
return a.cmdLog(lgr)
151122
}
152123
}
153124

154125
// Check if this is a "git undo undo" command
155126
if len(args) > 0 && args[0] == "undo" {
156127
// Get the last undoed entry (from current reference)
157-
lastEntry, err := a.lgr.GetLastEntry()
128+
lastEntry, err := lgr.GetLastEntry()
158129
if err != nil {
159130
a.logWarnf("something wrong with the log: %v", err)
160131
return nil
@@ -165,7 +136,7 @@ func (a *App) Run(args []string) error {
165136
}
166137

167138
// Unmark the entry in the log
168-
if err := a.lgr.ToggleEntry(lastEntry.GetIdentifier()); err != nil {
139+
if err := lgr.ToggleEntry(lastEntry.GetIdentifier()); err != nil {
169140
return fmt.Errorf("failed to unmark command: %w", err)
170141
}
171142

@@ -180,7 +151,7 @@ func (a *App) Run(args []string) error {
180151
return fmt.Errorf("invalid last undo-ed cmd[%s]: %w", lastEntry.Command, validationErr)
181152
}
182153

183-
if err := a.git.GitRun(gitCmd.Name, gitCmd.Args...); err != nil {
154+
if err := g.GitRun(gitCmd.Name, gitCmd.Args...); err != nil {
184155
return fmt.Errorf("failed to redo command[%s]: %w", lastEntry.Command, err)
185156
}
186157

@@ -189,7 +160,7 @@ func (a *App) Run(args []string) error {
189160
}
190161

191162
// Get the last git command
192-
lastEntry, err := a.lgr.GetLastRegularEntry()
163+
lastEntry, err := lgr.GetLastRegularEntry()
193164
if err != nil {
194165
return fmt.Errorf("failed to get last git command: %w", err)
195166
}
@@ -201,7 +172,7 @@ func (a *App) Run(args []string) error {
201172
a.logDebugf("Last git command[%s]: %s", lastEntry.Ref, yellowColor+lastEntry.Command+resetColor)
202173

203174
// Get the appropriate undoer
204-
u := undoer.New(lastEntry.Command, a.git)
175+
u := undoer.New(lastEntry.Command, g)
205176

206177
// Get the undo command
207178
undoCmd, err := u.GetUndoCommand()
@@ -225,7 +196,7 @@ func (a *App) Run(args []string) error {
225196
}
226197

227198
// Mark the entry as undoed in the log
228-
if err := a.lgr.ToggleEntry(lastEntry.GetIdentifier()); err != nil {
199+
if err := lgr.ToggleEntry(lastEntry.GetIdentifier()); err != nil {
229200
a.logWarnf("Failed to mark command as undoed: %v", err)
230201
}
231202

@@ -238,7 +209,7 @@ func (a *App) Run(args []string) error {
238209
return nil
239210
}
240211

241-
func (a *App) cmdHook(hookArg string) error {
212+
func (a *App) cmdHook(lgr *logging.Logger, hookArg string) error {
242213
a.logDebugf("hook: start")
243214

244215
if !a.IsInternalCall() {
@@ -261,7 +232,7 @@ func (a *App) cmdHook(hookArg string) error {
261232
return nil
262233
}
263234

264-
if err := a.lgr.LogCommand(hooked); err != nil {
235+
if err := lgr.LogCommand(hooked); err != nil {
265236
return fmt.Errorf("failed to log command: %w", err)
266237
}
267238

@@ -270,69 +241,6 @@ func (a *App) cmdHook(hookArg string) error {
270241
}
271242

272243
// cmdLog displays the git-undo command log.
273-
func (a *App) cmdLog() error {
274-
return a.lgr.Dump(os.Stdout)
275-
}
276-
277-
// SetEmbeddedScripts sets the embedded scripts for self-management commands.
278-
func SetEmbeddedScripts(app *App, updateScript, uninstallScript string) {
279-
app.updateScript = updateScript
280-
app.uninstallScript = uninstallScript
281-
}
282-
283-
func (a *App) cmdSelfUpdate() error {
284-
a.logDebugf("Running embedded self-update script...")
285-
return a.runEmbeddedScript(a.updateScript, "update")
286-
}
287-
288-
func (a *App) cmdSelfUninstall() error {
289-
a.logDebugf("Running embedded self-uninstall script...")
290-
return a.runEmbeddedScript(a.uninstallScript, "uninstall")
291-
}
292-
293-
// runEmbeddedScript creates a temporary script file and executes it.
294-
func (a *App) runEmbeddedScript(script, name string) error {
295-
if script == "" {
296-
return fmt.Errorf("embedded %s script not available", name)
297-
}
298-
299-
// Create temp file with proper extension
300-
tmpFile, err := os.CreateTemp("", fmt.Sprintf("git-undo-%s-*.sh", name))
301-
if err != nil {
302-
return fmt.Errorf("failed to create temp script: %w", err)
303-
}
304-
defer func() {
305-
// TODO: handle error: log warnings at least
306-
_ = tmpFile.Close()
307-
_ = os.Remove(tmpFile.Name())
308-
}()
309-
310-
// Write script content
311-
if _, err := tmpFile.WriteString(script); err != nil {
312-
return fmt.Errorf("failed to write script: %w", err)
313-
}
314-
315-
// Close file before making it executable and running it
316-
_ = tmpFile.Close()
317-
318-
// Make executable
319-
//nolint:gosec // TODO: fix me in future
320-
if err := os.Chmod(tmpFile.Name(), 0755); err != nil {
321-
return fmt.Errorf("failed to make script executable: %w", err)
322-
}
323-
324-
a.logDebugf("Executing embedded %s script...", name)
325-
326-
// Execute script
327-
//nolint:gosec // TODO: fix me in future
328-
cmd := exec.Command("bash", tmpFile.Name())
329-
cmd.Stdout = os.Stdout
330-
cmd.Stderr = os.Stderr
331-
332-
return cmd.Run()
333-
}
334-
335-
func (a *App) cmdVersion() error {
336-
fmt.Fprintf(os.Stdout, "git-undo %s\n", a.buildVersion)
337-
return nil
244+
func (a *App) cmdLog(lgr *logging.Logger) error {
245+
return lgr.Dump(os.Stdout)
338246
}

0 commit comments

Comments
 (0)