plan method
- 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,
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);
}