diff --git a/.changeset/add-since-flag.md b/.changeset/add-since-flag.md new file mode 100644 index 000000000..40a82cd95 --- /dev/null +++ b/.changeset/add-since-flag.md @@ -0,0 +1,11 @@ +--- +"@changesets/cli": minor +--- + +Add `--since` flag to `add` command + +The `add` command now supports a `--since` flag that allows you to specify which branch, tag, or git ref to use when detecting changed packages. This is useful for gitflow workflows where you have multiple target branches and the `baseBranch` config option doesn't cover all use cases. + +Example: `changeset add --since=develop` + +If not provided, the command falls back to the `baseBranch` value in your `.changeset/config.json`. diff --git a/docs/command-line-options.md b/docs/command-line-options.md index f7b96b614..df181979a 100644 --- a/docs/command-line-options.md +++ b/docs/command-line-options.md @@ -3,7 +3,7 @@ The command line for changesets is the main way of interacting with it. There are 4 main commands. If you are looking for how we recommend you setup and manage changesets with the commands, check out our [intro to using changesets](./intro-to-using-changesets.md) - init -- add [--empty] [--open] [--message ] +- add [--empty] [--open] [--since ] [--message ] - version [--ignore, --snapshot] - publish [--otp=code, --tag] - status [--since=master --verbose --output=JSON_FILE.json] @@ -67,6 +67,12 @@ If you set the commit option in the config, the command will add the updated cha - `--open` - opens the created changeset in an external editor - `--message` (or `-m`) - provides the changeset summary from the command line instead of prompting for it. +- `--since` - uses the provided branch, tag, or git ref (such as `main` or a git commit hash) to detect which packages have changed when populating the list of changed packages in the CLI. This is useful in gitflow workflows where you have multiple target branches and `baseBranch` in the config doesn't cover all use cases. If not provided, the command falls back to the `baseBranch` value in your `.changeset/config.json`. + +``` +changeset add --since=develop +``` + ## version ``` diff --git a/packages/cli/src/commands/add/__tests__/add.ts b/packages/cli/src/commands/add/__tests__/add.ts index 2fe914b8d..bec9bbfd4 100644 --- a/packages/cli/src/commands/add/__tests__/add.ts +++ b/packages/cli/src/commands/add/__tests__/add.ts @@ -2,9 +2,11 @@ import path from "path"; import stripAnsi from "strip-ansi"; import * as git from "@changesets/git"; import { defaultConfig } from "@changesets/config"; -import { silenceLogsInBlock, testdir } from "@changesets/test-utils"; -import writeChangeset from "@changesets/write"; +import { silenceLogsInBlock, testdir, gitdir } from "@changesets/test-utils"; import { error as loggerError } from "@changesets/logger"; +import getChangesets from "@changesets/read"; +import fs from "fs-extra"; +import spawn from "spawndamnit"; import { askCheckboxPlus, @@ -16,18 +18,6 @@ import { import addChangeset from ".."; jest.mock("../../../utils/cli-utilities"); -jest.mock("@changesets/git"); -jest.mock("@changesets/write"); -// @ts-ignore -writeChangeset.mockImplementation(() => Promise.resolve("abcdefg")); -// @ts-ignore -git.commit.mockImplementation(() => Promise.resolve(true)); - -// @ts-ignore -git.getChangedPackagesSinceRef.mockImplementation(({ ref }) => { - expect(ref).toBe("master"); - return []; -}); // @ts-ignore const mockUserResponses = (mockResponses) => { @@ -107,9 +97,9 @@ describe("Add command", () => { mockUserResponses({ releases: { "pkg-a": "patch" } }); await addChangeset(cwd, { empty: false }, defaultConfig); - // @ts-ignore - const call = writeChangeset.mock.calls[0][0]; - expect(call).toEqual( + const changesets = await getChangesets(cwd); + expect(changesets.length).toBe(1); + expect(changesets[0]).toEqual( expect.objectContaining({ summary: "summary message mock", releases: [{ name: "pkg-a", type: "patch" }], @@ -149,9 +139,9 @@ describe("Add command", () => { }); await addChangeset(cwd, { empty: false }, defaultConfig); - // @ts-ignore - const call = writeChangeset.mock.calls[0][0]; - expect(call).toEqual( + const changesets = await getChangesets(cwd); + expect(changesets.length).toBe(1); + expect(changesets[0]).toEqual( expect.objectContaining({ summary: expectedSummary, releases: [{ name: "pkg-a", type: "patch" }], @@ -194,9 +184,9 @@ describe("Add command", () => { await addChangeset(cwd, { empty: false }, defaultConfig); - // @ts-ignore - const call = writeChangeset.mock.calls[0][0]; - expect(call).toEqual( + const changesets = await getChangesets(cwd); + expect(changesets.length).toBe(1); + expect(changesets[0]).toEqual( expect.objectContaining({ summary: "summary message mock", releases: [{ name: "single-package", type: "minor" }], @@ -205,7 +195,7 @@ describe("Add command", () => { }); it("should commit when the commit flag is passed in", async () => { - const cwd = await testdir({ + const cwd = await gitdir({ "package.json": JSON.stringify({ private: true, workspaces: ["packages/*"], @@ -232,8 +222,10 @@ describe("Add command", () => { commit: [path.resolve(__dirname, "..", "..", "..", "commit"), null], } ); - expect(git.add).toHaveBeenCalledTimes(1); - expect(git.commit).toHaveBeenCalledTimes(1); + + const result = await spawn("git", ["log", "--oneline", "-1"], { cwd }); + const lastCommitMsg = result.stdout.toString("utf-8").trim(); + expect(lastCommitMsg).toContain("docs(changeset): summary message mock"); }); it("should create empty changeset when empty flag is passed in", async () => { @@ -250,9 +242,9 @@ describe("Add command", () => { await addChangeset(cwd, { empty: true }, defaultConfig); - // @ts-ignore - const call = writeChangeset.mock.calls[0][0]; - expect(call).toEqual( + const changesets = await getChangesets(cwd); + expect(changesets.length).toBe(1); + expect(changesets[0]).toEqual( expect.objectContaining({ releases: [], summary: "", @@ -280,9 +272,9 @@ describe("Add command", () => { defaultConfig ); - // @ts-ignore - const call = writeChangeset.mock.calls[0][0]; - expect(call).toEqual( + const changesets = await getChangesets(cwd); + expect(changesets.length).toBe(1); + expect(changesets[0]).toEqual( expect.objectContaining({ summary: "summary from message", releases: [{ name: "single-package", type: "minor" }], @@ -309,9 +301,9 @@ describe("Add command", () => { await addChangeset(cwd, { empty: false, message: "" }, defaultConfig); - // @ts-ignore - const call = writeChangeset.mock.calls[0][0]; - expect(call).toEqual( + const changesets = await getChangesets(cwd); + expect(changesets.length).toBe(1); + expect(changesets[0]).toEqual( expect.objectContaining({ summary: "", releases: [{ name: "single-package", type: "patch" }], @@ -344,9 +336,9 @@ describe("Add command", () => { defaultConfig ); - // @ts-ignore - const call = writeChangeset.mock.calls[0][0]; - expect(call).toEqual( + const changesets = await getChangesets(cwd); + expect(changesets.length).toBe(1); + expect(changesets[0]).toEqual( expect.objectContaining({ summary: "monorepo summary from message", releases: [{ name: "pkg-a", type: "patch" }], @@ -375,9 +367,9 @@ describe("Add command", () => { defaultConfig ); - // @ts-ignore - const call = writeChangeset.mock.calls[0][0]; - expect(call).toEqual( + const changesets = await getChangesets(cwd); + expect(changesets.length).toBe(1); + expect(changesets[0]).toEqual( expect.objectContaining({ releases: [], summary: "empty changeset summary", @@ -385,6 +377,61 @@ describe("Add command", () => { ); }); + it("should detect changed packages since the given ref", async () => { + const cwd = await gitdir({ + "package.json": JSON.stringify({ + private: true, + workspaces: ["packages/*"], + }), + "packages/pkg-a/package.json": JSON.stringify({ + name: "pkg-a", + version: "1.0.0", + }), + "packages/pkg-b/package.json": JSON.stringify({ + name: "pkg-b", + version: "1.0.0", + }), + ".changeset/config.json": JSON.stringify({}), + }); + + await spawn("git", ["checkout", "-b", "foo"], { cwd }); + await fs.outputFile( + path.join(cwd, "packages/pkg-a/a.js"), + 'export default "a"' + ); + await git.add(".", cwd); + await git.commit("update pkg-a", cwd); + + await spawn("git", ["checkout", "-b", "bar"], { cwd }); + await fs.outputFile( + path.join(cwd, "packages/pkg-b/b.js"), + 'export default "b"' + ); + await git.add(".", cwd); + await git.commit("update pkg-b", cwd); + + mockUserResponses({ releases: { "pkg-b": "patch" } }); + await addChangeset(cwd, { empty: false, since: "foo" }, defaultConfig); + + expect(askCheckboxPlus).toHaveBeenCalledWith( + expect.stringContaining("Which packages"), + [ + { name: "changed packages", choices: ["pkg-b"] }, + { name: "unchanged packages", choices: ["pkg-a"] }, + ], + expect.any(Function) + ); + + const changesets = await getChangesets(cwd); + expect(changesets.length).toBe(1); + expect(changesets[0]).toEqual( + expect.objectContaining({ + summary: "summary message mock", + releases: [{ name: "pkg-b", type: "patch" }], + }) + ); + }); + it("should not include ignored packages in the prompt", async () => { const cwd = await testdir({ "package.json": JSON.stringify({ diff --git a/packages/cli/src/commands/add/index.ts b/packages/cli/src/commands/add/index.ts index a0db9dedf..110bfa061 100644 --- a/packages/cli/src/commands/add/index.ts +++ b/packages/cli/src/commands/add/index.ts @@ -21,8 +21,9 @@ export default async function add( { empty, open, + since, message, - }: { empty?: boolean; open?: boolean; message?: string }, + }: { empty?: boolean; open?: boolean; since?: string; message?: string }, config: Config ): Promise { const packages = await getPackages(cwd); @@ -63,13 +64,17 @@ export default async function add( changedPackagesNames = ( await getVersionableChangedPackages(config, { cwd, + ref: since, }) ).map((pkg) => pkg.packageJson.name); } catch (e: any) { // NOTE: Getting the changed packages is best effort as it's only being used for easier selection // in the CLI. So if any error happens while we try to do so, we only log a warning and continue + const branch = since ?? config.baseBranch; warn( - `Failed to find changed packages from the "${config.baseBranch}" base branch due to error below` + `Failed to find changed packages from the "${branch}" ${ + since ? "ref" : "base branch" + } due to error below` ); warn(e); } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 46804c89d..57b9b11aa 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -54,7 +54,7 @@ if (parsed.help && args.length === 1) { $ changeset [command] Commands init - add [--empty] [--open] [--message ] + add [--empty] [--open] [--since ] [--message ] version [--ignore] [--snapshot ] [--snapshot-prerelease-template