-
Notifications
You must be signed in to change notification settings - Fork 0
/
git.util.ts
188 lines (175 loc) · 6.9 KB
/
git.util.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
import { exec, ExecOptions } from 'child_process';
import { promisify } from 'util';
import path from 'path';
import replaceInFile from 'replace-in-file';
const execAsync = promisify(exec);
/** The name for the multi-extension template remote as used in the git scripts */
export const MULTI_TEMPLATE_NAME = 'paranext-multi-extension-template';
/** The url for the multi-extension template remote as used in the git scripts */
export const MULTI_TEMPLATE_URL = 'https://github.com/paranext/paranext-multi-extension-template';
/** The branch to use in pulling changes from `paranext-multi-extension-template` */
export const MULTI_TEMPLATE_BRANCH = 'main';
/** The name for the single extension template remote as used in the git scripts */
export const SINGLE_TEMPLATE_NAME = 'paranext-extension-template';
/** The url for the single extension template remote as used in the git scripts */
export const SINGLE_TEMPLATE_URL = 'https://github.com/paranext/paranext-extension-template';
/** The branch to use in pulling changes from `paranext-extension-template` */
export const SINGLE_TEMPLATE_BRANCH = 'main';
// #region localization
/**
* Error strings to be checked for in git output for various reasons
*
* `{key}` is replaced where applicable with the equivalent value from `GIT_CONSTANTS[key]`
*/
const errorStringTemplates = {
multiRemoteExists: 'remote {MULTI_TEMPLATE_NAME} already exists',
singleRemoteExists: 'remote {SINGLE_TEMPLATE_NAME} already exists',
/** `{0} is the subtree name aka the OS-independent extension directory path */
subtreeNeverAdded: "can't squash-merge: '{0}' was never added.\n",
};
/** Object mapping const names for template replacing */
const GIT_CONSTANTS = Object.freeze({
MULTI_TEMPLATE_NAME,
MULTI_TEMPLATE_URL,
MULTI_TEMPLATE_BRANCH,
SINGLE_TEMPLATE_NAME,
SINGLE_TEMPLATE_URL,
SINGLE_TEMPLATE_BRANCH,
});
/**
* Formats a string, replacing `GIT_CONSTANTS` values in brackets like `{MULTI_TEMPLATE_NAME}` and
* such with their equivalent actual values
*
* @param str String to format
* @param args Rest args of strings to replace `{x}` with, where x is the arg index - 1
* @returns Formatted string
*/
function formatGitErrorTemplate(str: string): string {
return str.replace(/{([^}]+)}/g, function replaceTemplateNumber(match) {
const matchingGitConstant = GIT_CONSTANTS[match.slice(1, -1)];
return matchingGitConstant !== undefined ? matchingGitConstant : match;
});
}
/** Error strings to be checked for in git output for various reasons */
// We are just mapping an object with strings to an object with strings. wah
// eslint-disable-next-line no-type-assertion/no-type-assertion
export const ERROR_STRINGS = Object.fromEntries(
Object.entries(errorStringTemplates).map(([key, value]) => [key, formatGitErrorTemplate(value)]),
) as typeof errorStringTemplates;
// #endregion
/**
* Executes a git command from the repo root directory, logging both the command and the results.
*
* For some reason, git likes to use stderr to return things that are not errors, so we only throw
* if the command throws
*
* @param command
* @param options The options for the exec command. Add quiet to not log anything
*/
export async function execGitCommand(
command: string,
options: ExecOptions & { quiet?: boolean } = {},
): Promise<{ stdout: string; stderr: string }> {
const { quiet, ...execOptions } = options;
if (!quiet) console.log(`\n> ${command}`);
try {
const result = await execAsync(command, {
cwd: path.resolve(path.join(__dirname, '..')),
...execOptions,
});
if (!quiet && result.stdout) console.log(result.stdout);
if (!quiet && result.stderr) console.log(result.stderr);
return result;
} catch (e) {
throw new Error(
`code ${e.code}!${e.stderr ? `\n${e.stderr}` : ''}${e.stdout ? `\n${e.stdout}` : ''}`,
);
}
}
/**
* Check the repo for working changes
*
* @param quiet Whether to log an error if there are working changes
* @returns True if there were working changes, false otherwise
*/
export async function checkForWorkingChanges(quiet = false) {
// Check the git status to make sure there are no working changes
const status = await execGitCommand('git status --porcelain=v2', {
quiet: true,
});
if (status.stderr || status.stdout) {
if (!quiet)
console.error(
`Working changes detected! Please stash or commit your changes. git status output: ${JSON.stringify(
status,
)}`,
);
return true;
}
return false;
}
/**
* Fetch latest from SINGLE_TEMPLATE_REMOTE_NAME
*
* @returns True if successful, false otherwise
*/
export async function fetchFromSingleTemplate() {
// Fetch latest SINGLE_TEMPLATE_REMOTE_NAME branch
try {
await execGitCommand(`git fetch ${SINGLE_TEMPLATE_NAME} ${SINGLE_TEMPLATE_BRANCH}`);
} catch (e) {
console.error(`Error on git fetch on ${SINGLE_TEMPLATE_NAME}: ${e}`);
return false;
}
return true;
}
/**
* Format an extension folder to make the extension template folder work as a subfolder of this repo
*
* This function may be called many times for one extension folder, so make sure all operations work
* properly no matter how many times this function is called
*
* @param extensionFolderPath Path to the extension to format relative to root
*/
export async function formatExtensionFolder(extensionFolderPath: string) {
// Replace ../paranext-core with ../../../paranext-core to fix ts-config and package.json and such
const results = await replaceInFile({
files: `${extensionFolderPath}/**/*`,
ignore: [
'**/node_modules/**/*',
'**/temp-build/**/*',
'**/logs/**/*',
'**/*.log',
'**/.eslintcache',
'**/dist/**/*',
'**/release/**/*',
// With npm workspaces, child workspace package-lock.json files are not used. Let's not format
// them so they can stay the same as how they were in the template to avoid merge conflicts
'**/package-lock.json',
],
from: /([^/])\.\.\/paranext-core/g,
to: '$1../../../paranext-core',
countMatches: true,
allowEmptyPaths: true,
});
const replaceStats = results.reduce(
(replacements, replaceResult) => ({
totalReplacements: replacements.totalReplacements + (replaceResult.numReplacements ?? 0),
filesChanged: replaceResult.hasChanged
? [...replacements.filesChanged, replaceResult.file]
: [...replacements.filesChanged],
}),
// filesChanged starts as an empty array, so it can't tell it should contain strings without
// type assertion
// eslint-disable-next-line no-type-assertion/no-type-assertion
{ totalReplacements: 0, filesChanged: [] as string[] },
);
if (replaceStats.totalReplacements > 0)
console.log(
`Formatting ${extensionFolderPath}: Successfully updated relative path to paranext-core ${
replaceStats.totalReplacements
} times in ${replaceStats.filesChanged.length} files:\n\t${replaceStats.filesChanged.join(
'\n\t',
)}\n`,
);
}