@hominis/fireforge 0.9.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/CHANGELOG.md +7 -0
- package/LICENSE.md +294 -0
- package/README.md +435 -0
- package/dist/bin/fireforge.d.ts +10 -0
- package/dist/bin/fireforge.js +29 -0
- package/dist/src/cli.d.ts +33 -0
- package/dist/src/cli.js +180 -0
- package/dist/src/commands/bootstrap.d.ts +9 -0
- package/dist/src/commands/bootstrap.js +73 -0
- package/dist/src/commands/build.d.ts +11 -0
- package/dist/src/commands/build.js +102 -0
- package/dist/src/commands/config.d.ts +13 -0
- package/dist/src/commands/config.js +135 -0
- package/dist/src/commands/discard.d.ts +12 -0
- package/dist/src/commands/discard.js +84 -0
- package/dist/src/commands/doctor.d.ts +18 -0
- package/dist/src/commands/doctor.js +356 -0
- package/dist/src/commands/download.d.ts +11 -0
- package/dist/src/commands/download.js +127 -0
- package/dist/src/commands/export-all.d.ts +11 -0
- package/dist/src/commands/export-all.js +122 -0
- package/dist/src/commands/export-shared.d.ts +48 -0
- package/dist/src/commands/export-shared.js +208 -0
- package/dist/src/commands/export.d.ts +13 -0
- package/dist/src/commands/export.js +178 -0
- package/dist/src/commands/furnace/apply.d.ts +7 -0
- package/dist/src/commands/furnace/apply.js +80 -0
- package/dist/src/commands/furnace/create.d.ts +8 -0
- package/dist/src/commands/furnace/create.js +377 -0
- package/dist/src/commands/furnace/deploy.d.ts +8 -0
- package/dist/src/commands/furnace/deploy.js +338 -0
- package/dist/src/commands/furnace/diff.d.ts +7 -0
- package/dist/src/commands/furnace/diff.js +119 -0
- package/dist/src/commands/furnace/index.d.ts +16 -0
- package/dist/src/commands/furnace/index.js +121 -0
- package/dist/src/commands/furnace/list.d.ts +5 -0
- package/dist/src/commands/furnace/list.js +65 -0
- package/dist/src/commands/furnace/override.d.ts +8 -0
- package/dist/src/commands/furnace/override.js +188 -0
- package/dist/src/commands/furnace/preview.d.ts +7 -0
- package/dist/src/commands/furnace/preview.js +96 -0
- package/dist/src/commands/furnace/remove.d.ts +8 -0
- package/dist/src/commands/furnace/remove.js +159 -0
- package/dist/src/commands/furnace/scan.d.ts +5 -0
- package/dist/src/commands/furnace/scan.js +112 -0
- package/dist/src/commands/furnace/status.d.ts +7 -0
- package/dist/src/commands/furnace/status.js +137 -0
- package/dist/src/commands/furnace/validate.d.ts +6 -0
- package/dist/src/commands/furnace/validate.js +91 -0
- package/dist/src/commands/furnace/validation-output.d.ts +7 -0
- package/dist/src/commands/furnace/validation-output.js +22 -0
- package/dist/src/commands/import.d.ts +11 -0
- package/dist/src/commands/import.js +241 -0
- package/dist/src/commands/lint.d.ts +10 -0
- package/dist/src/commands/lint.js +118 -0
- package/dist/src/commands/package.d.ts +11 -0
- package/dist/src/commands/package.js +80 -0
- package/dist/src/commands/re-export.d.ts +12 -0
- package/dist/src/commands/re-export.js +242 -0
- package/dist/src/commands/rebase/abort.d.ts +7 -0
- package/dist/src/commands/rebase/abort.js +49 -0
- package/dist/src/commands/rebase/confirm.d.ts +18 -0
- package/dist/src/commands/rebase/confirm.js +33 -0
- package/dist/src/commands/rebase/continue.d.ts +7 -0
- package/dist/src/commands/rebase/continue.js +81 -0
- package/dist/src/commands/rebase/index.d.ts +22 -0
- package/dist/src/commands/rebase/index.js +127 -0
- package/dist/src/commands/rebase/patch-loop.d.ts +9 -0
- package/dist/src/commands/rebase/patch-loop.js +135 -0
- package/dist/src/commands/rebase/summary.d.ts +12 -0
- package/dist/src/commands/rebase/summary.js +43 -0
- package/dist/src/commands/rebase.d.ts +4 -0
- package/dist/src/commands/rebase.js +6 -0
- package/dist/src/commands/register.d.ts +13 -0
- package/dist/src/commands/register.js +67 -0
- package/dist/src/commands/reset.d.ts +11 -0
- package/dist/src/commands/reset.js +83 -0
- package/dist/src/commands/resolve.d.ts +9 -0
- package/dist/src/commands/resolve.js +124 -0
- package/dist/src/commands/run.d.ts +9 -0
- package/dist/src/commands/run.js +91 -0
- package/dist/src/commands/setup-support.d.ts +23 -0
- package/dist/src/commands/setup-support.js +310 -0
- package/dist/src/commands/setup.d.ts +11 -0
- package/dist/src/commands/setup.js +94 -0
- package/dist/src/commands/status.d.ts +11 -0
- package/dist/src/commands/status.js +268 -0
- package/dist/src/commands/test.d.ts +12 -0
- package/dist/src/commands/test.js +182 -0
- package/dist/src/commands/token-coverage.d.ts +5 -0
- package/dist/src/commands/token-coverage.js +57 -0
- package/dist/src/commands/token.d.ts +14 -0
- package/dist/src/commands/token.js +121 -0
- package/dist/src/commands/watch.d.ts +9 -0
- package/dist/src/commands/watch.js +112 -0
- package/dist/src/commands/wire.d.ts +13 -0
- package/dist/src/commands/wire.js +149 -0
- package/dist/src/core/ast-utils.d.ts +47 -0
- package/dist/src/core/ast-utils.js +57 -0
- package/dist/src/core/brand-validation.d.ts +7 -0
- package/dist/src/core/brand-validation.js +15 -0
- package/dist/src/core/branding.d.ts +49 -0
- package/dist/src/core/branding.js +229 -0
- package/dist/src/core/browser-wire.d.ts +40 -0
- package/dist/src/core/browser-wire.js +66 -0
- package/dist/src/core/build-prepare.d.ts +25 -0
- package/dist/src/core/build-prepare.js +93 -0
- package/dist/src/core/config-mutate.d.ts +15 -0
- package/dist/src/core/config-mutate.js +51 -0
- package/dist/src/core/config-paths.d.ts +28 -0
- package/dist/src/core/config-paths.js +65 -0
- package/dist/src/core/config-state.d.ts +28 -0
- package/dist/src/core/config-state.js +152 -0
- package/dist/src/core/config-validate.d.ts +11 -0
- package/dist/src/core/config-validate.js +141 -0
- package/dist/src/core/config.d.ts +39 -0
- package/dist/src/core/config.js +70 -0
- package/dist/src/core/file-lock.d.ts +11 -0
- package/dist/src/core/file-lock.js +80 -0
- package/dist/src/core/firefox-archive.d.ts +40 -0
- package/dist/src/core/firefox-archive.js +63 -0
- package/dist/src/core/firefox-cache.d.ts +23 -0
- package/dist/src/core/firefox-cache.js +134 -0
- package/dist/src/core/firefox-download.d.ts +21 -0
- package/dist/src/core/firefox-download.js +129 -0
- package/dist/src/core/firefox-extract.d.ts +21 -0
- package/dist/src/core/firefox-extract.js +53 -0
- package/dist/src/core/firefox.d.ts +34 -0
- package/dist/src/core/firefox.js +78 -0
- package/dist/src/core/furnace-apply-helpers.d.ts +21 -0
- package/dist/src/core/furnace-apply-helpers.js +244 -0
- package/dist/src/core/furnace-apply.d.ts +16 -0
- package/dist/src/core/furnace-apply.js +147 -0
- package/dist/src/core/furnace-config.d.ts +94 -0
- package/dist/src/core/furnace-config.js +372 -0
- package/dist/src/core/furnace-constants.d.ts +4 -0
- package/dist/src/core/furnace-constants.js +6 -0
- package/dist/src/core/furnace-registration-ast.d.ts +24 -0
- package/dist/src/core/furnace-registration-ast.js +218 -0
- package/dist/src/core/furnace-registration-remove.d.ts +14 -0
- package/dist/src/core/furnace-registration-remove.js +89 -0
- package/dist/src/core/furnace-registration-validate.d.ts +20 -0
- package/dist/src/core/furnace-registration-validate.js +40 -0
- package/dist/src/core/furnace-registration.d.ts +29 -0
- package/dist/src/core/furnace-registration.js +96 -0
- package/dist/src/core/furnace-rollback.d.ts +20 -0
- package/dist/src/core/furnace-rollback.js +66 -0
- package/dist/src/core/furnace-scanner.d.ts +40 -0
- package/dist/src/core/furnace-scanner.js +143 -0
- package/dist/src/core/furnace-stories.d.ts +37 -0
- package/dist/src/core/furnace-stories.js +185 -0
- package/dist/src/core/furnace-validate-accessibility.d.ts +6 -0
- package/dist/src/core/furnace-validate-accessibility.js +32 -0
- package/dist/src/core/furnace-validate-checks.d.ts +4 -0
- package/dist/src/core/furnace-validate-checks.js +7 -0
- package/dist/src/core/furnace-validate-compatibility.d.ts +6 -0
- package/dist/src/core/furnace-validate-compatibility.js +57 -0
- package/dist/src/core/furnace-validate-helpers.d.ts +28 -0
- package/dist/src/core/furnace-validate-helpers.js +129 -0
- package/dist/src/core/furnace-validate-registration.d.ts +37 -0
- package/dist/src/core/furnace-validate-registration.js +220 -0
- package/dist/src/core/furnace-validate-structure.d.ts +6 -0
- package/dist/src/core/furnace-validate-structure.js +66 -0
- package/dist/src/core/furnace-validate.d.ts +16 -0
- package/dist/src/core/furnace-validate.js +103 -0
- package/dist/src/core/git-base.d.ts +47 -0
- package/dist/src/core/git-base.js +50 -0
- package/dist/src/core/git-diff.d.ts +63 -0
- package/dist/src/core/git-diff.js +246 -0
- package/dist/src/core/git-file-ops.d.ts +65 -0
- package/dist/src/core/git-file-ops.js +141 -0
- package/dist/src/core/git-status.d.ts +65 -0
- package/dist/src/core/git-status.js +163 -0
- package/dist/src/core/git.d.ts +113 -0
- package/dist/src/core/git.js +363 -0
- package/dist/src/core/license-headers.d.ts +36 -0
- package/dist/src/core/license-headers.js +83 -0
- package/dist/src/core/mach-build-artifacts.d.ts +29 -0
- package/dist/src/core/mach-build-artifacts.js +117 -0
- package/dist/src/core/mach-mozconfig.d.ts +17 -0
- package/dist/src/core/mach-mozconfig.js +50 -0
- package/dist/src/core/mach-python.d.ts +16 -0
- package/dist/src/core/mach-python.js +126 -0
- package/dist/src/core/mach.d.ts +106 -0
- package/dist/src/core/mach.js +166 -0
- package/dist/src/core/manifest-helpers.d.ts +25 -0
- package/dist/src/core/manifest-helpers.js +96 -0
- package/dist/src/core/manifest-register.d.ts +30 -0
- package/dist/src/core/manifest-register.js +65 -0
- package/dist/src/core/manifest-rules.d.ts +39 -0
- package/dist/src/core/manifest-rules.js +151 -0
- package/dist/src/core/manifest-tokenizers.d.ts +34 -0
- package/dist/src/core/manifest-tokenizers.js +84 -0
- package/dist/src/core/parser-fallback.d.ts +36 -0
- package/dist/src/core/parser-fallback.js +43 -0
- package/dist/src/core/patch-apply-fuzz.d.ts +29 -0
- package/dist/src/core/patch-apply-fuzz.js +70 -0
- package/dist/src/core/patch-apply.d.ts +46 -0
- package/dist/src/core/patch-apply.js +235 -0
- package/dist/src/core/patch-export.d.ts +99 -0
- package/dist/src/core/patch-export.js +314 -0
- package/dist/src/core/patch-files.d.ts +11 -0
- package/dist/src/core/patch-files.js +51 -0
- package/dist/src/core/patch-lint.d.ts +72 -0
- package/dist/src/core/patch-lint.js +403 -0
- package/dist/src/core/patch-lock.d.ts +8 -0
- package/dist/src/core/patch-lock.js +29 -0
- package/dist/src/core/patch-manifest-consistency.d.ts +24 -0
- package/dist/src/core/patch-manifest-consistency.js +135 -0
- package/dist/src/core/patch-manifest-io.d.ts +36 -0
- package/dist/src/core/patch-manifest-io.js +77 -0
- package/dist/src/core/patch-manifest-query.d.ts +48 -0
- package/dist/src/core/patch-manifest-query.js +124 -0
- package/dist/src/core/patch-manifest-validate.d.ts +22 -0
- package/dist/src/core/patch-manifest-validate.js +72 -0
- package/dist/src/core/patch-manifest.d.ts +11 -0
- package/dist/src/core/patch-manifest.js +12 -0
- package/dist/src/core/patch-parse.d.ts +43 -0
- package/dist/src/core/patch-parse.js +143 -0
- package/dist/src/core/patch-transform.d.ts +21 -0
- package/dist/src/core/patch-transform.js +138 -0
- package/dist/src/core/rebase-session.d.ts +47 -0
- package/dist/src/core/rebase-session.js +65 -0
- package/dist/src/core/register-browser-content.d.ts +11 -0
- package/dist/src/core/register-browser-content.js +116 -0
- package/dist/src/core/register-module.d.ts +11 -0
- package/dist/src/core/register-module.js +76 -0
- package/dist/src/core/register-shared-css.d.ts +11 -0
- package/dist/src/core/register-shared-css.js +117 -0
- package/dist/src/core/register-test-manifest.d.ts +18 -0
- package/dist/src/core/register-test-manifest.js +99 -0
- package/dist/src/core/state-file.d.ts +4 -0
- package/dist/src/core/state-file.js +25 -0
- package/dist/src/core/token-coverage.d.ts +12 -0
- package/dist/src/core/token-coverage.js +74 -0
- package/dist/src/core/token-manager.d.ts +55 -0
- package/dist/src/core/token-manager.js +387 -0
- package/dist/src/core/wire-destroy.d.ts +21 -0
- package/dist/src/core/wire-destroy.js +103 -0
- package/dist/src/core/wire-dom-fragment.d.ts +23 -0
- package/dist/src/core/wire-dom-fragment.js +129 -0
- package/dist/src/core/wire-init.d.ts +23 -0
- package/dist/src/core/wire-init.js +201 -0
- package/dist/src/core/wire-subscript.d.ts +20 -0
- package/dist/src/core/wire-subscript.js +134 -0
- package/dist/src/core/wire-targets.d.ts +7 -0
- package/dist/src/core/wire-targets.js +9 -0
- package/dist/src/core/wire-utils.d.ts +88 -0
- package/dist/src/core/wire-utils.js +279 -0
- package/dist/src/errors/base.d.ts +60 -0
- package/dist/src/errors/base.js +87 -0
- package/dist/src/errors/build.d.ts +52 -0
- package/dist/src/errors/build.js +114 -0
- package/dist/src/errors/codes.d.ts +29 -0
- package/dist/src/errors/codes.js +30 -0
- package/dist/src/errors/config.d.ts +31 -0
- package/dist/src/errors/config.js +61 -0
- package/dist/src/errors/download.d.ts +42 -0
- package/dist/src/errors/download.js +95 -0
- package/dist/src/errors/furnace.d.ts +10 -0
- package/dist/src/errors/furnace.js +22 -0
- package/dist/src/errors/git.d.ts +41 -0
- package/dist/src/errors/git.js +99 -0
- package/dist/src/errors/patch.d.ts +10 -0
- package/dist/src/errors/patch.js +26 -0
- package/dist/src/errors/rebase.d.ts +20 -0
- package/dist/src/errors/rebase.js +30 -0
- package/dist/src/index.d.ts +21 -0
- package/dist/src/index.js +21 -0
- package/dist/src/types/cli.d.ts +14 -0
- package/dist/src/types/cli.js +2 -0
- package/dist/src/types/commands/index.d.ts +6 -0
- package/dist/src/types/commands/index.js +6 -0
- package/dist/src/types/commands/options.d.ts +239 -0
- package/dist/src/types/commands/options.js +6 -0
- package/dist/src/types/commands/patches.d.ts +89 -0
- package/dist/src/types/commands/patches.js +6 -0
- package/dist/src/types/commands/project.d.ts +71 -0
- package/dist/src/types/commands/project.js +6 -0
- package/dist/src/types/config.d.ts +101 -0
- package/dist/src/types/config.js +2 -0
- package/dist/src/types/furnace.d.ts +158 -0
- package/dist/src/types/furnace.js +2 -0
- package/dist/src/types/index.d.ts +6 -0
- package/dist/src/types/index.js +6 -0
- package/dist/src/utils/errors.d.ts +2 -0
- package/dist/src/utils/errors.js +15 -0
- package/dist/src/utils/fs.d.ts +72 -0
- package/dist/src/utils/fs.js +179 -0
- package/dist/src/utils/logger.d.ts +58 -0
- package/dist/src/utils/logger.js +120 -0
- package/dist/src/utils/options.d.ts +8 -0
- package/dist/src/utils/options.js +16 -0
- package/dist/src/utils/package-root.d.ts +10 -0
- package/dist/src/utils/package-root.js +53 -0
- package/dist/src/utils/parse.d.ts +110 -0
- package/dist/src/utils/parse.js +200 -0
- package/dist/src/utils/paths.d.ts +10 -0
- package/dist/src/utils/paths.js +43 -0
- package/dist/src/utils/platform.d.ts +38 -0
- package/dist/src/utils/platform.js +56 -0
- package/dist/src/utils/process.d.ts +80 -0
- package/dist/src/utils/process.js +188 -0
- package/dist/src/utils/regex.d.ts +24 -0
- package/dist/src/utils/regex.js +40 -0
- package/dist/src/utils/validation.d.ts +133 -0
- package/dist/src/utils/validation.js +250 -0
- package/package.json +106 -0
- package/templates/configs/common.mozconfig +24 -0
- package/templates/configs/darwin.mozconfig +10 -0
- package/templates/configs/linux.mozconfig +12 -0
- package/templates/configs/win32.mozconfig +14 -0
- package/templates/licenses/0BSD.md +14 -0
- package/templates/licenses/EUPL-1.2.md +294 -0
- package/templates/licenses/GPL-2.0-or-later.md +339 -0
- package/templates/licenses/MPL-2.0.md +383 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { basename, join } from 'node:path';
|
|
5
|
+
import { toError } from '../utils/errors.js';
|
|
6
|
+
import { pathExists, readText } from '../utils/fs.js';
|
|
7
|
+
import { verbose } from '../utils/logger.js';
|
|
8
|
+
import { exec } from '../utils/process.js';
|
|
9
|
+
import { ensureGit } from './git-base.js';
|
|
10
|
+
import { fileExistsInHead } from './git-file-ops.js';
|
|
11
|
+
import { getUntrackedFiles } from './git-status.js';
|
|
12
|
+
/**
|
|
13
|
+
* Gets the diff for a specific file.
|
|
14
|
+
* @param repoDir - Repository directory
|
|
15
|
+
* @param filePath - Path to the file (relative to repo)
|
|
16
|
+
* @returns Diff content
|
|
17
|
+
*/
|
|
18
|
+
export async function getFileDiff(repoDir, filePath) {
|
|
19
|
+
await ensureGit();
|
|
20
|
+
const result = await exec('git', ['diff', 'HEAD', '--', filePath], { cwd: repoDir });
|
|
21
|
+
return result.stdout;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Generates a unified diff for a new (untracked) file.
|
|
25
|
+
* @param repoDir - Repository directory
|
|
26
|
+
* @param filePath - Path to the file (relative to repo)
|
|
27
|
+
* @returns Diff content in unified diff format
|
|
28
|
+
*/
|
|
29
|
+
export async function generateNewFileDiff(repoDir, filePath) {
|
|
30
|
+
const fullPath = join(repoDir, filePath);
|
|
31
|
+
const content = await readText(fullPath);
|
|
32
|
+
// Compute the abbreviated git blob hash for the index line
|
|
33
|
+
let blobHash = '0000000000';
|
|
34
|
+
try {
|
|
35
|
+
const hashResult = await exec('git', ['hash-object', fullPath], { cwd: repoDir });
|
|
36
|
+
const fullHash = hashResult.stdout.trim();
|
|
37
|
+
if (fullHash.length >= 10) {
|
|
38
|
+
blobHash = fullHash.slice(0, 10);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
verbose(`git hash-object failed for ${filePath}; falling back to zero blob hash: ${toError(error).message}`);
|
|
43
|
+
}
|
|
44
|
+
// Handle empty files
|
|
45
|
+
if (content.length === 0) {
|
|
46
|
+
return [
|
|
47
|
+
`diff --git a/${filePath} b/${filePath}`,
|
|
48
|
+
'new file mode 100644',
|
|
49
|
+
`index 0000000000..${blobHash}`,
|
|
50
|
+
'--- /dev/null',
|
|
51
|
+
`+++ b/${filePath}`,
|
|
52
|
+
'',
|
|
53
|
+
].join('\n');
|
|
54
|
+
}
|
|
55
|
+
const lines = content.split('\n');
|
|
56
|
+
// Handle files that don't end with newline
|
|
57
|
+
const hasTrailingNewline = content.endsWith('\n');
|
|
58
|
+
const lineCount = hasTrailingNewline ? lines.length - 1 : lines.length;
|
|
59
|
+
// Build the unified diff format for a new file
|
|
60
|
+
const diffLines = [
|
|
61
|
+
`diff --git a/${filePath} b/${filePath}`,
|
|
62
|
+
'new file mode 100644',
|
|
63
|
+
`index 0000000000..${blobHash}`,
|
|
64
|
+
'--- /dev/null',
|
|
65
|
+
`+++ b/${filePath}`,
|
|
66
|
+
`@@ -0,0 +1,${lineCount} @@`,
|
|
67
|
+
];
|
|
68
|
+
// Add each line with a + prefix
|
|
69
|
+
for (let i = 0; i < lineCount; i++) {
|
|
70
|
+
diffLines.push(`+${lines[i]}`);
|
|
71
|
+
}
|
|
72
|
+
// Add "No newline at end of file" marker if needed
|
|
73
|
+
if (!hasTrailingNewline && lineCount > 0) {
|
|
74
|
+
diffLines.push('\');
|
|
75
|
+
}
|
|
76
|
+
return diffLines.join('\n') + '\n';
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Generates a patch for a file.
|
|
80
|
+
* If the file is tracked in HEAD, it generates a standard contextual diff.
|
|
81
|
+
* If the file is untracked (new), it generates a "new file" format patch (snapshot).
|
|
82
|
+
* This ensures standard 3-way mergeable context diffs for existing Mozilla files.
|
|
83
|
+
* @param repoDir - Repository directory
|
|
84
|
+
* @param filePath - Path to the file (relative to repo)
|
|
85
|
+
* @returns Diff content in unified diff format
|
|
86
|
+
*/
|
|
87
|
+
export async function generateFullFilePatch(repoDir, filePath) {
|
|
88
|
+
await ensureGit();
|
|
89
|
+
// If file exists in HEAD, use standard git diff HEAD -- <file>
|
|
90
|
+
// This generates a contextual diff that is safer for rebasing
|
|
91
|
+
if (await fileExistsInHead(repoDir, filePath)) {
|
|
92
|
+
return getFileDiff(repoDir, filePath);
|
|
93
|
+
}
|
|
94
|
+
// If file is new/untracked, use the full-file "new file" format
|
|
95
|
+
return generateNewFileDiff(repoDir, filePath);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Generates a unified diff between base content and current file content.
|
|
99
|
+
* @param repoDir - Repository directory
|
|
100
|
+
* @param filePath - Path to the file (relative to repo)
|
|
101
|
+
* @param baseContent - The base content to diff against
|
|
102
|
+
* @returns Unified diff in git format
|
|
103
|
+
*/
|
|
104
|
+
export async function generateModificationDiff(repoDir, filePath, baseContent) {
|
|
105
|
+
const fullPath = join(repoDir, filePath);
|
|
106
|
+
const currentContent = await readText(fullPath);
|
|
107
|
+
// If contents are identical, return empty diff
|
|
108
|
+
if (baseContent === currentContent) {
|
|
109
|
+
return '';
|
|
110
|
+
}
|
|
111
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'fireforge-diff-'));
|
|
112
|
+
const tempFile = join(tempDir, basename(filePath));
|
|
113
|
+
try {
|
|
114
|
+
await writeFile(tempFile, baseContent);
|
|
115
|
+
// git diff --no-index exits code 1 when files differ — that's normal
|
|
116
|
+
const result = await exec('git', ['diff', '--no-index', '--', tempFile, fullPath], {
|
|
117
|
+
cwd: repoDir,
|
|
118
|
+
});
|
|
119
|
+
const output = result.stdout;
|
|
120
|
+
if (!output) {
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
// Post-process: fix paths in the diff header only (before the first @@ hunk)
|
|
124
|
+
const lines = output.split('\n');
|
|
125
|
+
let pastHeader = false;
|
|
126
|
+
const fixedLines = lines.map((line) => {
|
|
127
|
+
if (!pastHeader && line.startsWith('@@')) {
|
|
128
|
+
pastHeader = true;
|
|
129
|
+
}
|
|
130
|
+
if (!pastHeader) {
|
|
131
|
+
if (line.startsWith('diff --git')) {
|
|
132
|
+
return `diff --git a/${filePath} b/${filePath}`;
|
|
133
|
+
}
|
|
134
|
+
if (line.startsWith('--- ')) {
|
|
135
|
+
return `--- a/${filePath}`;
|
|
136
|
+
}
|
|
137
|
+
if (line.startsWith('+++ ')) {
|
|
138
|
+
return `+++ b/${filePath}`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return line;
|
|
142
|
+
});
|
|
143
|
+
return fixedLines.join('\n');
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Gets the diff for all modified files, including untracked (new) files.
|
|
151
|
+
* @param repoDir - Repository directory
|
|
152
|
+
* @returns Diff content
|
|
153
|
+
*/
|
|
154
|
+
export async function getAllDiff(repoDir) {
|
|
155
|
+
await ensureGit();
|
|
156
|
+
// Get diff for tracked files
|
|
157
|
+
const result = await exec('git', ['diff', 'HEAD'], { cwd: repoDir });
|
|
158
|
+
const trackedDiff = result.stdout;
|
|
159
|
+
// Get untracked files (properly expanded, not directories)
|
|
160
|
+
const untrackedFiles = await getUntrackedFiles(repoDir);
|
|
161
|
+
// Generate diffs for untracked files
|
|
162
|
+
const untrackedDiffs = [];
|
|
163
|
+
for (const file of untrackedFiles) {
|
|
164
|
+
const diff = await generateNewFileDiff(repoDir, file);
|
|
165
|
+
untrackedDiffs.push(diff);
|
|
166
|
+
}
|
|
167
|
+
// Combine all diffs — each already ends with \n, so concatenate directly
|
|
168
|
+
// to avoid inserting blank lines between diff sections.
|
|
169
|
+
const allDiffs = [trackedDiff, ...untrackedDiffs].filter((d) => d.trim().length > 0);
|
|
170
|
+
const combined = allDiffs.join('');
|
|
171
|
+
return combined.endsWith('\n') ? combined : combined + '\n';
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Builds a combined diff against HEAD for the provided files without touching
|
|
175
|
+
* the real git index. Tracked files use `git diff HEAD`; untracked files use
|
|
176
|
+
* synthesized new-file diffs.
|
|
177
|
+
* @param repoDir - Repository directory
|
|
178
|
+
* @param files - File paths to diff (relative to repo root)
|
|
179
|
+
* @returns Combined diff content
|
|
180
|
+
*/
|
|
181
|
+
export async function getDiffForFilesAgainstHead(repoDir, files) {
|
|
182
|
+
await ensureGit();
|
|
183
|
+
const uniqueFiles = [...new Set(files)].sort();
|
|
184
|
+
const diffs = [];
|
|
185
|
+
for (const file of uniqueFiles) {
|
|
186
|
+
if (await fileExistsInHead(repoDir, file)) {
|
|
187
|
+
const diff = await getFileDiff(repoDir, file);
|
|
188
|
+
if (diff.trim()) {
|
|
189
|
+
diffs.push(diff);
|
|
190
|
+
}
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (!(await pathExists(join(repoDir, file)))) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const diff = await generateNewFileDiff(repoDir, file);
|
|
197
|
+
if (diff.trim()) {
|
|
198
|
+
diffs.push(diff);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (diffs.length === 0) {
|
|
202
|
+
return '';
|
|
203
|
+
}
|
|
204
|
+
// Each diff from git already ends with \n. Concatenate directly to
|
|
205
|
+
// preserve context lines (including trailing whitespace-only context)
|
|
206
|
+
// and avoid inserting blank lines between diff sections.
|
|
207
|
+
const combined = diffs.join('');
|
|
208
|
+
return combined.endsWith('\n') ? combined : combined + '\n';
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Generates a combined diff for staged files against HEAD.
|
|
212
|
+
* @param repoDir - Repository directory
|
|
213
|
+
* @param files - File paths to diff (relative to repo)
|
|
214
|
+
* @returns Diff content for the staged files
|
|
215
|
+
*/
|
|
216
|
+
export async function getStagedDiffForFiles(repoDir, files) {
|
|
217
|
+
await ensureGit();
|
|
218
|
+
const result = await exec('git', ['diff', '--cached', 'HEAD', '--', ...files], { cwd: repoDir });
|
|
219
|
+
return result.stdout;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Generates a GIT binary patch for a binary file.
|
|
223
|
+
* For tracked files, uses `git diff --binary HEAD`.
|
|
224
|
+
* For untracked files, temporarily stages with `--intent-to-add` to produce a diff.
|
|
225
|
+
* @param repoDir - Repository directory
|
|
226
|
+
* @param filePath - File path (relative to repo root)
|
|
227
|
+
* @returns The binary diff string, or empty string if no diff
|
|
228
|
+
*/
|
|
229
|
+
export async function generateBinaryFilePatch(repoDir, filePath) {
|
|
230
|
+
await ensureGit();
|
|
231
|
+
// Try tracked file diff first
|
|
232
|
+
const result = await exec('git', ['diff', '--binary', 'HEAD', '--', filePath], { cwd: repoDir });
|
|
233
|
+
if (result.stdout.trim())
|
|
234
|
+
return result.stdout;
|
|
235
|
+
// For untracked files, stage temporarily to produce a binary diff
|
|
236
|
+
try {
|
|
237
|
+
await exec('git', ['add', '--intent-to-add', '--', filePath], { cwd: repoDir });
|
|
238
|
+
const diffResult = await exec('git', ['diff', '--binary', '--', filePath], { cwd: repoDir });
|
|
239
|
+
return diffResult.stdout;
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
// Always unstage, even if diff fails
|
|
243
|
+
await exec('git', ['reset', 'HEAD', '--', filePath], { cwd: repoDir });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
//# sourceMappingURL=git-diff.js.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { GitStatusEntry } from './git-base.js';
|
|
2
|
+
/**
|
|
3
|
+
* Discards changes to a specific file.
|
|
4
|
+
* @param repoDir - Repository directory
|
|
5
|
+
* @param filePath - Path to the file (relative to repo)
|
|
6
|
+
*/
|
|
7
|
+
export declare function discardFile(repoDir: string, filePath: string): Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Restores a tracked path from HEAD, including staged changes.
|
|
10
|
+
* @param repoDir - Repository directory
|
|
11
|
+
* @param filePath - Path to the file (relative to repo)
|
|
12
|
+
*/
|
|
13
|
+
export declare function restoreTrackedPath(repoDir: string, filePath: string): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Removes an untracked path from disk.
|
|
16
|
+
* @param repoDir - Repository directory
|
|
17
|
+
* @param filePath - Path to the file (relative to repo)
|
|
18
|
+
*/
|
|
19
|
+
export declare function removeUntrackedPath(repoDir: string, filePath: string): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Removes a path that is present only in the index/worktree and not in HEAD.
|
|
22
|
+
* @param repoDir - Repository directory
|
|
23
|
+
* @param filePath - Path to remove
|
|
24
|
+
*/
|
|
25
|
+
export declare function removeAddedPath(repoDir: string, filePath: string): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Discards a status entry according to its git state.
|
|
28
|
+
* @param repoDir - Repository directory
|
|
29
|
+
* @param entry - Parsed git status entry
|
|
30
|
+
*/
|
|
31
|
+
export declare function discardStatusEntry(repoDir: string, entry: GitStatusEntry): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Stages specific files in the repository.
|
|
34
|
+
* @param repoDir - Repository directory
|
|
35
|
+
* @param files - File paths to stage (relative to repo)
|
|
36
|
+
*/
|
|
37
|
+
export declare function stageFiles(repoDir: string, files: string[]): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Unstages specific files from the index.
|
|
40
|
+
* @param repoDir - Repository directory
|
|
41
|
+
* @param files - File paths to unstage (relative to repo)
|
|
42
|
+
*/
|
|
43
|
+
export declare function unstageFiles(repoDir: string, files: string[]): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Checks if a file exists in the HEAD commit.
|
|
46
|
+
* @param repoDir - Repository directory
|
|
47
|
+
* @param filePath - Path to the file (relative to repo)
|
|
48
|
+
* @returns true if file exists in HEAD
|
|
49
|
+
*/
|
|
50
|
+
export declare function fileExistsInHead(repoDir: string, filePath: string): Promise<boolean>;
|
|
51
|
+
/**
|
|
52
|
+
* Gets the content of a file from HEAD commit.
|
|
53
|
+
* @param repoDir - Repository directory
|
|
54
|
+
* @param filePath - Path to the file (relative to repo)
|
|
55
|
+
* @returns File content or null if file doesn't exist in HEAD
|
|
56
|
+
*/
|
|
57
|
+
export declare function getFileContentFromHead(repoDir: string, filePath: string): Promise<string | null>;
|
|
58
|
+
/**
|
|
59
|
+
* Checks if a file is binary by looking for NUL bytes in the first 8KB.
|
|
60
|
+
* Uses the same heuristic as git.
|
|
61
|
+
* @param repoDir - Repository directory
|
|
62
|
+
* @param filePath - File path (relative to repo root)
|
|
63
|
+
* @returns true if the file appears to be binary
|
|
64
|
+
*/
|
|
65
|
+
export declare function isBinaryFile(repoDir: string, filePath: string): Promise<boolean>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { open } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { removeFile } from '../utils/fs.js';
|
|
5
|
+
import { exec } from '../utils/process.js';
|
|
6
|
+
import { ensureGit, git } from './git-base.js';
|
|
7
|
+
/**
|
|
8
|
+
* Discards changes to a specific file.
|
|
9
|
+
* @param repoDir - Repository directory
|
|
10
|
+
* @param filePath - Path to the file (relative to repo)
|
|
11
|
+
*/
|
|
12
|
+
export async function discardFile(repoDir, filePath) {
|
|
13
|
+
await restoreTrackedPath(repoDir, filePath);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Restores a tracked path from HEAD, including staged changes.
|
|
17
|
+
* @param repoDir - Repository directory
|
|
18
|
+
* @param filePath - Path to the file (relative to repo)
|
|
19
|
+
*/
|
|
20
|
+
export async function restoreTrackedPath(repoDir, filePath) {
|
|
21
|
+
await ensureGit();
|
|
22
|
+
await git(['restore', '--source', 'HEAD', '--staged', '--worktree', '--', filePath], repoDir);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Removes an untracked path from disk.
|
|
26
|
+
* @param repoDir - Repository directory
|
|
27
|
+
* @param filePath - Path to the file (relative to repo)
|
|
28
|
+
*/
|
|
29
|
+
export async function removeUntrackedPath(repoDir, filePath) {
|
|
30
|
+
const fullPath = join(repoDir, filePath);
|
|
31
|
+
await removeFile(fullPath);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Removes a path that is present only in the index/worktree and not in HEAD.
|
|
35
|
+
* @param repoDir - Repository directory
|
|
36
|
+
* @param filePath - Path to remove
|
|
37
|
+
*/
|
|
38
|
+
export async function removeAddedPath(repoDir, filePath) {
|
|
39
|
+
await ensureGit();
|
|
40
|
+
await exec('git', ['reset', 'HEAD', '--', filePath], { cwd: repoDir });
|
|
41
|
+
await removeUntrackedPath(repoDir, filePath);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Discards a status entry according to its git state.
|
|
45
|
+
* @param repoDir - Repository directory
|
|
46
|
+
* @param entry - Parsed git status entry
|
|
47
|
+
*/
|
|
48
|
+
export async function discardStatusEntry(repoDir, entry) {
|
|
49
|
+
if (entry.isUntracked) {
|
|
50
|
+
await removeUntrackedPath(repoDir, entry.file);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (entry.isRenameOrCopy && entry.originalPath) {
|
|
54
|
+
await restoreTrackedPath(repoDir, entry.originalPath);
|
|
55
|
+
if (await fileExistsInHead(repoDir, entry.file)) {
|
|
56
|
+
await restoreTrackedPath(repoDir, entry.file);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
await removeAddedPath(repoDir, entry.file);
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!(await fileExistsInHead(repoDir, entry.file))) {
|
|
64
|
+
await removeAddedPath(repoDir, entry.file);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
await restoreTrackedPath(repoDir, entry.file);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Stages specific files in the repository.
|
|
71
|
+
* @param repoDir - Repository directory
|
|
72
|
+
* @param files - File paths to stage (relative to repo)
|
|
73
|
+
*/
|
|
74
|
+
export async function stageFiles(repoDir, files) {
|
|
75
|
+
await ensureGit();
|
|
76
|
+
await git(['add', '--', ...files], repoDir);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Unstages specific files from the index.
|
|
80
|
+
* @param repoDir - Repository directory
|
|
81
|
+
* @param files - File paths to unstage (relative to repo)
|
|
82
|
+
*/
|
|
83
|
+
export async function unstageFiles(repoDir, files) {
|
|
84
|
+
await ensureGit();
|
|
85
|
+
await git(['reset', 'HEAD', '--', ...files], repoDir);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Checks if a file exists in the HEAD commit.
|
|
89
|
+
* @param repoDir - Repository directory
|
|
90
|
+
* @param filePath - Path to the file (relative to repo)
|
|
91
|
+
* @returns true if file exists in HEAD
|
|
92
|
+
*/
|
|
93
|
+
export async function fileExistsInHead(repoDir, filePath) {
|
|
94
|
+
await ensureGit();
|
|
95
|
+
const result = await exec('git', ['ls-tree', 'HEAD', '--', filePath], { cwd: repoDir });
|
|
96
|
+
return result.stdout.trim().length > 0;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Gets the content of a file from HEAD commit.
|
|
100
|
+
* @param repoDir - Repository directory
|
|
101
|
+
* @param filePath - Path to the file (relative to repo)
|
|
102
|
+
* @returns File content or null if file doesn't exist in HEAD
|
|
103
|
+
*/
|
|
104
|
+
export async function getFileContentFromHead(repoDir, filePath) {
|
|
105
|
+
await ensureGit();
|
|
106
|
+
const result = await exec('git', ['show', `HEAD:${filePath}`], { cwd: repoDir });
|
|
107
|
+
if (result.exitCode !== 0) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return result.stdout;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Checks if a file is binary by looking for NUL bytes in the first 8KB.
|
|
114
|
+
* Uses the same heuristic as git.
|
|
115
|
+
* @param repoDir - Repository directory
|
|
116
|
+
* @param filePath - File path (relative to repo root)
|
|
117
|
+
* @returns true if the file appears to be binary
|
|
118
|
+
*/
|
|
119
|
+
export async function isBinaryFile(repoDir, filePath) {
|
|
120
|
+
const fullPath = join(repoDir, filePath);
|
|
121
|
+
try {
|
|
122
|
+
const fh = await open(fullPath, 'r');
|
|
123
|
+
try {
|
|
124
|
+
const buf = Buffer.alloc(8192);
|
|
125
|
+
const { bytesRead } = await fh.read(buf, 0, 8192, 0);
|
|
126
|
+
for (let i = 0; i < bytesRead; i++) {
|
|
127
|
+
if (buf[i] === 0)
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
await fh.close();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
void error;
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=git-file-ops.js.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { GitStatusEntry } from './git-base.js';
|
|
2
|
+
/**
|
|
3
|
+
* Parses NUL-delimited porcelain status output.
|
|
4
|
+
* @param output - Raw git status output
|
|
5
|
+
* @returns Parsed entries
|
|
6
|
+
*/
|
|
7
|
+
/** @internal Exported for testing */
|
|
8
|
+
export declare function parsePorcelainStatus(output: string): GitStatusEntry[];
|
|
9
|
+
/**
|
|
10
|
+
* Gets structured working tree status entries.
|
|
11
|
+
* @param repoDir - Repository directory
|
|
12
|
+
* @returns Parsed git status entries
|
|
13
|
+
*/
|
|
14
|
+
export declare function getWorkingTreeStatus(repoDir: string): Promise<GitStatusEntry[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Expands collapsed untracked directory entries into individual file entries.
|
|
17
|
+
* Git status may report "?? dir/" instead of listing each file underneath.
|
|
18
|
+
* @param repoDir - Repository directory
|
|
19
|
+
* @param entries - Parsed status entries
|
|
20
|
+
* @returns Status entries with untracked directories expanded to individual files
|
|
21
|
+
*/
|
|
22
|
+
export declare function expandUntrackedDirectoryEntries(repoDir: string, entries: GitStatusEntry[]): Promise<GitStatusEntry[]>;
|
|
23
|
+
/**
|
|
24
|
+
* Gets the list of modified files.
|
|
25
|
+
* @param repoDir - Repository directory
|
|
26
|
+
* @returns List of modified file paths
|
|
27
|
+
*/
|
|
28
|
+
export declare function getModifiedFiles(repoDir: string): Promise<string[]>;
|
|
29
|
+
/**
|
|
30
|
+
* Gets all untracked files (including files inside untracked directories).
|
|
31
|
+
* @param repoDir - Repository directory
|
|
32
|
+
* @returns List of untracked file paths
|
|
33
|
+
*/
|
|
34
|
+
export declare function getUntrackedFiles(repoDir: string): Promise<string[]>;
|
|
35
|
+
/**
|
|
36
|
+
* Gets untracked files within a specific directory.
|
|
37
|
+
* Uses path-scoped git ls-files for efficiency in large repos.
|
|
38
|
+
* @param repoDir - Repository directory
|
|
39
|
+
* @param dir - Directory path (relative to repo root)
|
|
40
|
+
* @returns List of untracked file paths relative to repo root
|
|
41
|
+
*/
|
|
42
|
+
export declare function getUntrackedFilesInDir(repoDir: string, dir: string): Promise<string[]>;
|
|
43
|
+
/**
|
|
44
|
+
* Gets modified (tracked) files within a specific directory.
|
|
45
|
+
* Uses path-scoped git diff for efficiency in large repos.
|
|
46
|
+
* @param repoDir - Repository directory
|
|
47
|
+
* @param dir - Directory path (relative to repo root)
|
|
48
|
+
* @returns List of modified file paths relative to repo root
|
|
49
|
+
*/
|
|
50
|
+
export declare function getModifiedFilesInDir(repoDir: string, dir: string): Promise<string[]>;
|
|
51
|
+
/**
|
|
52
|
+
* Checks if any of the specified files have uncommitted changes.
|
|
53
|
+
* @param repoDir - Repository directory
|
|
54
|
+
* @param files - File paths to check (relative to repo root)
|
|
55
|
+
* @returns List of dirty file paths
|
|
56
|
+
*/
|
|
57
|
+
export declare function getDirtyFiles(repoDir: string, files: string[]): Promise<string[]>;
|
|
58
|
+
/**
|
|
59
|
+
* Lists all files in a directory (tracked and untracked, respecting .gitignore).
|
|
60
|
+
* Combines git ls-files for tracked files and --others for untracked files.
|
|
61
|
+
* @param repoDir - Repository directory
|
|
62
|
+
* @param dir - Directory path (relative to repo root)
|
|
63
|
+
* @returns List of file paths relative to repo root
|
|
64
|
+
*/
|
|
65
|
+
export declare function listAllFilesInDir(repoDir: string, dir: string): Promise<string[]>;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { exec } from '../utils/process.js';
|
|
3
|
+
import { ensureGit } from './git-base.js';
|
|
4
|
+
/**
|
|
5
|
+
* Parses NUL-delimited porcelain status output.
|
|
6
|
+
* @param output - Raw git status output
|
|
7
|
+
* @returns Parsed entries
|
|
8
|
+
*/
|
|
9
|
+
/** @internal Exported for testing */
|
|
10
|
+
export function parsePorcelainStatus(output) {
|
|
11
|
+
const records = output.split('\0').filter((record) => record.length > 0);
|
|
12
|
+
const entries = [];
|
|
13
|
+
for (let i = 0; i < records.length; i++) {
|
|
14
|
+
const record = records[i];
|
|
15
|
+
if (!record || record.length < 4)
|
|
16
|
+
continue;
|
|
17
|
+
const indexStatus = record[0] ?? ' ';
|
|
18
|
+
const worktreeStatus = record[1] ?? ' ';
|
|
19
|
+
const status = `${indexStatus}${worktreeStatus}`;
|
|
20
|
+
const pathField = record.slice(3);
|
|
21
|
+
const isRenameOrCopy = indexStatus === 'R' || indexStatus === 'C';
|
|
22
|
+
const originalPath = isRenameOrCopy ? records[i + 1] : undefined;
|
|
23
|
+
if (isRenameOrCopy) {
|
|
24
|
+
i++;
|
|
25
|
+
}
|
|
26
|
+
entries.push({
|
|
27
|
+
status,
|
|
28
|
+
indexStatus,
|
|
29
|
+
worktreeStatus,
|
|
30
|
+
file: pathField,
|
|
31
|
+
...(originalPath !== undefined ? { originalPath } : {}),
|
|
32
|
+
isUntracked: indexStatus === '?' && worktreeStatus === '?',
|
|
33
|
+
isRenameOrCopy,
|
|
34
|
+
isDeleted: indexStatus === 'D' || worktreeStatus === 'D',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return entries;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Gets structured working tree status entries.
|
|
41
|
+
* @param repoDir - Repository directory
|
|
42
|
+
* @returns Parsed git status entries
|
|
43
|
+
*/
|
|
44
|
+
export async function getWorkingTreeStatus(repoDir) {
|
|
45
|
+
await ensureGit();
|
|
46
|
+
const result = await exec('git', ['status', '--porcelain=v1', '-z'], { cwd: repoDir });
|
|
47
|
+
return parsePorcelainStatus(result.stdout);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Expands collapsed untracked directory entries into individual file entries.
|
|
51
|
+
* Git status may report "?? dir/" instead of listing each file underneath.
|
|
52
|
+
* @param repoDir - Repository directory
|
|
53
|
+
* @param entries - Parsed status entries
|
|
54
|
+
* @returns Status entries with untracked directories expanded to individual files
|
|
55
|
+
*/
|
|
56
|
+
export async function expandUntrackedDirectoryEntries(repoDir, entries) {
|
|
57
|
+
const expanded = [];
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
if (!entry.isUntracked || !entry.file.endsWith('/')) {
|
|
60
|
+
expanded.push(entry);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const individualFiles = await getUntrackedFilesInDir(repoDir, entry.file);
|
|
64
|
+
for (const file of individualFiles) {
|
|
65
|
+
expanded.push({
|
|
66
|
+
status: '??',
|
|
67
|
+
indexStatus: '?',
|
|
68
|
+
worktreeStatus: '?',
|
|
69
|
+
file,
|
|
70
|
+
isUntracked: true,
|
|
71
|
+
isRenameOrCopy: false,
|
|
72
|
+
isDeleted: false,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return expanded;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Gets the list of modified files.
|
|
80
|
+
* @param repoDir - Repository directory
|
|
81
|
+
* @returns List of modified file paths
|
|
82
|
+
*/
|
|
83
|
+
export async function getModifiedFiles(repoDir) {
|
|
84
|
+
const entries = await getWorkingTreeStatus(repoDir);
|
|
85
|
+
return entries.map((entry) => entry.file);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Gets all untracked files (including files inside untracked directories).
|
|
89
|
+
* @param repoDir - Repository directory
|
|
90
|
+
* @returns List of untracked file paths
|
|
91
|
+
*/
|
|
92
|
+
export async function getUntrackedFiles(repoDir) {
|
|
93
|
+
await ensureGit();
|
|
94
|
+
// Use git ls-files to get all untracked files, which properly expands directories
|
|
95
|
+
const result = await exec('git', ['ls-files', '--others', '--exclude-standard'], {
|
|
96
|
+
cwd: repoDir,
|
|
97
|
+
});
|
|
98
|
+
return result.stdout.split('\n').filter((line) => line.trim().length > 0);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Gets untracked files within a specific directory.
|
|
102
|
+
* Uses path-scoped git ls-files for efficiency in large repos.
|
|
103
|
+
* @param repoDir - Repository directory
|
|
104
|
+
* @param dir - Directory path (relative to repo root)
|
|
105
|
+
* @returns List of untracked file paths relative to repo root
|
|
106
|
+
*/
|
|
107
|
+
export async function getUntrackedFilesInDir(repoDir, dir) {
|
|
108
|
+
await ensureGit();
|
|
109
|
+
const result = await exec('git', ['ls-files', '--others', '--exclude-standard', '--', dir], {
|
|
110
|
+
cwd: repoDir,
|
|
111
|
+
});
|
|
112
|
+
return result.stdout.split('\n').filter((line) => line.trim().length > 0);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Gets modified (tracked) files within a specific directory.
|
|
116
|
+
* Uses path-scoped git diff for efficiency in large repos.
|
|
117
|
+
* @param repoDir - Repository directory
|
|
118
|
+
* @param dir - Directory path (relative to repo root)
|
|
119
|
+
* @returns List of modified file paths relative to repo root
|
|
120
|
+
*/
|
|
121
|
+
export async function getModifiedFilesInDir(repoDir, dir) {
|
|
122
|
+
await ensureGit();
|
|
123
|
+
const result = await exec('git', ['diff', '--name-only', 'HEAD', '--', dir], { cwd: repoDir });
|
|
124
|
+
return result.stdout.split('\n').filter((line) => line.trim().length > 0);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Checks if any of the specified files have uncommitted changes.
|
|
128
|
+
* @param repoDir - Repository directory
|
|
129
|
+
* @param files - File paths to check (relative to repo root)
|
|
130
|
+
* @returns List of dirty file paths
|
|
131
|
+
*/
|
|
132
|
+
export async function getDirtyFiles(repoDir, files) {
|
|
133
|
+
if (files.length === 0)
|
|
134
|
+
return [];
|
|
135
|
+
await ensureGit();
|
|
136
|
+
// Check both staged and unstaged changes for the given files
|
|
137
|
+
const result = await exec('git', ['diff', '--name-only', 'HEAD', '--', ...files], {
|
|
138
|
+
cwd: repoDir,
|
|
139
|
+
});
|
|
140
|
+
const tracked = result.stdout.split('\n').filter((line) => line.trim().length > 0);
|
|
141
|
+
// Also check for untracked files
|
|
142
|
+
const untrackedResult = await exec('git', ['ls-files', '--others', '--exclude-standard', '--', ...files], { cwd: repoDir });
|
|
143
|
+
const untracked = untrackedResult.stdout.split('\n').filter((line) => line.trim().length > 0);
|
|
144
|
+
return [...new Set([...tracked, ...untracked])].sort();
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Lists all files in a directory (tracked and untracked, respecting .gitignore).
|
|
148
|
+
* Combines git ls-files for tracked files and --others for untracked files.
|
|
149
|
+
* @param repoDir - Repository directory
|
|
150
|
+
* @param dir - Directory path (relative to repo root)
|
|
151
|
+
* @returns List of file paths relative to repo root
|
|
152
|
+
*/
|
|
153
|
+
export async function listAllFilesInDir(repoDir, dir) {
|
|
154
|
+
await ensureGit();
|
|
155
|
+
const tracked = await exec('git', ['ls-files', '--', dir], { cwd: repoDir });
|
|
156
|
+
const trackedFiles = tracked.stdout.split('\n').filter((line) => line.trim().length > 0);
|
|
157
|
+
const untracked = await exec('git', ['ls-files', '--others', '--exclude-standard', '--', dir], {
|
|
158
|
+
cwd: repoDir,
|
|
159
|
+
});
|
|
160
|
+
const untrackedFiles = untracked.stdout.split('\n').filter((line) => line.trim().length > 0);
|
|
161
|
+
return [...new Set([...trackedFiles, ...untrackedFiles])].sort();
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=git-status.js.map
|