Compare commits

..

2 Commits

Author SHA1 Message Date
gitea-actions[bot] 0fe1d769ea chore(release): build 06.13.00 [skip ci] 2026-06-07 18:39:00 +00:00
jmiller 18372c84a7 Merge pull request 'release: manifest distribution fields + update server fix' (#584) from dev into main
Generic: Project CI / Tests (push) Blocked by required conditions
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Project CI / Lint & Validate (push) Successful in 27s
Deploy MokoGitea / deploy (push) Failing after 3m42s
2026-06-07 18:38:10 +00:00
35 changed files with 4022 additions and 682 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
<name>MokoGitea</name>
<org>MokoConsulting</org>
<description>Moko fork of Gitea - adding project board REST API endpoints and custom enhancements</description>
<version>06.12.03</version>
<version>06.13.00</version>
<version-prefix>v1.26.1+MOKO</version-prefix>
<license spdx="GPL-3.0-or-later">GNU General Public License v3</license>
</identity>
+129
View File
@@ -0,0 +1,129 @@
<!-- Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
SPDX-License-Identifier: GPL-3.0-or-later
DEFGROUP: gitea-api-mcp.Documentation
REPO: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp
-->
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Changed
- **Renamed** package from `@mokoconsulting/gitea-api-mcp` to `@mokoconsulting/mokogitea-api-mcp` to distinguish Moko's forked Gitea MCP from upstream
- **Renamed** McpServer name and bin entry to `mokogitea-api-mcp`
## [0.0] - 2026-05-07
### Added
#### User / Auth (3 tools)
- `gitea_me` -- Get the authenticated user info
- `gitea_user_orgs` -- List organizations the authenticated user belongs to
- `gitea_user_repos` -- List repositories owned by the authenticated user
#### Repositories (8 tools)
- `gitea_repo_get` -- Get repository details
- `gitea_repo_create` -- Create a new repository
- `gitea_repo_delete` -- Delete a repository
- `gitea_repo_edit` -- Edit repository settings
- `gitea_repo_fork` -- Fork a repository
- `gitea_repo_search` -- Search repositories
- `gitea_org_repos` -- List repositories in an organization
- `gitea_list_connections` -- List configured Gitea connections
#### File Contents (5 tools)
- `gitea_file_get` -- Get file contents from a repository
- `gitea_dir_get` -- Get directory contents (file listing) from a repository
- `gitea_file_create_or_update` -- Create or update a file in a repository
- `gitea_file_delete` -- Delete a file from a repository
- `gitea_tree_get` -- Get the git tree for a repository (recursive file listing)
#### Branches (4 tools)
- `gitea_branches_list` -- List branches in a repository
- `gitea_branch_get` -- Get a specific branch
- `gitea_branch_create` -- Create a new branch
- `gitea_branch_delete` -- Delete a branch
#### Commits (2 tools)
- `gitea_commits_list` -- List commits in a repository
- `gitea_commit_get` -- Get a specific commit
#### Issues (7 tools)
- `gitea_issues_list` -- List issues in a repository
- `gitea_issue_get` -- Get a single issue by number
- `gitea_issue_create` -- Create a new issue
- `gitea_issue_update` -- Update an issue
- `gitea_issue_comments_list` -- List comments on an issue
- `gitea_issue_comment_create` -- Add a comment to an issue
- `gitea_issue_search` -- Search issues across all repositories
#### Labels (2 tools)
- `gitea_labels_list` -- List labels in a repository
- `gitea_label_create` -- Create a label
#### Milestones (2 tools)
- `gitea_milestones_list` -- List milestones in a repository
- `gitea_milestone_create` -- Create a milestone
#### Pull Requests (6 tools)
- `gitea_pulls_list` -- List pull requests
- `gitea_pull_get` -- Get a single pull request
- `gitea_pull_create` -- Create a pull request
- `gitea_pull_merge` -- Merge a pull request
- `gitea_pull_files` -- List files changed in a pull request
- `gitea_pull_review_create` -- Create a pull request review
#### Releases (5 tools)
- `gitea_releases_list` -- List releases
- `gitea_release_get` -- Get a single release by ID
- `gitea_release_latest` -- Get the latest release
- `gitea_release_create` -- Create a new release
- `gitea_release_delete` -- Delete a release
#### Tags (3 tools)
- `gitea_tags_list` -- List tags
- `gitea_tag_create` -- Create a tag
- `gitea_tag_delete` -- Delete a tag
#### Actions (2 tools)
- `gitea_actions_runs_list` -- List workflow runs for a repository
- `gitea_actions_run_get` -- Get a specific workflow run
#### Organizations (3 tools)
- `gitea_org_get` -- Get organization details
- `gitea_org_teams_list` -- List teams in an organization
- `gitea_org_members_list` -- List members of an organization
#### Users (2 tools)
- `gitea_user_get` -- Get a user profile
- `gitea_users_search` -- Search users
#### Webhooks (2 tools)
- `gitea_webhooks_list` -- List webhooks for a repository
- `gitea_webhook_create` -- Create a webhook
#### Wiki (2 tools)
- `gitea_wiki_pages_list` -- List wiki pages
- `gitea_wiki_page_get` -- Get a wiki page
#### Notifications (2 tools)
- `gitea_notifications_list` -- List notifications for the authenticated user
- `gitea_notifications_read` -- Mark all notifications as read
#### Generic (2 tools)
- `gitea_api_request` -- Make a raw API request to any Gitea v1 endpoint
- `gitea_list_connections` -- List configured Gitea connections
### Infrastructure
- Multi-connection config support via `~/.gitea-api-mcp.json`
- Token-based authentication (Gitea native `Authorization: token` header)
- Built on `node:https` / `node:http` (zero HTTP dependencies)
- MCP SDK v1.12.x with stdio transport
[0.0.1]: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp/releases/tag/v0.0.1
+18
View File
@@ -0,0 +1,18 @@
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --production=false
COPY tsconfig.json ./
COPY src/ ./src/
RUN npx tsc && npm prune --production
EXPOSE 3100
ENV PORT=3100
ENV NODE_ENV=production
# SSE mode by default for Docker deployments
CMD ["node", "dist/sse.js"]
+116
View File
@@ -0,0 +1,116 @@
# MokoGitea MCP Server
A comprehensive [Model Context Protocol](https://modelcontextprotocol.io) server for [Gitea](https://gitea.com) and [MokoGitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea). 120+ tools for repos, issues, PRs, projects, releases, custom fields, statuses, priorities, and manifests.
Works with any Gitea instance. MokoGitea-specific features degrade gracefully on vanilla Gitea.
## Quick Start
### npx (no install)
```bash
GITEA_URL=https://gitea.example.com GITEA_TOKEN=your_token npx @mokoconsulting/mokogitea-mcp
```
### Claude Code
Add to `.claude.json`:
```json
{
"mcpServers": {
"mokogitea": {
"command": "npx",
"args": ["@mokoconsulting/mokogitea-mcp"],
"env": {
"GITEA_URL": "https://gitea.example.com",
"GITEA_TOKEN": "your_token"
}
}
}
}
```
### Docker (SSE mode)
```bash
docker run -p 3100:3100 \
-e GITEA_URL=https://gitea.example.com \
-e GITEA_TOKEN=your_token \
mokoconsulting/mokogitea-mcp
```
Connect MCP client to `http://localhost:3100/sse`.
### Multi-instance config
Create `~/.mcp_mokogitea.json`:
```json
{
"defaultConnection": "production",
"connections": {
"production": { "baseUrl": "https://gitea.example.com", "token": "your_token" },
"dev": { "baseUrl": "https://dev.gitea.example.com", "token": "dev_token" }
}
}
```
## Configuration
| Method | Use Case |
|--------|----------|
| `GITEA_URL` + `GITEA_TOKEN` env vars | Single instance, quick setup |
| `~/.mcp_mokogitea.json` config file | Multiple instances |
| `GITEA_API_MCP_CONFIG` env var | Custom config path |
| `GITEA_INSECURE=true` | Skip TLS verification |
## Tools (120+)
### Repositories
`gitea_repo_create` `gitea_repo_get` `gitea_repo_edit` `gitea_repo_delete` `gitea_repo_search` `gitea_repo_fork` `gitea_repo_generate` `gitea_repo_languages` `gitea_repo_contributors` `gitea_repo_topics` `gitea_repo_topics_set`
### Issues
`gitea_issue_create` (dedup by title) `gitea_issue_get` `gitea_issue_update` `gitea_issues_list` `gitea_issue_search` `gitea_issue_comment_create` `gitea_issue_comments_list` `gitea_issue_labels_set` `gitea_issue_bulk_set_status`
### Pull Requests
`gitea_pull_create` `gitea_pull_get` `gitea_pulls_list` `gitea_pull_merge` `gitea_pull_files` `gitea_pull_review_create`
### Branches and Tags
`gitea_branches_list` `gitea_branch_create` `gitea_branch_delete` `gitea_branch_get` `gitea_tags_list` `gitea_tag_create` `gitea_tag_delete`
### Releases
`gitea_releases_list` `gitea_release_create` `gitea_release_get` `gitea_release_latest` `gitea_release_delete` `gitea_release_asset_upload` `gitea_release_asset_delete`
### Files and Trees
`gitea_file_get` `gitea_file_create_or_update` `gitea_file_delete` `gitea_dir_get` `gitea_tree_get` `gitea_bulk_file_push`
### Projects
`gitea_project_list` `gitea_project_create` `gitea_project_get` `gitea_project_update` `gitea_project_delete` `gitea_project_overview` `gitea_project_columns_list` `gitea_project_column_create` `gitea_project_column_delete` `gitea_project_cards_list` `gitea_project_card_add` `gitea_project_card_move` `gitea_project_card_remove`
### Organizations
`gitea_org_get` `gitea_org_repos` `gitea_org_members_list` `gitea_org_teams_list` `gitea_org_labels_list` `gitea_org_label_create`
### Wiki
`gitea_wiki_pages_list` `gitea_wiki_page_get`
### MokoGitea Extensions
`gitea_manifest_get` `gitea_manifest_update` `gitea_org_custom_fields_list` `gitea_org_custom_field_create` `gitea_org_custom_field_delete` `gitea_issue_custom_fields_get` `gitea_issue_custom_fields_set` `gitea_org_issue_statuses_list` `gitea_issue_set_status` `gitea_org_issue_priorities_list` `gitea_issue_set_priority`
### Admin and Other
`gitea_me` `gitea_users_search` `gitea_user_get` `gitea_notifications_list` `gitea_notifications_read` `gitea_commits_list` `gitea_commit_get` `gitea_compare` `gitea_webhooks_list` `gitea_webhook_create` `gitea_admin_users_list` `gitea_admin_orgs_list` `gitea_admin_cron_list` `gitea_admin_cron_run` `gitea_list_connections`
## SSE Server
For hosted deployments:
```
GET / Server info
GET /sse SSE connection endpoint
POST /message Tool call messages
GET /health Health check
```
## License
GPL-3.0-or-later - [Moko Consulting](https://mokoconsulting.tech)
+13
View File
@@ -0,0 +1,13 @@
{
"defaultConnection": "moko",
"connections": {
"moko": {
"baseUrl": "https://git.mokoconsulting.tech",
"token": "your-gitea-access-token"
},
"github-mirror": {
"baseUrl": "https://gitea.example.com",
"token": "your-other-token"
}
}
}
+1198
View File
File diff suppressed because it is too large Load Diff
+58
View File
@@ -0,0 +1,58 @@
{
"name": "@mokoconsulting/mokogitea-mcp",
"version": "1.1.0",
"description": "MCP server for Gitea and MokoGitea - 120+ tools for repos, issues, PRs, projects, releases, custom fields, statuses, priorities, and manifests",
"type": "module",
"main": "dist/index.js",
"bin": {
"mokogitea-mcp": "dist/index.js",
"mokogitea-mcp-sse": "dist/sse.js"
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"start": "node dist/index.js",
"start:sse": "node dist/sse.js",
"setup": "node scripts/setup.mjs",
"clean": "rm -rf dist/"
},
"keywords": [
"mcp",
"gitea",
"mokogitea",
"model-context-protocol",
"claude",
"ai",
"git",
"self-hosted",
"api",
"devops"
],
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"zod": "^3.24.4"
},
"devDependencies": {
"@types/node": "^22.15.3",
"typescript": "^5.8.3"
},
"engines": {
"node": ">=20.0.0"
},
"license": "GPL-3.0-or-later",
"author": "Moko Consulting <hello@mokoconsulting.tech>",
"homepage": "https://git.mokoconsulting.tech/MokoConsulting/mcp_mokogitea_api",
"repository": {
"type": "git",
"url": "https://git.mokoconsulting.tech/MokoConsulting/mcp_mokogitea_api.git"
},
"files": [
"dist/",
"config.example.json",
"README.md",
"LICENSE"
],
"publishConfig": {
"access": "public"
}
}
+15
View File
@@ -0,0 +1,15 @@
# mcp_mokogitea_api PowerShell Profile
# Source this with: . ./profile.ps1
$env:MCP_ROOT = $PSScriptRoot
$env:TEMP = 'A:\temp'
$env:TMP = 'A:\temp'
function mcp { Set-Location $PSScriptRoot }
function mcp-src { Set-Location (Join-Path $PSScriptRoot 'src') }
function mcp-build { Set-Location $PSScriptRoot; npm run build }
function mcp-dev { Set-Location $PSScriptRoot; npm run dev }
Write-Host "mcp_mokogitea_api profile loaded" -ForegroundColor Cyan
Write-Host " Commands: mcp-build, mcp-dev" -ForegroundColor DarkGray
Write-Host " Navigate: mcp, mcp-src" -ForegroundColor DarkGray
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env node
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
* SPDX-License-Identifier: GPL-3.0-or-later
* BRIEF: Interactive setup — prompts for Gitea connection details
*/
import { createInterface } from 'node:readline/promises';
import { readFile, writeFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { homedir } from 'node:os';
const CONFIG_PATH = resolve(homedir(), '.gitea-api-mcp.json');
const rl = createInterface({ input: process.stdin, output: process.stdout });
async function prompt(q, d) { const a = await rl.question(`${q}${d ? ` [${d}]` : ''}: `); return a.trim() || d || ''; }
async function promptRequired(q) { let a = ''; while (!a) { a = (await rl.question(`${q}: `)).trim(); if (!a) console.log(' Required.'); } return a; }
async function main() {
console.log('\n=== gitea-api-mcp Setup ===\n');
let existing = null;
try { existing = JSON.parse(await readFile(CONFIG_PATH, 'utf-8')); console.log(`Existing: ${Object.keys(existing.connections).join(', ')}\n`); } catch {}
const name = await prompt('Connection name', 'moko');
const baseUrl = await promptRequired('Gitea URL (e.g. https://git.mokoconsulting.tech)');
const token = await promptRequired('Access token (Settings > Applications > Generate Token)');
const insecure = (await prompt('Skip TLS verification? (y/N)', 'N')).toLowerCase() === 'y';
const conn = { baseUrl: baseUrl.replace(/\/+$/, ''), token };
if (insecure) conn.insecure = true;
const config = existing ?? { defaultConnection: name, connections: {} };
config.connections[name] = conn;
if (!existing) config.defaultConnection = name;
else if ((await prompt(`Set "${name}" as default? (y/N)`, 'N')).toLowerCase() === 'y') config.defaultConnection = name;
await writeFile(CONFIG_PATH, JSON.stringify(config, null, '\t') + '\n', 'utf-8');
console.log(`\nConfig written to ${CONFIG_PATH}\n`);
rl.close();
}
main().catch(e => { console.error(e.message); rl.close(); process.exit(1); });
+120
View File
@@ -0,0 +1,120 @@
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: gitea-api-mcp.Client
* INGROUP: gitea-api-mcp
* REPO: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp
* PATH: /src/client.ts
* VERSION: 01.00.00
* BRIEF: HTTP client for Gitea REST API v1
*/
import * as https from 'node:https';
import * as http from 'node:http';
import type { GiteaConnection, ApiResponse } from './types.js';
const API_PREFIX = '/api/v1';
const TIMEOUT_MS = 30_000;
export class GiteaClient {
private readonly base_url: string;
private readonly headers: Record<string, string>;
private readonly insecure: boolean;
constructor(conn: GiteaConnection) {
this.base_url = conn.baseUrl.replace(/\/+$/, '') + API_PREFIX;
this.headers = {
'Authorization': `token ${conn.token}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
};
this.insecure = conn.insecure ?? false;
}
async get(endpoint: string, params?: Record<string, string>): Promise<ApiResponse> {
return this.request(this.buildUrl(endpoint, params), 'GET');
}
async post(endpoint: string, body?: unknown): Promise<ApiResponse> {
return this.request(this.buildUrl(endpoint), 'POST', body);
}
async patch(endpoint: string, body: unknown): Promise<ApiResponse> {
return this.request(this.buildUrl(endpoint), 'PATCH', body);
}
async put(endpoint: string, body: unknown): Promise<ApiResponse> {
return this.request(this.buildUrl(endpoint), 'PUT', body);
}
async delete(endpoint: string): Promise<ApiResponse> {
return this.request(this.buildUrl(endpoint), 'DELETE');
}
private buildUrl(endpoint: string, params?: Record<string, string>): string {
const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
const url = new URL(`${this.base_url}${path}`);
if (params) {
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, value);
}
}
return url.toString();
}
private request(url: string, method: string, body?: unknown): Promise<ApiResponse> {
return new Promise((resolve, reject) => {
const parsed = new URL(url);
const is_https = parsed.protocol === 'https:';
const transport = is_https ? https : http;
const options: https.RequestOptions = {
hostname: parsed.hostname,
port: parsed.port || (is_https ? 443 : 80),
path: parsed.pathname + parsed.search,
method,
headers: { ...this.headers },
timeout: TIMEOUT_MS,
};
if (this.insecure && is_https) {
options.rejectUnauthorized = false;
}
const payload = body !== undefined ? JSON.stringify(body) : undefined;
if (payload) {
(options.headers as Record<string, string>)['Content-Length'] = Buffer.byteLength(payload).toString();
}
const req = transport.request(options, (res) => {
const chunks: Buffer[] = [];
res.on('data', (chunk: Buffer) => chunks.push(chunk));
res.on('end', () => {
const raw = Buffer.concat(chunks).toString('utf-8');
let data: unknown;
try {
data = JSON.parse(raw);
} catch {
data = raw;
}
resolve({ status: res.statusCode ?? 0, data });
});
});
req.on('error', (err) => reject(err));
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timed out'));
});
if (payload) {
req.write(payload);
}
req.end();
});
}
}
+61
View File
@@ -0,0 +1,61 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
import { readFile } from 'node:fs/promises';
import { resolve } from 'node:path';
import { homedir } from 'node:os';
import type { GiteaConfig, GiteaConnection } from './types.js';
const CONFIG_FILENAME = '.mcp_mokogitea.json';
export async function loadConfig(): Promise<GiteaConfig> {
// Priority 1: Environment variables (zero-config single instance)
if (process.env.GITEA_URL && process.env.GITEA_TOKEN) {
const conn: GiteaConnection = {
baseUrl: process.env.GITEA_URL,
token: process.env.GITEA_TOKEN,
insecure: process.env.GITEA_INSECURE === 'true',
};
return {
connections: { default: conn },
defaultConnection: 'default',
};
}
// Priority 2: Config file
const config_path = process.env.GITEA_API_MCP_CONFIG
? resolve(process.env.GITEA_API_MCP_CONFIG)
: resolve(homedir(), CONFIG_FILENAME);
try {
const raw = await readFile(config_path, 'utf-8');
const parsed = JSON.parse(raw) as Partial<GiteaConfig>;
if (!parsed.connections || Object.keys(parsed.connections).length === 0) {
throw new Error('No connections defined in config');
}
return {
connections: parsed.connections,
defaultConnection: parsed.defaultConnection ?? Object.keys(parsed.connections)[0],
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new Error(
`Failed to load config from ${config_path}: ${message}\n` +
`Option 1: Set GITEA_URL and GITEA_TOKEN environment variables\n` +
`Option 2: Create ${config_path} - see config.example.json for format`,
);
}
}
export function getConnection(config: GiteaConfig, name?: string): GiteaConnection {
const key = name ?? config.defaultConnection;
const conn = config.connections[key];
if (!conn) {
throw new Error(
`Connection "${key}" not found. Available: ${Object.keys(config.connections).join(', ')}`,
);
}
return conn;
}
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
//
// Creates a configured MCP server instance for use by both stdio and SSE transports.
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { GiteaConfig } from './types.js';
// Import index.ts to register all tools on its exported `server` singleton,
// then re-export a factory that initializes config and returns the server.
import { server, initConfig } from './index.js';
export function createMcpServer(cfg: GiteaConfig): McpServer {
initConfig(cfg);
return server;
}
+100
View File
@@ -0,0 +1,100 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
//
// SSE transport entry point for MokoGitea MCP server.
// Run with: node dist/sse.js
// Or: GITEA_URL=https://gitea.example.com GITEA_TOKEN=xxx node dist/sse.js
//
// Listens on PORT (default 3100) and serves SSE at /sse with POST at /message.
import { createServer } from 'node:http';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { createMcpServer } from './server.js';
import { loadConfig } from './config.js';
const PORT = parseInt(process.env.PORT ?? '3100', 10);
async function main(): Promise<void> {
const config = await loadConfig();
const transports = new Map<string, SSEServerTransport>();
const httpServer = createServer(async (req, res) => {
// CORS headers for browser clients
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
// Health check
if (req.url === '/health') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', tools: 120 }));
return;
}
// SSE endpoint - client connects here
if (req.url === '/sse' && req.method === 'GET') {
const transport = new SSEServerTransport('/message', res);
const sessionId = transport.sessionId;
transports.set(sessionId, transport);
const server = createMcpServer(config);
await server.connect(transport);
req.on('close', () => {
transports.delete(sessionId);
});
return;
}
// Message endpoint - client sends tool calls here
if (req.url?.startsWith('/message') && req.method === 'POST') {
const url = new URL(req.url, `http://${req.headers.host}`);
const sessionId = url.searchParams.get('sessionId');
if (!sessionId || !transports.has(sessionId)) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Invalid or missing sessionId' }));
return;
}
const transport = transports.get(sessionId)!;
await transport.handlePostMessage(req, res);
return;
}
// Root - info page
if (req.url === '/' || req.url === '') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
name: '@mokoconsulting/mokogitea-mcp',
version: '1.1.0',
description: 'MCP server for Gitea and MokoGitea - 120+ tools',
endpoints: {
sse: '/sse',
message: '/message',
health: '/health',
},
docs: 'https://git.mokoconsulting.tech/MokoConsulting/mcp_mokogitea_api',
}));
return;
}
res.writeHead(404);
res.end('Not found');
});
httpServer.listen(PORT, () => {
process.stderr.write(`MokoGitea MCP SSE server listening on port ${PORT}\n`);
process.stderr.write(` SSE: http://localhost:${PORT}/sse\n`);
process.stderr.write(` Health: http://localhost:${PORT}/health\n`);
});
}
main().catch((err) => {
process.stderr.write(`Fatal: ${err}\n`);
process.exit(1);
});
+37
View File
@@ -0,0 +1,37 @@
/* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: gitea-api-mcp.Types
* INGROUP: gitea-api-mcp
* REPO: https://git.mokoconsulting.tech/MokoConsulting/gitea-api-mcp
* PATH: /src/types.ts
* VERSION: 01.00.00
* BRIEF: TypeScript type definitions for Gitea API MCP server
*/
export interface GiteaConnection {
baseUrl: string;
token: string;
/** Skip TLS certificate verification (self-signed certs) */
insecure?: boolean;
}
export interface GitHubBackupConfig {
token: string;
org: string;
}
export interface GiteaConfig {
connections: Record<string, GiteaConnection>;
defaultConnection: string;
github?: GitHubBackupConfig;
}
export interface ApiResponse {
status: number;
data: unknown;
}
+19
View File
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
+1 -1
View File
@@ -5,7 +5,7 @@
# FILE INFORMATION
# DEFGROUP: Gitea.Workflow
# INGROUP: mokoplatform.Automation
# VERSION: 06.12.03
# VERSION: 06.13.00
# BRIEF: Auto-create feature branch when an issue is opened
name: "Universal: Issue Branch"
-27
View File
@@ -2,22 +2,6 @@
All notable changes to MokoGitea are documented here. Versions follow the format
`v{upstream}-moko.{major}.{minor}` (e.g. `v1.26.1-moko.06.03`).
## [Unreleased]
* FEATURES
* feat(api): issue status/priority/type exposed in REST API - GET/PATCH on issues now includes status_id, priority_id, type_id with resolved names
* feat(api): org-level issue metadata endpoints - GET /orgs/{org}/issue-statuses, /issue-priorities, /issue-types
* feat(wiki): org wiki tab - inline wiki rendering from convention repos (wiki / wiki-private)
* feat(wiki): public/private wiki toggle dropdown (same UX as org profile README selector)
* feat(wiki): external wiki support - link to an outside URL from the org wiki tab
* feat(settings): wiki mode setting in org settings (internal repos vs external URL)
* feat(mcp): 5 new MCP tools - gitea_org_issue_statuses_list, gitea_org_issue_priorities_list, gitea_org_issue_types_list, gitea_issue_set_status, gitea_issue_set_priority
* feat(mcp): gitea_issue_create and gitea_issue_update now accept status_id, priority_id, type_id
* MIGRATIONS
* migration 354: add wiki_mode and wiki_url columns to user table for org wiki settings
## [v1.26.1-moko.06.12] - 2026-06-07
* FEATURES
@@ -227,14 +211,3 @@ All notable changes to MokoGitea are documented here. Versions follow the format
* PROCESS
* Created `type: bug` and `upstream` labels for automated issue tracking
* Closed 24 upstream bug/security issues after backporting
## [v1.26.1-moko.03] - 2026-05-15
* FEATURES
* feat(api): Bulk issue operations — add/remove/replace labels, close/reopen, set milestone, assignees (#21)
* INFRASTRUCTURE
* Grafana: Standardized kiosk header across all 14 playlist dashboards
* PROCESS
* Reopened 9 closed issues lacking documented testing proof
* Created `pending: testing` label for features awaiting verification
* Established policy: issues must not be closed without documented testing proof
Submodule mcp-mokogitea-api deleted from c9eb6cfc89
-1
View File
@@ -431,7 +431,6 @@ func prepareMigrationTasks() []*migration {
newMigration(351, "Add CDN public flag to attachments", v1_27.AddAttachmentCDNPublic),
newMigration(352, "Add version prefix and element name to repo manifest", v1_27.AddManifestVersionPrefixAndElement),
newMigration(353, "Add distribution metadata fields to repo manifest", v1_27.AddManifestDistributionFields),
newMigration(354, "Add org wiki settings to user table", v1_27.AddOrgWikiSettings),
}
return preparedMigrations
}
-16
View File
@@ -1,16 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import "xorm.io/xorm"
// AddOrgWikiSettings adds wiki_mode and wiki_url columns to the user table
// for configuring org-level wiki behavior (internal convention repos vs external link).
func AddOrgWikiSettings(x *xorm.Engine) error {
type User struct {
WikiMode string `xorm:"VARCHAR(20) NOT NULL DEFAULT '' 'wiki_mode'"`
WikiURL string `xorm:"TEXT 'wiki_url'"`
}
return x.Sync(new(User))
}
-2
View File
@@ -153,8 +153,6 @@ type User struct {
Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"`
RepoAdminChangeTeamAccess bool `xorm:"NOT NULL DEFAULT false"`
ParentOrgID int64 `xorm:"INDEX DEFAULT 0"` // 0 = no parent (top-level org)
WikiMode string `xorm:"VARCHAR(20) NOT NULL DEFAULT '' 'wiki_mode'"` // "" = internal (convention repos), "external" = link to WikiURL
WikiURL string `xorm:"TEXT 'wiki_url'"` // external wiki URL (used when WikiMode == "external")
// Preferences
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
-49
View File
@@ -84,14 +84,6 @@ type Issue struct {
PinOrder int `json:"pin_order"`
// The version of the issue content for optimistic locking
ContentVersion int `json:"content_version"`
// Issue metadata (org-level definitions)
StatusID int64 `json:"status_id"`
StatusName string `json:"status_name"`
PriorityID int64 `json:"priority_id"`
PriorityName string `json:"priority_name"`
TypeID int64 `json:"type_id"`
TypeName string `json:"type_name"`
}
// CreateIssueOption options to create one issue
@@ -114,10 +106,6 @@ type CreateIssueOption struct {
Closed bool `json:"closed"`
// custom field values keyed by field name
CustomFields map[string]string `json:"custom_fields,omitempty"`
// org-level issue metadata IDs
StatusID *int64 `json:"status_id,omitempty"`
PriorityID *int64 `json:"priority_id,omitempty"`
TypeID *int64 `json:"type_id,omitempty"`
}
// EditIssueOption options for editing an issue
@@ -137,10 +125,6 @@ type EditIssueOption struct {
RemoveDeadline *bool `json:"unset_due_date"`
// The current version of the issue content to detect conflicts during editing
ContentVersion *int `json:"content_version"`
// org-level issue metadata IDs
StatusID *int64 `json:"status_id,omitempty"`
PriorityID *int64 `json:"priority_id,omitempty"`
TypeID *int64 `json:"type_id,omitempty"`
}
// EditDeadlineOption options for creating a deadline
@@ -157,39 +141,6 @@ type IssueDeadline struct {
Deadline *time.Time `json:"due_date"`
}
// IssueStatusDef represents an org-level issue status definition
// swagger:model
type IssueStatusDef struct {
ID int64 `json:"id"`
Name string `json:"name"`
Color string `json:"color"`
Description string `json:"description"`
ClosesIssue bool `json:"closes_issue"`
SortOrder int `json:"sort_order"`
}
// IssuePriorityDef represents an org-level issue priority definition
// swagger:model
type IssuePriorityDef struct {
ID int64 `json:"id"`
Name string `json:"name"`
Color string `json:"color"`
Description string `json:"description"`
SortOrder int `json:"sort_order"`
IsDefault bool `json:"is_default"`
}
// IssueTypeDef represents an org-level issue type definition
// swagger:model
type IssueTypeDef struct {
ID int64 `json:"id"`
Name string `json:"name"`
Color string `json:"color"`
Description string `json:"description"`
SortOrder int `json:"sort_order"`
IsDefault bool `json:"is_default"`
}
// IssueFormFieldType defines issue form field type, can be "markdown", "textarea", "input", "dropdown" or "checkboxes"
//
// swagger:enum IssueFormFieldType
-3
View File
@@ -1773,9 +1773,6 @@ func Routes() *web.Router {
m.Post("", reqToken(), reqOrgOwnership(), org.CreateOrgCustomField)
m.Delete("/{id}", reqToken(), reqOrgOwnership(), org.DeleteOrgCustomField)
})
m.Get("/issue-statuses", org.ListIssueStatuses)
m.Get("/issue-priorities", org.ListIssuePriorities)
m.Get("/issue-types", org.ListIssueTypes)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly())
m.Group("/teams/{teamid}", func() {
m.Combo("").Get(reqToken(), org.GetTeam).
-138
View File
@@ -1,138 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package org
import (
"net/http"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
api "code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
// ListIssueStatuses returns active issue status definitions for an org.
func ListIssueStatuses(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/issue-statuses organization orgListIssueStatuses
// ---
// summary: List an organization's issue status definitions
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// description: "IssueStatusDefList"
// schema:
// type: array
// items:
// "$ref": "#/definitions/IssueStatusDef"
// "404":
// "$ref": "#/responses/notFound"
defs, err := issues_model.GetIssueStatusDefsByOrg(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
result := make([]*api.IssueStatusDef, 0, len(defs))
for _, d := range defs {
result = append(result, &api.IssueStatusDef{
ID: d.ID,
Name: d.Name,
Color: d.Color,
Description: d.Description,
ClosesIssue: d.ClosesIssue,
SortOrder: d.SortOrder,
})
}
ctx.JSON(http.StatusOK, result)
}
// ListIssuePriorities returns active issue priority definitions for an org.
func ListIssuePriorities(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/issue-priorities organization orgListIssuePriorities
// ---
// summary: List an organization's issue priority definitions
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// description: "IssuePriorityDefList"
// schema:
// type: array
// items:
// "$ref": "#/definitions/IssuePriorityDef"
// "404":
// "$ref": "#/responses/notFound"
defs, err := issues_model.GetIssuePriorityDefsByOrg(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
result := make([]*api.IssuePriorityDef, 0, len(defs))
for _, d := range defs {
result = append(result, &api.IssuePriorityDef{
ID: d.ID,
Name: d.Name,
Color: d.Color,
Description: d.Description,
SortOrder: d.SortOrder,
IsDefault: d.IsDefault,
})
}
ctx.JSON(http.StatusOK, result)
}
// ListIssueTypes returns active issue type definitions for an org.
func ListIssueTypes(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/issue-types organization orgListIssueTypes
// ---
// summary: List an organization's issue type definitions
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// description: "IssueTypeDefList"
// schema:
// type: array
// items:
// "$ref": "#/definitions/IssueTypeDef"
// "404":
// "$ref": "#/responses/notFound"
defs, err := issues_model.GetIssueTypeDefsByOrg(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
result := make([]*api.IssueTypeDef, 0, len(defs))
for _, d := range defs {
result = append(result, &api.IssueTypeDef{
ID: d.ID,
Name: d.Name,
Color: d.Color,
Description: d.Description,
SortOrder: d.SortOrder,
IsDefault: d.IsDefault,
})
}
ctx.JSON(http.StatusOK, result)
}
-40
View File
@@ -756,26 +756,6 @@ func CreateIssue(ctx *context.APIContext) {
}
}
// Set org-level issue metadata (status/priority/type) if provided
if form.StatusID != nil && *form.StatusID > 0 {
if err := issues_model.SetIssueStatusID(ctx, issue.ID, *form.StatusID); err != nil {
ctx.APIErrorInternal(err)
return
}
}
if form.PriorityID != nil && *form.PriorityID > 0 {
if err := issues_model.SetIssuePriorityID(ctx, issue.ID, *form.PriorityID); err != nil {
ctx.APIErrorInternal(err)
return
}
}
if form.TypeID != nil && *form.TypeID > 0 {
if err := issues_model.SetIssueTypeID(ctx, issue.ID, *form.TypeID); err != nil {
ctx.APIErrorInternal(err)
return
}
}
if form.Closed {
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
if issues_model.IsErrDependenciesLeft(err) {
@@ -1000,26 +980,6 @@ func EditIssue(ctx *context.APIContext) {
}
}
// Update org-level issue metadata (status/priority/type)
if canWrite && form.StatusID != nil {
if err := issues_model.SetIssueStatusID(ctx, issue.ID, *form.StatusID); err != nil {
ctx.APIErrorInternal(err)
return
}
}
if canWrite && form.PriorityID != nil {
if err := issues_model.SetIssuePriorityID(ctx, issue.ID, *form.PriorityID); err != nil {
ctx.APIErrorInternal(err)
return
}
}
if canWrite && form.TypeID != nil {
if err := issues_model.SetIssueTypeID(ctx, issue.ID, *form.TypeID); err != nil {
ctx.APIErrorInternal(err)
return
}
}
// Refetch from database to assign some automatic values
issue, err = issues_model.GetIssueByID(ctx, issue.ID)
if err != nil {
-11
View File
@@ -126,17 +126,6 @@ func SettingsPost(ctx *context.Context) {
}
}
// Save wiki mode settings.
wikiMode := ctx.FormString("wiki_mode")
wikiURL := ctx.FormString("wiki_url")
orgUser := org.AsUser()
orgUser.WikiMode = wikiMode
orgUser.WikiURL = wikiURL
if err := user_model.UpdateUserCols(ctx, orgUser, "wiki_mode", "wiki_url"); err != nil {
ctx.ServerError("UpdateUserCols(wiki)", err)
return
}
log.Trace("Organization setting updated: %s", org.Name)
ctx.Flash.Success(ctx.Tr("org.settings.update_setting_success"))
ctx.Redirect(ctx.Org.OrgLink + "/settings")
-211
View File
@@ -1,211 +0,0 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package org
import (
"net/http"
"path"
"strings"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/renderhelper"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/gitrepo"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/markup/markdown"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/util"
shared_user "code.mokoconsulting.tech/MokoConsulting/MokoGitea/routers/web/shared/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
const tplOrgWiki templates.TplName = "org/wiki/view"
// OrgWikiPage represents a single page in the org wiki sidebar.
type OrgWikiPage struct {
Name string
SubURL string
}
// Wiki renders the org wiki tab.
func Wiki(ctx *context.Context) {
org := ctx.Org.Organization
if _, err := shared_user.RenderUserOrgHeader(ctx); err != nil {
ctx.ServerError("RenderUserOrgHeader", err)
return
}
ctx.Data["PageIsViewWiki"] = true
ctx.Data["Title"] = org.DisplayName() + " - Wiki"
// Determine which wiki repo to use (public vs member).
viewAs := ctx.FormString("view_as", util.Iif(ctx.Org.IsMember, "member", "public"))
viewAsMember := viewAs == "member"
wikiRepo, commit := findOrgWikiCommit(ctx, org.ID, util.Iif(viewAsMember, shared_user.RepoNameWikiPrivate, shared_user.RepoNameWikiPublic))
if wikiRepo == nil && viewAsMember {
// Fall back to public wiki if member wiki doesn't exist.
wikiRepo, commit = findOrgWikiCommit(ctx, org.ID, shared_user.RepoNameWikiPublic)
viewAsMember = false
}
if wikiRepo == nil && !viewAsMember {
// Fall back to member wiki if public wiki doesn't exist.
wikiRepo, commit = findOrgWikiCommit(ctx, org.ID, shared_user.RepoNameWikiPrivate)
viewAsMember = true
}
ctx.Data["IsViewingWikiAsMember"] = viewAsMember
// Check whether both repos exist (for the dropdown toggle).
publicExists := shared_user.OrgWikiRepoExists(ctx, org.ID, shared_user.RepoNameWikiPublic)
privateExists := shared_user.OrgWikiRepoExists(ctx, org.ID, shared_user.RepoNameWikiPrivate)
ctx.Data["ShowWikiViewSelector"] = publicExists && privateExists && ctx.Org.IsMember
if wikiRepo == nil || commit == nil {
ctx.Data["WikiEmpty"] = true
ctx.HTML(http.StatusOK, tplOrgWiki)
return
}
ctx.Data["WikiRepoLink"] = wikiRepo.Link()
// Build page list from repo root.
entries, err := commit.ListEntries()
if err != nil {
ctx.ServerError("ListEntries", err)
return
}
pages := make([]OrgWikiPage, 0, len(entries))
for _, entry := range entries {
if !entry.IsRegular() {
continue
}
name := entry.Name()
if !isMarkdownFile(name) {
continue
}
displayName := strings.TrimSuffix(name, path.Ext(name))
if strings.EqualFold(displayName, "_sidebar") || strings.EqualFold(displayName, "_footer") {
continue
}
pages = append(pages, OrgWikiPage{
Name: displayName,
SubURL: displayName,
})
}
ctx.Data["Pages"] = pages
// Determine which page to render.
pageName := ctx.PathParamRaw("*")
if pageName == "" {
pageName = "Home"
}
ctx.Data["CurrentPage"] = pageName
// Try to find the file: exact match, then with .md extension.
blob := findWikiBlob(commit, pageName)
if blob == nil {
// Page not found — show empty state with page list.
ctx.Data["WikiPageNotFound"] = true
ctx.HTML(http.StatusOK, tplOrgWiki)
return
}
content, err := blob.GetBlobContent(setting.UI.MaxDisplayFileSize)
if err != nil {
ctx.ServerError("GetBlobContent", err)
return
}
rctx := renderhelper.NewRenderContextRepoFile(ctx, wikiRepo, renderhelper.RepoFileOptions{
CurrentRefPath: path.Join("branch", util.PathEscapeSegments(wikiRepo.DefaultBranch)),
})
renderedContent, err := markdown.RenderString(rctx, content)
if err != nil {
log.Error("Failed to render org wiki page %q: %v", pageName, err)
ctx.ServerError("RenderString", err)
return
}
ctx.Data["WikiContent"] = renderedContent
// Render _Sidebar if it exists.
sidebarBlob := findWikiBlob(commit, "_Sidebar")
if sidebarBlob != nil {
sidebarContent, err := sidebarBlob.GetBlobContent(setting.UI.MaxDisplayFileSize)
if err == nil {
rendered, err := markdown.RenderString(rctx, sidebarContent)
if err == nil {
ctx.Data["WikiSidebarHTML"] = rendered
}
}
}
// Render _Footer if it exists.
footerBlob := findWikiBlob(commit, "_Footer")
if footerBlob != nil {
footerContent, err := footerBlob.GetBlobContent(setting.UI.MaxDisplayFileSize)
if err == nil {
rendered, err := markdown.RenderString(rctx, footerContent)
if err == nil {
ctx.Data["WikiFooterHTML"] = rendered
}
}
}
ctx.HTML(http.StatusOK, tplOrgWiki)
}
// findOrgWikiCommit locates the convention wiki repo and returns its HEAD commit.
func findOrgWikiCommit(ctx *context.Context, orgID int64, repoName string) (*repo_model.Repository, *git.Commit) {
dbRepo, err := repo_model.GetRepositoryByName(ctx, orgID, repoName)
if err != nil {
if !repo_model.IsErrRepoNotExist(err) {
log.Error("findOrgWikiCommit: GetRepositoryByName(%d, %s): %v", orgID, repoName, err)
}
return nil, nil
}
if dbRepo.IsEmpty {
return nil, nil
}
gitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, dbRepo)
if err != nil {
log.Error("findOrgWikiCommit: OpenRepository(%s): %v", dbRepo.FullName(), err)
return nil, nil
}
commit, err := gitRepo.GetBranchCommit(dbRepo.DefaultBranch)
if err != nil {
log.Error("findOrgWikiCommit: GetBranchCommit(%s, %s): %v", dbRepo.FullName(), dbRepo.DefaultBranch, err)
return nil, nil
}
return dbRepo, commit
}
// findWikiBlob looks up a markdown file in the commit by name.
// Tries exact match first, then appends .md.
func findWikiBlob(commit *git.Commit, name string) *git.Blob {
// Try exact match (e.g., "Home.md").
if blob, _ := commit.GetBlobByPath(name); blob != nil {
return blob
}
// Try with .md extension (e.g., "Home" → "Home.md").
if blob, _ := commit.GetBlobByPath(name + ".md"); blob != nil {
return blob
}
// Try with .markdown extension.
if blob, _ := commit.GetBlobByPath(name + ".markdown"); blob != nil {
return blob
}
return nil
}
// isMarkdownFile returns true if the filename looks like a markdown file.
func isMarkdownFile(name string) bool {
ext := strings.ToLower(path.Ext(name))
return ext == ".md" || ext == ".markdown"
}
-23
View File
@@ -137,8 +137,6 @@ type PrepareOwnerHeaderResult struct {
const (
RepoNameProfilePrivate = ".profile-private"
RepoNameProfile = ".profile"
RepoNameWikiPublic = "wiki"
RepoNameWikiPrivate = "wiki-private"
)
func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult, err error) {
@@ -157,18 +155,6 @@ func RenderUserOrgHeader(ctx *context.Context) (result *PrepareOwnerHeaderResult
result.ProfilePrivateRepo, result.ProfilePrivateReadmeBlob = FindOwnerProfileReadme(ctx, ctx.Doer, RepoNameProfilePrivate)
result.HasOrgProfileReadme = result.ProfilePublicReadmeBlob != nil || result.ProfilePrivateReadmeBlob != nil
ctx.Data["HasOrgProfileReadme"] = result.HasOrgProfileReadme // many pages need it to show the "overview" tab
// Check if org has a wiki (internal convention repos or external URL).
orgUser := ctx.ContextUser
if orgUser.WikiMode == "external" && orgUser.WikiURL != "" {
ctx.Data["HasOrgWiki"] = true
ctx.Data["OrgWikiIsExternal"] = true
ctx.Data["OrgWikiExternalURL"] = orgUser.WikiURL
} else {
hasWiki := OrgWikiRepoExists(ctx, ctx.ContextUser.ID, RepoNameWikiPublic) ||
OrgWikiRepoExists(ctx, ctx.ContextUser.ID, RepoNameWikiPrivate)
ctx.Data["HasOrgWiki"] = hasWiki
}
} else {
_, profileReadmeBlob := FindOwnerProfileReadme(ctx, ctx.Doer)
ctx.Data["HasUserProfileReadme"] = profileReadmeBlob != nil
@@ -208,12 +194,3 @@ func loadHeaderCount(ctx *context.Context) error {
return nil
}
// OrgWikiRepoExists checks whether a convention wiki repo exists and is non-empty.
func OrgWikiRepoExists(ctx *context.Context, ownerID int64, repoName string) bool {
dbRepo, err := repo_model.GetRepositoryByName(ctx, ownerID, repoName)
if err != nil || dbRepo.IsEmpty {
return false
}
return true
}
-5
View File
@@ -1162,11 +1162,6 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Get("/repositories", org.Repositories)
m.Get("/heatmap", user.DashboardHeatmap)
m.Group("/wiki", func() {
m.Get("", org.Wiki)
m.Get("/*", org.Wiki)
})
m.Group("/projects", func() {
m.Group("", func() {
m.Get("", org.Projects)
-20
View File
@@ -131,26 +131,6 @@ func toIssue(ctx context.Context, doer *user_model.User, issue *issues_model.Iss
apiIssue.Deadline = issue.DeadlineUnix.AsTimePtr()
}
// Populate org-level issue metadata (status/priority/type)
apiIssue.StatusID = issue.StatusID
apiIssue.PriorityID = issue.PriorityID
apiIssue.TypeID = issue.TypeID
if issue.StatusID > 0 {
if def, err := issues_model.GetIssueStatusDefByID(ctx, issue.StatusID); err == nil {
apiIssue.StatusName = def.Name
}
}
if issue.PriorityID > 0 {
if def, err := issues_model.GetIssuePriorityDefByID(ctx, issue.PriorityID); err == nil {
apiIssue.PriorityName = def.Name
}
}
if issue.TypeID > 0 {
if def, err := issues_model.GetIssueTypeDefByID(ctx, issue.TypeID); err == nil {
apiIssue.TypeName = def.Name
}
}
return apiIssue
}
-11
View File
@@ -38,17 +38,6 @@
{{svg "octicon-code"}} {{ctx.Locale.Tr "org.code"}}
</a>
{{end}}
{{if .HasOrgWiki}}
{{if .OrgWikiIsExternal}}
<a class="item" href="{{.OrgWikiExternalURL}}" target="_blank" rel="noopener noreferrer">
{{svg "octicon-book"}} Wiki {{svg "octicon-link-external" 12}}
</a>
{{else}}
<a class="{{if .PageIsViewWiki}}active {{end}}item" href="{{$.Org.HomeLink}}/-/wiki/">
{{svg "octicon-book"}} Wiki
</a>
{{end}}
{{end}}
{{if .NumMembers}}
<a class="{{if $.PageIsOrgMembers}}active {{end}}item" href="{{$.OrgLink}}/members">
{{svg "octicon-person"}} {{ctx.Locale.Tr "org.members"}}
-25
View File
@@ -63,31 +63,6 @@
<div class="divider"></div>
<div class="field">
<label>{{svg "octicon-book" 16}} Wiki</label>
<div class="field">
<div class="ui radio checkbox">
<input class="enable-system-radio" name="wiki_mode" type="radio" value="" data-context="#external_wiki_box" data-target="#internal_wiki_box" {{if eq .Org.WikiMode ""}}checked{{end}}>
<label>Internal wiki (uses <code>wiki</code> / <code>wiki-private</code> repos)</label>
</div>
</div>
<div id="internal_wiki_box" class="field tw-pl-4 {{if ne .Org.WikiMode ""}}disabled{{end}}">
<p class="help">Create repos named <code>wiki</code> (public) and/or <code>wiki-private</code> (members-only) under this organization.</p>
</div>
<div class="field">
<div class="ui radio checkbox">
<input class="enable-system-radio" name="wiki_mode" type="radio" value="external" data-context="#internal_wiki_box" data-target="#external_wiki_box" {{if eq .Org.WikiMode "external"}}checked{{end}}>
<label>External wiki</label>
</div>
</div>
<div id="external_wiki_box" class="field tw-pl-4 {{if ne .Org.WikiMode "external"}}disabled{{end}}">
<label for="wiki_url">External wiki URL</label>
<input id="wiki_url" name="wiki_url" type="url" value="{{.Org.WikiURL}}" placeholder="https://wiki.example.com">
</div>
</div>
<div class="divider"></div>
<div class="inline field">
<div class="ui checkbox">
<input type="checkbox" name="require_2fa" {{if .Org.Require2FA}}checked{{end}}>
-97
View File
@@ -1,97 +0,0 @@
{{template "base/head" .}}
<div role="main" aria-label="{{.Title}}" class="page-content organization wiki">
{{template "org/header" .}}
{{template "org/menu" .}}
<div class="ui container">
{{if .WikiEmpty}}
<div class="ui placeholder segment">
<div class="ui icon header">
{{svg "octicon-book" 48}}
<br>
This organization doesn't have a wiki yet.
</div>
<p class="tw-text-center">
Create a repository named <code>wiki</code> (public) or <code>wiki-private</code> (members-only)
with markdown files to get started.
</p>
</div>
{{else}}
<div class="repo-button-row tw-flex tw-items-center tw-gap-2 tw-mb-4">
<div class="tw-flex-1 tw-flex tw-items-center tw-gap-2">
{{svg "octicon-book" 16}}
<strong>{{.CurrentPage}}</strong>
</div>
{{if .ShowWikiViewSelector}}
<div class="ui dropdown jump">
{{- $viewAsRole := Iif (.IsViewingWikiAsMember) (ctx.Locale.Tr "org.members.member") (ctx.Locale.Tr "settings.visibility.public") -}}
<span class="text">{{svg "octicon-eye"}} {{ctx.Locale.Tr "org.view_as_role" $viewAsRole}}</span>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
<a href="{{$.Org.HomeLink}}/-/wiki/{{.CurrentPage}}?view_as=public" class="item {{if not .IsViewingWikiAsMember}}selected{{end}}">
{{svg "octicon-check" 14 (Iif (not .IsViewingWikiAsMember) "" "tw-invisible")}} {{ctx.Locale.Tr "settings.visibility.public"}}
</a>
<a href="{{$.Org.HomeLink}}/-/wiki/{{.CurrentPage}}?view_as=member" class="item {{if .IsViewingWikiAsMember}}selected{{end}}">
{{svg "octicon-check" 14 (Iif .IsViewingWikiAsMember "" "tw-invisible")}} {{ctx.Locale.Tr "org.members.member"}}
</a>
</div>
</div>
{{end}}
</div>
{{if .WikiPageNotFound}}
<div class="ui segment">
<div class="ui icon message">
{{svg "octicon-alert" 24}}
<div class="content">
<div class="header">Page not found</div>
<p>The page "{{.CurrentPage}}" does not exist in this wiki.</p>
</div>
</div>
{{if .Pages}}
<h4>Available pages:</h4>
<ul>
{{range .Pages}}
<li><a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}">{{.Name}}</a></li>
{{end}}
</ul>
{{end}}
</div>
{{else}}
<div class="wiki-content-parts">
<div class="render-content markup wiki-content-main {{if or .WikiSidebarHTML .Pages}}with-sidebar{{end}}">
{{.WikiContent}}
</div>
{{if or .WikiSidebarHTML .Pages}}
<div class="render-content markup wiki-content-sidebar">
{{if .WikiSidebarHTML}}
{{.WikiSidebarHTML}}
<div class="ui divider"></div>
{{end}}
{{if .Pages}}
<strong>{{svg "octicon-list-unordered" 14}} Pages</strong>
<ul class="wiki-tree-list">
{{range .Pages}}
<li>
{{svg "octicon-file" 14}}
<a href="{{$.Org.HomeLink}}/-/wiki/{{.SubURL}}" {{if eq $.CurrentPage .Name}}class="active"{{end}}>{{.Name}}</a>
</li>
{{end}}
</ul>
{{end}}
</div>
{{end}}
<div class="tw-clear-both"></div>
{{if .WikiFooterHTML}}
<div class="render-content markup wiki-content-footer">
{{.WikiFooterHTML}}
</div>
{{end}}
</div>
{{end}}
{{end}}
</div>
</div>
{{template "base/footer" .}}
+11
View File
@@ -126,6 +126,17 @@
</select>
<p class="help">{{ctx.Locale.Tr "repo.settings.manifest_package_type_help"}}</p>
</div>
{{end}}
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_element_full"}}</label>
<input name="element_name" value="{{.Manifest.ElementName}}" placeholder="{{.Manifest.AutoElementName}}">
{{if .Manifest.ElementNameMismatch}}
<p class="help tw-text-yellow-600">{{ctx.Locale.Tr "repo.settings.manifest_element_mismatch" .Manifest.AutoElementName}}</p>
{{else}}
<p class="help">{{ctx.Locale.Tr "repo.settings.manifest_element_full_help"}}</p>
{{end}}
</div>
{{end}}
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_element_full"}}</label>
<input name="element_name" value="{{.Manifest.ElementName}}" placeholder="{{.Manifest.AutoElementName}}">