Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/add-since-flag.md
Original file line number Diff line number Diff line change
@@ -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`.
8 changes: 7 additions & 1 deletion docs/command-line-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <text>]
- add [--empty] [--open] [--since <ref>] [--message <text>]
- version [--ignore, --snapshot]
- publish [--otp=code, --tag]
- status [--since=master --verbose --output=JSON_FILE.json]
Expand Down Expand Up @@ -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

```
Expand Down
129 changes: 88 additions & 41 deletions packages/cli/src/commands/add/__tests__/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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) => {
Expand Down Expand Up @@ -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" }],
Expand Down Expand Up @@ -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" }],
Expand Down Expand Up @@ -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" }],
Expand All @@ -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/*"],
Expand All @@ -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 () => {
Expand All @@ -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: "",
Expand Down Expand Up @@ -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" }],
Expand All @@ -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" }],
Expand Down Expand Up @@ -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" }],
Expand Down Expand Up @@ -375,16 +367,71 @@ 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",
})
);
});

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({
Expand Down
9 changes: 7 additions & 2 deletions packages/cli/src/commands/add/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
const packages = await getPackages(cwd);
Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ if (parsed.help && args.length === 1) {
$ changeset [command]
Commands
init
add [--empty] [--open] [--message <text>]
add [--empty] [--open] [--since <branch>] [--message <text>]
version [--ignore] [--snapshot <?name>] [--snapshot-prerelease-template <template>]
publish [--tag <name>] [--otp <code>] [--no-git-tag]
status [--since <branch>] [--verbose] [--output JSON_FILE.json]
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ export async function run(
}

if (input.length < 1) {
const { empty, open, message }: CliOptions = flags;
const { empty, open, since, message }: CliOptions = flags;
// @ts-ignore if this is undefined, we have already exited
await add(cwd, { empty, open, message }, config);
await add(cwd, { empty, open, since, message }, config);
} else if (input[0] !== "pre" && input.length > 1) {
error(
"Too many arguments passed to changesets - we only accept the command name as an argument"
Expand Down Expand Up @@ -107,7 +107,7 @@ export async function run(

switch (input[0]) {
case "add": {
await add(cwd, { empty, open, message }, config);
await add(cwd, { empty, open, since, message }, config);
return;
}
case "version": {
Expand Down