@kamaalio/codemod-kit 0.0.30 → 0.0.31
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/codemods/index.d.ts +2 -2
- package/dist/codemods/types.d.ts +8 -0
- package/dist/codemods/utils.d.ts +9 -5
- package/dist/git/branch.d.ts +10 -0
- package/dist/git/errors.d.ts +30 -0
- package/dist/git/index.d.ts +3 -0
- package/dist/git/repository.d.ts +41 -0
- package/dist/git/schemas.d.ts +18 -0
- package/dist/git/utils.d.ts +4 -0
- package/dist/github/index.d.ts +1 -0
- package/dist/github/utils.d.ts +11 -0
- package/dist/index.cjs +417 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +415 -3
- package/dist/utils/results.d.ts +7 -0
- package/package.json +4 -2
package/dist/codemods/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { runCodemods, runCodemod, commitEditModifications, findAndReplace, findAndReplaceEdits, findAndReplaceConfig, findAndReplaceConfigModifications, traverseUp, } from './utils.js';
|
|
2
|
-
export type { Codemod, Modifications, FindAndReplaceConfig, RunCodemodOkResult, RunCodemodResult } from './types.js';
|
|
1
|
+
export { runCodemods, runCodemod, commitEditModifications, findAndReplace, findAndReplaceEdits, findAndReplaceConfig, findAndReplaceConfigModifications, traverseUp, runCodemodsOnProjects, } from './utils.js';
|
|
2
|
+
export type { Codemod, Modifications, FindAndReplaceConfig, RunCodemodOkResult, RunCodemodResult, CodemodRunnerCodemod, RepositoryToClone, } from './types.js';
|
package/dist/codemods/types.d.ts
CHANGED
|
@@ -19,6 +19,14 @@ export type Codemod = {
|
|
|
19
19
|
results: Array<RunCodemodOkResult>;
|
|
20
20
|
}) => Promise<void>;
|
|
21
21
|
};
|
|
22
|
+
export type CodemodRunnerCodemod<Tag = string, C extends Codemod = Codemod> = C & {
|
|
23
|
+
tags: Set<Tag> | Array<Tag>;
|
|
24
|
+
commitMessage: string;
|
|
25
|
+
};
|
|
26
|
+
export type RepositoryToClone<Tag = string> = {
|
|
27
|
+
address: string;
|
|
28
|
+
tags: Set<Tag>;
|
|
29
|
+
};
|
|
22
30
|
export type ModificationsReport = {
|
|
23
31
|
changesApplied: number;
|
|
24
32
|
};
|
package/dist/codemods/utils.d.ts
CHANGED
|
@@ -2,20 +2,24 @@ import { type Edit, type SgRoot, type SgNode } from '@ast-grep/napi';
|
|
|
2
2
|
import type { Kinds, TypesMap } from '@ast-grep/napi/types/staticTypes.js';
|
|
3
3
|
import type { NapiLang } from '@ast-grep/napi/types/lang.js';
|
|
4
4
|
import { type types } from '@kamaalio/kamaal';
|
|
5
|
-
import type { Codemod, FindAndReplaceConfig, Modifications, RunCodemodResult } from './types.js';
|
|
6
|
-
type RunCodemodHooks<C extends Codemod> = {
|
|
5
|
+
import type { Codemod, CodemodRunnerCodemod, FindAndReplaceConfig, Modifications, RepositoryToClone, RunCodemodResult } from './types.js';
|
|
6
|
+
type RunCodemodHooks<C extends Codemod = Codemod> = {
|
|
7
7
|
targetFiltering?: (filepath: string, codemod: C) => boolean;
|
|
8
8
|
preCodemodRun?: (codemod: C) => Promise<void>;
|
|
9
9
|
postTransform?: (transformedContent: string, codemod: C) => Promise<string>;
|
|
10
10
|
};
|
|
11
|
-
type RunCodemodOptions<C extends Codemod> = {
|
|
11
|
+
type RunCodemodOptions<C extends Codemod = Codemod> = {
|
|
12
12
|
hooks?: RunCodemodHooks<C>;
|
|
13
13
|
log?: boolean;
|
|
14
14
|
dry?: boolean;
|
|
15
15
|
rootPaths?: Array<string>;
|
|
16
16
|
};
|
|
17
|
-
export declare function
|
|
18
|
-
|
|
17
|
+
export declare function runCodemodsOnProjects<Tag = string, C extends Codemod = Codemod>(repositoriesToClone: Array<RepositoryToClone<Tag>>, codemods: Array<CodemodRunnerCodemod<Tag, C>>, options: {
|
|
18
|
+
workingDirectory: string;
|
|
19
|
+
pushChanges: boolean;
|
|
20
|
+
}): Promise<void>;
|
|
21
|
+
export declare function runCodemods<C extends Codemod = Codemod>(codemods: Array<C>, transformationPath: string, options?: RunCodemodOptions<C>): Promise<Record<string, Array<RunCodemodResult>>>;
|
|
22
|
+
export declare function runCodemod<C extends Codemod = Codemod>(codemod: C, transformationPath: string, options?: RunCodemodOptions<C>): Promise<Array<RunCodemodResult>>;
|
|
19
23
|
export declare function traverseUp(node: SgNode<TypesMap, Kinds<TypesMap>>, until: (node: SgNode<TypesMap, Kinds<TypesMap>>) => boolean): types.Optional<SgNode<TypesMap, Kinds<TypesMap>>>;
|
|
20
24
|
export declare function findAndReplaceConfigModifications(modifications: Modifications, config: Array<FindAndReplaceConfig>): Promise<Modifications>;
|
|
21
25
|
export declare function findAndReplaceConfig(content: SgRoot<TypesMap>, lang: NapiLang, config: Array<FindAndReplaceConfig>): Promise<string>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { types } from '@kamaalio/kamaal';
|
|
2
|
+
import type Repository from './repository.js';
|
|
3
|
+
export declare class GitError extends Error {
|
|
4
|
+
readonly cause: types.Optional<unknown>;
|
|
5
|
+
readonly repository: Repository;
|
|
6
|
+
constructor(message: string, repository: Repository, options?: {
|
|
7
|
+
cause: unknown;
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
export declare class CloneError extends GitError {
|
|
11
|
+
constructor(repository: Repository);
|
|
12
|
+
}
|
|
13
|
+
export declare class CheckoutError extends GitError {
|
|
14
|
+
constructor(repository: Repository, branchName: string);
|
|
15
|
+
}
|
|
16
|
+
export declare class GetMainBranchError extends GitError {
|
|
17
|
+
constructor(repository: Repository, options?: {
|
|
18
|
+
cause: unknown;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export declare class PushError extends GitError {
|
|
22
|
+
constructor(repository: Repository, message: string, options?: {
|
|
23
|
+
cause: unknown;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export declare class RebaseError extends GitError {
|
|
27
|
+
constructor(repository: Repository, options?: {
|
|
28
|
+
cause: unknown;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type ResultAsync, type Result } from 'neverthrow';
|
|
2
|
+
import { type types } from '@kamaalio/kamaal';
|
|
3
|
+
import { GitError } from './errors.js';
|
|
4
|
+
import Branch from './branch.js';
|
|
5
|
+
interface IRepository {
|
|
6
|
+
name: string;
|
|
7
|
+
address: string;
|
|
8
|
+
path: string;
|
|
9
|
+
}
|
|
10
|
+
declare class Repository implements IRepository {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly address: string;
|
|
13
|
+
readonly path: string;
|
|
14
|
+
private currentBranch;
|
|
15
|
+
private constructor();
|
|
16
|
+
clone: () => Promise<Result<void, GitError>>;
|
|
17
|
+
commit: (message: string) => Promise<void>;
|
|
18
|
+
push: () => Promise<Result<void, GitError>>;
|
|
19
|
+
resetBranch: (workingBranchName: string) => Promise<Result<void, GitError>>;
|
|
20
|
+
updateBranchToLatestMain: (workingBranchName: string) => Promise<Result<void, GitError>>;
|
|
21
|
+
deleteBranch: (branchName: string) => ResultAsync<unknown, unknown>;
|
|
22
|
+
prepareForUpdate: (workingBranchName: string) => Promise<Result<Repository, GitError>>;
|
|
23
|
+
getMainBranch: () => Promise<Result<Branch, GitError>>;
|
|
24
|
+
getRemoteName: () => Promise<types.Optional<string>>;
|
|
25
|
+
checkoutBranch: (branchName: string, options?: {
|
|
26
|
+
forceCreateNew: boolean;
|
|
27
|
+
}) => Promise<Result<Branch, GitError>>;
|
|
28
|
+
getSelectedBranch: () => Promise<types.Optional<Branch>>;
|
|
29
|
+
getRemoteBranches: () => Promise<Array<Branch>>;
|
|
30
|
+
getBranches: () => Promise<Array<Branch>>;
|
|
31
|
+
private get exec();
|
|
32
|
+
private getCurrentBranch;
|
|
33
|
+
private setCurrentBranch;
|
|
34
|
+
private getMainBranchName;
|
|
35
|
+
static fromAddressAndCwd: ({ address, cwd }: {
|
|
36
|
+
address: string;
|
|
37
|
+
cwd: string;
|
|
38
|
+
}) => types.Optional<Repository>;
|
|
39
|
+
private static getRepoNameFromRepoAddress;
|
|
40
|
+
}
|
|
41
|
+
export default Repository;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
export declare const DefaultBranchRefSchema: z.ZodObject<{
|
|
3
|
+
defaultBranchRef: z.ZodObject<{
|
|
4
|
+
name: z.ZodString;
|
|
5
|
+
}, "strip", z.ZodTypeAny, {
|
|
6
|
+
name: string;
|
|
7
|
+
}, {
|
|
8
|
+
name: string;
|
|
9
|
+
}>;
|
|
10
|
+
}, "strip", z.ZodTypeAny, {
|
|
11
|
+
defaultBranchRef: {
|
|
12
|
+
name: string;
|
|
13
|
+
};
|
|
14
|
+
}, {
|
|
15
|
+
defaultBranchRef: {
|
|
16
|
+
name: string;
|
|
17
|
+
};
|
|
18
|
+
}>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type types } from '@kamaalio/kamaal';
|
|
2
|
+
import Repository from './repository.js';
|
|
3
|
+
export declare function cloneRepositories(repoAddresses: Array<string>, location: string): Promise<Array<Repository>>;
|
|
4
|
+
export declare function cloneRepository(repoAddress: string, location: string): Promise<types.Optional<Repository>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { makePullRequestsForCodemodResults, makePullRequestsForCodemodResult } from './utils.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Result } from 'neverthrow';
|
|
2
|
+
import type { Repository } from '../git/index.js';
|
|
3
|
+
import type { Codemod, CodemodRunnerCodemod } from '../codemods/index.js';
|
|
4
|
+
export declare function makePullRequestsForCodemodResults<Tag = string, C extends Codemod = Codemod>(codemods: Array<CodemodRunnerCodemod<Tag, C>>, codemodResults: Record<string, Array<Result<{
|
|
5
|
+
hasChanges: boolean;
|
|
6
|
+
content: string;
|
|
7
|
+
}, Error>>>, repositories: Array<Repository>): Promise<void>;
|
|
8
|
+
export declare function makePullRequestsForCodemodResult<Tag = string, C extends Codemod = Codemod>(codemod: CodemodRunnerCodemod<Tag, C>, codemodResult: Array<Result<{
|
|
9
|
+
hasChanges: boolean;
|
|
10
|
+
content: string;
|
|
11
|
+
}, Error>>, repositories: Array<Repository>): Promise<void>;
|
package/dist/index.cjs
CHANGED
|
@@ -33,6 +33,7 @@ var __webpack_require__ = {};
|
|
|
33
33
|
var __webpack_exports__ = {};
|
|
34
34
|
__webpack_require__.r(__webpack_exports__);
|
|
35
35
|
__webpack_require__.d(__webpack_exports__, {
|
|
36
|
+
traverseUp: ()=>traverseUp,
|
|
36
37
|
findAndReplace: ()=>findAndReplace,
|
|
37
38
|
findAndReplaceConfig: ()=>findAndReplaceConfig,
|
|
38
39
|
commitEditModifications: ()=>commitEditModifications,
|
|
@@ -40,7 +41,7 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
40
41
|
findAndReplaceConfigModifications: ()=>findAndReplaceConfigModifications,
|
|
41
42
|
findAndReplaceEdits: ()=>findAndReplaceEdits,
|
|
42
43
|
runCodemods: ()=>runCodemods,
|
|
43
|
-
|
|
44
|
+
runCodemodsOnProjects: ()=>runCodemodsOnProjects
|
|
44
45
|
});
|
|
45
46
|
const external_node_path_namespaceObject = require("node:path");
|
|
46
47
|
var external_node_path_default = /*#__PURE__*/ __webpack_require__.n(external_node_path_namespaceObject);
|
|
@@ -96,6 +97,419 @@ function groupBy(array, key) {
|
|
|
96
97
|
return acc;
|
|
97
98
|
}, {});
|
|
98
99
|
}
|
|
100
|
+
const external_execa_namespaceObject = require("execa");
|
|
101
|
+
class GitError extends Error {
|
|
102
|
+
cause;
|
|
103
|
+
repository;
|
|
104
|
+
constructor(message, repository, options){
|
|
105
|
+
super(message);
|
|
106
|
+
this.repository = repository;
|
|
107
|
+
this.cause = options?.cause;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
class CloneError extends GitError {
|
|
111
|
+
constructor(repository){
|
|
112
|
+
super(`Git clone failed for ${repository.address}`, repository);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
class CheckoutError extends GitError {
|
|
116
|
+
constructor(repository, branchName){
|
|
117
|
+
super(`Git checkout failed for ${repository.address}, couldn't checkout to ${branchName}`, repository);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
class GetMainBranchError extends GitError {
|
|
121
|
+
constructor(repository, options){
|
|
122
|
+
super(`Failed to get main branch for ${repository.address}`, repository, options);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
class PushError extends GitError {
|
|
126
|
+
constructor(repository, message, options){
|
|
127
|
+
super(`Git push failed for ${repository.address}; message: ${message}`, repository, options);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
class RebaseError extends GitError {
|
|
131
|
+
constructor(repository, options){
|
|
132
|
+
super(`Git rebase failed for ${repository.address}`, repository, options);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
class Branch {
|
|
136
|
+
isSelected;
|
|
137
|
+
name;
|
|
138
|
+
constructor(params){
|
|
139
|
+
this.name = params.name;
|
|
140
|
+
this.isSelected = params.isSelected;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const git_branch = Branch;
|
|
144
|
+
const external_zod_namespaceObject = require("zod");
|
|
145
|
+
var external_zod_default = /*#__PURE__*/ __webpack_require__.n(external_zod_namespaceObject);
|
|
146
|
+
const DefaultBranchRefSchema = external_zod_default().object({
|
|
147
|
+
defaultBranchRef: external_zod_default().object({
|
|
148
|
+
name: external_zod_default().string().nonempty()
|
|
149
|
+
})
|
|
150
|
+
});
|
|
151
|
+
function tryCatch(callback) {
|
|
152
|
+
try {
|
|
153
|
+
return (0, external_neverthrow_namespaceObject.ok)(callback());
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return (0, external_neverthrow_namespaceObject.err)(error);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function tryCatchAsync(callback) {
|
|
159
|
+
return external_neverthrow_namespaceObject.ResultAsync.fromPromise(callback(), (e)=>e);
|
|
160
|
+
}
|
|
161
|
+
function groupResults(results) {
|
|
162
|
+
return results.reduce((acc, result)=>{
|
|
163
|
+
if (result.isErr()) return {
|
|
164
|
+
...acc,
|
|
165
|
+
failure: [
|
|
166
|
+
...acc.failure,
|
|
167
|
+
result.error
|
|
168
|
+
]
|
|
169
|
+
};
|
|
170
|
+
return {
|
|
171
|
+
...acc,
|
|
172
|
+
success: [
|
|
173
|
+
...acc.success,
|
|
174
|
+
result.value
|
|
175
|
+
]
|
|
176
|
+
};
|
|
177
|
+
}, {
|
|
178
|
+
success: [],
|
|
179
|
+
failure: []
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
class Repository {
|
|
183
|
+
name;
|
|
184
|
+
address;
|
|
185
|
+
path;
|
|
186
|
+
currentBranch;
|
|
187
|
+
constructor(params){
|
|
188
|
+
this.name = params.name;
|
|
189
|
+
this.address = params.address;
|
|
190
|
+
this.path = params.path;
|
|
191
|
+
}
|
|
192
|
+
clone = async ()=>{
|
|
193
|
+
const cwd = this.path.split('/').slice(0, -1).join('/');
|
|
194
|
+
const exec = (0, external_execa_namespaceObject.$)({
|
|
195
|
+
cwd
|
|
196
|
+
});
|
|
197
|
+
const cloneResult = await exec`git clone ${this.address}`;
|
|
198
|
+
if (null != cloneResult.code && '0' !== cloneResult.code) return (0, external_neverthrow_namespaceObject.err)(new CloneError(this));
|
|
199
|
+
return (0, external_neverthrow_namespaceObject.ok)();
|
|
200
|
+
};
|
|
201
|
+
commit = async (message)=>{
|
|
202
|
+
await this.exec`git add .`;
|
|
203
|
+
await tryCatchAsync(()=>this.exec`git commit -m ${message} --no-verify`);
|
|
204
|
+
};
|
|
205
|
+
push = async ()=>{
|
|
206
|
+
const [remoteName, currentBranch, mainBranchNameResult] = await Promise.all([
|
|
207
|
+
this.getRemoteName(),
|
|
208
|
+
this.getCurrentBranch(),
|
|
209
|
+
this.getMainBranchName()
|
|
210
|
+
]);
|
|
211
|
+
if (null == remoteName || null == currentBranch) return (0, external_neverthrow_namespaceObject.err)(new PushError(this, 'Failed to find remote name'));
|
|
212
|
+
if (mainBranchNameResult.isErr()) return (0, external_neverthrow_namespaceObject.err)(mainBranchNameResult.error);
|
|
213
|
+
if (mainBranchNameResult.value === currentBranch.name) return (0, external_neverthrow_namespaceObject.err)(new PushError(this, "Can't push to main branch"));
|
|
214
|
+
await this.exec`git add .`;
|
|
215
|
+
await this.exec`git push --force --no-verify --set-upstream ${remoteName} ${currentBranch.name}`;
|
|
216
|
+
return (0, external_neverthrow_namespaceObject.ok)();
|
|
217
|
+
};
|
|
218
|
+
resetBranch = async (workingBranchName)=>{
|
|
219
|
+
await this.exec`git add .`;
|
|
220
|
+
await this.exec`git reset --hard`;
|
|
221
|
+
await this.exec`git fetch`;
|
|
222
|
+
return this.updateBranchToLatestMain(workingBranchName);
|
|
223
|
+
};
|
|
224
|
+
updateBranchToLatestMain = async (workingBranchName)=>{
|
|
225
|
+
const [remoteName, mainBranchNameResult] = await Promise.all([
|
|
226
|
+
this.getRemoteName(),
|
|
227
|
+
this.getMainBranchName()
|
|
228
|
+
]);
|
|
229
|
+
if (mainBranchNameResult.isErr()) return (0, external_neverthrow_namespaceObject.err)(new RebaseError(this, {
|
|
230
|
+
cause: mainBranchNameResult.error
|
|
231
|
+
}));
|
|
232
|
+
if (null == remoteName) return (0, external_neverthrow_namespaceObject.err)(new RebaseError(this));
|
|
233
|
+
const remoteBranches = await this.getRemoteBranches();
|
|
234
|
+
const existsInRemote = remoteBranches.some((branch)=>branch.name === workingBranchName);
|
|
235
|
+
const checkoutWorkingBranchResult = await this.checkoutBranch(workingBranchName);
|
|
236
|
+
if (checkoutWorkingBranchResult.isErr()) return (0, external_neverthrow_namespaceObject.err)(checkoutWorkingBranchResult.error);
|
|
237
|
+
const rebaseResult = await tryCatchAsync(()=>this.exec`git rebase ${remoteName}/${mainBranchNameResult.value}`);
|
|
238
|
+
if (rebaseResult.isErr() || !existsInRemote) {
|
|
239
|
+
if (workingBranchName === mainBranchNameResult.value) return (0, external_neverthrow_namespaceObject.err)(new GitError('Checked out branch is main branch', this));
|
|
240
|
+
const checkoutResult = await this.checkoutBranch(mainBranchNameResult.value);
|
|
241
|
+
if (checkoutResult.isErr()) return (0, external_neverthrow_namespaceObject.err)(checkoutResult.error);
|
|
242
|
+
await this.deleteBranch(workingBranchName);
|
|
243
|
+
const checkoutWorkingBranchResult = await this.checkoutBranch(workingBranchName, {
|
|
244
|
+
forceCreateNew: true
|
|
245
|
+
});
|
|
246
|
+
if (checkoutWorkingBranchResult.isErr()) return (0, external_neverthrow_namespaceObject.err)(checkoutWorkingBranchResult.error);
|
|
247
|
+
const currentBranch = await this.getCurrentBranch();
|
|
248
|
+
if (null == currentBranch) return (0, external_neverthrow_namespaceObject.err)(new GitError('Failed to find current branch', this));
|
|
249
|
+
if (currentBranch.name === mainBranchNameResult.value) return (0, external_neverthrow_namespaceObject.err)(new GitError('Checked out branch is main branch', this));
|
|
250
|
+
}
|
|
251
|
+
return (0, external_neverthrow_namespaceObject.ok)();
|
|
252
|
+
};
|
|
253
|
+
deleteBranch = (branchName)=>tryCatchAsync(()=>this.exec`git branch -D ${branchName}`);
|
|
254
|
+
prepareForUpdate = async (workingBranchName)=>{
|
|
255
|
+
const checkoutBranchResult = await this.checkoutBranch(workingBranchName);
|
|
256
|
+
if (checkoutBranchResult.isErr()) return (0, external_neverthrow_namespaceObject.err)(checkoutBranchResult.error);
|
|
257
|
+
const resetBranchResult = await this.resetBranch(workingBranchName);
|
|
258
|
+
if (resetBranchResult.isErr()) return (0, external_neverthrow_namespaceObject.err)(resetBranchResult.error);
|
|
259
|
+
return (0, external_neverthrow_namespaceObject.ok)(this);
|
|
260
|
+
};
|
|
261
|
+
getMainBranch = async ()=>{
|
|
262
|
+
const [branches, mainBranchNameResult] = await Promise.all([
|
|
263
|
+
this.getBranches(),
|
|
264
|
+
this.getMainBranchName()
|
|
265
|
+
]);
|
|
266
|
+
if (mainBranchNameResult.isErr()) return (0, external_neverthrow_namespaceObject.err)(mainBranchNameResult.error);
|
|
267
|
+
const mainBranch = branches.find((branch)=>branch.name === mainBranchNameResult.value);
|
|
268
|
+
if (null == mainBranch) return (0, external_neverthrow_namespaceObject.err)(new GetMainBranchError(this));
|
|
269
|
+
return (0, external_neverthrow_namespaceObject.ok)(mainBranch);
|
|
270
|
+
};
|
|
271
|
+
getRemoteName = async ()=>{
|
|
272
|
+
const remoteResult = await this.exec`git remote`;
|
|
273
|
+
return remoteResult.stdout.split('\n')[0];
|
|
274
|
+
};
|
|
275
|
+
checkoutBranch = async (branchName, options)=>{
|
|
276
|
+
const currentBranch = await this.getCurrentBranch();
|
|
277
|
+
const forceCreateNew = options?.forceCreateNew ?? false;
|
|
278
|
+
if (null != currentBranch && currentBranch.name === branchName && !forceCreateNew) return (0, external_neverthrow_namespaceObject.ok)(currentBranch);
|
|
279
|
+
const branches = await this.getBranches();
|
|
280
|
+
const existingBranch = branches.find((branch)=>branch.name === branchName);
|
|
281
|
+
if (null != existingBranch && !forceCreateNew) {
|
|
282
|
+
const checkoutResult = await this.exec`git checkout ${branchName}`;
|
|
283
|
+
if (null != checkoutResult.code && '0' !== checkoutResult.code) return (0, external_neverthrow_namespaceObject.err)(new CheckoutError(this, branchName));
|
|
284
|
+
return (0, external_neverthrow_namespaceObject.ok)(existingBranch);
|
|
285
|
+
}
|
|
286
|
+
const checkoutResult = await this.exec`git checkout -b ${branchName}`;
|
|
287
|
+
if (null != checkoutResult.code && '0' !== checkoutResult.code) return (0, external_neverthrow_namespaceObject.err)(new CheckoutError(this, branchName));
|
|
288
|
+
const branch = new git_branch({
|
|
289
|
+
name: branchName,
|
|
290
|
+
isSelected: true
|
|
291
|
+
});
|
|
292
|
+
this.setCurrentBranch(branch);
|
|
293
|
+
return (0, external_neverthrow_namespaceObject.ok)(branch);
|
|
294
|
+
};
|
|
295
|
+
getSelectedBranch = async ()=>{
|
|
296
|
+
const currentBranch = await this.getCurrentBranch();
|
|
297
|
+
if (null != currentBranch) return currentBranch;
|
|
298
|
+
const branches = await this.getBranches();
|
|
299
|
+
const branch = branches.find((branch)=>branch.isSelected);
|
|
300
|
+
if (null == branch) return null;
|
|
301
|
+
this.setCurrentBranch(branch);
|
|
302
|
+
return branch;
|
|
303
|
+
};
|
|
304
|
+
getRemoteBranches = async ()=>{
|
|
305
|
+
const remoteHeadsResult = await this.exec`git ls-remote --heads`;
|
|
306
|
+
return kamaal_namespaceObject.arrays.compactMap(remoteHeadsResult.stdout.split('\n'), (line)=>{
|
|
307
|
+
const isValid = line.includes('refs/heads/');
|
|
308
|
+
if (!isValid) return null;
|
|
309
|
+
const name = line.trim().split('refs/heads/')[1];
|
|
310
|
+
if (!name) return null;
|
|
311
|
+
return new git_branch({
|
|
312
|
+
name,
|
|
313
|
+
isSelected: false
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
};
|
|
317
|
+
getBranches = async ()=>{
|
|
318
|
+
const branchResult = await this.exec`git branch`;
|
|
319
|
+
return branchResult.stdout.split('\n').map((branch)=>{
|
|
320
|
+
const name = branch.split('*').join('').trim();
|
|
321
|
+
const isSelected = branch.startsWith('*');
|
|
322
|
+
return new git_branch({
|
|
323
|
+
name,
|
|
324
|
+
isSelected
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
};
|
|
328
|
+
get exec() {
|
|
329
|
+
return (0, external_execa_namespaceObject.$)({
|
|
330
|
+
cwd: this.path
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
getCurrentBranch = async ()=>{
|
|
334
|
+
if (null != this.currentBranch) return this.currentBranch;
|
|
335
|
+
const branches = await this.getBranches();
|
|
336
|
+
return branches.find((branch)=>branch.isSelected);
|
|
337
|
+
};
|
|
338
|
+
setCurrentBranch = (branch)=>{
|
|
339
|
+
this.currentBranch = branch;
|
|
340
|
+
};
|
|
341
|
+
getMainBranchName = async ()=>{
|
|
342
|
+
const rawDefaultBranchRefResult = await this.exec`gh repo view --json defaultBranchRef`;
|
|
343
|
+
if (null != rawDefaultBranchRefResult.code && '0' !== rawDefaultBranchRefResult.code) return (0, external_neverthrow_namespaceObject.err)(new GetMainBranchError(this));
|
|
344
|
+
const parsedStdoutResult = tryCatch(()=>JSON.parse(rawDefaultBranchRefResult.stdout)).mapErr((e)=>new GetMainBranchError(this, {
|
|
345
|
+
cause: e
|
|
346
|
+
}));
|
|
347
|
+
if (parsedStdoutResult.isErr()) return (0, external_neverthrow_namespaceObject.err)(parsedStdoutResult.error);
|
|
348
|
+
const defaultBranchRef = await DefaultBranchRefSchema.safeParseAsync(parsedStdoutResult.value);
|
|
349
|
+
if (defaultBranchRef.error) return (0, external_neverthrow_namespaceObject.err)(new GetMainBranchError(this, {
|
|
350
|
+
cause: defaultBranchRef.error
|
|
351
|
+
}));
|
|
352
|
+
return (0, external_neverthrow_namespaceObject.ok)(defaultBranchRef.data.defaultBranchRef.name);
|
|
353
|
+
};
|
|
354
|
+
static fromAddressAndCwd = ({ address, cwd })=>{
|
|
355
|
+
const name = Repository.getRepoNameFromRepoAddress(address);
|
|
356
|
+
if (null == name) return null;
|
|
357
|
+
return new Repository({
|
|
358
|
+
name,
|
|
359
|
+
address,
|
|
360
|
+
path: external_node_path_default().resolve(cwd, name)
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
static getRepoNameFromRepoAddress = (repoAddress)=>{
|
|
364
|
+
const repoAddressComponents = repoAddress.split('/');
|
|
365
|
+
return repoAddressComponents[repoAddressComponents.length - 1]?.split('.').slice(0, -1).join('.');
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
const git_repository = Repository;
|
|
369
|
+
async function cloneRepositories(repoAddresses, location) {
|
|
370
|
+
await (0, external_execa_namespaceObject.$)`mkdir -p ${location}`;
|
|
371
|
+
const exec = (0, external_execa_namespaceObject.$)({
|
|
372
|
+
cwd: location
|
|
373
|
+
});
|
|
374
|
+
const dedupedRepos = dedupeRepositoriesToClone(repoAddresses, location);
|
|
375
|
+
const existingRepositories = await getExistingRepositories(exec, repoAddresses);
|
|
376
|
+
const results = await Promise.all(dedupedRepos.map(cloneRepositoryInternal(existingRepositories)));
|
|
377
|
+
return kamaal_namespaceObject.arrays.compactMap(results, (result, index)=>{
|
|
378
|
+
const repo = dedupedRepos[index];
|
|
379
|
+
if (result.isErr()) {
|
|
380
|
+
console.error(result.error.message);
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
return repo;
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
async function getExistingRepositories(exec, repoAddresses) {
|
|
387
|
+
const lsResult = await exec`ls`;
|
|
388
|
+
const existingNames = new Set(lsResult.stdout.split('\n'));
|
|
389
|
+
return kamaal_namespaceObject.arrays.compactMap(repoAddresses, (address)=>{
|
|
390
|
+
const repository = git_repository.fromAddressAndCwd({
|
|
391
|
+
address,
|
|
392
|
+
cwd: lsResult.cwd
|
|
393
|
+
});
|
|
394
|
+
if (null == repository) return null;
|
|
395
|
+
if (!existingNames.has(repository.name)) return null;
|
|
396
|
+
return repository;
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
function cloneRepositoryInternal(existingRepositories) {
|
|
400
|
+
const existingRepositoryAddresses = new Set(existingRepositories.map((repo)=>repo.address));
|
|
401
|
+
return async (repository)=>{
|
|
402
|
+
if (existingRepositoryAddresses.has(repository.address)) return (0, external_neverthrow_namespaceObject.ok)();
|
|
403
|
+
return repository.clone();
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
function dedupeRepositoriesToClone(repoAddresses, cwd) {
|
|
407
|
+
const initialDedupeResult = {
|
|
408
|
+
result: [],
|
|
409
|
+
names: []
|
|
410
|
+
};
|
|
411
|
+
const dedupedRepos = repoAddresses.reduce((acc, repoAddress)=>{
|
|
412
|
+
const repository = git_repository.fromAddressAndCwd({
|
|
413
|
+
address: repoAddress,
|
|
414
|
+
cwd
|
|
415
|
+
});
|
|
416
|
+
if (null == repository) return acc;
|
|
417
|
+
if (acc.names.includes(repository.name)) return acc;
|
|
418
|
+
return {
|
|
419
|
+
result: [
|
|
420
|
+
...acc.result,
|
|
421
|
+
repository
|
|
422
|
+
],
|
|
423
|
+
names: [
|
|
424
|
+
...acc.names,
|
|
425
|
+
repository.name
|
|
426
|
+
]
|
|
427
|
+
};
|
|
428
|
+
}, initialDedupeResult).result;
|
|
429
|
+
return dedupedRepos;
|
|
430
|
+
}
|
|
431
|
+
async function makePullRequestsForCodemodResults(codemods, codemodResults, repositories) {
|
|
432
|
+
for (const [codemodName, codemodResult] of Object.entries(codemodResults)){
|
|
433
|
+
const codemod = codemods.find((c)=>c.name === codemodName);
|
|
434
|
+
if (null == codemod) throw new Error('Invariant found codemod should be present');
|
|
435
|
+
await makePullRequestsForCodemodResult(codemod, codemodResult, repositories);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async function makePullRequestsForCodemodResult(codemod, codemodResult, repositories) {
|
|
439
|
+
const { success } = groupResults(codemodResult);
|
|
440
|
+
const hasChanges = success.some((result)=>result.hasChanges);
|
|
441
|
+
if (!hasChanges) return void console.log(`\u{1F438} nothing transformed for '${codemod.name}'`);
|
|
442
|
+
await Promise.all(repositories.map(async (repository)=>{
|
|
443
|
+
await repository.commit(codemod.commitMessage);
|
|
444
|
+
const pushResult = await repository.push();
|
|
445
|
+
if (pushResult.isErr()) return void console.error(`\u{274C} Failed to push changes`, pushResult.error.message);
|
|
446
|
+
const pullRequestResult = await makePullRequest({
|
|
447
|
+
workingDirectory: repository.path,
|
|
448
|
+
title: codemod.commitMessage
|
|
449
|
+
});
|
|
450
|
+
if (pullRequestResult.isErr()) return void console.error(`\u{2705} already pushed transformation for`, repository.address);
|
|
451
|
+
console.log(`\u{2705} pull request created`, pullRequestResult.value.stdout);
|
|
452
|
+
}));
|
|
453
|
+
}
|
|
454
|
+
async function makePullRequest(params) {
|
|
455
|
+
return tryCatchAsync(()=>(0, external_execa_namespaceObject.$)({
|
|
456
|
+
cwd: params.workingDirectory
|
|
457
|
+
})`gh pr create --title ${params.title} --fill`);
|
|
458
|
+
}
|
|
459
|
+
async function runCodemodsOnProjects(repositoriesToClone, codemods, options) {
|
|
460
|
+
const clonedRepositories = await cloneRepositories(repositoriesToClone.map((repo)=>repo.address), options.workingDirectory);
|
|
461
|
+
console.log(`\u{1F5A8}\u{FE0F} cloned ${clonedRepositories.length} ${1 === clonedRepositories.length ? 'repository' : 'repositories'}`);
|
|
462
|
+
const mappingsByName = clonedRepositories.reduce((acc, repository)=>{
|
|
463
|
+
const tags = repositoriesToClone.find(({ address })=>address === repository.address)?.tags;
|
|
464
|
+
kamaal_namespaceObject.asserts.invariant(null != tags, 'Tags should be present');
|
|
465
|
+
return {
|
|
466
|
+
...acc,
|
|
467
|
+
[repository.name]: {
|
|
468
|
+
repository,
|
|
469
|
+
tags
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
}, {});
|
|
473
|
+
const codemodResults = await runCodemodRunner(codemods, clonedRepositories, mappingsByName, options.workingDirectory);
|
|
474
|
+
if (options.pushChanges) await makePullRequestsForCodemodResults(codemods, codemodResults, clonedRepositories);
|
|
475
|
+
}
|
|
476
|
+
function codemodTargetFiltering(mappingsByName, failedRepositoryAddressesMappedByCodemodNames) {
|
|
477
|
+
return (filepath, codemod)=>{
|
|
478
|
+
const projectName = filepath.split('/')[0];
|
|
479
|
+
kamaal_namespaceObject.asserts.invariant(null != projectName, 'project name should be present');
|
|
480
|
+
const mapping = mappingsByName[projectName];
|
|
481
|
+
if (null == mapping) return false;
|
|
482
|
+
const failedRepositoryAddressesSet = failedRepositoryAddressesMappedByCodemodNames[codemod.name];
|
|
483
|
+
if (null == failedRepositoryAddressesSet || collectionIsEmpty(failedRepositoryAddressesSet)) return true;
|
|
484
|
+
return !failedRepositoryAddressesSet.has(mapping.repository.address) && (collectionIsEmpty(codemod.tags) || collectionIsEmpty(mapping.tags) || [
|
|
485
|
+
...codemod.tags
|
|
486
|
+
].some((tag)=>mapping.tags.has(tag)));
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
async function codemodPreCodemodRun(repositories, codemod) {
|
|
490
|
+
const preparationResults = await Promise.all(repositories.map((repo)=>repo.prepareForUpdate(codemod.name)));
|
|
491
|
+
const { failure: preparationFailures } = groupResults(preparationResults);
|
|
492
|
+
const failedRepositoryAddresses = preparationFailures.map((failure)=>failure.repository.address);
|
|
493
|
+
if (preparationFailures.length > 0) console.error(`\u{274C} failed to prepare [${failedRepositoryAddresses.join(', ')}] for '${codemod.name}'`);
|
|
494
|
+
return new Set(failedRepositoryAddresses);
|
|
495
|
+
}
|
|
496
|
+
async function codemodPostTransform(transformedContent) {
|
|
497
|
+
return transformedContent;
|
|
498
|
+
}
|
|
499
|
+
async function runCodemodRunner(codemods, repositories, mappingsByName, workingDirectory) {
|
|
500
|
+
const rootPaths = repositories.map((repository)=>repository.path);
|
|
501
|
+
const failedRepositoryAddressesMappedByCodemodNames = {};
|
|
502
|
+
return runCodemods(codemods, workingDirectory, {
|
|
503
|
+
rootPaths,
|
|
504
|
+
hooks: {
|
|
505
|
+
preCodemodRun: async (codemod)=>{
|
|
506
|
+
failedRepositoryAddressesMappedByCodemodNames[codemod.name] = await codemodPreCodemodRun(repositories, codemod);
|
|
507
|
+
},
|
|
508
|
+
targetFiltering: codemodTargetFiltering(mappingsByName, failedRepositoryAddressesMappedByCodemodNames),
|
|
509
|
+
postTransform: codemodPostTransform
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
}
|
|
99
513
|
async function runCodemods(codemods, transformationPath, options) {
|
|
100
514
|
const results = {};
|
|
101
515
|
for (const codemod of codemods)results[codemod.name] = await runCodemod(codemod, transformationPath, options);
|
|
@@ -309,6 +723,7 @@ exports.findAndReplaceConfigModifications = __webpack_exports__.findAndReplaceCo
|
|
|
309
723
|
exports.findAndReplaceEdits = __webpack_exports__.findAndReplaceEdits;
|
|
310
724
|
exports.runCodemod = __webpack_exports__.runCodemod;
|
|
311
725
|
exports.runCodemods = __webpack_exports__.runCodemods;
|
|
726
|
+
exports.runCodemodsOnProjects = __webpack_exports__.runCodemodsOnProjects;
|
|
312
727
|
exports.traverseUp = __webpack_exports__.traverseUp;
|
|
313
728
|
for(var __webpack_i__ in __webpack_exports__)if (-1 === [
|
|
314
729
|
"commitEditModifications",
|
|
@@ -318,6 +733,7 @@ for(var __webpack_i__ in __webpack_exports__)if (-1 === [
|
|
|
318
733
|
"findAndReplaceEdits",
|
|
319
734
|
"runCodemod",
|
|
320
735
|
"runCodemods",
|
|
736
|
+
"runCodemodsOnProjects",
|
|
321
737
|
"traverseUp"
|
|
322
738
|
].indexOf(__webpack_i__)) exports[__webpack_i__] = __webpack_exports__[__webpack_i__];
|
|
323
739
|
Object.defineProperty(exports, '__esModule', {
|
package/dist/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { runCodemods, runCodemod, commitEditModifications, findAndReplace, findAndReplaceEdits, findAndReplaceConfig, findAndReplaceConfigModifications, traverseUp, type Codemod, type Modifications, type FindAndReplaceConfig, type RunCodemodOkResult, type RunCodemodResult, } from './codemods/index.js';
|
|
1
|
+
export { runCodemods, runCodemod, commitEditModifications, findAndReplace, findAndReplaceEdits, findAndReplaceConfig, findAndReplaceConfigModifications, traverseUp, runCodemodsOnProjects, type Codemod, type Modifications, type FindAndReplaceConfig, type RunCodemodOkResult, type RunCodemodResult, type CodemodRunnerCodemod, type RepositoryToClone, } from './codemods/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import node_path from "node:path";
|
|
2
2
|
import promises from "node:fs/promises";
|
|
3
3
|
import fast_glob from "fast-glob";
|
|
4
|
-
import { err, ok } from "neverthrow";
|
|
4
|
+
import { ResultAsync, err, ok } from "neverthrow";
|
|
5
5
|
import { Lang, parseAsync } from "@ast-grep/napi";
|
|
6
|
-
import { arrays } from "@kamaalio/kamaal";
|
|
6
|
+
import { arrays, asserts } from "@kamaalio/kamaal";
|
|
7
|
+
import { $ } from "execa";
|
|
8
|
+
import zod from "zod";
|
|
7
9
|
const JAVASCRIPT_EXTENSIONS = [
|
|
8
10
|
'.js',
|
|
9
11
|
'.cjs',
|
|
@@ -49,6 +51,416 @@ function groupBy(array, key) {
|
|
|
49
51
|
return acc;
|
|
50
52
|
}, {});
|
|
51
53
|
}
|
|
54
|
+
class GitError extends Error {
|
|
55
|
+
cause;
|
|
56
|
+
repository;
|
|
57
|
+
constructor(message, repository, options){
|
|
58
|
+
super(message);
|
|
59
|
+
this.repository = repository;
|
|
60
|
+
this.cause = options?.cause;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
class CloneError extends GitError {
|
|
64
|
+
constructor(repository){
|
|
65
|
+
super(`Git clone failed for ${repository.address}`, repository);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
class CheckoutError extends GitError {
|
|
69
|
+
constructor(repository, branchName){
|
|
70
|
+
super(`Git checkout failed for ${repository.address}, couldn't checkout to ${branchName}`, repository);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
class GetMainBranchError extends GitError {
|
|
74
|
+
constructor(repository, options){
|
|
75
|
+
super(`Failed to get main branch for ${repository.address}`, repository, options);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
class PushError extends GitError {
|
|
79
|
+
constructor(repository, message, options){
|
|
80
|
+
super(`Git push failed for ${repository.address}; message: ${message}`, repository, options);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
class RebaseError extends GitError {
|
|
84
|
+
constructor(repository, options){
|
|
85
|
+
super(`Git rebase failed for ${repository.address}`, repository, options);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
class Branch {
|
|
89
|
+
isSelected;
|
|
90
|
+
name;
|
|
91
|
+
constructor(params){
|
|
92
|
+
this.name = params.name;
|
|
93
|
+
this.isSelected = params.isSelected;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const git_branch = Branch;
|
|
97
|
+
const DefaultBranchRefSchema = zod.object({
|
|
98
|
+
defaultBranchRef: zod.object({
|
|
99
|
+
name: zod.string().nonempty()
|
|
100
|
+
})
|
|
101
|
+
});
|
|
102
|
+
function tryCatch(callback) {
|
|
103
|
+
try {
|
|
104
|
+
return ok(callback());
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return err(error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function tryCatchAsync(callback) {
|
|
110
|
+
return ResultAsync.fromPromise(callback(), (e)=>e);
|
|
111
|
+
}
|
|
112
|
+
function groupResults(results) {
|
|
113
|
+
return results.reduce((acc, result)=>{
|
|
114
|
+
if (result.isErr()) return {
|
|
115
|
+
...acc,
|
|
116
|
+
failure: [
|
|
117
|
+
...acc.failure,
|
|
118
|
+
result.error
|
|
119
|
+
]
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
...acc,
|
|
123
|
+
success: [
|
|
124
|
+
...acc.success,
|
|
125
|
+
result.value
|
|
126
|
+
]
|
|
127
|
+
};
|
|
128
|
+
}, {
|
|
129
|
+
success: [],
|
|
130
|
+
failure: []
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
class Repository {
|
|
134
|
+
name;
|
|
135
|
+
address;
|
|
136
|
+
path;
|
|
137
|
+
currentBranch;
|
|
138
|
+
constructor(params){
|
|
139
|
+
this.name = params.name;
|
|
140
|
+
this.address = params.address;
|
|
141
|
+
this.path = params.path;
|
|
142
|
+
}
|
|
143
|
+
clone = async ()=>{
|
|
144
|
+
const cwd = this.path.split('/').slice(0, -1).join('/');
|
|
145
|
+
const exec = $({
|
|
146
|
+
cwd
|
|
147
|
+
});
|
|
148
|
+
const cloneResult = await exec`git clone ${this.address}`;
|
|
149
|
+
if (null != cloneResult.code && '0' !== cloneResult.code) return err(new CloneError(this));
|
|
150
|
+
return ok();
|
|
151
|
+
};
|
|
152
|
+
commit = async (message)=>{
|
|
153
|
+
await this.exec`git add .`;
|
|
154
|
+
await tryCatchAsync(()=>this.exec`git commit -m ${message} --no-verify`);
|
|
155
|
+
};
|
|
156
|
+
push = async ()=>{
|
|
157
|
+
const [remoteName, currentBranch, mainBranchNameResult] = await Promise.all([
|
|
158
|
+
this.getRemoteName(),
|
|
159
|
+
this.getCurrentBranch(),
|
|
160
|
+
this.getMainBranchName()
|
|
161
|
+
]);
|
|
162
|
+
if (null == remoteName || null == currentBranch) return err(new PushError(this, 'Failed to find remote name'));
|
|
163
|
+
if (mainBranchNameResult.isErr()) return err(mainBranchNameResult.error);
|
|
164
|
+
if (mainBranchNameResult.value === currentBranch.name) return err(new PushError(this, "Can't push to main branch"));
|
|
165
|
+
await this.exec`git add .`;
|
|
166
|
+
await this.exec`git push --force --no-verify --set-upstream ${remoteName} ${currentBranch.name}`;
|
|
167
|
+
return ok();
|
|
168
|
+
};
|
|
169
|
+
resetBranch = async (workingBranchName)=>{
|
|
170
|
+
await this.exec`git add .`;
|
|
171
|
+
await this.exec`git reset --hard`;
|
|
172
|
+
await this.exec`git fetch`;
|
|
173
|
+
return this.updateBranchToLatestMain(workingBranchName);
|
|
174
|
+
};
|
|
175
|
+
updateBranchToLatestMain = async (workingBranchName)=>{
|
|
176
|
+
const [remoteName, mainBranchNameResult] = await Promise.all([
|
|
177
|
+
this.getRemoteName(),
|
|
178
|
+
this.getMainBranchName()
|
|
179
|
+
]);
|
|
180
|
+
if (mainBranchNameResult.isErr()) return err(new RebaseError(this, {
|
|
181
|
+
cause: mainBranchNameResult.error
|
|
182
|
+
}));
|
|
183
|
+
if (null == remoteName) return err(new RebaseError(this));
|
|
184
|
+
const remoteBranches = await this.getRemoteBranches();
|
|
185
|
+
const existsInRemote = remoteBranches.some((branch)=>branch.name === workingBranchName);
|
|
186
|
+
const checkoutWorkingBranchResult = await this.checkoutBranch(workingBranchName);
|
|
187
|
+
if (checkoutWorkingBranchResult.isErr()) return err(checkoutWorkingBranchResult.error);
|
|
188
|
+
const rebaseResult = await tryCatchAsync(()=>this.exec`git rebase ${remoteName}/${mainBranchNameResult.value}`);
|
|
189
|
+
if (rebaseResult.isErr() || !existsInRemote) {
|
|
190
|
+
if (workingBranchName === mainBranchNameResult.value) return err(new GitError('Checked out branch is main branch', this));
|
|
191
|
+
const checkoutResult = await this.checkoutBranch(mainBranchNameResult.value);
|
|
192
|
+
if (checkoutResult.isErr()) return err(checkoutResult.error);
|
|
193
|
+
await this.deleteBranch(workingBranchName);
|
|
194
|
+
const checkoutWorkingBranchResult = await this.checkoutBranch(workingBranchName, {
|
|
195
|
+
forceCreateNew: true
|
|
196
|
+
});
|
|
197
|
+
if (checkoutWorkingBranchResult.isErr()) return err(checkoutWorkingBranchResult.error);
|
|
198
|
+
const currentBranch = await this.getCurrentBranch();
|
|
199
|
+
if (null == currentBranch) return err(new GitError('Failed to find current branch', this));
|
|
200
|
+
if (currentBranch.name === mainBranchNameResult.value) return err(new GitError('Checked out branch is main branch', this));
|
|
201
|
+
}
|
|
202
|
+
return ok();
|
|
203
|
+
};
|
|
204
|
+
deleteBranch = (branchName)=>tryCatchAsync(()=>this.exec`git branch -D ${branchName}`);
|
|
205
|
+
prepareForUpdate = async (workingBranchName)=>{
|
|
206
|
+
const checkoutBranchResult = await this.checkoutBranch(workingBranchName);
|
|
207
|
+
if (checkoutBranchResult.isErr()) return err(checkoutBranchResult.error);
|
|
208
|
+
const resetBranchResult = await this.resetBranch(workingBranchName);
|
|
209
|
+
if (resetBranchResult.isErr()) return err(resetBranchResult.error);
|
|
210
|
+
return ok(this);
|
|
211
|
+
};
|
|
212
|
+
getMainBranch = async ()=>{
|
|
213
|
+
const [branches, mainBranchNameResult] = await Promise.all([
|
|
214
|
+
this.getBranches(),
|
|
215
|
+
this.getMainBranchName()
|
|
216
|
+
]);
|
|
217
|
+
if (mainBranchNameResult.isErr()) return err(mainBranchNameResult.error);
|
|
218
|
+
const mainBranch = branches.find((branch)=>branch.name === mainBranchNameResult.value);
|
|
219
|
+
if (null == mainBranch) return err(new GetMainBranchError(this));
|
|
220
|
+
return ok(mainBranch);
|
|
221
|
+
};
|
|
222
|
+
getRemoteName = async ()=>{
|
|
223
|
+
const remoteResult = await this.exec`git remote`;
|
|
224
|
+
return remoteResult.stdout.split('\n')[0];
|
|
225
|
+
};
|
|
226
|
+
checkoutBranch = async (branchName, options)=>{
|
|
227
|
+
const currentBranch = await this.getCurrentBranch();
|
|
228
|
+
const forceCreateNew = options?.forceCreateNew ?? false;
|
|
229
|
+
if (null != currentBranch && currentBranch.name === branchName && !forceCreateNew) return ok(currentBranch);
|
|
230
|
+
const branches = await this.getBranches();
|
|
231
|
+
const existingBranch = branches.find((branch)=>branch.name === branchName);
|
|
232
|
+
if (null != existingBranch && !forceCreateNew) {
|
|
233
|
+
const checkoutResult = await this.exec`git checkout ${branchName}`;
|
|
234
|
+
if (null != checkoutResult.code && '0' !== checkoutResult.code) return err(new CheckoutError(this, branchName));
|
|
235
|
+
return ok(existingBranch);
|
|
236
|
+
}
|
|
237
|
+
const checkoutResult = await this.exec`git checkout -b ${branchName}`;
|
|
238
|
+
if (null != checkoutResult.code && '0' !== checkoutResult.code) return err(new CheckoutError(this, branchName));
|
|
239
|
+
const branch = new git_branch({
|
|
240
|
+
name: branchName,
|
|
241
|
+
isSelected: true
|
|
242
|
+
});
|
|
243
|
+
this.setCurrentBranch(branch);
|
|
244
|
+
return ok(branch);
|
|
245
|
+
};
|
|
246
|
+
getSelectedBranch = async ()=>{
|
|
247
|
+
const currentBranch = await this.getCurrentBranch();
|
|
248
|
+
if (null != currentBranch) return currentBranch;
|
|
249
|
+
const branches = await this.getBranches();
|
|
250
|
+
const branch = branches.find((branch)=>branch.isSelected);
|
|
251
|
+
if (null == branch) return null;
|
|
252
|
+
this.setCurrentBranch(branch);
|
|
253
|
+
return branch;
|
|
254
|
+
};
|
|
255
|
+
getRemoteBranches = async ()=>{
|
|
256
|
+
const remoteHeadsResult = await this.exec`git ls-remote --heads`;
|
|
257
|
+
return arrays.compactMap(remoteHeadsResult.stdout.split('\n'), (line)=>{
|
|
258
|
+
const isValid = line.includes('refs/heads/');
|
|
259
|
+
if (!isValid) return null;
|
|
260
|
+
const name = line.trim().split('refs/heads/')[1];
|
|
261
|
+
if (!name) return null;
|
|
262
|
+
return new git_branch({
|
|
263
|
+
name,
|
|
264
|
+
isSelected: false
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
};
|
|
268
|
+
getBranches = async ()=>{
|
|
269
|
+
const branchResult = await this.exec`git branch`;
|
|
270
|
+
return branchResult.stdout.split('\n').map((branch)=>{
|
|
271
|
+
const name = branch.split('*').join('').trim();
|
|
272
|
+
const isSelected = branch.startsWith('*');
|
|
273
|
+
return new git_branch({
|
|
274
|
+
name,
|
|
275
|
+
isSelected
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
};
|
|
279
|
+
get exec() {
|
|
280
|
+
return $({
|
|
281
|
+
cwd: this.path
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
getCurrentBranch = async ()=>{
|
|
285
|
+
if (null != this.currentBranch) return this.currentBranch;
|
|
286
|
+
const branches = await this.getBranches();
|
|
287
|
+
return branches.find((branch)=>branch.isSelected);
|
|
288
|
+
};
|
|
289
|
+
setCurrentBranch = (branch)=>{
|
|
290
|
+
this.currentBranch = branch;
|
|
291
|
+
};
|
|
292
|
+
getMainBranchName = async ()=>{
|
|
293
|
+
const rawDefaultBranchRefResult = await this.exec`gh repo view --json defaultBranchRef`;
|
|
294
|
+
if (null != rawDefaultBranchRefResult.code && '0' !== rawDefaultBranchRefResult.code) return err(new GetMainBranchError(this));
|
|
295
|
+
const parsedStdoutResult = tryCatch(()=>JSON.parse(rawDefaultBranchRefResult.stdout)).mapErr((e)=>new GetMainBranchError(this, {
|
|
296
|
+
cause: e
|
|
297
|
+
}));
|
|
298
|
+
if (parsedStdoutResult.isErr()) return err(parsedStdoutResult.error);
|
|
299
|
+
const defaultBranchRef = await DefaultBranchRefSchema.safeParseAsync(parsedStdoutResult.value);
|
|
300
|
+
if (defaultBranchRef.error) return err(new GetMainBranchError(this, {
|
|
301
|
+
cause: defaultBranchRef.error
|
|
302
|
+
}));
|
|
303
|
+
return ok(defaultBranchRef.data.defaultBranchRef.name);
|
|
304
|
+
};
|
|
305
|
+
static fromAddressAndCwd = ({ address, cwd })=>{
|
|
306
|
+
const name = Repository.getRepoNameFromRepoAddress(address);
|
|
307
|
+
if (null == name) return null;
|
|
308
|
+
return new Repository({
|
|
309
|
+
name,
|
|
310
|
+
address,
|
|
311
|
+
path: node_path.resolve(cwd, name)
|
|
312
|
+
});
|
|
313
|
+
};
|
|
314
|
+
static getRepoNameFromRepoAddress = (repoAddress)=>{
|
|
315
|
+
const repoAddressComponents = repoAddress.split('/');
|
|
316
|
+
return repoAddressComponents[repoAddressComponents.length - 1]?.split('.').slice(0, -1).join('.');
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const git_repository = Repository;
|
|
320
|
+
async function cloneRepositories(repoAddresses, location) {
|
|
321
|
+
await $`mkdir -p ${location}`;
|
|
322
|
+
const exec = $({
|
|
323
|
+
cwd: location
|
|
324
|
+
});
|
|
325
|
+
const dedupedRepos = dedupeRepositoriesToClone(repoAddresses, location);
|
|
326
|
+
const existingRepositories = await getExistingRepositories(exec, repoAddresses);
|
|
327
|
+
const results = await Promise.all(dedupedRepos.map(cloneRepositoryInternal(existingRepositories)));
|
|
328
|
+
return arrays.compactMap(results, (result, index)=>{
|
|
329
|
+
const repo = dedupedRepos[index];
|
|
330
|
+
if (result.isErr()) {
|
|
331
|
+
console.error(result.error.message);
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
return repo;
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
async function getExistingRepositories(exec, repoAddresses) {
|
|
338
|
+
const lsResult = await exec`ls`;
|
|
339
|
+
const existingNames = new Set(lsResult.stdout.split('\n'));
|
|
340
|
+
return arrays.compactMap(repoAddresses, (address)=>{
|
|
341
|
+
const repository = git_repository.fromAddressAndCwd({
|
|
342
|
+
address,
|
|
343
|
+
cwd: lsResult.cwd
|
|
344
|
+
});
|
|
345
|
+
if (null == repository) return null;
|
|
346
|
+
if (!existingNames.has(repository.name)) return null;
|
|
347
|
+
return repository;
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
function cloneRepositoryInternal(existingRepositories) {
|
|
351
|
+
const existingRepositoryAddresses = new Set(existingRepositories.map((repo)=>repo.address));
|
|
352
|
+
return async (repository)=>{
|
|
353
|
+
if (existingRepositoryAddresses.has(repository.address)) return ok();
|
|
354
|
+
return repository.clone();
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function dedupeRepositoriesToClone(repoAddresses, cwd) {
|
|
358
|
+
const initialDedupeResult = {
|
|
359
|
+
result: [],
|
|
360
|
+
names: []
|
|
361
|
+
};
|
|
362
|
+
const dedupedRepos = repoAddresses.reduce((acc, repoAddress)=>{
|
|
363
|
+
const repository = git_repository.fromAddressAndCwd({
|
|
364
|
+
address: repoAddress,
|
|
365
|
+
cwd
|
|
366
|
+
});
|
|
367
|
+
if (null == repository) return acc;
|
|
368
|
+
if (acc.names.includes(repository.name)) return acc;
|
|
369
|
+
return {
|
|
370
|
+
result: [
|
|
371
|
+
...acc.result,
|
|
372
|
+
repository
|
|
373
|
+
],
|
|
374
|
+
names: [
|
|
375
|
+
...acc.names,
|
|
376
|
+
repository.name
|
|
377
|
+
]
|
|
378
|
+
};
|
|
379
|
+
}, initialDedupeResult).result;
|
|
380
|
+
return dedupedRepos;
|
|
381
|
+
}
|
|
382
|
+
async function makePullRequestsForCodemodResults(codemods, codemodResults, repositories) {
|
|
383
|
+
for (const [codemodName, codemodResult] of Object.entries(codemodResults)){
|
|
384
|
+
const codemod = codemods.find((c)=>c.name === codemodName);
|
|
385
|
+
if (null == codemod) throw new Error('Invariant found codemod should be present');
|
|
386
|
+
await makePullRequestsForCodemodResult(codemod, codemodResult, repositories);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async function makePullRequestsForCodemodResult(codemod, codemodResult, repositories) {
|
|
390
|
+
const { success } = groupResults(codemodResult);
|
|
391
|
+
const hasChanges = success.some((result)=>result.hasChanges);
|
|
392
|
+
if (!hasChanges) return void console.log(`\u{1F438} nothing transformed for '${codemod.name}'`);
|
|
393
|
+
await Promise.all(repositories.map(async (repository)=>{
|
|
394
|
+
await repository.commit(codemod.commitMessage);
|
|
395
|
+
const pushResult = await repository.push();
|
|
396
|
+
if (pushResult.isErr()) return void console.error(`\u{274C} Failed to push changes`, pushResult.error.message);
|
|
397
|
+
const pullRequestResult = await makePullRequest({
|
|
398
|
+
workingDirectory: repository.path,
|
|
399
|
+
title: codemod.commitMessage
|
|
400
|
+
});
|
|
401
|
+
if (pullRequestResult.isErr()) return void console.error(`\u{2705} already pushed transformation for`, repository.address);
|
|
402
|
+
console.log(`\u{2705} pull request created`, pullRequestResult.value.stdout);
|
|
403
|
+
}));
|
|
404
|
+
}
|
|
405
|
+
async function makePullRequest(params) {
|
|
406
|
+
return tryCatchAsync(()=>$({
|
|
407
|
+
cwd: params.workingDirectory
|
|
408
|
+
})`gh pr create --title ${params.title} --fill`);
|
|
409
|
+
}
|
|
410
|
+
async function runCodemodsOnProjects(repositoriesToClone, codemods, options) {
|
|
411
|
+
const clonedRepositories = await cloneRepositories(repositoriesToClone.map((repo)=>repo.address), options.workingDirectory);
|
|
412
|
+
console.log(`\u{1F5A8}\u{FE0F} cloned ${clonedRepositories.length} ${1 === clonedRepositories.length ? 'repository' : 'repositories'}`);
|
|
413
|
+
const mappingsByName = clonedRepositories.reduce((acc, repository)=>{
|
|
414
|
+
const tags = repositoriesToClone.find(({ address })=>address === repository.address)?.tags;
|
|
415
|
+
asserts.invariant(null != tags, 'Tags should be present');
|
|
416
|
+
return {
|
|
417
|
+
...acc,
|
|
418
|
+
[repository.name]: {
|
|
419
|
+
repository,
|
|
420
|
+
tags
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
}, {});
|
|
424
|
+
const codemodResults = await runCodemodRunner(codemods, clonedRepositories, mappingsByName, options.workingDirectory);
|
|
425
|
+
if (options.pushChanges) await makePullRequestsForCodemodResults(codemods, codemodResults, clonedRepositories);
|
|
426
|
+
}
|
|
427
|
+
function codemodTargetFiltering(mappingsByName, failedRepositoryAddressesMappedByCodemodNames) {
|
|
428
|
+
return (filepath, codemod)=>{
|
|
429
|
+
const projectName = filepath.split('/')[0];
|
|
430
|
+
asserts.invariant(null != projectName, 'project name should be present');
|
|
431
|
+
const mapping = mappingsByName[projectName];
|
|
432
|
+
if (null == mapping) return false;
|
|
433
|
+
const failedRepositoryAddressesSet = failedRepositoryAddressesMappedByCodemodNames[codemod.name];
|
|
434
|
+
if (null == failedRepositoryAddressesSet || collectionIsEmpty(failedRepositoryAddressesSet)) return true;
|
|
435
|
+
return !failedRepositoryAddressesSet.has(mapping.repository.address) && (collectionIsEmpty(codemod.tags) || collectionIsEmpty(mapping.tags) || [
|
|
436
|
+
...codemod.tags
|
|
437
|
+
].some((tag)=>mapping.tags.has(tag)));
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
async function codemodPreCodemodRun(repositories, codemod) {
|
|
441
|
+
const preparationResults = await Promise.all(repositories.map((repo)=>repo.prepareForUpdate(codemod.name)));
|
|
442
|
+
const { failure: preparationFailures } = groupResults(preparationResults);
|
|
443
|
+
const failedRepositoryAddresses = preparationFailures.map((failure)=>failure.repository.address);
|
|
444
|
+
if (preparationFailures.length > 0) console.error(`\u{274C} failed to prepare [${failedRepositoryAddresses.join(', ')}] for '${codemod.name}'`);
|
|
445
|
+
return new Set(failedRepositoryAddresses);
|
|
446
|
+
}
|
|
447
|
+
async function codemodPostTransform(transformedContent) {
|
|
448
|
+
return transformedContent;
|
|
449
|
+
}
|
|
450
|
+
async function runCodemodRunner(codemods, repositories, mappingsByName, workingDirectory) {
|
|
451
|
+
const rootPaths = repositories.map((repository)=>repository.path);
|
|
452
|
+
const failedRepositoryAddressesMappedByCodemodNames = {};
|
|
453
|
+
return runCodemods(codemods, workingDirectory, {
|
|
454
|
+
rootPaths,
|
|
455
|
+
hooks: {
|
|
456
|
+
preCodemodRun: async (codemod)=>{
|
|
457
|
+
failedRepositoryAddressesMappedByCodemodNames[codemod.name] = await codemodPreCodemodRun(repositories, codemod);
|
|
458
|
+
},
|
|
459
|
+
targetFiltering: codemodTargetFiltering(mappingsByName, failedRepositoryAddressesMappedByCodemodNames),
|
|
460
|
+
postTransform: codemodPostTransform
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
}
|
|
52
464
|
async function runCodemods(codemods, transformationPath, options) {
|
|
53
465
|
const results = {};
|
|
54
466
|
for (const codemod of codemods)results[codemod.name] = await runCodemod(codemod, transformationPath, options);
|
|
@@ -255,4 +667,4 @@ function defaultedHooks(hooks) {
|
|
|
255
667
|
preCodemodRun
|
|
256
668
|
};
|
|
257
669
|
}
|
|
258
|
-
export { commitEditModifications, findAndReplace, findAndReplaceConfig, findAndReplaceConfigModifications, findAndReplaceEdits, runCodemod, runCodemods, traverseUp };
|
|
670
|
+
export { commitEditModifications, findAndReplace, findAndReplaceConfig, findAndReplaceConfigModifications, findAndReplaceEdits, runCodemod, runCodemods, runCodemodsOnProjects, traverseUp };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ResultAsync, type Result } from 'neverthrow';
|
|
2
|
+
export declare function tryCatch<T>(callback: () => T): Result<T, unknown>;
|
|
3
|
+
export declare function tryCatchAsync<T>(callback: () => Promise<T>): ResultAsync<T, unknown>;
|
|
4
|
+
export declare function groupResults<S, E>(results: Array<Result<S, E>>): {
|
|
5
|
+
success: Array<S>;
|
|
6
|
+
failure: Array<E>;
|
|
7
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kamaalio/codemod-kit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.31",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"author": "Kamaal Farah",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@ast-grep/napi": "^0.38.5",
|
|
23
23
|
"@kamaalio/kamaal": "^0.7.8",
|
|
24
|
+
"execa": "^9.6.0",
|
|
24
25
|
"fast-glob": "^3.3.3",
|
|
25
|
-
"neverthrow": "^8.2.0"
|
|
26
|
+
"neverthrow": "^8.2.0",
|
|
27
|
+
"zod": "^3.25.67"
|
|
26
28
|
},
|
|
27
29
|
"devDependencies": {
|
|
28
30
|
"@eslint/js": "^9.29.0",
|