@aspruyt/xfg 5.7.0 → 6.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/sync-command.js +35 -37
- package/dist/cli/types.d.ts +12 -11
- package/dist/{output → cli}/unified-summary.d.ts +3 -3
- package/dist/{output → cli}/unified-summary.js +4 -4
- package/dist/config/file-reference-resolver.js +24 -56
- package/dist/config/normalizer.js +29 -40
- package/dist/config/validator.js +94 -102
- package/dist/lifecycle/ado-migration-source.d.ts +1 -1
- package/dist/lifecycle/ado-migration-source.js +1 -1
- package/dist/lifecycle/github-lifecycle-provider.d.ts +5 -6
- package/dist/lifecycle/github-lifecycle-provider.js +51 -20
- package/dist/lifecycle/lifecycle-formatter.d.ts +2 -1
- package/dist/lifecycle/lifecycle-formatter.js +1 -1
- package/dist/lifecycle/lifecycle-helpers.d.ts +1 -1
- package/dist/lifecycle/repo-lifecycle-manager.d.ts +1 -1
- package/dist/lifecycle/repo-lifecycle-manager.js +16 -6
- package/dist/lifecycle/types.d.ts +30 -8
- package/dist/output/lifecycle-report.d.ts +4 -2
- package/dist/output/settings-report.d.ts +4 -4
- package/dist/repo/detector.d.ts +8 -0
- package/dist/{shared/repo-detector.js → repo/detector.js} +1 -4
- package/dist/repo/index.d.ts +4 -0
- package/dist/repo/index.js +3 -0
- package/dist/{shared/repo-metadata-provider.d.ts → repo/metadata-provider.d.ts} +3 -3
- package/dist/{shared/repo-metadata-provider.js → repo/metadata-provider.js} +3 -3
- package/dist/{shared/repo-detector.d.ts → repo/types.d.ts} +1 -7
- package/dist/repo/types.js +1 -0
- package/dist/{shared/repo-info-utils.d.ts → repo/utils.d.ts} +1 -1
- package/dist/{shared/repo-info-utils.js → repo/utils.js} +1 -1
- package/dist/settings/base-processor.d.ts +1 -1
- package/dist/settings/base-processor.js +1 -1
- package/dist/settings/code-scanning/github-code-scanning-strategy.d.ts +1 -1
- package/dist/settings/code-scanning/github-code-scanning-strategy.js +1 -1
- package/dist/settings/code-scanning/processor.d.ts +2 -2
- package/dist/settings/code-scanning/types.d.ts +1 -1
- package/dist/settings/index.d.ts +1 -1
- package/dist/settings/labels/formatter.js +16 -11
- package/dist/settings/labels/github-labels-strategy.d.ts +1 -1
- package/dist/settings/labels/github-labels-strategy.js +1 -1
- package/dist/settings/labels/processor.d.ts +1 -1
- package/dist/settings/labels/types.d.ts +1 -1
- package/dist/settings/repo-settings/diff.d.ts +1 -1
- package/dist/settings/repo-settings/diff.js +1 -1
- package/dist/settings/repo-settings/formatter.js +2 -4
- package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +4 -4
- package/dist/settings/repo-settings/github-repo-settings-strategy.js +4 -4
- package/dist/settings/repo-settings/processor.d.ts +2 -2
- package/dist/settings/repo-settings/processor.js +5 -5
- package/dist/settings/repo-settings/types.d.ts +4 -4
- package/dist/settings/rulesets/diff-algorithm.js +1 -1
- package/dist/settings/rulesets/formatter.js +0 -3
- package/dist/settings/rulesets/github-ruleset-strategy.d.ts +1 -1
- package/dist/settings/rulesets/github-ruleset-strategy.js +1 -1
- package/dist/settings/rulesets/processor.d.ts +1 -1
- package/dist/settings/rulesets/types.d.ts +1 -1
- package/dist/shared/command-executor.js +3 -3
- package/dist/shared/gh-api-utils.d.ts +7 -4
- package/dist/shared/gh-api-utils.js +2 -2
- package/dist/shared/retry-utils.js +1 -1
- package/dist/shared/xfg-template.d.ts +22 -2
- package/dist/sync/auth-options-builder.d.ts +1 -1
- package/dist/sync/auth-options-builder.js +1 -1
- package/dist/sync/branch-manager.d.ts +1 -1
- package/dist/sync/commit-push-manager.d.ts +2 -2
- package/dist/sync/commit-push-manager.js +5 -3
- package/dist/sync/file-sync-orchestrator.d.ts +1 -1
- package/dist/sync/file-sync-strategy.d.ts +1 -1
- package/dist/sync/file-writer.js +44 -10
- package/dist/sync/repository-processor.d.ts +1 -1
- package/dist/sync/repository-session.d.ts +1 -1
- package/dist/sync/sync-workflow.d.ts +1 -1
- package/dist/sync/sync-workflow.js +2 -1
- package/dist/sync/types.d.ts +7 -4
- package/dist/vcs/{azure-pr-strategy.d.ts → ado-pr-strategy.d.ts} +2 -2
- package/dist/vcs/{azure-pr-strategy.js → ado-pr-strategy.js} +4 -4
- package/dist/vcs/authenticated-git-ops.d.ts +2 -0
- package/dist/vcs/authenticated-git-ops.js +6 -0
- package/dist/vcs/commit-strategy-selector.d.ts +2 -2
- package/dist/vcs/commit-strategy-selector.js +2 -2
- package/dist/vcs/file-mode-fixup-commit-strategy.d.ts +8 -6
- package/dist/vcs/file-mode-fixup-commit-strategy.js +79 -30
- package/dist/vcs/git-ops.d.ts +15 -3
- package/dist/vcs/git-ops.js +57 -24
- package/dist/vcs/github-app-token-manager.d.ts +3 -3
- package/dist/vcs/github-app-token-manager.js +4 -4
- package/dist/vcs/github-pr-strategy.d.ts +1 -1
- package/dist/vcs/github-pr-strategy.js +4 -4
- package/dist/vcs/gitlab-pr-strategy.d.ts +1 -1
- package/dist/vcs/gitlab-pr-strategy.js +4 -4
- package/dist/vcs/graphql-commit-strategy.js +8 -3
- package/dist/vcs/index.d.ts +1 -1
- package/dist/vcs/pr-creator.d.ts +1 -1
- package/dist/vcs/pr-strategy-factory.d.ts +1 -1
- package/dist/vcs/pr-strategy-factory.js +3 -3
- package/dist/vcs/pr-strategy.d.ts +1 -1
- package/dist/vcs/pr-strategy.js +1 -1
- package/dist/vcs/types.d.ts +10 -3
- package/package.json +2 -2
- /package/dist/{shared → vcs}/sanitize-utils.d.ts +0 -0
- /package/dist/{shared → vcs}/sanitize-utils.js +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createCommitStrategy } from "../vcs/index.js";
|
|
2
|
-
import { getRepoDisplayName } from "../
|
|
2
|
+
import { getRepoDisplayName } from "../repo/index.js";
|
|
3
3
|
import { toErrorMessage } from "../shared/type-guards.js";
|
|
4
4
|
export class CommitPushManager {
|
|
5
5
|
log;
|
|
@@ -22,6 +22,7 @@ export class CommitPushManager {
|
|
|
22
22
|
path,
|
|
23
23
|
content: info.content,
|
|
24
24
|
...(info.mode ? { mode: info.mode } : {}),
|
|
25
|
+
...(info.modeOnly ? { modeOnly: true } : {}),
|
|
25
26
|
}));
|
|
26
27
|
this.log.info("Staging changes...");
|
|
27
28
|
await gitOps.stageAll();
|
|
@@ -35,6 +36,7 @@ export class CommitPushManager {
|
|
|
35
36
|
const result = await commitStrategy.commit({
|
|
36
37
|
repoInfo,
|
|
37
38
|
branchName: pushBranch,
|
|
39
|
+
baseBranch: options.baseBranch,
|
|
38
40
|
message: commitMessage,
|
|
39
41
|
fileChanges: changes,
|
|
40
42
|
workDir,
|
|
@@ -47,10 +49,10 @@ export class CommitPushManager {
|
|
|
47
49
|
return { success: true };
|
|
48
50
|
}
|
|
49
51
|
catch (error) {
|
|
50
|
-
return this.
|
|
52
|
+
return this.classifyCommitError(error, isDirectMode, pushBranch, repoInfo);
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
|
-
|
|
55
|
+
classifyCommitError(error, isDirectMode, pushBranch, repoInfo) {
|
|
54
56
|
const repoName = getRepoDisplayName(repoInfo);
|
|
55
57
|
const message = toErrorMessage(error);
|
|
56
58
|
if (isDirectMode &&
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RepoConfig } from "../config/index.js";
|
|
2
|
-
import type { RepoInfo } from "../
|
|
2
|
+
import type { RepoInfo } from "../repo/index.js";
|
|
3
3
|
import type { ILogger } from "../shared/logger.js";
|
|
4
4
|
import type { IFileWriter, IManifestManager, SessionContext, ProcessorOptions, FileSyncResult, IFileSyncOrchestrator } from "./types.js";
|
|
5
5
|
export declare class FileSyncOrchestrator implements IFileSyncOrchestrator {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RepoConfig } from "../config/index.js";
|
|
2
|
-
import type { RepoInfo } from "../
|
|
2
|
+
import type { RepoInfo } from "../repo/index.js";
|
|
3
3
|
import type { IWorkStrategy, WorkResult, SessionContext, ProcessorOptions, IFileSyncOrchestrator } from "./types.js";
|
|
4
4
|
/**
|
|
5
5
|
* Strategy that performs full file synchronization.
|
package/dist/sync/file-writer.js
CHANGED
|
@@ -30,6 +30,7 @@ export class FileWriter {
|
|
|
30
30
|
const { gitOps, log } = deps;
|
|
31
31
|
const fileChanges = new Map();
|
|
32
32
|
const diffStats = createDiffStats();
|
|
33
|
+
const modeCache = new Map();
|
|
33
34
|
for (const file of files) {
|
|
34
35
|
const filePath = join(workDir, file.fileName);
|
|
35
36
|
const fileExistsLocal = existsSync(filePath);
|
|
@@ -64,31 +65,54 @@ export class FileWriter {
|
|
|
64
65
|
const action = fileExistsLocal ? "update" : "create";
|
|
65
66
|
const existingContent = gitOps.getFileContent(file.fileName);
|
|
66
67
|
const changed = gitOps.wouldChange(file.fileName, fileContent);
|
|
68
|
+
const desiredMode = shouldBeExecutable(file)
|
|
69
|
+
? "100755"
|
|
70
|
+
: "100644";
|
|
71
|
+
const currentMode = await gitOps.getFileMode(file.fileName);
|
|
72
|
+
modeCache.set(file.fileName, currentMode);
|
|
73
|
+
const modeDiffers = currentMode !== null && currentMode !== desiredMode;
|
|
67
74
|
if (changed) {
|
|
68
75
|
const writeResult = {
|
|
69
76
|
fileName: file.fileName,
|
|
70
77
|
content: fileContent,
|
|
71
78
|
action,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
...(desiredMode === "100755" || modeDiffers
|
|
80
|
+
? { mode: desiredMode }
|
|
81
|
+
: {}),
|
|
75
82
|
};
|
|
76
|
-
// Compute raw diff lines for text files (all modes)
|
|
77
83
|
if (!isBinaryFile(file.fileName)) {
|
|
78
84
|
writeResult.diffLines = computeUnifiedDiff(existingContent, fileContent);
|
|
79
85
|
}
|
|
80
86
|
fileChanges.set(file.fileName, writeResult);
|
|
81
87
|
}
|
|
88
|
+
else if (modeDiffers) {
|
|
89
|
+
fileChanges.set(file.fileName, {
|
|
90
|
+
fileName: file.fileName,
|
|
91
|
+
content: null,
|
|
92
|
+
action: "update",
|
|
93
|
+
mode: desiredMode,
|
|
94
|
+
modeOnly: true,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
82
97
|
if (dryRun) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
98
|
+
if (changed) {
|
|
99
|
+
const status = getFileStatus(existingContent !== null, changed);
|
|
100
|
+
incrementDiffStats(diffStats, status);
|
|
101
|
+
const diffLines = generateDiff(existingContent, fileContent);
|
|
102
|
+
log.fileDiff(file.fileName, status, diffLines);
|
|
103
|
+
}
|
|
104
|
+
else if (modeDiffers) {
|
|
105
|
+
incrementDiffStats(diffStats, "MODIFIED");
|
|
106
|
+
log.info(`Would change mode: ${file.fileName} ${currentMode} -> ${desiredMode}`);
|
|
107
|
+
}
|
|
87
108
|
}
|
|
88
109
|
else if (changed) {
|
|
89
110
|
incrementDiffStats(diffStats, action === "create" ? "NEW" : "MODIFIED");
|
|
90
111
|
gitOps.writeFile(file.fileName, fileContent);
|
|
91
112
|
}
|
|
113
|
+
else if (modeDiffers) {
|
|
114
|
+
incrementDiffStats(diffStats, "MODIFIED");
|
|
115
|
+
}
|
|
92
116
|
}
|
|
93
117
|
// Separate pass for executable permissions: git add must happen after file
|
|
94
118
|
// content is written, and setExecutable needs the file to already be tracked.
|
|
@@ -97,10 +121,20 @@ export class FileWriter {
|
|
|
97
121
|
if (tracked?.action === "skip") {
|
|
98
122
|
continue;
|
|
99
123
|
}
|
|
100
|
-
|
|
101
|
-
|
|
124
|
+
const desired = shouldBeExecutable(file);
|
|
125
|
+
const currentMode = modeCache.get(file.fileName) ?? null;
|
|
126
|
+
if (desired && currentMode !== "100755") {
|
|
127
|
+
log.info(ctx.dryRun
|
|
128
|
+
? `Would set executable: ${file.fileName}`
|
|
129
|
+
: `Setting executable: ${file.fileName}`);
|
|
102
130
|
await gitOps.setExecutable(file.fileName);
|
|
103
131
|
}
|
|
132
|
+
else if (!desired && currentMode === "100755") {
|
|
133
|
+
log.info(ctx.dryRun
|
|
134
|
+
? `Would clear executable: ${file.fileName}`
|
|
135
|
+
: `Clearing executable: ${file.fileName}`);
|
|
136
|
+
await gitOps.clearExecutable(file.fileName);
|
|
137
|
+
}
|
|
104
138
|
}
|
|
105
139
|
return { fileChanges, diffStats };
|
|
106
140
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RepoConfig } from "../config/index.js";
|
|
2
|
-
import type { RepoInfo } from "../
|
|
2
|
+
import type { RepoInfo } from "../repo/index.js";
|
|
3
3
|
import type { ILogger } from "../shared/logger.js";
|
|
4
4
|
import { type GitHubAppTokenManager } from "../vcs/index.js";
|
|
5
5
|
import type { IFileWriter, IManifestManager, IBranchManager, IAuthOptionsBuilder, IRepositorySession, ICommitPushManager, IFileSyncOrchestrator, IPRMergeHandler, ISyncWorkflow, IRepositoryProcessor, GitOpsFactory, ProcessorOptions, ProcessorResult } from "./types.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RepoInfo } from "../
|
|
1
|
+
import type { RepoInfo } from "../repo/index.js";
|
|
2
2
|
import type { ILogger } from "../shared/logger.js";
|
|
3
3
|
import type { GitOpsFactory, SessionOptions, SessionContext, IRepositorySession } from "./types.js";
|
|
4
4
|
export declare class RepositorySession implements IRepositorySession {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RepoConfig } from "../config/index.js";
|
|
2
|
-
import { type RepoInfo } from "../
|
|
2
|
+
import { type RepoInfo } from "../repo/index.js";
|
|
3
3
|
import type { DebugInfoLog } from "../shared/logger.js";
|
|
4
4
|
import type { ISyncWorkflow, IWorkStrategy, IAuthOptionsBuilder, IRepositorySession, IBranchManager, ICommitPushManager, IPRMergeHandler, ProcessorOptions, ProcessorResult } from "./types.js";
|
|
5
5
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getRepoDisplayName } from "../
|
|
1
|
+
import { getRepoDisplayName } from "../repo/index.js";
|
|
2
2
|
import { safeCleanup } from "../shared/cleanup-utils.js";
|
|
3
3
|
/**
|
|
4
4
|
* Orchestrates the common sync workflow steps.
|
|
@@ -69,6 +69,7 @@ export class SyncWorkflow {
|
|
|
69
69
|
fileChanges: workResult.fileChanges,
|
|
70
70
|
commitMessage: workResult.commitMessage,
|
|
71
71
|
pushBranch,
|
|
72
|
+
baseBranch: session.baseBranch,
|
|
72
73
|
isDirectMode,
|
|
73
74
|
hasAppCredentials: options.hasAppCredentials,
|
|
74
75
|
});
|
package/dist/sync/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { FileContent, RepoConfig } from "../config/index.js";
|
|
2
|
-
import type { RepoInfo } from "../
|
|
3
|
-
import type {
|
|
2
|
+
import type { RepoInfo } from "../repo/index.js";
|
|
3
|
+
import type { ActiveAction } from "../settings/index.js";
|
|
4
|
+
import type { ILocalGitOps, IGitOps, GitAuthOptions, GitOpsOptions, FileAction, FileActionKind } from "../vcs/index.js";
|
|
4
5
|
import type { DiffStats } from "./diff-utils.js";
|
|
5
6
|
import type { ILogger } from "../shared/logger.js";
|
|
6
7
|
import type { XfgManifest } from "./manifest.js";
|
|
@@ -9,11 +10,12 @@ export type GitOpsFactory = (options: GitOpsOptions, auth?: GitAuthOptions, retr
|
|
|
9
10
|
export interface FileWriteResult {
|
|
10
11
|
fileName: string;
|
|
11
12
|
content: string | null;
|
|
12
|
-
action:
|
|
13
|
+
action: FileActionKind;
|
|
13
14
|
diffLines?: string[];
|
|
14
15
|
/** Git file mode. Only set for executable files ("100755"). "100644" is included
|
|
15
16
|
* in the union for type completeness — non-executable files omit this field. */
|
|
16
17
|
mode?: "100755" | "100644";
|
|
18
|
+
modeOnly?: true;
|
|
17
19
|
}
|
|
18
20
|
export interface FileWriteContext {
|
|
19
21
|
repoInfo: RepoInfo;
|
|
@@ -105,6 +107,7 @@ export interface CommitPushOptions extends RunContext {
|
|
|
105
107
|
fileChanges: Map<string, FileWriteResult>;
|
|
106
108
|
commitMessage: string;
|
|
107
109
|
pushBranch: string;
|
|
110
|
+
baseBranch: string;
|
|
108
111
|
isDirectMode: boolean;
|
|
109
112
|
hasAppCredentials?: boolean;
|
|
110
113
|
}
|
|
@@ -137,7 +140,7 @@ export interface ProcessorOptions {
|
|
|
137
140
|
}
|
|
138
141
|
export interface FileChangeDetail {
|
|
139
142
|
path: string;
|
|
140
|
-
action:
|
|
143
|
+
action: ActiveAction;
|
|
141
144
|
diffLines?: string[];
|
|
142
145
|
}
|
|
143
146
|
export interface ProcessorResult {
|
|
@@ -3,7 +3,7 @@ import { BasePRStrategy } from "./pr-strategy.js";
|
|
|
3
3
|
import type { IPRStrategyLogger } from "./pr-strategy.js";
|
|
4
4
|
import type { PRStrategyOptions, CloseExistingPROptions, MergeOptions, MergeResult } from "./types.js";
|
|
5
5
|
import type { ICommandExecutor } from "../shared/command-executor.js";
|
|
6
|
-
export declare class
|
|
6
|
+
export declare class AdoPRStrategy extends BasePRStrategy {
|
|
7
7
|
constructor(executor: ICommandExecutor, log?: IPRStrategyLogger);
|
|
8
8
|
private getOrgUrl;
|
|
9
9
|
private buildPRUrl;
|
|
@@ -12,7 +12,7 @@ export declare class AzurePRStrategy extends BasePRStrategy {
|
|
|
12
12
|
* Returns the raw PR ID string, or null if none found.
|
|
13
13
|
*/
|
|
14
14
|
private findExistingPRId;
|
|
15
|
-
|
|
15
|
+
findExistingPRUrl(options: CloseExistingPROptions): Promise<string | null>;
|
|
16
16
|
closeExistingPR(options: CloseExistingPROptions): Promise<boolean>;
|
|
17
17
|
create(options: PRStrategyOptions): Promise<PRResult>;
|
|
18
18
|
/**
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { existsSync, writeFileSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { escapeShellArg } from "../shared/shell-utils.js";
|
|
4
|
-
import { assertAzureDevOpsRepo, } from "../
|
|
4
|
+
import { assertAzureDevOpsRepo, } from "../repo/index.js";
|
|
5
5
|
import { SyncError } from "../shared/errors.js";
|
|
6
6
|
import { BasePRStrategy } from "./pr-strategy.js";
|
|
7
7
|
import { withRetry, isPermanentError } from "../shared/retry-utils.js";
|
|
8
8
|
import { toErrorMessage } from "../shared/type-guards.js";
|
|
9
9
|
import { safeCleanup } from "../shared/cleanup-utils.js";
|
|
10
10
|
import { NO_OP_DEBUG_LOG } from "../shared/logger.js";
|
|
11
|
-
import { sanitizeCredentials } from "
|
|
11
|
+
import { sanitizeCredentials } from "./sanitize-utils.js";
|
|
12
12
|
import { getStderr } from "../shared/command-executor.js";
|
|
13
|
-
export class
|
|
13
|
+
export class AdoPRStrategy extends BasePRStrategy {
|
|
14
14
|
constructor(executor, log) {
|
|
15
15
|
super(executor, log);
|
|
16
16
|
this.bodyFilePath = ".pr-description.md";
|
|
@@ -43,7 +43,7 @@ export class AzurePRStrategy extends BasePRStrategy {
|
|
|
43
43
|
return null;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
-
async
|
|
46
|
+
async findExistingPRUrl(options) {
|
|
47
47
|
const { repoInfo, branchName, baseBranch, workDir, retries = 3 } = options;
|
|
48
48
|
assertAzureDevOpsRepo(repoInfo, "Azure PR strategy");
|
|
49
49
|
const azureRepoInfo = repoInfo;
|
|
@@ -33,6 +33,8 @@ export declare class AuthenticatedGitOps implements IGitOps {
|
|
|
33
33
|
createBranch(branchName: string): Promise<void>;
|
|
34
34
|
writeFile(fileName: string, content: string): void;
|
|
35
35
|
setExecutable(fileName: string): Promise<void>;
|
|
36
|
+
clearExecutable(fileName: string): Promise<void>;
|
|
37
|
+
getFileMode(fileName: string): Promise<"100755" | "100644" | null>;
|
|
36
38
|
getFileContent(fileName: string): string | null;
|
|
37
39
|
wouldChange(fileName: string, content: string): boolean;
|
|
38
40
|
hasChanges(): Promise<boolean>;
|
|
@@ -45,6 +45,12 @@ export class AuthenticatedGitOps {
|
|
|
45
45
|
setExecutable(fileName) {
|
|
46
46
|
return this.localOps.setExecutable(fileName);
|
|
47
47
|
}
|
|
48
|
+
clearExecutable(fileName) {
|
|
49
|
+
return this.localOps.clearExecutable(fileName);
|
|
50
|
+
}
|
|
51
|
+
getFileMode(fileName) {
|
|
52
|
+
return this.localOps.getFileMode(fileName);
|
|
53
|
+
}
|
|
48
54
|
getFileContent(fileName) {
|
|
49
55
|
return this.localOps.getFileContent(fileName);
|
|
50
56
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { type RepoInfo } from "../
|
|
1
|
+
import { type RepoInfo } from "../repo/index.js";
|
|
2
2
|
import type { ICommitStrategy } from "./types.js";
|
|
3
3
|
import { GitHubAppTokenManager } from "./github-app-token-manager.js";
|
|
4
4
|
import type { ICommandExecutor } from "../shared/command-executor.js";
|
|
5
5
|
interface GitHubAppCredentials {
|
|
6
|
-
|
|
6
|
+
clientId: string;
|
|
7
7
|
privateKey: string;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isGitHubRepo } from "../
|
|
1
|
+
import { isGitHubRepo } from "../repo/index.js";
|
|
2
2
|
import { GitCommitStrategy } from "./git-commit-strategy.js";
|
|
3
3
|
import { GraphQLCommitStrategy } from "./graphql-commit-strategy.js";
|
|
4
4
|
import { FileModeFixupCommitStrategy } from "./file-mode-fixup-commit-strategy.js";
|
|
@@ -10,7 +10,7 @@ export function createTokenManager(credentials) {
|
|
|
10
10
|
if (!credentials) {
|
|
11
11
|
return null;
|
|
12
12
|
}
|
|
13
|
-
return new GitHubAppTokenManager(credentials.
|
|
13
|
+
return new GitHubAppTokenManager(credentials.clientId, credentials.privateKey);
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* Returns FileModeFixupCommitStrategy (decorating GraphQLCommitStrategy) for
|
|
@@ -9,10 +9,11 @@ export type GhApiClientFactory = (executor: ICommandExecutor, retries: number, c
|
|
|
9
9
|
* The GitHub GraphQL createCommitOnBranch mutation cannot set file modes.
|
|
10
10
|
* After the inner strategy (GraphQLCommitStrategy) creates the content commit,
|
|
11
11
|
* this decorator creates a second commit via the REST Git Data API that
|
|
12
|
-
* patches tree modes
|
|
12
|
+
* patches tree modes to match the desired mode (100755 or 100644).
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
* When no
|
|
14
|
+
* Activates when fileChanges contain entries with an explicit `mode` field
|
|
15
|
+
* or `modeOnly` flag. When no such entries are present, delegates directly
|
|
16
|
+
* to the inner strategy.
|
|
16
17
|
*/
|
|
17
18
|
export declare class FileModeFixupCommitStrategy implements ICommitStrategy {
|
|
18
19
|
private readonly inner;
|
|
@@ -20,12 +21,13 @@ export declare class FileModeFixupCommitStrategy implements ICommitStrategy {
|
|
|
20
21
|
private readonly clientFactory;
|
|
21
22
|
constructor(inner: ICommitStrategy, executor: ICommandExecutor, clientFactory?: GhApiClientFactory);
|
|
22
23
|
commit(options: CommitOptions): Promise<CommitResult>;
|
|
24
|
+
private resolveBranchHeadSha;
|
|
23
25
|
/**
|
|
24
|
-
* Create a fixup commit that
|
|
26
|
+
* Create a fixup commit that patches file modes (100644 ↔ 100755).
|
|
25
27
|
*
|
|
26
28
|
* Flow:
|
|
27
|
-
* 1. GET the
|
|
28
|
-
* 2. GET the tree (recursive) to find blob SHAs for
|
|
29
|
+
* 1. GET the parent commit to find its tree SHA
|
|
30
|
+
* 2. GET the tree (recursive) to find blob SHAs for target files
|
|
29
31
|
* 3. POST a new tree with updated modes (base_tree carries forward unchanged)
|
|
30
32
|
* 4. POST a new commit with the new tree
|
|
31
33
|
* 5. PATCH the branch ref to point to the new commit
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { isGitHubRepo } from "../
|
|
1
|
+
import { isGitHubRepo } from "../repo/index.js";
|
|
2
2
|
import { GhApiClient } from "../shared/gh-api-utils.js";
|
|
3
3
|
import { parseApiJson } from "../shared/json-utils.js";
|
|
4
4
|
import { SyncError } from "../shared/errors.js";
|
|
5
|
+
import { validateSafeBranchName } from "./graphql-commit-strategy.js";
|
|
5
6
|
const defaultClientFactory = (executor, retries, cwd) => new GhApiClient(executor, retries, cwd);
|
|
6
7
|
/**
|
|
7
8
|
* Decorator that adds a follow-up commit to fix executable file modes.
|
|
@@ -9,10 +10,11 @@ const defaultClientFactory = (executor, retries, cwd) => new GhApiClient(executo
|
|
|
9
10
|
* The GitHub GraphQL createCommitOnBranch mutation cannot set file modes.
|
|
10
11
|
* After the inner strategy (GraphQLCommitStrategy) creates the content commit,
|
|
11
12
|
* this decorator creates a second commit via the REST Git Data API that
|
|
12
|
-
* patches tree modes
|
|
13
|
+
* patches tree modes to match the desired mode (100755 or 100644).
|
|
13
14
|
*
|
|
14
|
-
*
|
|
15
|
-
* When no
|
|
15
|
+
* Activates when fileChanges contain entries with an explicit `mode` field
|
|
16
|
+
* or `modeOnly` flag. When no such entries are present, delegates directly
|
|
17
|
+
* to the inner strategy.
|
|
16
18
|
*/
|
|
17
19
|
export class FileModeFixupCommitStrategy {
|
|
18
20
|
inner;
|
|
@@ -24,26 +26,72 @@ export class FileModeFixupCommitStrategy {
|
|
|
24
26
|
this.clientFactory = clientFactory;
|
|
25
27
|
}
|
|
26
28
|
async commit(options) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
29
|
+
validateSafeBranchName(options.branchName);
|
|
30
|
+
const executableFiles = options.fileChanges.filter((fc) => fc.modeOnly === true || fc.mode !== undefined);
|
|
31
|
+
const hasContentChanges = options.fileChanges.some((fc) => !fc.modeOnly);
|
|
30
32
|
if (executableFiles.length === 0) {
|
|
31
|
-
return
|
|
33
|
+
return this.inner.commit(options);
|
|
32
34
|
}
|
|
33
|
-
// Safety net: only GitHub repos use the REST Git Data API for fixup.
|
|
34
|
-
// Currently only composed for GitHub repos in createCommitStrategy(),
|
|
35
|
-
// but guard defensively in case the decorator is reused elsewhere.
|
|
36
35
|
if (!isGitHubRepo(options.repoInfo)) {
|
|
37
|
-
return
|
|
36
|
+
return this.inner.commit(options);
|
|
37
|
+
}
|
|
38
|
+
let parentSha;
|
|
39
|
+
let baseResult;
|
|
40
|
+
if (hasContentChanges) {
|
|
41
|
+
baseResult = await this.inner.commit(options);
|
|
42
|
+
parentSha = baseResult.sha;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
parentSha = await this.resolveBranchHeadSha(options.repoInfo, options.branchName, options.baseBranch, options.workDir, options.retries ?? 3, options.token);
|
|
46
|
+
baseResult = { sha: parentSha, verified: true, pushed: true };
|
|
47
|
+
}
|
|
48
|
+
return await this.createFixupCommit(options.repoInfo, options.branchName, baseResult, executableFiles, options.workDir, options.retries ?? 3, options.token);
|
|
49
|
+
}
|
|
50
|
+
async resolveBranchHeadSha(repoInfo, branchName, baseBranch, workDir, retries, token) {
|
|
51
|
+
validateSafeBranchName(branchName);
|
|
52
|
+
if (baseBranch !== undefined) {
|
|
53
|
+
validateSafeBranchName(baseBranch);
|
|
54
|
+
}
|
|
55
|
+
const client = this.clientFactory(this.executor, retries, workDir);
|
|
56
|
+
const apiOpts = { token, host: repoInfo.host };
|
|
57
|
+
const repoPath = `repos/${repoInfo.owner}/${repoInfo.repo}`;
|
|
58
|
+
const getBranchRef = async (ref) => {
|
|
59
|
+
const raw = await client.call("GET", `${repoPath}/git/ref/heads/${ref}`, {
|
|
60
|
+
options: apiOpts,
|
|
61
|
+
});
|
|
62
|
+
const parsed = parseApiJson(raw, "GET git ref");
|
|
63
|
+
return parsed.object.sha;
|
|
64
|
+
};
|
|
65
|
+
try {
|
|
66
|
+
return await getBranchRef(branchName);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
const is404 = err instanceof Error && /\b404\b|Not Found/i.test(err.message);
|
|
70
|
+
if (!is404 || !baseBranch)
|
|
71
|
+
throw err;
|
|
72
|
+
const baseSha = await getBranchRef(baseBranch);
|
|
73
|
+
try {
|
|
74
|
+
await client.call("POST", `${repoPath}/git/refs`, {
|
|
75
|
+
payload: { ref: `refs/heads/${branchName}`, sha: baseSha },
|
|
76
|
+
options: apiOpts,
|
|
77
|
+
});
|
|
78
|
+
return baseSha;
|
|
79
|
+
}
|
|
80
|
+
catch (createErr) {
|
|
81
|
+
const alreadyExists = createErr instanceof Error &&
|
|
82
|
+
/Reference already exists/i.test(createErr.message);
|
|
83
|
+
if (!alreadyExists)
|
|
84
|
+
throw createErr;
|
|
85
|
+
return await getBranchRef(branchName);
|
|
86
|
+
}
|
|
38
87
|
}
|
|
39
|
-
return await this.createFixupCommit(options.repoInfo, options.branchName, innerResult, executableFiles, options.workDir, options.retries ?? 3, options.token);
|
|
40
88
|
}
|
|
41
89
|
/**
|
|
42
|
-
* Create a fixup commit that
|
|
90
|
+
* Create a fixup commit that patches file modes (100644 ↔ 100755).
|
|
43
91
|
*
|
|
44
92
|
* Flow:
|
|
45
|
-
* 1. GET the
|
|
46
|
-
* 2. GET the tree (recursive) to find blob SHAs for
|
|
93
|
+
* 1. GET the parent commit to find its tree SHA
|
|
94
|
+
* 2. GET the tree (recursive) to find blob SHAs for target files
|
|
47
95
|
* 3. POST a new tree with updated modes (base_tree carries forward unchanged)
|
|
48
96
|
* 4. POST a new commit with the new tree
|
|
49
97
|
* 5. PATCH the branch ref to point to the new commit
|
|
@@ -63,31 +111,32 @@ export class FileModeFixupCommitStrategy {
|
|
|
63
111
|
// 2. Get tree entries to find blob SHAs
|
|
64
112
|
const treeRaw = await client.call("GET", `${repoPath}/git/trees/${treeSha}?recursive=1`, { options: apiOpts });
|
|
65
113
|
const treeData = parseApiJson(treeRaw, "GET git tree");
|
|
66
|
-
const executablePaths = new Set(executableFiles.map((f) => f.path));
|
|
67
114
|
const treeEntries = [];
|
|
115
|
+
const requestedByPath = new Map(executableFiles.map((f) => [f.path, f]));
|
|
68
116
|
for (const entry of treeData.tree) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
117
|
+
const requested = requestedByPath.get(entry.path);
|
|
118
|
+
if (!requested || entry.type !== "blob")
|
|
119
|
+
continue;
|
|
120
|
+
const desiredMode = requested.mode ?? "100755";
|
|
121
|
+
if (entry.mode === desiredMode)
|
|
122
|
+
continue;
|
|
123
|
+
treeEntries.push({
|
|
124
|
+
path: entry.path,
|
|
125
|
+
mode: desiredMode,
|
|
126
|
+
type: "blob",
|
|
127
|
+
sha: entry.sha,
|
|
128
|
+
});
|
|
79
129
|
}
|
|
80
|
-
// If tree was truncated (>100k entries), check that all executable files were found
|
|
81
130
|
if (treeData.truncated) {
|
|
82
131
|
const foundPaths = new Set(treeData.tree.filter((e) => e.type === "blob").map((e) => e.path));
|
|
83
|
-
const missing = [...
|
|
132
|
+
const missing = [...requestedByPath.keys()].filter((p) => !foundPaths.has(p));
|
|
84
133
|
if (missing.length > 0) {
|
|
85
134
|
throw new SyncError(`File mode fixup incomplete: tree response was truncated (>100k entries) ` +
|
|
86
135
|
`and ${missing.length} executable file(s) were not found: ${missing.join(", ")}`);
|
|
87
136
|
}
|
|
88
137
|
}
|
|
89
138
|
if (treeEntries.length === 0) {
|
|
90
|
-
// All requested files
|
|
139
|
+
// All requested files already have the desired mode or are absent from the tree.
|
|
91
140
|
// Absent files in a non-truncated tree means createCommitOnBranch did not
|
|
92
141
|
// include them (e.g., concurrent deletion) — safe to skip since there is
|
|
93
142
|
// no blob to patch.
|
package/dist/vcs/git-ops.d.ts
CHANGED
|
@@ -9,9 +9,9 @@ export interface GitOpsOptions {
|
|
|
9
9
|
log?: DebugLog;
|
|
10
10
|
}
|
|
11
11
|
export declare class GitOps implements ILocalGitOps {
|
|
12
|
-
private readonly
|
|
12
|
+
private readonly workDir;
|
|
13
13
|
private readonly dryRun;
|
|
14
|
-
private readonly
|
|
14
|
+
private readonly executor;
|
|
15
15
|
private readonly log?;
|
|
16
16
|
constructor(options: GitOpsOptions);
|
|
17
17
|
private exec;
|
|
@@ -36,6 +36,18 @@ export declare class GitOps implements ILocalGitOps {
|
|
|
36
36
|
* @param fileName - The file path relative to the work directory
|
|
37
37
|
*/
|
|
38
38
|
setExecutable(fileName: string): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Clears the executable bit on a file both on the filesystem and in git's index.
|
|
41
|
+
* Symmetric inverse of setExecutable.
|
|
42
|
+
* @param fileName - The file path relative to the work directory
|
|
43
|
+
*/
|
|
44
|
+
clearExecutable(fileName: string): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Returns the git index mode for a tracked file ("100755" or "100644"),
|
|
47
|
+
* or null if the file is not tracked.
|
|
48
|
+
* @param fileName - The file path relative to the work directory
|
|
49
|
+
*/
|
|
50
|
+
getFileMode(fileName: string): Promise<"100755" | "100644" | null>;
|
|
39
51
|
/**
|
|
40
52
|
* Get the content of a file in the workspace.
|
|
41
53
|
* Returns null if the file doesn't exist.
|
|
@@ -70,7 +82,7 @@ export declare class GitOps implements ILocalGitOps {
|
|
|
70
82
|
/**
|
|
71
83
|
* Stage all changes and commit with the given message.
|
|
72
84
|
* Uses --no-verify to skip pre-commit hooks (config sync should always succeed).
|
|
73
|
-
* @returns true if a commit was made
|
|
85
|
+
* @returns true if a commit was made, or false if there were no staged changes. In dry-run mode, always returns true without inspecting the working tree.
|
|
74
86
|
*/
|
|
75
87
|
commit(message: string): Promise<boolean>;
|
|
76
88
|
/**
|