Последняя активность 1 month ago

在GitLab仓库设置页面一键填充镜像仓库信息

Jetsung Chan ревизий этого фрагмента 1 month ago. К ревизии

2 files changed, 22 insertions, 22 deletions

gitea.user.js

@@ -1,7 +1,7 @@
1 1 // ==UserScript==
2 2 // @name Gitea 镜像仓库一键填充
3 3 // @namespace ScriptCat Scripts
4 - // @version 0.2.3
4 + // @version 0.2.4
5 5 // @description 在Gitea仓库设置页面一键填充镜像仓库信息,并在镜像列表中添加目标仓库跳转链接
6 6 // @author Jetsung Chan <[email protected]>
7 7 // @match https://git.idev.top/*/*/settings
@@ -19,7 +19,7 @@
19 19 // @connect codeup.aliyun.com
20 20 // @connect api.github.com
21 21 // @connect codeberg.org
22 - // @connect gitcode.com
22 + // @connect atomgit.com
23 23 // @connect framagit.org
24 24 // @downloadURL https://gist.asfd.cn/jetsung/gitsync/raw/HEAD/gitea.user.js
25 25 // @updateURL https://gist.asfd.cn/jetsung/gitsync/raw/HEAD/gitea.user.js
@@ -37,12 +37,12 @@
37 37 usernameKey: 'github_username',
38 38 domain: 'github.com'
39 39 },
40 - gitcode: {
41 - name: 'GitCode',
42 - url: 'https://gitcode.com/',
43 - tokenKey: 'gitcode_token',
44 - usernameKey: 'gitcode_username',
45 - domain: 'gitcode.com'
40 + atomgit: {
41 + name: 'AtomGit',
42 + url: 'https://atomgit.com/',
43 + tokenKey: 'atomgit_token',
44 + usernameKey: 'atomgit_username',
45 + domain: 'atomgit.com'
46 46 },
47 47 codeup: {
48 48 name: 'CodeUp',
@@ -399,12 +399,12 @@
399 399 namespace_id: namespaceId,
400 400 })
401 401 });
402 - } else if (key === 'gitcode') {
402 + } else if (key === 'atomgit') {
403 403 let reqPath = 'api/v5/user/repos'
404 404 if (username !== ownerPath) {
405 405 reqPath = `api/v5/orgs/${encodeURIComponent(ownerPath)}/repos`
406 406 }
407 - resp = await fetch(`https://api.gitcode.com/` + reqPath, {
407 + resp = await fetch(`https://api.atomgit.com/` + reqPath, {
408 408 method: 'POST',
409 409 headers: {
410 410 'Authorization': `Bearer ${token}`,
@@ -674,7 +674,7 @@
674 674 break;
675 675
676 676 case 'tiny':
677 - if (platformKey === 'github' || platformKey === 'gitcode') {
677 + if (platformKey === 'github' || platformKey === 'atomgit') {
678 678 group = 'tinyzen'
679 679 }
680 680 break;

gitlab.user.js

@@ -1,7 +1,7 @@
1 1 // ==UserScript==
2 2 // @name GitLab 镜像仓库一键填充
3 3 // @namespace ScriptCat Scripts
4 - // @version 0.3.3
4 + // @version 0.3.4
5 5 // @description 在GitLab仓库设置页面一键填充镜像仓库信息,并在镜像列表中添加目标仓库跳转链接
6 6 // @author Jetsung Chan <[email protected]>
7 7 // @match https://framagit.org/*/-/settings/repository*
@@ -19,7 +19,7 @@
19 19 // @connect codeup.aliyun.com
20 20 // @connect api.github.com
21 21 // @connect codeberg.org
22 - // @connect gitcode.com
22 + // @connect atomgit.com
23 23 // @downloadURL https://gist.asfd.cn/jetsung/gitsync/raw/HEAD/gitlab.user.js
24 24 // @updateURL https://gist.asfd.cn/jetsung/gitsync/raw/HEAD/gitlab.user.js
25 25 // ==/UserScript==
@@ -36,12 +36,12 @@
36 36 usernameKey: 'github_username',
37 37 domain: 'github.com'
38 38 },
39 - gitcode: {
40 - name: 'GitCode',
41 - url: 'https://gitcode.com/',
42 - tokenKey: 'gitcode_token',
43 - usernameKey: 'gitcode_username',
44 - domain: 'gitcode.com'
39 + atomgit: {
40 + name: 'AtomGit',
41 + url: 'https://atomgit.com/',
42 + tokenKey: 'atomgit_token',
43 + usernameKey: 'atomgit_username',
44 + domain: 'atomgit.com'
45 45 },
46 46 codeup: {
47 47 name: 'CodeUp',
@@ -341,9 +341,9 @@
341 341 },
342 342 body: JSON.stringify({ name: repoName, private: false, auto_init: false })
343 343 });
344 - } else if (key === 'gitcode') {
344 + } else if (key === 'atomgit') {
345 345 console.log(token)
346 - resp = await fetch(`https://api.gitcode.com/api/v5/orgs/${encodeURIComponent(ownerPath)}/repos`, {
346 + resp = await fetch(`https://api.atomgit.com/api/v5/orgs/${encodeURIComponent(ownerPath)}/repos`, {
347 347 method: 'POST',
348 348 headers: {
349 349 'Authorization': `Bearer ${token}`,
@@ -615,7 +615,7 @@
615 615 break;
616 616
617 617 case 'tiny':
618 - if (platformKey === 'github' || platformKey === 'gitcode') {
618 + if (platformKey === 'github' || platformKey === 'atomgit') {
619 619 group = 'tinyzen'
620 620 }
621 621 break;

Jetsung Chan ревизий этого фрагмента 3 months ago. К ревизии

1 file changed, 2 insertions, 2 deletions

gitea.user.js

@@ -1,7 +1,7 @@
1 1 // ==UserScript==
2 2 // @name Gitea 镜像仓库一键填充
3 3 // @namespace ScriptCat Scripts
4 - // @version 0.2.2
4 + // @version 0.2.3
5 5 // @description 在Gitea仓库设置页面一键填充镜像仓库信息,并在镜像列表中添加目标仓库跳转链接
6 6 // @author Jetsung Chan <[email protected]>
7 7 // @match https://git.idev.top/*/*/settings
@@ -477,6 +477,7 @@
477 477 name: repoName,
478 478 path: repoName,
479 479 namespaceId: targetNs.id,
480 + description: description,
480 481 }),
481 482 onload: r => {
482 483 const data = r.responseText ? JSON.parse(r.responseText) : {};
@@ -796,4 +797,3 @@
796 797 }
797 798 }).observe(document, { subtree: true, childList: true });
798 799 })();
799 -

Jetsung Chan ревизий этого фрагмента 3 months ago. К ревизии

1 file changed, 46 insertions, 7 deletions

gitea.user.js

@@ -1,7 +1,7 @@
1 1 // ==UserScript==
2 2 // @name Gitea 镜像仓库一键填充
3 3 // @namespace ScriptCat Scripts
4 - // @version 0.2.1
4 + // @version 0.2.2
5 5 // @description 在Gitea仓库设置页面一键填充镜像仓库信息,并在镜像列表中添加目标仓库跳转链接
6 6 // @author Jetsung Chan <[email protected]>
7 7 // @match https://git.idev.top/*/*/settings
@@ -241,6 +241,39 @@
241 241 return null;
242 242 }
243 243
244 + // 查询群组 ID(异步函数)
245 + async function getGitLabGroupId(token, url, ownerPath) {
246 + let namespaceId = null;
247 +
248 + try {
249 + const encodedOwnerPath = encodeURIComponent(ownerPath);
250 + const groupResp = await fetch(`${url}api/v4/groups/${encodedOwnerPath}`, {
251 + method: 'GET',
252 + headers: {
253 + 'PRIVATE-TOKEN': token,
254 + 'Accept': 'application/json'
255 + }
256 + });
257 +
258 + if (groupResp.ok) {
259 + const groupData = await groupResp.json();
260 + namespaceId = groupData.id;
261 + console.log(`找到群组 ${ownerPath},ID = ${namespaceId}`);
262 + } else if (groupResp.status === 404) {
263 + console.warn(`群组 ${ownerPath} 不存在,将创建在个人命名空间下`);
264 + // namespaceId 保持 null
265 + } else {
266 + const err = await groupResp.text();
267 + throw new Error(`查询群组失败: ${groupResp.status} - ${err}`);
268 + }
269 + } catch (err) {
270 + console.error('查询群组出错:', err);
271 + throw err; // 让调用方处理错误
272 + }
273 +
274 + return namespaceId;
275 + }
276 +
244 277 // 在推送镜像表格的操作列中添加「创建仓库」按钮
245 278 function addTargetRepoColumn() {
246 279 // 匹配推送镜像的表格
@@ -302,6 +335,7 @@
302 335 if (!ownerPath || !repoName) throw new Error('无法解析 owner 或 repo');
303 336
304 337 let resp, data;
338 + let namespaceId = null;
305 339
306 340 // === 统一创建接口 ===
307 341 if (key === 'github') {
@@ -342,22 +376,27 @@
342 376 })
343 377 });
344 378 } else if (p.type === 'gitlab' || key === 'framagit') {
345 - let reqPath = 'api/v4//projects'
379 + let reqPath = 'api/v4/projects'
346 380 if (username !== ownerPath) {
347 - // https://docs.gitlab.com/api/projects/#create-a-project
348 - // reqPath += `/user/${user_id}`
349 - reqPath = `api/v4/groups/${encodeURIComponent(ownerPath)}`
381 + try {
382 + namespaceId = await getGitLabGroupId(token, p.url, ownerPath);
383 + } catch (err) {
384 + console.error("获取群组ID失败", err);
385 + }
350 386 }
351 387 resp = await fetch(p.url + reqPath, {
352 388 method: 'POST',
353 389 headers: {
354 - 'Authorization': `PRIVATE-TOKEN: ${token}`,
355 - 'Content-Type': 'application/json'
390 + 'PRIVATE-TOKEN': token,
391 + 'Content-Type': 'application/json',
392 + 'Referer': p.url,
393 + 'Origin': p.url
356 394 },
357 395 body: JSON.stringify({
358 396 name: repoName,
359 397 visibility: 'private',
360 398 description: description,
399 + namespace_id: namespaceId,
361 400 })
362 401 });
363 402 } else if (key === 'gitcode') {

Jetsung Chan ревизий этого фрагмента 3 months ago. К ревизии

1 file changed, 23 insertions, 7 deletions

gitea.user.js

@@ -1,7 +1,7 @@
1 1 // ==UserScript==
2 2 // @name Gitea 镜像仓库一键填充
3 3 // @namespace ScriptCat Scripts
4 - // @version 0.2.0
4 + // @version 0.2.1
5 5 // @description 在Gitea仓库设置页面一键填充镜像仓库信息,并在镜像列表中添加目标仓库跳转链接
6 6 // @author Jetsung Chan <[email protected]>
7 7 // @match https://git.idev.top/*/*/settings
@@ -305,7 +305,11 @@
305 305
306 306 // === 统一创建接口 ===
307 307 if (key === 'github') {
308 - resp = await fetch('https://api.github.com/user/repos', {
308 + let reqPath = 'user/repos'
309 + if (username !== ownerPath) {
310 + reqPath = `orgs/${ownerPath}/repos`
311 + }
312 + resp = await fetch('https://api.github.com/' + reqPath, {
309 313 method: 'POST',
310 314 headers: {
311 315 'Authorization': `token ${token}`,
@@ -320,7 +324,11 @@
320 324 })
321 325 });
322 326 } else if (p.type === 'gitea' || key === 'codeberg') {
323 - resp = await fetch(p.url + 'api/v1/user/repos', {
327 + let reqPath = 'api/v1/user/repos'
328 + if (username !== ownerPath) {
329 + reqPath = `api/v1/orgs/${ownerPath}/repos`
330 + }
331 + resp = await fetch(p.url + reqPath, {
324 332 method: 'POST',
325 333 headers: {
326 334 'Authorization': `token ${token}`,
@@ -334,8 +342,13 @@
334 342 })
335 343 });
336 344 } else if (p.type === 'gitlab' || key === 'framagit') {
337 - // console.log(token)
338 - resp = await fetch(p.url + `api/v4/groups/${encodeURIComponent(ownerPath)}`, {
345 + let reqPath = 'api/v4//projects'
346 + if (username !== ownerPath) {
347 + // https://docs.gitlab.com/api/projects/#create-a-project
348 + // reqPath += `/user/${user_id}`
349 + reqPath = `api/v4/groups/${encodeURIComponent(ownerPath)}`
350 + }
351 + resp = await fetch(p.url + reqPath, {
339 352 method: 'POST',
340 353 headers: {
341 354 'Authorization': `PRIVATE-TOKEN: ${token}`,
@@ -348,8 +361,11 @@
348 361 })
349 362 });
350 363 } else if (key === 'gitcode') {
351 - // console.log(token)
352 - resp = await fetch(`https://api.gitcode.com/api/v5/orgs/${encodeURIComponent(ownerPath)}/repos`, {
364 + let reqPath = 'api/v5/user/repos'
365 + if (username !== ownerPath) {
366 + reqPath = `api/v5/orgs/${encodeURIComponent(ownerPath)}/repos`
367 + }
368 + resp = await fetch(`https://api.gitcode.com/` + reqPath, {
353 369 method: 'POST',
354 370 headers: {
355 371 'Authorization': `Bearer ${token}`,

Jetsung Chan ревизий этого фрагмента 3 months ago. К ревизии

1 file changed, 31 insertions, 16 deletions

gitea.user.js

@@ -1,7 +1,7 @@
1 1 // ==UserScript==
2 2 // @name Gitea 镜像仓库一键填充
3 3 // @namespace ScriptCat Scripts
4 - // @version 0.1.2
4 + // @version 0.2.0
5 5 // @description 在Gitea仓库设置页面一键填充镜像仓库信息,并在镜像列表中添加目标仓库跳转链接
6 6 // @author Jetsung Chan <[email protected]>
7 7 // @match https://git.idev.top/*/*/settings
@@ -69,6 +69,8 @@
69 69 }
70 70 };
71 71
72 + const description = document.getElementById('description').value;
73 +
72 74 // 添加样式
73 75 GM_addStyle(`
74 76 .mirror-helper-container {
@@ -223,7 +225,7 @@
223 225 let cleanUrl = mirrorUrl.replace(/^https?:\/\/[^@]+@/, 'https://')
224 226 .replace(/\.git$/, '');
225 227
226 - console.log(cleanUrl)
228 + // console.log(cleanUrl);
227 229 for (const [key, platform] of Object.entries(platforms)) {
228 230 if (cleanUrl.includes(platform.domain)) {
229 231 return {
@@ -272,7 +274,7 @@
272 274 btn.innerHTML = `<svg viewBox="0 0 16 16" class="svg octicon-plus" width="14" height="14" style="vertical-align: middle; margin-right: 2px;"><path d="M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z"></path></svg>`;
273 275 btn.title = `创建仓库`;
274 276
275 - console.log(info)
277 + // console.log(info);
276 278
277 279 btn.onclick = async () => {
278 280 const key = info.key;
@@ -310,7 +312,12 @@
310 312 'Content-Type': 'application/json',
311 313 'Accept': 'application/vnd.github.v3+json'
312 314 },
313 - body: JSON.stringify({ name: repoName, private: false, auto_init: false })
315 + body: JSON.stringify({
316 + name: repoName,
317 + private: false,
318 + auto_init: false,
319 + description: description,
320 + })
314 321 });
315 322 } else if (p.type === 'gitea' || key === 'codeberg') {
316 323 resp = await fetch(p.url + 'api/v1/user/repos', {
@@ -319,10 +326,15 @@
319 326 'Authorization': `token ${token}`,
320 327 'Content-Type': 'application/json'
321 328 },
322 - body: JSON.stringify({ name: repoName, private: false, auto_init: false })
329 + body: JSON.stringify({
330 + name: repoName,
331 + private: false,
332 + auto_init: false,
333 + description:description,
334 + })
323 335 });
324 336 } else if (p.type === 'gitlab' || key === 'framagit') {
325 - console.log(token)
337 + // console.log(token)
326 338 resp = await fetch(p.url + `api/v4/groups/${encodeURIComponent(ownerPath)}`, {
327 339 method: 'POST',
328 340 headers: {
@@ -331,11 +343,12 @@
331 343 },
332 344 body: JSON.stringify({
333 345 name: repoName,
334 - visibility: 'private'
346 + visibility: 'private',
347 + description: description,
335 348 })
336 349 });
337 350 } else if (key === 'gitcode') {
338 - console.log(token)
351 + // console.log(token)
339 352 resp = await fetch(`https://api.gitcode.com/api/v5/orgs/${encodeURIComponent(ownerPath)}/repos`, {
340 353 method: 'POST',
341 354 headers: {
@@ -347,7 +360,8 @@
347 360 path: repoName,
348 361 public: 1,
349 362 auto_init: false,
350 - default_branch: 'main'
363 + default_branch: 'main',
364 + description: description,
351 365 })
352 366 });
353 367 } else if (key === 'codeup') {
@@ -385,13 +399,13 @@
385 399 throw new Error(`未找到群组 ${ownerPath}\n可用群组:${paths || '无'}`);
386 400 }
387 401
388 - console.log('匹配群组成功:', targetNs);
389 - console.log(JSON.stringify({
390 - name: repoName,
391 - path: repoName,
392 - namespaceId: targetNs.id,
393 - readMeType: 'NONE'
394 - }));
402 + // console.log('匹配群组成功:', targetNs);
403 + // console.log(JSON.stringify({
404 + // name: repoName,
405 + // path: repoName,
406 + // namespaceId: targetNs.id,
407 + // readMeType: 'NONE'
408 + // }));
395 409
396 410 // Step 4: 正确创建仓库(使用 organizationId + namespaceId)
397 411 await new Promise((resolve, reject) => {
@@ -727,3 +741,4 @@
727 741 }
728 742 }).observe(document, { subtree: true, childList: true });
729 743 })();
744 +

jetsung ревизий этого фрагмента 3 months ago. К ревизии

3 files changed, 1430 insertions, 1 deletion

gistfile1.txt (файл удалён)

@@ -1 +0,0 @@
1 -

gitea.user.js(файл создан)

@@ -0,0 +1,729 @@
1 + // ==UserScript==
2 + // @name Gitea 镜像仓库一键填充
3 + // @namespace ScriptCat Scripts
4 + // @version 0.1.2
5 + // @description 在Gitea仓库设置页面一键填充镜像仓库信息,并在镜像列表中添加目标仓库跳转链接
6 + // @author Jetsung Chan <[email protected]>
7 + // @match https://git.idev.top/*/*/settings
8 + // @match https://git.asfd.cn/*/*/settings
9 + // @match https://codeberg.org/*/*/settings
10 + // @grant GM_getValue
11 + // @grant GM_setValue
12 + // @grant GM_deleteValue
13 + // @grant GM_addStyle
14 + // @grant GM_registerMenuCommand
15 + // @grant GM_unregisterMenuCommand
16 + // @grant GM_notification
17 + // @grant GM_xmlhttpRequest
18 + // @connect openapi-rdc.aliyuncs.com
19 + // @connect codeup.aliyun.com
20 + // @connect api.github.com
21 + // @connect codeberg.org
22 + // @connect gitcode.com
23 + // @connect framagit.org
24 + // @downloadURL https://gist.asfd.cn/jetsung/gitsync/raw/HEAD/gitea.user.js
25 + // @updateURL https://gist.asfd.cn/jetsung/gitsync/raw/HEAD/gitea.user.js
26 + // ==/UserScript==
27 +
28 + (function() {
29 + 'use strict';
30 +
31 + // 平台配置
32 + const platforms = {
33 + github: {
34 + name: 'GitHub',
35 + url: 'https://github.com/',
36 + tokenKey: 'github_token',
37 + usernameKey: 'github_username',
38 + domain: 'github.com'
39 + },
40 + gitcode: {
41 + name: 'GitCode',
42 + url: 'https://gitcode.com/',
43 + tokenKey: 'gitcode_token',
44 + usernameKey: 'gitcode_username',
45 + domain: 'gitcode.com'
46 + },
47 + codeup: {
48 + name: 'CodeUp',
49 + url: 'https://codeup.aliyun.com/jetsung/',
50 + tokenKey: 'codeup_token',
51 + usernameKey: 'codeup_username',
52 + domain: 'codeup.aliyun.com'
53 + },
54 + codeberg: {
55 + name: 'Codeberg',
56 + url: 'https://codeberg.org/',
57 + tokenKey: 'codeberg_token',
58 + usernameKey: 'codeberg_username',
59 + domain: 'codeberg.org',
60 + type: 'gitea'
61 + },
62 + framagit: {
63 + name: 'Framagit',
64 + url: 'https://framagit.org/',
65 + tokenKey: 'framagit_token',
66 + usernameKey: 'framagit_username',
67 + domain: 'framagit.org',
68 + type: 'gitlab'
69 + }
70 + };
71 +
72 + // 添加样式
73 + GM_addStyle(`
74 + .mirror-helper-container {
75 + position: fixed;
76 + top: 20px;
77 + right: 20px;
78 + z-index: 9999;
79 + background: white;
80 + border: 1px solid #ddd;
81 + border-radius: 8px;
82 + box-shadow: 0 4px 12px rgba(0,0,0,0.15);
83 + padding: 20px;
84 + width: 270px;
85 + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
86 + }
87 +
88 + .mirror-helper-header {
89 + font-size: 18px;
90 + font-weight: 600;
91 + margin-bottom: 15px;
92 + color: #333;
93 + border-bottom: 1px solid #eee;
94 + padding-bottom: 10px;
95 + }
96 +
97 + .mirror-helper-section {
98 + margin-bottom: 15px;
99 + }
100 +
101 + .mirror-helper-section h4 {
102 + font-size: 14px;
103 + margin: 0 0 8px 0;
104 + color: #555;
105 + }
106 +
107 + .mirror-helper-input {
108 + width: 100%;
109 + padding: 8px 10px;
110 + margin-bottom: 8px;
111 + border: 1px solid #ddd;
112 + border-radius: 4px;
113 + font-size: 14px;
114 + box-sizing: border-box;
115 + }
116 +
117 + .mirror-helper-button {
118 + background: #1f75cb;
119 + color: white;
120 + border: none;
121 + border-radius: 4px;
122 + padding: 8px 16px;
123 + font-size: 14px;
124 + cursor: pointer;
125 + margin-right: 8px;
126 + margin-bottom: 8px;
127 + transition: background 0.2s;
128 + }
129 +
130 + .mirror-helper-button:hover {
131 + background: #1b5faa;
132 + }
133 +
134 + .mirror-helper-button.secondary {
135 + background: #e6e6e6;
136 + color: #333;
137 + }
138 +
139 + .mirror-helper-button.secondary:hover {
140 + background: #d0d0d0;
141 + }
142 +
143 + .mirror-helper-select {
144 + width: 100%;
145 + padding: 8px 10px;
146 + margin-bottom: 12px;
147 + border: 1px solid #ddd;
148 + border-radius: 4px;
149 + font-size: 14px;
150 + box-sizing: border-box;
151 + }
152 +
153 + .mirror-helper-footer {
154 + margin-top: 15px;
155 + padding-top: 10px;
156 + border-top: 1px solid #eee;
157 + font-size: 12px;
158 + color: #777;
159 + }
160 +
161 + .mirror-helper-close {
162 + position: absolute;
163 + top: 10px;
164 + right: 10px;
165 + background: none;
166 + border: none;
167 + font-size: 18px;
168 + cursor: pointer;
169 + color: #999;
170 + }
171 +
172 + .mirror-helper-close:hover {
173 + color: #333;
174 + }
175 +
176 + /* 目标仓库列样式 */
177 + .target-repo-column {
178 + max-width: 200px;
179 + }
180 +
181 + .target-repo-link {
182 + color: #0366d6;
183 + text-decoration: none;
184 + font-weight: 500;
185 + display: inline-block;
186 + padding: 2px 4px;
187 + border-radius: 3px;
188 + transition: background-color 0.2s;
189 + }
190 +
191 + .target-repo-link:hover {
192 + background-color: rgba(3, 102, 214, 0.1);
193 + text-decoration: none;
194 + }
195 +
196 + .target-repo-link svg {
197 + width: 14px;
198 + height: 14px;
199 + margin-right: 4px;
200 + vertical-align: middle;
201 + }
202 +
203 + .no-target-link {
204 + color: #666;
205 + font-style: italic;
206 + }
207 +
208 + .create-repo-td { white-space: nowrap; }
209 + .create-repo-btn {
210 + background: #28a745; color: #fff; border: none; border-radius: 4px;
211 + padding: 4px 10px; font-size: 12px; cursor: pointer; transition: all 0.2s;
212 + }
213 + .create-repo-btn:hover { background: #218838; }
214 + .create-repo-btn:disabled { background: #6c757d; cursor: not-allowed; }
215 + .create-repo-btn.processing { background: #fd7e14 !important; }
216 + .create-repo-btn.success { background: #1e7e34 !important; }
217 + .create-repo-btn.error { background: #dc3545 !important; }
218 + `);
219 +
220 + // 解析镜像URL获取目标仓库信息
221 + function parseMirrorUrl(mirrorUrl) {
222 + try {
223 + let cleanUrl = mirrorUrl.replace(/^https?:\/\/[^@]+@/, 'https://')
224 + .replace(/\.git$/, '');
225 +
226 + console.log(cleanUrl)
227 + for (const [key, platform] of Object.entries(platforms)) {
228 + if (cleanUrl.includes(platform.domain)) {
229 + return {
230 + platform: platform.name,
231 + url: cleanUrl,
232 + key: key
233 + };
234 + }
235 + }
236 + } catch (e) {
237 + console.error('解析镜像URL失败:', mirrorUrl, e);
238 + }
239 + return null;
240 + }
241 +
242 + // 在推送镜像表格的操作列中添加「创建仓库」按钮
243 + function addTargetRepoColumn() {
244 + // 匹配推送镜像的表格
245 + const table = document.querySelector('table.ui.table');
246 + if (!table) return;
247 +
248 + const tbody = table.querySelector('tbody');
249 + if (!tbody) return;
250 +
251 + // 获取所有行,过滤掉最后的“添加镜像”表单行
252 + const rows = tbody.querySelectorAll('tr');
253 + rows.forEach(row => {
254 + // 如果行内包含 form 或者 colspan="4",说明是添加镜像的输入框行,跳过
255 + if (row.querySelector('form[action*="push-mirror-add"]') || row.querySelector('td[colspan]')) return;
256 +
257 + // 查找操作列(右对齐的那个 td)
258 + const actionTd = row.querySelector('td.tw-text-right');
259 + if (!actionTd || actionTd.querySelector('.create-repo-btn')) return;
260 +
261 + // 获取当前行的仓库 URL
262 + const urlTd = row.querySelector('td.tw-break-anywhere');
263 + if (!urlTd) return;
264 +
265 + const mirrorUrl = urlTd.textContent.trim();
266 + const info = parseMirrorUrl(mirrorUrl);
267 +
268 + // 创建「创建仓库」按钮
269 + const btn = document.createElement('button');
270 + btn.className = 'create-repo-btn ui primary tiny button';
271 + btn.style.marginRight = '4px'; // 留点间距
272 + btn.innerHTML = `<svg viewBox="0 0 16 16" class="svg octicon-plus" width="14" height="14" style="vertical-align: middle; margin-right: 2px;"><path d="M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z"></path></svg>`;
273 + btn.title = `创建仓库`;
274 +
275 + console.log(info)
276 +
277 + btn.onclick = async () => {
278 + const key = info.key;
279 + const p = platforms[key];
280 + const username = GM_getValue(p.usernameKey, '');
281 + const token = GM_getValue(p.tokenKey, '');
282 +
283 + if (!username || !token) {
284 + alert(`${p.name} 未配置账号信息`);
285 + return;
286 + }
287 +
288 + btn.disabled = true;
289 + btn.textContent = '创建中...';
290 + btn.classList.add('processing');
291 +
292 + try {
293 + // === 终极解析:支持多级子组 ===
294 + const cleanUrl = info.url.replace(/\.git$/, '').replace(/\/+$/, '');
295 + const parts = cleanUrl.split('/').slice(3); // 去掉 https://domain.com
296 + if (parts.length < 2) throw new Error('无法解析仓库地址:路径太短');
297 + const repoName = parts.pop(); // 最后一段:仓库名
298 + const ownerPath = parts.join('/'); // 前面所有:完整 owner(支持多级)
299 +
300 + if (!ownerPath || !repoName) throw new Error('无法解析 owner 或 repo');
301 +
302 + let resp, data;
303 +
304 + // === 统一创建接口 ===
305 + if (key === 'github') {
306 + resp = await fetch('https://api.github.com/user/repos', {
307 + method: 'POST',
308 + headers: {
309 + 'Authorization': `token ${token}`,
310 + 'Content-Type': 'application/json',
311 + 'Accept': 'application/vnd.github.v3+json'
312 + },
313 + body: JSON.stringify({ name: repoName, private: false, auto_init: false })
314 + });
315 + } else if (p.type === 'gitea' || key === 'codeberg') {
316 + resp = await fetch(p.url + 'api/v1/user/repos', {
317 + method: 'POST',
318 + headers: {
319 + 'Authorization': `token ${token}`,
320 + 'Content-Type': 'application/json'
321 + },
322 + body: JSON.stringify({ name: repoName, private: false, auto_init: false })
323 + });
324 + } else if (p.type === 'gitlab' || key === 'framagit') {
325 + console.log(token)
326 + resp = await fetch(p.url + `api/v4/groups/${encodeURIComponent(ownerPath)}`, {
327 + method: 'POST',
328 + headers: {
329 + 'Authorization': `PRIVATE-TOKEN: ${token}`,
330 + 'Content-Type': 'application/json'
331 + },
332 + body: JSON.stringify({
333 + name: repoName,
334 + visibility: 'private'
335 + })
336 + });
337 + } else if (key === 'gitcode') {
338 + console.log(token)
339 + resp = await fetch(`https://api.gitcode.com/api/v5/orgs/${encodeURIComponent(ownerPath)}/repos`, {
340 + method: 'POST',
341 + headers: {
342 + 'Authorization': `Bearer ${token}`,
343 + 'Content-Type': 'application/json'
344 + },
345 + body: JSON.stringify({
346 + name: repoName,
347 + path: repoName,
348 + public: 1,
349 + auto_init: false,
350 + default_branch: 'main'
351 + })
352 + });
353 + } else if (key === 'codeup') {
354 + const domain = 'https://openapi-rdc.aliyuncs.com';
355 + const referer = 'https://codeup.aliyun.com';
356 +
357 + // Step 1: 获取组织列表
358 + const orgs = await new Promise((res, rej) => GM_xmlhttpRequest({
359 + method: 'GET', url: `${domain}/oapi/v1/platform/organizations?perPage=100`,
360 + headers: { 'x-yunxiao-token': token, 'Content-Type': 'application/json', 'Referer': referer, 'Origin': referer },
361 + onload: r => r.status >= 200 && r.status < 300 ? res(JSON.parse(r.responseText)) : rej(new Error(`组织列表失败 ${r.status}`)),
362 + onerror: () => rej(new Error('网络错误')), ontimeout: () => rej(new Error('超时'))
363 + }));
364 +
365 + // Step 2: 获取群组空间
366 + const org = orgs.find(o => o.name.toLowerCase() === ownerPath.split('/')[0].toLowerCase());
367 + if (!org) throw new Error(`未找到组织 ${ownerPath.split('/')[0]}`);
368 +
369 + const namespaces = await new Promise((res, rej) => GM_xmlhttpRequest({
370 + method: 'GET', url: `${domain}/oapi/v1/codeup/organizations/${org.id}/namespaces`,
371 + headers: { 'x-yunxiao-token': token, 'Content-Type': 'application/json', 'Referer': referer, 'Origin': referer },
372 + onload: r => r.status >= 200 && r.status < 300 ? res(JSON.parse(r.responseText)) : rej(new Error(`群组列表失败 ${r.status}`)),
373 + onerror: () => rej(new Error('网络错误')), ontimeout: () => rej(new Error('超时'))
374 + }));
375 +
376 + // Step 3: 匹配群组(webUrl / pathWithNamespace / nameWithNamespace)
377 + const targetNs = namespaces.find(ns => {
378 + const web = (ns.webUrl || '').replace(/https?:\/\/[^\/]+/, '');
379 + const path = ns.pathWithNamespace || ns.nameWithNamespace || '';
380 + return web.includes(ownerPath) || path.toLowerCase() === ownerPath.toLowerCase();
381 + });
382 +
383 + if (!targetNs) {
384 + const paths = namespaces.map(n => n.pathWithNamespace || n.name).join(', ');
385 + throw new Error(`未找到群组 ${ownerPath}\n可用群组:${paths || '无'}`);
386 + }
387 +
388 + console.log('匹配群组成功:', targetNs);
389 + console.log(JSON.stringify({
390 + name: repoName,
391 + path: repoName,
392 + namespaceId: targetNs.id,
393 + readMeType: 'NONE'
394 + }));
395 +
396 + // Step 4: 正确创建仓库(使用 organizationId + namespaceId)
397 + await new Promise((resolve, reject) => {
398 + GM_xmlhttpRequest({
399 + method: 'POST',
400 + url: `${domain}/oapi/v1/codeup/organizations/${org.id}/repositories?createParentPath=true`,
401 + headers: {
402 + 'x-yunxiao-token': token,
403 + 'Content-Type': 'application/json',
404 + 'Referer': referer,
405 + 'Origin': referer
406 + },
407 + data: JSON.stringify({
408 + name: repoName,
409 + path: repoName,
410 + namespaceId: targetNs.id,
411 + }),
412 + onload: r => {
413 + const data = r.responseText ? JSON.parse(r.responseText) : {};
414 + if (r.status >= 200 && r.status < 300 || data.id || data.result?.id) {
415 + console.log('CodeUp 仓库创建成功:', data);
416 + resolve(data);
417 + } else {
418 + reject(new Error(data.message || data.error || `创建失败(${r.status})`));
419 + }
420 + },
421 + onerror: () => reject(new Error('网络错误')),
422 + ontimeout: () => reject(new Error('请求超时'))
423 + });
424 + });
425 +
426 + // 模拟成功响应
427 + resp = { ok: true, status: 200 };
428 + data = { id: 'success' };
429 + }
430 +
431 + // === 统一成功判断 ===
432 + if (
433 + resp.status >= 200 && resp.status < 400 ||
434 + data.id || data.full_name || data.html_url || data.name || data.path ||
435 + (data.message && /already exists|已存在/i.test(data.message))
436 + ) {
437 + btn.textContent = '成功';
438 + btn.classList.add('success');
439 +
440 + // 自动填回 URL
441 + const input = document.querySelector('#url, #project_remote_mirrors_attributes_0_url');
442 + if (input) input.value = info.url;
443 + } else {
444 + throw new Error(data.message || data.error || `HTTP ${resp.status}`);
445 + }
446 +
447 + } catch (e) {
448 + btn.textContent = '失败';
449 + btn.classList.add('error');
450 + console.error('创建仓库失败:', e);
451 + alert(`${p.name} 创建失败:${e.message}`);
452 + }
453 +
454 + // 3秒后恢复
455 + setTimeout(() => {
456 + btn.disabled = false;
457 + btn.textContent = '创建仓库';
458 + btn.className = 'create-repo-btn';
459 + }, 3000);
460 + };
461 +
462 + // 插入到操作按钮组的最前面
463 + actionTd.insertBefore(btn, actionTd.firstChild);
464 + });
465 + }
466 +
467 + // 创建设置面板
468 + function createSettingsPanel() {
469 + const panel = document.createElement('div');
470 + panel.className = 'mirror-helper-container';
471 + panel.id = 'mirror-settings-panel';
472 +
473 + let html = `
474 + <button class="mirror-helper-close" id="close-settings">×</button>
475 + <div class="mirror-helper-header">镜像仓库设置</div>
476 + `;
477 +
478 + // 为每个平台创建输入框
479 + for (const [key, platform] of Object.entries(platforms)) {
480 + html += `
481 + <div class="mirror-helper-section">
482 + <h4>${platform.name}</h4>
483 + <input type="text" class="mirror-helper-input" id="${key}-username"
484 + placeholder="用户名" value="${GM_getValue(platform.usernameKey, '')}">
485 + <input type="password" class="mirror-helper-input" id="${key}-token"
486 + placeholder="Token" value="${GM_getValue(platform.tokenKey, '')}">
487 + </div>
488 + `;
489 + }
490 +
491 + html += `
492 + <button class="mirror-helper-button" id="save-settings">保存设置</button>
493 + <button class="mirror-helper-button secondary" id="clear-settings">清除设置</button>
494 + <div class="mirror-helper-footer">设置将保存在浏览器本地</div>
495 + `;
496 +
497 + panel.innerHTML = html;
498 + document.body.appendChild(panel);
499 +
500 + // 添加事件监听
501 + document.getElementById('close-settings').addEventListener('click', () => {
502 + panel.remove();
503 + });
504 +
505 + document.getElementById('save-settings').addEventListener('click', () => {
506 + for (const [key, platform] of Object.entries(platforms)) {
507 + const username = document.getElementById(`${key}-username`).value;
508 + const token = document.getElementById(`${key}-token`).value;
509 +
510 + GM_setValue(platform.usernameKey, username);
511 + GM_setValue(platform.tokenKey, token);
512 + }
513 +
514 + alert('设置已保存!');
515 + panel.remove();
516 + location.reload(); // 刷新页面以应用新设置
517 + });
518 +
519 + document.getElementById('clear-settings').addEventListener('click', () => {
520 + if (confirm('确定要清除所有设置吗?')) {
521 + for (const platform of Object.values(platforms)) {
522 + GM_deleteValue(platform.usernameKey);
523 + GM_deleteValue(platform.tokenKey);
524 + }
525 + alert('设置已清除!');
526 + panel.remove();
527 + location.reload();
528 + }
529 + });
530 + }
531 +
532 + // 创建填充按钮
533 + function createFillButton() {
534 + const container = document.createElement('div');
535 + container.className = 'mirror-helper-container';
536 + container.id = 'mirror-fill-container';
537 +
538 + let html = `
539 + <button class="mirror-helper-close" id="close-fill">×</button>
540 + <div class="mirror-helper-header">镜像仓库填充</div>
541 + <div class="mirror-helper-section">
542 + <h4>选择平台</h4>
543 + <select class="mirror-helper-select" id="platform-select">
544 + `;
545 +
546 + // 添加平台选项
547 + for (const [key, platform] of Object.entries(platforms)) {
548 + const username = GM_getValue(platform.usernameKey, '');
549 + const token = GM_getValue(platform.tokenKey, '');
550 + const status = (username && token) ? '✓' : '✗';
551 + html += `<option value="${key}">${platform.name} ${status}</option>`;
552 + }
553 +
554 + html += `
555 + </select>
556 + </div>
557 + <button class="mirror-helper-button" id="fill-mirror">填充镜像仓库</button>
558 + <button class="mirror-helper-button secondary" id="open-settings">设置账号</button>
559 + <div class="mirror-helper-footer">点击填充按钮自动填写表单</div>
560 + `;
561 +
562 + container.innerHTML = html;
563 + document.body.appendChild(container);
564 +
565 + // 添加事件监听
566 + document.getElementById('close-fill').addEventListener('click', () => {
567 + container.remove();
568 + });
569 +
570 + document.getElementById('open-settings').addEventListener('click', () => {
571 + container.remove();
572 + createSettingsPanel();
573 + });
574 +
575 + document.getElementById('fill-mirror').addEventListener('click', () => {
576 + const selectedPlatform = document.getElementById('platform-select').value;
577 + fillMirrorForm(selectedPlatform);
578 + });
579 + }
580 +
581 + // 填充表单
582 + function fillMirrorForm(platformKey) {
583 + const platform = platforms[platformKey];
584 + const username = GM_getValue(platform.usernameKey, '');
585 + const token = GM_getValue(platform.tokenKey, '');
586 +
587 + if (!username || !token) {
588 + alert(`请先设置 ${platform.name} 的用户名和Token!`);
589 + return;
590 + }
591 +
592 + // 获取当前仓库路径
593 + let [, group, project] = window.location.pathname.split('/');
594 + if (!group || !project) {
595 + alert('无法获取仓库路径!');
596 + return;
597 + }
598 +
599 + switch (group) {
600 + case 'idev':
601 + if (platformKey === 'github') {
602 + group = 'idevsig'
603 + }
604 + break;
605 +
606 + case 'tiny':
607 + if (platformKey === 'github' || platformKey === 'gitcode') {
608 + group = 'tinyzen'
609 + }
610 + break;
611 + }
612 +
613 + // 构建镜像URL
614 + const mirrorUrl = `${platform.url}${group}/${project}.git`;
615 +
616 + // 填充表单
617 + document.getElementById('push_mirror_address').value = mirrorUrl;
618 + document.getElementById('push_mirror_username').value = username;
619 + document.getElementById('push_mirror_password').value = token;
620 + document.getElementById('push_mirror_interval').value = 0;
621 +
622 + // 勾选"推送提交时同步"
623 + const syncCheckbox = document.getElementById('push_mirror_sync_on_commit');
624 + if (syncCheckbox && !syncCheckbox.checked) {
625 + syncCheckbox.click();
626 + }
627 +
628 + // 显示成功消息
629 + const successMsg = document.createElement('div');
630 + successMsg.style.cssText = `
631 + position: fixed;
632 + top: 20px;
633 + left: 50%;
634 + transform: translateX(-50%);
635 + background: #4caf50;
636 + color: white;
637 + padding: 12px 24px;
638 + border-radius: 4px;
639 + box-shadow: 0 2px 8px rgba(0,0,0,0.2);
640 + z-index: 10000;
641 + font-size: 14px;
642 + `;
643 + successMsg.textContent = `已填充 ${platform.name} 镜像仓库信息!`;
644 + document.body.appendChild(successMsg);
645 +
646 + setTimeout(() => {
647 + successMsg.remove();
648 + }, 3000);
649 + }
650 +
651 + // 新增一个专门处理「推送镜像」列表中 URL 变成可点击链接的函数
652 + function makePushMirrorUrlsClickable() {
653 + // 找到所有推送镜像的 URL 单元格
654 + const urlCells = document.querySelectorAll(
655 + 'table.ui.table tbody tr:not([style*="display: none"]) td.tw-break-anywhere'
656 + );
657 +
658 + urlCells.forEach(cell => {
659 + // 避免重复处理
660 + if (cell.querySelector('a')) return;
661 +
662 + const urlText = cell.textContent.trim();
663 +
664 + // 简单验证是否像 Git URL
665 + if (urlText && (urlText.startsWith('http://') || urlText.startsWith('https://'))) {
666 + const link = document.createElement('a');
667 + link.href = urlText;
668 + link.textContent = urlText;
669 + link.target = '_blank'; // 新标签页打开
670 + link.rel = 'noopener noreferrer'; // 安全最佳实践
671 + link.style.color = '#0366d6'; // 模仿 Gitea 的蓝色链接
672 + link.style.textDecoration = 'none';
673 +
674 + // hover 效果(可选,更像原生链接)
675 + link.addEventListener('mouseover', () => {
676 + link.style.textDecoration = 'underline';
677 + });
678 + link.addEventListener('mouseout', () => {
679 + link.style.textDecoration = 'none';
680 + });
681 +
682 + // 清空原文字并替换成链接
683 + cell.innerHTML = '';
684 + cell.appendChild(link);
685 + }
686 + });
687 + }
688 +
689 + // 初始化
690 + function init() {
691 + // 添加目标仓库列(适用于镜像列表页面)
692 + if (document.querySelector('table.ui.table')) {
693 + // 延迟执行以确保DOM完全加载
694 + setTimeout(() => {
695 + addTargetRepoColumn();
696 + // 监听动态加载的行
697 + const observer = new MutationObserver(() => {
698 + addTargetRepoColumn();
699 + });
700 + observer.observe(document.body, { childList: true, subtree: true });
701 + }, 1000);
702 + }
703 +
704 + // 创建填充按钮(适用于设置页面)
705 + if (document.querySelector('table.ui.table')) {
706 + createFillButton();
707 + makePushMirrorUrlsClickable();
708 + }
709 +
710 + // 注册菜单命令
711 + GM_registerMenuCommand('镜像仓库设置', createSettingsPanel);
712 + }
713 +
714 + // 页面加载完成后初始化
715 + if (document.readyState === 'loading') {
716 + document.addEventListener('DOMContentLoaded', init);
717 + } else {
718 + init();
719 + }
720 +
721 + // 监听页面导航(SPA应用)
722 + let currentUrl = location.href;
723 + new MutationObserver(() => {
724 + if (location.href !== currentUrl) {
725 + currentUrl = location.href;
726 + setTimeout(init, 500);
727 + }
728 + }).observe(document, { subtree: true, childList: true });
729 + })();

gitlab.user.js(файл создан)

@@ -0,0 +1,701 @@
1 + // ==UserScript==
2 + // @name GitLab 镜像仓库一键填充
3 + // @namespace ScriptCat Scripts
4 + // @version 0.3.3
5 + // @description 在GitLab仓库设置页面一键填充镜像仓库信息,并在镜像列表中添加目标仓库跳转链接
6 + // @author Jetsung Chan <[email protected]>
7 + // @match https://framagit.org/*/-/settings/repository*
8 + // @match https://gitlab.com/*/-/settings/repository*
9 + // @match https://framagit.org/*/project_activity
10 + // @grant GM_getValue
11 + // @grant GM_setValue
12 + // @grant GM_deleteValue
13 + // @grant GM_addStyle
14 + // @grant GM_registerMenuCommand
15 + // @grant GM_unregisterMenuCommand
16 + // @grant GM_notification
17 + // @grant GM_xmlhttpRequest
18 + // @connect openapi-rdc.aliyuncs.com
19 + // @connect codeup.aliyun.com
20 + // @connect api.github.com
21 + // @connect codeberg.org
22 + // @connect gitcode.com
23 + // @downloadURL https://gist.asfd.cn/jetsung/gitsync/raw/HEAD/gitlab.user.js
24 + // @updateURL https://gist.asfd.cn/jetsung/gitsync/raw/HEAD/gitlab.user.js
25 + // ==/UserScript==
26 +
27 + (function() {
28 + 'use strict';
29 +
30 + // 平台配置
31 + const platforms = {
32 + github: {
33 + name: 'GitHub',
34 + url: 'https://github.com/',
35 + tokenKey: 'github_token',
36 + usernameKey: 'github_username',
37 + domain: 'github.com'
38 + },
39 + gitcode: {
40 + name: 'GitCode',
41 + url: 'https://gitcode.com/',
42 + tokenKey: 'gitcode_token',
43 + usernameKey: 'gitcode_username',
44 + domain: 'gitcode.com'
45 + },
46 + codeup: {
47 + name: 'CodeUp',
48 + url: 'https://codeup.aliyun.com/jetsung/',
49 + tokenKey: 'codeup_token',
50 + usernameKey: 'codeup_username',
51 + domain: 'codeup.aliyun.com'
52 + },
53 + codeberg: {
54 + name: 'Codeberg',
55 + url: 'https://codeberg.org/',
56 + tokenKey: 'codeberg_token',
57 + usernameKey: 'codeberg_username',
58 + domain: 'codeberg.org'
59 + }
60 + };
61 +
62 + // 添加样式
63 + GM_addStyle(`
64 + .mirror-helper-container {
65 + position: fixed;
66 + top: 20px;
67 + right: 20px;
68 + z-index: 9999;
69 + background: white;
70 + border: 1px solid #ddd;
71 + border-radius: 8px;
72 + box-shadow: 0 4px 12px rgba(0,0,0,0.15);
73 + padding: 20px;
74 + width: 270px;
75 + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
76 + }
77 +
78 + .mirror-helper-header {
79 + font-size: 18px;
80 + font-weight: 600;
81 + margin-bottom: 15px;
82 + color: #333;
83 + border-bottom: 1px solid #eee;
84 + padding-bottom: 10px;
85 + }
86 +
87 + .mirror-helper-section {
88 + margin-bottom: 15px;
89 + }
90 +
91 + .mirror-helper-section h4 {
92 + font-size: 14px;
93 + margin: 0 0 8px 0;
94 + color: #555;
95 + }
96 +
97 + .mirror-helper-input {
98 + width: 100%;
99 + padding: 8px 10px;
100 + margin-bottom: 8px;
101 + border: 1px solid #ddd;
102 + border-radius: 4px;
103 + font-size: 14px;
104 + box-sizing: border-box;
105 + }
106 +
107 + .mirror-helper-button {
108 + background: #1f75cb;
109 + color: white;
110 + border: none;
111 + border-radius: 4px;
112 + padding: 8px 16px;
113 + font-size: 14px;
114 + cursor: pointer;
115 + margin-right: 8px;
116 + margin-bottom: 8px;
117 + transition: background 0.2s;
118 + }
119 +
120 + .mirror-helper-button:hover {
121 + background: #1b5faa;
122 + }
123 +
124 + .mirror-helper-button.secondary {
125 + background: #e6e6e6;
126 + color: #333;
127 + }
128 +
129 + .mirror-helper-button.secondary:hover {
130 + background: #d0d0d0;
131 + }
132 +
133 + .mirror-helper-select {
134 + width: 100%;
135 + padding: 8px 10px;
136 + margin-bottom: 12px;
137 + border: 1px solid #ddd;
138 + border-radius: 4px;
139 + font-size: 14px;
140 + box-sizing: border-box;
141 + }
142 +
143 + .mirror-helper-footer {
144 + margin-top: 15px;
145 + padding-top: 10px;
146 + border-top: 1px solid #eee;
147 + font-size: 12px;
148 + color: #777;
149 + }
150 +
151 + .mirror-helper-close {
152 + position: absolute;
153 + top: 10px;
154 + right: 10px;
155 + background: none;
156 + border: none;
157 + font-size: 18px;
158 + cursor: pointer;
159 + color: #999;
160 + }
161 +
162 + .mirror-helper-close:hover {
163 + color: #333;
164 + }
165 +
166 + /* 目标仓库列样式 */
167 + .target-repo-column {
168 + max-width: 200px;
169 + }
170 +
171 + .target-repo-link {
172 + color: #0366d6;
173 + text-decoration: none;
174 + font-weight: 500;
175 + display: inline-block;
176 + padding: 2px 4px;
177 + border-radius: 3px;
178 + transition: background-color 0.2s;
179 + }
180 +
181 + .target-repo-link:hover {
182 + background-color: rgba(3, 102, 214, 0.1);
183 + text-decoration: none;
184 + }
185 +
186 + .target-repo-link svg {
187 + width: 14px;
188 + height: 14px;
189 + margin-right: 4px;
190 + vertical-align: middle;
191 + }
192 +
193 + .no-target-link {
194 + color: #666;
195 + font-style: italic;
196 + }
197 +
198 + .create-repo-td { white-space: nowrap; }
199 + .create-repo-btn {
200 + background: #28a745; color: #fff; border: none; border-radius: 4px;
201 + padding: 4px 10px; font-size: 12px; cursor: pointer; transition: all 0.2s;
202 + }
203 + .create-repo-btn:hover { background: #218838; }
204 + .create-repo-btn:disabled { background: #6c757d; cursor: not-allowed; }
205 + .create-repo-btn.processing { background: #fd7e14 !important; }
206 + .create-repo-btn.success { background: #1e7e34 !important; }
207 + .create-repo-btn.error { background: #dc3545 !important; }
208 + `);
209 +
210 + // 解析镜像URL获取目标仓库信息
211 + function parseMirrorUrl(mirrorUrl) {
212 + try {
213 + let cleanUrl = mirrorUrl.replace(/^https?:\/\/[^@]+@/, 'https://')
214 + .replace(/\.git$/, '');
215 +
216 + for (const [key, platform] of Object.entries(platforms)) {
217 + if (cleanUrl.includes(platform.domain)) {
218 + return {
219 + platform: platform.name,
220 + url: cleanUrl,
221 + displayText: '打开',
222 + key: key
223 + };
224 + }
225 + }
226 + } catch (e) {
227 + console.error('解析镜像URL失败:', mirrorUrl, e);
228 + }
229 + return null;
230 + }
231 +
232 + // 在表格中添加「打开」和「创建仓库」两列
233 + function addTargetRepoColumn() {
234 + const table = document.querySelector('table.gl-table');
235 + if (!table) return;
236 +
237 + const thead = table.querySelector('thead');
238 + const tbody = table.querySelector('tbody');
239 + if (!thead || !tbody) return;
240 +
241 + // 防止重复添加
242 + if (thead.querySelector('th[data-col="open"]')) return;
243 +
244 + const firstTh = thead.querySelector('tr th:first-child');
245 +
246 + // 1 「打开」列(保持你原来位置)
247 + const openTh = document.createElement('th');
248 + openTh.textContent = '目标仓库';
249 + openTh.className = 'target-repo-column';
250 + openTh.setAttribute('data-col', 'open');
251 + firstTh.parentNode.insertBefore(openTh, firstTh.nextSibling);
252 +
253 + // 2 「创建仓库」列(紧跟在打开后面)
254 + const createTh = document.createElement('th');
255 + createTh.textContent = '创建仓库';
256 + createTh.setAttribute('data-col', 'create');
257 + openTh.parentNode.insertBefore(createTh, openTh.nextSibling);
258 +
259 + // 处理每一行
260 + const rows = tbody.querySelectorAll('tr.rspec-mirrored-repository-row');
261 + rows.forEach(row => {
262 + const urlCell = row.querySelector('[data-testid="mirror-repository-url-content"]');
263 + if (!urlCell) return;
264 +
265 + const mirrorUrl = urlCell.textContent.trim();
266 + const info = parseMirrorUrl(mirrorUrl);
267 +
268 + // === 打开列(你原来的代码完全不动)===
269 + const openTd = document.createElement('td');
270 + openTd.className = 'target-repo-column';
271 + if (info) {
272 + const link = document.createElement('a');
273 + link.href = info.url;
274 + link.className = 'target-repo-link';
275 + link.target = '_blank';
276 + link.innerHTML = `
277 + <svg viewBox="0 0 24 24" fill="currentColor">
278 + <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
279 + </svg>
280 + ${info.displayText}
281 + `;
282 + openTd.appendChild(link);
283 + } else {
284 + openTd.innerHTML = '<span class="no-target-link">无法解析</span>';
285 + }
286 + const firstTd = row.querySelector('td:first-child');
287 + firstTd.parentNode.insertBefore(openTd, firstTd.nextSibling);
288 +
289 + // === 新增:创建仓库列 ===
290 + const createTd = document.createElement('td');
291 + createTd.className = 'create-repo-td';
292 + if (info && info.key) {
293 + const btn = document.createElement('button');
294 + btn.className = 'create-repo-btn';
295 + btn.textContent = '创建仓库';
296 +
297 + btn.onclick = async () => {
298 + const key = info.key;
299 + const p = platforms[key];
300 + const username = GM_getValue(p.usernameKey, '');
301 + const token = GM_getValue(p.tokenKey, '');
302 +
303 + if (!username || !token) {
304 + alert(`${p.name} 未配置账号信息`);
305 + return;
306 + }
307 +
308 + btn.disabled = true;
309 + btn.textContent = '创建中...';
310 + btn.classList.add('processing');
311 +
312 + try {
313 + // === 终极解析:支持多级子组 ===
314 + const cleanUrl = info.url.replace(/\.git$/, '').replace(/\/+$/, '');
315 + const parts = cleanUrl.split('/').slice(3); // 去掉 https://domain.com
316 + if (parts.length < 2) throw new Error('无法解析仓库地址:路径太短');
317 + const repoName = parts.pop(); // 最后一段:仓库名
318 + const ownerPath = parts.join('/'); // 前面所有:完整 owner(支持多级)
319 +
320 + if (!ownerPath || !repoName) throw new Error('无法解析 owner 或 repo');
321 +
322 + let resp, data;
323 +
324 + // === 统一创建接口 ===
325 + if (key === 'github') {
326 + resp = await fetch('https://api.github.com/user/repos', {
327 + method: 'POST',
328 + headers: {
329 + 'Authorization': `token ${token}`,
330 + 'Content-Type': 'application/json',
331 + 'Accept': 'application/vnd.github.v3+json'
332 + },
333 + body: JSON.stringify({ name: repoName, private: false, auto_init: false })
334 + });
335 + } else if (key === 'codeberg') {
336 + resp = await fetch('https://codeberg.org/api/v1/user/repos', {
337 + method: 'POST',
338 + headers: {
339 + 'Authorization': `token ${token}`,
340 + 'Content-Type': 'application/json'
341 + },
342 + body: JSON.stringify({ name: repoName, private: false, auto_init: false })
343 + });
344 + } else if (key === 'gitcode') {
345 + console.log(token)
346 + resp = await fetch(`https://api.gitcode.com/api/v5/orgs/${encodeURIComponent(ownerPath)}/repos`, {
347 + method: 'POST',
348 + headers: {
349 + 'Authorization': `Bearer ${token}`,
350 + 'Content-Type': 'application/json'
351 + },
352 + body: JSON.stringify({
353 + name: repoName,
354 + path: repoName,
355 + public: 1,
356 + auto_init: false,
357 + default_branch: 'main'
358 + })
359 + });
360 + } else if (key === 'codeup') {
361 + const domain = 'https://openapi-rdc.aliyuncs.com';
362 + const referer = 'https://codeup.aliyun.com';
363 +
364 + // Step 1: 获取组织列表
365 + const orgs = await new Promise((res, rej) => GM_xmlhttpRequest({
366 + method: 'GET', url: `${domain}/oapi/v1/platform/organizations?perPage=100`,
367 + headers: { 'x-yunxiao-token': token, 'Content-Type': 'application/json', 'Referer': referer, 'Origin': referer },
368 + onload: r => r.status >= 200 && r.status < 300 ? res(JSON.parse(r.responseText)) : rej(new Error(`组织列表失败 ${r.status}`)),
369 + onerror: () => rej(new Error('网络错误')), ontimeout: () => rej(new Error('超时'))
370 + }));
371 +
372 + // Step 2: 获取群组空间
373 + const org = orgs.find(o => o.name.toLowerCase() === ownerPath.split('/')[0].toLowerCase());
374 + if (!org) throw new Error(`未找到组织 ${ownerPath.split('/')[0]}`);
375 +
376 + const namespaces = await new Promise((res, rej) => GM_xmlhttpRequest({
377 + method: 'GET', url: `${domain}/oapi/v1/codeup/organizations/${org.id}/namespaces`,
378 + headers: { 'x-yunxiao-token': token, 'Content-Type': 'application/json', 'Referer': referer, 'Origin': referer },
379 + onload: r => r.status >= 200 && r.status < 300 ? res(JSON.parse(r.responseText)) : rej(new Error(`群组列表失败 ${r.status}`)),
380 + onerror: () => rej(new Error('网络错误')), ontimeout: () => rej(new Error('超时'))
381 + }));
382 +
383 + // Step 3: 匹配群组(webUrl / pathWithNamespace / nameWithNamespace)
384 + const targetNs = namespaces.find(ns => {
385 + const web = (ns.webUrl || '').replace(/https?:\/\/[^\/]+/, '');
386 + const path = ns.pathWithNamespace || ns.nameWithNamespace || '';
387 + return web.includes(ownerPath) || path.toLowerCase() === ownerPath.toLowerCase();
388 + });
389 +
390 + if (!targetNs) {
391 + const paths = namespaces.map(n => n.pathWithNamespace || n.name).join(', ');
392 + throw new Error(`未找到群组 ${ownerPath}\n可用群组:${paths || '无'}`);
393 + }
394 +
395 + console.log('匹配群组成功:', targetNs);
396 + console.log(JSON.stringify({
397 + name: repoName,
398 + path: repoName,
399 + namespaceId: targetNs.id,
400 + readMeType: 'NONE'
401 + }));
402 +
403 + // Step 4: 正确创建仓库(使用 organizationId + namespaceId)
404 + await new Promise((resolve, reject) => {
405 + GM_xmlhttpRequest({
406 + method: 'POST',
407 + url: `${domain}/oapi/v1/codeup/organizations/${org.id}/repositories?createParentPath=true`,
408 + headers: {
409 + 'x-yunxiao-token': token,
410 + 'Content-Type': 'application/json',
411 + 'Referer': referer,
412 + 'Origin': referer
413 + },
414 + data: JSON.stringify({
415 + name: repoName,
416 + path: repoName,
417 + namespaceId: targetNs.id,
418 + }),
419 + onload: r => {
420 + const data = r.responseText ? JSON.parse(r.responseText) : {};
421 + if (r.status >= 200 && r.status < 300 || data.id || data.result?.id) {
422 + console.log('CodeUp 仓库创建成功:', data);
423 + resolve(data);
424 + } else {
425 + reject(new Error(data.message || data.error || `创建失败(${r.status})`));
426 + }
427 + },
428 + onerror: () => reject(new Error('网络错误')),
429 + ontimeout: () => reject(new Error('请求超时'))
430 + });
431 + });
432 +
433 + // 模拟成功响应
434 + resp = { ok: true, status: 200 };
435 + data = { id: 'success' };
436 + }
437 +
438 + // === 统一成功判断 ===
439 + if (
440 + resp.status >= 200 && resp.status < 400 ||
441 + data.id || data.full_name || data.html_url || data.name || data.path ||
442 + (data.message && /already exists|已存在/i.test(data.message))
443 + ) {
444 + btn.textContent = '成功';
445 + btn.classList.add('success');
446 +
447 + // 自动填回 URL
448 + const input = document.querySelector('#url, #project_remote_mirrors_attributes_0_url');
449 + if (input) input.value = info.url;
450 + } else {
451 + throw new Error(data.message || data.error || `HTTP ${resp.status}`);
452 + }
453 +
454 + } catch (e) {
455 + btn.textContent = '失败';
456 + btn.classList.add('error');
457 + console.error('创建仓库失败:', e);
458 + alert(`${p.name} 创建失败:${e.message}`);
459 + }
460 +
461 + // 3秒后恢复
462 + setTimeout(() => {
463 + btn.disabled = false;
464 + btn.textContent = '创建仓库';
465 + btn.className = 'create-repo-btn';
466 + }, 3000);
467 + };
468 +
469 + createTd.appendChild(btn);
470 + } else {
471 + createTd.textContent = '—';
472 + }
473 + // 插入到「打开」列的后面
474 + openTd.parentNode.insertBefore(createTd, openTd.nextSibling);
475 + });
476 + }
477 +
478 + // 创建设置面板
479 + function createSettingsPanel() {
480 + const panel = document.createElement('div');
481 + panel.className = 'mirror-helper-container';
482 + panel.id = 'mirror-settings-panel';
483 +
484 + let html = `
485 + <button class="mirror-helper-close" id="close-settings">×</button>
486 + <div class="mirror-helper-header">镜像仓库设置</div>
487 + `;
488 +
489 + // 为每个平台创建输入框
490 + for (const [key, platform] of Object.entries(platforms)) {
491 + html += `
492 + <div class="mirror-helper-section">
493 + <h4>${platform.name}</h4>
494 + <input type="text" class="mirror-helper-input" id="${key}-username"
495 + placeholder="用户名" value="${GM_getValue(platform.usernameKey, '')}">
496 + <input type="password" class="mirror-helper-input" id="${key}-token"
497 + placeholder="Token" value="${GM_getValue(platform.tokenKey, '')}">
498 + </div>
499 + `;
500 + }
501 +
502 + html += `
503 + <button class="mirror-helper-button" id="save-settings">保存设置</button>
504 + <button class="mirror-helper-button secondary" id="clear-settings">清除设置</button>
505 + <div class="mirror-helper-footer">设置将保存在浏览器本地</div>
506 + `;
507 +
508 + panel.innerHTML = html;
509 + document.body.appendChild(panel);
510 +
511 + // 添加事件监听
512 + document.getElementById('close-settings').addEventListener('click', () => {
513 + panel.remove();
514 + });
515 +
516 + document.getElementById('save-settings').addEventListener('click', () => {
517 + for (const [key, platform] of Object.entries(platforms)) {
518 + const username = document.getElementById(`${key}-username`).value;
519 + const token = document.getElementById(`${key}-token`).value;
520 +
521 + GM_setValue(platform.usernameKey, username);
522 + GM_setValue(platform.tokenKey, token);
523 + }
524 +
525 + alert('设置已保存!');
526 + panel.remove();
527 + location.reload(); // 刷新页面以应用新设置
528 + });
529 +
530 + document.getElementById('clear-settings').addEventListener('click', () => {
531 + if (confirm('确定要清除所有设置吗?')) {
532 + for (const platform of Object.values(platforms)) {
533 + GM_deleteValue(platform.usernameKey);
534 + GM_deleteValue(platform.tokenKey);
535 + }
536 + alert('设置已清除!');
537 + panel.remove();
538 + location.reload();
539 + }
540 + });
541 + }
542 +
543 + // 创建填充按钮
544 + function createFillButton() {
545 + const container = document.createElement('div');
546 + container.className = 'mirror-helper-container';
547 + container.id = 'mirror-fill-container';
548 +
549 + let html = `
550 + <button class="mirror-helper-close" id="close-fill">×</button>
551 + <div class="mirror-helper-header">镜像仓库填充</div>
552 + <div class="mirror-helper-section">
553 + <h4>选择平台</h4>
554 + <select class="mirror-helper-select" id="platform-select">
555 + `;
556 +
557 + // 添加平台选项
558 + for (const [key, platform] of Object.entries(platforms)) {
559 + const username = GM_getValue(platform.usernameKey, '');
560 + const token = GM_getValue(platform.tokenKey, '');
561 + const status = (username && token) ? '✓' : '✗';
562 + html += `<option value="${key}">${platform.name} ${status}</option>`;
563 + }
564 +
565 + html += `
566 + </select>
567 + </div>
568 + <button class="mirror-helper-button" id="fill-mirror">填充镜像仓库</button>
569 + <button class="mirror-helper-button secondary" id="open-settings">设置账号</button>
570 + <div class="mirror-helper-footer">点击填充按钮自动填写表单</div>
571 + `;
572 +
573 + container.innerHTML = html;
574 + document.body.appendChild(container);
575 +
576 + // 添加事件监听
577 + document.getElementById('close-fill').addEventListener('click', () => {
578 + container.remove();
579 + });
580 +
581 + document.getElementById('open-settings').addEventListener('click', () => {
582 + container.remove();
583 + createSettingsPanel();
584 + });
585 +
586 + document.getElementById('fill-mirror').addEventListener('click', () => {
587 + const selectedPlatform = document.getElementById('platform-select').value;
588 + fillMirrorForm(selectedPlatform);
589 + });
590 + }
591 +
592 + // 填充表单
593 + function fillMirrorForm(platformKey) {
594 + const platform = platforms[platformKey];
595 + const username = GM_getValue(platform.usernameKey, '');
596 + const token = GM_getValue(platform.tokenKey, '');
597 +
598 + if (!username || !token) {
599 + alert(`请先设置 ${platform.name} 的用户名和Token!`);
600 + return;
601 + }
602 +
603 + // 获取当前仓库路径
604 + let [, group, project] = window.location.pathname.split('/');
605 + if (!group || !project) {
606 + alert('无法获取仓库路径!');
607 + return;
608 + }
609 +
610 + switch (group) {
611 + case 'idev':
612 + if (platformKey === 'github') {
613 + group = 'idevsig'
614 + }
615 + break;
616 +
617 + case 'tiny':
618 + if (platformKey === 'github' || platformKey === 'gitcode') {
619 + group = 'tinyzen'
620 + }
621 + break;
622 + }
623 +
624 + // 构建镜像URL
625 + const mirrorUrl = `${platform.url}${group}/${project}.git`;
626 +
627 + // 填充表单
628 + document.getElementById('url').value = mirrorUrl;
629 + document.getElementById('project_remote_mirrors_attributes_0_url').value = mirrorUrl;
630 + document.getElementById('project_remote_mirrors_attributes_0_user').value = username;
631 + document.getElementById('project_remote_mirrors_attributes_0_password').value = token;
632 +
633 + // 勾选"仅镜像受保护的分支"
634 + const protectedCheckbox = document.getElementById('only_protected_branches');
635 + if (protectedCheckbox && !protectedCheckbox.checked) {
636 + protectedCheckbox.click();
637 + }
638 +
639 + // 显示成功消息
640 + const successMsg = document.createElement('div');
641 + successMsg.style.cssText = `
642 + position: fixed;
643 + top: 20px;
644 + left: 50%;
645 + transform: translateX(-50%);
646 + background: #4caf50;
647 + color: white;
648 + padding: 12px 24px;
649 + border-radius: 4px;
650 + box-shadow: 0 2px 8px rgba(0,0,0,0.2);
651 + z-index: 10000;
652 + font-size: 14px;
653 + `;
654 + successMsg.textContent = `已填充 ${platform.name} 镜像仓库信息!`;
655 + document.body.appendChild(successMsg);
656 +
657 + setTimeout(() => {
658 + successMsg.remove();
659 + }, 3000);
660 + }
661 +
662 + // 初始化
663 + function init() {
664 + // 添加目标仓库列(适用于镜像列表页面)
665 + if (document.querySelector('.js-mirrors-table-body')) {
666 + // 延迟执行以确保DOM完全加载
667 + setTimeout(() => {
668 + addTargetRepoColumn();
669 + // 监听动态加载的行
670 + const observer = new MutationObserver(() => {
671 + addTargetRepoColumn();
672 + });
673 + observer.observe(document.body, { childList: true, subtree: true });
674 + }, 1000);
675 + }
676 +
677 + // 创建填充按钮(适用于设置页面)
678 + if (document.querySelector('.js-mirror-form')) {
679 + createFillButton();
680 + }
681 +
682 + // 注册菜单命令
683 + GM_registerMenuCommand('镜像仓库设置', createSettingsPanel);
684 + }
685 +
686 + // 页面加载完成后初始化
687 + if (document.readyState === 'loading') {
688 + document.addEventListener('DOMContentLoaded', init);
689 + } else {
690 + init();
691 + }
692 +
693 + // 监听页面导航(SPA应用)
694 + let currentUrl = location.href;
695 + new MutationObserver(() => {
696 + if (location.href !== currentUrl) {
697 + currentUrl = location.href;
698 + setTimeout(init, 500);
699 + }
700 + }).observe(document, { subtree: true, childList: true });
701 + })();

jetsung ревизий этого фрагмента 3 months ago. К ревизии

1 file changed, 1 insertion

gistfile1.txt(файл создан)

@@ -0,0 +1 @@
1 +
Новее Позже