procoder-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,224 @@
1
+ package app
2
+
3
+ import (
4
+ "fmt"
5
+ "io"
6
+ "strings"
7
+
8
+ "github.com/amxv/procoder/internal/apply"
9
+ "github.com/amxv/procoder/internal/errs"
10
+ "github.com/amxv/procoder/internal/prepare"
11
+ )
12
+
13
+ const commandName = "procoder"
14
+
15
+ var version = "dev"
16
+ var runPrepare = prepare.Run
17
+ var runApplyDryRun = apply.RunDryRun
18
+ var runApply = apply.Run
19
+
20
+ func Run(args []string, stdout, stderr io.Writer) error {
21
+ _ = stderr
22
+
23
+ if len(args) == 0 || isHelpArg(args[0]) {
24
+ printRootHelp(stdout)
25
+ return nil
26
+ }
27
+
28
+ switch args[0] {
29
+ case "version":
30
+ _, _ = fmt.Fprintf(stdout, "%s %s\n", commandName, version)
31
+ return nil
32
+ case "prepare":
33
+ if len(args) > 1 && isHelpArg(args[1]) {
34
+ printPrepareHelp(stdout)
35
+ return nil
36
+ }
37
+ if len(args) > 1 {
38
+ return errs.New(
39
+ errs.CodeUnknownCommand,
40
+ fmt.Sprintf("unknown argument %q for `procoder prepare`", args[1]),
41
+ errs.WithHint("run `procoder prepare --help`"),
42
+ )
43
+ }
44
+ result, err := runPrepare(prepare.Options{ToolVersion: version})
45
+ if err != nil {
46
+ return err
47
+ }
48
+ writeLines(stdout, prepare.FormatSuccess(result))
49
+ return nil
50
+ case "apply":
51
+ if len(args) > 1 && isHelpArg(args[1]) {
52
+ printApplyHelp(stdout)
53
+ return nil
54
+ }
55
+
56
+ parsed, err := parseApplyArgs(args[1:])
57
+ if err != nil {
58
+ return err
59
+ }
60
+
61
+ if parsed.DryRun {
62
+ plan, err := runApplyDryRun(apply.Options{
63
+ ReturnPackagePath: parsed.ReturnPackagePath,
64
+ Namespace: parsed.Namespace,
65
+ })
66
+ if err != nil {
67
+ return err
68
+ }
69
+ writeLines(stdout, apply.FormatDryRun(plan))
70
+ return nil
71
+ }
72
+
73
+ result, err := runApply(apply.Options{
74
+ ReturnPackagePath: parsed.ReturnPackagePath,
75
+ Namespace: parsed.Namespace,
76
+ Checkout: parsed.Checkout,
77
+ })
78
+ if err != nil {
79
+ return err
80
+ }
81
+ writeLines(stdout, apply.FormatSuccess(result))
82
+ return nil
83
+ default:
84
+ return errs.New(
85
+ errs.CodeUnknownCommand,
86
+ fmt.Sprintf("unknown command %q", args[0]),
87
+ errs.WithHint(fmt.Sprintf("run `%s --help`", commandName)),
88
+ )
89
+ }
90
+ }
91
+
92
+ type applyArgs struct {
93
+ ReturnPackagePath string
94
+ DryRun bool
95
+ Namespace string
96
+ Checkout bool
97
+ }
98
+
99
+ func parseApplyArgs(args []string) (applyArgs, error) {
100
+ parsed := applyArgs{}
101
+ for i := 0; i < len(args); i++ {
102
+ arg := args[i]
103
+ switch {
104
+ case arg == "--dry-run":
105
+ parsed.DryRun = true
106
+ case arg == "--checkout":
107
+ parsed.Checkout = true
108
+ case arg == "--namespace":
109
+ if i+1 >= len(args) {
110
+ return applyArgs{}, errs.New(
111
+ errs.CodeUnknownCommand,
112
+ "missing value for `--namespace`",
113
+ errs.WithHint("run `procoder apply --help`"),
114
+ )
115
+ }
116
+ i++
117
+ value := strings.TrimSpace(args[i])
118
+ if value == "" {
119
+ return applyArgs{}, errs.New(
120
+ errs.CodeUnknownCommand,
121
+ "namespace prefix for `--namespace` must not be empty",
122
+ errs.WithHint("run `procoder apply --help`"),
123
+ )
124
+ }
125
+ parsed.Namespace = value
126
+ case strings.HasPrefix(arg, "--namespace="):
127
+ value := strings.TrimSpace(strings.TrimPrefix(arg, "--namespace="))
128
+ if value == "" {
129
+ return applyArgs{}, errs.New(
130
+ errs.CodeUnknownCommand,
131
+ "namespace prefix for `--namespace` must not be empty",
132
+ errs.WithHint("run `procoder apply --help`"),
133
+ )
134
+ }
135
+ parsed.Namespace = value
136
+ case strings.HasPrefix(arg, "-"):
137
+ return applyArgs{}, unknownApplyArgument(arg)
138
+ default:
139
+ if parsed.ReturnPackagePath == "" {
140
+ parsed.ReturnPackagePath = arg
141
+ continue
142
+ }
143
+ return applyArgs{}, unknownApplyArgument(arg)
144
+ }
145
+ }
146
+
147
+ if strings.TrimSpace(parsed.ReturnPackagePath) == "" {
148
+ return applyArgs{}, errs.New(
149
+ errs.CodeUnknownCommand,
150
+ "missing return package path for `procoder apply`",
151
+ errs.WithHint("run `procoder apply --help`"),
152
+ )
153
+ }
154
+
155
+ return parsed, nil
156
+ }
157
+
158
+ func unknownApplyArgument(arg string) error {
159
+ return errs.New(
160
+ errs.CodeUnknownCommand,
161
+ fmt.Sprintf("unknown argument %q for `procoder apply`", arg),
162
+ errs.WithHint("run `procoder apply --help`"),
163
+ )
164
+ }
165
+
166
+ func isHelpArg(v string) bool {
167
+ switch v {
168
+ case "-h", "--help", "help":
169
+ return true
170
+ default:
171
+ return false
172
+ }
173
+ }
174
+
175
+ func printRootHelp(w io.Writer) {
176
+ writeLines(w,
177
+ "procoder",
178
+ "",
179
+ "Usage:",
180
+ " procoder <command> [arguments]",
181
+ "",
182
+ "Commands:",
183
+ " prepare create a task package",
184
+ " apply <return-package.zip> apply a return package",
185
+ " version print CLI version",
186
+ "",
187
+ "Examples:",
188
+ " procoder prepare",
189
+ " procoder apply procoder-return-<exchange-id>.zip --dry-run",
190
+ " procoder version",
191
+ )
192
+ }
193
+
194
+ func printPrepareHelp(w io.Writer) {
195
+ writeLines(w,
196
+ "procoder prepare - create a task package",
197
+ "",
198
+ "Usage:",
199
+ " procoder prepare",
200
+ "",
201
+ "Examples:",
202
+ " procoder prepare",
203
+ )
204
+ }
205
+
206
+ func printApplyHelp(w io.Writer) {
207
+ writeLines(w,
208
+ "procoder apply - apply a return package",
209
+ "",
210
+ "Usage:",
211
+ " procoder apply <return-package.zip> [--dry-run] [--namespace <prefix>] [--checkout]",
212
+ "",
213
+ "Examples:",
214
+ " procoder apply procoder-return-<exchange-id>.zip",
215
+ " procoder apply procoder-return-<exchange-id>.zip --dry-run",
216
+ " procoder apply procoder-return-<exchange-id>.zip --dry-run --namespace procoder-import",
217
+ )
218
+ }
219
+
220
+ func writeLines(w io.Writer, lines ...string) {
221
+ for _, line := range lines {
222
+ _, _ = fmt.Fprintln(w, line)
223
+ }
224
+ }
@@ -0,0 +1,253 @@
1
+ package app
2
+
3
+ import (
4
+ "bytes"
5
+ "strings"
6
+ "testing"
7
+
8
+ "github.com/amxv/procoder/internal/apply"
9
+ "github.com/amxv/procoder/internal/errs"
10
+ "github.com/amxv/procoder/internal/prepare"
11
+ )
12
+
13
+ func TestRunRootHelp(t *testing.T) {
14
+ var out bytes.Buffer
15
+ var errBuf bytes.Buffer
16
+
17
+ err := Run([]string{"--help"}, &out, &errBuf)
18
+ if err != nil {
19
+ t.Fatalf("Run returned error: %v", err)
20
+ }
21
+
22
+ if !strings.Contains(out.String(), "Usage:") {
23
+ t.Fatalf("expected help output, got: %q", out.String())
24
+ }
25
+ if !strings.Contains(out.String(), "prepare") {
26
+ t.Fatalf("expected prepare command in help output, got: %q", out.String())
27
+ }
28
+ }
29
+
30
+ func TestRunVersion(t *testing.T) {
31
+ var out bytes.Buffer
32
+ var errBuf bytes.Buffer
33
+
34
+ err := Run([]string{"version"}, &out, &errBuf)
35
+ if err != nil {
36
+ t.Fatalf("Run returned error: %v", err)
37
+ }
38
+
39
+ if !strings.Contains(out.String(), "procoder ") {
40
+ t.Fatalf("unexpected output: %q", out.String())
41
+ }
42
+ }
43
+
44
+ func TestRunApplyHelpTerminology(t *testing.T) {
45
+ var out bytes.Buffer
46
+ var errBuf bytes.Buffer
47
+
48
+ err := Run([]string{"apply", "--help"}, &out, &errBuf)
49
+ if err != nil {
50
+ t.Fatalf("Run returned error: %v", err)
51
+ }
52
+
53
+ got := out.String()
54
+ if !strings.Contains(got, "return package") {
55
+ t.Fatalf("expected return package terminology in apply help, got: %q", got)
56
+ }
57
+ if strings.Contains(got, "<reply.zip>") {
58
+ t.Fatalf("unexpected stale reply terminology in apply help, got: %q", got)
59
+ }
60
+ }
61
+
62
+ func TestRunPrepareCommand(t *testing.T) {
63
+ originalRunPrepare := runPrepare
64
+ t.Cleanup(func() {
65
+ runPrepare = originalRunPrepare
66
+ })
67
+
68
+ runPrepare = func(opts prepare.Options) (prepare.Result, error) {
69
+ if opts.ToolVersion == "" {
70
+ t.Fatal("expected tool version to be passed into prepare options")
71
+ }
72
+ return prepare.Result{
73
+ ExchangeID: "20260320-120000-a1b2c3",
74
+ TaskRootRef: "refs/heads/procoder/20260320-120000-a1b2c3/task",
75
+ TaskPackagePath: "/tmp/procoder-task-20260320-120000-a1b2c3.zip",
76
+ }, nil
77
+ }
78
+
79
+ var out bytes.Buffer
80
+ var errBuf bytes.Buffer
81
+
82
+ err := Run([]string{"prepare"}, &out, &errBuf)
83
+ if err != nil {
84
+ t.Fatalf("Run returned error: %v", err)
85
+ }
86
+
87
+ got := out.String()
88
+ if !strings.Contains(got, "Prepared exchange.") {
89
+ t.Fatalf("expected prepare success header, got: %q", got)
90
+ }
91
+ if !strings.Contains(got, "refs/heads/procoder/20260320-120000-a1b2c3/task") {
92
+ t.Fatalf("expected task branch in output, got: %q", got)
93
+ }
94
+ if !strings.Contains(got, "/tmp/procoder-task-20260320-120000-a1b2c3.zip") {
95
+ t.Fatalf("expected task package path in output, got: %q", got)
96
+ }
97
+ }
98
+
99
+ func TestRunApplyDryRunCommand(t *testing.T) {
100
+ originalRunApplyDryRun := runApplyDryRun
101
+ originalRunApply := runApply
102
+ t.Cleanup(func() {
103
+ runApplyDryRun = originalRunApplyDryRun
104
+ runApply = originalRunApply
105
+ })
106
+ runApply = func(opts apply.Options) (apply.Result, error) {
107
+ t.Fatalf("write-mode apply should not be called during dry-run test: %#v", opts)
108
+ return apply.Result{}, nil
109
+ }
110
+
111
+ runApplyDryRun = func(opts apply.Options) (apply.Plan, error) {
112
+ if opts.ReturnPackagePath != "procoder-return-20260320-120000-a1b2c3.zip" {
113
+ t.Fatalf("unexpected return package path: %q", opts.ReturnPackagePath)
114
+ }
115
+ if opts.Namespace != "procoder-import" {
116
+ t.Fatalf("unexpected namespace: %q", opts.Namespace)
117
+ }
118
+ return apply.Plan{
119
+ ExchangeID: "20260320-120000-a1b2c3",
120
+ ReturnPackagePath: "/tmp/procoder-return-20260320-120000-a1b2c3.zip",
121
+ Namespace: "procoder-import",
122
+ Checks: []apply.Check{
123
+ {Name: "bundle verification", Detail: "git bundle verify passed"},
124
+ },
125
+ Entries: []apply.PlanEntry{
126
+ {
127
+ Action: apply.ActionUpdate,
128
+ SourceRef: "refs/heads/procoder/20260320-120000-a1b2c3/task",
129
+ DestinationRef: "refs/heads/procoder-import/20260320-120000-a1b2c3/task",
130
+ OldOID: "old",
131
+ NewOID: "new",
132
+ Remapped: true,
133
+ },
134
+ },
135
+ Summary: apply.Summary{Updates: 1},
136
+ }, nil
137
+ }
138
+
139
+ var out bytes.Buffer
140
+ var errBuf bytes.Buffer
141
+ err := Run([]string{
142
+ "apply",
143
+ "procoder-return-20260320-120000-a1b2c3.zip",
144
+ "--dry-run",
145
+ "--namespace",
146
+ "procoder-import",
147
+ }, &out, &errBuf)
148
+ if err != nil {
149
+ t.Fatalf("Run returned error: %v", err)
150
+ }
151
+
152
+ got := out.String()
153
+ if !strings.Contains(got, "Dry-run apply plan.") {
154
+ t.Fatalf("expected dry-run plan header, got: %q", got)
155
+ }
156
+ if !strings.Contains(got, "REMAP") {
157
+ t.Fatalf("expected remap output, got: %q", got)
158
+ }
159
+ }
160
+
161
+ func TestRunApplyWriteModeCommand(t *testing.T) {
162
+ originalRunApplyDryRun := runApplyDryRun
163
+ originalRunApply := runApply
164
+ t.Cleanup(func() {
165
+ runApplyDryRun = originalRunApplyDryRun
166
+ runApply = originalRunApply
167
+ })
168
+ runApplyDryRun = func(opts apply.Options) (apply.Plan, error) {
169
+ t.Fatalf("dry-run apply should not be called in write-mode test: %#v", opts)
170
+ return apply.Plan{}, nil
171
+ }
172
+ runApply = func(opts apply.Options) (apply.Result, error) {
173
+ if opts.ReturnPackagePath != "procoder-return-20260320-120000-a1b2c3.zip" {
174
+ t.Fatalf("unexpected return package path: %q", opts.ReturnPackagePath)
175
+ }
176
+ if opts.Namespace != "procoder-import" {
177
+ t.Fatalf("unexpected namespace: %q", opts.Namespace)
178
+ }
179
+ if !opts.Checkout {
180
+ t.Fatalf("expected checkout=true in apply options")
181
+ }
182
+ return apply.Result{
183
+ Plan: apply.Plan{
184
+ ExchangeID: "20260320-120000-a1b2c3",
185
+ ReturnPackagePath: "/tmp/procoder-return-20260320-120000-a1b2c3.zip",
186
+ Namespace: "procoder-import",
187
+ Summary: apply.Summary{Creates: 1, Updates: 1},
188
+ },
189
+ CheckedOutRef: "refs/heads/procoder-import/20260320-120000-a1b2c3/task",
190
+ }, nil
191
+ }
192
+
193
+ var out bytes.Buffer
194
+ var errBuf bytes.Buffer
195
+
196
+ err := Run([]string{
197
+ "apply",
198
+ "procoder-return-20260320-120000-a1b2c3.zip",
199
+ "--namespace",
200
+ "procoder-import",
201
+ "--checkout",
202
+ }, &out, &errBuf)
203
+ if err != nil {
204
+ t.Fatalf("Run returned error: %v", err)
205
+ }
206
+
207
+ got := out.String()
208
+ if !strings.Contains(got, "Applied return package.") {
209
+ t.Fatalf("expected apply success output, got: %q", got)
210
+ }
211
+ if !strings.Contains(got, "Checked out: refs/heads/procoder-import/20260320-120000-a1b2c3/task") {
212
+ t.Fatalf("expected checked-out ref in output, got: %q", got)
213
+ }
214
+ }
215
+
216
+ func TestRunApplyMissingPackagePath(t *testing.T) {
217
+ var out bytes.Buffer
218
+ var errBuf bytes.Buffer
219
+
220
+ err := Run([]string{"apply", "--dry-run"}, &out, &errBuf)
221
+ if err == nil {
222
+ t.Fatal("expected error")
223
+ }
224
+
225
+ typed, ok := errs.As(err)
226
+ if !ok {
227
+ t.Fatalf("expected typed error, got %T", err)
228
+ }
229
+ if typed.Code != errs.CodeUnknownCommand {
230
+ t.Fatalf("unexpected code: got %s want %s", typed.Code, errs.CodeUnknownCommand)
231
+ }
232
+ if !strings.Contains(typed.Message, "missing return package path") {
233
+ t.Fatalf("expected missing path message, got %q", typed.Message)
234
+ }
235
+ }
236
+
237
+ func TestRunUnknownCommand(t *testing.T) {
238
+ var out bytes.Buffer
239
+ var errBuf bytes.Buffer
240
+
241
+ err := Run([]string{"unknown"}, &out, &errBuf)
242
+ if err == nil {
243
+ t.Fatal("expected error for unknown command")
244
+ }
245
+
246
+ typed, ok := errs.As(err)
247
+ if !ok {
248
+ t.Fatalf("expected typed error, got %T", err)
249
+ }
250
+ if typed.Code != errs.CodeUnknownCommand {
251
+ t.Fatalf("unexpected error code: %s", typed.Code)
252
+ }
253
+ }
@@ -0,0 +1,215 @@
1
+ package app_test
2
+
3
+ import (
4
+ "archive/zip"
5
+ "bytes"
6
+ "io"
7
+ "os"
8
+ "os/exec"
9
+ "path/filepath"
10
+ "strings"
11
+ "testing"
12
+ "time"
13
+
14
+ apppkg "github.com/amxv/procoder/internal/app"
15
+ "github.com/amxv/procoder/internal/exchange"
16
+ "github.com/amxv/procoder/internal/returnpkg"
17
+ "github.com/amxv/procoder/internal/testutil/gitrepo"
18
+ )
19
+
20
+ func TestCLIEndToEndPrepareReturnApply(t *testing.T) {
21
+ source := gitrepo.New(t)
22
+ source.WriteFile("README.md", "source\n")
23
+ source.CommitAll("initial")
24
+
25
+ helperPath := writeHelperBinary(t)
26
+ t.Setenv("PROCODER_RETURN_HELPER", helperPath)
27
+ t.Chdir(source.Dir)
28
+
29
+ var stdout bytes.Buffer
30
+ var stderr bytes.Buffer
31
+ if err := apppkg.Run([]string{"prepare"}, &stdout, &stderr); err != nil {
32
+ t.Fatalf("app.Run prepare failed: %v", err)
33
+ }
34
+ if !strings.Contains(stdout.String(), "Prepared exchange.") {
35
+ t.Fatalf("expected prepare success output, got:\n%s", stdout.String())
36
+ }
37
+
38
+ taskPackages, err := filepath.Glob(filepath.Join(source.Dir, "procoder-task-*.zip"))
39
+ if err != nil {
40
+ t.Fatalf("glob task package failed: %v", err)
41
+ }
42
+ if len(taskPackages) != 1 {
43
+ t.Fatalf("expected exactly one task package, got %d (%v)", len(taskPackages), taskPackages)
44
+ }
45
+
46
+ exportRoot := unzipTaskPackage(t, taskPackages[0])
47
+ ex, err := exchange.ReadExchange(filepath.Join(exportRoot, ".git", "procoder", "exchange.json"))
48
+ if err != nil {
49
+ t.Fatalf("ReadExchange(export) failed: %v", err)
50
+ }
51
+
52
+ appendFile(t, filepath.Join(exportRoot, "README.md"), "remote change\n")
53
+ runGit(t, exportRoot, "add", "README.md")
54
+ runGit(t, exportRoot, "commit", "-m", "remote task update")
55
+
56
+ retResult, err := returnpkg.Run(returnpkg.Options{
57
+ CWD: exportRoot,
58
+ ToolVersion: "0.1.0-test",
59
+ Now: func() time.Time { return time.Date(2026, time.March, 20, 16, 0, 0, 0, time.UTC) },
60
+ })
61
+ if err != nil {
62
+ t.Fatalf("returnpkg.Run failed: %v", err)
63
+ }
64
+
65
+ retRecord := readReturnRecordFromZip(t, retResult.ReturnPackagePath)
66
+ taskUpdate, ok := findUpdate(retRecord.Updates, ex.Task.RootRef)
67
+ if !ok {
68
+ t.Fatalf("expected task root update in return record")
69
+ }
70
+
71
+ stdout.Reset()
72
+ stderr.Reset()
73
+ if err := apppkg.Run([]string{"apply", retResult.ReturnPackagePath, "--checkout"}, &stdout, &stderr); err != nil {
74
+ t.Fatalf("app.Run apply failed: %v", err)
75
+ }
76
+ if !strings.Contains(stdout.String(), "Applied return package.") {
77
+ t.Fatalf("expected apply success output, got:\n%s", stdout.String())
78
+ }
79
+ if !strings.Contains(stdout.String(), "Checked out: "+ex.Task.RootRef) {
80
+ t.Fatalf("expected checked-out ref in apply output, got:\n%s", stdout.String())
81
+ }
82
+
83
+ if got := strings.TrimSpace(source.Git("rev-parse", ex.Task.RootRef)); got != taskUpdate.NewOID {
84
+ t.Fatalf("task ref mismatch after CLI apply: got %q want %q", got, taskUpdate.NewOID)
85
+ }
86
+ if got := strings.TrimSpace(source.Git("symbolic-ref", "--quiet", "HEAD")); got != ex.Task.RootRef {
87
+ t.Fatalf("expected CLI apply to check out task ref: got %q want %q", got, ex.Task.RootRef)
88
+ }
89
+ assertNoImportRefs(t, source.Dir)
90
+ }
91
+
92
+ func writeHelperBinary(t *testing.T) string {
93
+ t.Helper()
94
+
95
+ path := filepath.Join(t.TempDir(), "procoder-return")
96
+ if err := os.WriteFile(path, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
97
+ t.Fatalf("write helper binary failed: %v", err)
98
+ }
99
+ return path
100
+ }
101
+
102
+ func readReturnRecordFromZip(t *testing.T, zipPath string) exchange.Return {
103
+ t.Helper()
104
+
105
+ extracted := unzipFile(t, zipPath)
106
+ record, err := exchange.ReadReturn(filepath.Join(extracted, "procoder-return.json"))
107
+ if err != nil {
108
+ t.Fatalf("ReadReturn failed: %v", err)
109
+ }
110
+ return record
111
+ }
112
+
113
+ func unzipTaskPackage(t *testing.T, zipPath string) string {
114
+ t.Helper()
115
+
116
+ dest := unzipFile(t, zipPath)
117
+ return filepath.Join(dest, filepath.Base(filepath.Dir(zipPath)))
118
+ }
119
+
120
+ func unzipFile(t *testing.T, zipPath string) string {
121
+ t.Helper()
122
+
123
+ reader, err := zip.OpenReader(zipPath)
124
+ if err != nil {
125
+ t.Fatalf("zip.OpenReader failed: %v", err)
126
+ }
127
+ defer reader.Close()
128
+
129
+ dest := t.TempDir()
130
+ for _, file := range reader.File {
131
+ targetPath := filepath.Join(dest, file.Name)
132
+ safePrefix := filepath.Clean(dest) + string(os.PathSeparator)
133
+ if !strings.HasPrefix(filepath.Clean(targetPath), safePrefix) {
134
+ t.Fatalf("zip entry escapes destination: %q", file.Name)
135
+ }
136
+
137
+ if file.FileInfo().IsDir() {
138
+ if err := os.MkdirAll(targetPath, 0o755); err != nil {
139
+ t.Fatalf("mkdir failed: %v", err)
140
+ }
141
+ continue
142
+ }
143
+
144
+ if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
145
+ t.Fatalf("mkdir file dir failed: %v", err)
146
+ }
147
+ in, err := file.Open()
148
+ if err != nil {
149
+ t.Fatalf("open zip entry failed: %v", err)
150
+ }
151
+ out, err := os.OpenFile(targetPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, file.Mode())
152
+ if err != nil {
153
+ _ = in.Close()
154
+ t.Fatalf("open extracted file failed: %v", err)
155
+ }
156
+ if _, err := io.Copy(out, in); err != nil {
157
+ _ = out.Close()
158
+ _ = in.Close()
159
+ t.Fatalf("copy extracted file failed: %v", err)
160
+ }
161
+ _ = out.Close()
162
+ _ = in.Close()
163
+ }
164
+ return dest
165
+ }
166
+
167
+ func appendFile(t *testing.T, path, extra string) {
168
+ t.Helper()
169
+
170
+ f, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0o644)
171
+ if err != nil {
172
+ t.Fatalf("open %s for append failed: %v", path, err)
173
+ }
174
+ if _, err := f.WriteString(extra); err != nil {
175
+ _ = f.Close()
176
+ t.Fatalf("append to %s failed: %v", path, err)
177
+ }
178
+ if err := f.Close(); err != nil {
179
+ t.Fatalf("close %s failed: %v", path, err)
180
+ }
181
+ }
182
+
183
+ func runGit(t *testing.T, dir string, args ...string) string {
184
+ t.Helper()
185
+
186
+ cmd := exec.Command("git", args...)
187
+ cmd.Dir = dir
188
+ var stdout bytes.Buffer
189
+ var stderr bytes.Buffer
190
+ cmd.Stdout = &stdout
191
+ cmd.Stderr = &stderr
192
+
193
+ if err := cmd.Run(); err != nil {
194
+ t.Fatalf("git %s failed:\nstdout:\n%s\nstderr:\n%s\nerr:%v", strings.Join(args, " "), stdout.String(), stderr.String(), err)
195
+ }
196
+ return stdout.String()
197
+ }
198
+
199
+ func findUpdate(updates []exchange.RefUpdate, ref string) (exchange.RefUpdate, bool) {
200
+ for _, update := range updates {
201
+ if update.Ref == ref {
202
+ return update, true
203
+ }
204
+ }
205
+ return exchange.RefUpdate{}, false
206
+ }
207
+
208
+ func assertNoImportRefs(t *testing.T, dir string) {
209
+ t.Helper()
210
+
211
+ importRefs := strings.TrimSpace(runGit(t, dir, "for-each-ref", "--format=%(refname)", "refs/procoder/import"))
212
+ if importRefs != "" {
213
+ t.Fatalf("expected temporary import refs to be cleaned up, got:\n%s", importRefs)
214
+ }
215
+ }