@andrebuzeli/git-mcp 11.0.5 → 12.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -47
- package/package.json +24 -19
- package/src/index.js +84 -0
- package/src/providers/providerManager.js +107 -0
- package/src/tools/git-branches.js +63 -0
- package/src/tools/git-config.js +53 -0
- package/src/tools/git-files.js +41 -0
- package/src/tools/git-history.js +36 -0
- package/src/tools/git-ignore.js +48 -0
- package/src/tools/git-issues.js +58 -12
- package/src/tools/git-pulls.js +61 -0
- package/src/tools/git-remote.js +182 -29
- package/src/tools/git-reset.js +35 -0
- package/src/tools/git-stash.js +57 -0
- package/src/tools/git-sync.js +44 -40
- package/src/tools/git-tags.js +58 -0
- package/src/tools/git-workflow.js +85 -0
- package/src/utils/errors.js +22 -0
- package/src/utils/gitAdapter.js +116 -0
- package/src/utils/providerExec.js +13 -0
- package/src/utils/repoHelpers.js +25 -0
- package/src/utils/retry.js +12 -0
- package/bin/git-mcp.js +0 -21
- package/docs/TOOLS.md +0 -110
- package/mcp.json.template +0 -12
- package/src/local/git.js +0 -14
- package/src/providers/gitea.js +0 -13
- package/src/providers/github.js +0 -13
- package/src/server.js +0 -130
- package/src/tools/git-actions.js +0 -19
- package/src/tools/git-activity.js +0 -28
- package/src/tools/git-admin.js +0 -20
- package/src/tools/git-checks.js +0 -14
- package/src/tools/git-commits.js +0 -34
- package/src/tools/git-contents.js +0 -30
- package/src/tools/git-deployments.js +0 -21
- package/src/tools/git-gists.js +0 -15
- package/src/tools/git-gitdata.js +0 -19
- package/src/tools/git-issues-prs.js +0 -44
- package/src/tools/git-local.js +0 -66
- package/src/tools/git-meta.js +0 -19
- package/src/tools/git-misc.js +0 -21
- package/src/tools/git-orgs.js +0 -26
- package/src/tools/git-packages.js +0 -12
- package/src/tools/git-raw.js +0 -14
- package/src/tools/git-releases.js +0 -17
- package/src/tools/git-repos.js +0 -60
- package/src/tools/git-search.js +0 -18
- package/src/tools/git-user.js +0 -26
- package/src/tools/schema.js +0 -3
- package/src/utils/fs.js +0 -29
- package/src/utils/project.js +0 -7
- package/tests/errors.js +0 -26
- package/tests/full_suite.js +0 -98
- package/tests/run.js +0 -50
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as git from "isomorphic-git";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import http from "isomorphic-git/http/node";
|
|
5
|
+
import { MCPError } from "./errors.js";
|
|
6
|
+
import { getProvidersEnv } from "./repoHelpers.js";
|
|
7
|
+
|
|
8
|
+
export class GitAdapter {
|
|
9
|
+
constructor(providerManager) {
|
|
10
|
+
this.pm = providerManager;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async init(dir, defaultBranch = "main") {
|
|
14
|
+
await git.init({ fs, dir, defaultBranch });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async status(dir) {
|
|
18
|
+
const matrix = await git.statusMatrix({ fs, dir });
|
|
19
|
+
const FILE = 0, HEAD = 1, WORKDIR = 2, STAGE = 3;
|
|
20
|
+
const modified = [], created = [], deleted = [], not_added = [], files = [];
|
|
21
|
+
for (const row of matrix) {
|
|
22
|
+
const f = row[FILE], h = row[HEAD], w = row[WORKDIR], s = row[STAGE];
|
|
23
|
+
if (h === 0 && w === 2 && s === 0) { not_added.push(f); files.push({ path: f, working_dir: "new" }); }
|
|
24
|
+
else if (h === 0 && w === 2 && s === 2) { created.push(f); files.push({ path: f, index: "new", working_dir: "new" }); }
|
|
25
|
+
else if (h === 1 && w === 2 && s === 1) { not_added.push(f); files.push({ path: f, working_dir: "modified" }); }
|
|
26
|
+
else if (h === 1 && w === 2 && s === 2) { modified.push(f); files.push({ path: f, index: "modified", working_dir: "modified" }); }
|
|
27
|
+
else if (h === 1 && w === 0 && s === 1) { not_added.push(f); files.push({ path: f, working_dir: "deleted" }); }
|
|
28
|
+
else if (h === 1 && w === 0 && s === 0) { deleted.push(f); files.push({ path: f, index: "deleted", working_dir: "deleted" }); }
|
|
29
|
+
}
|
|
30
|
+
const current = await this.getCurrentBranch(dir);
|
|
31
|
+
const staged = [...modified, ...created, ...deleted];
|
|
32
|
+
const isClean = modified.length === 0 && created.length === 0 && deleted.length === 0 && not_added.length === 0;
|
|
33
|
+
return { modified, created, deleted, renamed: [], not_added, staged, conflicted: [], current, tracking: null, ahead: 0, behind: 0, isClean, files };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async add(dir, files) {
|
|
37
|
+
for (const fp of files) {
|
|
38
|
+
if (fp === ".") {
|
|
39
|
+
const matrix = await git.statusMatrix({ fs, dir });
|
|
40
|
+
for (const row of matrix) {
|
|
41
|
+
const file = row[0];
|
|
42
|
+
const work = row[2];
|
|
43
|
+
if (work === 2) await git.add({ fs, dir, filepath: file });
|
|
44
|
+
else if (work === 0 && row[1] === 1) await git.remove({ fs, dir, filepath: file });
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
await git.add({ fs, dir, filepath: fp });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async remove(dir, files) {
|
|
53
|
+
for (const fp of files) await git.remove({ fs, dir, filepath: fp });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async getAuthor(dir) {
|
|
57
|
+
const name = await git.getConfig({ fs, dir, path: "user.name" }).catch(() => "");
|
|
58
|
+
const email = await git.getConfig({ fs, dir, path: "user.email" }).catch(() => "");
|
|
59
|
+
if (name && email) return { name, email };
|
|
60
|
+
const ownerGH = await this.pm.getGitHubOwner();
|
|
61
|
+
const ownerGE = await this.pm.getGiteaOwner();
|
|
62
|
+
const who = ownerGH || ownerGE;
|
|
63
|
+
if (who) {
|
|
64
|
+
const inferred = { name: who, email: `${who}@users.noreply` };
|
|
65
|
+
await git.setConfig({ fs, dir, path: "user.name", value: inferred.name }).catch(() => {});
|
|
66
|
+
await git.setConfig({ fs, dir, path: "user.email", value: inferred.email }).catch(() => {});
|
|
67
|
+
return inferred;
|
|
68
|
+
}
|
|
69
|
+
throw new MCPError("AUTHOR_ERROR", "Configure user.name e user.email ou tokens de provider");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async commit(dir, message) {
|
|
73
|
+
const author = await this.getAuthor(dir);
|
|
74
|
+
const sha = await git.commit({ fs, dir, message, author: { name: author.name, email: author.email } });
|
|
75
|
+
return sha;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async getCurrentBranch(dir) {
|
|
79
|
+
return (await git.currentBranch({ fs, dir, fullname: false }).catch(() => "HEAD")) || "HEAD";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async ensureRemotes(dir, { githubUrl, giteaUrl }) {
|
|
83
|
+
const remotes = await git.listRemotes({ fs, dir });
|
|
84
|
+
const names = remotes.map(r => r.remote);
|
|
85
|
+
if (githubUrl && !names.includes("github")) await git.addRemote({ fs, dir, remote: "github", url: githubUrl });
|
|
86
|
+
if (giteaUrl && !names.includes("gitea")) await git.addRemote({ fs, dir, remote: "gitea", url: giteaUrl });
|
|
87
|
+
if (githubUrl && !names.includes("origin")) await git.addRemote({ fs, dir, remote: "origin", url: githubUrl });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getAuth(remoteUrl) {
|
|
91
|
+
const { githubToken, giteaToken } = getProvidersEnv();
|
|
92
|
+
if (remoteUrl.includes("github.com")) {
|
|
93
|
+
if (!githubToken) throw new MCPError("AUTH", "GITHUB_TOKEN ausente");
|
|
94
|
+
return () => ({ username: githubToken, password: "x-oauth-basic" });
|
|
95
|
+
}
|
|
96
|
+
if (giteaToken) return () => ({ username: "git", password: giteaToken });
|
|
97
|
+
throw new MCPError("AUTH", "GITEA_TOKEN ausente");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async pushOne(dir, remote, branch) {
|
|
101
|
+
const remotes = await git.listRemotes({ fs, dir });
|
|
102
|
+
const info = remotes.find(r => r.remote === remote);
|
|
103
|
+
if (!info) throw new MCPError("REMOTE", `Remote '${remote}' não encontrado`);
|
|
104
|
+
const onAuth = this.getAuth(info.url);
|
|
105
|
+
const ref = branch.startsWith("refs/") ? branch : `refs/heads/${branch}`;
|
|
106
|
+
await git.push({ fs, http, dir, remote, ref, remoteRef: ref, onAuth });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async pushParallel(dir, branch) {
|
|
110
|
+
const remotes = await git.listRemotes({ fs, dir });
|
|
111
|
+
const targets = remotes.filter(r => ["github", "gitea"].includes(r.remote));
|
|
112
|
+
if (targets.length === 0) throw new MCPError("REMOTE", "Nenhum remote github/gitea configurado");
|
|
113
|
+
await Promise.all(targets.map(t => this.pushOne(dir, t.remote, branch)));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export async function runBoth(pm, handlers) {
|
|
2
|
+
const out = { github: null, gitea: null };
|
|
3
|
+
const ghOwner = await pm.getGitHubOwner();
|
|
4
|
+
const geOwner = await pm.getGiteaOwner();
|
|
5
|
+
if (pm.github && ghOwner && handlers.github) {
|
|
6
|
+
try { out.github = await handlers.github(ghOwner); } catch (e) { out.github = { ok: false, error: String(e?.message || e) }; }
|
|
7
|
+
}
|
|
8
|
+
if (pm.giteaUrl && pm.giteaToken && geOwner && handlers.gitea) {
|
|
9
|
+
try { out.gitea = await handlers.gitea(geOwner); } catch (e) { out.gitea = { ok: false, error: String(e?.message || e) }; }
|
|
10
|
+
}
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
export function getRepoNameFromPath(projectPath) {
|
|
4
|
+
const base = path.basename(projectPath).trim();
|
|
5
|
+
return base.replace(/\s+/g, "_").replace(/[^A-Za-z0-9_\-]/g, "");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function normalizeProjectFilePath(projectPath, relative) {
|
|
9
|
+
const joined = path.resolve(projectPath, relative || ".");
|
|
10
|
+
return joined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function getEnv(key) {
|
|
14
|
+
const v = process.env[key];
|
|
15
|
+
return v === undefined ? "" : String(v);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getProvidersEnv() {
|
|
19
|
+
return {
|
|
20
|
+
githubToken: getEnv("GITHUB_TOKEN"),
|
|
21
|
+
giteaUrl: getEnv("GITEA_URL"),
|
|
22
|
+
giteaToken: getEnv("GITEA_TOKEN"),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export async function withRetry(fn, { retries = 3, baseDelayMs = 300 } = {}) {
|
|
2
|
+
let attempt = 0;
|
|
3
|
+
let lastErr;
|
|
4
|
+
while (attempt < retries) {
|
|
5
|
+
try { return await fn(); } catch (e) { lastErr = e; }
|
|
6
|
+
const delay = baseDelayMs * Math.pow(2, attempt);
|
|
7
|
+
await new Promise(r => setTimeout(r, delay));
|
|
8
|
+
attempt++;
|
|
9
|
+
}
|
|
10
|
+
throw lastErr;
|
|
11
|
+
}
|
|
12
|
+
|
package/bin/git-mcp.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { initMcpJson, startServer, callOnce } from '../src/server.js'
|
|
3
|
-
|
|
4
|
-
const argv = process.argv.slice(2)
|
|
5
|
-
const cmd = argv[0] || 'start'
|
|
6
|
-
|
|
7
|
-
function getFlag(name) {
|
|
8
|
-
const idx = argv.findIndex(a => a === `--${name}`)
|
|
9
|
-
if (idx >= 0) return argv[idx + 1]
|
|
10
|
-
return undefined
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async function main() {
|
|
14
|
-
if (cmd === 'init') { const pathArg = getFlag('path'); const force = !!getFlag('force'); await initMcpJson(pathArg, force); return }
|
|
15
|
-
if (cmd === 'start') { await startServer(); return }
|
|
16
|
-
if (cmd === 'call') { const tool = getFlag('tool'); const action = getFlag('action'); const projectPath = getFlag('projectPath'); const argsStr = getFlag('args'); const args = argsStr ? JSON.parse(argsStr) : undefined; const res = await callOnce({ tool, action, projectPath, args }); console.log(JSON.stringify(res)); return }
|
|
17
|
-
console.error('Comando inválido')
|
|
18
|
-
process.exit(1)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
main()
|
package/docs/TOOLS.md
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
# Tools e Actions — @andrebuzeli/git-mcp
|
|
2
|
-
|
|
3
|
-
Todas as tools operam com chamadas no formato padrão via stdio:
|
|
4
|
-
|
|
5
|
-
Entrada:
|
|
6
|
-
```
|
|
7
|
-
{ "tool": "<nome>", "action": "<acao>", "projectPath": "<caminho>", "args": { ... } }
|
|
8
|
-
```
|
|
9
|
-
Saída:
|
|
10
|
-
```
|
|
11
|
-
{ "ok": true, "res": <resultado> }
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## Parâmetros
|
|
15
|
-
- `projectPath` (string): caminho completo do projeto no IDE
|
|
16
|
-
- `action` (string): ação dentro da tool
|
|
17
|
-
- `args` (objeto, opcional): parâmetros da ação
|
|
18
|
-
- Nome do projeto é derivado do último segmento de `projectPath` com espaços substituídos por underline
|
|
19
|
-
|
|
20
|
-
## Tools
|
|
21
|
-
|
|
22
|
-
### git-local
|
|
23
|
-
- Ações: `status`, `init`, `add`, `commit`, `push`, `pull`, `branch`, `tag`, `clone`
|
|
24
|
-
- Observações:
|
|
25
|
-
- Independente de Git instalado (usa `isomorphic-git`)
|
|
26
|
-
- `branch`: subactions `list|create|switch` via `args.subaction`
|
|
27
|
-
- `push`/`pull`: aceita `args.remote` ou `args.url` e faz fallback por Contents API
|
|
28
|
-
- Exemplos:
|
|
29
|
-
```
|
|
30
|
-
call --tool git-local --action init --projectPath "C:\Projetos\X"
|
|
31
|
-
call --tool git-local --action add --projectPath "..." --args '{"patterns":["README.md"]}'
|
|
32
|
-
call --tool git-local --action commit --projectPath "..." --args '{"message":"init"}'
|
|
33
|
-
call --tool git-local --action push --projectPath "..." --args '{"branch":"master"}'
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
### git-remote
|
|
37
|
-
- Ações: `createRepo`, `deleteRepo`, `ensureRemotes`
|
|
38
|
-
- Cria/garante repositórios/remotes em Gitea e GitHub simultaneamente
|
|
39
|
-
|
|
40
|
-
### git-sync
|
|
41
|
-
- Ações: `mirror`
|
|
42
|
-
- Sincroniza conteúdo para ambos provedores; tenta push e, se necessário, sobe todos arquivos via Contents API
|
|
43
|
-
|
|
44
|
-
### git-repos
|
|
45
|
-
- Ações: `get`, `patch`, `languages`, `tags`, `topics-get`, `topics-put`, `collaborators`, `subscribers`, `subscription-get`, `subscription-put`, `subscription-delete`, `stargazers`, `forks-get`, `forks-post`, `activities`, `hooks`, `keys`
|
|
46
|
-
|
|
47
|
-
### git-contents
|
|
48
|
-
- Ações: `list`, `create`, `get`
|
|
49
|
-
- `create`: `args.path`, `args.content` (base64), `args.message`, opcional `args.branch`
|
|
50
|
-
|
|
51
|
-
### git-commits
|
|
52
|
-
- Ações: `branches`, `create-branch`, `branch-protections`, `commits`, `refs`
|
|
53
|
-
|
|
54
|
-
### git-issues-prs
|
|
55
|
-
- Ações: `issue-create`, `issue-comment`, `issue-labels`, `labels-list`, `labels-create`, `milestones-list`, `milestones-create`, `pr-create`, `pr-get`
|
|
56
|
-
- Auto-correções: cria labels/branches quando necessário
|
|
57
|
-
|
|
58
|
-
### git-releases
|
|
59
|
-
- Ações: `create`
|
|
60
|
-
- `args`: `tag`, `title`, `body`
|
|
61
|
-
|
|
62
|
-
### git-actions (GitHub)
|
|
63
|
-
- Ações: `workflows`, `runs`, `artifacts`, `runners`, `secrets`
|
|
64
|
-
|
|
65
|
-
### git-activity
|
|
66
|
-
- Ações: `notifications-get`, `notifications-put`, `events`, `feeds`, `starring-put`, `starring-get`, `starring-delete`, `subscription-put`, `subscription-delete`
|
|
67
|
-
|
|
68
|
-
### git-search
|
|
69
|
-
- Ações: `repos`, `users`, `issues`, `repos-gitea`
|
|
70
|
-
|
|
71
|
-
### git-gitdata (GitHub)
|
|
72
|
-
- Ações: `blob-create`, `blob-get`, `tree-create`, `refs-get`
|
|
73
|
-
|
|
74
|
-
### git-checks (GitHub)
|
|
75
|
-
- Ações: `status`
|
|
76
|
-
- `args`: `sha`, `state`, `context`
|
|
77
|
-
|
|
78
|
-
### git-deployments (GitHub)
|
|
79
|
-
- Ações: `create`, `status`
|
|
80
|
-
- `create` detecta automaticamente `ref` (`main`/`master`)
|
|
81
|
-
|
|
82
|
-
### git-misc
|
|
83
|
-
- Ações: `version`, `markdown`, `markdown-raw`, `licenses`, `gitignore-templates`, `label-templates`, `emojis`, `rate_limit`, `zen`
|
|
84
|
-
|
|
85
|
-
### git-gists (GitHub)
|
|
86
|
-
- Ações: `create`, `get`, `star`, `delete`
|
|
87
|
-
|
|
88
|
-
### git-packages (Gitea)
|
|
89
|
-
- Ações: `list`
|
|
90
|
-
|
|
91
|
-
### git-raw (Gitea)
|
|
92
|
-
- Ações: `get`
|
|
93
|
-
|
|
94
|
-
## Integração IDE
|
|
95
|
-
- Configure `mcp.json` com:
|
|
96
|
-
```
|
|
97
|
-
{
|
|
98
|
-
"git-mcp": {
|
|
99
|
-
"command": "npx",
|
|
100
|
-
"type": "stdio",
|
|
101
|
-
"args": ["@andrebuzeli/git-mcp@latest"],
|
|
102
|
-
"env": {
|
|
103
|
-
"GITEA_URL": "",
|
|
104
|
-
"GITEA_TOKEN": "",
|
|
105
|
-
"GITHUB_TOKEN": ""
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
```
|
|
110
|
-
- IDEs (VSCode, Cursor, Trae) podem invocar via stdio enviando o JSON de chamada descrito acima
|
package/mcp.json.template
DELETED
package/src/local/git.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import simpleGit from 'simple-git'
|
|
2
|
-
|
|
3
|
-
export class LocalGit {
|
|
4
|
-
constructor(projectPath) { this.projectPath = projectPath; this.git = simpleGit({ baseDir: projectPath }) }
|
|
5
|
-
async status() { return await this.git.status() }
|
|
6
|
-
async init() { await this.git.init(); return { ok: true } }
|
|
7
|
-
async add(patterns) { await this.git.add(patterns); return { ok: true } }
|
|
8
|
-
async commit(message) { await this.git.commit(message); return { ok: true } }
|
|
9
|
-
async addOrSetRemote(name, url) { const rems = await this.git.getRemotes(true); const exists = rems.find(r => r.name === name); if (exists) { await this.git.remote(['set-url', name, url]) } else { await this.git.addRemote(name, url) } return { ok: true } }
|
|
10
|
-
async push(remote, branch) { await this.git.push(remote, branch); return { ok: true } }
|
|
11
|
-
async pull(remote, branch) { await this.git.pull(remote, branch); return { ok: true } }
|
|
12
|
-
async branch(subaction, args) { if (subaction === 'list') { return await this.git.branchLocal() } if (subaction === 'create') { await this.git.checkoutLocalBranch(args?.name); return { ok: true } } if (subaction === 'switch') { await this.git.checkout(args?.name); return { ok: true } } throw new Error('subaction inválida') }
|
|
13
|
-
async tag(subaction, args) { if (subaction === 'list') { return await this.git.tags() } if (subaction === 'create') { await this.git.addTag(args?.name); return { ok: true } } throw new Error('subaction inválida') }
|
|
14
|
-
}
|
package/src/providers/gitea.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import axios from 'axios'
|
|
2
|
-
|
|
3
|
-
export class Gitea {
|
|
4
|
-
constructor(baseUrl, token) { this.baseUrl = baseUrl; this.token = token; this.http = axios.create({ baseURL: baseUrl + '/api/v1', headers: { Authorization: `token ${token}` } }); this._me = null }
|
|
5
|
-
async me() { if (this._me) return this._me; const r = await this.http.get('/user'); this._me = r.data; return r.data }
|
|
6
|
-
async request(method, endpoint, data) { const r = await this.http.request({ method, url: endpoint, data }); return r.data }
|
|
7
|
-
async createRepo(name, priv, auto) { const r = await this.request('POST', '/user/repos', { name, private: priv, auto_init: auto }); return r }
|
|
8
|
-
async deleteRepo(name) { const me = await this.me(); await this.request('DELETE', `/repos/${me.login}/${name}`); return { ok: true } }
|
|
9
|
-
repoHttpsUrl(name) { return this.me().then(me => `${this.baseUrl}/${me.login}/${name}.git`) }
|
|
10
|
-
async createIssue(name, title, body) { const me = await this.me(); const r = await this.request('POST', `/repos/${me.login}/${name}/issues`, { title, body }); return r }
|
|
11
|
-
async createRelease(name, tag, title, body) { const me = await this.me(); const r = await this.request('POST', `/repos/${me.login}/${name}/releases`, { tag_name: tag, name: title, body }); return r }
|
|
12
|
-
async putContent(name, path, content, message, branch) { const me = await this.me(); const r = await this.request('POST', `/repos/${me.login}/${name}/contents/${path}`, { content, message, branch: branch || 'master' }); return r }
|
|
13
|
-
}
|
package/src/providers/github.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import axios from 'axios'
|
|
2
|
-
|
|
3
|
-
export class Github {
|
|
4
|
-
constructor(token) { this.http = axios.create({ baseURL: 'https://api.github.com', headers: { Authorization: `token ${token}`, Accept: 'application/vnd.github.v3+json', 'User-Agent': 'git-mcp' } }); this._me = null }
|
|
5
|
-
async me() { if (this._me) return this._me; const r = await this.http.get('/user'); this._me = r.data; return r.data }
|
|
6
|
-
async request(method, endpoint, data) { const r = await this.http.request({ method, url: endpoint, data }); return r.data }
|
|
7
|
-
async createRepo(name, priv, auto) { const r = await this.request('POST', '/user/repos', { name, private: priv, auto_init: auto }); return r }
|
|
8
|
-
async deleteRepo(name) { const me = await this.me(); await this.request('DELETE', `/repos/${me.login}/${name}`); return { ok: true } }
|
|
9
|
-
repoHttpsUrl(name) { return this.me().then(me => `https://github.com/${me.login}/${name}.git`) }
|
|
10
|
-
async createIssue(name, title, body) { const me = await this.me(); const r = await this.request('POST', `/repos/${me.login}/${name}/issues`, { title, body }); return r }
|
|
11
|
-
async createRelease(name, tag, title, body) { const me = await this.me(); const r = await this.request('POST', `/repos/${me.login}/${name}/releases`, { tag_name: tag, name: title, body, prerelease: false, draft: false }); return r }
|
|
12
|
-
async putContent(name, path, content, message, branch) { const me = await this.me(); const r = await this.request('PUT', `/repos/${me.login}/${name}/contents/${path}`, { content, message, branch: branch || 'master' }); return r }
|
|
13
|
-
}
|
package/src/server.js
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import fs from 'fs'
|
|
2
|
-
import path from 'path'
|
|
3
|
-
import { fileURLToPath } from 'url'
|
|
4
|
-
import { deriveProjectName } from './utils/project.js'
|
|
5
|
-
|
|
6
|
-
export async function initMcpJson(targetPath, force) {
|
|
7
|
-
const base = targetPath || process.cwd()
|
|
8
|
-
const file = path.join(base, 'mcp.json')
|
|
9
|
-
const templatePath = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'mcp.json.template')
|
|
10
|
-
const tpl = fs.readFileSync(templatePath, 'utf-8')
|
|
11
|
-
if (!force && fs.existsSync(file)) { console.log('mcp.json já existe'); return }
|
|
12
|
-
fs.writeFileSync(file, tpl, 'utf-8')
|
|
13
|
-
console.log('mcp.json criado')
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function getEnv() { return { GITEA_URL: process.env.GITEA_URL, GITEA_TOKEN: process.env.GITEA_TOKEN, GITHUB_TOKEN: process.env.GITHUB_TOKEN } }
|
|
17
|
-
|
|
18
|
-
async function getTool(toolName, env) {
|
|
19
|
-
if (toolName === 'git-local') { const { GitLocalTool } = await import('./tools/git-local.js'); return new GitLocalTool() }
|
|
20
|
-
if (toolName === 'git-remote') { const { GitRemoteTool } = await import('./tools/git-remote.js'); return new GitRemoteTool(env) }
|
|
21
|
-
if (toolName === 'git-sync') { const { GitSyncTool } = await import('./tools/git-sync.js'); return new GitSyncTool(env) }
|
|
22
|
-
if (toolName === 'git-issues') { const { GitIssuesTool } = await import('./tools/git-issues.js'); return new GitIssuesTool(env) }
|
|
23
|
-
if (toolName === 'git-releases') { const { GitReleasesTool } = await import('./tools/git-releases.js'); return new GitReleasesTool(env) }
|
|
24
|
-
if (toolName === 'git-repos') { const { GitReposTool } = await import('./tools/git-repos.js'); return new GitReposTool(env) }
|
|
25
|
-
if (toolName === 'git-contents') { const { GitContentsTool } = await import('./tools/git-contents.js'); return new GitContentsTool(env) }
|
|
26
|
-
if (toolName === 'git-commits') { const { GitCommitsTool } = await import('./tools/git-commits.js'); return new GitCommitsTool(env) }
|
|
27
|
-
if (toolName === 'git-issues-prs') { const { GitIssuesPRsTool } = await import('./tools/git-issues-prs.js'); return new GitIssuesPRsTool(env) }
|
|
28
|
-
if (toolName === 'git-actions') { const { GitActionsTool } = await import('./tools/git-actions.js'); return new GitActionsTool(env) }
|
|
29
|
-
if (toolName === 'git-activity') { const { GitActivityTool } = await import('./tools/git-activity.js'); return new GitActivityTool(env) }
|
|
30
|
-
if (toolName === 'git-admin') { const { GitAdminTool } = await import('./tools/git-admin.js'); return new GitAdminTool(env) }
|
|
31
|
-
if (toolName === 'git-user') { const { GitUserTool } = await import('./tools/git-user.js'); return new GitUserTool(env) }
|
|
32
|
-
if (toolName === 'git-orgs') { const { GitOrgsTool } = await import('./tools/git-orgs.js'); return new GitOrgsTool(env) }
|
|
33
|
-
if (toolName === 'git-gists') { const { GitGistsTool } = await import('./tools/git-gists.js'); return new GitGistsTool(env) }
|
|
34
|
-
if (toolName === 'git-search') { const { GitSearchTool } = await import('./tools/git-search.js'); return new GitSearchTool(env) }
|
|
35
|
-
if (toolName === 'git-gitdata') { const { GitGitDataTool } = await import('./tools/git-gitdata.js'); return new GitGitDataTool(env) }
|
|
36
|
-
if (toolName === 'git-checks') { const { GitChecksTool } = await import('./tools/git-checks.js'); return new GitChecksTool(env) }
|
|
37
|
-
if (toolName === 'git-deployments') { const { GitDeploymentsTool } = await import('./tools/git-deployments.js'); return new GitDeploymentsTool(env) }
|
|
38
|
-
if (toolName === 'git-misc') { const { GitMiscTool } = await import('./tools/git-misc.js'); return new GitMiscTool(env) }
|
|
39
|
-
if (toolName === 'git-packages') { const { GitPackagesTool } = await import('./tools/git-packages.js'); return new GitPackagesTool(env) }
|
|
40
|
-
if (toolName === 'git-raw') { const { GitRawTool } = await import('./tools/git-raw.js'); return new GitRawTool(env) }
|
|
41
|
-
if (toolName === 'git-meta') { const { GitMetaTool } = await import('./tools/git-meta.js'); return new GitMetaTool(env) }
|
|
42
|
-
return null
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function dispatch(req) {
|
|
46
|
-
const { tool, action, projectPath, args } = req
|
|
47
|
-
const name = deriveProjectName(projectPath)
|
|
48
|
-
const inst = await getTool(tool, getEnv())
|
|
49
|
-
if (inst) return await inst.handle(action, projectPath, args, getEnv())
|
|
50
|
-
throw new Error('Tool/action não suportado')
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function writeJsonRpc(id, result, error) {
|
|
54
|
-
const msg = error ? { jsonrpc: '2.0', id, error } : { jsonrpc: '2.0', id, result }
|
|
55
|
-
const json = JSON.stringify(msg)
|
|
56
|
-
const out = `Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n${json}`
|
|
57
|
-
process.stdout.write(out)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function listSchemas() {
|
|
61
|
-
const mod = await import('./tools/git-meta.js')
|
|
62
|
-
const meta = new mod.GitMetaTool(getEnv())
|
|
63
|
-
return await meta.handle('list', process.cwd(), {})
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export async function startServer() {
|
|
67
|
-
process.stdin.setEncoding('utf-8')
|
|
68
|
-
let buf = ''
|
|
69
|
-
process.stdin.resume()
|
|
70
|
-
process.stdin.on('data', async chunk => {
|
|
71
|
-
buf += chunk
|
|
72
|
-
// Try LSP-like framing with Content-Length
|
|
73
|
-
while (true) {
|
|
74
|
-
const headerEnd = buf.indexOf('\r\n\r\n')
|
|
75
|
-
if (headerEnd === -1) break
|
|
76
|
-
const header = buf.slice(0, headerEnd)
|
|
77
|
-
const m = header.match(/Content-Length:\s*(\d+)/i)
|
|
78
|
-
const len = m ? parseInt(m[1], 10) : null
|
|
79
|
-
const bodyStart = headerEnd + 4
|
|
80
|
-
if (len == null) {
|
|
81
|
-
// No content-length, fall back to line-based
|
|
82
|
-
const line = buf.slice(bodyStart)
|
|
83
|
-
buf = ''
|
|
84
|
-
try { const req = JSON.parse(line); await handleRpc(req) } catch { /* ignore */ }
|
|
85
|
-
break
|
|
86
|
-
}
|
|
87
|
-
if (buf.length - bodyStart < len) break
|
|
88
|
-
const body = buf.slice(bodyStart, bodyStart + len)
|
|
89
|
-
buf = buf.slice(bodyStart + len)
|
|
90
|
-
try { const req = JSON.parse(body); await handleRpc(req) } catch (e) { /* ignore parse errors */ }
|
|
91
|
-
}
|
|
92
|
-
})
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async function handleRpc(req) {
|
|
96
|
-
if (req && req.jsonrpc === '2.0' && typeof req.method === 'string') {
|
|
97
|
-
const id = req.id
|
|
98
|
-
const m = req.method
|
|
99
|
-
const p = req.params || {}
|
|
100
|
-
try {
|
|
101
|
-
if (m === 'initialize') {
|
|
102
|
-
const result = { serverInfo: { name: '@andrebuzeli/git-mcp', version: '11.0.4' }, capabilities: { tools: true } }
|
|
103
|
-
writeJsonRpc(id, result)
|
|
104
|
-
} else if (m === 'tools/list' || m === 'listOfferings') {
|
|
105
|
-
let schemas = await listSchemas()
|
|
106
|
-
if (schemas && !Array.isArray(schemas)) { schemas = Object.entries(schemas).map(([name, s]) => ({ name, ...s })) }
|
|
107
|
-
writeJsonRpc(id, { tools: schemas })
|
|
108
|
-
} else if (m === 'tools/call' || m === 'callTool' || m === 'call') {
|
|
109
|
-
const call = {
|
|
110
|
-
tool: p.name || p.tool,
|
|
111
|
-
action: p.action,
|
|
112
|
-
projectPath: p.projectPath || p.path,
|
|
113
|
-
args: p.args || p.arguments || {}
|
|
114
|
-
}
|
|
115
|
-
const res = await dispatch(call)
|
|
116
|
-
writeJsonRpc(id, { result: res })
|
|
117
|
-
} else if (m === 'ping') {
|
|
118
|
-
writeJsonRpc(id, { ok: true })
|
|
119
|
-
} else {
|
|
120
|
-
writeJsonRpc(id, undefined, { code: -32601, message: 'Method not found', data: m })
|
|
121
|
-
}
|
|
122
|
-
} catch (err) {
|
|
123
|
-
writeJsonRpc(id, undefined, { code: -32000, message: err?.message || 'Server error' })
|
|
124
|
-
}
|
|
125
|
-
} else {
|
|
126
|
-
writeJsonRpc(req?.id ?? null, undefined, { code: -32600, message: 'Invalid Request' })
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export async function callOnce(req) { return await dispatch(req) }
|
package/src/tools/git-actions.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { Github } from '../providers/github.js'
|
|
2
|
-
import { makeSchema } from './schema.js'
|
|
3
|
-
|
|
4
|
-
export class GitActionsTool {
|
|
5
|
-
constructor(env) { this.gh = new Github(env.GITHUB_TOKEN) }
|
|
6
|
-
async handle(action, projectPath, args) {
|
|
7
|
-
const me = await this.gh.me(); const name = projectPath.split(/[\\/]/).pop().replace(/\s+/g, '_'); const repo = `${me.login}/${name}`
|
|
8
|
-
if (action === 'workflows') return await this.gh.request('GET', `/repos/${repo}/actions/workflows`)
|
|
9
|
-
if (action === 'runs') return await this.gh.request('GET', `/repos/${repo}/actions/runs`)
|
|
10
|
-
if (action === 'artifacts') return await this.gh.request('GET', `/repos/${repo}/actions/artifacts`)
|
|
11
|
-
if (action === 'runners') return await this.gh.request('GET', `/repos/${repo}/actions/runners`)
|
|
12
|
-
if (action === 'secrets') return await this.gh.request('GET', `/repos/${repo}/actions/secrets`)
|
|
13
|
-
return { error: 'ação inválida' }
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function getSchema() {
|
|
18
|
-
return makeSchema({ name: 'git-actions', description: 'Consultas a GitHub Actions', actions: { workflows: { description: 'Listar workflows' }, runs: { description: 'Listar runs' }, artifacts: { description: 'Listar artifacts' }, runners: { description: 'Listar runners' }, secrets: { description: 'Listar secrets' } } })
|
|
19
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { Gitea } from '../providers/gitea.js'
|
|
2
|
-
import { Github } from '../providers/github.js'
|
|
3
|
-
import { deriveProjectName } from '../utils/project.js'
|
|
4
|
-
import { makeSchema } from './schema.js'
|
|
5
|
-
|
|
6
|
-
export class GitActivityTool {
|
|
7
|
-
constructor(env) { this.providers = { gitea: new Gitea(env.GITEA_URL, env.GITEA_TOKEN), github: new Github(env.GITHUB_TOKEN) } }
|
|
8
|
-
async handle(action, projectPath, args) {
|
|
9
|
-
const name = deriveProjectName(projectPath)
|
|
10
|
-
const meG = await this.providers.gitea.me(); const meH = await this.providers.github.me()
|
|
11
|
-
const gRepo = `${meG.login}/${name}`; const hRepo = `${meH.login}/${name}`
|
|
12
|
-
const par = async (fnG, fnH) => { const r = await Promise.allSettled([ fnG(), fnH() ]); return r.map(x => x.status === 'fulfilled' ? x.value : { error: x.reason?.message }) }
|
|
13
|
-
if (action === 'notifications-get') return await par(() => this.providers.gitea.request('GET', '/notifications'), () => this.providers.github.request('GET', '/notifications'))
|
|
14
|
-
if (action === 'notifications-put') return await par(() => this.providers.gitea.request('PUT', '/notifications', { last_read_at: Date.now() / 1000 }), () => ({ ok: true }))
|
|
15
|
-
if (action === 'events') return await par(() => this.providers.gitea.request('GET', `/repos/${gRepo}/activities/feeds`), () => this.providers.github.request('GET', '/events'))
|
|
16
|
-
if (action === 'feeds') return await this.providers.github.request('GET', '/feeds')
|
|
17
|
-
if (action === 'starring-put') return await this.providers.github.request('PUT', `/user/starred/${hRepo}`)
|
|
18
|
-
if (action === 'starring-get') return await this.providers.github.request('GET', `/user/starred/${hRepo}`)
|
|
19
|
-
if (action === 'starring-delete') return await this.providers.github.request('DELETE', `/user/starred/${hRepo}`)
|
|
20
|
-
if (action === 'subscription-put') return await par(() => this.providers.gitea.request('PUT', `/repos/${gRepo}/subscription`, { subscribed: true }), () => this.providers.github.request('PUT', `/repos/${hRepo}/subscription`, { subscribed: true }))
|
|
21
|
-
if (action === 'subscription-delete') return await par(() => this.providers.gitea.request('DELETE', `/repos/${gRepo}/subscription`), () => this.providers.github.request('DELETE', `/repos/${hRepo}/subscription`))
|
|
22
|
-
return { error: 'ação inválida' }
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function getSchema() {
|
|
27
|
-
return makeSchema({ name: 'git-activity', description: 'Notificações e atividades', actions: { 'notifications-get': { description: 'Obter notificações' }, 'notifications-put': { description: 'Marcar como lidas' }, events: { description: 'Eventos' }, feeds: { description: 'Feeds (GitHub)' }, 'starring-put': { description: 'Estrelas' }, 'starring-get': { description: 'Checar estrela' }, 'starring-delete': { description: 'Remover estrela' }, 'subscription-put': { description: 'Assinar repo' }, 'subscription-delete': { description: 'Cancelar assinatura' } } })
|
|
28
|
-
}
|
package/src/tools/git-admin.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { Gitea } from '../providers/gitea.js'
|
|
2
|
-
import { makeSchema } from './schema.js'
|
|
3
|
-
|
|
4
|
-
export class GitAdminTool {
|
|
5
|
-
constructor(env) { this.gitea = new Gitea(env.GITEA_URL, env.GITEA_TOKEN) }
|
|
6
|
-
async handle(action, projectPath, args) {
|
|
7
|
-
if (action === 'cron') return await this.gitea.request('GET', '/admin/cron')
|
|
8
|
-
if (action === 'emails') return await this.gitea.request('GET', '/admin/emails')
|
|
9
|
-
if (action === 'orgs') return await this.gitea.request('GET', '/admin/orgs')
|
|
10
|
-
if (action === 'users') return await this.gitea.request('GET', '/admin/users')
|
|
11
|
-
if (action === 'user-create') return await this.gitea.request('POST', '/admin/users', { username: args?.username, email: args?.email, password: args?.password || 'Password123!', must_change_password: false })
|
|
12
|
-
if (action === 'user-patch') return await this.gitea.request('PATCH', `/admin/users/${args?.username}`, { full_name: args?.full_name || 'Updated Name', login_name: args?.username, email: args?.email })
|
|
13
|
-
if (action === 'user-delete') return await this.gitea.request('DELETE', `/admin/users/${args?.username}`)
|
|
14
|
-
if (action === 'unadopted') return await this.gitea.request('GET', '/admin/unadopted')
|
|
15
|
-
if (action === 'hooks') return await this.gitea.request('GET', '/admin/hooks')
|
|
16
|
-
return { error: 'ação inválida' }
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function getSchema() { return makeSchema({ name: 'git-admin', description: 'Endpoints de administração (Gitea)', actions: { cron: { description: 'Cron jobs' }, emails: { description: 'Emails' }, orgs: { description: 'Organizações' }, users: { description: 'Usuários' }, 'user-create': { description: 'Criar usuário' }, 'user-patch': { description: 'Atualizar usuário' }, 'user-delete': { description: 'Excluir usuário' }, unadopted: { description: 'Repositórios não adotados' }, hooks: { description: 'Hooks admin' } } }) }
|
package/src/tools/git-checks.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { Github } from '../providers/github.js'
|
|
2
|
-
import { deriveProjectName } from '../utils/project.js'
|
|
3
|
-
import { makeSchema } from './schema.js'
|
|
4
|
-
|
|
5
|
-
export class GitChecksTool {
|
|
6
|
-
constructor(env) { this.gh = new Github(env.GITHUB_TOKEN) }
|
|
7
|
-
async handle(action, projectPath, args) {
|
|
8
|
-
const me = await this.gh.me(); const name = deriveProjectName(projectPath); const repo = `${me.login}/${name}`
|
|
9
|
-
if (action === 'status') return await this.gh.request('POST', `/repos/${repo}/statuses/${args?.sha}`, { state: args?.state || 'success', context: args?.context || 'test' })
|
|
10
|
-
return { error: 'ação inválida' }
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function getSchema() { return makeSchema({ name: 'git-checks', description: 'Statuses/Checks (GitHub)', actions: { status: { description: 'Definir status para SHA' } } }) }
|
package/src/tools/git-commits.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { Gitea } from '../providers/gitea.js'
|
|
2
|
-
import { Github } from '../providers/github.js'
|
|
3
|
-
import { deriveProjectName } from '../utils/project.js'
|
|
4
|
-
import { makeSchema } from './schema.js'
|
|
5
|
-
|
|
6
|
-
export class GitCommitsTool {
|
|
7
|
-
constructor(env) { this.providers = { gitea: new Gitea(env.GITEA_URL, env.GITEA_TOKEN), github: new Github(env.GITHUB_TOKEN) } }
|
|
8
|
-
async handle(action, projectPath, args) {
|
|
9
|
-
const name = deriveProjectName(projectPath)
|
|
10
|
-
const meG = await this.providers.gitea.me(); const meH = await this.providers.github.me()
|
|
11
|
-
const gRepo = `${meG.login}/${name}`; const hRepo = `${meH.login}/${name}`
|
|
12
|
-
const par = async (fnG, fnH) => { const r = await Promise.allSettled([ fnG(), fnH() ]); return r.map(x => x.status === 'fulfilled' ? x.value : { error: x.reason?.message }) }
|
|
13
|
-
if (action === 'branches') return await par(() => this.providers.gitea.request('GET', `/repos/${gRepo}/branches`), () => this.providers.github.request('GET', `/repos/${hRepo}/branches`))
|
|
14
|
-
if (action === 'create-branch') return await par(() => this.providers.gitea.request('POST', `/repos/${gRepo}/branches`, { new_branch_name: args?.name || 'dev', old_branch_name: args?.from || 'main' }), async () => { const ref = await this.providers.github.request('GET', `/repos/${hRepo}/git/ref/heads/${args?.from || 'main'}`); const sha = ref.object.sha; return await this.providers.github.request('POST', `/repos/${hRepo}/git/refs`, { ref: `refs/heads/${args?.name || 'dev'}`, sha }) })
|
|
15
|
-
if (action === 'branch-protections') return await par(() => this.providers.gitea.request('GET', `/repos/${gRepo}/branch_protections`), () => ({ ok: true }))
|
|
16
|
-
if (action === 'commits') return await par(() => this.providers.gitea.request('GET', `/repos/${gRepo}/commits`), () => this.providers.github.request('GET', `/repos/${hRepo}/commits`))
|
|
17
|
-
if (action === 'refs') return await par(() => this.providers.gitea.request('GET', `/repos/${gRepo}/git/refs`), () => this.providers.github.request('GET', `/repos/${hRepo}/git/refs`))
|
|
18
|
-
return { error: 'ação inválida' }
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function getSchema() {
|
|
23
|
-
return makeSchema({
|
|
24
|
-
name: 'git-commits',
|
|
25
|
-
description: 'Branches, commits e refs',
|
|
26
|
-
actions: {
|
|
27
|
-
branches: { description: 'Listar branches' },
|
|
28
|
-
'create-branch': { description: 'Criar branch' },
|
|
29
|
-
'branch-protections': { description: 'Proteções de branch (Gitea)' },
|
|
30
|
-
commits: { description: 'Listar commits' },
|
|
31
|
-
refs: { description: 'Listar refs' }
|
|
32
|
-
}
|
|
33
|
-
})
|
|
34
|
-
}
|