1import { Command } from '@expo/commander';
2import chalk from 'chalk';
3import semver from 'semver';
4
5import { Changelog, MemChangelog, UNPUBLISHED_VERSION_NAME } from '../Changelogs';
6import Git from '../Git';
7import { Package, getListOfPackagesAsync } from '../Packages';
8import { filterAsync } from '../Utils';
9
10type CommandOptions = {
11  branch: string;
12};
13
14export default (program: Command) => {
15  program
16    .command('sync-sdk-branch-changelogs')
17    .alias('ssbc')
18    .description('Sync packages changelogs from sdk branch to current branch.')
19    .option('-b, --branch <name>', 'Source branch name')
20    .asyncAction(async (options: CommandOptions) => {
21      if (!options.branch) {
22        throw new Error(
23          'Missing branch name. Run with `--branch <name>` to specify source branch.'
24        );
25      }
26
27      console.log('\u203A Fetching git changes');
28      await Git.fetchAsync();
29      console.log('');
30
31      const packages = await getPackagesWithChangelogAsync();
32
33      for (const pkg of packages) {
34        let changed = false;
35        try {
36          changed = await syncChangelogAsync(pkg, options.branch);
37        } catch (e) {
38          const errorMessage = e.message ?? String(e);
39          console.error(`❌ ${chalk.red(pkg.packageName)}: ${errorMessage}`);
40        }
41        if (changed) {
42          console.log(`✅ ${pkg.packageName}`);
43        }
44      }
45    });
46};
47
48/**
49 * Gets packages with a changelog file.
50 */
51async function getPackagesWithChangelogAsync(): Promise<Package[]> {
52  const packages = await getListOfPackagesAsync();
53  return filterAsync(packages, (pkg) => pkg.hasChangelogAsync());
54}
55
56/**
57 * Syncs changelog of a package from `sourceBranch` to current branch
58 */
59async function syncChangelogAsync(pkg: Package, sourceBranch: string) {
60  const sourceChangelog = new MemChangelog(
61    await Git.readFileAsync(`origin/${sourceBranch}`, pkg.changelogPath)
62  );
63  const targetChangelog = new Changelog(pkg.changelogPath);
64
65  const sourceLastVersion = await sourceChangelog.getLastPublishedVersionAsync();
66  const targetLastVersion = await targetChangelog.getLastPublishedVersionAsync();
67  if (!sourceLastVersion || !targetLastVersion) {
68    throw new Error('Cannot determine latest published version');
69  }
70
71  if (semver.gt(targetLastVersion, sourceLastVersion)) {
72    throw new Error(
73      'Current version is newer than source branch and there might be some inconsistency in between, e.g. canary version published. Please update manually.'
74    );
75  } else if (semver.eq(sourceLastVersion, targetLastVersion)) {
76    return false;
77  }
78
79  const changes = await sourceChangelog.getChangesAsync(targetLastVersion, sourceLastVersion);
80
81  delete changes.versions[UNPUBLISHED_VERSION_NAME];
82  let updated = false;
83  for (const version of Object.keys(changes.versions).sort((v1, v2) =>
84    semver.gt(v1, v2) ? 1 : -1
85  )) {
86    const groupData = changes.versions[version];
87    if (Object.keys(groupData).length === 0) {
88      const result = await targetChangelog.insertEmptyPublishedVersionAsync(
89        version,
90        changes.versionDateMap[version]
91      );
92      updated ||= result;
93      continue;
94    }
95
96    for (const [changeType, entries] of Object.entries(groupData)) {
97      for (const entry of entries) {
98        const result = await targetChangelog.moveEntryBetweenVersionsAsync(
99          entry,
100          changeType,
101          UNPUBLISHED_VERSION_NAME,
102          version,
103          changes.versionDateMap[version]
104        );
105        updated ||= result;
106      }
107    }
108  }
109  if (updated) {
110    await targetChangelog.saveAsync();
111  }
112
113  return true;
114}
115