eoas 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +1 -0
- package/.eslintrc.js +73 -0
- package/.prettierrc +9 -0
- package/README.md +17 -0
- package/bin/dev.cmd +3 -0
- package/bin/dev.js +6 -0
- package/bin/run.cmd +3 -0
- package/bin/run.js +7 -0
- package/package.json +92 -0
- package/src/commands/generate-certs.ts +95 -0
- package/src/commands/init.ts +117 -0
- package/src/commands/publish.ts +277 -0
- package/src/index.d.ts +7 -0
- package/src/lib/assets.ts +118 -0
- package/src/lib/auth.ts +67 -0
- package/src/lib/expoConfig.ts +265 -0
- package/src/lib/log.ts +122 -0
- package/src/lib/ora.ts +113 -0
- package/src/lib/package.ts +6 -0
- package/src/lib/prompts.ts +97 -0
- package/src/lib/repo.ts +62 -0
- package/src/lib/runtimeVersion.ts +177 -0
- package/src/lib/utils.ts +3 -0
- package/src/lib/vcs/README.md +1 -0
- package/src/lib/vcs/clients/git.ts +390 -0
- package/src/lib/vcs/clients/gitNoCommit.ts +45 -0
- package/src/lib/vcs/clients/noVcs.ts +23 -0
- package/src/lib/vcs/git.ts +68 -0
- package/src/lib/vcs/index.ts +25 -0
- package/src/lib/vcs/local.ts +88 -0
- package/src/lib/vcs/vcs.ts +90 -0
- package/src/lib/workflow.ts +47 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { ExpoConfig } from '@expo/config';
|
|
2
|
+
import { Updates } from '@expo/config-plugins';
|
|
3
|
+
import { Env, Workflow } from '@expo/eas-build-job';
|
|
4
|
+
import spawnAsync from '@expo/spawn-async';
|
|
5
|
+
import fs from 'fs-extra';
|
|
6
|
+
import resolveFrom, { silent as silentResolveFrom } from 'resolve-from';
|
|
7
|
+
import semver from 'semver';
|
|
8
|
+
|
|
9
|
+
import Log, { link } from './log';
|
|
10
|
+
|
|
11
|
+
export class ExpoUpdatesCLIModuleNotFoundError extends Error {}
|
|
12
|
+
export class ExpoUpdatesCLIInvalidCommandError extends Error {}
|
|
13
|
+
export class ExpoUpdatesCLICommandFailedError extends Error {}
|
|
14
|
+
|
|
15
|
+
export async function expoUpdatesCommandAsync(
|
|
16
|
+
projectDir: string,
|
|
17
|
+
args: string[],
|
|
18
|
+
options: { env: Env | undefined; cwd?: string }
|
|
19
|
+
): Promise<string> {
|
|
20
|
+
let expoUpdatesCli;
|
|
21
|
+
try {
|
|
22
|
+
expoUpdatesCli =
|
|
23
|
+
silentResolveFrom(projectDir, 'expo-updates/bin/cli') ??
|
|
24
|
+
resolveFrom(projectDir, 'expo-updates/bin/cli.js');
|
|
25
|
+
} catch (e: any) {
|
|
26
|
+
if (e.code === 'MODULE_NOT_FOUND') {
|
|
27
|
+
throw new ExpoUpdatesCLIModuleNotFoundError(
|
|
28
|
+
`The \`expo-updates\` package was not found. Follow the installation directions at ${link(
|
|
29
|
+
'https://docs.expo.dev/bare/installing-expo-modules/'
|
|
30
|
+
)}`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
throw e;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
return (
|
|
38
|
+
await spawnAsync(expoUpdatesCli, args, {
|
|
39
|
+
stdio: 'pipe',
|
|
40
|
+
env: { ...process.env, ...options.env },
|
|
41
|
+
cwd: options.cwd,
|
|
42
|
+
})
|
|
43
|
+
).stdout;
|
|
44
|
+
} catch (e: any) {
|
|
45
|
+
if (e.stderr && typeof e.stderr === 'string') {
|
|
46
|
+
if (e.stderr.includes('Invalid command')) {
|
|
47
|
+
throw new ExpoUpdatesCLIInvalidCommandError(
|
|
48
|
+
`The command specified by ${args} was not valid in the \`expo-updates\` CLI.`
|
|
49
|
+
);
|
|
50
|
+
} else {
|
|
51
|
+
throw new ExpoUpdatesCLICommandFailedError(e.stderr);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
throw e;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function getExpoUpdatesPackageVersionIfInstalledAsync(
|
|
60
|
+
projectDir: string
|
|
61
|
+
): Promise<string | null> {
|
|
62
|
+
const maybePackageJson = resolveFrom.silent(projectDir, 'expo-updates/package.json');
|
|
63
|
+
if (!maybePackageJson) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const { version } = await fs.readJson(maybePackageJson);
|
|
67
|
+
return version ?? null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync(
|
|
71
|
+
projectDir: string
|
|
72
|
+
): Promise<boolean> {
|
|
73
|
+
const expoUpdatesPackageVersion = await getExpoUpdatesPackageVersionIfInstalledAsync(projectDir);
|
|
74
|
+
if (expoUpdatesPackageVersion === null) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (expoUpdatesPackageVersion.includes('canary')) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Anything SDK 51 or greater uses the expo-updates CLI
|
|
83
|
+
return semver.gte(expoUpdatesPackageVersion, '0.25.4');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function resolveRuntimeVersionUsingCLIAsync({
|
|
87
|
+
platform,
|
|
88
|
+
workflow,
|
|
89
|
+
projectDir,
|
|
90
|
+
env,
|
|
91
|
+
cwd,
|
|
92
|
+
}: {
|
|
93
|
+
platform: 'ios' | 'android';
|
|
94
|
+
workflow: Workflow;
|
|
95
|
+
projectDir: string;
|
|
96
|
+
env: Env | undefined;
|
|
97
|
+
cwd?: string;
|
|
98
|
+
}): Promise<{
|
|
99
|
+
runtimeVersion: string | null;
|
|
100
|
+
expoUpdatesRuntimeFingerprint: {
|
|
101
|
+
fingerprintSources: object[];
|
|
102
|
+
isDebugFingerprintSource: boolean;
|
|
103
|
+
} | null;
|
|
104
|
+
expoUpdatesRuntimeFingerprintHash: string | null;
|
|
105
|
+
}> {
|
|
106
|
+
Log.debug('Using expo-updates runtimeversion:resolve CLI for runtime version resolution');
|
|
107
|
+
|
|
108
|
+
const useDebugFingerprintSource = Log.isDebug;
|
|
109
|
+
|
|
110
|
+
const extraArgs = useDebugFingerprintSource ? ['--debug'] : [];
|
|
111
|
+
|
|
112
|
+
const resolvedRuntimeVersionJSONResult = await expoUpdatesCommandAsync(
|
|
113
|
+
projectDir,
|
|
114
|
+
['runtimeversion:resolve', '--platform', platform, '--workflow', workflow, ...extraArgs],
|
|
115
|
+
{ env, cwd }
|
|
116
|
+
);
|
|
117
|
+
const runtimeVersionResult = JSON.parse(resolvedRuntimeVersionJSONResult);
|
|
118
|
+
|
|
119
|
+
Log.debug('runtimeversion:resolve output:');
|
|
120
|
+
Log.debug(resolvedRuntimeVersionJSONResult);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
runtimeVersion: runtimeVersionResult.runtimeVersion ?? null,
|
|
124
|
+
expoUpdatesRuntimeFingerprint: runtimeVersionResult.fingerprintSources
|
|
125
|
+
? {
|
|
126
|
+
fingerprintSources: runtimeVersionResult.fingerprintSources,
|
|
127
|
+
isDebugFingerprintSource: useDebugFingerprintSource,
|
|
128
|
+
}
|
|
129
|
+
: null,
|
|
130
|
+
expoUpdatesRuntimeFingerprintHash: runtimeVersionResult.fingerprintSources
|
|
131
|
+
? runtimeVersionResult.runtimeVersion
|
|
132
|
+
: null,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function resolveRuntimeVersionAsync({
|
|
137
|
+
exp,
|
|
138
|
+
platform,
|
|
139
|
+
workflow,
|
|
140
|
+
projectDir,
|
|
141
|
+
env,
|
|
142
|
+
cwd,
|
|
143
|
+
}: {
|
|
144
|
+
exp: ExpoConfig;
|
|
145
|
+
platform: 'ios' | 'android';
|
|
146
|
+
workflow: Workflow;
|
|
147
|
+
projectDir: string;
|
|
148
|
+
env: Env | undefined;
|
|
149
|
+
cwd?: string;
|
|
150
|
+
}): Promise<{
|
|
151
|
+
runtimeVersion: string | null;
|
|
152
|
+
expoUpdatesRuntimeFingerprint: {
|
|
153
|
+
fingerprintSources: object[];
|
|
154
|
+
isDebugFingerprintSource: boolean;
|
|
155
|
+
} | null;
|
|
156
|
+
expoUpdatesRuntimeFingerprintHash: string | null;
|
|
157
|
+
} | null> {
|
|
158
|
+
if (!(await isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync(projectDir))) {
|
|
159
|
+
// fall back to the previous behavior (using the @expo/config-plugins eas-cli dependency rather
|
|
160
|
+
// than the versioned @expo/config-plugins dependency in the project)
|
|
161
|
+
return {
|
|
162
|
+
runtimeVersion: await Updates.getRuntimeVersionNullableAsync(projectDir, exp, platform),
|
|
163
|
+
expoUpdatesRuntimeFingerprint: null,
|
|
164
|
+
expoUpdatesRuntimeFingerprintHash: null,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
return await resolveRuntimeVersionUsingCLIAsync({ platform, workflow, projectDir, env, cwd });
|
|
170
|
+
} catch (e: any) {
|
|
171
|
+
// if expo-updates is not installed, there's no need for a runtime version in the build
|
|
172
|
+
if (e instanceof ExpoUpdatesCLIModuleNotFoundError) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
throw e;
|
|
176
|
+
}
|
|
177
|
+
}
|
package/src/lib/utils.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
This library is copied from eas-cli[https://github.com/expo/eas-cli] to ensure consistent user experience.
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import * as PackageManagerUtils from '@expo/package-manager';
|
|
2
|
+
import spawnAsync from '@expo/spawn-async';
|
|
3
|
+
import { Errors } from '@oclif/core';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
|
|
7
|
+
import Log, { learnMore } from '../../log';
|
|
8
|
+
import { ora } from '../../ora';
|
|
9
|
+
import { confirmAsync, promptAsync } from '../../prompts';
|
|
10
|
+
import {
|
|
11
|
+
doesGitRepoExistAsync,
|
|
12
|
+
getGitDiffOutputAsync,
|
|
13
|
+
gitDiffAsync,
|
|
14
|
+
gitStatusAsync,
|
|
15
|
+
isGitInstalledAsync,
|
|
16
|
+
} from '../git';
|
|
17
|
+
import { Client } from '../vcs';
|
|
18
|
+
|
|
19
|
+
export default class GitClient extends Client {
|
|
20
|
+
constructor(private readonly maybeCwdOverride?: string) {
|
|
21
|
+
super();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public override async ensureRepoExistsAsync(): Promise<void> {
|
|
25
|
+
try {
|
|
26
|
+
if (!(await isGitInstalledAsync())) {
|
|
27
|
+
Log.error(
|
|
28
|
+
`${chalk.bold('git')} command not found. Install it before proceeding or set ${chalk.bold(
|
|
29
|
+
'EAS_NO_VCS=1'
|
|
30
|
+
)} to use EAS CLI without Git (or any other version control system).`
|
|
31
|
+
);
|
|
32
|
+
Log.error(learnMore('https://expo.fyi/eas-vcs-workflow'));
|
|
33
|
+
Errors.exit(1);
|
|
34
|
+
}
|
|
35
|
+
} catch (error: any) {
|
|
36
|
+
Log.error(
|
|
37
|
+
`${chalk.bold('git')} found, but ${chalk.bold(
|
|
38
|
+
'git --help'
|
|
39
|
+
)} exited with status ${error?.status}${error?.stderr ? `:` : '.'}`
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (error?.stderr) {
|
|
43
|
+
Log.error(error?.stderr);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Log.error(
|
|
47
|
+
`Repair your Git installation, or set ${chalk.bold(
|
|
48
|
+
'EAS_NO_VCS=1'
|
|
49
|
+
)} to use EAS CLI without Git (or any other version control system).`
|
|
50
|
+
);
|
|
51
|
+
Log.error(learnMore('https://expo.fyi/eas-vcs-workflow'));
|
|
52
|
+
Errors.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (await doesGitRepoExistAsync(this.maybeCwdOverride)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Log.warn("It looks like you haven't initialized the git repository yet.");
|
|
60
|
+
Log.warn('EAS requires you to use a git repository for your project.');
|
|
61
|
+
|
|
62
|
+
const cwd = process.cwd();
|
|
63
|
+
const repoRoot = PackageManagerUtils.resolveWorkspaceRoot(cwd) ?? cwd;
|
|
64
|
+
const confirmInit = await confirmAsync({
|
|
65
|
+
message: `Would you like us to run 'git init' in ${
|
|
66
|
+
this.maybeCwdOverride ?? repoRoot
|
|
67
|
+
} for you?`,
|
|
68
|
+
type: 'confirm',
|
|
69
|
+
name: 'confirmInit',
|
|
70
|
+
});
|
|
71
|
+
if (!confirmInit) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
'A git repository is required for building your project. Initialize it and run this command again.'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
await spawnAsync('git', ['init'], { cwd: this.maybeCwdOverride ?? repoRoot });
|
|
77
|
+
|
|
78
|
+
Log.log("We're going to make an initial commit for your repository.");
|
|
79
|
+
|
|
80
|
+
const { message } = await promptAsync({
|
|
81
|
+
type: 'text',
|
|
82
|
+
name: 'message',
|
|
83
|
+
message: 'Commit message:',
|
|
84
|
+
initial: 'Initial commit',
|
|
85
|
+
validate: (input: string) => input !== '',
|
|
86
|
+
});
|
|
87
|
+
await this.commitAsync({ commitAllFiles: true, commitMessage: message, nonInteractive: false });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public override async commitAsync({
|
|
91
|
+
commitMessage,
|
|
92
|
+
commitAllFiles,
|
|
93
|
+
nonInteractive = false,
|
|
94
|
+
}: {
|
|
95
|
+
commitMessage: string;
|
|
96
|
+
commitAllFiles?: boolean;
|
|
97
|
+
nonInteractive: boolean;
|
|
98
|
+
}): Promise<void> {
|
|
99
|
+
await ensureGitConfiguredAsync({ nonInteractive });
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
if (commitAllFiles) {
|
|
103
|
+
await spawnAsync('git', ['add', '-A'], {
|
|
104
|
+
cwd: this.maybeCwdOverride,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
await spawnAsync('git', ['add', '-u'], {
|
|
108
|
+
cwd: this.maybeCwdOverride,
|
|
109
|
+
});
|
|
110
|
+
await spawnAsync('git', ['commit', '-m', commitMessage], {
|
|
111
|
+
cwd: this.maybeCwdOverride,
|
|
112
|
+
});
|
|
113
|
+
} catch (err: any) {
|
|
114
|
+
if (err?.stdout) {
|
|
115
|
+
Log.error(err.stdout);
|
|
116
|
+
}
|
|
117
|
+
if (err?.stderr) {
|
|
118
|
+
Log.error(err.stderr);
|
|
119
|
+
}
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public override async isCommitRequiredAsync(): Promise<boolean> {
|
|
125
|
+
return await this.hasUncommittedChangesAsync();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
public override async showChangedFilesAsync(): Promise<void> {
|
|
129
|
+
const gitStatusOutput = await gitStatusAsync({
|
|
130
|
+
showUntracked: true,
|
|
131
|
+
cwd: this.maybeCwdOverride,
|
|
132
|
+
});
|
|
133
|
+
Log.log(gitStatusOutput);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
public override async hasUncommittedChangesAsync(): Promise<boolean> {
|
|
137
|
+
const changes = await gitStatusAsync({ showUntracked: true, cwd: this.maybeCwdOverride });
|
|
138
|
+
return changes.length > 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public async getRootPathAsync(): Promise<string> {
|
|
142
|
+
return (
|
|
143
|
+
await spawnAsync('git', ['rev-parse', '--show-toplevel'], {
|
|
144
|
+
cwd: this.maybeCwdOverride,
|
|
145
|
+
})
|
|
146
|
+
).stdout.trim();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public async makeShallowCopyAsync(destinationPath: string): Promise<void> {
|
|
150
|
+
if (await this.hasUncommittedChangesAsync()) {
|
|
151
|
+
// it should already be checked before this function is called, but in case it wasn't
|
|
152
|
+
// we want to ensure that any changes were introduced by call to `setGitCaseSensitivityAsync`
|
|
153
|
+
throw new Error('You have some uncommitted changes in your repository.');
|
|
154
|
+
}
|
|
155
|
+
let gitRepoUri;
|
|
156
|
+
if (process.platform === 'win32') {
|
|
157
|
+
// getRootDirectoryAsync() will return C:/path/to/repo on Windows and path
|
|
158
|
+
// prefix should be file:///
|
|
159
|
+
gitRepoUri = `file:///${await this.getRootPathAsync()}`;
|
|
160
|
+
} else {
|
|
161
|
+
// getRootDirectoryAsync() will /path/to/repo, and path prefix should be
|
|
162
|
+
// file:/// so only file:// needs to be prepended
|
|
163
|
+
gitRepoUri = `file://${await this.getRootPathAsync()}`;
|
|
164
|
+
}
|
|
165
|
+
const isCaseSensitive = await isGitCaseSensitiveAsync(this.maybeCwdOverride);
|
|
166
|
+
await setGitCaseSensitivityAsync(true, this.maybeCwdOverride);
|
|
167
|
+
try {
|
|
168
|
+
if (await this.hasUncommittedChangesAsync()) {
|
|
169
|
+
Log.error('Detected inconsistent filename casing between your local filesystem and git.');
|
|
170
|
+
Log.error('This will likely cause your build to fail. Impacted files:');
|
|
171
|
+
await spawnAsync('git', ['status', '--short'], {
|
|
172
|
+
stdio: 'inherit',
|
|
173
|
+
cwd: this.maybeCwdOverride,
|
|
174
|
+
});
|
|
175
|
+
Log.newLine();
|
|
176
|
+
Log.error(
|
|
177
|
+
`Error: Resolve filename casing inconsistencies before proceeding. ${learnMore(
|
|
178
|
+
'https://expo.fyi/macos-ignorecase'
|
|
179
|
+
)}`
|
|
180
|
+
);
|
|
181
|
+
throw new Error('You have some uncommitted changes in your repository.');
|
|
182
|
+
}
|
|
183
|
+
await spawnAsync(
|
|
184
|
+
'git',
|
|
185
|
+
['clone', '--no-hardlinks', '--depth', '1', gitRepoUri, destinationPath],
|
|
186
|
+
{
|
|
187
|
+
cwd: this.maybeCwdOverride,
|
|
188
|
+
}
|
|
189
|
+
);
|
|
190
|
+
} finally {
|
|
191
|
+
await setGitCaseSensitivityAsync(isCaseSensitive, this.maybeCwdOverride);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
public override async getCommitHashAsync(): Promise<string | undefined> {
|
|
196
|
+
try {
|
|
197
|
+
return (
|
|
198
|
+
await spawnAsync('git', ['rev-parse', 'HEAD'], {
|
|
199
|
+
cwd: this.maybeCwdOverride,
|
|
200
|
+
})
|
|
201
|
+
).stdout.trim();
|
|
202
|
+
} catch {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
public override async trackFileAsync(file: string): Promise<void> {
|
|
208
|
+
await spawnAsync('git', ['add', '--intent-to-add', file], {
|
|
209
|
+
cwd: this.maybeCwdOverride,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
public override async getBranchNameAsync(): Promise<string | null> {
|
|
214
|
+
try {
|
|
215
|
+
return (
|
|
216
|
+
await spawnAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
217
|
+
cwd: this.maybeCwdOverride,
|
|
218
|
+
})
|
|
219
|
+
).stdout.trim();
|
|
220
|
+
} catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
public override async getLastCommitMessageAsync(): Promise<string | null> {
|
|
226
|
+
try {
|
|
227
|
+
return (
|
|
228
|
+
await spawnAsync('git', ['--no-pager', 'log', '-1', '--pretty=%B'], {
|
|
229
|
+
cwd: this.maybeCwdOverride,
|
|
230
|
+
})
|
|
231
|
+
).stdout.trim();
|
|
232
|
+
} catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
public override async showDiffAsync(): Promise<void> {
|
|
238
|
+
const outputTooLarge =
|
|
239
|
+
(await getGitDiffOutputAsync(this.maybeCwdOverride)).split(/\r\n|\r|\n/).length > 100;
|
|
240
|
+
await gitDiffAsync({ withPager: outputTooLarge, cwd: this.maybeCwdOverride });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
public async isFileUntrackedAsync(path: string): Promise<boolean> {
|
|
244
|
+
const withUntrackedFiles = await gitStatusAsync({
|
|
245
|
+
showUntracked: true,
|
|
246
|
+
cwd: this.maybeCwdOverride,
|
|
247
|
+
});
|
|
248
|
+
const trackedFiles = await gitStatusAsync({ showUntracked: false, cwd: this.maybeCwdOverride });
|
|
249
|
+
const pathWithoutLeadingDot = path.replace(/^\.\//, ''); // remove leading './' from path
|
|
250
|
+
return (
|
|
251
|
+
withUntrackedFiles.includes(pathWithoutLeadingDot) &&
|
|
252
|
+
!trackedFiles.includes(pathWithoutLeadingDot)
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
public override async isFileIgnoredAsync(filePath: string): Promise<boolean> {
|
|
257
|
+
try {
|
|
258
|
+
await spawnAsync('git', ['check-ignore', '-q', filePath], {
|
|
259
|
+
cwd: this.maybeCwdOverride ?? path.normalize(await this.getRootPathAsync()),
|
|
260
|
+
});
|
|
261
|
+
return true;
|
|
262
|
+
} catch {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
public override canGetLastCommitMessage(): boolean {
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function ensureGitConfiguredAsync({
|
|
273
|
+
nonInteractive,
|
|
274
|
+
}: {
|
|
275
|
+
nonInteractive: boolean;
|
|
276
|
+
}): Promise<void> {
|
|
277
|
+
let usernameConfigured = true;
|
|
278
|
+
let emailConfigured = true;
|
|
279
|
+
try {
|
|
280
|
+
await spawnAsync('git', ['config', '--get', 'user.name']);
|
|
281
|
+
} catch (err: any) {
|
|
282
|
+
Log.debug(err);
|
|
283
|
+
usernameConfigured = false;
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
await spawnAsync('git', ['config', '--get', 'user.email']);
|
|
287
|
+
} catch (err: any) {
|
|
288
|
+
Log.debug(err);
|
|
289
|
+
emailConfigured = false;
|
|
290
|
+
}
|
|
291
|
+
if (usernameConfigured && emailConfigured) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
Log.warn(
|
|
296
|
+
`You need to configure Git with your ${[
|
|
297
|
+
!usernameConfigured && 'username (user.name)',
|
|
298
|
+
!emailConfigured && 'email address (user.email)',
|
|
299
|
+
]
|
|
300
|
+
.filter(i => i)
|
|
301
|
+
.join(' and ')}`
|
|
302
|
+
);
|
|
303
|
+
if (nonInteractive) {
|
|
304
|
+
throw new Error('Git cannot be configured automatically in non-interactive mode');
|
|
305
|
+
}
|
|
306
|
+
if (!usernameConfigured) {
|
|
307
|
+
const { username } = await promptAsync({
|
|
308
|
+
type: 'text',
|
|
309
|
+
name: 'username',
|
|
310
|
+
message: 'Username:',
|
|
311
|
+
validate: (input: string) => input !== '',
|
|
312
|
+
});
|
|
313
|
+
const spinner = ora(
|
|
314
|
+
`Running ${chalk.bold(`git config --local user.name ${username}`)}`
|
|
315
|
+
).start();
|
|
316
|
+
try {
|
|
317
|
+
await spawnAsync('git', ['config', '--local', 'user.name', username]);
|
|
318
|
+
spinner.succeed();
|
|
319
|
+
} catch (err: any) {
|
|
320
|
+
spinner.fail();
|
|
321
|
+
throw err;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (!emailConfigured) {
|
|
325
|
+
const { email } = await promptAsync({
|
|
326
|
+
type: 'text',
|
|
327
|
+
name: 'email',
|
|
328
|
+
message: 'Email address:',
|
|
329
|
+
validate: (input: string) => input !== '',
|
|
330
|
+
});
|
|
331
|
+
const spinner = ora(`Running ${chalk.bold(`git config --local user.email ${email}`)}`).start();
|
|
332
|
+
try {
|
|
333
|
+
await spawnAsync('git', ['config', '--local', 'user.email', email]);
|
|
334
|
+
spinner.succeed();
|
|
335
|
+
} catch (err: any) {
|
|
336
|
+
spinner.fail();
|
|
337
|
+
throw err;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Checks if git is configured to be case sensitive
|
|
344
|
+
* @returns {boolean | undefined}
|
|
345
|
+
* - boolean - is git case sensitive
|
|
346
|
+
* - undefined - case sensitivity is not configured and git is using default behavior
|
|
347
|
+
*/
|
|
348
|
+
export async function isGitCaseSensitiveAsync(
|
|
349
|
+
cwd: string | undefined
|
|
350
|
+
): Promise<boolean | undefined> {
|
|
351
|
+
if (process.platform !== 'darwin') {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const result = await spawnAsync('git', ['config', '--get', 'core.ignorecase'], {
|
|
357
|
+
cwd,
|
|
358
|
+
});
|
|
359
|
+
const isIgnoreCaseEnabled = result.stdout.trim();
|
|
360
|
+
if (isIgnoreCaseEnabled === '') {
|
|
361
|
+
return undefined;
|
|
362
|
+
} else if (isIgnoreCaseEnabled === 'true') {
|
|
363
|
+
return false;
|
|
364
|
+
} else {
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
} catch {
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function setGitCaseSensitivityAsync(
|
|
373
|
+
enable: boolean | undefined,
|
|
374
|
+
cwd: string | undefined
|
|
375
|
+
): Promise<void> {
|
|
376
|
+
// we are assuming that if someone sets that on non-macos device then
|
|
377
|
+
// they know what they are doing
|
|
378
|
+
if (process.platform !== 'darwin') {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (enable === undefined) {
|
|
382
|
+
await spawnAsync('git', ['config', '--unset', 'core.ignorecase'], {
|
|
383
|
+
cwd,
|
|
384
|
+
});
|
|
385
|
+
} else {
|
|
386
|
+
await spawnAsync('git', ['config', 'core.ignorecase', String(!enable)], {
|
|
387
|
+
cwd,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import spawnAsync from '@expo/spawn-async';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
import GitClient from './git';
|
|
6
|
+
import Log from '../../log';
|
|
7
|
+
import { Ignore, makeShallowCopyAsync } from '../local';
|
|
8
|
+
|
|
9
|
+
export default class GitNoCommitClient extends GitClient {
|
|
10
|
+
public override async isCommitRequiredAsync(): Promise<boolean> {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public override async getRootPathAsync(): Promise<string> {
|
|
15
|
+
return (await spawnAsync('git', ['rev-parse', '--show-toplevel'])).stdout.trim();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public override async makeShallowCopyAsync(destinationPath: string): Promise<void> {
|
|
19
|
+
// normalize converts C:/some/path to C:\some\path on windows
|
|
20
|
+
const srcPath = path.normalize(await this.getRootPathAsync());
|
|
21
|
+
await makeShallowCopyAsync(srcPath, destinationPath);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public override async isFileIgnoredAsync(filePath: string): Promise<boolean> {
|
|
25
|
+
// normalize converts C:/some/path to C:\some\path on windows
|
|
26
|
+
const rootPath = path.normalize(await this.getRootPathAsync());
|
|
27
|
+
const ignore = new Ignore(rootPath);
|
|
28
|
+
await ignore.initIgnoreAsync();
|
|
29
|
+
return ignore.ignores(filePath);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public override async trackFileAsync(file: string): Promise<void> {
|
|
33
|
+
try {
|
|
34
|
+
await super.trackFileAsync(file);
|
|
35
|
+
} catch {
|
|
36
|
+
// In the no commit workflow it doesn't matter if we fail to track changes,
|
|
37
|
+
// so we can ignore if this throws an exception
|
|
38
|
+
Log.warn(
|
|
39
|
+
`Unable to track ${chalk.bold(path.basename(file))} in Git. Proceeding without tracking.`
|
|
40
|
+
);
|
|
41
|
+
Log.warn(` Reason: the command ${chalk.bold(`"git add ${file}"`)} exited with an error.`);
|
|
42
|
+
Log.newLine();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Ignore, getRootPath, makeShallowCopyAsync } from '../local';
|
|
2
|
+
import { Client } from '../vcs';
|
|
3
|
+
|
|
4
|
+
export default class NoVcsClient extends Client {
|
|
5
|
+
public async getRootPathAsync(): Promise<string> {
|
|
6
|
+
return getRootPath();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
public async makeShallowCopyAsync(destinationPath: string): Promise<void> {
|
|
10
|
+
const srcPath = getRootPath();
|
|
11
|
+
await makeShallowCopyAsync(srcPath, destinationPath);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public override async isFileIgnoredAsync(filePath: string): Promise<boolean> {
|
|
15
|
+
const ignore = new Ignore(getRootPath());
|
|
16
|
+
await ignore.initIgnoreAsync();
|
|
17
|
+
return ignore.ignores(filePath);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public override canGetLastCommitMessage(): boolean {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|