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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nojaja
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# browser-git-ops
|
|
2
|
+
|
|
3
|
+
**目的**: ブラウザネイティブ環境(OPFS + Github/Gitlab APIs)で動作する軽量な Git 操作ライブラリを提供します。VirtualFS によるローカル差分生成と、GitHub/GitLab 向けのアダプタを通じたリモート操作を抽象化します。
|
|
4
|
+
|
|
5
|
+
**主な特徴**
|
|
6
|
+
- **仮想ファイルシステム (VirtualFS)**: ローカルワークスペースとベーススナップショットを管理し、変更セット(create/update/delete)を生成します。
|
|
7
|
+
- **Git アダプタ抽象**: `GitAdapter` インターフェースに準拠するアダプタで GitHub/GitLab API を操作できます(サンプル実装あり)。
|
|
8
|
+
- **Idempotent push**: commitKey による冪等処理サポート。
|
|
9
|
+
- **衝突検知とマージ補助**: リモート差分取り込み時にローカルの未コミット変更を検出して conflicts を報告します。
|
|
10
|
+
|
|
11
|
+
**注意(⚠️)**: 実装はユーティリティ/ライブラリ向けで、環境やアクセストークンの管理、エラーハンドリング方針は利用側で制御してください。
|
|
12
|
+
|
|
13
|
+
**プロジェクト構成(抜粋)**
|
|
14
|
+
- **src/virtualfs/virtualfs.ts**: VirtualFS 本体(初期化、read/write、push/pull、差分生成) - [src/virtualfs/virtualfs.ts](src/virtualfs/virtualfs.ts)
|
|
15
|
+
- **src/virtualfs/persistence.ts**: ストレージバックエンドの抽象と Node 用実装 - [src/virtualfs/persistence.ts](src/virtualfs/persistence.ts)
|
|
16
|
+
- **src/git/adapter.ts**: `GitAdapter` インターフェース定義 - [src/git/adapter.ts](src/git/adapter.ts)
|
|
17
|
+
- **src/git/githubAdapter.ts**: GitHub 用アダプタ(blob/tree/commit フロー、再試行ロジック) - [src/git/githubAdapter.ts](src/git/githubAdapter.ts)
|
|
18
|
+
- **test/**: ユニット/E2E テスト群(Jest / Playwright) - [test](test)
|
|
19
|
+
|
|
20
|
+
**技術スタック**
|
|
21
|
+
- TypeScript 5.x
|
|
22
|
+
- Node.js (ESM, `type: "module"`)
|
|
23
|
+
- テスト: Jest(unit) / Playwright(E2E)
|
|
24
|
+
- ビルド: `tsc`
|
|
25
|
+
|
|
26
|
+
--------------------------------------------------
|
|
27
|
+
**クイックスタート(開発用)**
|
|
28
|
+
|
|
29
|
+
1. 依存インストール
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm ci
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
2. ユニットテスト実行
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm run test
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
3. E2E テスト(Playwright)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm run test:e2e
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
4. ビルド
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm run build
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
--------------------------------------------------
|
|
54
|
+
**ライブラリ利用ガイド(Library Usage)**
|
|
55
|
+
|
|
56
|
+
下記はライブラリの代表的な使い方(TypeScript)です。詳しい実装は各ファイルを参照してください。
|
|
57
|
+
|
|
58
|
+
例: `VirtualFS` を初期化してローカル編集を push する
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import VirtualFS from './src/virtualfs/virtualfs'
|
|
62
|
+
import GitHubAdapter from './src/git/githubAdapter'
|
|
63
|
+
|
|
64
|
+
async function example() {
|
|
65
|
+
const vfs = new VirtualFS({ storageDir: '.apigit' })
|
|
66
|
+
await vfs.init()
|
|
67
|
+
|
|
68
|
+
// ワークスペース編集
|
|
69
|
+
await vfs.writeWorkspace('foo.txt', 'hello')
|
|
70
|
+
|
|
71
|
+
// 変更セット取得
|
|
72
|
+
const changes = await vfs.getChangeSet()
|
|
73
|
+
|
|
74
|
+
// GitHub アダプタを使って push
|
|
75
|
+
const gh = new GitHubAdapter({ owner: 'ORG', repo: 'REPO', token: process.env.GH_TOKEN })
|
|
76
|
+
const res = await vfs.push({ parentSha: vfs.getIndex().head, message: 'update', changes }, gh as any)
|
|
77
|
+
console.log('commitSha', res.commitSha)
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
API の概略(主要メソッド)
|
|
82
|
+
- `new VirtualFS(options?)` - オプション: `{ storageDir?: string, backend?: StorageBackend }`
|
|
83
|
+
- `init()` - バックエンド初期化と index 読み込み
|
|
84
|
+
- `writeWorkspace(filepath, content)` - ワークスペースにファイルを書き込む
|
|
85
|
+
- `deleteWorkspace(filepath)` - ワークスペース上のファイルを削除(トゥームストーン管理)
|
|
86
|
+
- `renameWorkspace(from, to)` - rename(内部では delete + create)
|
|
87
|
+
- `readWorkspace(filepath)` - ワークスペース/ベースから内容を読み出す
|
|
88
|
+
- `applyBaseSnapshot(snapshot, headSha)` - リモートスナップショットを適用
|
|
89
|
+
- `getIndex()` - 現在の index を返す
|
|
90
|
+
- `listPaths()` - 登録パス一覧
|
|
91
|
+
- `getChangeSet()` - create/update/delete の配列を生成
|
|
92
|
+
- `pull(remoteHead, baseSnapshot)` - リモート差分取り込み(conflicts を返す)
|
|
93
|
+
- `push(input, adapter?)` - 変更をコミットし(adapter があれば)リモートへ反映
|
|
94
|
+
|
|
95
|
+
GitAdapter インターフェース(`src/git/adapter.ts`)
|
|
96
|
+
- `createBlobs(changes, concurrency?)` -> Promise<Record<string,string>>
|
|
97
|
+
- `createTree(changes, baseTreeSha?)` -> Promise<string>
|
|
98
|
+
- `createCommit(message, parentSha, treeSha)` -> Promise<string>
|
|
99
|
+
- `updateRef(ref, commitSha, force?)` -> Promise<void>
|
|
100
|
+
|
|
101
|
+
実装済みアダプタの注意点:
|
|
102
|
+
- `GitHubAdapter` は `blob/tree/commit` フローを実装し、HTTP 再試行ロジックを内蔵しています(5xx, 429 のリトライ等)。実装は [src/git/githubAdapter.ts](src/git/githubAdapter.ts) を参照してください。
|
|
103
|
+
- GitLab 用実装はリポジトリ内に存在しますが、環境差異により API の振る舞いが異なるため本番運用前に検証してください。⚠️
|
|
104
|
+
|
|
105
|
+
--------------------------------------------------
|
|
106
|
+
**開発セットアップ**
|
|
107
|
+
- Node: 任意の recent Node.js(ESM サポート済み)
|
|
108
|
+
- 実行手順
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
git clone <repo>
|
|
112
|
+
cd APIGitWorkspace01
|
|
113
|
+
npm ci
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
- コマンド一覧
|
|
117
|
+
- `npm run test` : ユニットテスト(Jest)
|
|
118
|
+
- `npm run test:e2e` : Playwright E2E
|
|
119
|
+
- `npm run lint` : ESLint
|
|
120
|
+
- `npm run build` : TypeScript ビルド
|
|
121
|
+
|
|
122
|
+
テスト関連の注意:
|
|
123
|
+
- Jest は ESM を扱うため `node --experimental-vm-modules` を使用するスクリプトが package.json に設定されています。
|
|
124
|
+
|
|
125
|
+
--------------------------------------------------
|
|
126
|
+
**現在のステータス**
|
|
127
|
+
- 実装済み: `VirtualFS` のコア機能(差分生成、push/pull シミュレーション、index 管理)、`GitHubAdapter` の主要な API 呼び出し。
|
|
128
|
+
- テスト: unit テストと一部の E2E テストが含まれています(`test/` 配下)。
|
|
129
|
+
- 未確定/要検証: 外部サービス(GitLab)の細かい API 挙動、production 用のエラー・認可ポリシー。
|
|
130
|
+
|
|
131
|
+
--------------------------------------------------
|
|
132
|
+
**ライセンスとメタデータ**
|
|
133
|
+
- package name: `browser-git-ops`
|
|
134
|
+
- version: `0.0.0`
|
|
135
|
+
- module type: CommonJS (`type: commonjs`)
|
|
136
|
+
- License: MIT License
|
|
137
|
+
|
|
138
|
+
--------------------------------------------------
|
|
139
|
+
追加で欲しいもの
|
|
140
|
+
- サンプルユースケースを示す小さなサンプルリポジトリまたは `examples/` ディレクトリ
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
--------------------------------------------------
|
|
144
|
+
貢献・問い合わせ
|
|
145
|
+
- PR/Issue を歓迎します。まず issue を立て、簡単な実装提案(変更点の概要)を添えてください。
|
|
146
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface Change {
|
|
2
|
+
type: 'create' | 'update' | 'delete';
|
|
3
|
+
path: string;
|
|
4
|
+
content?: string;
|
|
5
|
+
baseSha?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface CommitResult {
|
|
8
|
+
commitSha: string;
|
|
9
|
+
}
|
|
10
|
+
export interface GitAdapter {
|
|
11
|
+
createBlobs(_changes: Change[], _concurrency?: number): Promise<Record<string, string>>;
|
|
12
|
+
createTree(_changes: Change[], _baseTreeSha?: string): Promise<string>;
|
|
13
|
+
createCommit(_message: string, _parentSha: string, _treeSha: string): Promise<string>;
|
|
14
|
+
updateRef(_ref: string, _commitSha: string, _force?: boolean): Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../../src/git/adapter.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAA;IACpC,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,UAAU;IAEzB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAEvF,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAEtE,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAA;IAErF,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7E"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { GitAdapter } from './adapter';
|
|
2
|
+
type GHOptions = {
|
|
3
|
+
owner: string;
|
|
4
|
+
repo: string;
|
|
5
|
+
token: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* リトライ可能なエラー。
|
|
9
|
+
*/
|
|
10
|
+
export declare class RetryableError extends Error {
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* リトライ不可能なエラー。
|
|
14
|
+
*/
|
|
15
|
+
export declare class NonRetryableError extends Error {
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* fetch を再試行付きで実行するユーティリティ。
|
|
19
|
+
* 5xx や 429 はリトライ対象、それ以外は NonRetryableError を投げる。
|
|
20
|
+
* @param input RequestInfo
|
|
21
|
+
* @param init RequestInit
|
|
22
|
+
* @param attempts 試行回数
|
|
23
|
+
* @param baseDelay ベースの遅延(ms)
|
|
24
|
+
*/
|
|
25
|
+
declare function fetchWithRetry(input: RequestInfo, init: RequestInit, attempts?: number, baseDelay?: number): Promise<Response>;
|
|
26
|
+
declare function classifyStatus(status: number): boolean;
|
|
27
|
+
declare function getDelayForResponse(res: Response | null, i: number, baseDelay: number): number;
|
|
28
|
+
declare function processResponseWithDelay(res: Response, i: number, baseDelay: number): Promise<Response>;
|
|
29
|
+
/**
|
|
30
|
+
* 非同期マップを並列実行するユーティリティ
|
|
31
|
+
* @param items 入力配列
|
|
32
|
+
* @param mapper マッピング関数
|
|
33
|
+
* @param concurrency 同時実行数
|
|
34
|
+
*/
|
|
35
|
+
declare function mapWithConcurrency<T, R>(items: T[], mapper: (_t: T) => Promise<R>, concurrency?: number): Promise<R[]>;
|
|
36
|
+
export declare class GitHubAdapter implements GitAdapter {
|
|
37
|
+
private opts;
|
|
38
|
+
private baseUrl;
|
|
39
|
+
private headers;
|
|
40
|
+
private _fetchWithRetry;
|
|
41
|
+
private blobCache;
|
|
42
|
+
constructor(opts: GHOptions);
|
|
43
|
+
createBlobs(changes: any[], concurrency?: number): Promise<Record<string, string>>;
|
|
44
|
+
createTree(changes: any[], baseTreeSha?: string): Promise<string>;
|
|
45
|
+
createCommit(message: string, parentSha: string, treeSha: string): Promise<string>;
|
|
46
|
+
updateRef(ref: string, commitSha: string, force?: boolean): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
export { fetchWithRetry, classifyStatus, getDelayForResponse, processResponseWithDelay, mapWithConcurrency };
|
|
49
|
+
export default GitHubAdapter;
|
|
50
|
+
//# sourceMappingURL=githubAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"githubAdapter.d.ts","sourceRoot":"","sources":["../../src/git/githubAdapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAGtC,KAAK,SAAS,GAAG;IACf,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd,CAAA;AAED;;GAEG;AACH,qBAAa,cAAe,SAAQ,KAAK;CAAG;AAE5C;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;CAAG;AAU/C;;;;;;;GAOG;AAEH,iBAAe,cAAc,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,QAAQ,SAAI,EAAE,SAAS,SAAM,qBAcjG;AAED,iBAAS,cAAc,CAAC,MAAM,EAAE,MAAM,WAErC;AAED,iBAAS,mBAAmB,CAAC,GAAG,EAAE,QAAQ,GAAG,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,UAI9E;AAED,iBAAe,wBAAwB,CAAC,GAAG,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,qBAQlF;AAED;;;;;GAKG;AAEH,iBAAS,kBAAkB,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,WAAW,SAAI,gBAc3F;AAED,qBAAa,aAAc,YAAW,UAAU;IAMlC,OAAO,CAAC,IAAI;IALxB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,eAAe,CAAqF;IAE5G,OAAO,CAAC,SAAS,CAAiC;gBAC9B,IAAI,EAAE,SAAS;IAU7B,WAAW,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,WAAW,SAAI;IAqB3C,UAAU,CAAC,OAAO,EAAE,GAAG,EAAE,EAAE,WAAW,CAAC,EAAE,MAAM;IAkB/C,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;IAQhE,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,UAAQ;CAQ9D;AAED,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,mBAAmB,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,CAAA;AAC5G,eAAe,aAAa,CAAA"}
|
|
@@ -0,0 +1,179 @@
|
|
|
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.mapWithConcurrency = exports.processResponseWithDelay = exports.getDelayForResponse = exports.classifyStatus = exports.fetchWithRetry = exports.GitHubAdapter = exports.NonRetryableError = exports.RetryableError = void 0;
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
/**
|
|
9
|
+
* リトライ可能なエラー。
|
|
10
|
+
*/
|
|
11
|
+
class RetryableError extends Error {
|
|
12
|
+
}
|
|
13
|
+
exports.RetryableError = RetryableError;
|
|
14
|
+
/**
|
|
15
|
+
* リトライ不可能なエラー。
|
|
16
|
+
*/
|
|
17
|
+
class NonRetryableError extends Error {
|
|
18
|
+
}
|
|
19
|
+
exports.NonRetryableError = NonRetryableError;
|
|
20
|
+
/**
|
|
21
|
+
* 指定ミリ秒だけ sleep するユーティリティ
|
|
22
|
+
* @param ms ミリ秒
|
|
23
|
+
*/
|
|
24
|
+
function sleep(ms) {
|
|
25
|
+
return new Promise((res) => setTimeout(res, ms));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* fetch を再試行付きで実行するユーティリティ。
|
|
29
|
+
* 5xx や 429 はリトライ対象、それ以外は NonRetryableError を投げる。
|
|
30
|
+
* @param input RequestInfo
|
|
31
|
+
* @param init RequestInit
|
|
32
|
+
* @param attempts 試行回数
|
|
33
|
+
* @param baseDelay ベースの遅延(ms)
|
|
34
|
+
*/
|
|
35
|
+
/* istanbul ignore next */
|
|
36
|
+
async function fetchWithRetry(input, init, attempts = 4, baseDelay = 300) {
|
|
37
|
+
let lastErr;
|
|
38
|
+
for (let i = 0; i < attempts; i++) {
|
|
39
|
+
try {
|
|
40
|
+
const res = await fetch(input, init);
|
|
41
|
+
return await processResponseWithDelay(res, i, baseDelay);
|
|
42
|
+
/* istanbul ignore next */
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
if (err instanceof NonRetryableError)
|
|
46
|
+
throw err;
|
|
47
|
+
lastErr = err;
|
|
48
|
+
await sleep(getDelayForResponse(null, i, baseDelay));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
throw new RetryableError(`Failed after ${attempts} attempts: ${lastErr}`);
|
|
52
|
+
}
|
|
53
|
+
exports.fetchWithRetry = fetchWithRetry;
|
|
54
|
+
function classifyStatus(status) {
|
|
55
|
+
return status >= 500 || status === 429;
|
|
56
|
+
}
|
|
57
|
+
exports.classifyStatus = classifyStatus;
|
|
58
|
+
function getDelayForResponse(res, i, baseDelay) {
|
|
59
|
+
if (!res)
|
|
60
|
+
return baseDelay * Math.pow(2, i) + Math.random() * 100;
|
|
61
|
+
const retryAfter = res.headers.get('Retry-After');
|
|
62
|
+
return retryAfter ? Number(retryAfter) * 1000 : baseDelay * Math.pow(2, i) + Math.random() * 100;
|
|
63
|
+
}
|
|
64
|
+
exports.getDelayForResponse = getDelayForResponse;
|
|
65
|
+
async function processResponseWithDelay(res, i, baseDelay) {
|
|
66
|
+
if (res.ok)
|
|
67
|
+
return res;
|
|
68
|
+
if (classifyStatus(res.status)) {
|
|
69
|
+
await sleep(getDelayForResponse(res, i, baseDelay));
|
|
70
|
+
throw new RetryableError(`HTTP ${res.status}`);
|
|
71
|
+
}
|
|
72
|
+
const txt = await res.text().catch(() => '');
|
|
73
|
+
throw new NonRetryableError(`HTTP ${res.status}: ${txt}`);
|
|
74
|
+
}
|
|
75
|
+
exports.processResponseWithDelay = processResponseWithDelay;
|
|
76
|
+
/**
|
|
77
|
+
* 非同期マップを並列実行するユーティリティ
|
|
78
|
+
* @param items 入力配列
|
|
79
|
+
* @param mapper マッピング関数
|
|
80
|
+
* @param concurrency 同時実行数
|
|
81
|
+
*/
|
|
82
|
+
/* istanbul ignore next */
|
|
83
|
+
function mapWithConcurrency(items, mapper, concurrency = 5) {
|
|
84
|
+
const results = [];
|
|
85
|
+
let idx = 0;
|
|
86
|
+
const runners = [];
|
|
87
|
+
const run = async () => {
|
|
88
|
+
while (idx < items.length) {
|
|
89
|
+
const i = idx++;
|
|
90
|
+
if (i >= items.length)
|
|
91
|
+
break;
|
|
92
|
+
const r = await mapper(items[i]);
|
|
93
|
+
results[i] = r;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
for (let i = 0; i < Math.min(concurrency, items.length); i++)
|
|
97
|
+
runners.push(run());
|
|
98
|
+
return Promise.all(runners).then(() => results);
|
|
99
|
+
}
|
|
100
|
+
exports.mapWithConcurrency = mapWithConcurrency;
|
|
101
|
+
class GitHubAdapter {
|
|
102
|
+
opts;
|
|
103
|
+
baseUrl;
|
|
104
|
+
headers;
|
|
105
|
+
_fetchWithRetry;
|
|
106
|
+
// simple in-memory blob cache: contentSha -> blobSha
|
|
107
|
+
blobCache = new Map();
|
|
108
|
+
constructor(opts) {
|
|
109
|
+
this.opts = opts;
|
|
110
|
+
this.baseUrl = `https://api.github.com/repos/${opts.owner}/${opts.repo}`;
|
|
111
|
+
this.headers = {
|
|
112
|
+
Authorization: `token ${opts.token}`,
|
|
113
|
+
Accept: 'application/vnd.github+json',
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
};
|
|
116
|
+
this._fetchWithRetry = fetchWithRetry;
|
|
117
|
+
}
|
|
118
|
+
async createBlobs(changes, concurrency = 5) {
|
|
119
|
+
const tasks = changes.filter((c) => c.type === 'create' || c.type === 'update');
|
|
120
|
+
const mapper = async (ch) => {
|
|
121
|
+
// compute simple content hash to enable cache lookup
|
|
122
|
+
const contentHash = crypto_1.default.createHash('sha1').update(ch.content || '', 'utf8').digest('hex');
|
|
123
|
+
const cached = this.blobCache.get(contentHash);
|
|
124
|
+
if (cached)
|
|
125
|
+
return { path: ch.path, sha: cached };
|
|
126
|
+
const body = JSON.stringify({ content: ch.content, encoding: 'utf-8' });
|
|
127
|
+
const res = await this._fetchWithRetry(`${this.baseUrl}/git/blobs`, { method: 'POST', headers: this.headers, body }, 4, 300);
|
|
128
|
+
const j = await res.json();
|
|
129
|
+
if (!j.sha)
|
|
130
|
+
throw new NonRetryableError('blob response missing sha');
|
|
131
|
+
this.blobCache.set(contentHash, j.sha);
|
|
132
|
+
return { path: ch.path, sha: j.sha };
|
|
133
|
+
};
|
|
134
|
+
const results = await mapWithConcurrency(tasks, mapper, concurrency);
|
|
135
|
+
const map = {};
|
|
136
|
+
for (const r of results)
|
|
137
|
+
map[r.path] = r.sha;
|
|
138
|
+
return map;
|
|
139
|
+
}
|
|
140
|
+
async createTree(changes, baseTreeSha) {
|
|
141
|
+
const tree = [];
|
|
142
|
+
for (const c of changes) {
|
|
143
|
+
if (c.type === 'delete') {
|
|
144
|
+
tree.push({ path: c.path, mode: '100644', sha: null });
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
if (!c.blobSha)
|
|
148
|
+
throw new NonRetryableError(`missing blobSha for ${c.path}`);
|
|
149
|
+
tree.push({ path: c.path, mode: '100644', type: 'blob', sha: c.blobSha });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const body = { tree };
|
|
153
|
+
if (baseTreeSha)
|
|
154
|
+
body.base_tree = baseTreeSha;
|
|
155
|
+
const res = await this._fetchWithRetry(`${this.baseUrl}/git/trees`, { method: 'POST', headers: this.headers, body: JSON.stringify(body) }, 4, 300);
|
|
156
|
+
const j = await res.json();
|
|
157
|
+
if (!j.sha)
|
|
158
|
+
throw new NonRetryableError('createTree response missing sha');
|
|
159
|
+
return j.sha;
|
|
160
|
+
}
|
|
161
|
+
async createCommit(message, parentSha, treeSha) {
|
|
162
|
+
const body = JSON.stringify({ message, tree: treeSha, parents: [parentSha] });
|
|
163
|
+
const res = await this._fetchWithRetry(`${this.baseUrl}/git/commits`, { method: 'POST', headers: this.headers, body }, 4, 300);
|
|
164
|
+
const j = await res.json();
|
|
165
|
+
if (!j.sha)
|
|
166
|
+
throw new NonRetryableError('createCommit response missing sha');
|
|
167
|
+
return j.sha;
|
|
168
|
+
}
|
|
169
|
+
async updateRef(ref, commitSha, force = false) {
|
|
170
|
+
const body = JSON.stringify({ sha: commitSha, force });
|
|
171
|
+
const res = await this._fetchWithRetry(`${this.baseUrl}/git/refs/${ref}`, { method: 'PATCH', headers: this.headers, body }, 4, 300);
|
|
172
|
+
if (!res.ok) {
|
|
173
|
+
const txt = await res.text().catch(() => '');
|
|
174
|
+
throw new NonRetryableError(`updateRef failed: ${res.status} ${txt}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
exports.GitHubAdapter = GitHubAdapter;
|
|
179
|
+
exports.default = GitHubAdapter;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { GitAdapter } from './adapter';
|
|
2
|
+
type GLOpts = {
|
|
3
|
+
projectId: string;
|
|
4
|
+
token: string;
|
|
5
|
+
host?: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
*
|
|
9
|
+
*/
|
|
10
|
+
export declare class GitLabAdapter implements GitAdapter {
|
|
11
|
+
private opts;
|
|
12
|
+
private baseUrl;
|
|
13
|
+
private headers;
|
|
14
|
+
private pendingActions;
|
|
15
|
+
private maxRetries;
|
|
16
|
+
private baseBackoff;
|
|
17
|
+
/**
|
|
18
|
+
* GitLabAdapter を初期化します。
|
|
19
|
+
* @param {GLOpts} opts 設定オブジェクト
|
|
20
|
+
*/
|
|
21
|
+
constructor(opts: GLOpts);
|
|
22
|
+
/**
|
|
23
|
+
* コンテンツから sha1 を算出します。
|
|
24
|
+
* @param {string} content コンテンツ
|
|
25
|
+
* @returns {string} sha1 ハッシュ
|
|
26
|
+
*/
|
|
27
|
+
private shaOf;
|
|
28
|
+
/**
|
|
29
|
+
* 変更一覧から blob sha のマップを作成します(疑似実装)。
|
|
30
|
+
* @param {any[]} changes 変更一覧
|
|
31
|
+
* @returns {Promise<Record<string,string>>} path->sha マップ
|
|
32
|
+
*/
|
|
33
|
+
createBlobs(changes: any[]): Promise<Record<string, string>>;
|
|
34
|
+
/**
|
|
35
|
+
* 互換用のツリー作成。実際には actions を保持しておき、マーカーを返します。
|
|
36
|
+
* @param {any[]} _changes 変更一覧
|
|
37
|
+
* @param {string} [_baseTreeSha] ベースツリー(未使用)
|
|
38
|
+
* @returns {Promise<string>} マーカー文字列
|
|
39
|
+
*/
|
|
40
|
+
createTree(_changes: any[], _baseTreeSha?: string): Promise<string>;
|
|
41
|
+
/**
|
|
42
|
+
* createTree で保持した actions があればコミットし、なければ parentSha を返します。
|
|
43
|
+
* @param {string} message コミットメッセージ
|
|
44
|
+
* @param {string} parentSha 親コミット SHA
|
|
45
|
+
* @param {string} _treeSha ツリー SHA(未使用)
|
|
46
|
+
* @returns {Promise<string>} 新規コミット SHA または parentSha
|
|
47
|
+
*/
|
|
48
|
+
createCommit(message: string, parentSha: string, _treeSha: string): Promise<any>;
|
|
49
|
+
/**
|
|
50
|
+
* actions を用いて GitLab のコミット API を呼び出します。
|
|
51
|
+
* @param {string} branch ブランチ名
|
|
52
|
+
* @param {string} message コミットメッセージ
|
|
53
|
+
* @param {{type:string,path:string,content?:string}[]} changes 変更一覧
|
|
54
|
+
* @returns {Promise<any>} コミット応答(id など)
|
|
55
|
+
*/
|
|
56
|
+
createCommitWithActions(branch: string, message: string, changes: Array<{
|
|
57
|
+
type: string;
|
|
58
|
+
path: string;
|
|
59
|
+
content?: string;
|
|
60
|
+
}>): Promise<any>;
|
|
61
|
+
/**
|
|
62
|
+
* fetch をリトライ付きで実行します。
|
|
63
|
+
* @param {string} url リクエスト URL
|
|
64
|
+
* @param {RequestInit} opts fetch オプション
|
|
65
|
+
* @param {number} [retries] 最大リトライ回数
|
|
66
|
+
* @returns {Promise<Response>} レスポンス
|
|
67
|
+
*/
|
|
68
|
+
private fetchWithRetry;
|
|
69
|
+
/**
|
|
70
|
+
* ステータスが再試行対象か判定します。
|
|
71
|
+
* @param {number} status ステータスコード
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
74
|
+
private isRetryableStatus;
|
|
75
|
+
/**
|
|
76
|
+
* バックオフ時間を計算します。
|
|
77
|
+
* @param {number} attempt 試行回数(1..)
|
|
78
|
+
* @returns {number} ミリ秒
|
|
79
|
+
*/
|
|
80
|
+
private backoffMs;
|
|
81
|
+
/**
|
|
82
|
+
* リファレンス更新は不要なため noop 実装です。
|
|
83
|
+
* @param {string} _ref ref 名
|
|
84
|
+
* @param {string} _commitSha コミット SHA
|
|
85
|
+
* @param {boolean} [_force]
|
|
86
|
+
* @returns {Promise<void>}
|
|
87
|
+
*/
|
|
88
|
+
updateRef(_ref: string, _commitSha: string, _force?: boolean): Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
export default GitLabAdapter;
|
|
91
|
+
//# sourceMappingURL=gitlabAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gitlabAdapter.d.ts","sourceRoot":"","sources":["../../src/git/gitlabAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAGtC,KAAK,MAAM,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEjE;;GAEG;AACH,qBAAa,aAAc,YAAW,UAAU;IAWlC,OAAO,CAAC,IAAI;IAVxB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,cAAc,CAA8E;IACpG,OAAO,CAAC,UAAU,CAAI;IACtB,OAAO,CAAC,WAAW,CAAM;IAEzB;;;OAGG;gBACiB,IAAI,EAAE,MAAM;IAMhC;;;;OAIG;IACH,OAAO,CAAC,KAAK;IAIb;;;;OAIG;IACG,WAAW,CAAC,OAAO,EAAE,GAAG,EAAE;IAOhC;;;;;OAKG;IACG,UAAU,CAAC,QAAQ,EAAE,GAAG,EAAE,EAAE,YAAY,CAAC,EAAE,MAAM;IAWvD;;;;;;OAMG;IACG,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;IAgBvE;;;;;;OAMG;IACG,uBAAuB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAgC/H;;;;;;OAMG;YACW,cAAc;IAkB5B;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAIzB;;;;OAIG;IACH,OAAO,CAAC,SAAS;IAMjB;;;;;;OAMG;IACG,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,UAAQ;CAGjE;AAED,eAAe,aAAa,CAAA"}
|