Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[eas-cli] Fix .git x .easignore interaction #2925

Merged
merged 3 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ This is the log of notable changes to EAS CLI and related packages.

### 🐛 Bug fixes

- Fixed `.git` being always unexpectedly removed if you had `requireCommit: true` and `.easignore` present. ([#2925](https://github.com/expo/eas-cli/pull/2925) by [@sjchmiela](https://github.com/sjchmiela))

### 🧹 Chores

- Fix `eas fingerprint:compare` URL generation and pretty prints. ([#2909](https://github.com/expo/eas-cli/pull/2909) by [@quinlanj](https://github.com/quinlanj))
Expand Down
34 changes: 27 additions & 7 deletions packages/eas-cli/src/vcs/__tests__/local-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe(Ignore, () => {
'/root'
);

const ignore = await Ignore.createAsync('/root');
const ignore = await Ignore.createForCopyingAsync('/root');
expect(ignore.ignores('aaa')).toBe(true);
expect(ignore.ignores('bbb')).toBe(false);
expect(ignore.ignores('dir/aaa')).toBe(true);
Expand All @@ -35,7 +35,7 @@ describe(Ignore, () => {
'/root'
);

const ignore = await Ignore.createAsync('/root');
const ignore = await Ignore.createForCopyingAsync('/root');
expect(ignore.ignores('aaa')).toBe(false);
expect(ignore.ignores('bbb')).toBe(false);
expect(ignore.ignores('ccc')).toBe(true);
Expand All @@ -52,7 +52,7 @@ describe(Ignore, () => {
'/root'
);

const ignore = await Ignore.createAsync('/root');
const ignore = await Ignore.createForCopyingAsync('/root');
expect(ignore.ignores('aaa')).toBe(true);
expect(ignore.ignores('bbb')).toBe(false);
expect(ignore.ignores('node_modules/aaa')).toBe(true);
Expand All @@ -69,16 +69,36 @@ describe(Ignore, () => {
'/root'
);

const ignore = await Ignore.createAsync('/root');
const ignore = await Ignore.createForCopyingAsync('/root');
expect(ignore.ignores('dir/ccc')).toBe(true);
});

it('ignores .git', async () => {
it('ignores .git if copying', async () => {
vol.fromJSON({}, '/root');

const ignore = await Ignore.createAsync('/root');
const ignore = await Ignore.createForCopyingAsync('/root');
expect(ignore.ignores('.git')).toBe(true);
});
describe('for checking', () => {
it('does not necessarily ignore .git', async () => {
vol.fromJSON({}, '/root');

const ignore = await Ignore.createForCheckingAsync('/root');
expect(ignore.ignores('.git')).toBe(false);
});

it('ignores .git if present in .easignore', async () => {
vol.fromJSON(
{
'.easignore': '.git\n',
},
'/root'
);

const ignore = await Ignore.createForCheckingAsync('/root');
expect(ignore.ignores('.git')).toBe(true);
});
});

it('does not throw an error if there is a trailing backslash in the gitignore', async () => {
vol.fromJSON(
Expand All @@ -88,7 +108,7 @@ describe(Ignore, () => {
'/root'
);

const ignore = await Ignore.createAsync('/root');
const ignore = await Ignore.createForCopyingAsync('/root');
expect(() => ignore.ignores('dir/test')).not.toThrowError();
});
});
52 changes: 52 additions & 0 deletions packages/eas-cli/src/vcs/clients/__tests__/git.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,42 @@ describe('git', () => {
await expect(vcs.makeShallowCopyAsync(repoCloneIgnored)).resolves.not.toThrow();
await expect(fs.stat(path.join(repoCloneIgnored, 'results'))).rejects.toThrow('ENOENT');
});

it('if .git is present in .easignore', async () => {
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await spawnAsync('git', ['init'], { cwd: repoRoot });
const vcs = new GitClient({
requireCommit: false,
maybeCwdOverride: repoRoot,
});

await fs.writeFile(`${repoRoot}/.easignore`, '.git\n');

await spawnAsync('git', ['add', '.'], { cwd: repoRoot });
await spawnAsync('git', ['commit', '-m', 'temp commit'], { cwd: repoRoot });

const repoClone = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await expect(vcs.makeShallowCopyAsync(repoClone)).resolves.not.toThrow();
await expect(fs.stat(path.join(repoClone, '.git'))).rejects.toThrow('ENOENT');
});

it('if .git is not present in .easignore', async () => {
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await spawnAsync('git', ['init'], { cwd: repoRoot });
const vcs = new GitClient({
requireCommit: false,
maybeCwdOverride: repoRoot,
});

await fs.writeFile(`${repoRoot}/.easignore`, '');

await spawnAsync('git', ['add', '.'], { cwd: repoRoot });
await spawnAsync('git', ['commit', '-m', 'temp commit'], { cwd: repoRoot });

const repoClone = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await expect(vcs.makeShallowCopyAsync(repoClone)).resolves.not.toThrow();
await expect(fs.readdir(path.join(repoClone, '.git'))).resolves.toBeDefined();
});
});

it('does not include files that have been removed in the working directory', async () => {
Expand Down Expand Up @@ -205,4 +241,20 @@ describe('git', () => {
fs.stat(path.join(requireCommitClone, 'new-tracked-file.txt'))
).resolves.not.toThrow();
});

it('does not allow .easignore if requireCommit is true', async () => {
const repoRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'eas-cli-git-test-'));
await spawnAsync('git', ['init'], { cwd: repoRoot });
const vcs = new GitClient({
requireCommit: true,
maybeCwdOverride: repoRoot,
});

await fs.writeFile(`${repoRoot}/.easignore`, '*easignored*\n');

await spawnAsync('git', ['add', '.'], { cwd: repoRoot });
await spawnAsync('git', ['commit', '-m', 'tmp commit'], { cwd: repoRoot });

await expect(vcs.makeShallowCopyAsync(repoRoot)).rejects.toThrow('Detected ".easignore" file');
});
});
21 changes: 19 additions & 2 deletions packages/eas-cli/src/vcs/clients/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,21 @@ export default class GitClient extends Client {
}

const rootPath = await this.getRootPathAsync();
const sourceEasignorePath = path.join(rootPath, EASIGNORE_FILENAME);
const doesEasignoreExist = await fs.exists(sourceEasignorePath);
if (this.requireCommit && doesEasignoreExist) {
Log.error(
`".easignore" file is not supported if you also have "requireCommit" set to "true" in "eas.json".`
);
Log.error(
`Remove "${sourceEasignorePath}" and use ".gitignore" instead. ${learnMore(
'https://expo.fyi/eas-build-archive'
)}`
);
throw new Error(
`Detected ".easignore" file ${sourceEasignorePath} while in "requireCommit = true" mode.`
);
}

let gitRepoUri;
if (process.platform === 'win32') {
Expand Down Expand Up @@ -238,7 +253,9 @@ export default class GitClient extends Client {
);

// Special-case `.git` which `git ls-files` will never consider ignored.
const ignore = await Ignore.createAsync(rootPath);
// We don't want to ignore anything by default. We want to know what does
// the user want to ignore.
const ignore = await Ignore.createForCheckingAsync(rootPath);
if (ignore.ignores('.git')) {
await fs.rm(path.join(destinationPath, '.git'), { recursive: true, force: true });
Log.debug('deleted .git', {
Expand Down Expand Up @@ -341,7 +358,7 @@ export default class GitClient extends Client {

const easIgnorePath = path.join(rootPath, EASIGNORE_FILENAME);
if (await fs.exists(easIgnorePath)) {
const ignore = await Ignore.createAsync(rootPath);
const ignore = await Ignore.createForCheckingAsync(rootPath);
const wouldNotBeCopiedToClone = ignore.ignores(filePath);
const wouldBeDeletedFromClone =
(
Expand Down
2 changes: 1 addition & 1 deletion packages/eas-cli/src/vcs/clients/noVcs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default class NoVcsClient extends Client {
}

public override async isFileIgnoredAsync(filePath: string): Promise<boolean> {
const ignore = await Ignore.createAsync(getRootPath());
const ignore = await Ignore.createForCheckingAsync(getRootPath());
return ignore.ignores(filePath);
}

Expand Down
32 changes: 20 additions & 12 deletions packages/eas-cli/src/vcs/local.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@ import Log from '../log';

export const EASIGNORE_FILENAME = '.easignore';
const GITIGNORE_FILENAME = '.gitignore';

const DEFAULT_IGNORE = `
.git
node_modules
`;

export function getRootPath(): string {
const rootPath = process.env.EAS_PROJECT_ROOT ?? process.cwd();
if (!path.isAbsolute(rootPath)) {
Expand All @@ -37,17 +31,31 @@ export class Ignore {

private constructor(private readonly rootDir: string) {}

static async createAsync(rootDir: string): Promise<Ignore> {
static async createForCopyingAsync(rootDir: string): Promise<Ignore> {
const ignore = new Ignore(rootDir);
await ignore.initIgnoreAsync({
defaultIgnore: `
.git
node_modules
`,
});
return ignore;
}

/** Does not include the default .git and node_modules ignore rules. */
static async createForCheckingAsync(rootDir: string): Promise<Ignore> {
const ignore = new Ignore(rootDir);
await ignore.initIgnoreAsync();
await ignore.initIgnoreAsync({
defaultIgnore: ``,
});
return ignore;
}

public async initIgnoreAsync(): Promise<void> {
public async initIgnoreAsync({ defaultIgnore }: { defaultIgnore: string }): Promise<void> {
const easIgnorePath = path.join(this.rootDir, EASIGNORE_FILENAME);
if (await fsExtra.pathExists(easIgnorePath)) {
this.ignoreMapping = [
['', createIgnore().add(DEFAULT_IGNORE)],
['', createIgnore().add(defaultIgnore)],
['', createIgnore().add(await fsExtra.readFile(easIgnorePath, 'utf-8'))],
];

Expand All @@ -74,7 +82,7 @@ export class Ignore {
] as const;
})
);
this.ignoreMapping = [['', createIgnore().add(DEFAULT_IGNORE)], ...ignoreMapping];
this.ignoreMapping = [['', createIgnore().add(defaultIgnore)], ...ignoreMapping];

Log.debug('initializing ignore mapping with .gitignore files', {
ignoreFilePaths,
Expand All @@ -100,7 +108,7 @@ export async function makeShallowCopyAsync(_src: string, dst: string): Promise<v
const src = path.toNamespacedPath(path.normalize(_src));

Log.debug('makeShallowCopyAsync', { src, dst });
const ignore = await Ignore.createAsync(src);
const ignore = await Ignore.createForCopyingAsync(src);
Log.debug('makeShallowCopyAsync ignoreMapping', { ignoreMapping: ignore.ignoreMapping });

await fs.cp(src, dst, {
Expand Down