@gadgetinc/ggt 0.4.10 → 1.0.0
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/README.md +165 -93
- package/lib/__generated__/graphql.js +66 -1
- package/lib/__generated__/graphql.js.map +1 -1
- package/lib/commands/deploy.js +328 -230
- package/lib/commands/deploy.js.map +1 -1
- package/lib/commands/dev.js +445 -0
- package/lib/commands/dev.js.map +1 -0
- package/lib/commands/list.js +27 -19
- package/lib/commands/list.js.map +1 -1
- package/lib/commands/login.js +15 -11
- package/lib/commands/login.js.map +1 -1
- package/lib/commands/logout.js +5 -5
- package/lib/commands/logout.js.map +1 -1
- package/lib/commands/open.js +200 -0
- package/lib/commands/open.js.map +1 -0
- package/lib/commands/pull.js +128 -0
- package/lib/commands/pull.js.map +1 -0
- package/lib/commands/push.js +126 -0
- package/lib/commands/push.js.map +1 -0
- package/lib/commands/root.js +46 -28
- package/lib/commands/root.js.map +1 -1
- package/lib/commands/status.js +61 -0
- package/lib/commands/status.js.map +1 -0
- package/lib/commands/version.js +6 -6
- package/lib/commands/version.js.map +1 -1
- package/lib/commands/whoami.js +6 -6
- package/lib/commands/whoami.js.map +1 -1
- package/lib/ggt.js +33 -8
- package/lib/ggt.js.map +1 -1
- package/lib/main.js +5 -0
- package/lib/main.js.map +1 -0
- package/lib/services/app/api/api.js +191 -0
- package/lib/services/app/api/api.js.map +1 -0
- package/lib/services/app/api/operation.js +12 -0
- package/lib/services/app/api/operation.js.map +1 -0
- package/lib/services/app/app.js +44 -10
- package/lib/services/app/app.js.map +1 -1
- package/lib/services/app/{edit/client.js → client.js} +29 -19
- package/lib/services/app/client.js.map +1 -0
- package/lib/services/app/edit/edit.js +67 -31
- package/lib/services/app/edit/edit.js.map +1 -1
- package/lib/services/app/edit/operation.js +4 -3
- package/lib/services/app/edit/operation.js.map +1 -1
- package/lib/services/app/{edit/error.js → error.js} +6 -6
- package/lib/services/app/error.js.map +1 -0
- package/lib/services/command/arg.js +4 -4
- package/lib/services/command/arg.js.map +1 -1
- package/lib/services/command/command.js +9 -7
- package/lib/services/command/command.js.map +1 -1
- package/lib/services/command/context.js +82 -20
- package/lib/services/command/context.js.map +1 -1
- package/lib/services/config/config.js +4 -7
- package/lib/services/config/config.js.map +1 -1
- package/lib/services/config/env.js +1 -1
- package/lib/services/config/env.js.map +1 -1
- package/lib/services/filesync/changes.js +76 -37
- package/lib/services/filesync/changes.js.map +1 -1
- package/lib/services/filesync/conflicts.js +10 -9
- package/lib/services/filesync/conflicts.js.map +1 -1
- package/lib/services/filesync/directory.js +16 -1
- package/lib/services/filesync/directory.js.map +1 -1
- package/lib/services/filesync/error.js +96 -27
- package/lib/services/filesync/error.js.map +1 -1
- package/lib/services/filesync/filesync.js +448 -490
- package/lib/services/filesync/filesync.js.map +1 -1
- package/lib/services/filesync/hashes.js +8 -5
- package/lib/services/filesync/hashes.js.map +1 -1
- package/lib/services/filesync/strategy.js +59 -0
- package/lib/services/filesync/strategy.js.map +1 -0
- package/lib/services/filesync/sync-json.js +475 -0
- package/lib/services/filesync/sync-json.js.map +1 -0
- package/lib/services/http/auth.js +30 -1
- package/lib/services/http/auth.js.map +1 -1
- package/lib/services/http/http.js +5 -0
- package/lib/services/http/http.js.map +1 -1
- package/lib/services/output/confirm.js +149 -0
- package/lib/services/output/confirm.js.map +1 -0
- package/lib/services/output/footer.js +22 -0
- package/lib/services/output/footer.js.map +1 -0
- package/lib/services/output/log/format/pretty.js +2 -1
- package/lib/services/output/log/format/pretty.js.map +1 -1
- package/lib/services/output/log/logger.js +13 -5
- package/lib/services/output/log/logger.js.map +1 -1
- package/lib/services/output/log/structured.js +2 -2
- package/lib/services/output/log/structured.js.map +1 -1
- package/lib/services/output/output.js +197 -0
- package/lib/services/output/output.js.map +1 -0
- package/lib/services/output/print.js +31 -0
- package/lib/services/output/print.js.map +1 -0
- package/lib/services/output/problems.js +84 -0
- package/lib/services/output/problems.js.map +1 -0
- package/lib/services/output/prompt.js +173 -40
- package/lib/services/output/prompt.js.map +1 -1
- package/lib/services/output/report.js +63 -19
- package/lib/services/output/report.js.map +1 -1
- package/lib/services/output/select.js +198 -0
- package/lib/services/output/select.js.map +1 -0
- package/lib/services/output/spinner.js +141 -0
- package/lib/services/output/spinner.js.map +1 -0
- package/lib/services/output/sprint.js +38 -15
- package/lib/services/output/sprint.js.map +1 -1
- package/lib/services/output/symbols.js +23 -0
- package/lib/services/output/symbols.js.map +1 -0
- package/lib/services/output/table.js +98 -0
- package/lib/services/output/table.js.map +1 -0
- package/lib/services/output/timestamp.js +12 -0
- package/lib/services/output/timestamp.js.map +1 -0
- package/lib/services/output/update.js +29 -9
- package/lib/services/output/update.js.map +1 -1
- package/lib/services/user/session.js +4 -0
- package/lib/services/user/session.js.map +1 -1
- package/lib/services/user/user.js +15 -10
- package/lib/services/user/user.js.map +1 -1
- package/lib/services/util/assert.js +11 -0
- package/lib/services/util/assert.js.map +1 -0
- package/lib/services/util/boolean.js +2 -2
- package/lib/services/util/boolean.js.map +1 -1
- package/lib/services/util/function.js +45 -7
- package/lib/services/util/function.js.map +1 -1
- package/lib/services/util/is.js +23 -2
- package/lib/services/util/is.js.map +1 -1
- package/lib/services/util/json.js +16 -13
- package/lib/services/util/json.js.map +1 -1
- package/lib/services/util/object.js +2 -2
- package/lib/services/util/object.js.map +1 -1
- package/lib/services/util/promise.js +5 -2
- package/lib/services/util/promise.js.map +1 -1
- package/lib/services/util/types.js.map +1 -1
- package/npm-shrinkwrap.json +3415 -2973
- package/package.json +47 -40
- package/bin/dev.cmd +0 -3
- package/bin/dev.js +0 -14
- package/bin/run.cmd +0 -3
- package/bin/run.js +0 -5
- package/lib/commands/sync.js +0 -284
- package/lib/commands/sync.js.map +0 -1
- package/lib/services/app/edit/client.js.map +0 -1
- package/lib/services/app/edit/error.js.map +0 -1
- package/lib/services/output/log/printer.js +0 -120
- package/lib/services/output/log/printer.js.map +0 -1
- package/lib/services/output/stream.js +0 -54
- package/lib/services/output/stream.js.map +0 -1
|
@@ -1,168 +1,183 @@
|
|
|
1
1
|
import { _ as _define_property } from "@swc/helpers/_/_define_property";
|
|
2
|
-
import dayjs from "dayjs";
|
|
3
2
|
import { execa } from "execa";
|
|
4
|
-
import { findUp } from "find-up";
|
|
5
3
|
import fs from "fs-extra";
|
|
6
4
|
import ms from "ms";
|
|
7
5
|
import assert from "node:assert";
|
|
8
|
-
import path from "node:path";
|
|
9
6
|
import process from "node:process";
|
|
10
7
|
import pMap from "p-map";
|
|
11
8
|
import PQueue from "p-queue";
|
|
12
9
|
import pRetry from "p-retry";
|
|
13
|
-
import
|
|
10
|
+
import pluralize from "pluralize";
|
|
14
11
|
import { FileSyncEncoding } from "../../__generated__/graphql.js";
|
|
15
|
-
import { getApps } from "../app/app.js";
|
|
16
|
-
import { AppArg } from "../app/arg.js";
|
|
17
|
-
import { Edit } from "../app/edit/edit.js";
|
|
18
|
-
import { EditError } from "../app/edit/error.js";
|
|
19
12
|
import { FILE_SYNC_COMPARISON_HASHES_QUERY, FILE_SYNC_FILES_QUERY, FILE_SYNC_HASHES_QUERY, PUBLISH_FILE_SYNC_EVENTS_MUTATION, REMOTE_FILE_SYNC_EVENTS_SUBSCRIPTION } from "../app/edit/operation.js";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
13
|
+
import { config } from "../config/config.js";
|
|
14
|
+
import { confirm } from "../output/confirm.js";
|
|
15
|
+
import { println } from "../output/print.js";
|
|
16
|
+
import { filesyncProblemsToProblems, sprintProblems } from "../output/problems.js";
|
|
17
|
+
import { EdgeCaseError } from "../output/report.js";
|
|
18
|
+
import { select } from "../output/select.js";
|
|
19
|
+
import { spin } from "../output/spinner.js";
|
|
20
|
+
import { sprint, sprintln } from "../output/sprint.js";
|
|
21
|
+
import { symbol } from "../output/symbols.js";
|
|
22
|
+
import { ts } from "../output/timestamp.js";
|
|
26
23
|
import { noop } from "../util/function.js";
|
|
27
|
-
import {
|
|
28
|
-
import { Changes, printChanges } from "./changes.js";
|
|
24
|
+
import { serializeError } from "../util/object.js";
|
|
25
|
+
import { Changes, printChanges, sprintChanges } from "./changes.js";
|
|
29
26
|
import { getConflicts, printConflicts, withoutConflictingChanges } from "./conflicts.js";
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
27
|
+
import { supportsPermissions, swallowEnoent } from "./directory.js";
|
|
28
|
+
import { TooManyMergeAttemptsError, isFilesVersionMismatchError, swallowFilesVersionMismatch } from "./error.js";
|
|
29
|
+
import { getNecessaryChanges, isEqualHashes } from "./hashes.js";
|
|
30
|
+
import { MergeConflictPreference } from "./strategy.js";
|
|
31
|
+
/**
|
|
32
|
+
* The maximum attempts to automatically merge local and environment
|
|
33
|
+
* file changes when a FilesVersionMismatchError is encountered before
|
|
34
|
+
* throwing a {@linkcode TooManyMergeAttemptsError}.
|
|
35
|
+
*/ export const MAX_MERGE_ATTEMPTS = 10;
|
|
36
|
+
/**
|
|
37
|
+
* The maximum length of file content that can be pushed to Gadget in a
|
|
38
|
+
* single request.
|
|
39
|
+
*/ export const MAX_PUSH_CONTENT_LENGTH = 50 * 1024 * 1024; // 50mb
|
|
33
40
|
export class FileSync {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
41
|
+
async hashes(ctx) {
|
|
42
|
+
const spinner = spin({
|
|
43
|
+
ensureEmptyLineAbove: true
|
|
44
|
+
})`
|
|
45
|
+
Calculating file changes.
|
|
46
|
+
`;
|
|
47
|
+
try {
|
|
48
|
+
const [localHashes, { localFilesVersionHashes, environmentHashes, environmentFilesVersion }] = await Promise.all([
|
|
49
|
+
// get the hashes of our local files
|
|
50
|
+
this.syncJson.directory.hashes(),
|
|
51
|
+
// get the hashes of our local filesVersion and the latest filesVersion
|
|
52
|
+
(async ()=>{
|
|
53
|
+
let localFilesVersionHashes;
|
|
54
|
+
let environmentHashes;
|
|
55
|
+
let environmentFilesVersion;
|
|
56
|
+
if (this.syncJson.filesVersion === 0n) {
|
|
57
|
+
// we're either syncing for the first time or we're syncing a
|
|
58
|
+
// non-empty directory without a `.gadget/sync.json` file,
|
|
59
|
+
// regardless get the hashes of the latest filesVersion
|
|
60
|
+
const { fileSyncHashes } = await this.syncJson.edit.query({
|
|
61
|
+
query: FILE_SYNC_HASHES_QUERY
|
|
62
|
+
});
|
|
63
|
+
environmentFilesVersion = BigInt(fileSyncHashes.filesVersion);
|
|
64
|
+
environmentHashes = fileSyncHashes.hashes;
|
|
65
|
+
localFilesVersionHashes = {}; // represents an empty directory
|
|
66
|
+
} else {
|
|
67
|
+
// this isn't the first time we're syncing, so get the
|
|
68
|
+
// hashes of the files at our local filesVersion and the
|
|
69
|
+
// latest filesVersion
|
|
70
|
+
const { fileSyncComparisonHashes } = await this.syncJson.edit.query({
|
|
71
|
+
query: FILE_SYNC_COMPARISON_HASHES_QUERY,
|
|
72
|
+
variables: {
|
|
73
|
+
filesVersion: String(this.syncJson.filesVersion)
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
localFilesVersionHashes = fileSyncComparisonHashes.filesVersionHashes.hashes;
|
|
77
|
+
environmentHashes = fileSyncComparisonHashes.latestFilesVersionHashes.hashes;
|
|
78
|
+
environmentFilesVersion = BigInt(fileSyncComparisonHashes.latestFilesVersionHashes.filesVersion);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
localFilesVersionHashes,
|
|
82
|
+
environmentHashes,
|
|
83
|
+
environmentFilesVersion
|
|
84
|
+
};
|
|
85
|
+
})()
|
|
86
|
+
]);
|
|
87
|
+
const inSync = isEqualHashes(ctx, localHashes, environmentHashes);
|
|
88
|
+
const localChanges = getNecessaryChanges(ctx, {
|
|
89
|
+
from: localFilesVersionHashes,
|
|
90
|
+
to: localHashes,
|
|
91
|
+
existing: environmentHashes,
|
|
92
|
+
ignore: [
|
|
93
|
+
".gadget/"
|
|
94
|
+
]
|
|
95
|
+
});
|
|
96
|
+
let environmentChanges = getNecessaryChanges(ctx, {
|
|
97
|
+
from: localFilesVersionHashes,
|
|
98
|
+
to: environmentHashes,
|
|
99
|
+
existing: localHashes
|
|
100
|
+
});
|
|
101
|
+
if (!inSync && localChanges.size === 0 && environmentChanges.size === 0) {
|
|
102
|
+
// we're not in sync, but neither the local filesystem nor the
|
|
103
|
+
// environment's filesystem have any changes; this is only
|
|
104
|
+
// possible if the local filesystem has modified .gadget/ files
|
|
105
|
+
environmentChanges = getNecessaryChanges(ctx, {
|
|
106
|
+
from: localHashes,
|
|
107
|
+
to: environmentHashes
|
|
108
|
+
});
|
|
109
|
+
assert(environmentChanges.size > 0, "expected environmentChanges to have changes");
|
|
110
|
+
assert(Array.from(environmentChanges.keys()).every((path)=>path.startsWith(".gadget/")), "expected all environmentChanges to be .gadget/ files");
|
|
111
|
+
}
|
|
112
|
+
assert(inSync || localChanges.size > 0 || environmentChanges.size > 0, "there must be changes if hashes don't match");
|
|
113
|
+
const localChangesToPush = getNecessaryChanges(ctx, {
|
|
114
|
+
from: environmentHashes,
|
|
115
|
+
to: localHashes,
|
|
116
|
+
ignore: [
|
|
117
|
+
".gadget/"
|
|
118
|
+
]
|
|
119
|
+
});
|
|
120
|
+
const environmentChangesToPull = getNecessaryChanges(ctx, {
|
|
121
|
+
from: localHashes,
|
|
122
|
+
to: environmentHashes
|
|
123
|
+
});
|
|
124
|
+
const onlyDotGadgetFilesChanged = Array.from(environmentChangesToPull.keys()).every((filepath)=>filepath.startsWith(".gadget/"));
|
|
125
|
+
const bothChanged = localChanges.size > 0 && environmentChanges.size > 0 && !onlyDotGadgetFilesChanged;
|
|
126
|
+
if (inSync) {
|
|
127
|
+
spinner.succeed`Your files are up to date. ${ts()}`;
|
|
76
128
|
} else {
|
|
77
|
-
|
|
78
|
-
dir = process.cwd();
|
|
129
|
+
spinner.succeed`Calculated file changes. ${ts()}`;
|
|
79
130
|
}
|
|
131
|
+
return {
|
|
132
|
+
inSync,
|
|
133
|
+
localFilesVersionHashes,
|
|
134
|
+
localHashes,
|
|
135
|
+
localChanges,
|
|
136
|
+
localChangesToPush,
|
|
137
|
+
environmentHashes,
|
|
138
|
+
environmentChanges,
|
|
139
|
+
environmentChangesToPull,
|
|
140
|
+
environmentFilesVersion,
|
|
141
|
+
onlyDotGadgetFilesChanged,
|
|
142
|
+
bothChanged
|
|
143
|
+
};
|
|
144
|
+
} catch (error) {
|
|
145
|
+
spinner.fail();
|
|
146
|
+
throw error;
|
|
80
147
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
148
|
+
}
|
|
149
|
+
async print(ctx, { hashes } = {}) {
|
|
150
|
+
const { inSync, localChanges, environmentChanges, onlyDotGadgetFilesChanged, bothChanged } = hashes ?? await this.hashes(ctx);
|
|
151
|
+
if (inSync) {
|
|
152
|
+
// the spinner in hashes will have already printed that we're in sync
|
|
153
|
+
return;
|
|
84
154
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
app: z.string(),
|
|
91
|
-
filesVersion: z.string(),
|
|
92
|
-
mtime: z.number()
|
|
93
|
-
}).parse(json)).catch(noop);
|
|
94
|
-
let appSlug = ctx.args["--app"] || state?.app;
|
|
95
|
-
if (!appSlug) {
|
|
96
|
-
// the user didn't specify an app, suggest some apps that they can sync to
|
|
97
|
-
appSlug = await select(ctx, {
|
|
98
|
-
message: "Select the app to sync to",
|
|
99
|
-
choices: apps.map((x)=>x.slug)
|
|
155
|
+
if (localChanges.size > 0) {
|
|
156
|
+
printChanges(ctx, {
|
|
157
|
+
changes: localChanges,
|
|
158
|
+
tense: "past",
|
|
159
|
+
title: sprint`Your local files {underline have} changed.`
|
|
100
160
|
});
|
|
161
|
+
} else {
|
|
162
|
+
println({
|
|
163
|
+
ensureEmptyLineAbove: true
|
|
164
|
+
})`
|
|
165
|
+
Your local files {underline have not} changed.
|
|
166
|
+
`;
|
|
101
167
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
// anymore, suggest some apps that are similar to the one they
|
|
108
|
-
// specified
|
|
109
|
-
const similarAppSlugs = sortBySimilar(appSlug, apps.map((app)=>app.slug)).slice(0, 5);
|
|
110
|
-
throw new ArgError(sprint`
|
|
111
|
-
Unknown application:
|
|
112
|
-
|
|
113
|
-
${appSlug}
|
|
114
|
-
|
|
115
|
-
Did you mean one of these?
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
`.concat(` • ${similarAppSlugs.join("\n • ")}`));
|
|
119
|
-
}
|
|
120
|
-
ctx.app = app;
|
|
121
|
-
const directory = await Directory.init(dir);
|
|
122
|
-
if (!state) {
|
|
123
|
-
// the .gadget/sync.json file didn't exist or contained invalid json
|
|
124
|
-
if (wasEmptyOrNonExistent || ctx.args["--force"]) {
|
|
125
|
-
// the directory was empty or the user passed --force
|
|
126
|
-
// either way, create a fresh .gadget/sync.json file
|
|
127
|
-
return new FileSync(ctx, directory, app, {
|
|
128
|
-
app: app.slug,
|
|
129
|
-
filesVersion: "0",
|
|
130
|
-
mtime: 0
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
// the directory isn't empty and the user didn't pass --force
|
|
134
|
-
throw new InvalidSyncFileError(dir, app.slug);
|
|
135
|
-
}
|
|
136
|
-
// the .gadget/sync.json file exists
|
|
137
|
-
if (state.app === app.slug) {
|
|
138
|
-
// the .gadget/sync.json file is for the same app that the user specified
|
|
139
|
-
return new FileSync(ctx, directory, app, state);
|
|
140
|
-
}
|
|
141
|
-
// the .gadget/sync.json file is for a different app
|
|
142
|
-
if (ctx.args["--force"]) {
|
|
143
|
-
// the user passed --force, so use the app they specified and overwrite everything
|
|
144
|
-
return new FileSync(ctx, directory, app, {
|
|
145
|
-
app: app.slug,
|
|
146
|
-
filesVersion: "0",
|
|
147
|
-
mtime: 0
|
|
168
|
+
if (environmentChanges.size > 0 && !onlyDotGadgetFilesChanged) {
|
|
169
|
+
printChanges(ctx, {
|
|
170
|
+
changes: environmentChanges,
|
|
171
|
+
tense: "past",
|
|
172
|
+
title: sprint`Your environment's files {underline have}${bothChanged ? " also" : ""} changed.`
|
|
148
173
|
});
|
|
174
|
+
} else {
|
|
175
|
+
println({
|
|
176
|
+
ensureEmptyLineAbove: true
|
|
177
|
+
})`
|
|
178
|
+
Your environment's files {underline have not} changed.
|
|
179
|
+
`;
|
|
149
180
|
}
|
|
150
|
-
// the user didn't pass --force, so throw an error
|
|
151
|
-
throw new ArgError(sprint`
|
|
152
|
-
You were about to sync the following app to the following directory:
|
|
153
|
-
|
|
154
|
-
{dim ${app.slug}} → {dim ${dir}}
|
|
155
|
-
|
|
156
|
-
However, that directory has already been synced with this app:
|
|
157
|
-
|
|
158
|
-
{dim ${state.app}}
|
|
159
|
-
|
|
160
|
-
If you're sure that you want to sync:
|
|
161
|
-
|
|
162
|
-
{dim ${app.slug}} → {dim ${dir}}
|
|
163
|
-
|
|
164
|
-
Then run {dim ggt sync} again with the {dim --force} flag.
|
|
165
|
-
`);
|
|
166
181
|
}
|
|
167
182
|
/**
|
|
168
183
|
* Waits for all pending and ongoing filesync operations to complete.
|
|
@@ -170,23 +185,31 @@ export class FileSync {
|
|
|
170
185
|
await this._syncOperations.onIdle();
|
|
171
186
|
}
|
|
172
187
|
/**
|
|
173
|
-
*
|
|
188
|
+
* Attempts to send file changes to the Gadget. If a files version
|
|
189
|
+
* mismatch error occurs, this function will merge the changes with
|
|
190
|
+
* Gadget instead.
|
|
174
191
|
*
|
|
192
|
+
* @param ctx - The context to use.
|
|
175
193
|
* @param options - The options to use.
|
|
176
194
|
* @param options.changes - The changes to send.
|
|
195
|
+
* @param options.printLocalChangesOptions - The options to use when printing the local changes.
|
|
196
|
+
* @param options.printEnvironmentChangesOptions - The options to use when printing the changes from Gadget.
|
|
177
197
|
* @returns A promise that resolves when the changes have been sent.
|
|
178
|
-
*/ async
|
|
198
|
+
*/ async mergeChangesWithEnvironment(ctx, { changes, printLocalChangesOptions, printEnvironmentChangesOptions }) {
|
|
179
199
|
await this._syncOperations.add(async ()=>{
|
|
180
200
|
try {
|
|
181
|
-
await this.
|
|
182
|
-
changes
|
|
201
|
+
await this._sendChangesToEnvironment(ctx, {
|
|
202
|
+
changes,
|
|
203
|
+
printLocalChangesOptions
|
|
183
204
|
});
|
|
184
205
|
} catch (error) {
|
|
185
|
-
swallowFilesVersionMismatch(
|
|
206
|
+
swallowFilesVersionMismatch(ctx, error);
|
|
186
207
|
// we either sent the wrong expectedFilesVersion or we received
|
|
187
208
|
// a filesVersion that is greater than the expectedFilesVersion
|
|
188
209
|
// + 1, so we need to stop what we're doing and get in sync
|
|
189
|
-
await this.
|
|
210
|
+
await this.merge(ctx, {
|
|
211
|
+
printEnvironmentChangesOptions
|
|
212
|
+
});
|
|
190
213
|
}
|
|
191
214
|
});
|
|
192
215
|
}
|
|
@@ -194,13 +217,15 @@ export class FileSync {
|
|
|
194
217
|
* Subscribes to file changes on Gadget and executes the provided
|
|
195
218
|
* callbacks before and after the changes occur.
|
|
196
219
|
*
|
|
220
|
+
* @param ctx - The context to use.
|
|
197
221
|
* @param options - The options to use.
|
|
198
222
|
* @param options.beforeChanges - A callback that is called before the changes occur.
|
|
199
223
|
* @param options.afterChanges - A callback that is called after the changes occur.
|
|
200
224
|
* @param options.onError - A callback that is called if an error occurs.
|
|
225
|
+
* @param options.printEnvironmentChangesOptions - The options to use when printing the changes from Gadget.
|
|
201
226
|
* @returns A function that unsubscribes from changes on Gadget.
|
|
202
|
-
*/
|
|
203
|
-
return this.edit.subscribe({
|
|
227
|
+
*/ subscribeToEnvironmentChanges(ctx, { beforeChanges = noop, printEnvironmentChangesOptions, afterChanges = noop, onError }) {
|
|
228
|
+
return this.syncJson.edit.subscribe({
|
|
204
229
|
subscription: REMOTE_FILE_SYNC_EVENTS_SUBSCRIPTION,
|
|
205
230
|
// the reason this is a function rather than a static value is
|
|
206
231
|
// so that it will be re-evaluated if the connection is lost and
|
|
@@ -208,26 +233,26 @@ export class FileSync {
|
|
|
208
233
|
// filesVersion rather than the one that was sent when we first
|
|
209
234
|
// subscribed
|
|
210
235
|
variables: ()=>({
|
|
211
|
-
localFilesVersion: String(this.filesVersion)
|
|
236
|
+
localFilesVersion: String(this.syncJson.filesVersion)
|
|
212
237
|
}),
|
|
213
238
|
onError,
|
|
214
239
|
onData: ({ remoteFileSyncEvents: { changed, deleted, remoteFilesVersion } })=>{
|
|
215
240
|
this._syncOperations.add(async ()=>{
|
|
216
|
-
if (BigInt(remoteFilesVersion) < this.filesVersion) {
|
|
217
|
-
|
|
241
|
+
if (BigInt(remoteFilesVersion) < this.syncJson.filesVersion) {
|
|
242
|
+
ctx.log.warn("skipping received changes because files version is outdated", {
|
|
218
243
|
filesVersion: remoteFilesVersion
|
|
219
244
|
});
|
|
220
245
|
return;
|
|
221
246
|
}
|
|
222
|
-
|
|
247
|
+
ctx.log.debug("received files", {
|
|
223
248
|
remoteFilesVersion,
|
|
224
249
|
changed: changed.map((change)=>change.path),
|
|
225
250
|
deleted: deleted.map((change)=>change.path)
|
|
226
251
|
});
|
|
227
252
|
const filterIgnoredFiles = (file)=>{
|
|
228
|
-
const ignored = this.directory.ignores(file.path);
|
|
253
|
+
const ignored = this.syncJson.directory.ignores(file.path);
|
|
229
254
|
if (ignored) {
|
|
230
|
-
|
|
255
|
+
ctx.log.warn("skipping received change because file is ignored", {
|
|
231
256
|
path: file.path
|
|
232
257
|
});
|
|
233
258
|
}
|
|
@@ -236,26 +261,25 @@ export class FileSync {
|
|
|
236
261
|
changed = changed.filter(filterIgnoredFiles);
|
|
237
262
|
deleted = deleted.filter(filterIgnoredFiles);
|
|
238
263
|
if (changed.length === 0 && deleted.length === 0) {
|
|
239
|
-
await this.
|
|
264
|
+
await this.syncJson.save(remoteFilesVersion);
|
|
240
265
|
return;
|
|
241
266
|
}
|
|
242
267
|
await beforeChanges({
|
|
243
268
|
changed: changed.map((file)=>file.path),
|
|
244
269
|
deleted: deleted.map((file)=>file.path)
|
|
245
270
|
});
|
|
246
|
-
const changes = await this._writeToLocalFilesystem({
|
|
271
|
+
const changes = await this._writeToLocalFilesystem(ctx, {
|
|
247
272
|
filesVersion: remoteFilesVersion,
|
|
248
273
|
files: changed,
|
|
249
|
-
delete: deleted.map((file)=>file.path)
|
|
250
|
-
|
|
251
|
-
if (changes.size > 0) {
|
|
252
|
-
printChanges(this.ctx, {
|
|
253
|
-
message: sprint`← Received {gray ${dayjs().format("hh:mm:ss A")}}`,
|
|
254
|
-
changes,
|
|
274
|
+
delete: deleted.map((file)=>file.path),
|
|
275
|
+
printEnvironmentChangesOptions: {
|
|
255
276
|
tense: "past",
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
277
|
+
ensureEmptyLineAbove: true,
|
|
278
|
+
title: sprintln`{green ${symbol.tick}} Pulled ${pluralize("file", changed.length + deleted.length)}. ${symbol.arrowLeft} ${ts()}`,
|
|
279
|
+
limit: 5,
|
|
280
|
+
...printEnvironmentChangesOptions
|
|
281
|
+
}
|
|
282
|
+
});
|
|
259
283
|
await afterChanges({
|
|
260
284
|
changes
|
|
261
285
|
});
|
|
@@ -268,95 +292,131 @@ export class FileSync {
|
|
|
268
292
|
* - All non-conflicting changes are automatically merged.
|
|
269
293
|
* - Conflicts are resolved by prompting the user to either keep their local changes or keep Gadget's changes.
|
|
270
294
|
* - This function will not return until the filesystem is in sync.
|
|
271
|
-
*/ async
|
|
295
|
+
*/ async merge(ctx, { hashes, maxAttempts = 10, printLocalChangesOptions, printEnvironmentChangesOptions } = {}) {
|
|
272
296
|
let attempt = 0;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
297
|
+
do {
|
|
298
|
+
if (attempt === 0) {
|
|
299
|
+
hashes ??= await this.hashes(ctx);
|
|
300
|
+
} else {
|
|
301
|
+
hashes = await this.hashes(ctx);
|
|
302
|
+
}
|
|
303
|
+
if (hashes.inSync) {
|
|
277
304
|
this._syncOperations.clear();
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
});
|
|
281
|
-
await this._save(hashes.gadgetFilesVersion);
|
|
305
|
+
ctx.log.info("filesystem in sync");
|
|
306
|
+
await this.syncJson.save(hashes.environmentFilesVersion);
|
|
282
307
|
return;
|
|
283
308
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
309
|
+
attempt += 1;
|
|
310
|
+
ctx.log.info("merging", {
|
|
311
|
+
attempt,
|
|
312
|
+
...hashes
|
|
313
|
+
});
|
|
287
314
|
try {
|
|
288
|
-
this.ctx
|
|
289
|
-
|
|
290
|
-
|
|
315
|
+
await this._merge(ctx, {
|
|
316
|
+
hashes,
|
|
317
|
+
printLocalChangesOptions,
|
|
318
|
+
printEnvironmentChangesOptions
|
|
291
319
|
});
|
|
292
|
-
await this._sync(hashes);
|
|
293
320
|
} catch (error) {
|
|
294
|
-
swallowFilesVersionMismatch(
|
|
321
|
+
swallowFilesVersionMismatch(ctx, error);
|
|
295
322
|
// we either sent the wrong expectedFilesVersion or we received
|
|
296
323
|
// a filesVersion that is greater than the expectedFilesVersion
|
|
297
324
|
// + 1, so try again
|
|
298
325
|
}
|
|
299
|
-
}
|
|
326
|
+
}while (attempt < maxAttempts)
|
|
327
|
+
throw new TooManyMergeAttemptsError(maxAttempts);
|
|
300
328
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
329
|
+
/**
|
|
330
|
+
* Pushes any changes made to the local filesystem since the last sync
|
|
331
|
+
* to Gadget.
|
|
332
|
+
*
|
|
333
|
+
* If Gadget has also made changes since the last sync, and --force
|
|
334
|
+
* was not passed, the user will be prompted to discard them.
|
|
335
|
+
*/ async push(ctx, { hashes, force, printLocalChangesOptions } = {}) {
|
|
336
|
+
const { localChangesToPush, environmentChanges, environmentFilesVersion, onlyDotGadgetFilesChanged } = hashes ?? await this.hashes(ctx);
|
|
337
|
+
assert(localChangesToPush.size > 0, "cannot push if there are no changes");
|
|
338
|
+
// TODO: lift this check up to the push command
|
|
339
|
+
if (// they didn't pass --force
|
|
340
|
+
!(force ?? ctx.args["--force"]) && // their environment's files have changed
|
|
341
|
+
environmentChanges.size > 0 && // some of the changes aren't .gadget/ files
|
|
342
|
+
!onlyDotGadgetFilesChanged) {
|
|
343
|
+
await confirm({
|
|
344
|
+
ensureEmptyLineAbove: true
|
|
345
|
+
})`
|
|
346
|
+
Are you sure you want to {underline discard} your environment's changes?
|
|
347
|
+
`;
|
|
348
|
+
}
|
|
349
|
+
try {
|
|
350
|
+
await this._sendChangesToEnvironment(ctx, {
|
|
351
|
+
// what changes need to be made to your local files to make
|
|
352
|
+
// them match the environment's files
|
|
353
|
+
changes: localChangesToPush,
|
|
354
|
+
expectedFilesVersion: environmentFilesVersion,
|
|
355
|
+
printLocalChangesOptions
|
|
323
356
|
});
|
|
357
|
+
} catch (error) {
|
|
358
|
+
swallowFilesVersionMismatch(ctx, error);
|
|
359
|
+
// we were told to push their local changes, but their
|
|
360
|
+
// environment's files have changed since we last checked, so
|
|
361
|
+
// throw a nicer error message
|
|
362
|
+
// TODO: we don't have to do this if only .gadget/ files changed
|
|
363
|
+
throw new EdgeCaseError(sprint`
|
|
364
|
+
Your environment's files have changed since we last checked.
|
|
365
|
+
|
|
366
|
+
Please re-run "ggt ${ctx.command}" to see the changes and try again.
|
|
367
|
+
`);
|
|
324
368
|
}
|
|
325
|
-
|
|
369
|
+
}
|
|
370
|
+
async pull(ctx, { hashes, force, printEnvironmentChangesOptions } = {}) {
|
|
371
|
+
const { localChanges, environmentChangesToPull, environmentFilesVersion } = hashes ?? await this.hashes(ctx);
|
|
372
|
+
assert(environmentChangesToPull.size > 0, "cannot push if there are no changes");
|
|
373
|
+
// TODO: lift this check up to the pull command
|
|
374
|
+
if (localChanges.size > 0 && !(force ?? ctx.args["--force"])) {
|
|
375
|
+
await confirm`
|
|
376
|
+
Are you sure you want to {underline discard} your local changes?
|
|
377
|
+
`;
|
|
378
|
+
}
|
|
379
|
+
await this._getChangesFromEnvironment(ctx, {
|
|
380
|
+
changes: environmentChangesToPull,
|
|
381
|
+
filesVersion: environmentFilesVersion,
|
|
382
|
+
printEnvironmentChangesOptions
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
async _merge(ctx, { hashes: { localChanges, environmentChanges, environmentFilesVersion }, printLocalChangesOptions, printEnvironmentChangesOptions }) {
|
|
326
386
|
const conflicts = getConflicts({
|
|
327
387
|
localChanges,
|
|
328
|
-
|
|
388
|
+
environmentChanges
|
|
329
389
|
});
|
|
330
390
|
if (conflicts.size > 0) {
|
|
331
|
-
|
|
391
|
+
ctx.log.debug("conflicts detected", {
|
|
332
392
|
conflicts
|
|
333
393
|
});
|
|
334
|
-
let preference =
|
|
394
|
+
let preference = ctx.args["--prefer"];
|
|
335
395
|
if (!preference) {
|
|
336
|
-
printConflicts(
|
|
337
|
-
message: sprint`{bold You have conflicting changes with Gadget}`,
|
|
396
|
+
printConflicts({
|
|
338
397
|
conflicts
|
|
339
398
|
});
|
|
340
|
-
preference = await select(
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
399
|
+
preference = await select({
|
|
400
|
+
choices: Object.values(MergeConflictPreference)
|
|
401
|
+
})`
|
|
402
|
+
{bold How should we resolve these conflicts?}
|
|
403
|
+
`;
|
|
344
404
|
}
|
|
345
405
|
switch(preference){
|
|
346
|
-
case
|
|
406
|
+
case MergeConflictPreference.CANCEL:
|
|
347
407
|
{
|
|
348
408
|
process.exit(0);
|
|
349
409
|
break;
|
|
350
410
|
}
|
|
351
|
-
case
|
|
411
|
+
case MergeConflictPreference.LOCAL:
|
|
352
412
|
{
|
|
353
|
-
|
|
413
|
+
environmentChanges = withoutConflictingChanges({
|
|
354
414
|
conflicts,
|
|
355
|
-
changes:
|
|
415
|
+
changes: environmentChanges
|
|
356
416
|
});
|
|
357
417
|
break;
|
|
358
418
|
}
|
|
359
|
-
case
|
|
419
|
+
case MergeConflictPreference.ENVIRONMENT:
|
|
360
420
|
{
|
|
361
421
|
localChanges = withoutConflictingChanges({
|
|
362
422
|
conflicts,
|
|
@@ -366,101 +426,66 @@ export class FileSync {
|
|
|
366
426
|
}
|
|
367
427
|
}
|
|
368
428
|
}
|
|
369
|
-
if (
|
|
370
|
-
await this.
|
|
371
|
-
changes:
|
|
372
|
-
filesVersion:
|
|
429
|
+
if (environmentChanges.size > 0) {
|
|
430
|
+
await this._getChangesFromEnvironment(ctx, {
|
|
431
|
+
changes: environmentChanges,
|
|
432
|
+
filesVersion: environmentFilesVersion,
|
|
433
|
+
printEnvironmentChangesOptions
|
|
373
434
|
});
|
|
374
435
|
}
|
|
375
436
|
if (localChanges.size > 0) {
|
|
376
|
-
await this.
|
|
437
|
+
await this._sendChangesToEnvironment(ctx, {
|
|
377
438
|
changes: localChanges,
|
|
378
|
-
expectedFilesVersion:
|
|
439
|
+
expectedFilesVersion: environmentFilesVersion,
|
|
440
|
+
printLocalChangesOptions
|
|
379
441
|
});
|
|
380
442
|
}
|
|
381
443
|
}
|
|
382
|
-
async
|
|
383
|
-
|
|
384
|
-
// get the hashes of our local files
|
|
385
|
-
this.directory.hashes(),
|
|
386
|
-
// get the hashes of our local filesVersion and the latest filesVersion
|
|
387
|
-
(async ()=>{
|
|
388
|
-
let gadgetFilesVersion;
|
|
389
|
-
let gadgetHashes;
|
|
390
|
-
let filesVersionHashes;
|
|
391
|
-
if (this.filesVersion === 0n) {
|
|
392
|
-
// this is the first time we're syncing, so just get the
|
|
393
|
-
// hashes of the latest filesVersion
|
|
394
|
-
const { fileSyncHashes } = await this.edit.query({
|
|
395
|
-
query: FILE_SYNC_HASHES_QUERY
|
|
396
|
-
});
|
|
397
|
-
gadgetFilesVersion = BigInt(fileSyncHashes.filesVersion);
|
|
398
|
-
gadgetHashes = fileSyncHashes.hashes;
|
|
399
|
-
filesVersionHashes = {};
|
|
400
|
-
} else {
|
|
401
|
-
// this isn't the first time we're syncing, so get the hashes
|
|
402
|
-
// of the files at our local filesVersion and the latest
|
|
403
|
-
// filesVersion
|
|
404
|
-
const { fileSyncComparisonHashes } = await this.edit.query({
|
|
405
|
-
query: FILE_SYNC_COMPARISON_HASHES_QUERY,
|
|
406
|
-
variables: {
|
|
407
|
-
filesVersion: String(this.filesVersion)
|
|
408
|
-
}
|
|
409
|
-
});
|
|
410
|
-
gadgetFilesVersion = BigInt(fileSyncComparisonHashes.latestFilesVersionHashes.filesVersion);
|
|
411
|
-
gadgetHashes = fileSyncComparisonHashes.latestFilesVersionHashes.hashes;
|
|
412
|
-
filesVersionHashes = fileSyncComparisonHashes.filesVersionHashes.hashes;
|
|
413
|
-
}
|
|
414
|
-
return {
|
|
415
|
-
filesVersionHashes,
|
|
416
|
-
gadgetHashes,
|
|
417
|
-
gadgetFilesVersion
|
|
418
|
-
};
|
|
419
|
-
})()
|
|
420
|
-
]);
|
|
421
|
-
return {
|
|
422
|
-
filesVersionHashes,
|
|
423
|
-
localHashes,
|
|
424
|
-
gadgetHashes,
|
|
425
|
-
gadgetFilesVersion,
|
|
426
|
-
inSync: isEqualHashes(this.ctx, localHashes, gadgetHashes)
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
async _getChangesFromGadget({ filesVersion, changes }) {
|
|
430
|
-
this.ctx.log.debug("getting changes from gadget", {
|
|
444
|
+
async _getChangesFromEnvironment(ctx, { filesVersion, changes, printEnvironmentChangesOptions }) {
|
|
445
|
+
ctx.log.debug("getting changes from gadget", {
|
|
431
446
|
filesVersion,
|
|
432
447
|
changes
|
|
433
448
|
});
|
|
434
449
|
const created = changes.created();
|
|
435
450
|
const updated = changes.updated();
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
451
|
+
const spinner = spin({
|
|
452
|
+
ensureEmptyLineAbove: true
|
|
453
|
+
})(sprintChanges(ctx, {
|
|
454
|
+
changes,
|
|
455
|
+
tense: "present",
|
|
456
|
+
title: sprint`Pulling ${pluralize("file", changes.size)}. ${symbol.arrowLeft}`,
|
|
457
|
+
...printEnvironmentChangesOptions
|
|
458
|
+
}));
|
|
459
|
+
try {
|
|
460
|
+
let files = [];
|
|
461
|
+
if (created.length > 0 || updated.length > 0) {
|
|
462
|
+
const { fileSyncFiles } = await this.syncJson.edit.query({
|
|
463
|
+
query: FILE_SYNC_FILES_QUERY,
|
|
464
|
+
variables: {
|
|
465
|
+
paths: [
|
|
466
|
+
...created,
|
|
467
|
+
...updated
|
|
468
|
+
],
|
|
469
|
+
filesVersion: String(filesVersion),
|
|
470
|
+
encoding: FileSyncEncoding.Base64
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
files = fileSyncFiles.files;
|
|
474
|
+
}
|
|
475
|
+
await this._writeToLocalFilesystem(ctx, {
|
|
476
|
+
filesVersion,
|
|
477
|
+
files,
|
|
478
|
+
delete: changes.deleted(),
|
|
479
|
+
spinner,
|
|
480
|
+
printEnvironmentChangesOptions
|
|
448
481
|
});
|
|
449
|
-
|
|
482
|
+
} catch (error) {
|
|
483
|
+
spinner.fail();
|
|
484
|
+
throw error;
|
|
450
485
|
}
|
|
451
|
-
await this._writeToLocalFilesystem({
|
|
452
|
-
filesVersion,
|
|
453
|
-
files,
|
|
454
|
-
delete: changes.deleted()
|
|
455
|
-
});
|
|
456
|
-
printChanges(this.ctx, {
|
|
457
|
-
changes,
|
|
458
|
-
tense: "past",
|
|
459
|
-
message: sprint`← Received {gray ${dayjs().format("hh:mm:ss A")}}`
|
|
460
|
-
});
|
|
461
486
|
}
|
|
462
|
-
async
|
|
463
|
-
|
|
487
|
+
async _sendChangesToEnvironment(ctx, { changes, expectedFilesVersion = this.syncJson.filesVersion, printLocalChangesOptions }) {
|
|
488
|
+
ctx.log.debug("sending changes to gadget", {
|
|
464
489
|
expectedFilesVersion,
|
|
465
490
|
changes
|
|
466
491
|
});
|
|
@@ -473,13 +498,13 @@ export class FileSync {
|
|
|
473
498
|
});
|
|
474
499
|
return;
|
|
475
500
|
}
|
|
476
|
-
const absolutePath = this.directory.absolute(normalizedPath);
|
|
501
|
+
const absolutePath = this.syncJson.directory.absolute(normalizedPath);
|
|
477
502
|
let stats;
|
|
478
503
|
try {
|
|
479
504
|
stats = await fs.stat(absolutePath);
|
|
480
505
|
} catch (error) {
|
|
481
506
|
swallowEnoent(error);
|
|
482
|
-
|
|
507
|
+
ctx.log.debug("skipping change because file doesn't exist", {
|
|
483
508
|
path: normalizedPath
|
|
484
509
|
});
|
|
485
510
|
return;
|
|
@@ -501,70 +526,93 @@ export class FileSync {
|
|
|
501
526
|
});
|
|
502
527
|
});
|
|
503
528
|
if (changed.length === 0 && deleted.length === 0) {
|
|
504
|
-
|
|
529
|
+
ctx.log.debug("skipping send because there are no changes");
|
|
505
530
|
return;
|
|
506
531
|
}
|
|
507
|
-
const
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
532
|
+
const contentLength = changed.map((change)=>change.content.length).reduce((a, b)=>a + b, 0);
|
|
533
|
+
if (contentLength > MAX_PUSH_CONTENT_LENGTH) {
|
|
534
|
+
throw new EdgeCaseError(sprint`
|
|
535
|
+
{underline Your file changes are too large to push.}
|
|
536
|
+
|
|
537
|
+
Run "ggt status" to see your changes and consider
|
|
538
|
+
ignoring some files or pushing in smaller batches.
|
|
539
|
+
`);
|
|
540
|
+
}
|
|
541
|
+
const spinner = spin({
|
|
542
|
+
ensureEmptyLineAbove: true
|
|
543
|
+
})(sprintChanges(ctx, {
|
|
544
|
+
changes,
|
|
545
|
+
tense: "present",
|
|
546
|
+
title: sprintln`Pushing ${pluralize("file", changed.length + deleted.length)}. ${symbol.arrowRight}`,
|
|
547
|
+
...printLocalChangesOptions
|
|
548
|
+
}));
|
|
549
|
+
try {
|
|
550
|
+
const { publishFileSyncEvents: { remoteFilesVersion, problems: filesyncProblems } } = await this.syncJson.edit.mutate({
|
|
551
|
+
mutation: PUBLISH_FILE_SYNC_EVENTS_MUTATION,
|
|
552
|
+
variables: {
|
|
553
|
+
input: {
|
|
554
|
+
expectedRemoteFilesVersion: String(expectedFilesVersion),
|
|
555
|
+
changed,
|
|
556
|
+
deleted
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
http: {
|
|
560
|
+
retry: {
|
|
561
|
+
// we can retry this request because
|
|
562
|
+
// expectedRemoteFilesVersion makes it idempotent
|
|
563
|
+
methods: [
|
|
564
|
+
"POST"
|
|
565
|
+
],
|
|
566
|
+
calculateDelay: ({ error, computedValue })=>{
|
|
567
|
+
if (isFilesVersionMismatchError(error.response?.body)) {
|
|
568
|
+
// don't retry if we get a files version mismatch error
|
|
569
|
+
return 0;
|
|
570
|
+
}
|
|
571
|
+
return computedValue;
|
|
527
572
|
}
|
|
528
|
-
return computedValue;
|
|
529
573
|
}
|
|
530
574
|
}
|
|
531
|
-
}
|
|
532
|
-
});
|
|
533
|
-
printChanges(this.ctx, {
|
|
534
|
-
changes,
|
|
535
|
-
tense: "past",
|
|
536
|
-
message: sprint`→ Sent {gray ${dayjs().format("hh:mm:ss A")}}`,
|
|
537
|
-
limit: printLimit
|
|
538
|
-
});
|
|
539
|
-
if (BigInt(remoteFilesVersion) > expectedFilesVersion + 1n) {
|
|
540
|
-
// we can't save the remoteFilesVersion because we haven't
|
|
541
|
-
// received the intermediate filesVersions yet
|
|
542
|
-
throw new Error("Files version mismatch");
|
|
543
|
-
}
|
|
544
|
-
if (problems.length > 0) {
|
|
545
|
-
const problemGroup = {};
|
|
546
|
-
problems.forEach((problem)=>{
|
|
547
|
-
if (!(problem.path in problemGroup)) {
|
|
548
|
-
problemGroup[problem.path] = [];
|
|
549
|
-
}
|
|
550
|
-
problemGroup[problem.path]?.push(problem.message);
|
|
551
|
-
});
|
|
552
|
-
this.ctx.log.println2`{red Gadget has detected the following fatal errors with your files:}`;
|
|
553
|
-
Object.entries(problemGroup).forEach(([path, messages])=>{
|
|
554
|
-
this.ctx.log.println`{red [${path}]}`;
|
|
555
|
-
messages.forEach((message)=>{
|
|
556
|
-
this.ctx.log.println`{red - ${message}}`;
|
|
557
|
-
});
|
|
558
575
|
});
|
|
559
|
-
|
|
560
|
-
|
|
576
|
+
if (BigInt(remoteFilesVersion) > expectedFilesVersion + 1n) {
|
|
577
|
+
// we can't save the remoteFilesVersion because we haven't
|
|
578
|
+
// received the intermediate filesVersions yet
|
|
579
|
+
throw new Error("Files version mismatch");
|
|
580
|
+
}
|
|
581
|
+
await this.syncJson.save(remoteFilesVersion);
|
|
582
|
+
spinner.succeed(sprintChanges(ctx, {
|
|
583
|
+
changes,
|
|
584
|
+
tense: "past",
|
|
585
|
+
title: sprintln`Pushed ${pluralize("file", changed.length + deleted.length)}. ${symbol.arrowRight} ${ts()}`,
|
|
586
|
+
...printLocalChangesOptions
|
|
587
|
+
}));
|
|
588
|
+
if (filesyncProblems.length > 0) {
|
|
589
|
+
println({
|
|
590
|
+
ensureEmptyLineAbove: true
|
|
591
|
+
})`
|
|
592
|
+
{red Gadget has detected the following fatal errors with your files:}
|
|
593
|
+
|
|
594
|
+
${sprintProblems({
|
|
595
|
+
problems: filesyncProblemsToProblems(filesyncProblems),
|
|
596
|
+
showFileTypes: false,
|
|
597
|
+
indent: 10
|
|
598
|
+
})}
|
|
599
|
+
|
|
600
|
+
{red Your app will not be operational until all fatal errors are fixed.}
|
|
601
|
+
`;
|
|
602
|
+
}
|
|
603
|
+
} catch (error) {
|
|
604
|
+
if (isFilesVersionMismatchError(error)) {
|
|
605
|
+
spinner.clear();
|
|
606
|
+
} else {
|
|
607
|
+
spinner.fail();
|
|
608
|
+
}
|
|
609
|
+
throw error;
|
|
561
610
|
}
|
|
562
|
-
await this._save(remoteFilesVersion);
|
|
563
611
|
}
|
|
564
|
-
async _writeToLocalFilesystem(options) {
|
|
612
|
+
async _writeToLocalFilesystem(ctx, options) {
|
|
565
613
|
const filesVersion = BigInt(options.filesVersion);
|
|
566
|
-
assert(filesVersion >= this.filesVersion, "filesVersion must be greater than or equal to current filesVersion");
|
|
567
|
-
|
|
614
|
+
assert(filesVersion >= this.syncJson.filesVersion, "filesVersion must be greater than or equal to current filesVersion");
|
|
615
|
+
ctx.log.debug("writing to local filesystem", {
|
|
568
616
|
filesVersion,
|
|
569
617
|
files: options.files.map((file)=>file.path),
|
|
570
618
|
delete: options.delete
|
|
@@ -572,8 +620,8 @@ export class FileSync {
|
|
|
572
620
|
const created = [];
|
|
573
621
|
const updated = [];
|
|
574
622
|
await pMap(options.delete, async (filepath)=>{
|
|
575
|
-
const currentPath = this.directory.absolute(filepath);
|
|
576
|
-
const backupPath = this.directory.absolute(".gadget/backup", this.directory.relative(filepath));
|
|
623
|
+
const currentPath = this.syncJson.directory.absolute(filepath);
|
|
624
|
+
const backupPath = this.syncJson.directory.absolute(".gadget/backup", this.syncJson.directory.relative(filepath));
|
|
577
625
|
// rather than `rm -rf`ing files, we move them to
|
|
578
626
|
// `.gadget/backup/` so that users can recover them if something
|
|
579
627
|
// goes wrong. We've seen a lot of EBUSY/EINVAL errors when moving
|
|
@@ -594,7 +642,7 @@ export class FileSync {
|
|
|
594
642
|
retries: config.windows ? 4 : 2,
|
|
595
643
|
minTimeout: ms("100ms"),
|
|
596
644
|
onFailedAttempt: (error)=>{
|
|
597
|
-
|
|
645
|
+
ctx.log.warn("failed to move file to backup", {
|
|
598
646
|
error,
|
|
599
647
|
currentPath,
|
|
600
648
|
backupPath
|
|
@@ -603,7 +651,7 @@ export class FileSync {
|
|
|
603
651
|
});
|
|
604
652
|
});
|
|
605
653
|
await pMap(options.files, async (file)=>{
|
|
606
|
-
const absolutePath = this.directory.absolute(file.path);
|
|
654
|
+
const absolutePath = this.syncJson.directory.absolute(file.path);
|
|
607
655
|
if (await fs.pathExists(absolutePath)) {
|
|
608
656
|
updated.push(file.path);
|
|
609
657
|
} else {
|
|
@@ -620,11 +668,11 @@ export class FileSync {
|
|
|
620
668
|
// ensure the file has the correct mode
|
|
621
669
|
await fs.chmod(absolutePath, file.mode & 0o777);
|
|
622
670
|
}
|
|
623
|
-
if (absolutePath === this.directory.absolute(".ignore")) {
|
|
624
|
-
await this.directory.loadIgnoreFile();
|
|
671
|
+
if (absolutePath === this.syncJson.directory.absolute(".ignore")) {
|
|
672
|
+
await this.syncJson.directory.loadIgnoreFile();
|
|
625
673
|
}
|
|
626
674
|
});
|
|
627
|
-
await this.
|
|
675
|
+
await this.syncJson.save(String(filesVersion));
|
|
628
676
|
const changes = new Changes([
|
|
629
677
|
...created.map((path)=>[
|
|
630
678
|
path,
|
|
@@ -645,142 +693,52 @@ export class FileSync {
|
|
|
645
693
|
}
|
|
646
694
|
])
|
|
647
695
|
]);
|
|
696
|
+
options.spinner?.clear();
|
|
697
|
+
printChanges(ctx, {
|
|
698
|
+
changes,
|
|
699
|
+
tense: "past",
|
|
700
|
+
title: sprint`{green ${symbol.tick}} Pulled ${pluralize("file", changes.size)}. ${symbol.arrowLeft} ${ts()}`,
|
|
701
|
+
...options.printEnvironmentChangesOptions
|
|
702
|
+
});
|
|
648
703
|
if (changes.has("yarn.lock")) {
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
704
|
+
const spinner = spin({
|
|
705
|
+
ensureEmptyLineAbove: true
|
|
706
|
+
})('Running "yarn install --check-files"');
|
|
707
|
+
try {
|
|
708
|
+
await execa("yarn", [
|
|
709
|
+
"install",
|
|
710
|
+
"--check-files"
|
|
711
|
+
], {
|
|
712
|
+
cwd: this.syncJson.directory.path
|
|
713
|
+
});
|
|
714
|
+
spinner.succeed`Ran "yarn install --check-files" ${ts()}`;
|
|
715
|
+
} catch (error) {
|
|
716
|
+
spinner.fail();
|
|
717
|
+
ctx.log.error("yarn install failed", {
|
|
656
718
|
error
|
|
657
|
-
})
|
|
719
|
+
});
|
|
720
|
+
const message = serializeError(error).message;
|
|
721
|
+
if (message) {
|
|
722
|
+
println({
|
|
723
|
+
ensureEmptyLineAbove: true,
|
|
724
|
+
indent: 2
|
|
725
|
+
})(message);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
658
728
|
}
|
|
659
729
|
return changes;
|
|
660
730
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
*/ async _save(filesVersion) {
|
|
664
|
-
this._syncJson = {
|
|
665
|
-
...this._syncJson,
|
|
666
|
-
mtime: Date.now() + 1,
|
|
667
|
-
filesVersion: String(filesVersion)
|
|
668
|
-
};
|
|
669
|
-
this.ctx.log.debug("saving .gadget/sync.json");
|
|
670
|
-
await fs.outputJSON(this.directory.absolute(".gadget/sync.json"), this._syncJson, {
|
|
671
|
-
spaces: 2
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
constructor(/**
|
|
675
|
-
* The {@linkcode Context} that was used to initialize this
|
|
676
|
-
* {@linkcode FileSync} instance.
|
|
677
|
-
*/ ctx, /**
|
|
678
|
-
* The directory that is being synced to.
|
|
679
|
-
*/ directory, /**
|
|
680
|
-
* The Gadget application that is being synced to.
|
|
681
|
-
*/ app, /**
|
|
682
|
-
* The state of the filesystem.
|
|
683
|
-
*
|
|
684
|
-
* This is persisted to `.gadget/sync.json` within the {@linkcode directory}.
|
|
685
|
-
*/ _syncJson){
|
|
686
|
-
_define_property(this, "ctx", void 0);
|
|
687
|
-
_define_property(this, "directory", void 0);
|
|
688
|
-
_define_property(this, "app", void 0);
|
|
689
|
-
_define_property(this, "_syncJson", void 0);
|
|
690
|
-
_define_property(this, "edit", void 0);
|
|
731
|
+
constructor(syncJson){
|
|
732
|
+
_define_property(this, "syncJson", void 0);
|
|
691
733
|
/**
|
|
692
734
|
* A FIFO async callback queue that ensures we process filesync events
|
|
693
735
|
* in the order we receive them.
|
|
694
736
|
*/ _define_property(this, "_syncOperations", void 0);
|
|
695
|
-
this.
|
|
696
|
-
this.directory = directory;
|
|
697
|
-
this.app = app;
|
|
698
|
-
this._syncJson = _syncJson;
|
|
737
|
+
this.syncJson = syncJson;
|
|
699
738
|
this._syncOperations = new PQueue({
|
|
700
739
|
concurrency: 1
|
|
701
740
|
});
|
|
702
|
-
this.ctx = ctx.child({
|
|
703
|
-
fields: ()=>({
|
|
704
|
-
filesync: {
|
|
705
|
-
directory: this.directory.path,
|
|
706
|
-
filesVersion: this.filesVersion
|
|
707
|
-
}
|
|
708
|
-
})
|
|
709
|
-
});
|
|
710
|
-
this.edit = new Edit(this.ctx);
|
|
711
741
|
}
|
|
712
742
|
}
|
|
713
|
-
/**
|
|
714
|
-
* Checks if a directory is empty or non-existent.
|
|
715
|
-
*
|
|
716
|
-
* @param dir - The directory path to check.
|
|
717
|
-
* @returns A Promise that resolves to a boolean indicating whether the directory is empty or non-existent.
|
|
718
|
-
*/ export const isEmptyOrNonExistentDir = async (dir)=>{
|
|
719
|
-
try {
|
|
720
|
-
for await (const _ of (await fs.opendir(dir, {
|
|
721
|
-
bufferSize: 1
|
|
722
|
-
}))){
|
|
723
|
-
return false;
|
|
724
|
-
}
|
|
725
|
-
return true;
|
|
726
|
-
} catch (error) {
|
|
727
|
-
swallowEnoent(error);
|
|
728
|
-
return true;
|
|
729
|
-
}
|
|
730
|
-
};
|
|
731
|
-
export const assertAllGadgetFiles = ({ gadgetChanges })=>{
|
|
732
|
-
assert(gadgetChanges.created().length > 0 || gadgetChanges.deleted().length > 0 || gadgetChanges.updated().length > 0, "expected gadgetChanges to have changes");
|
|
733
|
-
const allGadgetFiles = Array.from(gadgetChanges.keys()).every((path)=>path.startsWith(".gadget/"));
|
|
734
|
-
assert(allGadgetFiles, "expected all gadgetChanges to be .gadget/ files");
|
|
735
|
-
};
|
|
736
|
-
export const ConflictPreference = Object.freeze({
|
|
737
|
-
CANCEL: "Cancel (Ctrl+C)",
|
|
738
|
-
LOCAL: "Keep my conflicting changes",
|
|
739
|
-
GADGET: "Keep Gadget's conflicting changes"
|
|
740
|
-
});
|
|
741
|
-
export const ConflictPreferenceArg = (value, name)=>{
|
|
742
|
-
if ([
|
|
743
|
-
"local",
|
|
744
|
-
"gadget"
|
|
745
|
-
].includes(value)) {
|
|
746
|
-
return ConflictPreference[value.toUpperCase()];
|
|
747
|
-
}
|
|
748
|
-
throw new ArgError(sprint`
|
|
749
|
-
${name} must be {bold local} or {bold gadget}
|
|
750
|
-
|
|
751
|
-
{bold EXAMPLES:}
|
|
752
|
-
${name} local
|
|
753
|
-
${name} gadget
|
|
754
|
-
`);
|
|
755
|
-
};
|
|
756
|
-
export const FileSyncArgs = {
|
|
757
|
-
"--app": {
|
|
758
|
-
type: AppArg,
|
|
759
|
-
alias: "-a"
|
|
760
|
-
},
|
|
761
|
-
"--prefer": ConflictPreferenceArg,
|
|
762
|
-
"--force": Boolean
|
|
763
|
-
};
|
|
764
|
-
export const isFilesVersionMismatchError = (error)=>{
|
|
765
|
-
if (error instanceof EditError) {
|
|
766
|
-
error = error.cause;
|
|
767
|
-
}
|
|
768
|
-
if (isGraphQLResult(error)) {
|
|
769
|
-
error = error.errors;
|
|
770
|
-
}
|
|
771
|
-
if (isGraphQLErrors(error)) {
|
|
772
|
-
error = error[0];
|
|
773
|
-
}
|
|
774
|
-
return isObject(error) && "message" in error && isString(error.message) && error.message.includes("Files version mismatch");
|
|
775
|
-
};
|
|
776
|
-
const swallowFilesVersionMismatch = (ctx, error)=>{
|
|
777
|
-
if (isFilesVersionMismatchError(error)) {
|
|
778
|
-
ctx.log.debug("swallowing files version mismatch", {
|
|
779
|
-
error
|
|
780
|
-
});
|
|
781
|
-
return;
|
|
782
|
-
}
|
|
783
|
-
throw error;
|
|
784
|
-
};
|
|
785
743
|
|
|
786
744
|
//# sourceMappingURL=filesync.js.map
|