@markjaquith/agency 0.5.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.
- package/LICENSE +21 -0
- package/README.md +109 -0
- package/cli.ts +569 -0
- package/index.ts +1 -0
- package/package.json +65 -0
- package/src/commands/base.test.ts +198 -0
- package/src/commands/base.ts +198 -0
- package/src/commands/clean.test.ts +299 -0
- package/src/commands/clean.ts +320 -0
- package/src/commands/emit.test.ts +412 -0
- package/src/commands/emit.ts +521 -0
- package/src/commands/emitted.test.ts +226 -0
- package/src/commands/emitted.ts +57 -0
- package/src/commands/init.test.ts +311 -0
- package/src/commands/init.ts +140 -0
- package/src/commands/merge.test.ts +365 -0
- package/src/commands/merge.ts +253 -0
- package/src/commands/pull.test.ts +385 -0
- package/src/commands/pull.ts +205 -0
- package/src/commands/push.test.ts +394 -0
- package/src/commands/push.ts +346 -0
- package/src/commands/save.test.ts +247 -0
- package/src/commands/save.ts +162 -0
- package/src/commands/source.test.ts +195 -0
- package/src/commands/source.ts +72 -0
- package/src/commands/status.test.ts +489 -0
- package/src/commands/status.ts +258 -0
- package/src/commands/switch.test.ts +194 -0
- package/src/commands/switch.ts +84 -0
- package/src/commands/task-branching.test.ts +334 -0
- package/src/commands/task-edit.test.ts +141 -0
- package/src/commands/task-main.test.ts +872 -0
- package/src/commands/task.ts +712 -0
- package/src/commands/tasks.test.ts +335 -0
- package/src/commands/tasks.ts +155 -0
- package/src/commands/template-delete.test.ts +178 -0
- package/src/commands/template-delete.ts +98 -0
- package/src/commands/template-list.test.ts +135 -0
- package/src/commands/template-list.ts +87 -0
- package/src/commands/template-view.test.ts +158 -0
- package/src/commands/template-view.ts +86 -0
- package/src/commands/template.test.ts +32 -0
- package/src/commands/template.ts +96 -0
- package/src/commands/use.test.ts +87 -0
- package/src/commands/use.ts +97 -0
- package/src/commands/work.test.ts +462 -0
- package/src/commands/work.ts +193 -0
- package/src/errors.ts +17 -0
- package/src/schemas.ts +33 -0
- package/src/services/AgencyMetadataService.ts +287 -0
- package/src/services/ClaudeService.test.ts +184 -0
- package/src/services/ClaudeService.ts +91 -0
- package/src/services/ConfigService.ts +115 -0
- package/src/services/FileSystemService.ts +222 -0
- package/src/services/GitService.ts +751 -0
- package/src/services/OpencodeService.ts +263 -0
- package/src/services/PromptService.ts +183 -0
- package/src/services/TemplateService.ts +75 -0
- package/src/test-utils.ts +362 -0
- package/src/types/native-exec.d.ts +8 -0
- package/src/types.ts +216 -0
- package/src/utils/colors.ts +178 -0
- package/src/utils/command.ts +17 -0
- package/src/utils/effect.ts +281 -0
- package/src/utils/exec.ts +48 -0
- package/src/utils/paths.ts +51 -0
- package/src/utils/pr-branch.test.ts +372 -0
- package/src/utils/pr-branch.ts +473 -0
- package/src/utils/process.ts +110 -0
- package/src/utils/spinner.ts +82 -0
- package/templates/AGENCY.md +20 -0
- package/templates/AGENTS.md +11 -0
- package/templates/CLAUDE.md +3 -0
- package/templates/TASK.md +5 -0
- package/templates/opencode.json +4 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { basename } from "path"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import type { BaseCommandOptions } from "../utils/command"
|
|
4
|
+
import { GitService } from "../services/GitService"
|
|
5
|
+
import { PromptService } from "../services/PromptService"
|
|
6
|
+
import { TemplateService } from "../services/TemplateService"
|
|
7
|
+
import highlight, { done } from "../utils/colors"
|
|
8
|
+
import { createLoggers, ensureGitRepo, getTemplateName } from "../utils/effect"
|
|
9
|
+
|
|
10
|
+
interface InitOptions extends BaseCommandOptions {
|
|
11
|
+
template?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const init = (options: InitOptions = {}) =>
|
|
15
|
+
Effect.gen(function* () {
|
|
16
|
+
const { silent = false, cwd } = options
|
|
17
|
+
const { log, verboseLog } = createLoggers(options)
|
|
18
|
+
|
|
19
|
+
const git = yield* GitService
|
|
20
|
+
const promptService = yield* PromptService
|
|
21
|
+
const templateService = yield* TemplateService
|
|
22
|
+
|
|
23
|
+
const gitRoot = yield* ensureGitRepo(cwd)
|
|
24
|
+
|
|
25
|
+
// Check if already initialized
|
|
26
|
+
const existingTemplate = yield* getTemplateName(gitRoot)
|
|
27
|
+
if (existingTemplate && !options.template) {
|
|
28
|
+
return yield* Effect.fail(
|
|
29
|
+
new Error(
|
|
30
|
+
`Already initialized with template ${highlight.template(existingTemplate)}.\n` +
|
|
31
|
+
`To change template, run: agency init --template <name>`,
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let templateName = options.template
|
|
37
|
+
|
|
38
|
+
// If template name not provided, show interactive selection
|
|
39
|
+
if (!templateName) {
|
|
40
|
+
if (silent) {
|
|
41
|
+
return yield* Effect.fail(
|
|
42
|
+
new Error(
|
|
43
|
+
"Template name required. Use --template flag in silent mode.",
|
|
44
|
+
),
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const existingTemplates = yield* templateService.listTemplates()
|
|
49
|
+
verboseLog(`Found ${existingTemplates.length} existing templates`)
|
|
50
|
+
|
|
51
|
+
// Get current directory name as default suggestion
|
|
52
|
+
let defaultTemplateName: string | undefined
|
|
53
|
+
const dirName = basename(gitRoot)
|
|
54
|
+
const sanitizedDirName =
|
|
55
|
+
yield* promptService.sanitizeTemplateName(dirName)
|
|
56
|
+
|
|
57
|
+
if (sanitizedDirName && !existingTemplates.includes(sanitizedDirName)) {
|
|
58
|
+
defaultTemplateName = sanitizedDirName
|
|
59
|
+
verboseLog(`Suggesting default template name: ${defaultTemplateName}`)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const selectedName = yield* promptService.promptForTemplate(
|
|
63
|
+
existingTemplates,
|
|
64
|
+
{
|
|
65
|
+
defaultValue: defaultTemplateName,
|
|
66
|
+
allowNew: true,
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
// If selected from list, use as-is; otherwise sanitize new name
|
|
71
|
+
if (existingTemplates.includes(selectedName)) {
|
|
72
|
+
templateName = selectedName
|
|
73
|
+
} else {
|
|
74
|
+
templateName = yield* promptService.sanitizeTemplateName(selectedName)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
verboseLog(`Selected template: ${templateName}`)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!templateName) {
|
|
81
|
+
return yield* Effect.fail(new Error("Template name is required."))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Resolve and save default remote (with smart precedence)
|
|
85
|
+
const remote = yield* git
|
|
86
|
+
.resolveRemote(gitRoot)
|
|
87
|
+
.pipe(Effect.catchAll(() => Effect.succeed(null)))
|
|
88
|
+
|
|
89
|
+
if (remote) {
|
|
90
|
+
yield* git.setRemoteConfig(remote, gitRoot)
|
|
91
|
+
verboseLog(`Detected and saved remote: ${remote}`)
|
|
92
|
+
} else {
|
|
93
|
+
verboseLog("No remote detected - skip remote configuration")
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Save template name to git config
|
|
97
|
+
yield* git.setGitConfig("agency.template", templateName, gitRoot)
|
|
98
|
+
log(
|
|
99
|
+
done(
|
|
100
|
+
`Initialized with template ${highlight.template(templateName)}${existingTemplate && existingTemplate !== templateName ? ` (was ${highlight.template(existingTemplate)})` : ""}`,
|
|
101
|
+
),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
// Note: We do NOT create the template directory here
|
|
105
|
+
// It will be created when the user runs 'agency template save'
|
|
106
|
+
verboseLog(
|
|
107
|
+
`Template directory will be created when you save files with 'agency template save'`,
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
export const help = `
|
|
112
|
+
Usage: agency init [options]
|
|
113
|
+
|
|
114
|
+
Initialize agency for this repository by selecting a template.
|
|
115
|
+
|
|
116
|
+
This command must be run before using other agency commands like 'agency task'.
|
|
117
|
+
It saves your template selection to .git/config (not committed to the repository).
|
|
118
|
+
|
|
119
|
+
On first run, you'll be prompted to either:
|
|
120
|
+
- Select an existing template from your ~/.config/agency/templates/
|
|
121
|
+
- Create a new template by entering a new name
|
|
122
|
+
|
|
123
|
+
If no templates exist, the default suggestion is the current directory name.
|
|
124
|
+
|
|
125
|
+
The template directory itself is only created when you actually save files to it
|
|
126
|
+
using 'agency template save'.
|
|
127
|
+
|
|
128
|
+
Options:
|
|
129
|
+
-t, --template Specify template name (skips interactive prompt)
|
|
130
|
+
|
|
131
|
+
Examples:
|
|
132
|
+
agency init # Interactive template selection
|
|
133
|
+
agency init --template work # Set template to 'work'
|
|
134
|
+
|
|
135
|
+
Notes:
|
|
136
|
+
- Template name is saved to .git/config (agency.template)
|
|
137
|
+
- Template directory is NOT created until 'agency template save' is used
|
|
138
|
+
- To change template later, run 'agency init --template <name>'
|
|
139
|
+
- Run 'agency task' after initialization to create template files
|
|
140
|
+
`
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { test, expect, describe, beforeEach, afterEach } from "bun:test"
|
|
2
|
+
import { join } from "path"
|
|
3
|
+
import { merge } from "./merge"
|
|
4
|
+
import { emit } from "./emit"
|
|
5
|
+
import { task } from "./task"
|
|
6
|
+
import {
|
|
7
|
+
createTempDir,
|
|
8
|
+
cleanupTempDir,
|
|
9
|
+
initGitRepo,
|
|
10
|
+
initAgency,
|
|
11
|
+
getGitOutput,
|
|
12
|
+
getCurrentBranch,
|
|
13
|
+
createCommit,
|
|
14
|
+
branchExists,
|
|
15
|
+
checkoutBranch,
|
|
16
|
+
createBranch,
|
|
17
|
+
addAndCommit,
|
|
18
|
+
setupRemote,
|
|
19
|
+
deleteBranch,
|
|
20
|
+
runTestEffect,
|
|
21
|
+
} from "../test-utils"
|
|
22
|
+
|
|
23
|
+
// Cache the git-filter-repo availability check (it doesn't change during test run)
|
|
24
|
+
let hasGitFilterRepoCache: boolean | null = null
|
|
25
|
+
async function checkGitFilterRepo(): Promise<boolean> {
|
|
26
|
+
if (hasGitFilterRepoCache === null) {
|
|
27
|
+
const proc = Bun.spawn(["which", "git-filter-repo"], {
|
|
28
|
+
stdout: "pipe",
|
|
29
|
+
stderr: "pipe",
|
|
30
|
+
})
|
|
31
|
+
await proc.exited
|
|
32
|
+
hasGitFilterRepoCache = proc.exitCode === 0
|
|
33
|
+
}
|
|
34
|
+
return hasGitFilterRepoCache
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("merge command", () => {
|
|
38
|
+
let tempDir: string
|
|
39
|
+
let originalCwd: string
|
|
40
|
+
let hasGitFilterRepo: boolean
|
|
41
|
+
|
|
42
|
+
beforeEach(async () => {
|
|
43
|
+
tempDir = await createTempDir()
|
|
44
|
+
originalCwd = process.cwd()
|
|
45
|
+
process.chdir(tempDir)
|
|
46
|
+
|
|
47
|
+
// Set config path to non-existent file to use defaults
|
|
48
|
+
process.env.AGENCY_CONFIG_PATH = join(tempDir, "non-existent-config.json")
|
|
49
|
+
// Set config dir to temp dir to avoid picking up user's config files
|
|
50
|
+
process.env.AGENCY_CONFIG_DIR = await createTempDir()
|
|
51
|
+
|
|
52
|
+
// Check if git-filter-repo is available (cached)
|
|
53
|
+
hasGitFilterRepo = await checkGitFilterRepo()
|
|
54
|
+
|
|
55
|
+
// Initialize git repo with main branch (already includes initial commit)
|
|
56
|
+
await initGitRepo(tempDir)
|
|
57
|
+
|
|
58
|
+
// Set up origin for git-filter-repo
|
|
59
|
+
await setupRemote(tempDir, "origin", tempDir)
|
|
60
|
+
|
|
61
|
+
// Create a source branch (with agency/ prefix per new default config)
|
|
62
|
+
await createBranch(tempDir, "agency/feature")
|
|
63
|
+
|
|
64
|
+
// Initialize AGENTS.md on feature branch
|
|
65
|
+
await initAgency(tempDir, "test")
|
|
66
|
+
|
|
67
|
+
await runTestEffect(task({ silent: true }))
|
|
68
|
+
|
|
69
|
+
// Ensure agency.json has baseBranch set (task should auto-detect it, but ensure it's there)
|
|
70
|
+
const agencyJsonPath = join(tempDir, "agency.json")
|
|
71
|
+
const agencyJson = await Bun.file(agencyJsonPath).json()
|
|
72
|
+
if (!agencyJson.baseBranch) {
|
|
73
|
+
agencyJson.baseBranch = "origin/main"
|
|
74
|
+
await Bun.write(
|
|
75
|
+
agencyJsonPath,
|
|
76
|
+
JSON.stringify(agencyJson, null, 2) + "\n",
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await addAndCommit(tempDir, "AGENTS.md agency.json", "Add AGENTS.md")
|
|
81
|
+
|
|
82
|
+
// Create another commit on feature branch
|
|
83
|
+
await createCommit(tempDir, "Feature work")
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
afterEach(async () => {
|
|
87
|
+
process.chdir(originalCwd)
|
|
88
|
+
delete process.env.AGENCY_CONFIG_PATH
|
|
89
|
+
if (process.env.AGENCY_CONFIG_DIR) {
|
|
90
|
+
await cleanupTempDir(process.env.AGENCY_CONFIG_DIR)
|
|
91
|
+
delete process.env.AGENCY_CONFIG_DIR
|
|
92
|
+
}
|
|
93
|
+
await cleanupTempDir(tempDir)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe("merge from source branch", () => {
|
|
97
|
+
test("creates emit branch and merges when run from source branch", async () => {
|
|
98
|
+
if (!hasGitFilterRepo) {
|
|
99
|
+
console.log("Skipping test: git-filter-repo not installed")
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// We're on feature branch (source)
|
|
104
|
+
const currentBranch = await getCurrentBranch(tempDir)
|
|
105
|
+
expect(currentBranch).toBe("agency/feature")
|
|
106
|
+
|
|
107
|
+
// Run merge - should create feature--PR and merge it to main
|
|
108
|
+
await runTestEffect(merge({ silent: true }))
|
|
109
|
+
|
|
110
|
+
// Should be on main branch after merge
|
|
111
|
+
const afterMergeBranch = await getCurrentBranch(tempDir)
|
|
112
|
+
expect(afterMergeBranch).toBe("main")
|
|
113
|
+
|
|
114
|
+
// emit branch should exist
|
|
115
|
+
const prExists = await branchExists(tempDir, "feature")
|
|
116
|
+
expect(prExists).toBe(true)
|
|
117
|
+
|
|
118
|
+
// Main should have the feature work but not AGENTS.md
|
|
119
|
+
const files = await getGitOutput(tempDir, ["ls-files"])
|
|
120
|
+
expect(files).not.toContain("AGENTS.md")
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test("recreates emit branch if it already exists", async () => {
|
|
124
|
+
if (!hasGitFilterRepo) {
|
|
125
|
+
console.log("Skipping test: git-filter-repo not installed")
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Create emit branch first
|
|
130
|
+
await runTestEffect(emit({ silent: true }))
|
|
131
|
+
|
|
132
|
+
// Go back to feature branch
|
|
133
|
+
await checkoutBranch(tempDir, "agency/feature")
|
|
134
|
+
|
|
135
|
+
// Make additional changes
|
|
136
|
+
await createCommit(tempDir, "More feature work")
|
|
137
|
+
|
|
138
|
+
// Run merge - should recreate emit branch with new changes
|
|
139
|
+
await runTestEffect(merge({ silent: true }))
|
|
140
|
+
|
|
141
|
+
// Should be on main after merge
|
|
142
|
+
const currentBranch = await getCurrentBranch(tempDir)
|
|
143
|
+
expect(currentBranch).toBe("main")
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe("merge from emit branch", () => {
|
|
148
|
+
test("merges emit branch directly when run from emit branch", async () => {
|
|
149
|
+
if (!hasGitFilterRepo) {
|
|
150
|
+
console.log("Skipping test: git-filter-repo not installed")
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Create emit branch
|
|
155
|
+
await runTestEffect(emit({ silent: true }))
|
|
156
|
+
|
|
157
|
+
// emit() now stays on source branch, so we need to checkout to emit branch
|
|
158
|
+
await checkoutBranch(tempDir, "feature")
|
|
159
|
+
|
|
160
|
+
// We're on feature--PR now
|
|
161
|
+
const currentBranch = await getCurrentBranch(tempDir)
|
|
162
|
+
expect(currentBranch).toBe("feature")
|
|
163
|
+
|
|
164
|
+
// Run merge - should merge feature--PR to main
|
|
165
|
+
await runTestEffect(merge({ silent: true }))
|
|
166
|
+
|
|
167
|
+
// Should be on main after merge
|
|
168
|
+
const afterMergeBranch = await getCurrentBranch(tempDir)
|
|
169
|
+
expect(afterMergeBranch).toBe("main")
|
|
170
|
+
|
|
171
|
+
// Main should have the feature work but not AGENTS.md
|
|
172
|
+
const files = await getGitOutput(tempDir, ["ls-files"])
|
|
173
|
+
expect(files).not.toContain("AGENTS.md")
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test("throws error if emit branch has no corresponding source branch", async () => {
|
|
177
|
+
if (!hasGitFilterRepo) {
|
|
178
|
+
console.log("Skipping test: git-filter-repo not installed")
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Create emit branch
|
|
183
|
+
await runTestEffect(emit({ silent: true }))
|
|
184
|
+
|
|
185
|
+
// pr() now stays on source branch, so checkout to emit branch
|
|
186
|
+
await checkoutBranch(tempDir, "feature")
|
|
187
|
+
|
|
188
|
+
// Delete the source branch
|
|
189
|
+
await deleteBranch(tempDir, "agency/feature", true)
|
|
190
|
+
|
|
191
|
+
// Try to merge - should fail (error message may vary since source branch is deleted)
|
|
192
|
+
await expect(runTestEffect(merge({ silent: true }))).rejects.toThrow()
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
describe("error handling", () => {
|
|
197
|
+
test("throws error when not in a git repository", async () => {
|
|
198
|
+
const nonGitDir = await createTempDir()
|
|
199
|
+
process.chdir(nonGitDir)
|
|
200
|
+
|
|
201
|
+
await expect(runTestEffect(merge({ silent: true }))).rejects.toThrow(
|
|
202
|
+
"Not in a git repository",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
await cleanupTempDir(nonGitDir)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test("throws error if base branch does not exist locally", async () => {
|
|
209
|
+
// Create emit branch (skipFilter for speed since we're testing error handling)
|
|
210
|
+
await runTestEffect(emit({ silent: true, skipFilter: true }))
|
|
211
|
+
|
|
212
|
+
// Delete main branch (the base)
|
|
213
|
+
await checkoutBranch(tempDir, "feature")
|
|
214
|
+
await deleteBranch(tempDir, "main", true)
|
|
215
|
+
|
|
216
|
+
// Try to merge - should fail
|
|
217
|
+
await expect(
|
|
218
|
+
runTestEffect(merge({ silent: true, skipFilter: true })),
|
|
219
|
+
).rejects.toThrow("does not exist locally")
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
describe("silent mode", () => {
|
|
224
|
+
test("silent flag suppresses output", async () => {
|
|
225
|
+
const logs: string[] = []
|
|
226
|
+
const originalLog = console.log
|
|
227
|
+
console.log = (...args: any[]) => logs.push(args.join(" "))
|
|
228
|
+
|
|
229
|
+
await runTestEffect(merge({ silent: true, skipFilter: true }))
|
|
230
|
+
|
|
231
|
+
console.log = originalLog
|
|
232
|
+
|
|
233
|
+
expect(logs.length).toBe(0)
|
|
234
|
+
})
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
describe("squash merge", () => {
|
|
238
|
+
test("performs squash merge when --squash flag is set", async () => {
|
|
239
|
+
// We're on feature branch (source)
|
|
240
|
+
const currentBranch = await getCurrentBranch(tempDir)
|
|
241
|
+
expect(currentBranch).toBe("agency/feature")
|
|
242
|
+
|
|
243
|
+
// Run merge with squash flag (skipFilter for speed, we're testing squash behavior)
|
|
244
|
+
await runTestEffect(
|
|
245
|
+
merge({ silent: true, squash: true, skipFilter: true }),
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
// Should be on main branch after merge
|
|
249
|
+
const afterMergeBranch = await getCurrentBranch(tempDir)
|
|
250
|
+
expect(afterMergeBranch).toBe("main")
|
|
251
|
+
|
|
252
|
+
// Check that changes are staged but not committed
|
|
253
|
+
const status = await getGitOutput(tempDir, ["status", "--porcelain"])
|
|
254
|
+
|
|
255
|
+
// Staged changes should be present (indicated by status codes in first column)
|
|
256
|
+
expect(status.trim().length).toBeGreaterThan(0)
|
|
257
|
+
|
|
258
|
+
// Get the log to verify no merge commit was created
|
|
259
|
+
const log = await getGitOutput(tempDir, ["log", "--oneline", "-5"])
|
|
260
|
+
|
|
261
|
+
// Should not contain a merge commit message
|
|
262
|
+
expect(log).not.toContain("Merge branch")
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
test("performs regular merge when --squash flag is not set", async () => {
|
|
266
|
+
// We're on feature branch (source)
|
|
267
|
+
const currentBranch = await getCurrentBranch(tempDir)
|
|
268
|
+
expect(currentBranch).toBe("agency/feature")
|
|
269
|
+
|
|
270
|
+
// Run merge without squash flag (skipFilter for speed, we're testing merge behavior)
|
|
271
|
+
await runTestEffect(
|
|
272
|
+
merge({ silent: true, squash: false, skipFilter: true }),
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
// Should be on main branch after merge
|
|
276
|
+
const afterMergeBranch = await getCurrentBranch(tempDir)
|
|
277
|
+
expect(afterMergeBranch).toBe("main")
|
|
278
|
+
|
|
279
|
+
// With regular merge (not squash), there should be no staged changes
|
|
280
|
+
const status = await getGitOutput(tempDir, ["status", "--porcelain"])
|
|
281
|
+
|
|
282
|
+
// No staged changes - everything should be committed
|
|
283
|
+
expect(status.trim().length).toBe(0)
|
|
284
|
+
|
|
285
|
+
// Get the log to verify commits were included
|
|
286
|
+
const log = await getGitOutput(tempDir, ["log", "--oneline", "-5"])
|
|
287
|
+
|
|
288
|
+
// Should contain the feature work commit (regular merge includes all commits)
|
|
289
|
+
expect(log).toContain("Feature work")
|
|
290
|
+
})
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
describe("push flag", () => {
|
|
294
|
+
test("pushes base branch to origin when --push flag is set", async () => {
|
|
295
|
+
// We're on feature branch (source)
|
|
296
|
+
const currentBranch = await getCurrentBranch(tempDir)
|
|
297
|
+
expect(currentBranch).toBe("agency/feature")
|
|
298
|
+
|
|
299
|
+
// Get the current commit on main before merge
|
|
300
|
+
await checkoutBranch(tempDir, "main")
|
|
301
|
+
const beforeCommit = (
|
|
302
|
+
await getGitOutput(tempDir, ["rev-parse", "HEAD"])
|
|
303
|
+
).trim()
|
|
304
|
+
|
|
305
|
+
// Go back to feature branch
|
|
306
|
+
await checkoutBranch(tempDir, "agency/feature")
|
|
307
|
+
|
|
308
|
+
// Run merge with push flag (skipFilter for speed, we're testing push behavior)
|
|
309
|
+
await runTestEffect(merge({ silent: true, push: true, skipFilter: true }))
|
|
310
|
+
|
|
311
|
+
// Should be on main branch after merge
|
|
312
|
+
const afterMergeBranch = await getCurrentBranch(tempDir)
|
|
313
|
+
expect(afterMergeBranch).toBe("main")
|
|
314
|
+
|
|
315
|
+
// Get the current commit on main after merge
|
|
316
|
+
const afterCommit = (
|
|
317
|
+
await getGitOutput(tempDir, ["rev-parse", "HEAD"])
|
|
318
|
+
).trim()
|
|
319
|
+
|
|
320
|
+
// The commit should have changed (merge happened)
|
|
321
|
+
expect(afterCommit).not.toBe(beforeCommit)
|
|
322
|
+
|
|
323
|
+
// Verify that origin/main points to the same commit as local main
|
|
324
|
+
const originMainCommit = (
|
|
325
|
+
await getGitOutput(tempDir, ["rev-parse", "origin/main"])
|
|
326
|
+
).trim()
|
|
327
|
+
|
|
328
|
+
// origin/main should be at the same commit as local main (push succeeded)
|
|
329
|
+
expect(originMainCommit).toBe(afterCommit)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
test("does not push when --push flag is not set", async () => {
|
|
333
|
+
// We're on feature branch (source)
|
|
334
|
+
const currentBranch = await getCurrentBranch(tempDir)
|
|
335
|
+
expect(currentBranch).toBe("agency/feature")
|
|
336
|
+
|
|
337
|
+
// Get the current commit on origin/main before merge
|
|
338
|
+
const beforeOriginCommit = (
|
|
339
|
+
await getGitOutput(tempDir, ["rev-parse", "origin/main"])
|
|
340
|
+
).trim()
|
|
341
|
+
|
|
342
|
+
// Run merge without push flag (skipFilter for speed, we're testing push behavior)
|
|
343
|
+
await runTestEffect(merge({ silent: true, skipFilter: true }))
|
|
344
|
+
|
|
345
|
+
// Should be on main branch after merge
|
|
346
|
+
const afterMergeBranch = await getCurrentBranch(tempDir)
|
|
347
|
+
expect(afterMergeBranch).toBe("main")
|
|
348
|
+
|
|
349
|
+
// Get the current commit on origin/main after merge
|
|
350
|
+
const afterOriginCommit = (
|
|
351
|
+
await getGitOutput(tempDir, ["rev-parse", "origin/main"])
|
|
352
|
+
).trim()
|
|
353
|
+
|
|
354
|
+
// origin/main should still be at the same commit (no push happened)
|
|
355
|
+
expect(afterOriginCommit).toBe(beforeOriginCommit)
|
|
356
|
+
|
|
357
|
+
// But local main should have moved forward
|
|
358
|
+
const localMainCommit = (
|
|
359
|
+
await getGitOutput(tempDir, ["rev-parse", "HEAD"])
|
|
360
|
+
).trim()
|
|
361
|
+
|
|
362
|
+
expect(localMainCommit).not.toBe(beforeOriginCommit)
|
|
363
|
+
})
|
|
364
|
+
})
|
|
365
|
+
})
|