diff --git a/package-lock.json b/package-lock.json index f49b726..c040240 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gitignore", - "version": "0.10.0-alpha.1", + "version": "0.11.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gitignore", - "version": "0.10.0-alpha.1", + "version": "0.11.0", "license": "MIT", "dependencies": { "https-proxy-agent": "^7.0.6" @@ -15,7 +15,7 @@ "@eslint/js": "^9.21.0", "@types/mocha": "^10.0.10", "@types/node": "^22.13.5", - "@types/vscode": ">=1.63.0 <1.64.0", + "@types/vscode": ">=1.66.0 <1.67.0", "@typescript-eslint/eslint-plugin": "^8.22.0", "@typescript-eslint/parser": "^8.22.0", "@vscode/test-cli": "^0.0.10", @@ -28,7 +28,7 @@ "typescript-eslint": "^8.25.0" }, "engines": { - "vscode": "^1.63.0" + "vscode": "^1.66.0" } }, "node_modules/@azure/abort-controller": { @@ -1007,9 +1007,9 @@ } }, "node_modules/@types/vscode": { - "version": "1.63.2", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.63.2.tgz", - "integrity": "sha512-awvdx4vX7SkMKyvWIlRjycjb4blYRSQI3Bav0YMn+lJLGN6gJgb20urN/dQCv/2ejDu5S6ADEBt6O15DOpIAkg==", + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.66.0.tgz", + "integrity": "sha512-ZfJck4M7nrGasfs4A4YbUoxis3Vu24cETw3DERsNYtDZmYSYtk6ljKexKFKhImO/ZmY6ZMsmegu2FPkXoUFImA==", "dev": true, "license": "MIT" }, diff --git a/package.json b/package.json index 79edd0d..55b3332 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "gitignore", "displayName": "gitignore", "description": "Lets you pull .gitignore templates from the https://github.com/github/gitignore repository. Language support for .gitignore files.", - "version": "0.10.0", + "version": "0.11.0", "author": "Marc-André Bühler", "publisher": "codezombiech", "icon": "icon.png", diff --git a/src/extension.ts b/src/extension.ts index 31518b1..6da7874 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; -import { join as joinPath } from 'path'; +import { dirname, join as joinPath } from 'path'; import { Cache } from './cache'; import { GitignoreTemplate, GitignoreOperation, GitignoreOperationType, GitignoreProvider } from './interfaces'; @@ -17,6 +17,27 @@ interface GitignoreQuickPickItem extends vscode.QuickPickItem { template: GitignoreTemplate; } +export interface GitignoreTemplateContent { + template: GitignoreTemplate; + content: string; +} + +export interface GitignoreFileState { + customContent: string; + templateIds: Set; +} + +const separator = '# ----------------------------------------------------------------------'; +const customRulesHeader = '# Custom ignore rules'; +const templatesHeader = '# GitHub gitignore templates'; +const templateHeaderPrefix = '# Template: '; +const templateSourcePrefix = '# Source: github/gitignore/'; +const templateStartPrefix = '# >>> vscode-gitignore template: '; +const templateSummaryPrefix = '# Templates: '; +const customRulesInstruction = '# Add project-specific ignore rules in this section. The extension preserves this content.'; +const legacyCustomRulesHeader = '# === vscode-gitignore custom rules ==='; +const legacyTemplatesHeader = '# === vscode-gitignore templates ==='; + // Initialize cache // The cache is the only instance shared across the whole lifetime of the extension @@ -50,7 +71,7 @@ function createNeverUsedCache() : Cache { * - using the single opened workspace * - prompting for the workspace to use when multiple workspaces are open */ -async function resolveWorkspaceFolder(gitIgnoreTemplate: GitignoreTemplate) { +async function resolveWorkspaceFolderPath() { const folders = vscode.workspace.workspaceFolders; // folders being falsy can have two reasons: // 1. no folder (workspace) open @@ -61,14 +82,14 @@ async function resolveWorkspaceFolder(gitIgnoreTemplate: GitignoreTemplate) { throw new CancellationError(); } else if (folders.length === 1) { - return { template: gitIgnoreTemplate, path: folders[0].uri.fsPath }; + return folders[0].uri.fsPath; } else { const folder = await vscode.window.showWorkspaceFolderPick(); if (!folder) { throw new CancellationError(); } - return { template: gitIgnoreTemplate, path: folder.uri.fsPath }; + return folder.uri.fsPath; } } @@ -84,41 +105,18 @@ function checkIfFileExists(path: string) { }); } -async function checkExistenceAndPromptForOperation(path: string, template: GitignoreTemplate): Promise { - path = joinPath(path, '.gitignore'); - - const exists = await checkIfFileExists(path); - if (!exists) { - // File does not exists -> we are fine to create it - return { path, template, type: GitignoreOperationType.Overwrite }; - } - - const operation = await promptForOperation(); - if (!operation) { - throw new CancellationError(); - } - const typedString = operation.label; - const type = GitignoreOperationType[typedString]; - - return { path, template, type }; -} - export async function downloadGitignoreFile(gitignoreRepository: GitignoreProvider, operation: GitignoreOperation) { - const flags = operation.type === GitignoreOperationType.Overwrite ? 'w' : 'a'; - const fileStream = fs.createWriteStream(operation.path, { flags: flags }); - - // If appending to the existing .gitignore file, write a NEWLINE as separator - if(flags === 'a') { - fileStream.write('\n'); - } + const existingContent = await readFileIfExists(operation.path); + const templateContents = await downloadTemplateContents(gitignoreRepository, operation.templates, dirname(operation.path)); + const state = getGitignoreFileState(existingContent, templateContents); + const content = renderGitignoreFile(state.customContent, templateContents); try { - // Store the file on file system - await gitignoreRepository.downloadToStream(operation.template.path, fileStream); + await fs.promises.writeFile(operation.path, content, {encoding: 'utf8'}); } catch(error) { // Delete the .gitignore file if we created it - if(flags === 'w') { + if(operation.type === GitignoreOperationType.Overwrite) { fs.unlink(operation.path, err => { if(err) { console.error(`vscode-gitignore: ${err.message}`); @@ -129,25 +127,203 @@ export async function downloadGitignoreFile(gitignoreRepository: GitignoreProvid } } -function promptForOperation() { - return vscode.window.showQuickPick([ - { - label: 'Append', - description: 'Append to existing .gitignore file' - }, - { - label: 'Overwrite', - description: 'Overwrite existing .gitignore file' +async function readFileIfExists(path: string): Promise { + if (!await checkIfFileExists(path)) { + return ''; + } + return fs.promises.readFile(path, {encoding: 'utf8'}); +} + +function normalizeLineEndings(content: string) { + return content.replace(/\r\n?/g, '\n'); +} + +async function downloadTemplateContent(gitignoreRepository: GitignoreProvider, template: GitignoreTemplate, directory: string): Promise { + const tempFileName = `.gitignore-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`; + const tempPath = joinPath(directory, tempFileName); + const fileStream = fs.createWriteStream(tempPath, { flags: 'w' }); + + try { + await gitignoreRepository.downloadToStream(template.path, fileStream); + const content = await fs.promises.readFile(tempPath, {encoding: 'utf8'}); + return { template, content: normalizeLineEndings(content).trimEnd() }; + } + finally { + await fs.promises.rm(tempPath, { force: true }); + } +} + +async function downloadTemplateContents(gitignoreRepository: GitignoreProvider, templates: GitignoreTemplate[], directory: string): Promise { + const contents: GitignoreTemplateContent[] = []; + const queue = sortTemplates(templates); + const workerCount = Math.min(6, queue.length); + const workers = Array.from({length: workerCount}, async () => { + let template = queue.shift(); + while (template) { + contents.push(await downloadTemplateContent(gitignoreRepository, template, directory)); + template = queue.shift(); } - ]); + }); + + await Promise.all(workers); + return contents.sort((a, b) => compareTemplates(a.template, b.template)); +} + +function compareTemplates(a: GitignoreTemplate, b: GitignoreTemplate) { + const nameComparison = a.name.localeCompare(b.name); + if (nameComparison !== 0) { + return nameComparison; + } + return a.path.localeCompare(b.path); +} + +function sortTemplates(templates: T[]): T[] { + return [...templates].sort(compareTemplates); +} + +function getTemplateId(template: GitignoreTemplate) { + return template.path || template.name; +} + +function getTemplateMarkerNames(content: string) { + const names = new Set(); + for (const line of normalizeLineEndings(content).split('\n')) { + if (line.startsWith(templateStartPrefix)) { + names.add(line.slice(templateStartPrefix.length).trim()); + } + } + return names; +} + +function getTemplateMarkerIds(content: string) { + const ids = new Set(); + for (const line of normalizeLineEndings(content).split('\n')) { + if (line.startsWith(templateSourcePrefix)) { + ids.add(line.slice(templateSourcePrefix.length).trim()); + } + } + return ids; +} + +function stripLegacyTemplateContent(customContent: string, templateContents: GitignoreTemplateContent[]) { + let nextContent = normalizeLineEndings(customContent); + for (const templateContent of templateContents) { + const content = templateContent.content.trim(); + if (content.length > 0) { + nextContent = nextContent.replace(content, ''); + } + } + return nextContent.replace(/\n{3,}/g, '\n\n').trim(); +} + +function getManagedCustomContent(content: string) { + const normalizedContent = normalizeLineEndings(content); + const headerPair = [ + { custom: customRulesHeader, templates: templatesHeader }, + { custom: legacyCustomRulesHeader, templates: legacyTemplatesHeader } + ].find(headers => { + const customStart = normalizedContent.indexOf(headers.custom); + const templateStart = normalizedContent.indexOf(headers.templates); + return customStart !== -1 && templateStart !== -1 && templateStart > customStart; + }); + + if (!headerPair) { + return undefined; + } + + const customStart = normalizedContent.indexOf(headerPair.custom); + const templateStart = normalizedContent.indexOf(headerPair.templates); + + return normalizedContent + .slice(customStart + headerPair.custom.length, templateStart) + .replace(/^\n/, '') + .replace(new RegExp(`^${separator}\\n?`), '') + .replace(customRulesInstruction, '') + .trim(); +} + +export function getGitignoreFileState(content: string, templateContents: GitignoreTemplateContent[]): GitignoreFileState { + const normalizedContent = normalizeLineEndings(content); + const managedCustomContent = getManagedCustomContent(normalizedContent); + const legacyTemplateNames = getTemplateMarkerNames(normalizedContent); + const templateIds = getTemplateMarkerIds(normalizedContent); + for (const templateName of legacyTemplateNames) { + templateIds.add(templateName); + } + const detectedTemplateContents = templateContents.filter(templateContent => { + const templateContentText = templateContent.content.trim(); + return templateIds.has(getTemplateId(templateContent.template)) || + legacyTemplateNames.has(templateContent.template.name) || + (templateContentText.length > 0 && normalizedContent.includes(templateContentText)); + }); + + for (const templateContent of detectedTemplateContents) { + templateIds.add(getTemplateId(templateContent.template)); + } + + const customContent = managedCustomContent ?? stripLegacyTemplateContent(normalizedContent, detectedTemplateContents); + return { customContent, templateIds }; +} + +export function renderGitignoreFile(customContent: string, templateContents: GitignoreTemplateContent[]) { + const custom = customContent.trim(); + const sortedTemplateContents = [...templateContents].sort((a, b) => compareTemplates(a.template, b.template)); + const templateNames = sortedTemplateContents.map(templateContent => templateContent.template.name).join(', ') || 'none'; + const parts = [ + separator, + customRulesHeader, + separator, + customRulesInstruction, + custom, + '', + separator, + templatesHeader, + `${templateSummaryPrefix}${templateNames}`, + separator + ].filter((part, index) => index !== 4 || part.length > 0); + + for (const templateContent of sortedTemplateContents) { + parts.push( + '', + separator, + `${templateHeaderPrefix}${templateContent.template.name}`, + `${templateSourcePrefix}${getTemplateId(templateContent.template)}`, + separator, + templateContent.content.trim() + ); + } + + return `${parts.join('\n')}\n`; +} + +async function promptForTemplates(items: GitignoreQuickPickItem[], selectedTemplateIds: Set): Promise { + return new Promise((resolve, reject) => { + const quickPick = vscode.window.createQuickPick(); + quickPick.items = items; + quickPick.canSelectMany = true; + quickPick.matchOnDescription = true; + quickPick.placeholder = 'Select .gitignore templates'; + quickPick.selectedItems = items.filter(item => selectedTemplateIds.has(getTemplateId(item.template)) || selectedTemplateIds.has(item.template.name)); + + quickPick.onDidAccept(() => { + const selectedItems = [...quickPick.selectedItems]; + quickPick.hide(); + resolve(selectedItems.map(item => item.template)); + }); + quickPick.onDidHide(() => { + quickPick.dispose(); + reject(new CancellationError()); + }); + quickPick.show(); + }); } function showSuccessMessage(operation: GitignoreOperation) { switch (operation.type) { - case GitignoreOperationType.Append: - return vscode.window.showInformationMessage(`Appended ${operation.template.path} to the existing .gitignore in the project root`); + case GitignoreOperationType.Update: + return vscode.window.showInformationMessage('Updated the existing .gitignore in the project root'); case GitignoreOperationType.Overwrite: - return vscode.window.showInformationMessage(`Created .gitignore file in the project root based on ${operation.template.path}`); + return vscode.window.showInformationMessage('Created .gitignore file in the project root'); default: throw new Error('Unsupported operation'); } @@ -174,27 +350,37 @@ export function activate(context: vscode.ExtensionContext) { // Load templates const templates = await gitignoreRepository.getTemplates(); + // Resolve the path to the folder where we should write the gitignore file + const workspacePath = await resolveWorkspaceFolderPath(); + const gitignorePath = joinPath(workspacePath, '.gitignore'); + const exists = await checkIfFileExists(gitignorePath); + const existingContent = await readFileIfExists(gitignorePath); + const existingTemplateContents = exists && getTemplateMarkerNames(existingContent).size < 1 && getTemplateMarkerIds(existingContent).size < 1 + ? await downloadTemplateContents(gitignoreRepository, templates, workspacePath) + : []; + const existingState = getGitignoreFileState(existingContent, existingTemplateContents); + // Let the user pick a gitignore file - const items = templates.map(t => { + const items = sortTemplates(templates).map(t => { label: t.name, description: t.path, url: t.download_url, template: t }); - // TODO: use thenable for items - const selectedItem = await vscode.window.showQuickPick(items); + const selectedTemplates = await promptForTemplates(items, existingState.templateIds); // Check if the user picked up a gitignore file fetched from Github - if (!selectedItem) { + if (selectedTemplates.length < 1) { throw new CancellationError(); } - // Resolve the path to the folder where we should write the gitignore file - const { template, path } = await resolveWorkspaceFolder(selectedItem.template); - // Calculate operation - console.log(`vscode-gitignore: add/append gitignore for directory: ${path}`); - const operation = await checkExistenceAndPromptForOperation(path, template); + console.log(`vscode-gitignore: update gitignore for directory: ${workspacePath}`); + const operation = { + path: gitignorePath, + templates: sortTemplates(selectedTemplates), + type: exists ? GitignoreOperationType.Update : GitignoreOperationType.Overwrite + }; // Store the file on file system await downloadGitignoreFile(gitignoreRepository, operation); diff --git a/src/interfaces.ts b/src/interfaces.ts index 7202d22..13f4c21 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -6,6 +6,7 @@ export interface GitignoreTemplate { path: string; download_url: string; type: string; + sha?: string; } export interface GitignoreProvider { @@ -14,7 +15,7 @@ export interface GitignoreProvider { } export enum GitignoreOperationType { - Append, + Update, Overwrite } @@ -25,7 +26,7 @@ export interface GitignoreOperation { */ path: string; /** - * gitignore template file to use + * gitignore template files to use */ - template: GitignoreTemplate; + templates: GitignoreTemplate[]; } diff --git a/src/providers/github-gitignore-repository.ts b/src/providers/github-gitignore-repository.ts index 118d22a..31c1aa5 100644 --- a/src/providers/github-gitignore-repository.ts +++ b/src/providers/github-gitignore-repository.ts @@ -14,6 +14,7 @@ interface GithubRepositoryItem { path: string; download_url: string; type: string; + sha: string; } /** @@ -22,6 +23,7 @@ interface GithubRepositoryItem { */ export class GithubGitignoreRepositoryProvider implements GitignoreProvider { private client: GitHubClient; + private templateShaByPath = new Map(); constructor(private cache: Cache, githubSession: GithubSession) { this.client = new GitHubClient(githubSession); @@ -34,7 +36,8 @@ export class GithubGitignoreRepositoryProvider implements GitignoreProvider { // Get lists of .gitignore files from Github const result = await Promise.all([ this.getFiles(), - this.getFiles('Global') + this.getFiles('Global'), + this.getFiles('community') ]); const files = (Array.prototype.concat.apply([], result) as GitignoreTemplate[]) .sort((a: GitignoreTemplate, b: GitignoreTemplate) => a.name.localeCompare(b.name)); @@ -48,6 +51,11 @@ export class GithubGitignoreRepositoryProvider implements GitignoreProvider { // If cached, return cached content const item = this.cache.get('gitignore/' + path) as GitignoreTemplate[]; if(typeof item !== 'undefined') { + item.forEach(template => { + if (template.sha) { + this.templateShaByPath.set(template.path, template.sha); + } + }); return item; } @@ -74,9 +82,11 @@ export class GithubGitignoreRepositoryProvider implements GitignoreProvider { return (item.type === 'file' && item.name.endsWith('.gitignore')); }) .map(item => { + this.templateShaByPath.set(item.path, item.sha); return { name: item.name.replace(/\.gitignore/, ''), - path: item.path + path: item.path, + sha: item.sha }; }); @@ -90,6 +100,19 @@ export class GithubGitignoreRepositoryProvider implements GitignoreProvider { * Downloads a .gitignore from the repository to the path passed */ public async downloadToStream(templatePath: string, writeStream: WriteStream): Promise { + const sha = this.templateShaByPath.get(templatePath); + const cacheKey = `gitignore/template/${templatePath}/${sha ?? 'latest'}`; + const cachedContent = this.cache.get(cacheKey) as string; + if (typeof cachedContent !== 'undefined') { + return new Promise(resolve => { + writeStream.on('finish', () => { + writeStream.close(); + resolve(); + }); + writeStream.end(cachedContent); + }); + } + /* curl \ -H "Accept: application/vnd.github.v3.raw" \ @@ -102,7 +125,14 @@ export class GithubGitignoreRepositoryProvider implements GitignoreProvider { headers: {...await this.client.getHeaders(), 'Accept': 'application/vnd.github.v3.raw'} }; - - await this.client.requestWriteStream(fullUrl, options, writeStream); + const responseBody = await this.client.requestString(fullUrl, options); + this.cache.add(new CacheItem(cacheKey, responseBody)); + return new Promise(resolve => { + writeStream.on('finish', () => { + writeStream.close(); + resolve(); + }); + writeStream.end(responseBody); + }); } } diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 21924b8..722f177 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -9,12 +9,26 @@ import assert from 'assert'; class GitignoreProviderMock implements GitignoreProvider { getTemplates(): Promise { - return Promise.resolve([{ - download_url :'', - name: 'example', - path: 'example', - type: 'foo' - }]); + return Promise.resolve([ + { + download_url :'', + name: 'example', + path: 'example', + type: 'foo' + }, + { + download_url :'', + name: 'Python', + path: 'Python.gitignore', + type: 'foo' + }, + { + download_url :'', + name: 'Node', + path: 'Node.gitignore', + type: 'foo' + } + ]); } downloadToStream(templatePath: string, writeStream: fs.WriteStream): Promise { return new Promise((resolve) => { @@ -35,14 +49,44 @@ function assertLines(path: string, ...expectedLines: string[]) { const content = fs.readFileSync(path, {encoding: 'utf8'}); const lines = content.split(/\r?\n/); - for (let i = 0; i < lines.length; ++i) { + assert.strictEqual(lines.length, expectedLines.length); + for (let i = 0; i < expectedLines.length; ++i) { const expected = expectedLines[i]; const got = lines[i]; console.log(`excepted: "${expected}", got: "${got}"`); - assert(expected === got); + assert.strictEqual(got, expected); } } +function expectedManagedLines(customRules: string[], templateNames: string[], templatePaths: string[]) { + const lines = [ + '# ----------------------------------------------------------------------', + '# Custom ignore rules', + '# ----------------------------------------------------------------------', + '# Add project-specific ignore rules in this section. The extension preserves this content.', + ...customRules, + '', + '# ----------------------------------------------------------------------', + '# GitHub gitignore templates', + `# Templates: ${templateNames.join(', ')}`, + '# ----------------------------------------------------------------------' + ]; + + for (let i = 0; i < templateNames.length; ++i) { + lines.push( + '', + '# ----------------------------------------------------------------------', + `# Template: ${templateNames[i]}`, + `# Source: github/gitignore/${templatePaths[i]}`, + '# ----------------------------------------------------------------------', + templatePaths[i] + ); + } + + lines.push(''); + return lines; +} + suite('Extension Test Suite', () => { // test('Sample test', async () => { @@ -59,7 +103,7 @@ suite('Extension Test Suite', () => { const templates = await gitignoreProvider.getTemplates(); const operation = { - template: templates[0], + templates: [templates[0]], path: path, type: GitignoreOperationType.Overwrite }; @@ -69,7 +113,10 @@ suite('Extension Test Suite', () => { const content = fs.readFileSync(path, {encoding: 'utf8'}); console.log(content); - assertLines(path, 'example', ''); + assertLines( + path, + ...expectedManagedLines([], ['example'], ['example']) + ); // Cleanup // if(fs.existsSync(path)) { @@ -77,7 +124,7 @@ suite('Extension Test Suite', () => { // } }); - test('can overwrite a gitignore file', async () => { + test('can convert an existing gitignore file and preserve custom rules', async () => { const testBaseDir = await createTmpTestDir('download'); const path = `${testBaseDir}/.gitignore`; fs.writeFileSync(path, "existing line"); @@ -87,14 +134,17 @@ suite('Extension Test Suite', () => { const templates = await gitignoreProvider.getTemplates(); const operation = { - template: templates[0], + templates: [templates[0]], path: path, type: GitignoreOperationType.Overwrite }; await downloadGitignoreFile(gitignoreProvider, operation); - assertLines(path, 'example', ''); + assertLines( + path, + ...expectedManagedLines(['existing line'], ['example'], ['example']) + ); // Cleanup if(fs.existsSync(path)) { @@ -102,23 +152,26 @@ suite('Extension Test Suite', () => { } }); - test('can append to a gitignore file', async () => { + test('can update a gitignore file without duplicating existing template content', async () => { const testBaseDir = await createTmpTestDir('download'); const path = `${testBaseDir}/.gitignore`; - fs.writeFileSync(path, "existing line\n"); + fs.writeFileSync(path, "existing line\nexample\n"); const gitignoreProvider = new GitignoreProviderMock(); const templates = await gitignoreProvider.getTemplates(); const operation = { - template: templates[0], + templates: [templates[0]], path: path, - type: GitignoreOperationType.Append + type: GitignoreOperationType.Update }; await downloadGitignoreFile(gitignoreProvider, operation); - assertLines(path, 'existing line', '', 'example',''); + assertLines( + path, + ...expectedManagedLines(['existing line'], ['example'], ['example']) + ); // Cleanup if(fs.existsSync(path)) { @@ -126,4 +179,44 @@ suite('Extension Test Suite', () => { } }); + test('writes selected templates in a reproducible order', async () => { + const firstTestBaseDir = await createTmpTestDir('download'); + const secondTestBaseDir = await createTmpTestDir('download'); + const firstPath = `${firstTestBaseDir}/.gitignore`; + const secondPath = `${secondTestBaseDir}/.gitignore`; + + const gitignoreProvider = new GitignoreProviderMock(); + const templates = await gitignoreProvider.getTemplates(); + const python = templates.find(template => template.name === 'Python'); + const node = templates.find(template => template.name === 'Node'); + assert(python !== undefined); + assert(node !== undefined); + + await downloadGitignoreFile(gitignoreProvider, { + templates: [node, python], + path: firstPath, + type: GitignoreOperationType.Overwrite + }); + await downloadGitignoreFile(gitignoreProvider, { + templates: [python, node], + path: secondPath, + type: GitignoreOperationType.Overwrite + }); + + const firstContent = fs.readFileSync(firstPath, {encoding: 'utf8'}); + const secondContent = fs.readFileSync(secondPath, {encoding: 'utf8'}); + assert.strictEqual(firstContent, secondContent); + assertLines( + firstPath, + ...expectedManagedLines([], ['Node', 'Python'], ['Node.gitignore', 'Python.gitignore']) + ); + + if(fs.existsSync(firstPath)) { + fs.unlinkSync(firstPath); + } + if(fs.existsSync(secondPath)) { + fs.unlinkSync(secondPath); + } + }); + }); diff --git a/src/test/providers/github-gitignore.test.ts b/src/test/providers/github-gitignore.test.ts index b62cae9..fc03674 100644 --- a/src/test/providers/github-gitignore.test.ts +++ b/src/test/providers/github-gitignore.test.ts @@ -3,7 +3,7 @@ import * as fs from 'fs'; import { Writable } from 'stream'; import { Cache } from '../../cache'; -import { GitignoreProvider, GitignoreOperation, GitignoreTemplate, GitignoreOperationType } from '../../interfaces'; +import { GitignoreProvider, GitignoreTemplate } from '../../interfaces'; import { GithubGitignoreApiProvider } from '../../providers/github-gitignore-api'; import { GithubGitignoreRepositoryProvider } from '../../providers/github-gitignore-repository'; import { GithubContext, GithubSession } from '../../github/session'; @@ -84,20 +84,17 @@ providers.forEach(provider => { fs.unlinkSync(path); } - const operation = { - template: templates.find(t => t.name === 'C'), - path: path, - type: GitignoreOperationType.Overwrite - }; + const template = templates.find(t => t.name === 'C'); + assert(template !== undefined); - const fileStream = fs.createWriteStream(operation.path, { flags: "w" }); - await provider.downloadToStream(operation.template.path, fileStream); + const fileStream = fs.createWriteStream(path, { flags: "w" }); + await provider.downloadToStream(template.path, fileStream); // Assert - const fileExists = await fileExits(operation.path); + const fileExists = await fileExits(path); assert(fileExists); - const content = fs.readFileSync(operation.path, {encoding: 'utf8'}); + const content = fs.readFileSync(path, {encoding: 'utf8'}); const lines = content.split(/\r?\n/); assert(lines[0] === '# Prerequisites'); @@ -113,16 +110,11 @@ providers.forEach(provider => { test('can download a root template (regular) to a writable stream', async () => { const memoryStream = new MemoryWritable(); - const path = provider.constructor.name + '.gitignore'; - - const operation = { - template: templates.find(t => t.name === 'Python'), - path: path, - type: GitignoreOperationType.Overwrite - }; + const template = templates.find(t => t.name === 'Python'); + assert(template !== undefined); // Act - await provider.downloadToStream(operation.template.path, memoryStream); + await provider.downloadToStream(template.path, memoryStream); // Assert const content = memoryStream.content; @@ -130,7 +122,7 @@ providers.forEach(provider => { assert(lines[0] === '# Byte-compiled / optimized / DLL files'); assert(lines[1] === '__pycache__/'); - assert(lines[2] === '*.py[cod]'); + assert(lines[2] === '*.py[codz]'); }); // Test for bug #21 @@ -138,16 +130,11 @@ providers.forEach(provider => { test('can download a root template (symlink) to a writable stream', async () => { const memoryStream = new MemoryWritable(); - const path = provider.constructor.name + '.gitignore'; - - const operation = { - template: templates.find(t => t.name === 'Clojure'), - path: path, - type: GitignoreOperationType.Overwrite - }; + const template = templates.find(t => t.name === 'Clojure'); + assert(template !== undefined); // Act - await provider.downloadToStream(operation.template.path, memoryStream); + await provider.downloadToStream(template.path, memoryStream); // Assert const content = memoryStream.content; @@ -170,24 +157,19 @@ providers.forEach(provider => { const memoryStream = new MemoryWritable(); - const path = provider.constructor.name + '.gitignore'; - - const operation = { - template: templates.find(t => t.name === 'VisualStudioCode'), - path: path, - type: GitignoreOperationType.Overwrite - }; + const template = templates.find(t => t.name === 'VisualStudioCode'); + assert(template !== undefined); // Act - await provider.downloadToStream(operation.template.path, memoryStream); + await provider.downloadToStream(template.path, memoryStream); // Assert const content = memoryStream.content; const lines = content.split(/\r?\n/); - assert(lines[0] === '.vscode/*'); - assert(lines[1] === '!.vscode/settings.json'); - assert(lines[2] === '!.vscode/tasks.json'); + assert(lines[0] === '# Visual Studio Code'); + assert(lines[1] === '.vscode/*'); + assert(lines[2] === '!.vscode/settings.json'); }); // Test for bug #21 @@ -201,16 +183,11 @@ providers.forEach(provider => { const memoryStream = new MemoryWritable(); - const path = provider.constructor.name + '.gitignore'; - - const operation = { - template: templates.find(t => t.name === 'Octave'), - path: path, - type: GitignoreOperationType.Overwrite - }; + const template = templates.find(t => t.name === 'Octave'); + assert(template !== undefined); // Act - await provider.downloadToStream(operation.template.path, memoryStream); + await provider.downloadToStream(template.path, memoryStream); // Assert const content = memoryStream.content; @@ -219,9 +196,9 @@ providers.forEach(provider => { // Ensure the content is not the name of the linked file assert(lines[0] !== 'MATLAB.gitignore'); - assert(lines[0] === '# Windows default autosave extension'); + assert(lines[0] === '# Autosave files'); assert(lines[1] === '*.asv'); - assert(lines[2] === ''); + assert(lines[2] === '*.m~'); }); });