GitLab 清理工具
-
📋 文档信息
- 版本: 1.0.0
- 最后更新: 2026-03-19
- 适用场景: GitLab 组和项目的批量清理
- 核心功能: 全量删除、选择性删除、递归删除、断点续删、试运行预览
一、工具概述
1.1 为什么需要这个工具?
在 GitLab 日常维护中,经常需要清理测试环境、旧项目或废弃的组。手动删除大量项目不仅耗时,还容易出错。本工具提供自动化清理方案:
- 测试环境重置:一键清理所有测试数据
- 旧项目归档:选择性删除不再使用的项目
- 组结构清理:递归删除整个组树
- 安全预览:先看效果再执行
1.2 核心功能
功能 说明 ✅ 全量删除 一键删除所有组和项目 ✅ 选择性删除 只删除指定的组或项目 ✅ 递归删除 自动先删项目、再删子组、最后删父组 ✅ 试运行模式 只预览不执行,避免误操作 ✅ 断点续删 记录已删除内容,中断后可继续 ✅ 日志记录 所有操作写入文件,便于追溯
二、快速开始
2.1 环境准备
安装依赖
pip install python-gitlab
验证安装
python -c "import gitlab; print('环境就绪')"
三、完整源码
import gitlab
import time
import logging
import json
import os
# 配置日志(同时输出到控制台和文件)
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
handlers=[
logging.StreamHandler(), # 控制台输出
logging.FileHandler('./clean.log', encoding='utf-8') # 文件输出
]
)
# ==================== 配置区域 ====================
# GitLab 连接配置(只需修改这里)
target_gitlab_url = "http://192.168.1.110:9080"
target_private_token = "your-admin-token"
# 清理选项配置
CLEAN_OPTIONS = {
# 清理模式: 'all' 全部删除, 'selected' 只删除指定内容
'mode': 'all', # 可选 'all' 或 'selected'
# 当 mode='selected' 时,指定要删除的组(支持递归删除)
'selected_groups': [
# "test-group",
# "old-project-2023",
# "archive/deprecated",
],
# 当 mode='selected' 时,指定要删除的项目
'selected_projects': [
# "group1/temp-project",
# "root-level-project",
],
# 试运行模式:True 只预览不删除,False 实际执行
'dry_run': True, # 建议先设为 True 预览
# 状态文件:记录已删除内容,用于断点续删
'state_file': './clean_state.json',
}
# ==================================================
class GitLabCleaner:
"""GitLab 清理工具"""
def __init__(self):
self.gitlab = None
self.deleted_projects = set() # 已删除的项目ID
self.deleted_groups = set() # 已删除的组ID
self.stats = { # 统计信息
'projects': 0, # 成功删除的项目数
'groups': 0, # 成功删除的组数
'failed': 0 # 失败次数
}
# 加载之前的状态(用于断点续删)
self.load_state()
logging.info("=" * 60)
logging.info("GitLab 清理工具启动")
logging.info(f"目标地址: {target_gitlab_url}")
logging.info(f"清理模式: {CLEAN_OPTIONS['mode']}")
logging.info(f"试运行模式: {CLEAN_OPTIONS['dry_run']}")
logging.info(f"状态文件: {os.path.abspath(CLEAN_OPTIONS['state_file'])}")
logging.info("=" * 60)
def load_state(self):
"""加载已删除记录(用于断点续删)"""
if os.path.exists(CLEAN_OPTIONS['state_file']):
try:
with open(CLEAN_OPTIONS['state_file'], 'r', encoding='utf-8') as f:
state = json.load(f)
self.deleted_projects = set(state.get('projects', []))
self.deleted_groups = set(state.get('groups', []))
self.stats = state.get('stats', self.stats)
logging.info(f"📂 加载上次进度: 已删除 {len(self.deleted_projects)} 个项目, {len(self.deleted_groups)} 个组")
logging.info(f" 将继续未完成的清理任务")
except Exception as e:
logging.warning(f"加载状态文件失败: {e}")
def save_state(self):
"""保存已删除记录(用于断点续删)"""
if CLEAN_OPTIONS['dry_run']:
return # 试运行模式不保存状态
try:
with open(CLEAN_OPTIONS['state_file'], 'w', encoding='utf-8') as f:
json.dump({
'projects': list(self.deleted_projects),
'groups': list(self.deleted_groups),
'stats': self.stats,
'last_update': time.strftime('%Y-%m-%d %H:%M:%S')
}, f, ensure_ascii=False, indent=2)
except Exception as e:
logging.warning(f"保存状态失败: {e}")
def connect(self):
"""连接 GitLab"""
try:
self.gitlab = gitlab.Gitlab(
target_gitlab_url,
private_token=target_private_token,
ssl_verify=False,
timeout=30,
keep_base_url=True # 防止URL重写
)
self.gitlab.auth()
logging.info(f"✅ 连接成功,当前用户: {self.gitlab.user.username}")
return True
except Exception as e:
logging.error(f"❌ 连接失败: {e}")
return False
def delete_project(self, project_id, project_path):
"""删除单个项目"""
# 如果已经删除过,跳过
if project_id in self.deleted_projects:
return
if CLEAN_OPTIONS['dry_run']:
logging.info(f"🔍 [试运行] 将删除项目: {project_path}")
self.stats['projects'] += 1
return
try:
self.gitlab.projects.delete(project_id)
logging.info(f"✅ 已删除项目: {project_path}")
self.deleted_projects.add(project_id)
self.stats['projects'] += 1
self.save_state() # 每删除一个就保存状态
time.sleep(0.3) # 避免请求过快
except Exception as e:
logging.error(f"❌ 删除失败 {project_path}: {e}")
self.stats['failed'] += 1
def delete_group(self, group_id, group_path, depth=0):
"""
递归删除组
删除顺序:
1. 先删除组内的所有项目
2. 再递归删除所有子组
3. 最后删除当前组
"""
# 如果已经删除过,跳过
if group_id in self.deleted_groups:
return
indent = " " * depth # 用于日志缩进,显示层级
try:
# 获取完整的组对象
group = self.gitlab.groups.get(group_id)
# 1. 删除组内的所有项目
projects = group.projects.list(all=True)
if projects:
logging.info(f"{indent}📁 组 {group_path} 包含 {len(projects)} 个项目")
for project in projects:
self.delete_project(project.id, f"{group_path}/{project.path}")
# 2. 递归删除所有子组
subgroups = group.subgroups.list(all=True)
if subgroups:
logging.info(f"{indent}📂 组 {group_path} 包含 {len(subgroups)} 个子组")
for subgroup in subgroups:
self.delete_group(subgroup.id, f"{group_path}/{subgroup.path}", depth + 1)
# 3. 删除当前组
if CLEAN_OPTIONS['dry_run']:
logging.info(f"{indent}🔍 [试运行] 将删除组: {group_path}")
self.stats['groups'] += 1
else:
group.delete()
logging.info(f"{indent}✅ 已删除组: {group_path}")
self.deleted_groups.add(group_id)
self.stats['groups'] += 1
self.save_state()
time.sleep(0.3)
except Exception as e:
logging.error(f"{indent}❌ 处理组失败 {group_path}: {e}")
self.stats['failed'] += 1
def run(self):
"""执行清理任务"""
# 1. 连接 GitLab
if not self.connect():
return
# 2. 如果不是试运行模式,需要用户确认
if not CLEAN_OPTIONS['dry_run']:
logging.warning("⚠️ 警告:此操作将永久删除 GitLab 上的内容!")
confirm = input("请输入 'YES' 确认删除: ")
if confirm != "YES":
logging.info("操作已取消")
return
# 3. 根据模式执行清理
if CLEAN_OPTIONS['mode'] == 'all':
# 模式1:全量删除
logging.info("\n" + "=" * 60)
logging.info("开始全量删除")
logging.info("=" * 60)
# 先删所有组(会自动删组内项目)
for group in self.gitlab.groups.list(all=True):
self.delete_group(group.id, group.path)
# 再删根级别项目(不在任何组中的项目)
for project in self.gitlab.projects.list(all=True):
self.delete_project(project.id, project.path_with_namespace)
elif CLEAN_OPTIONS['mode'] == 'selected':
# 模式2:选择性删除
logging.info("\n" + "=" * 60)
logging.info("开始选择性删除")
logging.info("=" * 60)
# 删除指定的组
if CLEAN_OPTIONS['selected_groups']:
logging.info("\n📂 删除指定的组:")
for group_path in CLEAN_OPTIONS['selected_groups']:
try:
group = self.gitlab.groups.get(group_path)
self.delete_group(group.id, group_path)
except Exception as e:
logging.error(f"组不存在或无法访问: {group_path}")
# 删除指定的项目
if CLEAN_OPTIONS['selected_projects']:
logging.info("\n📁 删除指定的项目:")
for project_path in CLEAN_OPTIONS['selected_projects']:
try:
project = self.gitlab.projects.get(project_path)
self.delete_project(project.id, project_path)
except Exception as e:
logging.error(f"项目不存在或无法访问: {project_path}")
else:
logging.error(f"未知的清理模式: {CLEAN_OPTIONS['mode']}")
return
# 4. 输出统计信息
self.print_stats()
def print_stats(self):
"""打印统计信息"""
logging.info("\n" + "=" * 60)
if CLEAN_OPTIONS['dry_run']:
logging.info("试运行统计(实际未删除)")
else:
logging.info("清理完成统计")
logging.info("=" * 60)
logging.info(f"✅ 成功删除项目: {self.stats['projects']} 个")
logging.info(f"✅ 成功删除组: {self.stats['groups']} 个")
logging.info(f"❌ 失败次数: {self.stats['failed']} 个")
logging.info("=" * 60)
logging.info(f"📁 状态文件: {os.path.abspath(CLEAN_OPTIONS['state_file'])}")
logging.info(f"📁 日志文件: {os.path.abspath('./clean.log')}")
logging.info("=" * 60)
if __name__ == "__main__":
try:
cleaner = GitLabCleaner()
cleaner.run()
except KeyboardInterrupt:
logging.info("⏸️ 用户中断,当前进度已保存")
except Exception as e:
logging.error(f"程序异常: {e}")
四、配置详解
4.1 基础连接配置
# 必须修改的部分
target_gitlab_url = "http://192.168.1.110:9080" # 你的 GitLab 地址
target_private_token = "glpat-your-token-here" # 管理员 token
4.2 清理模式配置
模式1:全量删除所有内容
CLEAN_OPTIONS = {
'mode': 'all', # 全量模式
'dry_run': True, # 建议先试运行
# 其他选项不需要填写
}
模式2:删除指定的组
CLEAN_OPTIONS = {
'mode': 'selected',
'selected_groups': [
"test-group", # 删除整个测试组
"old-project-2023", # 删除旧项目组
"archive/deprecated", # 删除深层子组
],
'selected_projects': [], # 不单独删项目
'dry_run': True,
}
模式3:删除指定的项目
CLEAN_OPTIONS = {
'mode': 'selected',
'selected_groups': [], # 不删组
'selected_projects': [
"group1/temp-project", # 组内项目
"group2/subgroup/test-app", # 深层项目
"root-level-project", # 根级别项目
],
'dry_run': True,
}
模式4:混合删除
CLEAN_OPTIONS = {
'mode': 'selected',
'selected_groups': [
"archive-2022", # 删除整个归档组
],
'selected_projects': [
"active-group/legacy", # 只删除活动组中的遗留项目
],
'dry_run': True,
}
4.3 试运行模式
'dry_run': True # 只预览,不实际删除(推荐先使用)
'dry_run': False # 实际执行删除(需要输入 YES 确认)
五、使用步骤
第一步:试运行预览
# 1. 设置 dry_run = True
# 2. 运行脚本
python gitlab_cleaner.py
试运行输出示例:
2026-03-19 15:30:45 - INFO - ============================================================
2026-03-19 15:30:45 - INFO - GitLab 清理工具启动
2026-03-19 15:30:45 - INFO - 目标地址: http://192.168.1.110:9080
2026-03-19 15:30:45 - INFO - 清理模式: selected
2026-03-19 15:30:45 - INFO - 试运行模式: True
2026-03-19 15:30:45 - INFO - ============================================================
2026-03-19 15:30:46 - INFO - 🔍 [试运行] 将删除组: test-group
2026-03-19 15:30:46 - INFO - 🔍 [试运行] 将删除项目: test-group/project1
2026-03-19 15:30:46 - INFO - 🔍 [试运行] 将删除项目: test-group/project2
...
2026-03-19 15:31:00 - INFO - ============================================================
2026-03-19 15:31:00 - INFO - 试运行统计(实际未删除)
2026-03-19 15:31:00 - INFO - ============================================================
2026-03-19 15:31:00 - INFO - ✅ 成功删除项目: 25 个
2026-03-19 15:31:00 - INFO - ✅ 成功删除组: 5 个
2026-03-19 15:31:00 - INFO - ❌ 失败次数: 0 个
第二步:确认后实际执行
# 1. 设置 dry_run = False
# 2. 再次运行
python gitlab_cleaner.py
# 3. 输入 YES 确认
实际执行确认提示:
⚠️ 警告:此操作将永久删除 GitLab 上的内容!
请输入 'YES' 确认删除: YES
第三步:中断后继续
# 如果中途中断(Ctrl+C),下次运行会自动继续
python gitlab_cleaner.py
2026-03-19 16:00:00 - INFO - 📂 加载上次进度: 已删除 50 个项目, 10 个组
2026-03-19 16:00:00 - INFO - 将继续未完成的清理任务
六、文件说明
运行后会在当前目录生成:
your-script-directory/
├── gitlab_cleaner.py # 主程序
├── clean_state.json # 状态文件(记录已删除内容)
└── clean.log # 日志文件(所有操作记录)
6.1 状态文件格式
{
"projects": [123, 456, 789], // 已删除的项目ID
"groups": [111, 222, 333], // 已删除的组ID
"stats": { // 统计信息
"projects": 50,
"groups": 10,
"failed": 0
},
"last_update": "2026-03-19 15:30:45" // 最后更新时间
}
6.2 文件用途
| 文件 | 作用 | 能否删除 |
|---|---|---|
clean_state.json | 记录已删除内容,用于断点续删 | ⚠️ 删除后无法继续 |
clean.log | 记录所有操作日志 | ✅ 可随时删除 |
七、使用示例
示例1:清理测试环境
# 配置
target_gitlab_url = "http://test.gitlab.com"
CLEAN_OPTIONS = {
'mode': 'all',
'dry_run': True, # 先预览
}
# 预览确认后改为 False 执行
示例2:删除旧项目归档
CLEAN_OPTIONS = {
'mode': 'selected',
'selected_groups': [
"archive-2022",
"archive-2023",
],
'dry_run': True,
}
示例3:删除单个项目
CLEAN_OPTIONS = {
'mode': 'selected',
'selected_projects': [
"test-group/bug-repro",
],
'selected_groups': [],
'dry_run': True,
}
示例4:分批次清理
# 第一批
CLEAN_OPTIONS = {
'mode': 'selected',
'selected_groups': ["group1", "group2"],
}
# 第二批(会自动跳过已删除的)
CLEAN_OPTIONS = {
'mode': 'selected',
'selected_groups': ["group3", "group4"],
}
八、注意事项
8.1 权限要求
-
必须使用管理员 token,否则可能无法删除某些内容
- token 需要
api权限
8.2 安全机制
- 试运行模式:先预览再执行
- 手动确认:实际执行需要输入 YES
- 状态记录:已删除的内容不会重复删除
- 中断保护:Ctrl+C 会保存当前进度
8.3 删除顺序(自动处理)
- token 需要
1.先删项目 → 2. 再删子组 → 3. 最后删父组
不需要手动关心顺序,工具会自动处理。
8.4 常见问题
Q: 如何从头开始清理?
# 删除状态文件即可
rm clean_state.json
Q: 删除过程中断了怎么办?
# 直接重新运行,会自动继续
python gitlab_cleaner.py
Q: 如何只预览不删除?
'dry_run': True
Q: 误删了重要内容?
GitLab 删除操作不可逆,务必先试运行确认!
九、版本历史
| 版本 | 日期 | 更新内容 |
|---|---|---|
| 1.0.0 | 2026-03-19 | 初始版本,支持全量/选择性删除、递归删除、断点续删、试运行 |
十、总结
这个工具的核心优势:
- 简单:只有 200 行代码,易于理解和修改
- 安全:试运行模式 + 手动确认
- 可靠:断点续删,不怕中断
- 灵活:支持全量删除和选择性删除
- 完整:自动处理删除顺序,递归删除所有内容
使用口诀:先试运行预览,确认后再执行,中断不用怕,继续接着删。