AI 生成: 一个从 Gitea 服务器获取最新项目的工具,支持命令行使用和 Web 界面查看。
This commit is contained in:
@@ -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`,
|
||||
按更新时间排序后返回最新的仓库。
|
||||
@@ -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()
|
||||
@@ -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]
|
||||
@@ -0,0 +1,2 @@
|
||||
requests>=2.28.0
|
||||
Flask>=3.0.0
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
Reference in New Issue
Block a user