# Directory Structure
```
├── .gitignore
├── bin.ts
├── common
│ ├── errors.ts
│ ├── server.ts
│ ├── types.ts
│ ├── utils.ts
│ └── version.ts
├── Dockerfile
├── index.ts
├── LICENSE
├── operations
│ ├── branches.ts
│ ├── files.ts
│ ├── issues.ts
│ ├── pulls.ts
│ ├── repos.ts
│ └── users.ts
├── package-lock.json
├── package.json
├── README.md
├── README.zh-cn.md
├── smithery.yaml
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
# node_modules
node_modules
# dist
dist
# logs
*.log
# dotenv
.env
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Gitee MCP Server
Let AI operate Gitee repositories/Issues/Pull Requests for you through MCP
[](./package.json)



[](./LICENSE)
[](https://smithery.ai/server/@normal-coder/gitee-mcp-server)
[<img width="380" height="200" src="https://glama.ai/mcp/servers/cck9xigm1d/badge" />](https://glama.ai/mcp/servers/Cck9XigM1d)
---
## Supported AI Operations
| Category | MCP Tool | Description |
|:----:|:----|:----|
| Repository Operations | `create_repository` | Create a Gitee repository |
| | `fork_repository` | Fork a Gitee repository |
| Branch Operations | `create_branch` | Create a new branch in a Gitee repository |
| | `list_branches` | List branches in a Gitee repository |
| | `get_branch` | Get details of a specific branch in a Gitee repository |
| File Operations | `get_file_contents` | Get contents of a file or directory in a Gitee repository |
| | `create_or_update_file` | Create or update a file in a Gitee repository |
| | `push_files` | Push multiple files to a Gitee repository |
| Issue Operations | `create_issue` | Create an Issue in a Gitee repository |
| | `list_issues` | List Issues in a Gitee repository |
| | `get_issue` | Get details of a specific Issue in a Gitee repository |
| | `update_issue` | Update an Issue in a Gitee repository |
| | `add_issue_comment` | Add a comment to an Issue in a Gitee repository |
| Pull Request Operations | `create_pull_request` | Create a Pull Request in a Gitee repository |
| | `list_pull_requests` | List Pull Requests in a Gitee repository |
| | `get_pull_request` | Get details of a specific Pull Request in a Gitee repository |
| | `update_pull_request` | Update a Pull Request in a Gitee repository |
| | `merge_pull_request` | Merge a Pull Request in a Gitee repository |
| User Operations | `get_user` | Get Gitee user information |
| | `get_current_user` | Get authenticated Gitee user information |
## Usage
### Installing via Smithery
To install Gitee MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@normal-coder/gitee-mcp-server):
```bash
npx -y @smithery/cli install @normal-coder/gitee-mcp-server --client claude
```
### Configuration
- `GITEE_API_BASE_URL`: Optional, Gitee OpenAPI Endpoint, default is `https://gitee.com/api/v5`
- `GITEE_PERSONAL_ACCESS_TOKEN`: Required, Gitee account personal access token (PAT), can be obtained from Gitee account settings [Personal Access Tokens](https://gitee.com/profile/personal_access_tokens)
- `DEBUG`: Optional, set to `true` to enable debug logging, default is disabled
### Run MCP Server via NPX
```json
{
"mcpServers": {
"Gitee": {
"command": "npx",
"args": [
"-y",
"gitee-mcp-server"
],
"env": {
"GITEE_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
}
}
}
}
```
### Run MCP Server via Docker Container
1. Get Docker Image
```bash
# Get from DockerHub
docker pull normalcoder/gitee-mcp-server
# Build locally
docker build -t normalcoder/gitee-mcp-server .
```
2. Configure MCP Server
```json
{
"mcpServers": {
"Gitee": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"GITEE_PERSONAL_ACCESS_TOKEN",
"normalcoder/gitee-mcp-server"
],
"env": {
"GITEE_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>"
}
}
}
}
```
## Development Guide
### Install Dependencies
```bash
npm install
```
### Build
```bash
npm run build
```
After successful build, `/dist` will contain the runnable MCP server.
### Run Server
```bash
npm start
```
The MCP server will run on stdio, allowing it to be used as a subprocess by MCP clients.
### Build Docker Image
You can also run the server using Docker:
```bash
docker build -t normalcoder/gitee-mcp-server .
```
Run MCP Server with Docker:
```bash
docker run -e GITEE_PERSONAL_ACCESS_TOKEN=<YOUR_TOKEN> normalcoder/gitee-mcp-server
```
### Debug MCP Server
You can use `@modelcontextprotocol/inspector` for debugging:
Create a `.env` file in the root directory for environment variables:
```.env
GITEE_API_BASE_URL=https://gitee.com/api/v5
GITEE_PERSONAL_ACCESS_TOKEN=<YOUR_TOKEN>
```
Run the debug tool to start the service and web debug interface:
```bash
npx @modelcontextprotocol/inspector npm run start --env-file=.env
```
The project includes a `debug()` function for printing debug information, usage:
```typescript
import { debug } from './common/utils.js';
debug('Message to log');
debug('Message with data:', { key: 'value' });
```
Debug logs are only printed when the `DEBUG` environment variable is set to `true`.
## Dependencies
- `@modelcontextprotocol/sdk`: MCP SDK for server implementation
- `universal-user-agent`: For generating user agent strings
- `zod`: For schema validation
- `zod-to-json-schema`: For converting Zod schemas to JSON schemas
## License
Licensed under MIT License. You are free to use, modify and distribute the software, subject to the terms and conditions of the MIT License. For more details, see the [LICENSE](./LICENSE) file in the project repository.
## Related Links
- [Model Context Protocol](https://modelcontextprotocol.io)
- [Gitee](https://gitee.com)
```
--------------------------------------------------------------------------------
/common/version.ts:
--------------------------------------------------------------------------------
```typescript
export const VERSION = "0.1.0";
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"outDir": "dist",
"rootDir": "."
},
"include": [
"./**/*.ts"
],
"exclude": [
"node_modules"
]
}
```
--------------------------------------------------------------------------------
/bin.ts:
--------------------------------------------------------------------------------
```typescript
#!/usr/bin/env node
import { config } from "dotenv";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import createGiteeMCPServer from "./index.js";
// 加载 .env 文件
config();
async function runServer() {
const server = createGiteeMCPServer();
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Gitee MCP Server running on stdio");
}
runServer().catch((error) => {
console.error("Fatal error in main():", error);
process.exit(1);
});
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "gitee-mcp-server",
"version": "0.1.1",
"description": "MCP Server for using the Gitee API",
"license": "MIT",
"author": {
"name": "诺墨",
"email": "[email protected]",
"url": "https://gitee.com/normalcoder/gitee-mcp-server"
},
"homepage": "https://gitee.com/normalcoder/gitee-mcp-server",
"type": "module",
"bin": {
"mcp-server-gitee": "dist/bin.js"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc && shx chmod +x dist/*.js",
"watch": "tsc --watch",
"start": "node dist/bin.js"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.1",
"dotenv": "^16.4.7",
"universal-user-agent": "^7.0.0",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.22.3"
},
"devDependencies": {
"@types/node": "^20.10.5",
"shx": "^0.3.4",
"typescript": "^5.8.2"
}
}
```
--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------
```yaml
# Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
startCommand:
type: stdio
configSchema:
# JSON Schema defining the configuration options for the MCP.
type: object
required:
- giteePersonalAccessToken
properties:
giteePersonalAccessToken:
type: string
description: Gitee personal access token, required for authentication.
giteeApiBaseUrl:
type: string
default: https://gitee.com/api/v5
description: Optional Gitee API base URL
debug:
type: boolean
description: Enable debug mode
commandFunction:
# A JS function that produces the CLI command based on the given config to start the MCP on stdio.
|-
(config) => ({
command: 'node',
args: ['dist/bin.js'],
env: {
GITEE_PERSONAL_ACCESS_TOKEN: config.giteePersonalAccessToken,
GITEE_API_BASE_URL: config.giteeApiBaseUrl || 'https://gitee.com/api/v5',
DEBUG: config.debug === true ? 'true' : undefined
}
})
exampleConfig:
giteePersonalAccessToken: <YOUR_GITEE_PERSONAL_ACCESS_TOKEN>
giteeApiBaseUrl: https://gitee.com/api/v5
debug: false
```
--------------------------------------------------------------------------------
/operations/users.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
import { giteeRequest, validateOwnerName, getGiteeApiBaseUrl } from "../common/utils.js";
import { GiteeUserSchema } from "../common/types.js";
// Schema definitions
export const GetUserSchema = z.object({
// 用户名
username: z.string().describe("Username"),
});
export const SearchUsersSchema = z.object({
// 搜索关键词
q: z.string().describe("Search keyword"),
// 当前的页码
page: z.number().int().min(1).default(1).optional().describe("Page number"),
// 每页的数量,最大为 100
per_page: z.number().int().min(1).max(100).default(30).optional().describe("Number of items per page, maximum 100"),
// 排序字段
sort: z.enum(["followers", "repositories", "joined"]).default("followers").optional().describe("Sort field"),
// 排序方式
order: z.enum(["desc", "asc"]).default("desc").optional().describe("Sort direction"),
});
// Type exports
export type GetUserOptions = z.infer<typeof GetUserSchema>;
export type SearchUsersOptions = z.infer<typeof SearchUsersSchema>;
// Function implementations
export async function getUser(username: string) {
username = validateOwnerName(username);
const url = `/users/${username}`;
const response = await giteeRequest(url, "GET");
return GiteeUserSchema.parse(response);
}
export async function getCurrentUser() {
const url = "/user";
const response = await giteeRequest(url, "GET");
return GiteeUserSchema.parse(response);
}
export async function searchUsers(options: SearchUsersOptions) {
const { q, page, per_page, sort, order } = options;
const url = new URL(`${getGiteeApiBaseUrl()}/search/users`);
url.searchParams.append("q", q);
if (page !== undefined) {
url.searchParams.append("page", page.toString());
}
if (per_page !== undefined) {
url.searchParams.append("per_page", per_page.toString());
}
if (sort) {
url.searchParams.append("sort", sort);
}
if (order) {
url.searchParams.append("order", order);
}
const response = await giteeRequest(url.toString(), "GET");
return {
total_count: (response as any).total_count || 0,
items: z.array(GiteeUserSchema).parse((response as any).items || []),
};
}
```
--------------------------------------------------------------------------------
/common/server.ts:
--------------------------------------------------------------------------------
```typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { isGiteeError } from "./errors.js";
type MCPServerOptions = {
name: string;
version: string;
};
type ToolDefinition = {
name: string;
description: string;
schema: z.ZodType<any, any, any>;
handler: (params: any) => Promise<any>;
};
export class MCPServer {
private server: Server;
private tools: Map<string, ToolDefinition> = new Map();
constructor(options: MCPServerOptions) {
this.server = new Server(
{
name: options.name,
version: options.version,
},
{
capabilities: {
tools: {},
},
}
);
this.setupRequestHandlers();
}
private setupRequestHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
const toolsList = Array.from(this.tools.values()).map((tool) => ({
name: tool.name,
description: tool.description,
inputSchema: zodToJsonSchema(tool.schema),
}));
return {
tools: toolsList,
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
if (!request.params.arguments) {
throw new Error("Parameters are required.");
}
const tool = this.tools.get(request.params.name);
if (!tool) {
throw new Error(`Unknown tool: ${request.params.name}`);
}
const args = tool.schema.parse(request.params.arguments);
const result = await tool.handler(args);
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(`Invalid input: ${JSON.stringify(error.errors)}`);
}
if (isGiteeError(error)) {
throw error;
}
throw error;
}
});
}
public registerTool(tool: ToolDefinition) {
this.tools.set(tool.name, tool);
}
public async connect(transport: StdioServerTransport) {
await this.server.connect(transport);
}
}
```
--------------------------------------------------------------------------------
/common/errors.ts:
--------------------------------------------------------------------------------
```typescript
export class GiteeError extends Error {
constructor(message: string) {
super(message);
this.name = "GiteeError";
}
}
export class GiteeValidationError extends GiteeError {
response?: unknown;
constructor(message: string, response?: unknown) {
super(message);
this.name = "GiteeValidationError";
this.response = response;
}
}
export class GiteeResourceNotFoundError extends GiteeError {
constructor(message: string) {
super(message);
this.name = "GiteeResourceNotFoundError";
}
}
export class GiteeAuthenticationError extends GiteeError {
constructor(message: string) {
super(message);
this.name = "GiteeAuthenticationError";
}
}
export class GiteePermissionError extends GiteeError {
constructor(message: string) {
super(message);
this.name = "GiteePermissionError";
}
}
export class GiteeRateLimitError extends GiteeError {
resetAt: Date;
constructor(message: string, resetAt: Date) {
super(message);
this.name = "GiteeRateLimitError";
this.resetAt = resetAt;
}
}
export class GiteeConflictError extends GiteeError {
constructor(message: string) {
super(message);
this.name = "GiteeConflictError";
}
}
export function isGiteeError(error: unknown): error is GiteeError {
return error instanceof GiteeError;
}
export function createGiteeError(status: number, responseBody: unknown): GiteeError {
let message = "Gitee API request failed";
let resetAt: Date | undefined;
if (typeof responseBody === "object" && responseBody !== null) {
const body = responseBody as Record<string, unknown>;
if (body.message && typeof body.message === "string") {
message = body.message;
}
if (body.documentation_url && typeof body.documentation_url === "string") {
message += ` - Documentation: ${body.documentation_url}`;
}
}
switch (status) {
case 400:
return new GiteeValidationError(message, responseBody);
case 401:
return new GiteeAuthenticationError(message);
case 403:
return new GiteePermissionError(message);
case 404:
return new GiteeResourceNotFoundError(message);
case 409:
return new GiteeConflictError(message);
case 429:
return new GiteeRateLimitError(
message,
resetAt || new Date(Date.now() + 60 * 1000) // Default: reset after 1 minute
);
default:
return new GiteeError(message);
}
}
```
--------------------------------------------------------------------------------
/operations/repos.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
import { giteeRequest, validateOwnerName, validateRepositoryName, getGiteeApiBaseUrl } from "../common/utils.js";
import { GiteeRepositorySchema } from "../common/types.js";
// Schema definitions
export const CreateRepositorySchema = z.object({
// 仓库名称
name: z.string().describe("Repository name"),
// 仓库描述
description: z.string().optional().describe("Repository description"),
// 主页地址
homepage: z.string().optional().describe("Homepage URL"),
// 是否私有
private: z.boolean().default(false).optional().describe("Whether the repository is private"),
// 是否开启 Issue 功能
has_issues: z.boolean().default(true).optional().describe("Whether to enable Issue functionality"),
// 是否开启 Wiki 功能
has_wiki: z.boolean().default(true).optional().describe("Whether to enable Wiki functionality"),
// 是否自动初始化仓库
auto_init: z.boolean().default(false).optional().describe("Whether to automatically initialize the repository"),
// Git Ignore 模板
gitignore_template: z.string().optional().describe("Git Ignore template"),
// License 模板
license_template: z.string().optional().describe("License template"),
// 仓库路径
path: z.string().optional().describe("Repository path"),
});
export const ForkRepositorySchema = z.object({
// 仓库所属空间地址 (企业、组织或个人的地址 path)
owner: z.string().describe("Repository owner path (enterprise, organization, or personal path)"),
// 仓库路径 (path)
repo: z.string().describe("Repository path"),
// 组织空间地址,不传默认为个人
organization: z.string().optional().describe("Organization path, defaults to personal account if not provided"),
});
// Type exports
export type CreateRepositoryOptions = z.infer<typeof CreateRepositorySchema>;
export type ForkRepositoryOptions = z.infer<typeof ForkRepositorySchema>;
// Function implementations
export async function createRepository(options: CreateRepositoryOptions) {
try {
console.log('Creating repository parameters:', JSON.stringify(options));
const url = "/user/repos";
const response = await giteeRequest(url, "POST", options);
console.log('Create repository response:', JSON.stringify(response));
// Try to parse the response
try {
return GiteeRepositorySchema.parse(response);
} catch (parseError) {
console.error('Failed to parse repository response:', parseError);
// Return the original response to avoid parsing errors
return response;
}
} catch (error) {
console.error('Failed to create repository request:', error);
throw error;
}
}
export async function forkRepository(
owner: string,
repo: string,
organization?: string
) {
owner = validateOwnerName(owner);
repo = validateRepositoryName(repo);
const url = `/repos/${owner}/${repo}/forks`;
const body: Record<string, string> = {};
if (organization) {
body.organization = validateOwnerName(organization);
}
const response = await giteeRequest(url, "POST", body);
return GiteeRepositorySchema.parse(response);
}
```
--------------------------------------------------------------------------------
/operations/branches.ts:
--------------------------------------------------------------------------------
```typescript
import { z } from "zod";
import { giteeRequest, validateBranchName, validateOwnerName, validateRepositoryName, getGiteeApiBaseUrl } from "../common/utils.js";
import { GiteeCompleteBranchSchema, GiteeBranchSchema } from "../common/types.js";
// Schema definitions
export const CreateBranchSchema = z.object({
// 仓库所属空间地址 (企业、组织或个人的地址 path)
owner: z.string().describe("Repository owner path (enterprise, organization, or personal path)"),
// 仓库路径 (path)
repo: z.string().describe("Repository path"),
// 新创建的分支名称
branch_name: z.string().describe("Name for the new branch"),
// 起点名称,默认:master
refs: z.string().default("master").describe("Source reference for the branch, default: master"),
});
export const ListBranchesSchema = z.object({
// 仓库所属空间地址 (企业、组织或个人的地址 path)
owner: z.string().describe("Repository owner path (enterprise, organization, or personal path)"),
// 仓库路径 (path)
repo: z.string().describe("Repository path"),
// 排序字段
sort: z.enum(["name", "updated"]).default("name").optional().describe("Sort field"),
// 排序方向
direction: z.enum(["asc", "desc"]).default("asc").optional().describe("Sort direction"),
// 当前的页码
page: z.number().int().default(1).optional().describe("Page number"),
// 每页的数量,最大为 100
per_page: z.number().int().min(1).max(100).optional().describe("Number of items per page, maximum 100"),
});
export const GetBranchSchema = z.object({
// 仓库所属空间地址 (企业、组织或个人的地址 path)
owner: z.string().describe("Repository owner path (enterprise, organization, or personal path)"),
// 仓库路径 (path)
repo: z.string().describe("Repository path"),
// 分支名称
branch: z.string().describe("Branch name"),
});
// Type exports
export type CreateBranchOptions = z.infer<typeof CreateBranchSchema>;
export type ListBranchesOptions = z.infer<typeof ListBranchesSchema>;
export type GetBranchOptions = z.infer<typeof GetBranchSchema>;
// Function implementations
export async function createBranchFromRef(
owner: string,
repo: string,
branchName: string,
refs: string = "master"
) {
owner = validateOwnerName(owner);
repo = validateRepositoryName(repo);
branchName = validateBranchName(branchName);
const url = `/repos/${owner}/${repo}/branches`;
const body = {
branch_name: branchName,
refs: refs,
};
const response = await giteeRequest(url, "POST", body);
return GiteeBranchSchema.parse(response);
}
export async function listBranches(
owner: string,
repo: string,
sort?: string,
direction?: string,
page?: number,
per_page?: number
) {
owner = validateOwnerName(owner);
repo = validateRepositoryName(repo);
const url = new URL(`${getGiteeApiBaseUrl()}/repos/${owner}/${repo}/branches`);
if (sort) {
url.searchParams.append("sort", sort);
}
if (direction) {
url.searchParams.append("direction", direction);
}
if (page !== undefined) {
url.searchParams.append("page", page.toString());
}
if (per_page !== undefined) {
url.searchParams.append("per_page", per_page.toString());
}
const response = await giteeRequest(url.toString());
return z.array(GiteeBranchSchema).parse(response);
}
export async function getBranch(owner: string, repo: string, branch: string) {
owner = validateOwnerName(owner);
repo = validateRepositoryName(repo);
branch = validateBranchName(branch);
const url = `/repos/${owner}/${repo}/branches/${branch}`;
const response = await giteeRequest(url);
return GiteeCompleteBranchSchema.parse(response);
}
```
--------------------------------------------------------------------------------
/common/utils.ts:
--------------------------------------------------------------------------------
```typescript
import { getUserAgent } from "universal-user-agent";
import { createGiteeError } from "./errors.js";
import { VERSION } from "./version.js";
// Default Gitee API base URL
const DEFAULT_GITEE_API_BASE_URL = "https://gitee.com/api/v5";
/**
* Get the Gitee API base URL from environment variables or use the default
* @returns The Gitee API base URL
*/
export function getGiteeApiBaseUrl(): string {
return process.env.GITEE_API_BASE_URL || DEFAULT_GITEE_API_BASE_URL;
}
type RequestOptions = {
method?: string;
body?: unknown;
headers?: Record<string, string>;
}
async function parseResponseBody(response: Response): Promise<unknown> {
const contentType = response.headers.get("content-type");
if (contentType?.includes("application/json")) {
return response.json();
}
return response.text();
}
export function buildUrl(baseUrl: string, params: Record<string, string | number | undefined>): string {
const url = new URL(baseUrl);
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined) {
url.searchParams.append(key, value.toString());
}
});
return url.toString();
}
const USER_AGENT = `modelcontextprotocol/servers/gitee/v${VERSION} ${getUserAgent()}`;
// Generate the equivalent curl command for debugging.
function generateCurlCommand(url: string, method: string, headers: Record<string, string>, body?: unknown): string {
let curl = `curl -X ${method} "${url}"`;
// Add request headers
Object.entries(headers).forEach(([key, value]) => {
curl += ` -H "${key}: ${value}"`;
});
// Add request body
if (body) {
curl += ` -d '${JSON.stringify(body)}'`;
}
return curl;
}
// debug utility function
export function debug(message: string, data?: unknown): void {
// Only output debug logs if DEBUG environment variable is set
if (process.env.DEBUG !== "true") {
return;
}
if (data !== undefined) {
console.error(`[DEBUG] ${message}`, typeof data === 'object' ? JSON.stringify(data, null, 2) : data);
} else {
console.error(`[DEBUG] ${message}`);
}
}
export async function giteeRequest(
urlPath: string,
method: string = "GET",
body?: unknown,
headers?: Record<string, string>
): Promise<unknown> {
// Check if the URL is already a full URL or a path
let url = urlPath.startsWith("http") ? urlPath : `${getGiteeApiBaseUrl()}${urlPath.startsWith("/") ? urlPath : `/${urlPath}`}`;
const requestHeaders: Record<string, string> = {
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": USER_AGENT,
...headers,
};
if (process.env.GITEE_PERSONAL_ACCESS_TOKEN) {
// The Gitee API uses `access_token` as a query parameter or in the `Authorization` header.
// Method 1: Add to URL Query Parameters
let urlObj = new URL(url);
urlObj.searchParams.append('access_token', process.env.GITEE_PERSONAL_ACCESS_TOKEN);
url = urlObj.toString();
// Method 2: Add to Request Headers (Two methods are tried to increase success rate)
requestHeaders["Authorization"] = `token ${process.env.GITEE_PERSONAL_ACCESS_TOKEN}`;
debug(`Using access token: ${process.env.GITEE_PERSONAL_ACCESS_TOKEN.substring(0, 4)}...`);
} else {
debug(`No access token found in environment variables`);
}
// Print the request
debug(`Request: ${method} ${url}`);
debug(`Headers:`, requestHeaders);
if (body) {
debug(`Body:`, body);
}
// Print the equivalent curl command
const curlCommand = generateCurlCommand(url, method, requestHeaders, body);
debug(`cURL: ${curlCommand}\n`);
const response = await fetch(url, {
method,
headers: requestHeaders,
body: body ? JSON.stringify(body) : undefined,
});
const responseBody = await parseResponseBody(response);
// Print the response
debug(`Response Status: ${response.status} ${response.statusText}`);
debug(`Response Body:`, responseBody);
if (!response.ok) {
throw createGiteeError(response.status, responseBody);
}
return responseBody;
}
export function validateBranchName(branch: string): string {
const sanitized = branch.trim();
if (!sanitized) {
throw new Error("分支名不能为空");
}
if (sanitized.includes("..")) {
throw new Error("分支名不能包含 '..'");
}
if (/[\s~^:?*[\\\]]/.test(sanitized)) {
throw new Error("分支名包含无效字符");
}
if (sanitized.startsWith("/") || sanitized.endsWith("/")) {
throw new Error("分支名不能以 '/' 开头或结尾");
}
if (sanitized.endsWith(".lock")) {
throw new Error("分支名不能以 '.lock' 结尾");
}
return sanitized;
}
export function validateRepositoryName(name: string): string {
const sanitized = name.trim();
if (!sanitized) {
throw new Error("仓库名不能为空");
}
if (!/^[a-zA-Z0-9_.-]+$/.test(sanitized)) {
throw new Error(
"仓库名只能包含字母、数字、连字符、句点和下划线"
);
}
if (sanitized.startsWith(".") || sanitized.endsWith(".")) {
throw new Error("仓库名不能以句点开头或结尾");
}
return sanitized;
}
export function validateOwnerName(owner: string): string {
const sanitized = owner.trim();
if (!sanitized) {
throw new Error("所有者名称不能为空");
}
if (!/^[a-zA-Z0-9][a-zA-Z0-9-]*$/.test(sanitized)) {
throw new Error(
"所有者名称只能包含字母、数字和连字符,且必须以字母或数字开头"
);
}
return sanitized;
}
export async function checkBranchExists(
owner: string,
repo: string,
branch: string
): Promise<boolean> {
try {
await giteeRequest(`/repos/${owner}/${repo}/branches/${branch}`, "GET");
return true;
} catch (error) {
if (error && typeof error === "object" && "name" in error && error.name === "GiteeResourceNotFoundError") {
return false;
}
throw error;
}
}
export async function checkUserExists(username: string): Promise<boolean> {
try {
await giteeRequest(`/users/${username}`, "GET");
return true;
} catch (error) {
if (error && typeof error === "object" && "name" in error && error.name === "GiteeResourceNotFoundError") {
return false;
}
throw error;
}
}
```