presubmit 0.1.0 → 0.1.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/README.md
CHANGED
|
@@ -18,7 +18,7 @@ presubmit /absolute/path/to/your/repository
|
|
|
18
18
|
For a locally built release, install its tarball instead:
|
|
19
19
|
|
|
20
20
|
```sh
|
|
21
|
-
npm install -g ./presubmit-0.1.
|
|
21
|
+
npm install -g ./presubmit-0.1.1.tgz
|
|
22
22
|
presubmit /absolute/path/to/your/repository
|
|
23
23
|
```
|
|
24
24
|
|
|
@@ -37,6 +37,10 @@ Sessions, uploads, worktrees, and credentials live under `~/.local/share/presubm
|
|
|
37
37
|
Set `PRESUBMIT_HOME` to use another private data directory. Keep it outside your
|
|
38
38
|
repository. These runtime files are not part of the npm package.
|
|
39
39
|
|
|
40
|
+
At startup, file watching excludes existing Git-ignored paths, including datasets
|
|
41
|
+
and downloaded models. Restart Presubmit after changing ignore rules or adding a
|
|
42
|
+
large ignored directory so the watcher refreshes its exclusions.
|
|
43
|
+
|
|
40
44
|
Use `presubmit --help` for CLI options. To follow an existing session in a terminal:
|
|
41
45
|
|
|
42
46
|
```sh
|
|
@@ -1708,21 +1708,24 @@ async function startLockedServer(options, cwd, lock) {
|
|
|
1708
1708
|
return refresh;
|
|
1709
1709
|
}
|
|
1710
1710
|
let watcher;
|
|
1711
|
+
const watcherError = (error) => {
|
|
1712
|
+
if (!closing)
|
|
1713
|
+
activity({
|
|
1714
|
+
type: 'error',
|
|
1715
|
+
text: 'File watcher unavailable; use Refresh. ' + errorText(error),
|
|
1716
|
+
});
|
|
1717
|
+
};
|
|
1711
1718
|
try {
|
|
1712
|
-
watcher = watchWorkspace(cwd, {
|
|
1719
|
+
watcher = await watchWorkspace(cwd, {
|
|
1713
1720
|
changed: () => {
|
|
1714
1721
|
void refreshWorkspace();
|
|
1715
1722
|
},
|
|
1716
|
-
error:
|
|
1717
|
-
if (!closing)
|
|
1718
|
-
activity({
|
|
1719
|
-
type: 'error',
|
|
1720
|
-
text: 'File watcher unavailable; use Refresh. ' + errorText(error),
|
|
1721
|
-
});
|
|
1722
|
-
},
|
|
1723
|
+
error: watcherError,
|
|
1723
1724
|
});
|
|
1724
1725
|
}
|
|
1725
|
-
catch {
|
|
1726
|
+
catch (error) {
|
|
1727
|
+
watcherError(error);
|
|
1728
|
+
}
|
|
1726
1729
|
let commandQueue = Promise.resolve();
|
|
1727
1730
|
function acceptChildReport(id, text) {
|
|
1728
1731
|
const operation = commandQueue.then(() => {
|
|
@@ -1,46 +1,101 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
1
2
|
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
2
5
|
import { watch } from 'chokidar';
|
|
3
6
|
import { diagnosticsEnabled, performanceMetrics, diagnosticGauges } from './diagnostics.js';
|
|
4
|
-
|
|
7
|
+
const execute = promisify(execFile);
|
|
8
|
+
/** Snapshot Git's exclusions before opening recursive filesystem subscriptions. */
|
|
9
|
+
export async function workspaceWatchFilter(cwd) {
|
|
10
|
+
const ignoredFiles = new Set();
|
|
11
|
+
const ignoredDirectories = new Set();
|
|
12
|
+
try {
|
|
13
|
+
const { stdout } = await execute('git', ['ls-files', '--others', '--ignored', '--exclude-standard', '--directory', '-z'], {
|
|
14
|
+
cwd,
|
|
15
|
+
encoding: 'utf8',
|
|
16
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
17
|
+
timeout: 30_000,
|
|
18
|
+
env: { ...process.env, GIT_OPTIONAL_LOCKS: '0', LC_ALL: 'C' },
|
|
19
|
+
});
|
|
20
|
+
for (const name of stdout.split('\0').filter(Boolean)) {
|
|
21
|
+
// Git keeps tracked files out of this list, even when an ignore rule matches.
|
|
22
|
+
if (name.endsWith('/'))
|
|
23
|
+
ignoredDirectories.add(resolve(cwd, name));
|
|
24
|
+
else
|
|
25
|
+
ignoredFiles.add(resolve(cwd, name));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
const failure = error;
|
|
30
|
+
if (failure.code !== 'ENOENT' && !failure.stderr?.includes('not a git repository'))
|
|
31
|
+
throw new Error('Could not load Git exclusions; automatic file watching is unavailable.', {
|
|
32
|
+
cause: error,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return (file) => {
|
|
36
|
+
const name = resolve(file);
|
|
37
|
+
const local = relative(cwd, name);
|
|
38
|
+
if (isAbsolute(local) || local === '..' || local.startsWith('..' + sep))
|
|
39
|
+
return true;
|
|
40
|
+
if (local.split(sep).some((part) => ['node_modules', '.git', 'dist', '.cache'].includes(part)))
|
|
41
|
+
return true;
|
|
42
|
+
if (ignoredFiles.has(name))
|
|
43
|
+
return true;
|
|
44
|
+
for (let parent = name; parent !== cwd; parent = dirname(parent)) {
|
|
45
|
+
if (ignoredDirectories.has(parent))
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function createWorkspaceWatchers(create = watch, debounceMs = 500, filter = workspaceWatchFilter) {
|
|
5
52
|
const entries = new Map();
|
|
53
|
+
let subscribers = 0;
|
|
6
54
|
const updateGauges = () => {
|
|
7
55
|
diagnosticGauges.set('workspace.watchers', entries.size);
|
|
8
|
-
diagnosticGauges.set('workspace.subscribers',
|
|
56
|
+
diagnosticGauges.set('workspace.subscribers', subscribers);
|
|
9
57
|
};
|
|
10
|
-
|
|
58
|
+
const subscribe = async (cwd, subscriber) => {
|
|
11
59
|
const key = realpathSync(cwd);
|
|
12
|
-
let
|
|
13
|
-
if (!
|
|
14
|
-
|
|
15
|
-
ignoreInitial: true,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
});
|
|
20
|
-
entry = { watcher, subscribers: new Set() };
|
|
21
|
-
entries.set(key, entry);
|
|
22
|
-
const shared = entry;
|
|
23
|
-
watcher.on('all', () => {
|
|
24
|
-
if (diagnosticsEnabled)
|
|
25
|
-
performanceMetrics.count('workspace.fileEvents');
|
|
26
|
-
// Coalesce a sustained stream as well as a short burst. Do not allocate
|
|
27
|
-
// a fresh timer for every event or postpone refresh indefinitely.
|
|
28
|
-
if (shared.timer)
|
|
29
|
-
return;
|
|
30
|
-
shared.timer = setTimeout(() => {
|
|
31
|
-
shared.timer = undefined;
|
|
60
|
+
let pending = entries.get(key);
|
|
61
|
+
if (!pending) {
|
|
62
|
+
pending = filter(key).then((ignored) => {
|
|
63
|
+
const watcher = create(key, { ignoreInitial: true, followSymlinks: false, ignored });
|
|
64
|
+
const entry = { watcher, subscribers: new Set() };
|
|
65
|
+
const shared = entry;
|
|
66
|
+
watcher.on('all', () => {
|
|
32
67
|
if (diagnosticsEnabled)
|
|
33
|
-
performanceMetrics.count('workspace.
|
|
68
|
+
performanceMetrics.count('workspace.fileEvents');
|
|
69
|
+
// Coalesce a sustained stream as well as a short burst. Do not allocate
|
|
70
|
+
// a fresh timer for every event or postpone refresh indefinitely.
|
|
71
|
+
if (shared.timer)
|
|
72
|
+
return;
|
|
73
|
+
shared.timer = setTimeout(() => {
|
|
74
|
+
shared.timer = undefined;
|
|
75
|
+
if (diagnosticsEnabled)
|
|
76
|
+
performanceMetrics.count('workspace.changeBatches');
|
|
77
|
+
for (const listener of shared.subscribers)
|
|
78
|
+
listener.changed();
|
|
79
|
+
}, debounceMs);
|
|
80
|
+
});
|
|
81
|
+
watcher.on('error', (error) => {
|
|
34
82
|
for (const listener of shared.subscribers)
|
|
35
|
-
listener.
|
|
36
|
-
}
|
|
83
|
+
listener.error(error);
|
|
84
|
+
});
|
|
85
|
+
return entry;
|
|
37
86
|
});
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
87
|
+
entries.set(key, pending);
|
|
88
|
+
void pending.catch(() => {
|
|
89
|
+
if (entries.get(key) === pending)
|
|
90
|
+
entries.delete(key);
|
|
41
91
|
});
|
|
42
92
|
}
|
|
93
|
+
const entry = await pending;
|
|
94
|
+
// A last unsubscribe may have closed this entry while initialization was awaited.
|
|
95
|
+
if (entries.get(key) !== pending)
|
|
96
|
+
return subscribe(cwd, subscriber);
|
|
43
97
|
entry.subscribers.add(subscriber);
|
|
98
|
+
subscribers++;
|
|
44
99
|
updateGauges();
|
|
45
100
|
const shared = entry;
|
|
46
101
|
let closed;
|
|
@@ -48,6 +103,7 @@ export function createWorkspaceWatchers(create = watch, debounceMs = 500) {
|
|
|
48
103
|
close() {
|
|
49
104
|
if (closed)
|
|
50
105
|
return closed;
|
|
106
|
+
subscribers--;
|
|
51
107
|
shared.subscribers.delete(subscriber);
|
|
52
108
|
if (!shared.subscribers.size) {
|
|
53
109
|
entries.delete(key);
|
|
@@ -61,5 +117,6 @@ export function createWorkspaceWatchers(create = watch, debounceMs = 500) {
|
|
|
61
117
|
},
|
|
62
118
|
};
|
|
63
119
|
};
|
|
120
|
+
return subscribe;
|
|
64
121
|
}
|
|
65
122
|
export const watchWorkspace = createWorkspaceWatchers();
|
|
@@ -24,6 +24,8 @@ async function gitRoot(cwd) {
|
|
|
24
24
|
const failure = error;
|
|
25
25
|
if (failure.code === 'ENOENT' || failure.stderr?.includes('not a git repository'))
|
|
26
26
|
return null;
|
|
27
|
+
if (failure.code === 'EMFILE' || failure.code === 'ENFILE')
|
|
28
|
+
throw new Error('Too many open files to inspect Git. Stop and restart Presubmit to release file watchers.', { cause: error });
|
|
27
29
|
throw new Error('Could not inspect the folder’s Git repository.', { cause: error });
|
|
28
30
|
}
|
|
29
31
|
}
|