AI 生成: 一个从 Gitea 服务器获取最新项目的工具,支持命令行使用和 Web 界面查看。

This commit is contained in:
admin
2026-09-28 21:36:36 +08:00
commit f68c77a49b
7 changed files with 398 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
# Gitea Latest Project Fetcher
从 Gitea 服务器获取最新的项目(仓库)列表。
## 功能
- 通过 Gitea API 获取指定用户或组织的最新仓库
- 支持命令行使用(Python)
- 提供 Web 界面查看结果(Flask)
## 目录结构
```
gitea-latest-project-fetcher/
├── README.md
├── requirements.txt
├── cli.py # 命令行工具
├── web_app.py # Flask Web 应用
├── gitea_client.py # Gitea API 客户端
├── templates/
│ └── index.html # Web 页面
└── static/
└── style.css # 样式
```
## 安装
```bash
pip install -r requirements.txt
```
## 命令行使用
```bash
# 获取用户的最新仓库
python cli.py --base-url https://gitea.example.com --owner myuser --limit 5
# 使用 token(私有仓库需要)
python cli.py --base-url https://gitea.example.com --owner myuser --token YOUR_TOKEN
```
## Web 使用
```bash
python web_app.py
# 打开浏览器访问 http://127.0.0.1:5000
```
## 配置
可通过环境变量设置默认值:
- `GITEA_BASE_URL`:Gitea 服务器地址
- `GITEA_TOKEN`:访问令牌
## 说明
本工具使用 Gitea 的 REST API:`GET /api/v1/users/{owner}/repos`,
按更新时间排序后返回最新的仓库。
+50
View File
@@ -0,0 +1,50 @@
"""命令行工具:从 Gitea 获取最新项目。"""
import argparse
import os
import sys
from gitea_client import GiteaClient
def main():
parser = argparse.ArgumentParser(description='从 Gitea 获取最新的项目列表')
parser.add_argument('--base-url', default=os.environ.get('GITEA_BASE_URL'),
help='Gitea 服务器地址,如 https://gitea.example.com')
parser.add_argument('--owner', required=True, help='用户名或组织名')
parser.add_argument('--token', default=os.environ.get('GITEA_TOKEN'),
help='访问令牌(私有仓库需要)')
parser.add_argument('--limit', type=int, default=10, help='返回数量,默认 10')
parser.add_argument('--org', action='store_true', help='将 owner 视为组织')
args = parser.parse_args()
if not args.base_url:
print('错误:请通过 --base-url 或环境变量 GITEA_BASE_URL 指定 Gitea 地址', file=sys.stderr)
sys.exit(1)
client = GiteaClient(args.base_url, args.token)
try:
if args.org:
repos = client.get_org_latest_repos(args.owner, args.limit)
else:
repos = client.get_latest_repos(args.owner, args.limit)
except Exception as e:
print(f'获取仓库失败:{e}', file=sys.stderr)
sys.exit(1)
if not repos:
print('未找到仓库。')
return
print(f'\n{args.owner} 的最新 {len(repos)} 个项目:\n')
for i, repo in enumerate(repos, 1):
print(f'{i}. {repo.get("full_name")}')
print(f' 描述: {repo.get("description") or "(无)"}')
print(f' 更新: {repo.get("updated_at")}')
print(f' 链接: {repo.get("html_url")}')
print()
if __name__ == '__main__':
main()
+51
View File
@@ -0,0 +1,51 @@
"""Gitea API 客户端:获取指定用户/组织的最新仓库。"""
import os
import requests
class GiteaClient:
"""简单的 Gitea API 客户端。"""
def __init__(self, base_url: str, token: str = None):
# 去掉末尾的斜杠,避免拼接出双斜杠
self.base_url = base_url.rstrip('/')
self.token = token or os.environ.get('GITEA_TOKEN')
def _headers(self):
headers = {'Accept': 'application/json'}
if self.token:
headers['Authorization'] = f'token {self.token}'
return headers
def get_latest_repos(self, owner: str, limit: int = 10):
"""获取指定用户或组织的最新仓库。
参数:
owner: 用户名或组织名
limit: 返回的仓库数量上限
返回:
仓库信息列表(dict),按更新时间倒序排列。
"""
url = f'{self.base_url}/api/v1/users/{owner}/repos'
params = {'sort': 'updated', 'limit': limit}
resp = requests.get(url, headers=self._headers(), params=params, timeout=15)
resp.raise_for_status()
repos = resp.json()
# 按 updated_at 倒序排序,确保拿到真正最新的
repos.sort(key=lambda r: r.get('updated_at', ''), reverse=True)
return repos[:limit]
def get_org_latest_repos(self, org: str, limit: int = 10):
"""获取指定组织的最新仓库。"""
url = f'{self.base_url}/api/v1/orgs/{org}/repos'
params = {'sort': 'updated', 'limit': limit}
resp = requests.get(url, headers=self._headers(), params=params, timeout=15)
resp.raise_for_status()
repos = resp.json()
repos.sort(key=lambda r: r.get('updated_at', ''), reverse=True)
return repos[:limit]
+2
View File
@@ -0,0 +1,2 @@
requests>=2.28.0
Flask>=3.0.0
+129
View File
@@ -0,0 +1,129 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: #f5f7fa;
color: #24292e;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 32px 16px;
}
h1 {
font-size: 28px;
margin-bottom: 24px;
}
h2 {
font-size: 20px;
margin-top: 32px;
}
.search-form {
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.field {
display: flex;
flex-direction: column;
}
.field label {
font-size: 13px;
color: #586069;
margin-bottom: 6px;
}
.field input[type="text"],
.field input[type="password"],
.field input[type="number"] {
padding: 8px 10px;
border: 1px solid #d1d5da;
border-radius: 6px;
font-size: 14px;
}
.field.checkbox {
justify-content: flex-end;
}
.field.checkbox label {
display: flex;
align-items: center;
gap: 6px;
color: #24292e;
font-size: 14px;
}
button {
grid-column: 1 / -1;
padding: 10px 16px;
background: #2ea44f;
color: #fff;
border: none;
border-radius: 6px;
font-size: 15px;
cursor: pointer;
}
button:hover {
background: #2c974b;
}
.error {
margin-top: 16px;
padding: 12px 16px;
background: #ffeef0;
color: #86181d;
border-radius: 6px;
}
.repo-list {
list-style: none;
padding: 0;
margin: 16px 0 0;
}
.repo-item {
background: #fff;
padding: 16px;
border-radius: 8px;
margin-bottom: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06);
}
.repo-name {
font-size: 17px;
font-weight: 600;
color: #0366d6;
text-decoration: none;
}
.repo-name:hover {
text-decoration: underline;
}
.repo-desc {
margin: 8px 0;
color: #586069;
font-size: 14px;
}
.repo-meta {
display: flex;
gap: 16px;
font-size: 12px;
color: #6a737d;
}
+61
View File
@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gitea 最新项目</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<h1>Gitea 最新项目</h1>
<form method="post" class="search-form">
<div class="field">
<label>Gitea 地址</label>
<input type="text" name="base_url" value="{{ form.base_url }}" placeholder="https://gitea.example.com" required>
</div>
<div class="field">
<label>用户名 / 组织名</label>
<input type="text" name="owner" value="{{ form.owner }}" placeholder="myuser" required>
</div>
<div class="field">
<label>访问令牌(可选)</label>
<input type="password" name="token" value="{{ form.token }}" placeholder="私有仓库需要">
</div>
<div class="field">
<label>数量</label>
<input type="number" name="limit" value="{{ form.limit }}" min="1" max="50">
</div>
<div class="field checkbox">
<label>
<input type="checkbox" name="is_org" {% if form.is_org %}checked{% endif %}>
作为组织查询
</label>
</div>
<button type="submit">获取最新项目</button>
</form>
{% if error %}
<div class="error">{{ error }}</div>
{% endif %}
{% if repos %}
<h2>结果({{ repos|length }} 个)</h2>
<ul class="repo-list">
{% for repo in repos %}
<li class="repo-item">
<a href="{{ repo.html_url }}" target="_blank" rel="noopener" class="repo-name">{{ repo.full_name }}</a>
<p class="repo-desc">{{ repo.description or '(无描述)' }}</p>
<div class="repo-meta">
<span>更新于 {{ repo.updated_at }}</span>
{% if repo.language %}<span>语言:{{ repo.language }}</span>{% endif %}
<span>⭐ {{ repo.stars_count }}</span>
</div>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
</body>
</html>
+46
View File
@@ -0,0 +1,46 @@
"""Flask Web 应用:在浏览器中查看 Gitea 最新项目。"""
import os
from flask import Flask, render_template, request
from gitea_client import GiteaClient
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def index():
repos = []
error = None
form = {
'base_url': os.environ.get('GITEA_BASE_URL', ''),
'owner': '',
'token': '',
'limit': 10,
'is_org': False,
}
if request.method == 'POST':
form['base_url'] = request.form.get('base_url', '').strip()
form['owner'] = request.form.get('owner', '').strip()
form['token'] = request.form.get('token', '').strip()
form['limit'] = int(request.form.get('limit', 10) or 10)
form['is_org'] = request.form.get('is_org') == 'on'
if not form['base_url'] or not form['owner']:
error = '请填写 Gitea 地址和用户名/组织名。'
else:
try:
client = GiteaClient(form['base_url'], form['token'] or None)
if form['is_org']:
repos = client.get_org_latest_repos(form['owner'], form['limit'])
else:
repos = client.get_latest_repos(form['owner'], form['limit'])
except Exception as e:
error = f'获取失败:{e}'
return render_template('index.html', repos=repos, error=error, form=form)
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000, debug=True)