@oh-my-ghaad/gitlab 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/README.md +105 -0
- package/dist/index.cjs +320 -0
- package/dist/index.d.cts +39 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +294 -0
- package/package.json +25 -0
- package/src/adapter.ts +346 -0
- package/src/index.ts +1 -0
- package/src/utils.ts +32 -0
- package/tsconfig.json +7 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2025 Chris Griffing
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# @oh-my-ghaad/adapter-github
|
|
2
|
+
|
|
3
|
+
GitHub adapter for Oh My GHAAD, providing GitHub-specific implementation for using GitHub repositories as JSON databases.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The GitHub adapter implements the core adapter interface to provide:
|
|
8
|
+
- GitHub repository access
|
|
9
|
+
- File operations (create, read, update, delete)
|
|
10
|
+
- OAuth integration
|
|
11
|
+
- Repository installation handling
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @oh-my-ghaad/adapter-github @oh-my-ghaad/core
|
|
17
|
+
# or
|
|
18
|
+
yarn add @oh-my-ghaad/adapter-github @oh-my-ghaad/core
|
|
19
|
+
# or
|
|
20
|
+
pnpm add @oh-my-ghaad/adapter-github @oh-my-ghaad/core
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Basic Usage
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { Engine } from '@oh-my-ghaad/core';
|
|
27
|
+
import { GithubAdapter } from '@oh-my-ghaad/adapter-github';
|
|
28
|
+
|
|
29
|
+
// Create the engine instance with the GitHub adapter
|
|
30
|
+
const engine = new Engine({
|
|
31
|
+
adapters: [
|
|
32
|
+
new GithubAdapter({
|
|
33
|
+
clientId: "YOUR_GITHUB_CLIENT_ID",
|
|
34
|
+
redirectUri: process.env.YOUR_GITHUB_REDIRECT_URI!,
|
|
35
|
+
accessManagementUrl:
|
|
36
|
+
"https://github.com/apps/[YOUR_GITHUB_APP_NAME]/installations/new",
|
|
37
|
+
}),
|
|
38
|
+
],
|
|
39
|
+
collections: [/* your collections */],
|
|
40
|
+
appConfig: {
|
|
41
|
+
persisted: true
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Configuration
|
|
47
|
+
|
|
48
|
+
The GitHub adapter requires the following configuration:
|
|
49
|
+
|
|
50
|
+
- `clientId`: The client ID of your GitHub app
|
|
51
|
+
- `redirectUri`: The redirect URI of your GitHub app
|
|
52
|
+
- `accessManagementUrl`: The URL to the access management page of your GitHub app
|
|
53
|
+
|
|
54
|
+
## OAUTH
|
|
55
|
+
|
|
56
|
+
You will need to set up an OAuth app on GitHub. You will also need to set up a backend server to handle the OAuth flow since GitHub does not allow the implicit flow for OAuth apps.
|
|
57
|
+
|
|
58
|
+
See GitHub's [OAuth App documentation](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app) for more details.
|
|
59
|
+
|
|
60
|
+
### Example Server Setup
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
export async function POST(request: Request) {
|
|
64
|
+
const { code } = await request.json();
|
|
65
|
+
|
|
66
|
+
if (!code) {
|
|
67
|
+
return new Response("Missing code", { status: 400 });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const response = await fetch("https://github.com/login/oauth/access_token", {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: {
|
|
73
|
+
Accept: "application/json",
|
|
74
|
+
"Content-Type": "application/json",
|
|
75
|
+
},
|
|
76
|
+
body: JSON.stringify({
|
|
77
|
+
client_id: process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID!,
|
|
78
|
+
client_secret: process.env.GITHUB_CLIENT_SECRET!,
|
|
79
|
+
code: code,
|
|
80
|
+
redirect_uri: process.env.NEXT_PUBLIC_GITHUB_REDIRECT_URI!,
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
return new Response("Failed to fetch token", { status: 500 });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const responseJson = await response.json();
|
|
89
|
+
|
|
90
|
+
if (!responseJson.access_token) {
|
|
91
|
+
return new Response("Failed to fetch token", { status: 500 });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return new Response(JSON.stringify(responseJson));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
## Related Packages
|
|
101
|
+
|
|
102
|
+
- [@oh-my-ghaad/core](../core/README.md) - Core functionality
|
|
103
|
+
- [@oh-my-ghaad/react](../react/README.md) - React integration
|
|
104
|
+
|
|
105
|
+
For more examples and detailed usage, see the [main README](../../README.md) and the [demo app](../../apps/demo-app/README.md).
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
|
+
var __export = (target, all) => {
|
|
6
|
+
for (var name in all)
|
|
7
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
8
|
+
};
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
18
|
+
|
|
19
|
+
// src/index.ts
|
|
20
|
+
var index_exports = {};
|
|
21
|
+
__export(index_exports, {
|
|
22
|
+
GitlabAdapter: () => GitlabAdapter
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(index_exports);
|
|
25
|
+
|
|
26
|
+
// src/adapter.ts
|
|
27
|
+
var import_core = require("@oh-my-ghaad/core");
|
|
28
|
+
var import_rest = require("@gitbeaker/rest");
|
|
29
|
+
|
|
30
|
+
// src/utils.ts
|
|
31
|
+
function base64ToUtf8(base64String) {
|
|
32
|
+
try {
|
|
33
|
+
const byteCharacters = atob(base64String);
|
|
34
|
+
const byteNumbers = new Array(byteCharacters.length);
|
|
35
|
+
for (let i = 0; i < byteCharacters.length; i++) {
|
|
36
|
+
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
37
|
+
}
|
|
38
|
+
const byteArray = new Uint8Array(byteNumbers);
|
|
39
|
+
const decoder = new TextDecoder("utf-8");
|
|
40
|
+
return decoder.decode(byteArray);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.error("Error decoding base64 with TextDecoder:", e);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function utf8ToBase64(utf8String) {
|
|
47
|
+
try {
|
|
48
|
+
const encoder = new TextEncoder();
|
|
49
|
+
const byteArray = encoder.encode(utf8String);
|
|
50
|
+
return btoa(String.fromCharCode(...byteArray));
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.error("Error encoding string as base64:", e);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/adapter.ts
|
|
58
|
+
var GitlabAdapter = class extends import_core.Adapter {
|
|
59
|
+
name = "GitLab";
|
|
60
|
+
icon = "https://about.gitlab.com/images/ico/favicon-192x192.png";
|
|
61
|
+
primaryColor = "#FC6D26";
|
|
62
|
+
secondaryColor = "#000000";
|
|
63
|
+
oauthUrl = "https://gitlab.com/oauth/authorize";
|
|
64
|
+
baseUrl = "https://gitlab.com";
|
|
65
|
+
api;
|
|
66
|
+
constructor(props) {
|
|
67
|
+
super(props);
|
|
68
|
+
this.api = new import_rest.Gitlab({
|
|
69
|
+
host: this.baseUrl,
|
|
70
|
+
oauthToken: this.token
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
setToken(token) {
|
|
74
|
+
super.setToken(token);
|
|
75
|
+
this.api = new import_rest.Gitlab({
|
|
76
|
+
host: this.baseUrl,
|
|
77
|
+
oauthToken: this.token
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async fetchRepositories() {
|
|
81
|
+
const repositoriesOrError = await this.api.Projects.all({
|
|
82
|
+
membership: true,
|
|
83
|
+
perPage: 100
|
|
84
|
+
}).catch((error) => {
|
|
85
|
+
console.error(error);
|
|
86
|
+
return error;
|
|
87
|
+
});
|
|
88
|
+
if (repositoriesOrError instanceof Error) {
|
|
89
|
+
if (repositoriesOrError.cause.response.status === 401 && this.unauthorizedHandler) {
|
|
90
|
+
this.unauthorizedHandler();
|
|
91
|
+
}
|
|
92
|
+
throw new Error("Failed to fetch projects");
|
|
93
|
+
}
|
|
94
|
+
const repositories = repositoriesOrError.map((repository) => ({
|
|
95
|
+
id: repository.id,
|
|
96
|
+
org: repository.namespace.path,
|
|
97
|
+
name: repository.path,
|
|
98
|
+
url: repository.web_url
|
|
99
|
+
}));
|
|
100
|
+
return repositories.flat();
|
|
101
|
+
}
|
|
102
|
+
async fetchFile(path) {
|
|
103
|
+
const fileOrError = await this.api.RepositoryFiles.show(
|
|
104
|
+
this.repo,
|
|
105
|
+
path,
|
|
106
|
+
"HEAD"
|
|
107
|
+
).catch((error) => {
|
|
108
|
+
console.error(error);
|
|
109
|
+
return error;
|
|
110
|
+
});
|
|
111
|
+
if (fileOrError instanceof Error) {
|
|
112
|
+
if (fileOrError.cause.response.status === 401 && this.unauthorizedHandler) {
|
|
113
|
+
this.unauthorizedHandler();
|
|
114
|
+
}
|
|
115
|
+
throw new Error("Failed to fetch file");
|
|
116
|
+
}
|
|
117
|
+
if (!fileOrError.content) {
|
|
118
|
+
throw new Error("Failed to fetch file");
|
|
119
|
+
}
|
|
120
|
+
const content = base64ToUtf8(fileOrError.content);
|
|
121
|
+
if (!content) {
|
|
122
|
+
console.error("Failed to decode file");
|
|
123
|
+
}
|
|
124
|
+
return content || "";
|
|
125
|
+
}
|
|
126
|
+
async fetchDirectory(directoryPath) {
|
|
127
|
+
const filesOrError = await this.api.Repositories.allRepositoryTrees(
|
|
128
|
+
this.repo,
|
|
129
|
+
{
|
|
130
|
+
path: directoryPath
|
|
131
|
+
}
|
|
132
|
+
).catch((error) => {
|
|
133
|
+
console.error(error);
|
|
134
|
+
return error;
|
|
135
|
+
});
|
|
136
|
+
if (filesOrError instanceof Error) {
|
|
137
|
+
if (filesOrError.cause.response.status === 401 && this.unauthorizedHandler) {
|
|
138
|
+
this.unauthorizedHandler();
|
|
139
|
+
}
|
|
140
|
+
throw new Error("Failed to fetch directory");
|
|
141
|
+
}
|
|
142
|
+
if (!Array.isArray(filesOrError)) {
|
|
143
|
+
throw new Error("Path is not a directory");
|
|
144
|
+
}
|
|
145
|
+
return Promise.all(
|
|
146
|
+
filesOrError.filter((file) => file.path.endsWith(".json")).map((file) => {
|
|
147
|
+
return this.fetchFile(file.path);
|
|
148
|
+
})
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
async createFile(path, content, message) {
|
|
152
|
+
const branchesOrError = await this.api.Branches.all(this.repo).catch(
|
|
153
|
+
(error) => {
|
|
154
|
+
console.error(error);
|
|
155
|
+
return error;
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
if (branchesOrError instanceof Error) {
|
|
159
|
+
if (branchesOrError.cause.response.status === 401 && this.unauthorizedHandler) {
|
|
160
|
+
this.unauthorizedHandler();
|
|
161
|
+
}
|
|
162
|
+
throw new Error("Failed to fetch default branch");
|
|
163
|
+
}
|
|
164
|
+
const branch = branchesOrError.find((branch2) => branch2.default);
|
|
165
|
+
if (!branch) {
|
|
166
|
+
throw new Error("Failed to find default branch");
|
|
167
|
+
}
|
|
168
|
+
const existingFileOrError = await this.api.RepositoryFiles.show(
|
|
169
|
+
this.repo,
|
|
170
|
+
path,
|
|
171
|
+
branch.name
|
|
172
|
+
).catch((error) => {
|
|
173
|
+
console.error(error);
|
|
174
|
+
return error;
|
|
175
|
+
});
|
|
176
|
+
if (existingFileOrError instanceof Error) {
|
|
177
|
+
if (existingFileOrError.cause.response.status === 401 && this.unauthorizedHandler) {
|
|
178
|
+
this.unauthorizedHandler();
|
|
179
|
+
}
|
|
180
|
+
throw new Error("Failed to fetch existing file");
|
|
181
|
+
}
|
|
182
|
+
if (existingFileOrError) {
|
|
183
|
+
throw new Error("File already exists");
|
|
184
|
+
}
|
|
185
|
+
const createdFileOrError = await this.api.Commits.create(
|
|
186
|
+
this.repo,
|
|
187
|
+
branch.name,
|
|
188
|
+
message || `Create file: ${path}`,
|
|
189
|
+
[
|
|
190
|
+
{
|
|
191
|
+
action: "create",
|
|
192
|
+
filePath: path,
|
|
193
|
+
content: utf8ToBase64(content),
|
|
194
|
+
encoding: "base64"
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
).catch((error) => {
|
|
198
|
+
console.error(error);
|
|
199
|
+
return error;
|
|
200
|
+
});
|
|
201
|
+
if (createdFileOrError instanceof Error) {
|
|
202
|
+
if (createdFileOrError.cause.response.status === 401 && this.unauthorizedHandler) {
|
|
203
|
+
this.unauthorizedHandler();
|
|
204
|
+
}
|
|
205
|
+
throw new Error("Failed to create file");
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
async updateFile(path, content, message) {
|
|
210
|
+
const branchesOrError = await this.api.Branches.all(this.repo).catch(
|
|
211
|
+
(error) => {
|
|
212
|
+
console.error(error);
|
|
213
|
+
return error;
|
|
214
|
+
}
|
|
215
|
+
);
|
|
216
|
+
if (branchesOrError instanceof Error) {
|
|
217
|
+
if (branchesOrError.cause.response.status === 401 && this.unauthorizedHandler) {
|
|
218
|
+
this.unauthorizedHandler();
|
|
219
|
+
}
|
|
220
|
+
throw new Error("Failed to fetch default branch");
|
|
221
|
+
}
|
|
222
|
+
const branch = branchesOrError.find((branch2) => branch2.default);
|
|
223
|
+
if (!branch) {
|
|
224
|
+
throw new Error("Failed to find default branch");
|
|
225
|
+
}
|
|
226
|
+
const existingFileOrError = await this.api.RepositoryFiles.show(
|
|
227
|
+
this.repo,
|
|
228
|
+
path,
|
|
229
|
+
branch.name
|
|
230
|
+
).catch((error) => {
|
|
231
|
+
console.error(error);
|
|
232
|
+
return error;
|
|
233
|
+
});
|
|
234
|
+
if (existingFileOrError instanceof Error) {
|
|
235
|
+
if (existingFileOrError.cause.response.status === 401 && this.unauthorizedHandler) {
|
|
236
|
+
this.unauthorizedHandler();
|
|
237
|
+
}
|
|
238
|
+
throw new Error("Failed to fetch existing file");
|
|
239
|
+
}
|
|
240
|
+
if (!existingFileOrError) {
|
|
241
|
+
throw new Error("File does not exist");
|
|
242
|
+
}
|
|
243
|
+
const createdFileOrError = await this.api.Commits.create(
|
|
244
|
+
this.repo,
|
|
245
|
+
branch.name,
|
|
246
|
+
message || `Create file: ${path}`,
|
|
247
|
+
[
|
|
248
|
+
{
|
|
249
|
+
action: "update",
|
|
250
|
+
filePath: path,
|
|
251
|
+
content: utf8ToBase64(content),
|
|
252
|
+
encoding: "base64"
|
|
253
|
+
}
|
|
254
|
+
]
|
|
255
|
+
).catch((error) => {
|
|
256
|
+
console.error(error);
|
|
257
|
+
return error;
|
|
258
|
+
});
|
|
259
|
+
if (createdFileOrError instanceof Error) {
|
|
260
|
+
if (createdFileOrError.cause.response.status === 401 && this.unauthorizedHandler) {
|
|
261
|
+
this.unauthorizedHandler();
|
|
262
|
+
}
|
|
263
|
+
throw new Error("Failed to create file");
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
async deleteFile(path, message) {
|
|
268
|
+
const responseOrError = await this.api.RepositoryFiles.remove(
|
|
269
|
+
this.repo,
|
|
270
|
+
path,
|
|
271
|
+
"HEAD",
|
|
272
|
+
message || `Delete file: ${path}`
|
|
273
|
+
).catch((error) => {
|
|
274
|
+
console.error(error);
|
|
275
|
+
return error;
|
|
276
|
+
});
|
|
277
|
+
if (responseOrError instanceof Error) {
|
|
278
|
+
if (responseOrError.cause.response.status === 401 && this.unauthorizedHandler) {
|
|
279
|
+
this.unauthorizedHandler();
|
|
280
|
+
}
|
|
281
|
+
throw new Error("Failed to delete file");
|
|
282
|
+
}
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// TODO: Why did I add this method?
|
|
286
|
+
async createCommit({}) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
// -----------------------------------------------------------------------------
|
|
290
|
+
// TODO: Pull Requests and File History are not yet implemented or used in the demo app yet
|
|
291
|
+
// -----------------------------------------------------------------------------
|
|
292
|
+
async fetchFileHistory(path) {
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
async fetchPullRequests() {
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
298
|
+
async createPullRequest() {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
async fetchPullRequest() {
|
|
302
|
+
return {
|
|
303
|
+
id: "",
|
|
304
|
+
title: "",
|
|
305
|
+
description: "",
|
|
306
|
+
commitSha: "",
|
|
307
|
+
createdAt: ""
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
async updatePullRequest() {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
async deletePullRequest() {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
318
|
+
0 && (module.exports = {
|
|
319
|
+
GitlabAdapter
|
|
320
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Adapter, IAdapter, CommitRequest } from '@oh-my-ghaad/core';
|
|
2
|
+
|
|
3
|
+
declare class GitlabAdapter extends Adapter implements IAdapter {
|
|
4
|
+
readonly name = "GitLab";
|
|
5
|
+
readonly icon = "https://about.gitlab.com/images/ico/favicon-192x192.png";
|
|
6
|
+
readonly primaryColor = "#FC6D26";
|
|
7
|
+
readonly secondaryColor = "#000000";
|
|
8
|
+
readonly oauthUrl = "https://gitlab.com/oauth/authorize";
|
|
9
|
+
readonly baseUrl = "https://gitlab.com";
|
|
10
|
+
private api;
|
|
11
|
+
constructor(props: any);
|
|
12
|
+
setToken(token: string | null): void;
|
|
13
|
+
fetchRepositories(): Promise<{
|
|
14
|
+
id: number;
|
|
15
|
+
org: string;
|
|
16
|
+
name: string;
|
|
17
|
+
url: string;
|
|
18
|
+
}[]>;
|
|
19
|
+
fetchFile(path: string): Promise<string>;
|
|
20
|
+
fetchDirectory(directoryPath: string): Promise<string[]>;
|
|
21
|
+
createFile(path: string, content: string, message?: string): Promise<void>;
|
|
22
|
+
updateFile(path: string, content: string, message?: string): Promise<void>;
|
|
23
|
+
deleteFile(path: string, message?: string): Promise<void>;
|
|
24
|
+
createCommit({}: CommitRequest): Promise<void>;
|
|
25
|
+
fetchFileHistory(path: string): Promise<any[]>;
|
|
26
|
+
fetchPullRequests(): Promise<any[]>;
|
|
27
|
+
createPullRequest(): Promise<void>;
|
|
28
|
+
fetchPullRequest(): Promise<{
|
|
29
|
+
id: string;
|
|
30
|
+
title: string;
|
|
31
|
+
description: string;
|
|
32
|
+
commitSha: string;
|
|
33
|
+
createdAt: string;
|
|
34
|
+
}>;
|
|
35
|
+
updatePullRequest(): Promise<void>;
|
|
36
|
+
deletePullRequest(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { GitlabAdapter };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Adapter, IAdapter, CommitRequest } from '@oh-my-ghaad/core';
|
|
2
|
+
|
|
3
|
+
declare class GitlabAdapter extends Adapter implements IAdapter {
|
|
4
|
+
readonly name = "GitLab";
|
|
5
|
+
readonly icon = "https://about.gitlab.com/images/ico/favicon-192x192.png";
|
|
6
|
+
readonly primaryColor = "#FC6D26";
|
|
7
|
+
readonly secondaryColor = "#000000";
|
|
8
|
+
readonly oauthUrl = "https://gitlab.com/oauth/authorize";
|
|
9
|
+
readonly baseUrl = "https://gitlab.com";
|
|
10
|
+
private api;
|
|
11
|
+
constructor(props: any);
|
|
12
|
+
setToken(token: string | null): void;
|
|
13
|
+
fetchRepositories(): Promise<{
|
|
14
|
+
id: number;
|
|
15
|
+
org: string;
|
|
16
|
+
name: string;
|
|
17
|
+
url: string;
|
|
18
|
+
}[]>;
|
|
19
|
+
fetchFile(path: string): Promise<string>;
|
|
20
|
+
fetchDirectory(directoryPath: string): Promise<string[]>;
|
|
21
|
+
createFile(path: string, content: string, message?: string): Promise<void>;
|
|
22
|
+
updateFile(path: string, content: string, message?: string): Promise<void>;
|
|
23
|
+
deleteFile(path: string, message?: string): Promise<void>;
|
|
24
|
+
createCommit({}: CommitRequest): Promise<void>;
|
|
25
|
+
fetchFileHistory(path: string): Promise<any[]>;
|
|
26
|
+
fetchPullRequests(): Promise<any[]>;
|
|
27
|
+
createPullRequest(): Promise<void>;
|
|
28
|
+
fetchPullRequest(): Promise<{
|
|
29
|
+
id: string;
|
|
30
|
+
title: string;
|
|
31
|
+
description: string;
|
|
32
|
+
commitSha: string;
|
|
33
|
+
createdAt: string;
|
|
34
|
+
}>;
|
|
35
|
+
updatePullRequest(): Promise<void>;
|
|
36
|
+
deletePullRequest(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { GitlabAdapter };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// src/adapter.ts
|
|
2
|
+
import { Adapter } from "@oh-my-ghaad/core";
|
|
3
|
+
import { Gitlab } from "@gitbeaker/rest";
|
|
4
|
+
|
|
5
|
+
// src/utils.ts
|
|
6
|
+
function base64ToUtf8(base64String) {
|
|
7
|
+
try {
|
|
8
|
+
const byteCharacters = atob(base64String);
|
|
9
|
+
const byteNumbers = new Array(byteCharacters.length);
|
|
10
|
+
for (let i = 0; i < byteCharacters.length; i++) {
|
|
11
|
+
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
12
|
+
}
|
|
13
|
+
const byteArray = new Uint8Array(byteNumbers);
|
|
14
|
+
const decoder = new TextDecoder("utf-8");
|
|
15
|
+
return decoder.decode(byteArray);
|
|
16
|
+
} catch (e) {
|
|
17
|
+
console.error("Error decoding base64 with TextDecoder:", e);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function utf8ToBase64(utf8String) {
|
|
22
|
+
try {
|
|
23
|
+
const encoder = new TextEncoder();
|
|
24
|
+
const byteArray = encoder.encode(utf8String);
|
|
25
|
+
return btoa(String.fromCharCode(...byteArray));
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.error("Error encoding string as base64:", e);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/adapter.ts
|
|
33
|
+
var GitlabAdapter = class extends Adapter {
|
|
34
|
+
name = "GitLab";
|
|
35
|
+
icon = "https://about.gitlab.com/images/ico/favicon-192x192.png";
|
|
36
|
+
primaryColor = "#FC6D26";
|
|
37
|
+
secondaryColor = "#000000";
|
|
38
|
+
oauthUrl = "https://gitlab.com/oauth/authorize";
|
|
39
|
+
baseUrl = "https://gitlab.com";
|
|
40
|
+
api;
|
|
41
|
+
constructor(props) {
|
|
42
|
+
super(props);
|
|
43
|
+
this.api = new Gitlab({
|
|
44
|
+
host: this.baseUrl,
|
|
45
|
+
oauthToken: this.token
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
setToken(token) {
|
|
49
|
+
super.setToken(token);
|
|
50
|
+
this.api = new Gitlab({
|
|
51
|
+
host: this.baseUrl,
|
|
52
|
+
oauthToken: this.token
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async fetchRepositories() {
|
|
56
|
+
const repositoriesOrError = await this.api.Projects.all({
|
|
57
|
+
membership: true,
|
|
58
|
+
perPage: 100
|
|
59
|
+
}).catch((error) => {
|
|
60
|
+
console.error(error);
|
|
61
|
+
return error;
|
|
62
|
+
});
|
|
63
|
+
if (repositoriesOrError instanceof Error) {
|
|
64
|
+
if (repositoriesOrError.cause.response.status === 401 && this.unauthorizedHandler) {
|
|
65
|
+
this.unauthorizedHandler();
|
|
66
|
+
}
|
|
67
|
+
throw new Error("Failed to fetch projects");
|
|
68
|
+
}
|
|
69
|
+
const repositories = repositoriesOrError.map((repository) => ({
|
|
70
|
+
id: repository.id,
|
|
71
|
+
org: repository.namespace.path,
|
|
72
|
+
name: repository.path,
|
|
73
|
+
url: repository.web_url
|
|
74
|
+
}));
|
|
75
|
+
return repositories.flat();
|
|
76
|
+
}
|
|
77
|
+
async fetchFile(path) {
|
|
78
|
+
const fileOrError = await this.api.RepositoryFiles.show(
|
|
79
|
+
this.repo,
|
|
80
|
+
path,
|
|
81
|
+
"HEAD"
|
|
82
|
+
).catch((error) => {
|
|
83
|
+
console.error(error);
|
|
84
|
+
return error;
|
|
85
|
+
});
|
|
86
|
+
if (fileOrError instanceof Error) {
|
|
87
|
+
if (fileOrError.cause.response.status === 401 && this.unauthorizedHandler) {
|
|
88
|
+
this.unauthorizedHandler();
|
|
89
|
+
}
|
|
90
|
+
throw new Error("Failed to fetch file");
|
|
91
|
+
}
|
|
92
|
+
if (!fileOrError.content) {
|
|
93
|
+
throw new Error("Failed to fetch file");
|
|
94
|
+
}
|
|
95
|
+
const content = base64ToUtf8(fileOrError.content);
|
|
96
|
+
if (!content) {
|
|
97
|
+
console.error("Failed to decode file");
|
|
98
|
+
}
|
|
99
|
+
return content || "";
|
|
100
|
+
}
|
|
101
|
+
async fetchDirectory(directoryPath) {
|
|
102
|
+
const filesOrError = await this.api.Repositories.allRepositoryTrees(
|
|
103
|
+
this.repo,
|
|
104
|
+
{
|
|
105
|
+
path: directoryPath
|
|
106
|
+
}
|
|
107
|
+
).catch((error) => {
|
|
108
|
+
console.error(error);
|
|
109
|
+
return error;
|
|
110
|
+
});
|
|
111
|
+
if (filesOrError instanceof Error) {
|
|
112
|
+
if (filesOrError.cause.response.status === 401 && this.unauthorizedHandler) {
|
|
113
|
+
this.unauthorizedHandler();
|
|
114
|
+
}
|
|
115
|
+
throw new Error("Failed to fetch directory");
|
|
116
|
+
}
|
|
117
|
+
if (!Array.isArray(filesOrError)) {
|
|
118
|
+
throw new Error("Path is not a directory");
|
|
119
|
+
}
|
|
120
|
+
return Promise.all(
|
|
121
|
+
filesOrError.filter((file) => file.path.endsWith(".json")).map((file) => {
|
|
122
|
+
return this.fetchFile(file.path);
|
|
123
|
+
})
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
async createFile(path, content, message) {
|
|
127
|
+
const branchesOrError = await this.api.Branches.all(this.repo).catch(
|
|
128
|
+
(error) => {
|
|
129
|
+
console.error(error);
|
|
130
|
+
return error;
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
if (branchesOrError instanceof Error) {
|
|
134
|
+
if (branchesOrError.cause.response.status === 401 && this.unauthorizedHandler) {
|
|
135
|
+
this.unauthorizedHandler();
|
|
136
|
+
}
|
|
137
|
+
throw new Error("Failed to fetch default branch");
|
|
138
|
+
}
|
|
139
|
+
const branch = branchesOrError.find((branch2) => branch2.default);
|
|
140
|
+
if (!branch) {
|
|
141
|
+
throw new Error("Failed to find default branch");
|
|
142
|
+
}
|
|
143
|
+
const existingFileOrError = await this.api.RepositoryFiles.show(
|
|
144
|
+
this.repo,
|
|
145
|
+
path,
|
|
146
|
+
branch.name
|
|
147
|
+
).catch((error) => {
|
|
148
|
+
console.error(error);
|
|
149
|
+
return error;
|
|
150
|
+
});
|
|
151
|
+
if (existingFileOrError instanceof Error) {
|
|
152
|
+
if (existingFileOrError.cause.response.status === 401 && this.unauthorizedHandler) {
|
|
153
|
+
this.unauthorizedHandler();
|
|
154
|
+
}
|
|
155
|
+
throw new Error("Failed to fetch existing file");
|
|
156
|
+
}
|
|
157
|
+
if (existingFileOrError) {
|
|
158
|
+
throw new Error("File already exists");
|
|
159
|
+
}
|
|
160
|
+
const createdFileOrError = await this.api.Commits.create(
|
|
161
|
+
this.repo,
|
|
162
|
+
branch.name,
|
|
163
|
+
message || `Create file: ${path}`,
|
|
164
|
+
[
|
|
165
|
+
{
|
|
166
|
+
action: "create",
|
|
167
|
+
filePath: path,
|
|
168
|
+
content: utf8ToBase64(content),
|
|
169
|
+
encoding: "base64"
|
|
170
|
+
}
|
|
171
|
+
]
|
|
172
|
+
).catch((error) => {
|
|
173
|
+
console.error(error);
|
|
174
|
+
return error;
|
|
175
|
+
});
|
|
176
|
+
if (createdFileOrError instanceof Error) {
|
|
177
|
+
if (createdFileOrError.cause.response.status === 401 && this.unauthorizedHandler) {
|
|
178
|
+
this.unauthorizedHandler();
|
|
179
|
+
}
|
|
180
|
+
throw new Error("Failed to create file");
|
|
181
|
+
}
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
async updateFile(path, content, message) {
|
|
185
|
+
const branchesOrError = await this.api.Branches.all(this.repo).catch(
|
|
186
|
+
(error) => {
|
|
187
|
+
console.error(error);
|
|
188
|
+
return error;
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
if (branchesOrError instanceof Error) {
|
|
192
|
+
if (branchesOrError.cause.response.status === 401 && this.unauthorizedHandler) {
|
|
193
|
+
this.unauthorizedHandler();
|
|
194
|
+
}
|
|
195
|
+
throw new Error("Failed to fetch default branch");
|
|
196
|
+
}
|
|
197
|
+
const branch = branchesOrError.find((branch2) => branch2.default);
|
|
198
|
+
if (!branch) {
|
|
199
|
+
throw new Error("Failed to find default branch");
|
|
200
|
+
}
|
|
201
|
+
const existingFileOrError = await this.api.RepositoryFiles.show(
|
|
202
|
+
this.repo,
|
|
203
|
+
path,
|
|
204
|
+
branch.name
|
|
205
|
+
).catch((error) => {
|
|
206
|
+
console.error(error);
|
|
207
|
+
return error;
|
|
208
|
+
});
|
|
209
|
+
if (existingFileOrError instanceof Error) {
|
|
210
|
+
if (existingFileOrError.cause.response.status === 401 && this.unauthorizedHandler) {
|
|
211
|
+
this.unauthorizedHandler();
|
|
212
|
+
}
|
|
213
|
+
throw new Error("Failed to fetch existing file");
|
|
214
|
+
}
|
|
215
|
+
if (!existingFileOrError) {
|
|
216
|
+
throw new Error("File does not exist");
|
|
217
|
+
}
|
|
218
|
+
const createdFileOrError = await this.api.Commits.create(
|
|
219
|
+
this.repo,
|
|
220
|
+
branch.name,
|
|
221
|
+
message || `Create file: ${path}`,
|
|
222
|
+
[
|
|
223
|
+
{
|
|
224
|
+
action: "update",
|
|
225
|
+
filePath: path,
|
|
226
|
+
content: utf8ToBase64(content),
|
|
227
|
+
encoding: "base64"
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
).catch((error) => {
|
|
231
|
+
console.error(error);
|
|
232
|
+
return error;
|
|
233
|
+
});
|
|
234
|
+
if (createdFileOrError instanceof Error) {
|
|
235
|
+
if (createdFileOrError.cause.response.status === 401 && this.unauthorizedHandler) {
|
|
236
|
+
this.unauthorizedHandler();
|
|
237
|
+
}
|
|
238
|
+
throw new Error("Failed to create file");
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
async deleteFile(path, message) {
|
|
243
|
+
const responseOrError = await this.api.RepositoryFiles.remove(
|
|
244
|
+
this.repo,
|
|
245
|
+
path,
|
|
246
|
+
"HEAD",
|
|
247
|
+
message || `Delete file: ${path}`
|
|
248
|
+
).catch((error) => {
|
|
249
|
+
console.error(error);
|
|
250
|
+
return error;
|
|
251
|
+
});
|
|
252
|
+
if (responseOrError instanceof Error) {
|
|
253
|
+
if (responseOrError.cause.response.status === 401 && this.unauthorizedHandler) {
|
|
254
|
+
this.unauthorizedHandler();
|
|
255
|
+
}
|
|
256
|
+
throw new Error("Failed to delete file");
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
// TODO: Why did I add this method?
|
|
261
|
+
async createCommit({}) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
// -----------------------------------------------------------------------------
|
|
265
|
+
// TODO: Pull Requests and File History are not yet implemented or used in the demo app yet
|
|
266
|
+
// -----------------------------------------------------------------------------
|
|
267
|
+
async fetchFileHistory(path) {
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
async fetchPullRequests() {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
async createPullRequest() {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
async fetchPullRequest() {
|
|
277
|
+
return {
|
|
278
|
+
id: "",
|
|
279
|
+
title: "",
|
|
280
|
+
description: "",
|
|
281
|
+
commitSha: "",
|
|
282
|
+
createdAt: ""
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
async updatePullRequest() {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
async deletePullRequest() {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
export {
|
|
293
|
+
GitlabAdapter
|
|
294
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oh-my-ghaad/gitlab",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"keywords": [],
|
|
9
|
+
"author": "",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@gitbeaker/rest": "^43.5.0",
|
|
13
|
+
"@oh-my-ghaad/core": "0.0.1"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"tsup": "^8.5.0",
|
|
17
|
+
"typescript": "^5.8.3"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"dev": "tsup src/index.ts --watch --dts --dts-resolve --format esm,cjs",
|
|
21
|
+
"build": "tsup src/index.ts --dts --dts-resolve --format esm,cjs",
|
|
22
|
+
"prepublish": "pnpm run build",
|
|
23
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { Adapter } from "@oh-my-ghaad/core";
|
|
2
|
+
import type { CommitRequest, IAdapter } from "@oh-my-ghaad/core";
|
|
3
|
+
import { GitbeakerRequestError, Gitlab } from "@gitbeaker/rest";
|
|
4
|
+
import { base64ToUtf8, utf8ToBase64 } from "./utils";
|
|
5
|
+
// import { base64ToUtf8, utf8ToBase64 } from "./utils";
|
|
6
|
+
|
|
7
|
+
// type EndpointPath = keyof Endpoints;
|
|
8
|
+
|
|
9
|
+
export class GitlabAdapter extends Adapter implements IAdapter {
|
|
10
|
+
readonly name = "GitLab";
|
|
11
|
+
readonly icon = "https://about.gitlab.com/images/ico/favicon-192x192.png";
|
|
12
|
+
readonly primaryColor = "#FC6D26";
|
|
13
|
+
readonly secondaryColor = "#000000";
|
|
14
|
+
readonly oauthUrl = "https://gitlab.com/oauth/authorize";
|
|
15
|
+
readonly baseUrl = "https://gitlab.com";
|
|
16
|
+
|
|
17
|
+
private api: InstanceType<typeof Gitlab>;
|
|
18
|
+
|
|
19
|
+
constructor(props) {
|
|
20
|
+
super(props);
|
|
21
|
+
|
|
22
|
+
this.api = new Gitlab({
|
|
23
|
+
host: this.baseUrl,
|
|
24
|
+
oauthToken: this.token,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setToken(token: string | null) {
|
|
29
|
+
super.setToken(token);
|
|
30
|
+
|
|
31
|
+
this.api = new Gitlab({
|
|
32
|
+
host: this.baseUrl,
|
|
33
|
+
oauthToken: this.token,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async fetchRepositories() {
|
|
38
|
+
const repositoriesOrError = await this.api.Projects.all({
|
|
39
|
+
membership: true,
|
|
40
|
+
perPage: 100,
|
|
41
|
+
}).catch((error: GitbeakerRequestError) => {
|
|
42
|
+
console.error(error);
|
|
43
|
+
return error;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (repositoriesOrError instanceof Error) {
|
|
47
|
+
if (
|
|
48
|
+
repositoriesOrError.cause.response.status === 401 &&
|
|
49
|
+
this.unauthorizedHandler
|
|
50
|
+
) {
|
|
51
|
+
this.unauthorizedHandler();
|
|
52
|
+
}
|
|
53
|
+
throw new Error("Failed to fetch projects");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const repositories = repositoriesOrError.map((repository) => ({
|
|
57
|
+
id: repository.id,
|
|
58
|
+
org: repository.namespace.path,
|
|
59
|
+
name: repository.path,
|
|
60
|
+
url: repository.web_url as string,
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
return repositories.flat();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async fetchFile(path: string) {
|
|
67
|
+
const fileOrError = await this.api.RepositoryFiles.show(
|
|
68
|
+
this.repo,
|
|
69
|
+
path,
|
|
70
|
+
"HEAD"
|
|
71
|
+
).catch((error: GitbeakerRequestError) => {
|
|
72
|
+
console.error(error);
|
|
73
|
+
return error;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (fileOrError instanceof Error) {
|
|
77
|
+
if (
|
|
78
|
+
fileOrError.cause.response.status === 401 &&
|
|
79
|
+
this.unauthorizedHandler
|
|
80
|
+
) {
|
|
81
|
+
this.unauthorizedHandler();
|
|
82
|
+
}
|
|
83
|
+
throw new Error("Failed to fetch file");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!fileOrError.content) {
|
|
87
|
+
throw new Error("Failed to fetch file");
|
|
88
|
+
}
|
|
89
|
+
const content = base64ToUtf8(fileOrError.content);
|
|
90
|
+
if (!content) {
|
|
91
|
+
console.error("Failed to decode file");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return content || "";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async fetchDirectory(directoryPath: string) {
|
|
98
|
+
const filesOrError = await this.api.Repositories.allRepositoryTrees(
|
|
99
|
+
this.repo,
|
|
100
|
+
{
|
|
101
|
+
path: directoryPath,
|
|
102
|
+
}
|
|
103
|
+
).catch((error: GitbeakerRequestError) => {
|
|
104
|
+
console.error(error);
|
|
105
|
+
return error;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (filesOrError instanceof Error) {
|
|
109
|
+
if (
|
|
110
|
+
filesOrError.cause.response.status === 401 &&
|
|
111
|
+
this.unauthorizedHandler
|
|
112
|
+
) {
|
|
113
|
+
this.unauthorizedHandler();
|
|
114
|
+
}
|
|
115
|
+
throw new Error("Failed to fetch directory");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!Array.isArray(filesOrError)) {
|
|
119
|
+
throw new Error("Path is not a directory");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return Promise.all(
|
|
123
|
+
filesOrError
|
|
124
|
+
.filter((file) => file.path.endsWith(".json"))
|
|
125
|
+
.map((file) => {
|
|
126
|
+
return this.fetchFile(file.path);
|
|
127
|
+
})
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async createFile(path: string, content: string, message?: string) {
|
|
132
|
+
const branchesOrError = await this.api.Branches.all(this.repo).catch(
|
|
133
|
+
(error: GitbeakerRequestError) => {
|
|
134
|
+
console.error(error);
|
|
135
|
+
return error;
|
|
136
|
+
}
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
if (branchesOrError instanceof Error) {
|
|
140
|
+
if (
|
|
141
|
+
branchesOrError.cause.response.status === 401 &&
|
|
142
|
+
this.unauthorizedHandler
|
|
143
|
+
) {
|
|
144
|
+
this.unauthorizedHandler();
|
|
145
|
+
}
|
|
146
|
+
throw new Error("Failed to fetch default branch");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const branch = branchesOrError.find((branch) => branch.default);
|
|
150
|
+
|
|
151
|
+
if (!branch) {
|
|
152
|
+
throw new Error("Failed to find default branch");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const existingFileOrError = await this.api.RepositoryFiles.show(
|
|
156
|
+
this.repo,
|
|
157
|
+
path,
|
|
158
|
+
branch.name
|
|
159
|
+
).catch((error: GitbeakerRequestError) => {
|
|
160
|
+
console.error(error);
|
|
161
|
+
return error;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (existingFileOrError instanceof Error) {
|
|
165
|
+
if (
|
|
166
|
+
existingFileOrError.cause.response.status === 401 &&
|
|
167
|
+
this.unauthorizedHandler
|
|
168
|
+
) {
|
|
169
|
+
this.unauthorizedHandler();
|
|
170
|
+
}
|
|
171
|
+
throw new Error("Failed to fetch existing file");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (existingFileOrError) {
|
|
175
|
+
throw new Error("File already exists");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const createdFileOrError = await this.api.Commits.create(
|
|
179
|
+
this.repo,
|
|
180
|
+
branch.name,
|
|
181
|
+
message || `Create file: ${path}`,
|
|
182
|
+
[
|
|
183
|
+
{
|
|
184
|
+
action: "create",
|
|
185
|
+
filePath: path,
|
|
186
|
+
content: utf8ToBase64(content),
|
|
187
|
+
encoding: "base64",
|
|
188
|
+
},
|
|
189
|
+
]
|
|
190
|
+
).catch((error: GitbeakerRequestError) => {
|
|
191
|
+
console.error(error);
|
|
192
|
+
return error;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (createdFileOrError instanceof Error) {
|
|
196
|
+
if (
|
|
197
|
+
createdFileOrError.cause.response.status === 401 &&
|
|
198
|
+
this.unauthorizedHandler
|
|
199
|
+
) {
|
|
200
|
+
this.unauthorizedHandler();
|
|
201
|
+
}
|
|
202
|
+
throw new Error("Failed to create file");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async updateFile(path: string, content: string, message?: string) {
|
|
209
|
+
const branchesOrError = await this.api.Branches.all(this.repo).catch(
|
|
210
|
+
(error: GitbeakerRequestError) => {
|
|
211
|
+
console.error(error);
|
|
212
|
+
return error;
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
if (branchesOrError instanceof Error) {
|
|
217
|
+
if (
|
|
218
|
+
branchesOrError.cause.response.status === 401 &&
|
|
219
|
+
this.unauthorizedHandler
|
|
220
|
+
) {
|
|
221
|
+
this.unauthorizedHandler();
|
|
222
|
+
}
|
|
223
|
+
throw new Error("Failed to fetch default branch");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const branch = branchesOrError.find((branch) => branch.default);
|
|
227
|
+
|
|
228
|
+
if (!branch) {
|
|
229
|
+
throw new Error("Failed to find default branch");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const existingFileOrError = await this.api.RepositoryFiles.show(
|
|
233
|
+
this.repo,
|
|
234
|
+
path,
|
|
235
|
+
branch.name
|
|
236
|
+
).catch((error: GitbeakerRequestError) => {
|
|
237
|
+
console.error(error);
|
|
238
|
+
return error;
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (existingFileOrError instanceof Error) {
|
|
242
|
+
if (
|
|
243
|
+
existingFileOrError.cause.response.status === 401 &&
|
|
244
|
+
this.unauthorizedHandler
|
|
245
|
+
) {
|
|
246
|
+
this.unauthorizedHandler();
|
|
247
|
+
}
|
|
248
|
+
throw new Error("Failed to fetch existing file");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (!existingFileOrError) {
|
|
252
|
+
throw new Error("File does not exist");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const createdFileOrError = await this.api.Commits.create(
|
|
256
|
+
this.repo,
|
|
257
|
+
branch.name,
|
|
258
|
+
message || `Create file: ${path}`,
|
|
259
|
+
[
|
|
260
|
+
{
|
|
261
|
+
action: "update",
|
|
262
|
+
filePath: path,
|
|
263
|
+
content: utf8ToBase64(content),
|
|
264
|
+
encoding: "base64",
|
|
265
|
+
},
|
|
266
|
+
]
|
|
267
|
+
).catch((error: GitbeakerRequestError) => {
|
|
268
|
+
console.error(error);
|
|
269
|
+
return error;
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
if (createdFileOrError instanceof Error) {
|
|
273
|
+
if (
|
|
274
|
+
createdFileOrError.cause.response.status === 401 &&
|
|
275
|
+
this.unauthorizedHandler
|
|
276
|
+
) {
|
|
277
|
+
this.unauthorizedHandler();
|
|
278
|
+
}
|
|
279
|
+
throw new Error("Failed to create file");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async deleteFile(path: string, message?: string) {
|
|
286
|
+
const responseOrError = await this.api.RepositoryFiles.remove(
|
|
287
|
+
this.repo,
|
|
288
|
+
path,
|
|
289
|
+
"HEAD",
|
|
290
|
+
message || `Delete file: ${path}`
|
|
291
|
+
).catch((error: GitbeakerRequestError) => {
|
|
292
|
+
console.error(error);
|
|
293
|
+
return error;
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
if (responseOrError instanceof Error) {
|
|
297
|
+
if (
|
|
298
|
+
responseOrError.cause.response.status === 401 &&
|
|
299
|
+
this.unauthorizedHandler
|
|
300
|
+
) {
|
|
301
|
+
this.unauthorizedHandler();
|
|
302
|
+
}
|
|
303
|
+
throw new Error("Failed to delete file");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// TODO: Why did I add this method?
|
|
310
|
+
async createCommit({}: CommitRequest) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// -----------------------------------------------------------------------------
|
|
315
|
+
// TODO: Pull Requests and File History are not yet implemented or used in the demo app yet
|
|
316
|
+
// -----------------------------------------------------------------------------
|
|
317
|
+
async fetchFileHistory(path: string) {
|
|
318
|
+
return [];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async fetchPullRequests() {
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async createPullRequest() {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async fetchPullRequest() {
|
|
330
|
+
return {
|
|
331
|
+
id: "",
|
|
332
|
+
title: "",
|
|
333
|
+
description: "",
|
|
334
|
+
commitSha: "",
|
|
335
|
+
createdAt: "",
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async updatePullRequest() {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async deletePullRequest() {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./adapter";
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function base64ToUtf8(base64String: string): string | null {
|
|
2
|
+
try {
|
|
3
|
+
// Decode base64 to a byte array (Uint8Array)
|
|
4
|
+
const byteCharacters = atob(base64String);
|
|
5
|
+
const byteNumbers = new Array(byteCharacters.length);
|
|
6
|
+
for (let i = 0; i < byteCharacters.length; i++) {
|
|
7
|
+
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
8
|
+
}
|
|
9
|
+
const byteArray = new Uint8Array(byteNumbers);
|
|
10
|
+
|
|
11
|
+
// Decode the byte array as UTF-8
|
|
12
|
+
const decoder = new TextDecoder("utf-8");
|
|
13
|
+
return decoder.decode(byteArray);
|
|
14
|
+
} catch (e) {
|
|
15
|
+
console.error("Error decoding base64 with TextDecoder:", e);
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function utf8ToBase64(utf8String: string): string | null {
|
|
21
|
+
try {
|
|
22
|
+
// Encode the string as UTF-8
|
|
23
|
+
const encoder = new TextEncoder();
|
|
24
|
+
const byteArray = encoder.encode(utf8String);
|
|
25
|
+
|
|
26
|
+
// Convert the byte array to base64 and return it
|
|
27
|
+
return btoa(String.fromCharCode(...byteArray));
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.error("Error encoding string as base64:", e);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|