diff --git a/issues.go b/issues.go index 9649590..2acd264 100644 --- a/issues.go +++ b/issues.go @@ -50,6 +50,11 @@ func NewIssue(ctx context.Context, client *github.Client, settings *Settings, in log.Fatal("title cannot be empty") } + description, err = PostProcessDescription(ctx, settings, description) + if err != nil { + return 0, err + } + gir = &github.IssueRequest{ Title: &title, Body: &description, @@ -73,6 +78,44 @@ func NewIssue(ctx context.Context, client *github.Client, settings *Settings, in return *i.Number, nil } +// PostProcessDescription runs the configured PostProcess hook against the +// supplied description and returns the processed result. The description is +// written to a temp file (the hook edits files in place, matching the +// interactive flow) and the file contents are read back afterwards. If no +// PostProcess hook is configured the description is returned unchanged. +func PostProcessDescription(ctx context.Context, settings *Settings, description string) (string, error) { + if settings.PostProcess == "" { + return description, nil + } + + tempFile, err := os.CreateTemp("", "git-open-pull") + if err != nil { + return "", err + } + defer os.Remove(tempFile.Name()) + + if _, err := io.WriteString(tempFile, description); err != nil { + tempFile.Close() + return "", err + } + if err := tempFile.Close(); err != nil { + return "", err + } + + cmd := exec.CommandContext(ctx, settings.PostProcess, tempFile.Name()) + out, err := cmd.CombinedOutput() + if err != nil { + log.Printf("error running post process template: %s\n error: %v\n output: %s", settings.PostProcess, err, out) + return "", err + } + + processed, err := os.ReadFile(tempFile.Name()) + if err != nil { + return "", err + } + return string(processed), nil +} + // PopulateIssueInteractive creates a template, parses the template and returns the Issue number if the user is in interactive mode func PopulateIssueInteractive(ctx context.Context, client *github.Client, settings *Settings, inputTitle, inputDescription string, labelSlice []string) (ir *github.IssueRequest, err error) { labels, err := Labels(ctx, client, settings) diff --git a/issues_test.go b/issues_test.go index 77e490b..9a251ea 100644 --- a/issues_test.go +++ b/issues_test.go @@ -1,7 +1,10 @@ package main import ( + "context" "fmt" + "os" + "path/filepath" "testing" ) @@ -30,3 +33,51 @@ func TestDetectIssueNumber(t *testing.T) { }) } } + +func TestPostProcessDescription(t *testing.T) { + type testCase struct { + name string + hook string // shell script body; empty means no hook configured + input string + expected string + expectErr bool + } + tests := []testCase{ + {"no hook configured", "", "resolves II-3573\n", "resolves II-3573\n", false}, + { + "hook edits file in place", + `sed -i 's#II-3573#[II-3573](https://example.test/browse/II-3573)#' "$1"`, + "resolves II-3573\n", + "resolves [II-3573](https://example.test/browse/II-3573)\n", + false, + }, + {"hook failure returns error", "exit 1", "resolves II-3573\n", "", true}, + } + for i, tc := range tests { + tc := tc + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + t.Parallel() + settings := &Settings{} + if tc.hook != "" { + hook := filepath.Join(t.TempDir(), "hook.sh") + if err := os.WriteFile(hook, []byte("#!/bin/sh\n"+tc.hook+"\n"), 0o755); err != nil { + t.Fatal(err) + } + settings.PostProcess = hook + } + got, err := PostProcessDescription(context.Background(), settings, tc.input) + if tc.expectErr { + if err == nil { + t.Errorf("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tc.expected { + t.Errorf("got %q expected %q", got, tc.expected) + } + }) + } +}