plan method

Future<PublishPlan> plan({
  1. required Directory ticketDir,
  2. required List<Node> subs,
  3. required GgLog ggLog,
  4. bool continueRun = false,
  5. bool publishUnchanged = false,
  6. bool mergeOnly = false,
  7. bool ask = true,
  8. bool reconfigure = false,
  9. bool requireAnswers = true,
  10. String? defaultMergeMessage,
  11. PublishPlanWording wording = PublishPlanWording.publish,
})

Plans the run for the repositories subs of the ticket in ticketDir.

config is whatever configuration the files already supply; its answers are reused and never asked again. continueRun makes a repository marked published count as done, publishUnchanged releases every repository, mergeOnly asks for no version increment because a merge releases nothing. defaultMergeMessage (-m) seeds the merge-message prompt.

reconfigure makes the pass ask every question again, with what is recorded as the pre-selected default — the behavior of gg do review and gg do configure-publish, where re-running IS the way to correct an earlier answer. Without it only unanswered questions are asked, so a gg do publish after a review asks nothing again.

ask turns the questions off altogether — the pass then only decides which repositories publish. requireAnswers decides what happens when a repository needs an answer and stdin is no terminal: gg do publish fails (it cannot release without an increment), gg do review passes false and leaves the question to the publish — its own job, filtering the pull requests, needs no answer.

Implementation

Future<PublishPlan> plan({
  required Directory ticketDir,
  required List<Node> subs,
  required GgLog ggLog,
  bool continueRun = false,
  bool publishUnchanged = false,
  bool mergeOnly = false,
  bool ask = true,
  bool reconfigure = false,
  bool requireAnswers = true,
  String? defaultMergeMessage,
  PublishPlanWording wording = PublishPlanWording.publish,
}) async {
  final explicitMessage = defaultMergeMessage?.trim();
  final hasExplicitMessage =
      explicitMessage != null && explicitMessage.isNotEmpty;
  final ticketSeed = seedMessageFor(ticketDir: ticketDir);

  // The versions later repos are judged against. A skipped repo contributes
  // its unchanged manifest version, a publishing one the version its chosen
  // increment will produce — computed exactly like gg_one computes it, from
  // the version last seen on the registry.
  final refVersions = <String, String>{};
  final configs = <String, gg.RepoPublishConfig>{};
  final entries = <PublishPlanEntry>[];

  for (final repo in subs) {
    final repoDir = repo.directory;
    final repoName = path.basename(repoDir.path);
    final files = loadTicketRepoPublishFiles(
      repoDir: repoDir,
      ticketDir: ticketDir,
    );
    var repoConfig = files.config;
    configs[repoName] = repoConfig;

    final alreadyPublished =
        continueRun && (files.state.status == 'published');

    final bool doesPublish;
    final String reason;
    if (alreadyPublished) {
      doesPublish = false;
      reason = 'already ${wording.done}';
    } else if (publishUnchanged) {
      doesPublish = true;
      reason = '--publish-unchanged';
    } else if (repoHasPublishStepProgress(repoDir)) {
      doesPublish = true;
      reason = 'a previous run already started publishing it';
    } else {
      final decision = await _publishSkipCheck.get(
        repo: repo,
        refVersions: refVersions,
      );
      doesPublish = !decision.skip;
      reason = decision.reason;
    }

    Version? baseline;

    if (doesPublish) {
      // A reconfiguring pass asks EVERY time, with the recorded answers
      // pre-selected — that is what re-running `gg do review` or
      // `gg do configure-publish` means. Every other pass asks only what
      // the configuration leaves open, so a `gg do publish` right after a
      // review does not ask the very questions the review just answered.
      final answered = _configAnswers(repoConfig, mergeOnly);
      if ((reconfigure || !answered) &&
          _canAsk(
            repoName: repoName,
            wording: wording,
            ask: ask,
            must: requireAnswers,
            hasAnswers: answered,
          )) {
        ggLog('\n${cH1(repoName)}');
        // An explicit `-m` is an instruction for this run and beats what
        // an earlier one recorded; without it the recorded answer is the
        // default, and the ticket description the last resort.
        final asked = await configureRepo(
          repoDir: repoDir,
          seedMessage: hasExplicitMessage
              ? explicitMessage
              : repoConfig.mergeMessage ?? ticketSeed,
          existing: repoConfig,
          mergeOnly: mergeOnly,
        );
        repoConfig = asked.config;
        configs[repoName] = repoConfig;
        baseline = asked.baseline;
      }
    } else if (!alreadyPublished) {
      ggLog(
        [
          '\n${cH1(repoName)}',
          cDetail('✓ ${'${wording.skipPrefix} $reason'.trim()}'),
        ].join('\n'),
      );
    }

    final increment = repoConfig.versionIncrement?.name;
    entries.add(
      PublishPlanEntry(
        name: repoName,
        directory: repoDir,
        publishes: doesPublish,
        reason: reason,
        alreadyPublished: alreadyPublished,
        versionIncrement: increment,
        mergeMessage: repoConfig.mergeMessage,
        pullRequestBody: repoConfig.pullRequestBody,
      ),
    );

    // Predict what later repos will resolve against.
    final predicted = await _predictedVersion(
      repoDir: repoDir,
      doesPublish: doesPublish,
      mergeOnly: mergeOnly,
      increment: increment,
      channel: files.state.channel,
      baseline: baseline,
    );

    for (final name in (await publishedNames(repoDir, repoName)).values) {
      if (predicted != null && predicted.isNotEmpty) {
        refVersions[name] = predicted;
      } else {
        // Unknown beats a guess: a dependent then publishes rather than
        // resolving against a version that may never exist.
        refVersions.remove(name);
      }
    }
  }

  return PublishPlan(entries: entries, configs: configs);
}