"use strict";

jest.mock("write-json-file");
jest.mock("write-pkg");
jest.mock("@lerna/git-utils");
jest.mock("@lerna/prompt");
jest.mock("@lerna/conventional-commits");
jest.mock("@lerna/npm-dist-tag");
jest.mock("@lerna/npm-publish");
jest.mock("@lerna/run-lifecycle");

const fs = require("fs-extra");
const normalizeNewline = require("normalize-newline");
const path = require("path");
const semver = require("semver");

// mocked or stubbed modules
const writeJsonFile = require("write-json-file");
const writePkg = require("write-pkg");
const ConventionalCommitUtilities = require("@lerna/conventional-commits");
const GitUtilities = require("@lerna/git-utils");
const PromptUtilities = require("@lerna/prompt");
const npmDistTag = require("@lerna/npm-dist-tag");
const npmPublish = require("@lerna/npm-publish");
const runLifecycle = require("@lerna/run-lifecycle");

// helpers
const consoleOutput = require("@lerna-test/console-output");
const loggingOutput = require("@lerna-test/logging-output");
const initFixture = require("@lerna-test/init-fixture")(__dirname);
const normalizeRelativeDir = require("@lerna-test/normalize-relative-dir");

// file under test
const lernaPublish = require("@lerna-test/command-runner")(require("../command"));

// assertion helpers
const publishedTagInDirectories = testDir =>
  npmPublish.mock.calls.reduce((arr, [pkg, tag]) => {
    const dir = normalizeRelativeDir(testDir, pkg.location);
    arr.push({ dir, tag });
    return arr;
  }, []);

const removedDistTagInDirectories = testDir =>
  npmDistTag.remove.mock.calls.reduce((obj, [pkg, tag]) => {
    const location = normalizeRelativeDir(testDir, pkg.location);
    obj[location] = tag;
    return obj;
  }, {});

const addedDistTagInDirectories = testDir =>
  npmDistTag.add.mock.calls.reduce((obj, [pkg, version, tag]) => {
    const location = normalizeRelativeDir(testDir, pkg.location);
    obj[location] = `${pkg.name}@${version} ${tag}`;
    return obj;
  }, {});

const gitAddedFiles = testDir =>
  GitUtilities.addFiles.mock.calls.reduce(
    (arr, [files]) => arr.concat(files.map(fp => normalizeRelativeDir(testDir, fp))),
    []
  );

const gitCommitMessage = () => normalizeNewline(GitUtilities.commit.mock.calls[0][0]);

const gitTagsAdded = () => GitUtilities.addTag.mock.calls.map(args => args[0]);

const updatedLernaJson = () => writeJsonFile.mock.calls[0][1];

const updatedPackageVersions = testDir =>
  writePkg.mock.calls.reduce((obj, args) => {
    const location = normalizeRelativeDir(testDir, path.dirname(args[0]));
    obj[location] = args[1].version;
    return obj;
  }, {});

const updatedPackageJSON = name =>
  writePkg.mock.calls
    .reduce((arr, args) => {
      if (args[1].name === name) {
        arr.push(args[1]);
      }
      return arr;
    }, [])
    .pop();

describe("PublishCommand", () => {
  // default exports that return Promises
  writePkg.mockResolvedValue();
  writeJsonFile.mockResolvedValue();

  // we've already tested these utilities elsewhere
  GitUtilities.isInitialized.mockReturnValue(true);
  GitUtilities.getCurrentBranch.mockReturnValue("master");
  GitUtilities.getCurrentSHA.mockReturnValue("FULL_SHA");
  GitUtilities.getShortSHA.mockReturnValue("deadbeef");
  GitUtilities.diffSinceIn.mockReturnValue("");

  npmPublish.mockResolvedValue();
  runLifecycle.mockImplementation(() => Promise.resolve());
  npmDistTag.check.mockReturnValue(true);

  PromptUtilities.select.mockResolvedValue("1.0.1");
  PromptUtilities.confirm.mockResolvedValue(true);

  /** =========================================================================
   * NORMAL
   * ======================================================================= */

  describe("normal mode", () => {
    it("should publish the changed packages", async () => {
      const testDir = await initFixture("normal");
      await lernaPublish(testDir)();

      expect(PromptUtilities.select.mock.calls).toMatchSnapshot("prompt");
      expect(PromptUtilities.confirm).toBeCalled();

      expect(updatedLernaJson()).toMatchObject({ version: "1.0.1" });
      expect(updatedPackageVersions(testDir)).toMatchSnapshot("updated packages");

      expect(updatedPackageJSON("package-1")).toEqual({
        name: "package-1",
        version: "1.0.1",
        gitHead: "FULL_SHA", // not versioned
      });
      expect(updatedPackageJSON("package-2").dependencies).toMatchObject({
        "package-1": "^1.0.1",
      });
      expect(updatedPackageJSON("package-3").devDependencies).toMatchObject({
        "package-2": "^1.0.1",
      });
      // peerDependencies are _never_ modified automatically
      expect(updatedPackageJSON("package-3").peerDependencies).toMatchObject({
        "package-2": "^1.0.0",
      });
      expect(updatedPackageJSON("package-4").dependencies).toMatchObject({
        "package-1": "^0.0.0",
      });
      expect(updatedPackageJSON("package-5").dependencies).toMatchObject({
        "package-1": "^1.0.1",
      });
      expect(updatedPackageJSON("package-5").optionalDependencies).toMatchObject({
        "package-3": "^1.0.1",
      });

      expect(gitAddedFiles(testDir)).toMatchSnapshot("git added files");
      expect(gitCommitMessage()).toEqual("v1.0.1");
      expect(gitTagsAdded()).toEqual(["v1.0.1"]);
      expect(GitUtilities.checkoutChanges).lastCalledWith(
        expect.stringContaining("packages/*/package.json"),
        expect.objectContaining({
          cwd: testDir,
        })
      );

      expect(publishedTagInDirectories(testDir)).toMatchSnapshot("npm published");

      expect(GitUtilities.pushWithTags).lastCalledWith(
        "origin",
        gitTagsAdded(),
        expect.objectContaining({
          cwd: testDir,
        })
      );
      expect(consoleOutput()).toMatchSnapshot("console output");
    });

    it("throws an error when --independent is passed", async () => {
      expect.assertions(1);
      const testDir = await initFixture("normal");
      try {
        await lernaPublish(testDir)("--independent");
      } catch (err) {
        expect(err.message).toMatch("independent");
      }
    });
  });

  /** =========================================================================
   * INDEPENDENT
   * ======================================================================= */

  describe("independent mode", () => {
    it("should publish the changed packages in independent mode", async () => {
      // mock version prompt choices
      ["1.0.1", "1.1.0", "2.0.0", "1.1.0", "1.0.1"].forEach(chosenVersion =>
        PromptUtilities.select.mockResolvedValueOnce(chosenVersion)
      );

      const testDir = await initFixture("independent");
      await lernaPublish(testDir)(); // --independent is only valid in InitCommand

      expect(PromptUtilities.confirm).toBeCalled();
      expect(writeJsonFile).not.toBeCalled();

      expect(updatedPackageVersions(testDir)).toMatchSnapshot("updated packages");

      expect(updatedPackageJSON("package-1")).toEqual({
        name: "package-1",
        version: "1.0.1",
        gitHead: "FULL_SHA", // not versioned
      });
      expect(updatedPackageJSON("package-2").dependencies).toMatchObject({
        "package-1": "^1.0.1",
      });
      expect(updatedPackageJSON("package-3").devDependencies).toMatchObject({
        "package-2": "^1.1.0",
      });
      expect(updatedPackageJSON("package-4").dependencies).toMatchObject({
        "package-1": "^0.0.0",
      });
      expect(updatedPackageJSON("package-5").dependencies).toMatchObject({
        "package-3": "^2.0.0",
      });

      expect(gitAddedFiles(testDir)).toMatchSnapshot("git added files");
      expect(gitCommitMessage()).toMatchSnapshot("git commit message");
      expect(gitTagsAdded()).toMatchSnapshot("git tags added");

      expect(publishedTagInDirectories(testDir)).toMatchSnapshot("npm published");

      expect(GitUtilities.pushWithTags).lastCalledWith(
        "origin",
        gitTagsAdded(),
        expect.objectContaining({
          cwd: testDir,
        })
      );
      expect(consoleOutput()).toMatchSnapshot("console output");
    });
  });

  /** =========================================================================
   * NORMAL - CANARY
   * ======================================================================= */

  describe("normal mode as canary", () => {
    it("should publish the changed packages", async () => {
      const testDir = await initFixture("normal");
      await lernaPublish(testDir)("--canary");

      expect(PromptUtilities.select).not.toBeCalled();

      expect(writeJsonFile).not.toBeCalled();
      expect(updatedPackageVersions(testDir)).toMatchSnapshot("updated packages");

      expect(updatedPackageJSON("package-2").dependencies).toMatchObject({
        "package-1": "^1.1.0-alpha.deadbeef",
      });
      expect(updatedPackageJSON("package-3").devDependencies).toMatchObject({
        "package-2": "^1.1.0-alpha.deadbeef",
      });
      expect(updatedPackageJSON("package-4").dependencies).toMatchObject({
        "package-1": "^0.0.0",
      });

      expect(GitUtilities.addFiles).not.toBeCalled();
      expect(GitUtilities.commit).not.toBeCalled();
      expect(GitUtilities.addTag).not.toBeCalled();
      expect(GitUtilities.checkoutChanges).lastCalledWith(
        expect.stringContaining("packages/*/package.json"),
        expect.objectContaining({
          cwd: testDir,
        })
      );

      expect(GitUtilities.pushWithTags).not.toBeCalled();
      expect(publishedTagInDirectories(testDir)).toMatchSnapshot("npm published");
    });

    it("should use the provided value as the meta suffix", async () => {
      const testDir = await initFixture("normal");
      await lernaPublish(testDir)("--canary", "beta");

      expect(updatedPackageVersions(testDir)).toMatchSnapshot("updated packages");

      expect(updatedPackageJSON("package-2").dependencies).toMatchObject({
        "package-1": "^1.1.0-beta.deadbeef",
      });
      expect(updatedPackageJSON("package-3").devDependencies).toMatchObject({
        "package-2": "^1.1.0-beta.deadbeef",
      });
      expect(updatedPackageJSON("package-4").dependencies).toMatchObject({
        "package-1": "^0.0.0",
      });
    });

    it("should work with --canary and --cd-version=patch", async () => {
      const testDir = await initFixture("normal");
      await lernaPublish(testDir)("--canary", "--cd-version", "patch");

      expect(updatedPackageVersions(testDir)).toMatchSnapshot("updated packages");

      expect(updatedPackageJSON("package-2").dependencies).toMatchObject({
        "package-1": "^1.0.1-alpha.deadbeef",
      });
      expect(updatedPackageJSON("package-3").devDependencies).toMatchObject({
        "package-2": "^1.0.1-alpha.deadbeef",
      });
      expect(updatedPackageJSON("package-4").dependencies).toMatchObject({
        "package-1": "^0.0.0",
      });
    });
  });

  /** =========================================================================
   * INDEPENDENT - CANARY
   * ======================================================================= */

  describe("independent mode as canary", () => {
    it("should publish the changed packages", async () => {
      const testDir = await initFixture("independent");
      await lernaPublish(testDir)("--canary");

      expect(PromptUtilities.select).not.toBeCalled();

      expect(writeJsonFile).not.toBeCalled();
      expect(updatedPackageVersions(testDir)).toMatchSnapshot("updated packages");

      expect(updatedPackageJSON("package-2").dependencies).toMatchObject({
        "package-1": "^1.1.0-alpha.deadbeef",
      });
      expect(updatedPackageJSON("package-3").devDependencies).toMatchObject({
        "package-2": "^2.1.0-alpha.deadbeef",
      });
      expect(updatedPackageJSON("package-4").dependencies).toMatchObject({
        "package-1": "^0.0.0",
      });

      expect(publishedTagInDirectories(testDir)).toMatchSnapshot("npm published");
    });

    it("should use the provided value as the meta suffix", async () => {
      const testDir = await initFixture("independent");
      await lernaPublish(testDir)("--canary", "beta");

      expect(updatedPackageJSON("package-2").dependencies).toMatchObject({
        "package-1": "^1.1.0-beta.deadbeef",
      });
      expect(updatedPackageJSON("package-3").devDependencies).toMatchObject({
        "package-2": "^2.1.0-beta.deadbeef",
      });
      expect(updatedPackageJSON("package-4").dependencies).toMatchObject({
        "package-1": "^0.0.0",
      });
    });
  });

  /** =========================================================================
   * NORMAL - SKIP GIT
   * ======================================================================= */

  describe("normal mode with --skip-git", () => {
    it("should publish the changed packages", async () => {
      const testDir = await initFixture("normal");
      await lernaPublish(testDir)("--skip-git");

      expect(GitUtilities.addFiles).not.toBeCalled();
      expect(GitUtilities.commit).not.toBeCalled();
      expect(GitUtilities.addTag).not.toBeCalled();
      expect(GitUtilities.pushWithTags).not.toBeCalled();

      expect(updatedPackageJSON("package-1")).toEqual({
        name: "package-1",
        version: "1.0.1",
        gitHead: "FULL_SHA", // not versioned
      });
      expect(publishedTagInDirectories(testDir)).toMatchSnapshot("npm published");
    });

    it("should display a message that git is skipped", async () => {
      const testDir = await initFixture("normal");
      await lernaPublish(testDir)("--skip-git");

      const logMessages = loggingOutput("info");
      expect(logMessages).toContain("Skipping git commit/push");
    });
  });

  /** =========================================================================
   * NORMAL - SKIP NPM
   * ======================================================================= */

  describe("normal mode with --skip-npm", () => {
    it("should update versions and push changes but not publish", async () => {
      const testDir = await initFixture("normal");
      await lernaPublish(testDir)("--skip-npm");

      expect(npmPublish).not.toBeCalled();
      expect(updatedPackageJSON("package-1")).toEqual({
        name: "package-1",
        version: "1.0.1",
        // gitHead not annotated
      });

      expect(gitCommitMessage()).toEqual("v1.0.1");
      expect(GitUtilities.pushWithTags).lastCalledWith(
        "origin",
        ["v1.0.1"],
        expect.objectContaining({
          cwd: testDir,
        })
      );
    });

    it("should display a message that npm is skipped", async () => {
      const testDir = await initFixture("normal");
      await lernaPublish(testDir)("--skip-npm");

      const logMessages = loggingOutput("info");
      expect(logMessages).toContain("Skipping publish to registry");
    });
  });

  /** =========================================================================
   * NORMAL - SKIP GIT AND SKIP NPM
   * ======================================================================= */

  describe("normal mode with --skip-git and --skip-npm", () => {
    it("should update versions but not push changes or publish", async () => {
      const testDir = await initFixture("normal");
      await lernaPublish(testDir)("--skip-git", "--skip-npm");

      expect(updatedLernaJson()).toMatchObject({ version: "1.0.1" });
      expect(updatedPackageVersions(testDir)).toMatchSnapshot("updated packages");

      expect(updatedPackageJSON("package-2").dependencies).toMatchObject({
        "package-1": "^1.0.1",
      });
      expect(updatedPackageJSON("package-3").devDependencies).toMatchObject({
        "package-2": "^1.0.1",
      });
      expect(updatedPackageJSON("package-4").dependencies).toMatchObject({
        "package-1": "^0.0.0",
      });
      expect(updatedPackageJSON("package-5").dependencies).toMatchObject({
        "package-1": "^1.0.1",
      });

      expect(GitUtilities.addFiles).not.toBeCalled();
      expect(GitUtilities.commit).not.toBeCalled();
      expect(GitUtilities.addTag).not.toBeCalled();
      expect(GitUtilities.pushWithTags).not.toBeCalled();

      expect(npmPublish).not.toBeCalled();
      expect(npmDistTag.check).not.toBeCalled();
      expect(npmDistTag.remove).not.toBeCalled();
      expect(npmDistTag.add).not.toBeCalled();
    });

    it("should display a message that npm and git are skipped", async () => {
      const testDir = await initFixture("normal");
      await lernaPublish(testDir)("--skip-git", "--skip-npm");

      const logMessages = loggingOutput("info");
      expect(logMessages).toContain("Skipping git commit/push");
      expect(logMessages).toContain("Skipping publish to registry");
    });
  });

  /** =========================================================================
   * NORMAL - TEMP TAG
   * ======================================================================= */

  describe("normal mode with --temp-tag", () => {
    it("should publish the changed packages with a temp tag", async () => {
      const testDir = await initFixture("normal");
      await lernaPublish(testDir)("--temp-tag");

      expect(publishedTagInDirectories(testDir)).toMatchSnapshot("npm published");
      expect(removedDistTagInDirectories(testDir)).toMatchSnapshot("npm dist-tag rm");
      expect(addedDistTagInDirectories(testDir)).toMatchSnapshot("npm dist-tag add");

      expect(GitUtilities.pushWithTags).lastCalledWith(
        "origin",
        ["v1.0.1"],
        expect.objectContaining({
          cwd: testDir,
        })
      );
    });
  });

  /** =========================================================================
   * NORMAL - NPM TAG
   * ======================================================================= */

  describe("normal mode with --npm-tag", () => {
    it("should publish the changed packages with npm tag", async () => {
      const testDir = await initFixture("normal");
      await lernaPublish(testDir)("--npm-tag", "custom");

      expect(publishedTagInDirectories(testDir)).toMatchSnapshot("npm published");
    });
  });

  /** =========================================================================
   * AUTO-ACCEPT
   * ======================================================================= */

  describe("with --yes", () => {
    it("skips confirmation prompt", async () => {
      const testDir = await initFixture("normal");
      await lernaPublish(testDir)("--yes", "--repo-version", "1.0.1-auto-confirm");

      expect(PromptUtilities.select).not.toBeCalled();
      expect(PromptUtilities.confirm).not.toBeCalled();
      expect(updatedLernaJson()).toMatchObject({ version: "1.0.1-auto-confirm" });
    });
  });

  /** =========================================================================
   * NORMAL - REGISTRY
   * ======================================================================= */

  describe("normal mode with --registry", () => {
    it("passes registry to npm commands", async () => {
      const testDir = await initFixture("normal");
      const registry = "https://my-private-registry";

      await lernaPublish(testDir)("--registry", registry);

      expect(npmDistTag.check).not.toBeCalled();
      expect(npmDistTag.remove).not.toBeCalled();
      expect(npmDistTag.add).not.toBeCalled();
      // FIXME: this isn't actually asserting anything about --registry
      expect(publishedTagInDirectories(testDir)).toMatchSnapshot("npm published");
    });
  });

  /** =========================================================================
   * NORMAL - REPO VERSION
   * ======================================================================= */

  describe("normal mode with --repo-version", () => {
    it("skips version prompt and publishes changed packages with designated version", async () => {
      const testDir = await initFixture("normal");
      await lernaPublish(testDir)("--repo-version", "1.0.1-beta");

      expect(PromptUtilities.select).not.toBeCalled();
      expect(updatedLernaJson()).toMatchObject({ version: "1.0.1-beta" });
    });
  });

  /** =========================================================================
   * NORMAL - EXACT
   * ======================================================================= */

  describe("normal mode with --exact", () => {
    it("updates matching local dependencies of published packages with exact versions", async () => {
      const testDir = await initFixture("normal");
      await lernaPublish(testDir)("--exact");

      expect(updatedPackageJSON("package-2").dependencies).toMatchObject({
        "package-1": "1.0.1",
      });
      expect(updatedPackageJSON("package-3").devDependencies).toMatchObject({
        "package-2": "1.0.1",
      });
      // package-4's dependency on package-1 remains semver because
      // it does not match the version of package-1 being published
      expect(updatedPackageJSON("package-4").dependencies).toMatchObject({
        "package-1": "^0.0.0",
      });
      expect(updatedPackageJSON("package-5").dependencies).toMatchObject({
        "package-1": "1.0.1",
      });
    });

    it("updates existing exact versions", async () => {
      const testDir = await initFixture("normal-exact");
      await lernaPublish(testDir)();

      expect(updatedPackageJSON("package-2").dependencies).toMatchObject({
        "package-1": "1.0.1",
      });
      expect(updatedPackageJSON("package-3").devDependencies).toMatchObject({
        "package-2": "1.0.1",
      });
      expect(updatedPackageJSON("package-4").dependencies).toMatchObject({
        "package-1": "0.1.0",
      });
      expect(updatedPackageJSON("package-5").dependencies).toMatchObject({
        "package-1": "1.0.1",
      });
    });
  });

  /** =========================================================================
   * NORMAL MODE - CD VERSION
   * ======================================================================= */

  describe("normal mode with --cd-version", () => {
    it("should use semver increments when passed to cdVersion flag", async () => {
      const testDir = await initFixture("normal");
      await lernaPublish(testDir)("--cd-version", "minor");

      expect(PromptUtilities.select).not.toBeCalled();
      expect(gitCommitMessage()).toBe("v1.1.0");
    });

    it("throws an error when an invalid semver keyword is used", async () => {
      expect.assertions(1);

      const testDir = await initFixture("normal");
      try {
        await lernaPublish(testDir)("--cd-version", "poopypants");
      } catch (err) {
        expect(err.message).toBe(
          "--cd-version must be one of: " +
            "'major', 'minor', 'patch', 'premajor', 'preminor', 'prepatch', or 'prerelease'."
        );
      }
    });
  });

  /** =========================================================================
   * CD VERSION - REPUBLISH PRERELEASED
   * ======================================================================= */

  describe("normal mode with previous prerelease", () => {
    beforeEach(() => {
      GitUtilities.hasTags.mockReturnValueOnce(true);
      GitUtilities.getLastTag.mockReturnValueOnce("v1.0.1-beta.3");
      GitUtilities.diffSinceIn
        .mockReturnValueOnce("")
        .mockReturnValueOnce("")
        .mockReturnValueOnce("packages/package-3/newfile.json");
    });

    it("publishes changed & prereleased packages if --cd-version is non-prerelease", async () => {
      const testDir = await initFixture("republish-prereleased");
      // should republish 3, 4, and 5 because:
      // package 3 changed
      // package 5 has a prerelease version
      // package 4 depends on package 5
      await lernaPublish(testDir)("--cd-version", "patch");

      expect(gitCommitMessage()).toBe("v1.0.1");
      expect(updatedPackageVersions(testDir)).toMatchSnapshot("updated packages");
      expect(updatedPackageJSON("package-4").dependencies).toMatchObject({
        "package-5": "^1.0.1",
      });
    });

    it("should not publish prereleased packages if --cd-version is a pre-* increment", async () => {
      const testDir = await initFixture("republish-prereleased");
      // should republish only package 3, because it changed
      await lernaPublish(testDir)("--cd-version", "prerelease", "---preid", "beta");

      expect(gitCommitMessage()).toBe("v1.0.1-beta.4");
      expect(updatedPackageVersions(testDir)).toMatchSnapshot("updated packages");
    });
  });

  /** =========================================================================
   * INDEPENDENT - CD VERSION
   * ======================================================================= */

  describe("indepdendent mode with --cd-version", () => {
    it("should use semver increments when passed to cdVersion flag", async () => {
      const testDir = await initFixture("independent");
      await lernaPublish(testDir)("--cd-version", "patch");

      expect(PromptUtilities.select).not.toBeCalled();
      expect(gitCommitMessage()).toMatchSnapshot("git commit message");
    });

    /** =========================================================================
     * INDEPENDENT - CD VERSION - PRERELEASE
     * ======================================================================= */

    it("should bump to prerelease versions with --cd-version=prerelease --preid=foo", async () => {
      const testDir = await initFixture("independent");
      await lernaPublish(testDir)("--cd-version", "prerelease", "--preid", "foo");

      expect(updatedPackageVersions(testDir)).toMatchSnapshot("updated packages");

      expect(updatedPackageJSON("package-2").dependencies).toMatchObject({
        "package-1": "^1.0.1-foo.0",
      });
      expect(updatedPackageJSON("package-3").devDependencies).toMatchObject({
        "package-2": "^2.0.1-foo.0",
      });
      expect(updatedPackageJSON("package-4").dependencies).toMatchObject({
        "package-1": "^0.0.0",
      });
    });

    it("should bump to prerelease versions with --cd-version prerelease (no --preid)", async () => {
      const testDir = await initFixture("independent");
      await lernaPublish(testDir)("--cd-version", "prerelease");

      expect(updatedPackageVersions(testDir)).toMatchSnapshot("updated packages");

      expect(updatedPackageJSON("package-2").dependencies).toMatchObject({
        "package-1": "^1.0.1-0",
      });
      expect(updatedPackageJSON("package-3").devDependencies).toMatchObject({
        "package-2": "^2.0.1-0",
      });
      expect(updatedPackageJSON("package-4").dependencies).toMatchObject({
        "package-1": "^0.0.0",
      });
    });
  });

  /** =========================================================================
   * NORMAL - GIT REMOTE
   * ======================================================================= */

  describe("normal mode with --git-remote", () => {
    it("pushes tags to specified remote", async () => {
      const testDir = await initFixture("normal");
      await lernaPublish(testDir)("--git-remote", "upstream");

      expect(GitUtilities.pushWithTags).lastCalledWith(
        "upstream",
        ["v1.0.1"],
        expect.objectContaining({
          cwd: testDir,
        })
      );
    });
  });

  /** =========================================================================
   * NORMAL - IGNORE CHANGES
   * ======================================================================= */

  describe("normal mode with --ignore-changes", () => {
    it("does not publish packages with ignored changes", async () => {
      const testDir = await initFixture("normal");

      GitUtilities.hasTags.mockReturnValueOnce(true);
      GitUtilities.getLastTag.mockReturnValueOnce("v1.0.0");
      GitUtilities.diffSinceIn
        .mockReturnValueOnce("")
        .mockReturnValueOnce("packages/package-2/README.md")
        .mockReturnValueOnce("packages/package-3/__tests__/pkg3.test.js")
        .mockReturnValueOnce("packages/package-4/lib/foo.js");

      await lernaPublish(testDir)(
        "--ignore-changes",
        "README.md",

        "--ignore-changes",
        "**/__tests__/**",

        "--ignore-changes",
        "package-4" // notably does NOT work, needs to be "**/package-4/**" to match
      );

      expect(gitAddedFiles(testDir)).toMatchSnapshot("git added files");
      expect(publishedTagInDirectories(testDir)).toMatchSnapshot("npm published");
    });

    it("maps deprecated --ignore", async () => {
      const testDir = await initFixture("normal");

      GitUtilities.hasTags.mockReturnValueOnce(true);
      GitUtilities.getLastTag.mockReturnValueOnce("v1.0.0");
      GitUtilities.diffSinceIn
        .mockReturnValueOnce("")
        .mockReturnValueOnce("")
        .mockReturnValueOnce("packages/package-3/README.md")
        .mockReturnValueOnce("packages/package-4/lib/foo.js");

      await lernaPublish(testDir)("--ignore", "*.md");

      expect(gitAddedFiles(testDir)).toMatchSnapshot("git added files");
      expect(publishedTagInDirectories(testDir)).toMatchSnapshot("npm published");
    });
  });

  /** =========================================================================
   * NORMAL - MESSAGE
   * ======================================================================= */

  describe("normal mode with --message", () => {
    it("commits changes with a custom message using %s", async () => {
      const testDir = await initFixture("normal");
      await lernaPublish(testDir)("--message", "chore: Release %s :rocket:");

      expect(GitUtilities.commit).lastCalledWith(
        "chore: Release v1.0.1 :rocket:",
        expect.objectContaining({
          cwd: testDir,
        })
      );
    });

    it("commits changes with a custom message using %v", async () => {
      const testDir = await initFixture("normal");
      await lernaPublish(testDir)("--message", "chore: Release %v :rocket:");

      expect(GitUtilities.commit).lastCalledWith(
        "chore: Release 1.0.1 :rocket:",
        expect.objectContaining({
          cwd: testDir,
        })
      );
    });
  });

  /** =========================================================================
   * INDEPENDENT - MESSAGE
   * ======================================================================= */

  describe("independent mode with --message", () => {
    it("commits changes with a custom message", async () => {
      const testDir = await initFixture("independent");
      await lernaPublish(testDir)("-m", "chore: Custom publish message");

      expect(GitUtilities.commit).lastCalledWith(
        expect.stringContaining("chore: Custom publish message"),
        expect.objectContaining({
          cwd: testDir,
        })
      );
      expect(gitCommitMessage()).toMatchSnapshot("git commit message");
    });
  });

  /** =========================================================================
   * CONVENTIONAL COMMITS
   * ======================================================================= */

  describe("--conventional-commits", () => {
    ConventionalCommitUtilities.updateChangelog.mockImplementation(pkg =>
      Promise.resolve(path.join(pkg.location, "CHANGELOG.md"))
    );

    describe("independent mode", () => {
      const versionBumps = ["1.0.1", "2.1.0", "4.0.0", "4.1.0", "5.0.1"];

      beforeEach(() => {
        versionBumps.forEach(bump =>
          ConventionalCommitUtilities.recommendVersion.mockResolvedValueOnce(bump)
        );
      });

      it("should use conventional-commits utility to guess version bump and generate CHANGELOG", async () => {
        const testDir = await initFixture("independent");

        await lernaPublish(testDir)("--conventional-commits");

        expect(gitAddedFiles(testDir)).toMatchSnapshot("git added files");
        expect(gitCommitMessage()).toMatchSnapshot("git commit message");

        ["package-1", "package-2", "package-3", "package-4", "package-5"].forEach((name, idx) => {
          expect(ConventionalCommitUtilities.recommendVersion).toBeCalledWith(
            expect.objectContaining({ name }),
            "independent",
            { changelogPreset: undefined, rootPath: testDir }
          );
          expect(ConventionalCommitUtilities.updateChangelog).toBeCalledWith(
            expect.objectContaining({ name, version: versionBumps[idx] }),
            "independent",
            { changelogPreset: undefined, rootPath: testDir }
          );
        });
      });

      it("accepts --changelog-preset option", async () => {
        const testDir = await initFixture("independent");
        const changelogOpts = { changelogPreset: "foo-bar", rootPath: testDir };

        await lernaPublish(testDir)("--conventional-commits", "--changelog-preset", "foo-bar");

        expect(ConventionalCommitUtilities.recommendVersion).toBeCalledWith(
          expect.any(Object),
          "independent",
          changelogOpts
        );
        expect(ConventionalCommitUtilities.updateChangelog).toBeCalledWith(
          expect.any(Object),
          "independent",
          changelogOpts
        );
      });
    });

    describe("fixed mode", () => {
      beforeEach(() => {
        ConventionalCommitUtilities.recommendVersion
          .mockResolvedValueOnce("1.0.1")
          .mockResolvedValueOnce("1.1.0")
          .mockResolvedValueOnce("2.0.0")
          .mockResolvedValueOnce("1.1.0")
          .mockResolvedValueOnce("1.0.0");
      });

      it("should use conventional-commits utility to guess version bump and generate CHANGELOG", async () => {
        const testDir = await initFixture("normal");

        await lernaPublish(testDir)("--conventional-commits");

        expect(gitAddedFiles(testDir)).toMatchSnapshot("git added files");
        expect(gitCommitMessage()).toMatchSnapshot("git commit message");

        ["package-1", "package-2", "package-3", "package-4", "package-5"].forEach(name => {
          const location = path.join(testDir, "packages", name);

          expect(ConventionalCommitUtilities.recommendVersion).toBeCalledWith(
            expect.objectContaining({ name, location }),
            "fixed",
            { changelogPreset: undefined, rootPath: testDir }
          );

          expect(ConventionalCommitUtilities.updateChangelog).toBeCalledWith(
            expect.objectContaining({ name, version: "2.0.0" }),
            "fixed",
            { changelogPreset: undefined, rootPath: testDir }
          );
        });

        expect(ConventionalCommitUtilities.updateChangelog).lastCalledWith(
          expect.objectContaining({
            name: "normal",
            location: testDir,
          }),
          "root",
          { changelogPreset: undefined, rootPath: testDir, version: "2.0.0" }
        );
      });

      it("accepts --changelog-preset option", async () => {
        const testDir = await initFixture("normal");
        const changelogOpts = { changelogPreset: "baz-qux", rootPath: testDir };

        await lernaPublish(testDir)("--conventional-commits", "--changelog-preset", "baz-qux");

        expect(ConventionalCommitUtilities.recommendVersion).toBeCalledWith(
          expect.any(Object),
          "fixed",
          changelogOpts
        );
        expect(ConventionalCommitUtilities.updateChangelog).toBeCalledWith(
          expect.any(Object),
          "fixed",
          changelogOpts
        );
      });
    });

    it("avoids duplicating previously-released version", async () => {
      const testDir = await initFixture("normal-no-inter-dependencies");

      GitUtilities.hasTags.mockReturnValueOnce(true);
      GitUtilities.getLastTag.mockReturnValueOnce("v1.0.0");
      GitUtilities.diffSinceIn.mockReturnValueOnce(`packages/package-1/package.json`);

      // feat(package-1): add thing
      ConventionalCommitUtilities.recommendVersion.mockResolvedValueOnce("1.1.0");

      await lernaPublish(testDir)("--conventional-commits");

      expect(updatedPackageVersions(testDir)).toEqual({
        "packages/package-1": "1.1.0",
      });

      // make subsequent publish start with the right globalVersion
      await fs.writeJSON(path.join(testDir, "lerna.json"), { version: "1.1.0" });

      // clear previous publish mock records
      jest.clearAllMocks();

      GitUtilities.hasTags.mockReturnValueOnce(true);
      GitUtilities.getLastTag.mockReturnValueOnce("v1.1.0");
      GitUtilities.diffSinceIn
        .mockReturnValueOnce("") // package-1 has no changes
        .mockReturnValueOnce(`packages/package-2/package.json`);

      // fix(package-2): oops thing
      ConventionalCommitUtilities.recommendVersion.mockImplementationOnce(pkg =>
        Promise.resolve(semver.inc(pkg.version, "patch"))
      );

      await lernaPublish(testDir)("--conventional-commits", "--yes");

      expect(updatedPackageVersions(testDir)).toEqual({
        "packages/package-2": "1.1.1",
      });
    });
  });

  /** =========================================================================
   * INDEPENDENT - CANARY + NPMTAG + YES + EXACT
   * ======================================================================= */

  describe("independent mode --canary --npm-tag=next --yes --exact", () => {
    it("should publish the changed packages", async () => {
      const testDir = await initFixture("independent");
      await lernaPublish(testDir)("--canary", "--npm-tag", "next", "--yes", "--exact");

      expect(publishedTagInDirectories(testDir)).toMatchSnapshot("npm published");
    });
  });

  describe("--allow-branch", () => {
    describe("cli", () => {
      it("should reject a non matching branch", async () => {
        GitUtilities.getCurrentBranch.mockReturnValueOnce("unmatched");

        const testDir = await initFixture("normal");
        try {
          await lernaPublish(testDir)("--allow-branch", "master");
        } catch (err) {
          expect(err.message).toMatch("Branch 'unmatched' is restricted from publishing");
        }
      });

      it("should accept an exactly matching branch", async () => {
        GitUtilities.getCurrentBranch.mockReturnValueOnce("exact-match");

        const testDir = await initFixture("normal");
        await lernaPublish(testDir)("--allow-branch", "exact-match");

        expect(publishedTagInDirectories(testDir)).toHaveLength(4);
      });

      it("should accept a branch that matches by wildcard", async () => {
        GitUtilities.getCurrentBranch.mockReturnValueOnce("feature/awesome");

        const testDir = await initFixture("normal");
        await lernaPublish(testDir)("--allow-branch", "feature/*");

        expect(publishedTagInDirectories(testDir)).toHaveLength(4);
      });

      it("should accept a branch that matches one of the items passed", async () => {
        GitUtilities.getCurrentBranch.mockReturnValueOnce("feature/awesome");

        const testDir = await initFixture("normal");
        await lernaPublish(testDir)("--allow-branch", "master", "feature/*");

        expect(publishedTagInDirectories(testDir)).toHaveLength(4);
      });
    });

    describe("lerna.json", () => {
      it("should reject a non matching branch", async () => {
        GitUtilities.getCurrentBranch.mockReturnValueOnce("unmatched");

        const testDir = await initFixture("allow-branch-lerna");
        try {
          await lernaPublish(testDir)();
        } catch (err) {
          expect(err.message).toMatch("Branch 'unmatched' is restricted from publishing");
        }
      });

      it("should accept a matching branch", async () => {
        GitUtilities.getCurrentBranch.mockReturnValueOnce("lerna");

        const testDir = await initFixture("allow-branch-lerna");
        await lernaPublish(testDir)();

        expect(publishedTagInDirectories(testDir)).toHaveLength(1);
      });

      it("should prioritize cli over defaults", async () => {
        GitUtilities.getCurrentBranch.mockReturnValueOnce("cli-override");

        const testDir = await initFixture("allow-branch-lerna");
        await lernaPublish(testDir)("--allow-branch", "cli-override");

        expect(publishedTagInDirectories(testDir)).toHaveLength(1);
      });
    });

    describe("with --canary", () => {
      it("does not restrict publishing canary versions", async () => {
        const testDir = await initFixture("normal");
        GitUtilities.getCurrentBranch.mockReturnValueOnce("other");

        await lernaPublish(testDir)("--allow-branch", "master", "--canary");
        expect(updatedPackageVersions(testDir)).toMatchSnapshot("updated packages");
      });
    });
  });

  describe("when local clone is behind upstream", () => {
    it("throws an error during interactive publish", async () => {
      const testDir = await initFixture("normal");

      GitUtilities.isBehindUpstream.mockReturnValueOnce(true);

      try {
        await lernaPublish(testDir)();
      } catch (err) {
        expect(err.message).toMatch("behind remote upstream");
        expect(err.message).toMatch("Please merge remote changes");
      }
    });

    it("logs a warning and exits early during CI publish", async () => {
      const testDir = await initFixture("normal");

      GitUtilities.isBehindUpstream.mockReturnValueOnce(true);

      await lernaPublish(testDir)("--ci");

      const [warning] = loggingOutput("warn");
      expect(warning).toMatch("behind remote upstream");
      expect(warning).toMatch("exiting");
    });
  });

  /** =========================================================================
   * VERSION LIFECYCLE SCRIPTS
   * ======================================================================= */

  describe("lifecycle scripts", () => {
    it("calls version and publish lifecycle scripts for root and packages", async () => {
      const testDir = await initFixture("lifecycle");
      await lernaPublish(testDir)();

      expect(runLifecycle).toHaveBeenCalledTimes(12);

      ["preversion", "version", "postversion"].forEach(script => {
        expect(runLifecycle).toHaveBeenCalledWith(
          expect.objectContaining({ name: "lifecycle" }),
          script,
          expect.any(Object) // conf
        );
        expect(runLifecycle).toHaveBeenCalledWith(
          expect.objectContaining({ name: "package-1" }),
          script,
          expect.any(Object) // conf
        );
      });

      // package-2 lacks version lifecycle scripts
      expect(runLifecycle).not.toHaveBeenCalledWith(
        expect.objectContaining({ name: "package-2" }),
        expect.any(String),
        expect.any(Object) // conf
      );

      expect(runLifecycle.mock.calls.map(([pkg, script]) => [pkg.name, script])).toEqual([
        ["lifecycle", "preversion"],
        ["package-1", "preversion"],
        ["package-1", "version"],
        ["lifecycle", "version"],
        ["package-1", "postversion"],
        ["lifecycle", "postversion"],
        ["lifecycle", "prepare"],
        ["lifecycle", "prepublishOnly"],
        ["package-1", "prepare"],
        ["package-1", "prepublishOnly"],
        ["package-1", "postpublish"],
        ["lifecycle", "postpublish"],
      ]);
    });

    it("logs lifecycle errors but preserves chain", async () => {
      const testDir = await initFixture("lifecycle");

      runLifecycle.mockImplementationOnce(() => Promise.reject(new Error("boom")));

      await lernaPublish(testDir)();

      expect(runLifecycle).toHaveBeenCalledTimes(12);
      expect(updatedPackageVersions(testDir)).toMatchSnapshot("updated packages");

      const [errorLog] = loggingOutput("error");
      expect(errorLog).toMatch("error running preversion in lifecycle");
    });

    it("adapts to missing root package name", async () => {
      const testDir = await initFixture("lifecycle-no-root-name");

      await lernaPublish(testDir)();

      expect(runLifecycle).toHaveBeenCalledTimes(6);

      ["preversion", "version", "postversion"].forEach(script => {
        expect(runLifecycle).toHaveBeenCalledWith(
          expect.objectContaining({
            // defaulted from dirname, like npm init
            name: path.basename(testDir),
          }),
          script,
          expect.any(Object) // conf
        );
      });
    });
  });

  it("publishes all transitive dependents after change", async () => {
    const testDir = await initFixture("snake-graph");

    GitUtilities.hasTags.mockReturnValueOnce(true);
    GitUtilities.getLastTag.mockReturnValueOnce("v1.0.0");
    GitUtilities.diffSinceIn.mockReturnValueOnce("packages/package-1/package.json");

    await lernaPublish(testDir)("--cd-version", "major", "--yes");

    expect(updatedPackageVersions(testDir)).toMatchSnapshot();
  });

  describe("with git-hosted sibling dependencies", () => {
    it("updates gitCommittish versions as sshurls", async () => {
      const testDir = await initFixture("git-hosted-sibling-committish");

      await lernaPublish(testDir)("--cd-version", "minor");

      expect(updatedPackageVersions(testDir)).toMatchSnapshot();

      // package-1 doesn't have any dependencies
      expect(updatedPackageJSON("package-2").dependencies).toMatchObject({
        "package-1": "git+ssh://git@github.com/user/package-1.git#v1.1.0",
      });
      expect(updatedPackageJSON("package-3").devDependencies).toMatchObject({
        "package-2": "git+ssh://git@github.com/user/package-2.git#v1.1.0",
      });
      expect(updatedPackageJSON("package-4").dependencies).toMatchObject({
        "package-1": "github:user/package-1#v0.0.0", // non-matching semver
      });
      expect(updatedPackageJSON("package-5").dependencies).toMatchObject({
        "package-1": "git+ssh://git@github.com/user/package-1.git#v1.1.0",
      });
    });

    it("updates gitRange versions as sshurls", async () => {
      const testDir = await initFixture("git-hosted-sibling-semver");

      await lernaPublish(testDir)("--cd-version", "prerelease", "--preid", "beta");

      expect(updatedPackageVersions(testDir)).toMatchSnapshot();

      // package-1 doesn't have any dependencies
      expect(updatedPackageJSON("package-2").dependencies).toMatchObject({
        "package-1": "git+ssh://git@github.com/user/package-1.git#semver:^1.0.1-beta.0",
      });
      expect(updatedPackageJSON("package-3").devDependencies).toMatchObject({
        "package-2": "git+ssh://git@github.com/user/package-2.git#semver:^1.0.1-beta.0",
      });
      expect(updatedPackageJSON("package-4").dependencies).toMatchObject({
        "package-1": "github:user/package-1#semver:^0.1.0", // non-matching semver
      });
      expect(updatedPackageJSON("package-5").dependencies).toMatchObject({
        "package-1": "git+ssh://git@github.com/user/package-1.git#semver:^1.0.1-beta.0",
      });
    });
  });

  describe("with relative file: specifiers", () => {
    beforeEach(() => {
      GitUtilities.hasTags.mockReturnValueOnce(true);
      GitUtilities.getLastTag.mockReturnValueOnce("v1.0.0");
      GitUtilities.diffSinceIn.mockReturnValueOnce("packages/package-1/package.json");
    });

    it("overwrites relative link with local version before npm publish but after git commit", async () => {
      const testDir = await initFixture("relative-file-specs");

      await lernaPublish(testDir)("--cd-version", "major", "--yes");

      expect(updatedPackageVersions(testDir)).toMatchSnapshot();

      // notably missing is package-1, which has no relative file: dependencies
      expect(updatedPackageJSON("package-2").dependencies).toMatchObject({
        "package-1": "^2.0.0",
      });
      expect(updatedPackageJSON("package-3").dependencies).toMatchObject({
        "package-2": "^2.0.0",
      });
      expect(updatedPackageJSON("package-4").optionalDependencies).toMatchObject({
        "package-3": "^2.0.0",
      });
      expect(updatedPackageJSON("package-5").dependencies).toMatchObject({
        "package-4": "^2.0.0",
        "package-6": "^1.0.0", // FIXME: awkward... (_intentional_, for now)
      });
      // private packages do not need local version resolution
      expect(updatedPackageJSON("package-7").dependencies).toMatchObject({
        "package-1": "file:../package-1",
      });
    });

    it("falls back to existing relative version when it is not updated", async () => {
      const testDir = await initFixture("relative-independent");

      await lernaPublish(testDir)("--cd-version", "minor", "--yes");

      expect(updatedPackageVersions(testDir)).toMatchSnapshot();

      // package-4 was updated, but package-6 was not
      expect(updatedPackageJSON("package-5").dependencies).toMatchObject({
        "package-4": "^4.1.0",
        "package-6": "^6.0.0",
      });
    });

    it("respects --exact", async () => {
      const testDir = await initFixture("relative-independent");

      await lernaPublish(testDir)("--cd-version", "patch", "--yes", "--exact");

      // package-4 was updated, but package-6 was not
      expect(updatedPackageJSON("package-5").dependencies).toMatchObject({
        "package-4": "4.0.1",
        "package-6": "6.0.0",
      });
    });

    it("works around npm-incompatible link: specifiers", async () => {
      const testDir = await initFixture("yarn-link-spec");

      await lernaPublish(testDir)("--cd-version", "major", "--yes");

      expect(updatedPackageJSON("package-2").dependencies).toMatchObject({
        "package-1": "^2.0.0",
      });
    });
  });

  describe("in a cyclical repo", () => {
    it("should throw an error with --reject-cycles", async () => {
      expect.assertions(1);

      try {
        const testDir = await initFixture("toposort");

        await lernaPublish(testDir)("--reject-cycles");
      } catch (err) {
        expect(err.message).toMatch("Dependency cycles detected, you should fix these!");
      }
    });
  });
});


keep keep keep replace keep keep keep keep


keep keep keep keep replace keep keep keep


"use strict";

module.exports = envReplace;

// https://github.com/npm/npm/blob/latest/lib/config/core.js#L409-L423
function envReplace(str) {
  if (typeof str !== "string" || !str) {
    return str;
  }

  // Replace any ${ENV} values with the appropriate environment
  const regex = /(\\*)\$\{([^}]+)\}/g;

  return str.replace(regex, (orig, esc, name) => {
    // eslint-disable-next-line no-param-reassign
    esc = esc.length > 0 && esc.length % 2;

    if (esc) {
      return orig;
    }

    if (process.env[name] === undefined) {
      throw new Error(`Failed to replace env in config: ${orig}`);
    }

    return process.env[name];
  });
}


keep keep keep keep keep replace keep keep keep keep keep


"use strict";

const util = require("util");
const multimatch = require("multimatch");
const log = require("npmlog");

const ValidationError = require("@lerna/validation-error");

module.exports = filterPackages;

/**
 * Filters a given set of packages and returns all packages that match the scope glob
 * and do not match the ignore glob
 *
 * @param {!Array.<Package>} packagesToFilter The packages to filter
 * @param {Array.<String>} include A list of globs to match the package name against
 * @param {Array.<String>} exclude A list of globs to filter the package name against
 * @param {Boolean} showPrivate When false, filter out private packages
 * @return {Array.<Package>} The packages with a name matching the glob
 * @throws when a given glob would produce an empty list of packages
 */
function filterPackages(packagesToFilter, include = [], exclude = [], showPrivate) {
  const filtered = new Set(packagesToFilter);
  const patterns = [].concat(arrify(include), negate(exclude));

  if (showPrivate === false) {
    for (const pkg of filtered) {
      if (pkg.private) {
        filtered.delete(pkg);
      }
    }
  }

  if (patterns.length) {
    log.info("filter", patterns);

    if (!include.length) {
      // only excludes needs to select all items first
      // globstar is for matching scoped packages
      patterns.unshift("**");
    }

    const pnames = Array.from(filtered).map(pkg => pkg.name);
    const chosen = new Set(multimatch(pnames, patterns));

    for (const pkg of filtered) {
      if (!chosen.has(pkg.name)) {
        filtered.delete(pkg);
      }
    }

    if (!filtered.size) {
      throw new ValidationError("EFILTER", util.format("No packages remain after filtering", patterns));
    }
  }

  return Array.from(filtered);
}

function arrify(thing) {
  if (!thing) {
    return [];
  }

  if (!Array.isArray(thing)) {
    return [thing];
  }

  return thing;
}

function negate(patterns) {
  return arrify(patterns).map(pattern => `!${pattern}`);
}


"use strict";

const { EOL } = require("os");
const log = require("npmlog");
const path = require("path");
const slash = require("slash");
const tempWrite = require("temp-write");

const ChildProcessUtilities = require("@lerna/child-process");

function isDetachedHead(opts) {
  log.silly("isDetachedHead");

  const branchName = exports.getCurrentBranch(opts);
  const isDetached = branchName === "HEAD";
  log.verbose("isDetachedHead", isDetached);

  return isDetached;
}

function isInitialized(opts) {
  log.silly("isInitialized");
  let initialized;

  try {
    // we only want the return code, so ignore stdout/stderr
    ChildProcessUtilities.execSync(
      "git",
      ["rev-parse"],
      Object.assign({}, opts, {
        stdio: "ignore",
      })
    );
    initialized = true;
  } catch (err) {
    log.verbose("isInitialized", "swallowed error", err);
    initialized = false;
  }

  // this does not need to be verbose
  log.silly("isInitialized", initialized);
  return initialized;
}

function addFiles(files, opts) {
  log.silly("addFiles", files);
  const filePaths = files.map(file => slash(path.relative(opts.cwd, path.resolve(opts.cwd, file))));
  return ChildProcessUtilities.exec("git", ["add", "--", ...filePaths], opts);
}

function commit(message, opts) {
  log.silly("commit", message);
  const args = ["commit", "--no-verify"];

  if (message.indexOf(EOL) > -1) {
    // Use tempfile to allow multi\nline strings.
    args.push("-F", tempWrite.sync(message, "lerna-commit.txt"));
  } else {
    args.push("-m", message);
  }

  log.verbose("commit", args);
  return ChildProcessUtilities.exec("git", args, opts);
}

function addTag(tag, opts) {
  log.silly("addTag", tag);
  return ChildProcessUtilities.exec("git", ["tag", tag, "-m", tag], opts);
}

function hasTags(opts) {
  log.silly("hasTags");
  let result = false;

  try {
    result = !!ChildProcessUtilities.execSync("git", ["tag"], opts);
  } catch (err) {
    log.warn("ENOTAGS", "No git tags were reachable from this branch!");
    log.verbose("hasTags error", err);
  }

  log.verbose("hasTags", result);

  return result;
}

function getLastTaggedCommit(opts) {
  log.silly("getLastTaggedCommit");

  const taggedCommit = ChildProcessUtilities.execSync("git", ["rev-list", "--tags", "--max-count=1"], opts);
  log.verbose("getLastTaggedCommit", taggedCommit);

  return taggedCommit;
}

function getFirstCommit(opts) {
  log.silly("getFirstCommit");

  const firstCommit = ChildProcessUtilities.execSync("git", ["rev-list", "--max-parents=0", "HEAD"], opts);
  log.verbose("getFirstCommit", firstCommit);

  return firstCommit;
}

function pushWithTags(remote, tags, opts) {
  log.silly("pushWithTags", [remote, tags]);

  return Promise.resolve(exports.getCurrentBranch(opts)).then(branch =>
    ChildProcessUtilities.exec("git", ["push", "--no-verify", remote, branch].concat(tags), opts)
  );
}

function getLastTag(opts) {
  log.silly("getLastTag");

  const lastTag = ChildProcessUtilities.execSync("git", ["describe", "--tags", "--abbrev=0"], opts);
  log.verbose("getLastTag", lastTag);

  return lastTag;
}

function diffSinceIn(committish, location, opts) {
  const args = ["diff", "--name-only", committish];
  const formattedLocation = slash(path.relative(opts.cwd, location));

  if (formattedLocation) {
    // avoid same-directory path.relative() === ""
    args.push("--", formattedLocation);
  }

  log.silly("diffSinceIn", committish, formattedLocation);

  const diff = ChildProcessUtilities.execSync("git", args, opts);
  log.silly("diff", diff);

  return diff;
}

function getWorkspaceRoot(opts) {
  log.silly("getWorkspaceRoot");

  const workRoot = ChildProcessUtilities.execSync("git", ["rev-parse", "--show-toplevel"], opts);
  log.verbose("getWorkspaceRoot", workRoot);

  return workRoot;
}

function getCurrentBranch(opts) {
  log.silly("getCurrentBranch");

  const currentBranch = ChildProcessUtilities.execSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], opts);
  log.verbose("getCurrentBranch", currentBranch);

  return currentBranch;
}

function getCurrentSHA(opts) {
  log.silly("getCurrentSHA");

  const sha = ChildProcessUtilities.execSync("git", ["rev-parse", "HEAD"], opts);
  log.verbose("getCurrentSHA", sha);

  return sha;
}

function getShortSHA(opts) {
  log.silly("getShortSHA");

  const sha = ChildProcessUtilities.execSync("git", ["rev-parse", "--short", "HEAD"], opts);
  log.verbose("getShortSHA", sha);

  return sha;
}

function checkoutChanges(fileGlob, opts) {
  log.silly("checkoutChanges", fileGlob);
  return ChildProcessUtilities.exec("git", ["checkout", "--", fileGlob], opts);
}

function init(opts) {
  log.silly("git init");
  ChildProcessUtilities.execSync("git", ["init"], opts);
}

function hasCommit(opts) {
  log.silly("hasCommit");
  let retVal;

  try {
    ChildProcessUtilities.execSync("git", ["log"], opts);
    retVal = true;
  } catch (e) {
    retVal = false;
  }

  log.verbose("hasCommit", retVal);
  return retVal;
}

function isBehindUpstream(gitRemote, opts) {
  log.silly("isBehindUpstream");

  // git fetch, but for everything
  ChildProcessUtilities.execSync("git", ["remote", "update"], opts);

  const branch = exports.getCurrentBranch(opts);
  const countLeftRight = ChildProcessUtilities.execSync(
    "git",
    ["rev-list", "--left-right", "--count", `${gitRemote}/${branch}...${branch}`],
    opts
  );
  const [behind, ahead] = countLeftRight.split("\t").map(val => parseInt(val, 10));

  log.silly(
    "isBehindUpstream",
    `${branch} is behind ${gitRemote}/${branch} by ${behind} commit(s) and ahead by ${ahead}`
  );

  return Boolean(behind);
}

exports.isDetachedHead = isDetachedHead;
exports.isInitialized = isInitialized;
exports.addFiles = addFiles;
exports.commit = commit;
exports.addTag = addTag;
exports.hasTags = hasTags;
exports.getLastTaggedCommit = getLastTaggedCommit;
exports.getFirstCommit = getFirstCommit;
exports.pushWithTags = pushWithTags;
exports.getLastTag = getLastTag;
exports.diffSinceIn = diffSinceIn;
exports.getWorkspaceRoot = getWorkspaceRoot;
exports.getCurrentBranch = getCurrentBranch;
exports.getCurrentSHA = getCurrentSHA;
exports.getShortSHA = getShortSHA;
exports.checkoutChanges = checkoutChanges;
exports.init = init;
exports.hasCommit = hasCommit;
exports.isBehindUpstream = isBehindUpstream;


# `@lerna/link`

> description TODO

## Usage

TODO


const testDir = await initFixture("normal");


relative-independent

file: specifiers should act just like recognized semver ranges


return GitUtilities.pushWithTags(this.gitRemote, this.tags, this.execOpts);

// resolve relative file: links to their actual version range

"use strict";

const execa = require("execa");
const fs = require("fs-extra");
const { EOL } = require("os");
const path = require("path");
const slash = require("slash");
const tempy = require("tempy");

// helpers
const initFixture = require("@lerna-test/init-fixture")(__dirname);
const cloneFixture = require("@lerna-test/clone-fixture")(__dirname);

// file under test
const GitUtilities = require("..");

describe("GitUtilities", () => {
  describe(".isDetachedHead()", () => {
    it("returns true when branchName is HEAD", async () => {
      const cwd = await initFixture("basic");

      expect(GitUtilities.isDetachedHead({ cwd })).toBe(false);

      const sha = await execa.stdout("git", ["rev-parse", "HEAD"], { cwd });
      await execa("git", ["checkout", sha], { cwd }); // detach head

      expect(GitUtilities.isDetachedHead({ cwd })).toBe(true);
    });
  });

  describe(".isInitialized()", () => {
    it("returns true when git command succeeds", async () => {
      const cwd = await initFixture("basic");

      expect(GitUtilities.isInitialized({ cwd })).toBe(true);

      await fs.remove(path.join(cwd, ".git"));
      expect(GitUtilities.isInitialized({ cwd })).toBe(false);
    });
  });

  describe(".addFiles()", () => {
    it("calls git add with files argument", async () => {
      const cwd = await initFixture("basic");
      const file = path.join("packages", "pkg-1", "index.js");

      await fs.outputFile(path.join(cwd, file), "hello");
      await GitUtilities.addFiles([file], { cwd });

      const list = await execa.stdout("git", ["diff", "--cached", "--name-only"], { cwd });
      expect(slash(list)).toBe("packages/pkg-1/index.js");
    });

    it("works with absolute path for files", async () => {
      const cwd = await initFixture("basic");
      const file = path.join(cwd, "packages", "pkg-2", "index.js");

      await fs.outputFile(file, "hello");
      await GitUtilities.addFiles([file], { cwd });

      const list = await execa.stdout("git", ["diff", "--cached", "--name-only"], { cwd });
      expect(slash(list)).toBe("packages/pkg-2/index.js");
    });
  });

  describe(".commit()", () => {
    it("calls git commit with message", async () => {
      const cwd = await initFixture("basic");

      await fs.outputFile(path.join(cwd, "packages", "pkg-3", "index.js"), "hello");
      await execa("git", ["add", "."], { cwd });
      await GitUtilities.commit("foo", { cwd });

      const message = await execa.stdout("git", ["log", "-1", "--pretty=format:%B"], { cwd });
      expect(message).toBe("foo");
    });

    it("allows multiline message", async () => {
      const cwd = await initFixture("basic");

      await fs.outputFile(path.join(cwd, "packages", "pkg-4", "index.js"), "hello");
      await execa("git", ["add", "."], { cwd });
      await GitUtilities.commit(`foo${EOL}${EOL}bar`, { cwd });

      const subject = await execa.stdout("git", ["log", "-1", "--pretty=format:%s"], { cwd });
      const body = await execa.stdout("git", ["log", "-1", "--pretty=format:%b"], { cwd });

      expect(subject).toBe("foo");
      expect(body).toBe("bar");
    });
  });

  describe(".addTag()", () => {
    it("creates annotated git tag", async () => {
      const cwd = await initFixture("basic");

      await GitUtilities.addTag("v1.2.3", { cwd });

      const list = await execa.stdout("git", ["tag", "--list"], { cwd });
      expect(list).toMatch("v1.2.3");
    });
  });

  describe(".hasTags()", () => {
    it("returns true when one or more git tags exist", async () => {
      const cwd = await initFixture("basic");

      expect(GitUtilities.hasTags({ cwd })).toBe(false);

      await execa("git", ["tag", "v1.2.3", "-m", "v1.2.3"], { cwd });
      expect(GitUtilities.hasTags({ cwd })).toBe(true);
    });
  });

  describe(".getLastTaggedCommit()", () => {
    it("returns SHA of closest git tag", async () => {
      const cwd = await initFixture("basic");

      await execa("git", ["tag", "v1.2.3", "-m", "v1.2.3"], { cwd });

      expect(GitUtilities.getLastTaggedCommit({ cwd })).toMatch(/^[0-9a-f]{40}$/);
    });
  });

  describe(".getFirstCommit()", () => {
    it("returns SHA of first commit", async () => {
      const cwd = await initFixture("basic");

      expect(GitUtilities.getFirstCommit({ cwd })).toMatch(/^[0-9a-f]{40}$/);
    });
  });

  describe(".pushWithTags()", () => {
    it("pushes current branch and specified tag(s) to origin", async () => {
      const { cwd } = await cloneFixture("basic");

      await execa("git", ["commit", "--allow-empty", "-m", "change"], { cwd });
      await execa("git", ["tag", "v1.2.3", "-m", "v1.2.3"], { cwd });

      await GitUtilities.pushWithTags("origin", ["v1.2.3"], { cwd });

      const list = await execa.stdout("git", ["ls-remote", "--tags", "--refs", "--quiet"], { cwd });
      expect(list).toMatch("v1.2.3");
    });
  });

  describe(".getLastTag()", () => {
    it("returns the closest tag", async () => {
      const cwd = await initFixture("basic");

      await execa("git", ["tag", "v1.2.3", "-m", "v1.2.3"], { cwd });

      expect(GitUtilities.getLastTag({ cwd })).toBe("v1.2.3");
    });
  });

  describe(".diffSinceIn()", () => {
    it("omits location filter when location is current working directory", async () => {
      const cwd = await initFixture("basic");
      const file = path.join(cwd, "packages", "pkg-5", "index.js");

      await execa("git", ["tag", "v1.2.3", "-m", "v1.2.3"], { cwd });

      await fs.outputFile(file, "hello");
      await fs.writeJSON(path.join(cwd, "package.json"), { foo: "bar" });

      await execa("git", ["add", "."], { cwd });
      await execa("git", ["commit", "-m", "change"], { cwd });

      const rootDiff = GitUtilities.diffSinceIn("v1.2.3", cwd, { cwd });
      const fileDiff = GitUtilities.diffSinceIn("v1.2.3", path.dirname(file), { cwd });

      expect(rootDiff).toMatch("package.json");
      expect(fileDiff).toBe("packages/pkg-5/index.js");
    });
  });

  describe(".getWorkspaceRoot()", () => {
    it("calls `git rev-parse --show-toplevel`", async () => {
      const topLevel = await initFixture("basic");
      const cwd = path.join(topLevel, "foo");

      await fs.mkdirp(cwd);
      expect(GitUtilities.getWorkspaceRoot({ cwd })).toBe(slash(topLevel));
    });
  });

  describe(".getCurrentBranch()", () => {
    it("calls `git rev-parse --abbrev-ref HEAD`", async () => {
      const cwd = await initFixture("basic");

      expect(GitUtilities.getCurrentBranch({ cwd })).toBe("master");
    });
  });

  describe(".getCurrentSHA()", () => {
    it("returns full SHA of current ref", async () => {
      const cwd = await initFixture("basic");

      expect(GitUtilities.getCurrentSHA({ cwd })).toMatch(/^[0-9a-f]{40}$/);
    });
  });

  describe(".getShortSHA()", () => {
    it("returns short SHA of current ref", async () => {
      const cwd = await initFixture("basic");

      expect(GitUtilities.getShortSHA({ cwd })).toMatch(/^[0-9a-f]{7,8}$/);
    });
  });

  describe(".checkoutChanges()", () => {
    it("calls git checkout with specified arg", async () => {
      const cwd = await initFixture("basic");

      await fs.writeJSON(path.join(cwd, "package.json"), { foo: "bar" });
      await GitUtilities.checkoutChanges("package.json", { cwd });

      const modified = await execa.stdout("git", ["ls-files", "--modified"], { cwd });
      expect(modified).toBe("");
    });
  });

  describe(".init()", () => {
    it("calls git init", async () => {
      const cwd = tempy.directory();

      GitUtilities.init({ cwd });

      const { code } = await execa("git", ["status"], { cwd });
      expect(code).toBe(0);
    });
  });

  describe(".hasCommit()", () => {
    it("returns true when git command succeeds", async () => {
      const cwd = await initFixture("basic");

      expect(GitUtilities.hasCommit({ cwd })).toBe(true);

      await fs.remove(path.join(cwd, ".git"));
      expect(GitUtilities.hasCommit({ cwd })).toBe(false);
    });
  });

  describe(".isBehindUpstream()", () => {
    it("returns true when behind upstream", async () => {
      const { cwd } = await cloneFixture("basic");

      expect(GitUtilities.isBehindUpstream("origin", { cwd })).toBe(false);

      await execa("git", ["commit", "--allow-empty", "-m", "change"], { cwd });
      await execa("git", ["push", "origin", "master"], { cwd });
      await execa("git", ["reset", "--hard", "HEAD^"], { cwd });

      expect(GitUtilities.isBehindUpstream("origin", { cwd })).toBe(true);
    });
  });
});


// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`CreateCommand creates a stub cli 1`] = `
diff --git a/packages/my-cli/README.md b/packages/my-cli/README.md
new file mode 100644
index SHA..SHA
--- /dev/null
+++ b/packages/my-cli/README.md
@@ -0,0 +1,13 @@
+# \`my-cli\`
+
+> TODO: description
+
+## Usage
+
+\`\`\`
+npm -g i my-cli
+
+my-cli --help
+
+// TODO: DEMONSTRATE API
+\`\`\`
diff --git a/packages/my-cli/__tests__/cli.test.js b/packages/my-cli/__tests__/cli.test.js
new file mode 100644
index SHA..SHA
--- /dev/null
+++ b/packages/my-cli/__tests__/cli.test.js
@@ -0,0 +1,8 @@
+'use strict';
+
+const cli = require('../lib/cli');
+
+describe('my-cli cli', () => {
+  // const argv = cli(cwd).parse(['args']);
+  it('needs tests');
+});
diff --git a/packages/my-cli/__tests__/my-cli.test.js b/packages/my-cli/__tests__/my-cli.test.js
new file mode 100644
index SHA..SHA
--- /dev/null
+++ b/packages/my-cli/__tests__/my-cli.test.js
@@ -0,0 +1,7 @@
+'use strict';
+
+const myCli = require('..');
+
+describe('my-cli', () => {
+    it('needs tests');
+});
diff --git a/packages/my-cli/bin/my-cli b/packages/my-cli/bin/my-cli
new file mode 100755
index SHA..SHA
--- /dev/null
+++ b/packages/my-cli/bin/my-cli
@@ -0,0 +1,6 @@
+#!/usr/bin/env node
+
+'use strict';
+
+// eslint-disable-next-line no-unused-expressions
+require('../lib/cli')().parse(process.argv.slice(2));
diff --git a/packages/my-cli/lib/cli.js b/packages/my-cli/lib/cli.js
new file mode 100644
index SHA..SHA
--- /dev/null
+++ b/packages/my-cli/lib/cli.js
@@ -0,0 +1,26 @@
+'use strict';
+
+const yargs = require('yargs/yargs');
+const myCli = require('./my-cli');
+
+module.exports = cli;
+
+function cli(cwd) {
+  const parser = factory(null, cwd);
+
+  parser.alias('h', 'help');
+  parser.alias('v', 'version');
+
+  parser.usage(
+    "$0",
+    "TODO: description",
+    yargs => {
+      yargs.options({
+        // TODO: options
+      });
+    },
+    argv => myCli(argv)
+  );
+
+  return parser;
+}
diff --git a/packages/my-cli/lib/my-cli.js b/packages/my-cli/lib/my-cli.js
new file mode 100644
index SHA..SHA
--- /dev/null
+++ b/packages/my-cli/lib/my-cli.js
@@ -0,0 +1,7 @@
+'use strict';
+
+module.exports = myCli;
+
+function myCli() {
+    // TODO
+}
diff --git a/packages/my-cli/package.json b/packages/my-cli/package.json
new file mode 100644
index SHA..SHA
--- /dev/null
+++ b/packages/my-cli/package.json
@@ -0,0 +1,34 @@
+{
+  "name": "my-cli",
+  "version": "1.2.3",
+  "description": "Now I’m the model of a modern major general / The venerated Virginian veteran whose men are all / Lining up, to put me up on a pedestal / Writin’ letters to relatives / Embellishin’ my elegance and eloquence / But the elephant is in the room / The truth is in ya face when ya hear the British cannons go / BOOM",
+  "keywords": [],
+  "author": "Tester McPerson <test@example.com> (http://example.com)",
+  "license": "ISC",
+  "main": "lib/my-cli.js",
+  "bin": {
+    "my-cli": "bin/my-cli"
+  },
+  "directories": {
+    "lib": "lib",
+    "test": "__tests__"
+  },
+  "files": [
+    "bin",
+    "lib"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/test/test.git"
+  },
+  "scripts": {
+    "test": "echo \\"Error: run tests from root\\" && exit 1"
+  },
+  "dependencies": {
+    "yargs": "^1.0.0-mocked"
+  },
+  "bugs": {
+    "url": "https://github.com/test/test/issues"
+  },
+  "homepage": "https://github.com/test/test#readme"
+}
`;

exports[`CreateCommand creates a stub cli with transpiled output 1`] = `
diff --git a/packages/my-es-cli/README.md b/packages/my-es-cli/README.md
new file mode 100644
index SHA..SHA
--- /dev/null
+++ b/packages/my-es-cli/README.md
@@ -0,0 +1,13 @@
+# \`my-es-cli\`
+
+> TODO: description
+
+## Usage
+
+\`\`\`
+npm -g i my-es-cli
+
+my-es-cli --help
+
+// TODO: DEMONSTRATE API
+\`\`\`
diff --git a/packages/my-es-cli/__tests__/cli.test.js b/packages/my-es-cli/__tests__/cli.test.js
new file mode 100644
index SHA..SHA
--- /dev/null
+++ b/packages/my-es-cli/__tests__/cli.test.js
@@ -0,0 +1,6 @@
+import cli from '../src/cli';
+
+describe('my-es-cli cli', () => {
+  // const argv = cli(cwd).parse(['args']);
+  it('needs tests');
+});
diff --git a/packages/my-es-cli/__tests__/my-es-cli.test.js b/packages/my-es-cli/__tests__/my-es-cli.test.js
new file mode 100644
index SHA..SHA
--- /dev/null
+++ b/packages/my-es-cli/__tests__/my-es-cli.test.js
@@ -0,0 +1,5 @@
+import myEsCli from '../src/my-es-cli';
+
+describe('my-es-cli', () => {
+    it('needs tests');
+});
diff --git a/packages/my-es-cli/bin/my-es-cli b/packages/my-es-cli/bin/my-es-cli
new file mode 100755
index SHA..SHA
--- /dev/null
+++ b/packages/my-es-cli/bin/my-es-cli
@@ -0,0 +1,6 @@
+#!/usr/bin/env node
+
+'use strict';
+
+// eslint-disable-next-line no-unused-expressions
+require('../dist/cli').default().parse(process.argv.slice(2));
diff --git a/packages/my-es-cli/package.json b/packages/my-es-cli/package.json
new file mode 100644
index SHA..SHA
--- /dev/null
+++ b/packages/my-es-cli/package.json
@@ -0,0 +1,35 @@
+{
+  "name": "my-es-cli",
+  "version": "1.2.3",
+  "description": "Now I’m the model of a modern major general / The venerated Virginian veteran whose men are all / Lining up, to put me up on a pedestal / Writin’ letters to relatives / Embellishin’ my elegance and eloquence / But the elephant is in the room / The truth is in ya face when ya hear the British cannons go / BOOM",
+  "keywords": [],
+  "author": "Tester McPerson <test@example.com> (http://example.com)",
+  "license": "ISC",
+  "main": "dist/my-es-cli.js",
+  "module": "dist/my-es-cli.module.js",
+  "bin": {
+    "my-es-cli": "bin/my-es-cli"
+  },
+  "directories": {
+    "lib": "dist",
+    "test": "__tests__"
+  },
+  "files": [
+    "bin",
+    "dist"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/test/test.git"
+  },
+  "scripts": {
+    "test": "echo \\"Error: run tests from root\\" && exit 1"
+  },
+  "dependencies": {
+    "yargs": "^1.0.0-mocked"
+  },
+  "bugs": {
+    "url": "https://github.com/test/test/issues"
+  },
+  "homepage": "https://github.com/test/test#readme"
+}
diff --git a/packages/my-es-cli/src/cli.js b/packages/my-es-cli/src/cli.js
new file mode 100644
index SHA..SHA
--- /dev/null
+++ b/packages/my-es-cli/src/cli.js
@@ -0,0 +1,24 @@
+import factory from 'yargs/yargs';
+import myEsCli from './my-es-cli';
+
+export default cli;
+
+function cli(cwd) {
+  const parser = factory(null, cwd);
+
+  parser.alias('h', 'help');
+  parser.alias('v', 'version');
+
+  parser.usage(
+    "$0",
+    "TODO: description",
+    yargs => {
+      yargs.options({
+        // TODO: options
+      });
+    },
+    argv => myEsCli(argv)
+  );
+
+  return parser;
+}
diff --git a/packages/my-es-cli/src/my-es-cli.js b/packages/my-es-cli/src/my-es-cli.js
new file mode 100644
index SHA..SHA
--- /dev/null
+++ b/packages/my-es-cli/src/my-es-cli.js
@@ -0,0 +1,3 @@
+export default function myEsCli() {
+    // TODO
+}
`;

exports[`CreateCommand creates a stub package 1`] = `
diff --git a/packages/my-pkg/README.md b/packages/my-pkg/README.md
new file mode 100644
index SHA..SHA
--- /dev/null
+++ b/packages/my-pkg/README.md
@@ -0,0 +1,11 @@
+# \`my-pkg\`
+
+> TODO: description
+
+## Usage
+
+\`\`\`
+const myPkg = require('my-pkg');
+
+// TODO: DEMONSTRATE API
+\`\`\`
diff --git a/packages/my-pkg/__tests__/my-pkg.test.js b/packages/my-pkg/__tests__/my-pkg.test.js
new file mode 100644
index SHA..SHA
--- /dev/null
+++ b/packages/my-pkg/__tests__/my-pkg.test.js
@@ -0,0 +1,7 @@
+'use strict';
+
+const myPkg = require('..');
+
+describe('my-pkg', () => {
+    it('needs tests');
+});
diff --git a/packages/my-pkg/lib/my-pkg.js b/packages/my-pkg/lib/my-pkg.js
new file mode 100644
index SHA..SHA
--- /dev/null
+++ b/packages/my-pkg/lib/my-pkg.js
@@ -0,0 +1,7 @@
+'use strict';
+
+module.exports = myPkg;
+
+function myPkg() {
+    // TODO
+}
diff --git a/packages/my-pkg/package.json b/packages/my-pkg/package.json
new file mode 100644
index SHA..SHA
--- /dev/null
+++ b/packages/my-pkg/package.json
@@ -0,0 +1,27 @@
+{
+  "name": "my-pkg",
+  "version": "1.2.3",
+  "description": "Now I’m the model of a modern major general / The venerated Virginian veteran whose men are all / Lining up, to put me up on a pedestal / Writin’ letters to relatives / Embellishin’ my elegance and eloquence / But the elephant is in the room / The truth is in ya face when ya hear the British cannons go / BOOM",
+  "keywords": [],
+  "author": "Tester McPerson <test@example.com> (http://example.com)",
+  "license": "ISC",
+  "main": "lib/my-pkg.js",
+  "directories": {
+    "lib": "lib",
+    "test": "__tests__"
+  },
+  "files": [
+    "lib"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/test/test.git"
+  },
+  "scripts": {
+    "test": "echo \\"Error: run tests from root\\" && exit 1"
+  },
+  "bugs": {
+    "url": "https://github.com/test/test/issues"
+  },
+  "homepage": "https://github.com/test/test#readme"
+}
`;

exports[`CreateCommand creates a stub package with transpiled output 1`] = `
diff --git a/packages/my-pkg/README.md b/packages/my-pkg/README.md
new file mode 100644
index SHA..SHA
--- /dev/null
+++ b/packages/my-pkg/README.md
@@ -0,0 +1,11 @@
+# \`my-pkg\`
+
+> TODO: description
+
+## Usage
+
+\`\`\`
+import myPkg from 'my-pkg';
+
+// TODO: DEMONSTRATE API
+\`\`\`
diff --git a/packages/my-pkg/__tests__/my-pkg.test.js b/packages/my-pkg/__tests__/my-pkg.test.js
new file mode 100644
index SHA..SHA
--- /dev/null
+++ b/packages/my-pkg/__tests__/my-pkg.test.js
@@ -0,0 +1,5 @@
+import myPkg from '../src/my-pkg';
+
+describe('my-pkg', () => {
+    it('needs tests');
+});
diff --git a/packages/my-pkg/package.json b/packages/my-pkg/package.json
new file mode 100644
index SHA..SHA
--- /dev/null
+++ b/packages/my-pkg/package.json
@@ -0,0 +1,28 @@
+{
+  "name": "my-pkg",
+  "version": "1.2.3",
+  "description": "Now I’m the model of a modern major general / The venerated Virginian veteran whose men are all / Lining up, to put me up on a pedestal / Writin’ letters to relatives / Embellishin’ my elegance and eloquence / But the elephant is in the room / The truth is in ya face when ya hear the British cannons go / BOOM",
+  "keywords": [],
+  "author": "Tester McPerson <test@example.com> (http://example.com)",
+  "license": "ISC",
+  "main": "dist/my-pkg.js",
+  "module": "dist/my-pkg.module.js",
+  "directories": {
+    "lib": "dist",
+    "test": "__tests__"
+  },
+  "files": [
+    "dist"
+  ],
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/test/test.git"
+  },
+  "scripts": {
+    "test": "echo \\"Error: run tests from root\\" && exit 1"
+  },
+  "bugs": {
+    "url": "https://github.com/test/test/issues"
+  },
+  "homepage": "https://github.com/test/test#readme"
+}
diff --git a/packages/my-pkg/src/my-pkg.js b/packages/my-pkg/src/my-pkg.js
new file mode 100644
index SHA..SHA
--- /dev/null
+++ b/packages/my-pkg/src/my-pkg.js
@@ -0,0 +1,3 @@
+export default function myPkg() {
+    // TODO
+}
`;


@lerna/collect-packages

TODO


const { cwd } = await cloneFixture("basic");

await execa("git", ["commit", "--allow-empty", "-m", "change"], { cwd });

await GitUtilities.pushWithTags("origin", ["v1.2.3"], { cwd });

const list = await execa.stdout("git", ["ls-remote", "--tags", "--refs", "--quiet"], { cwd });


// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`LsCommand filtering excludes packages when --ignore is a brace-expanded list 1`] = `
"package-a-1 v1.0.0
package-a-2 v1.0.0"
`;

exports[`LsCommand filtering excludes packages when --ignore is a glob 1`] = `
"package-a-1 v1.0.0
package-a-2 v1.0.0"
`;

exports[`LsCommand filtering excludes packages when --ignore is a package name 1`] = `
"package-4   v1.0.0
package-a-1 v1.0.0
package-a-2 v1.0.0"
`;

exports[`LsCommand filtering filters packages when both --scope and --ignore are passed 1`] = `"package-a-1 v1.0.0"`;

exports[`LsCommand filtering includes all packages when --scope is omitted 1`] = `
"package-3   v1.0.0
package-4   v1.0.0
package-a-1 v1.0.0
package-a-2 v1.0.0"
`;

exports[`LsCommand filtering includes packages when --scope is a glob 1`] = `
"package-a-1 v1.0.0
package-a-2 v1.0.0"
`;

exports[`LsCommand filtering includes packages when --scope is a package name 1`] = `"package-3 v1.0.0"`;

exports[`LsCommand in a Yarn workspace should use package.json/workspaces setting 1`] = `
"package-1 v1.0.0
package-2 v1.0.0"
`;

exports[`LsCommand in a basic repo does not list changes for ignored packages 1`] = `"package-1 v1.0.0"`;

exports[`LsCommand in a basic repo lists changes for a given scope 1`] = `"package-1 v1.0.0"`;

exports[`LsCommand in a basic repo should list packages 1`] = `
"package-1 v1.0.0
package-2 v1.0.0
package-3 v1.0.0
package-4 v1.0.0
package-5 v1.0.0 (private)"
`;

exports[`LsCommand in a repo with packages outside of packages/ should list packages 1`] = `
"package-1 v1.0.0
package-2 v1.0.0
package-3 v1.0.0"
`;

exports[`LsCommand with --include-filtered-dependencies should list packages, including filtered ones 1`] = `
"@test/package-2 v1.0.0
@test/package-1 v1.0.0"
`;

exports[`LsCommand with --json should list packages as json objects 1`] = `
Array [
  Object {
    "name": "package-1",
    "private": false,
    "version": "1.0.0",
  },
  Object {
    "name": "package-2",
    "private": false,
    "version": "1.0.0",
  },
  Object {
    "name": "package-3",
    "private": false,
    "version": "1.0.0",
  },
  Object {
    "name": "package-4",
    "private": false,
    "version": "1.0.0",
  },
  Object {
    "name": "package-5",
    "private": true,
    "version": "1.0.0",
  },
]
`;

exports[`LsCommand with an undefined version should list packages 1`] = `"package-1 MISSING"`;

exports[`LsCommand with fancy 'packages' configuration lists globstar-nested packages 1`] = `
"globstar  v1.0.0
package-2 v1.0.0
package-4 v1.0.0
package-1 v1.0.0
package-3 v1.0.0
package-5 v1.0.0"
`;

exports[`LsCommand with fancy 'packages' configuration lists packages under explicitly configured node_modules directories 1`] = `
"alle-pattern-root v1.0.0
package-1         v1.0.0
package-2         v1.0.0
package-3         v1.0.0
package-4         v1.0.0
@scoped/package-5 v1.0.0"
`;

exports[`LsCommand with terribly complicated dependency cycles should list all packages with no repeats 1`] = `
"package-1 v1.0.0
package-2 v1.0.0
package-3 v1.0.0
package-4 v1.0.0
package-5 v1.0.0
package-6 v1.0.0"
`;


"use strict";

const filterable = require("@lerna/filter-options");

/**
 * @see https://github.com/yargs/yargs/blob/master/docs/advanced.md#providing-a-command-module
 */
exports.command = "add <pkg> [globs..]";

exports.describe = "Add a dependency to matched packages";

exports.builder = yargs => {
  yargs
    .positional("pkg", {
      describe: "Package name to add as a dependency",
      type: "string",
    })
    .positional("globs", {
      describe: "Optional package directory globs to match",
      type: "array",
    })
    .options({
      dev: {
        group: "Command Options:",
        describe: "Save to devDependencies",
      },
    });

  return filterable(yargs);
};

exports.handler = function handler(argv) {
  return require(".")(argv);
};


# Change Log

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

# [3.0.0-beta.13](https://github.com/lerna/lerna/compare/v3.0.0-beta.12...v3.0.0-beta.13) (2018-03-31)

### Bug Fixes

* **child-process:** Do not merge lines of streaming stderr ([59dc2d4](https://github.com/lerna/lerna/commit/59dc2d4)), closes [#994](https://github.com/lerna/lerna/issues/994)

# [3.0.0-beta.12](https://github.com/lerna/lerna/compare/v3.0.0-beta.11...v3.0.0-beta.12) (2018-03-30)

### Features

* Add --no-prefix for streaming output ([#1352](https://github.com/lerna/lerna/issues/1352)) ([36c1fad](https://github.com/lerna/lerna/commit/36c1fad))

# [3.0.0-beta.11](https://github.com/lerna/lerna/compare/v3.0.0-beta.10...v3.0.0-beta.11) (2018-03-29)

### Features

* **child-process:** Allow exec() opts.stdio override ([fd84013](https://github.com/lerna/lerna/commit/fd84013))