Skip to content

Commit ab407f3

Browse files
committed
Add addToGitIgnore to cli-kit to append entries to existing .gitignore files
1 parent 7b3ba77 commit ab407f3

File tree

5 files changed

+115
-26
lines changed

5 files changed

+115
-26
lines changed

.changeset/thick-flies-rest.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/cli-kit': patch
3+
---
4+
5+
Add `addToGitIgnore` to cli-kit to append entries to existing `.gitignore` files

packages/cli-kit/src/public/node/git.test.ts

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as git from './git.js'
2-
import {appendFileSync} from './fs.js'
2+
import {appendFileSync, fileExistsSync, inTemporaryDirectory, readFileSync, writeFileSync} from './fs.js'
33
import {hasGit} from './context/local.js'
44
import {beforeEach, describe, expect, test, vi} from 'vitest'
55
import simpleGit from 'simple-git'
@@ -25,9 +25,15 @@ const simpleGitProperties = {
2525
status: mockedGitStatus,
2626
}
2727

28-
vi.mock('./context/local.js')
29-
vi.mock('./fs.js')
3028
vi.mock('simple-git')
29+
vi.mock('./context/local.js')
30+
vi.mock('./fs.js', async () => {
31+
const fs = await vi.importActual('./fs.js')
32+
return {
33+
...fs,
34+
appendFileSync: vi.fn(),
35+
}
36+
})
3137

3238
beforeEach(() => {
3339
vi.mocked(hasGit).mockResolvedValue(true)
@@ -375,3 +381,67 @@ describe('isGitClean()', () => {
375381
expect(simpleGit).toHaveBeenCalledWith({baseDir: directory})
376382
})
377383
})
384+
385+
describe('addToGitIgnore()', () => {
386+
test('does nothing when .gitignore does not exist', async () => {
387+
await inTemporaryDirectory(async (tmpDir) => {
388+
// Given
389+
const gitIgnorePath = `${tmpDir}/.gitignore`
390+
391+
// When
392+
git.addToGitIgnore(tmpDir, '.shopify')
393+
394+
// Then
395+
expect(fileExistsSync(gitIgnorePath)).toBe(false)
396+
})
397+
})
398+
399+
test('does nothing when pattern already exists in .gitignore', async () => {
400+
await inTemporaryDirectory(async (tmpDir) => {
401+
// Given
402+
const gitIgnorePath = `${tmpDir}/.gitignore`
403+
const gitIgnoreContent = ' .shopify \nnode_modules\n'
404+
405+
writeFileSync(gitIgnorePath, gitIgnoreContent)
406+
407+
// When
408+
git.addToGitIgnore(tmpDir, '.shopify')
409+
410+
// Then
411+
const actualContent = readFileSync(gitIgnorePath).toString()
412+
expect(actualContent).toBe(gitIgnoreContent)
413+
})
414+
})
415+
416+
test('appends pattern to .gitignore when file exists and pattern not present', async () => {
417+
await inTemporaryDirectory(async (tmpDir) => {
418+
// Given
419+
const gitIgnorePath = `${tmpDir}/.gitignore`
420+
421+
writeFileSync(gitIgnorePath, 'node_modules\ndist')
422+
423+
// When
424+
git.addToGitIgnore(tmpDir, '.shopify')
425+
426+
// Then
427+
const gitIgnoreContent = readFileSync(gitIgnorePath).toString()
428+
expect(gitIgnoreContent).toBe('node_modules\ndist\n.shopify\n')
429+
})
430+
})
431+
432+
test('appends pattern to .gitignore when file exists and pattern not present without duplicating the last empty line', async () => {
433+
await inTemporaryDirectory(async (tmpDir) => {
434+
// Given
435+
const gitIgnorePath = `${tmpDir}/.gitignore`
436+
437+
writeFileSync(gitIgnorePath, 'node_modules\ndist\n')
438+
439+
// When
440+
git.addToGitIgnore(tmpDir, '.shopify')
441+
442+
// Then
443+
const gitIgnoreContent = readFileSync(gitIgnorePath).toString()
444+
expect(gitIgnoreContent).toBe('node_modules\ndist\n.shopify\n')
445+
})
446+
})
447+
})

packages/cli-kit/src/public/node/git.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/* eslint-disable @typescript-eslint/no-base-to-string */
22
import {hasGit, isTerminalInteractive} from './context/local.js'
3-
import {appendFileSync} from './fs.js'
3+
import {appendFileSync, detectEOL, fileExistsSync, readFileSync, writeFileSync} from './fs.js'
44
import {AbortError} from './error.js'
5-
import {cwd} from './path.js'
5+
import {cwd, joinPath} from './path.js'
66
import {runWithTimer} from './metadata.js'
77
import {outputContent, outputToken, outputDebug} from '../../public/node/output.js'
88
import git, {TaskOptions, SimpleGitProgressEvent, DefaultLogFields, ListLogLine, SimpleGit} from 'simple-git'
@@ -63,6 +63,38 @@ export function createGitIgnore(directory: string, template: GitIgnoreTemplate):
6363
appendFileSync(filePath, fileContent)
6464
}
6565

66+
/**
67+
* Add an entry to an existing .gitignore file.
68+
*
69+
* If the .gitignore file doesn't exist, or if the entry is already present,
70+
* no changes will be made.
71+
*
72+
* @param root - The directory containing the .gitignore file.
73+
* @param entry - The entry to add to the .gitignore file.
74+
*/
75+
export function addToGitIgnore(root: string, entry: string): void {
76+
const gitIgnorePath = joinPath(root, '.gitignore')
77+
78+
if (!fileExistsSync(gitIgnorePath)) {
79+
// When the .gitignore file does not exist, the CLI should not be opinionated about creating it
80+
return
81+
}
82+
83+
const gitIgnoreContent = readFileSync(gitIgnorePath).toString()
84+
const eol = detectEOL(gitIgnoreContent)
85+
86+
if (gitIgnoreContent.split(eol).some((line) => line.trim() === entry.trim())) {
87+
// The file already existing in the .gitignore
88+
return
89+
}
90+
91+
if (gitIgnoreContent.endsWith(eol)) {
92+
writeFileSync(gitIgnorePath, `${gitIgnoreContent}${entry}${eol}`)
93+
} else {
94+
writeFileSync(gitIgnorePath, `${gitIgnoreContent}${eol}${entry}${eol}`)
95+
}
96+
}
97+
6698
/**
6799
* Options to use when cloning a git repository.
68100
*

packages/theme/src/cli/services/metafields-pull.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ describe('metafields-pull', () => {
9999

100100
// Then
101101
await expect(fileExists(gitIgnorePath)).resolves.toBe(true)
102-
await expect(readFile(gitIgnorePath)).resolves.toBe(`.DS_Store\n.shopify/secrets.json\n.shopify`)
102+
await expect(readFile(gitIgnorePath)).resolves.toBe(`.DS_Store\n.shopify/secrets.json\n.shopify\n`)
103103
})
104104

105105
expect(capturedOutput.info()).toContain('Metafield definitions have been successfully downloaded.')

packages/theme/src/cli/services/metafields-pull.ts

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import {AdminSession, ensureAuthenticatedThemes} from '@shopify/cli-kit/node/ses
66
import {cwd, joinPath} from '@shopify/cli-kit/node/path'
77
import {metafieldDefinitionsByOwnerType} from '@shopify/cli-kit/node/themes/api'
88
import {renderError, renderSuccess} from '@shopify/cli-kit/node/ui'
9-
import {detectEOL, fileExistsSync, mkdirSync, readFileSync, writeFileSync} from '@shopify/cli-kit/node/fs'
9+
import {fileExistsSync, mkdirSync, writeFileSync} from '@shopify/cli-kit/node/fs'
1010
import {outputDebug} from '@shopify/cli-kit/node/output'
11+
import {addToGitIgnore} from '@shopify/cli-kit/node/git'
1112

1213
interface MetafieldsPullOptions {
1314
path: string
@@ -169,22 +170,3 @@ function writeMetafieldDefinitionsToFile(path: string, content: unknown) {
169170

170171
writeFileSync(filePath, fileContent)
171172
}
172-
173-
function addToGitIgnore(root: string, entry: string) {
174-
const gitIgnorePath = joinPath(root, '.gitignore')
175-
176-
if (!fileExistsSync(gitIgnorePath)) {
177-
// When the .gitignore file does not exist, the CLI should not be opinionated about creating it
178-
return
179-
}
180-
181-
const gitIgnoreContent = readFileSync(gitIgnorePath).toString()
182-
const eol = detectEOL(gitIgnoreContent)
183-
184-
if (gitIgnoreContent.split(eol).includes(entry)) {
185-
// The file already existing in the .gitignore
186-
return
187-
}
188-
189-
writeFileSync(gitIgnorePath, `${gitIgnoreContent}${eol}${entry}`)
190-
}

0 commit comments

Comments
 (0)