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  if (changes.totalCount < 1) {
82    return false;
83  }
84
85  delete changes.versions[UNPUBLISHED_VERSION_NAME];
86  let updated = false;
87  for (const version of Object.keys(changes.versions).sort((v1, v2) =>
88    semver.gt(v1, v2) ? 1 : -1
89  )) {
90    const groupData = changes.versions[version];
91    if (Object.keys(groupData).length === 0) {
92      const result = await targetChangelog.insertEmptyPublishedVersionAsync(
93        version,
94        changes.versionDateMap[version]
95      );
96      updated ||= result;
97      continue;
98    }
99
100    for (const [changeType, entries] of Object.entries(groupData)) {
101      for (const entry of entries) {
102        const result = await targetChangelog.moveEntryBetweenVersionsAsync(
103          entry,
104          changeType,
105          UNPUBLISHED_VERSION_NAME,
106          version,
107          changes.versionDateMap[version]
108        );
109        updated ||= result;
110      }
111    }
112  }
113  if (updated) {
114    await targetChangelog.saveAsync();
115  }
116
117  return true;
118}
119