1import crypto from 'crypto'; 2import fs from 'fs'; 3 4type MergeResults = { 5 contents: string; 6 didClear: boolean; 7 didMerge: boolean; 8}; 9/** 10 * Merge two gitignore files together and add a generated header. 11 * 12 * @param targetGitIgnorePath 13 * @param sourceGitIgnorePath 14 * 15 * @returns `null` if one of the gitignore files doesn't exist. Otherwise, returns the merged contents. 16 */ 17export function mergeGitIgnorePaths( 18 targetGitIgnorePath: string, 19 sourceGitIgnorePath: string 20): null | MergeResults { 21 if (!fs.existsSync(targetGitIgnorePath)) { 22 // No gitignore in the project already, no need to merge anything into anything. I guess they 23 // are not using git :O 24 return null; 25 } 26 27 if (!fs.existsSync(sourceGitIgnorePath)) { 28 // Maybe we don't have a gitignore in the template project 29 return null; 30 } 31 32 const targetGitIgnore = fs.readFileSync(targetGitIgnorePath).toString(); 33 const sourceGitIgnore = fs.readFileSync(sourceGitIgnorePath).toString(); 34 const merged = mergeGitIgnoreContents(targetGitIgnore, sourceGitIgnore); 35 // Only rewrite the file if it was modified. 36 if (merged.contents) { 37 fs.writeFileSync(targetGitIgnorePath, merged.contents); 38 } 39 40 return merged; 41} 42 43const generatedHeaderPrefix = `# @generated expo-cli`; 44export const generatedFooterComment = `# @end expo-cli`; 45 46/** 47 * Get line indexes for the generated section of a gitignore. 48 * 49 * @param gitIgnore 50 */ 51function getGeneratedSectionIndexes(gitIgnore: string): { 52 contents: string[]; 53 start: number; 54 end: number; 55} { 56 const contents = gitIgnore.split('\n'); 57 const start = contents.findIndex((line) => line.startsWith(generatedHeaderPrefix)); 58 const end = contents.findIndex((line) => line.startsWith(generatedFooterComment)); 59 60 return { contents, start, end }; 61} 62 63/** 64 * Removes the generated section from a gitignore, returns null when nothing can be removed. 65 * This sways heavily towards not removing lines unless it's certain that modifications were not made to the gitignore manually. 66 * 67 * @param gitIgnore 68 */ 69export function removeGeneratedGitIgnoreContents(gitIgnore: string): string | null { 70 const { contents, start, end } = getGeneratedSectionIndexes(gitIgnore); 71 if (start > -1 && end > -1 && start < end) { 72 contents.splice(start, end - start + 1); 73 // TODO: We could in theory check that the contents we're removing match the hash used in the header, 74 // this would ensure that we don't accidentally remove lines that someone added or removed from the generated section. 75 return contents.join('\n'); 76 } 77 return null; 78} 79 80/** 81 * Merge the contents of two gitignores together and add a generated header. 82 * 83 * @param targetGitIgnore contents of the existing gitignore 84 * @param sourceGitIgnore contents of the extra gitignore 85 */ 86export function mergeGitIgnoreContents( 87 targetGitIgnore: string, 88 sourceGitIgnore: string 89): MergeResults { 90 const header = createGeneratedHeaderComment(sourceGitIgnore); 91 if (!targetGitIgnore.includes(header)) { 92 // Ensure the old generated gitignore contents are removed. 93 const sanitizedTarget = removeGeneratedGitIgnoreContents(targetGitIgnore); 94 return { 95 contents: [ 96 sanitizedTarget ?? targetGitIgnore, 97 header, 98 `# The following patterns were generated by expo-cli`, 99 ``, 100 sourceGitIgnore, 101 generatedFooterComment, 102 ].join('\n'), 103 didMerge: true, 104 didClear: !!sanitizedTarget, 105 }; 106 } 107 return { contents: targetGitIgnore, didClear: false, didMerge: false }; 108} 109 110export function createGeneratedHeaderComment(gitIgnore: string): string { 111 const hashKey = createGitIgnoreHash(getSanitizedGitIgnoreLines(gitIgnore).join('\n')); 112 113 return `${generatedHeaderPrefix} ${hashKey}`; 114} 115 116/** 117 * Normalize the contents of a gitignore to ensure that minor changes like new lines or sort order don't cause a regeneration. 118 */ 119export function getSanitizedGitIgnoreLines(gitIgnore: string): string[] { 120 // filter, trim, and sort the lines. 121 return gitIgnore 122 .split('\n') 123 .filter((v) => { 124 const line = v.trim(); 125 // Strip comments 126 if (line.startsWith('#')) { 127 return false; 128 } 129 return !!line; 130 }) 131 .sort(); 132} 133 134export function createGitIgnoreHash(gitIgnore: string): string { 135 // this doesn't need to be secure, the shorter the better. 136 const hash = crypto.createHash('sha1').update(gitIgnore).digest('hex'); 137 return `sync-${hash}`; 138} 139