* Copyright (c) 2025 Huawei Technologies Co., Ltd.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { exec, execSync } from 'node:child_process';
import { glob } from 'glob';
import pathUtils from 'node:path';
if (require.main === module) {
(async () => {
try {
if (process.argv.length > 2 && process.argv[2] === '--check') {
await checkClangFormat();
} else {
await clangFormat();
}
} catch (error) {
console.error('Error:', error);
process.exit(1);
}
})();
}
async function checkClangFormat(): Promise<void> {
const clangFormatPath = await getClangFormatPath();
const changedFiles = await findChangedCppFiles();
if (changedFiles.size === 0) {
return;
}
const command = `"${clangFormatPath}" --dry-run --Werror --style=file:.clang-format ${stringifyFilePaths(changedFiles)}`;
execSync(command, { stdio: 'inherit' });
}
async function clangFormat(): Promise<void> {
const clangFormatPath = await getClangFormatPath();
const changedFiles = await findChangedCppFiles();
if (changedFiles.size === 0) {
return;
}
const command = `"${clangFormatPath}" -i ${stringifyFilePaths(changedFiles)} --style=file:.clang-format`;
execSync(command, { stdio: 'inherit' });
}
async function getClangFormatPath(): Promise<string> {
const sdkHome = process.env.DEVECO_SDK_HOME;
if (!sdkHome) {
console.error('DEVECO_SDK_HOME environment variable is not set');
process.exit(1);
}
const clangFormatPathCandidates = await glob(
`${pathUtils.join(sdkHome, '..')}/**/clang-format${process.platform === 'win32' ? '.exe' : ''}`,
{ nodir: true }
);
if (clangFormatPathCandidates.length === 0) {
console.error('clang-format not found in DEVECO_SDK_HOME');
process.exit(1);
}
return clangFormatPathCandidates[0];
}
export async function findChangedCppFiles(): Promise<Set<string>> {
const changedFiles = await findChangedFiles();
for (const path of changedFiles) {
if (
(!path.endsWith('.cpp') && !path.endsWith('.h')) ||
(path.includes('third-party') && !path.includes('third-party/jsvm')) ||
path.includes('generated') ||
path.includes('patches')
) {
changedFiles.delete(path);
}
}
return changedFiles;
}
export async function findChangedFiles(): Promise<Set<string>> {
const changeInfo = process.env.CHANGE_INFO;
if (changeInfo) {
const changedFileList = JSON.parse(changeInfo)['third_party/ohos_react_native']['changed_file_list'];
const renameFiles = changedFileList['rename']?.map((res: string[]) => res[1]) || [];
const changedFiles = [
...changedFileList['modified'] ?? [],
...renameFiles,
...changedFileList['added'] ?? []
];
console.log(`changedFiles: \n ${changedFiles}`);
return new Set(changedFiles);
}
console.log('process.env.CHANGE_INFO is undefined');
const currentBranch = execSync('git branch --show-current').toString().trim();
if (!currentBranch) {
throw Error(
'Can\'t get current branch nor changed files. Don\'t know what to format.'
);
}
try {
const upstreamDiffCmd = 'git diff --name-only --diff-filter=d @{u} HEAD';
return new Set(await execGitDiffCommand(upstreamDiffCmd));
} catch {
const revList = execSync(
`git rev-list --exclude ${currentBranch} --branches -1`
)
.toString()
.trim();
const diffFromBranchCmd = `git diff --name-only --diff-filter=d ${revList} HEAD`;
const diffUnstagedCmd = 'git diff --name-only --diff-filter=d';
const diffStagedCmd = 'git diff --name-only --diff-filter=d --cached';
const result = await Promise.all([
execGitDiffCommand(diffFromBranchCmd),
execGitDiffCommand(diffUnstagedCmd),
execGitDiffCommand(diffStagedCmd),
]);
return new Set(result.flat());
}
}
async function execGitDiffCommand(command: string): Promise<string[]> {
return new Promise((resolve, reject) => {
exec(command, (error, stdout) => {
if (error) {
reject(error);
return;
}
const files = stdout.trim().split('\n').filter(Boolean);
resolve(files);
});
});
}
function stringifyFilePaths(changedFiles: Set<string>): string {
return Array.from(changedFiles).join(' ');
}