browser-git-ops 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/dist/git/adapter.d.ts +16 -0
- package/dist/git/adapter.d.ts.map +1 -0
- package/dist/git/adapter.js +2 -0
- package/dist/git/githubAdapter.d.ts +50 -0
- package/dist/git/githubAdapter.d.ts.map +1 -0
- package/dist/git/githubAdapter.js +179 -0
- package/dist/git/gitlabAdapter.d.ts +91 -0
- package/dist/git/gitlabAdapter.d.ts.map +1 -0
- package/dist/git/gitlabAdapter.js +182 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/test/e2e/github.spec.d.ts +2 -0
- package/dist/test/e2e/github.spec.d.ts.map +1 -0
- package/dist/test/e2e/github.spec.js +47 -0
- package/dist/test/e2e/gitlab.spec.d.ts +2 -0
- package/dist/test/e2e/gitlab.spec.d.ts.map +1 -0
- package/dist/test/e2e/gitlab.spec.js +34 -0
- package/dist/test/e2e/virtualfs.spec.d.ts +2 -0
- package/dist/test/e2e/virtualfs.spec.d.ts.map +1 -0
- package/dist/test/e2e/virtualfs.spec.js +409 -0
- package/dist/virtualfs/persistence.d.ts +149 -0
- package/dist/virtualfs/persistence.d.ts.map +1 -0
- package/dist/virtualfs/persistence.js +294 -0
- package/dist/virtualfs/types.d.ts +46 -0
- package/dist/virtualfs/types.d.ts.map +1 -0
- package/dist/virtualfs/types.js +2 -0
- package/dist/virtualfs/virtualfs.d.ts +189 -0
- package/dist/virtualfs/virtualfs.d.ts.map +1 -0
- package/dist/virtualfs/virtualfs.js +496 -0
- package/package.json +47 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.GitLabAdapter = void 0;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
/**
|
|
9
|
+
*
|
|
10
|
+
*/
|
|
11
|
+
class GitLabAdapter {
|
|
12
|
+
opts;
|
|
13
|
+
baseUrl;
|
|
14
|
+
headers;
|
|
15
|
+
pendingActions = null;
|
|
16
|
+
maxRetries = 3;
|
|
17
|
+
baseBackoff = 300;
|
|
18
|
+
/**
|
|
19
|
+
* GitLabAdapter を初期化します。
|
|
20
|
+
* @param {GLOpts} opts 設定オブジェクト
|
|
21
|
+
*/
|
|
22
|
+
constructor(opts) {
|
|
23
|
+
this.opts = opts;
|
|
24
|
+
const host = opts.host || 'https://gitlab.com';
|
|
25
|
+
this.baseUrl = `${host}/api/v4/projects/${encodeURIComponent(opts.projectId)}`;
|
|
26
|
+
this.headers = { 'PRIVATE-TOKEN': opts.token, 'Content-Type': 'application/json' };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* コンテンツから sha1 を算出します。
|
|
30
|
+
* @param {string} content コンテンツ
|
|
31
|
+
* @returns {string} sha1 ハッシュ
|
|
32
|
+
*/
|
|
33
|
+
shaOf(content) {
|
|
34
|
+
return crypto_1.default.createHash('sha1').update(content, 'utf8').digest('hex');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 変更一覧から blob sha のマップを作成します(疑似実装)。
|
|
38
|
+
* @param {any[]} changes 変更一覧
|
|
39
|
+
* @returns {Promise<Record<string,string>>} path->sha マップ
|
|
40
|
+
*/
|
|
41
|
+
async createBlobs(changes) {
|
|
42
|
+
const map = {};
|
|
43
|
+
for (const c of changes) {
|
|
44
|
+
if (c.type === 'create' || c.type === 'update')
|
|
45
|
+
map[c.path] = this.shaOf(c.content);
|
|
46
|
+
}
|
|
47
|
+
return map;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 互換用のツリー作成。実際には actions を保持しておき、マーカーを返します。
|
|
51
|
+
* @param {any[]} _changes 変更一覧
|
|
52
|
+
* @param {string} [_baseTreeSha] ベースツリー(未使用)
|
|
53
|
+
* @returns {Promise<string>} マーカー文字列
|
|
54
|
+
*/
|
|
55
|
+
async createTree(_changes, _baseTreeSha) {
|
|
56
|
+
// Store actions for later commit; return marker token
|
|
57
|
+
const actions = (_changes || []).map((c) => {
|
|
58
|
+
if (c.type === 'delete')
|
|
59
|
+
return { action: 'delete', file_path: c.path };
|
|
60
|
+
if (c.type === 'create')
|
|
61
|
+
return { action: 'create', file_path: c.path, content: c.content };
|
|
62
|
+
return { action: 'update', file_path: c.path, content: c.content };
|
|
63
|
+
});
|
|
64
|
+
this.pendingActions = actions;
|
|
65
|
+
return `gitlab-tree-${Date.now()}-${Math.floor(Math.random() * 100000)}`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* createTree で保持した actions があればコミットし、なければ parentSha を返します。
|
|
69
|
+
* @param {string} message コミットメッセージ
|
|
70
|
+
* @param {string} parentSha 親コミット SHA
|
|
71
|
+
* @param {string} _treeSha ツリー SHA(未使用)
|
|
72
|
+
* @returns {Promise<string>} 新規コミット SHA または parentSha
|
|
73
|
+
*/
|
|
74
|
+
async createCommit(message, parentSha, _treeSha) {
|
|
75
|
+
// If pendingActions exist (created via createTree), use commits API
|
|
76
|
+
if (this.pendingActions && this.pendingActions.length > 0) {
|
|
77
|
+
const branch = 'main';
|
|
78
|
+
const res = await this.createCommitWithActions(branch, message, this.pendingActions.map((a) => ({ type: a.action === 'delete' ? 'delete' : a.action === 'create' ? 'create' : 'update', path: a.file_path, content: a.content })));
|
|
79
|
+
this.pendingActions = null;
|
|
80
|
+
return res;
|
|
81
|
+
}
|
|
82
|
+
// Fallback: no-op commit (return parentSha)
|
|
83
|
+
return parentSha;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* actions を用いて GitLab のコミット API を呼び出します。
|
|
87
|
+
* @param {string} branch ブランチ名
|
|
88
|
+
* @param {string} message コミットメッセージ
|
|
89
|
+
* @param {{type:string,path:string,content?:string}[]} changes 変更一覧
|
|
90
|
+
* @returns {Promise<any>} コミット応答(id など)
|
|
91
|
+
*/
|
|
92
|
+
async createCommitWithActions(branch, message, changes) {
|
|
93
|
+
const url = `${this.baseUrl}/repository/commits`;
|
|
94
|
+
const actions = changes.map((c) => {
|
|
95
|
+
if (c.type === 'delete')
|
|
96
|
+
return { action: 'delete', file_path: c.path };
|
|
97
|
+
if (c.type === 'create')
|
|
98
|
+
return { action: 'create', file_path: c.path, content: c.content };
|
|
99
|
+
return { action: 'update', file_path: c.path, content: c.content };
|
|
100
|
+
});
|
|
101
|
+
/**
|
|
102
|
+
* GitLab のコミット API を呼び出します。
|
|
103
|
+
* @param {string} branch ブランチ名
|
|
104
|
+
* @param {string} message コミットメッセージ
|
|
105
|
+
* @param {{type:string,path:string,content?:string}[]} changes 変更一覧
|
|
106
|
+
* @returns {Promise<any>} 作成されたコミットの識別子
|
|
107
|
+
*/
|
|
108
|
+
const body = JSON.stringify({ branch, commit_message: message, actions });
|
|
109
|
+
const res = await this.fetchWithRetry(url, { method: 'POST', headers: this.headers, body });
|
|
110
|
+
const text = await res.text().catch(() => '');
|
|
111
|
+
let j = null;
|
|
112
|
+
try {
|
|
113
|
+
j = text ? JSON.parse(text) : null;
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
throw new Error(`GitLab commit invalid JSON response: ${text}`);
|
|
117
|
+
}
|
|
118
|
+
// validate expected fields (GitLab returns 'id' for commit id)
|
|
119
|
+
if (!j || (!j.id && !j.commit)) {
|
|
120
|
+
throw new Error(`GitLab commit unexpected response: ${JSON.stringify(j)}`);
|
|
121
|
+
}
|
|
122
|
+
return j.id || j.commit || j;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* fetch をリトライ付きで実行します。
|
|
126
|
+
* @param {string} url リクエスト URL
|
|
127
|
+
* @param {RequestInit} opts fetch オプション
|
|
128
|
+
* @param {number} [retries] 最大リトライ回数
|
|
129
|
+
* @returns {Promise<Response>} レスポンス
|
|
130
|
+
*/
|
|
131
|
+
async fetchWithRetry(url, opts, retries = this.maxRetries) {
|
|
132
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
133
|
+
try {
|
|
134
|
+
const res = await fetch(url, opts);
|
|
135
|
+
if (!this.isRetryableStatus(res.status))
|
|
136
|
+
return res;
|
|
137
|
+
if (attempt === retries)
|
|
138
|
+
return res;
|
|
139
|
+
const wait = this.backoffMs(attempt);
|
|
140
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
if (attempt === retries)
|
|
144
|
+
throw err;
|
|
145
|
+
const wait = this.backoffMs(attempt);
|
|
146
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// should not reach here
|
|
150
|
+
throw new Error('fetchWithRetry: unexpected exit');
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* ステータスが再試行対象か判定します。
|
|
154
|
+
* @param {number} status ステータスコード
|
|
155
|
+
* @returns {boolean}
|
|
156
|
+
*/
|
|
157
|
+
isRetryableStatus(status) {
|
|
158
|
+
return status === 429 || (status >= 500 && status < 600);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* バックオフ時間を計算します。
|
|
162
|
+
* @param {number} attempt 試行回数(1..)
|
|
163
|
+
* @returns {number} ミリ秒
|
|
164
|
+
*/
|
|
165
|
+
backoffMs(attempt) {
|
|
166
|
+
const base = this.baseBackoff * Math.pow(2, attempt - 1);
|
|
167
|
+
const jitter = Math.floor(Math.random() * base * 0.3);
|
|
168
|
+
return base + jitter;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* リファレンス更新は不要なため noop 実装です。
|
|
172
|
+
* @param {string} _ref ref 名
|
|
173
|
+
* @param {string} _commitSha コミット SHA
|
|
174
|
+
* @param {boolean} [_force]
|
|
175
|
+
* @returns {Promise<void>}
|
|
176
|
+
*/
|
|
177
|
+
async updateRef(_ref, _commitSha, _force = false) {
|
|
178
|
+
// Not required when using commits API
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
exports.GitLabAdapter = GitLabAdapter;
|
|
182
|
+
exports.default = GitLabAdapter;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as VirtualFS } from './virtualfs/virtualfs';
|
|
2
|
+
export { default as GitHubAdapter } from './git/githubAdapter';
|
|
3
|
+
export { default as GitLabAdapter } from './git/gitlabAdapter';
|
|
4
|
+
export { default } from './virtualfs/virtualfs';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,uBAAuB,CAAA;AAC5D,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAC9D,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAC9D,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.default = exports.GitLabAdapter = exports.GitHubAdapter = exports.VirtualFS = void 0;
|
|
7
|
+
var virtualfs_1 = require("./virtualfs/virtualfs");
|
|
8
|
+
Object.defineProperty(exports, "VirtualFS", { enumerable: true, get: function () { return __importDefault(virtualfs_1).default; } });
|
|
9
|
+
var githubAdapter_1 = require("./git/githubAdapter");
|
|
10
|
+
Object.defineProperty(exports, "GitHubAdapter", { enumerable: true, get: function () { return __importDefault(githubAdapter_1).default; } });
|
|
11
|
+
var gitlabAdapter_1 = require("./git/gitlabAdapter");
|
|
12
|
+
Object.defineProperty(exports, "GitLabAdapter", { enumerable: true, get: function () { return __importDefault(gitlabAdapter_1).default; } });
|
|
13
|
+
var virtualfs_2 = require("./virtualfs/virtualfs");
|
|
14
|
+
Object.defineProperty(exports, "default", { enumerable: true, get: function () { return __importDefault(virtualfs_2).default; } });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github.spec.d.ts","sourceRoot":"","sources":["../../../test/e2e/github.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const test_1 = require("@playwright/test");
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
// runtime: load built library from dist (compiled tests live under dist/test/e2e)
|
|
10
|
+
// @ts-ignore: require of built JS module for e2e runtime
|
|
11
|
+
const { GitHubAdapter } = require('../../index.js');
|
|
12
|
+
const cfgPath = path_1.default.join(__dirname, 'github.config.json');
|
|
13
|
+
(0, test_1.test)('github adapter smoke (skips if config missing)', async () => {
|
|
14
|
+
if (!fs_1.default.existsSync(cfgPath)) {
|
|
15
|
+
test_1.test.skip(true, 'github.config.json not found — fill test/e2e/github.config.json based on example');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const raw = fs_1.default.readFileSync(cfgPath, 'utf8');
|
|
19
|
+
const cfg = JSON.parse(raw);
|
|
20
|
+
if (!cfg.token || cfg.token.startsWith('ghp_replace')) {
|
|
21
|
+
test_1.test.skip(true, 'github token not set in config');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const adapter = new GitHubAdapter({ owner: cfg.owner, repo: cfg.repo, token: cfg.token });
|
|
25
|
+
// simple smoke: create a small blob and then delete it via a simulated tree/commit flow
|
|
26
|
+
const tmpPath = `e2e-test-${Date.now()}.txt`;
|
|
27
|
+
const changes = [{ type: 'create', path: tmpPath, content: 'playwright test content' }];
|
|
28
|
+
const blobMap = await adapter.createBlobs(changes, 2);
|
|
29
|
+
(0, test_1.expect)(blobMap[tmpPath]).toBeTruthy();
|
|
30
|
+
// create tree and commit require a parent SHA — attempt to get default branch ref
|
|
31
|
+
// fetch default branch ref
|
|
32
|
+
const res = await fetch(`https://api.github.com/repos/${cfg.owner}/${cfg.repo}`, { headers: { Authorization: `token ${cfg.token}`, Accept: 'application/vnd.github+json' } });
|
|
33
|
+
(0, test_1.expect)(res.ok).toBeTruthy();
|
|
34
|
+
const repoInfo = await res.json();
|
|
35
|
+
const defaultBranch = repoInfo.default_branch || 'main';
|
|
36
|
+
// get commit sha of default branch
|
|
37
|
+
const refRes = await fetch(`https://api.github.com/repos/${cfg.owner}/${cfg.repo}/git/ref/heads/${defaultBranch}`, { headers: { Authorization: `token ${cfg.token}`, Accept: 'application/vnd.github+json' } });
|
|
38
|
+
(0, test_1.expect)(refRes.ok).toBeTruthy();
|
|
39
|
+
const refInfo = await refRes.json();
|
|
40
|
+
const parentSha = refInfo.object.sha;
|
|
41
|
+
const treeSha = await adapter.createTree([{ ...changes[0], blobSha: blobMap[tmpPath] }], undefined);
|
|
42
|
+
(0, test_1.expect)(treeSha).toBeTruthy();
|
|
43
|
+
const commitSha = await adapter.createCommit('playwright e2e test', parentSha, treeSha);
|
|
44
|
+
(0, test_1.expect)(commitSha).toBeTruthy();
|
|
45
|
+
// cleanup: attempt to update ref back to parentSha (do not force)
|
|
46
|
+
await adapter.updateRef(`heads/${defaultBranch}`, commitSha, false);
|
|
47
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitlab.spec.d.ts","sourceRoot":"","sources":["../../../test/e2e/gitlab.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const test_1 = require("@playwright/test");
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
// runtime: load built library from dist (compiled tests live under dist/test/e2e)
|
|
10
|
+
// @ts-ignore: require of built JS module for e2e runtime
|
|
11
|
+
const { GitLabAdapter } = require('../../index.js');
|
|
12
|
+
const cfgPath = path_1.default.join(__dirname, 'gitlab.config.json');
|
|
13
|
+
(0, test_1.test)('gitlab adapter smoke (skips if config missing)', async () => {
|
|
14
|
+
if (!fs_1.default.existsSync(cfgPath)) {
|
|
15
|
+
test_1.test.skip(true, 'gitlab.config.json not found — fill test/e2e/gitlab.config.json based on example');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const raw = fs_1.default.readFileSync(cfgPath, 'utf8');
|
|
19
|
+
const cfg = JSON.parse(raw);
|
|
20
|
+
if (!cfg.token || cfg.token.startsWith('glpat_replace')) {
|
|
21
|
+
test_1.test.skip(true, 'gitlab token not set in config');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const adapter = new GitLabAdapter({ projectId: cfg.projectId, token: cfg.token, host: cfg.host });
|
|
25
|
+
const tmpPath = `e2e-gitlab-${Date.now()}.txt`;
|
|
26
|
+
const changes = [{ type: 'create', path: tmpPath, content: 'gitlab e2e test content' }];
|
|
27
|
+
// create commit that adds the file
|
|
28
|
+
const branch = 'main'; // default; could be parameterized
|
|
29
|
+
const commit = await adapter.createCommitWithActions(branch, 'playwright gitlab e2e create', changes);
|
|
30
|
+
(0, test_1.expect)(commit).toBeTruthy();
|
|
31
|
+
// cleanup: delete the file in another commit
|
|
32
|
+
const del = await adapter.createCommitWithActions(branch, 'playwright gitlab e2e delete', [{ type: 'delete', path: tmpPath }]);
|
|
33
|
+
(0, test_1.expect)(del).toBeTruthy();
|
|
34
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"virtualfs.spec.d.ts","sourceRoot":"","sources":["../../../test/e2e/virtualfs.spec.ts"],"names":[],"mappings":""}
|