ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

GitHub批量删除仓库:API脚本与实战避坑

2026/9/7 16:45:30 拓冰建站 浏览量
GitHub批量删除仓库:API脚本与实战避坑 如果你的GitHub账号里躺着几百个仓库其中一半是当年随手创建的练手项目、课程作业、临时Demo还有一堆fork下来就没再管过的开源仓库你应该能理解我的痛点。前阵子我痛下决心清理账号结果发现手动删除一个仓库至少要七八次点击仓库一多这个工作量完全不可接受。我干脆花了一下午折腾出一套批量删除方案整个过程并不复杂今天就把完整的思路、脚本和踩过的坑拆开讲清楚。这套方案主要解决三类人的需求一是仓库数量已经多到影响日常管理的老账号用户二是准备把旧账号废弃、需要清空内容再交接的开发者三是接手了历史项目、需要批量整理组织和团队下无用仓库的维护者。不用一个个点网页直接用GitHub官方API就能在几分钟内删掉几十上百个仓库而且能按名称、更新时间、是否fork等条件精准筛选避免误删。1. 什么时候你会需要批量删仓库先说说什么情况下这件事会变成一个刚需。很多人觉得删除仓库而已用不到几次但GitHub账号的仓库列表是会越来越膨胀的尤其是下面几类行为几乎注定会让你在某一天面对一个臃肿到不想打开的仓库列表。第一类是大量fork。看到感兴趣的开源项目就点fork美其名曰“先收藏”实际上fork完之后基本不会再看第二次。fork的仓库会占掉你仓库列表的很大一块而且默认还会出现在你的主页绿点矩阵里让别人觉得你天天在活跃。真正要清理的时候这些fork往往占了总数的一半以上。第二类是各种自动化测试和CI实验。很多人会在GitHub上建一堆临时仓库用来测试GitHub Actions、webhook、Pages部署测试完就扔在那里。这类仓库名字通常是test-xxx、demo-xxx、tmp-xxx没有任何保留价值。第三类是早些年学习时创建的项目。GPA不高但仓库很多的同学应该懂当年为了交作业建了一堆PythonAssignment、JavaCourseDesign之类的仓库代码质量一言难尽留着占地方删了又有点舍不得。但说实话这类仓库留着才是真正的负担代码本身没什么价值还干扰你找正经项目。当你发现自己仓库数量超过100个、每次打开仓库列表都要翻好几页才能找到目标时就该考虑做一次批量清理了。我当时的仓库数量是200多个逐个手动删除需要点击一千多次这显然不是一个人该干的活儿。1.1 手动删除到底有多麻烦你可能觉得手动删除也没那么恐怖但实际走一遍流程就明白了。在GitHub网页端删除一个仓库需要先进入仓库页面点击Settings滚到页面最底部的Danger Zone点击Delete this repository然后输入完整的仓库名来确认最后点击确认按钮。这还只是正常路径如果仓库是被保护状态或者你是组织成员而非owner还需要额外处理权限。一次点击流程下来大约30秒到1分钟如果是100个仓库意味着你要花一个小时甚至更久全程都在做高度重复的操作而且中间任何一个步骤走神输错仓库名就会进入一个尴尬的确认失败循环。更别说网页端操作不存在批量勾选这个功能GitHub本身也没有提供“全选删除”这样的入口。所以最合理的方案是绕过网页直接用GitHub REST API。删除仓库的API非常简洁一个DELETE请求就能完成而且支持脚本化、支持筛选、支持并发控制。只要注意认证方式和限速策略就能写出一个既安全又高效的批量删除脚本。2. 方案选型网页删除与API脚本对比动手之前先想清楚选哪条路省得到时候返工。批量删除GitHub仓库常见的有三种方案我分别说一下它们的适用场景和坑在哪里。第一种是纯网页手动删除适合仓库数量在10个以内、删除频率极低的场景。老实说如果你只需要处理三五个仓库写脚本反而浪费时间。网页删除的优点是直观、安全毕竟每一步都需要肉眼确认误删概率极低。缺点是效率差且对于大量仓库完全不现实。第二种是GitHub官方命令行工具gh适合已经装了gh CLI、习惯用命令行的用户。gh repo delete命令可以删除单个仓库配合gh repo list的JSON输出可以写shell脚本来实现批量删除。这个方案的优点是代码量少认证配置简单gh auth login以后不需要手动管理token。缺点是需要额外安装gh而且脚本里对JSON解析的处理稍显繁琐如果对jq不熟悉调试起来会有点头疼。第三种是直接调用GitHub REST API写脚本这也是我个人最推荐的方式。只要有HTTP请求库理论上任何语言都能实现Python、Node.js、Go都可以。自己写脚本的灵活性最高可以自由定制筛选条件、删除顺序、错误处理和日志输出。缺点是需要自己管理认证token对初次接触API的用户有轻微上手门槛。2.1 为什么我最终选了Python脚本三条路我都试过最终留下的是Python脚本方案。主要原因有三个第一筛选逻辑可控。用Python写筛选条件比如“删除所有fork仓库且一年没更新的”“删除所有名字带test的仓库”就是几个if语句的事。用gh加jq虽然也能做但处理复杂筛选时语法可读性明显不如Python。第二执行过程可观测。脚本里我可以在删除前打印出完整清单删除后打印每个仓库的状态码出错时把响应体记录下来。这种可观测性在批量操作中非常重要尤其是要删几十个仓库的时候你不能指望删错了再靠记忆恢复。第三可以反复测试。我可以在脚本里加一个dry_run参数先跑一遍看看待删除清单确认无误后再真正执行删除。这种“干跑模式”在gh脚本里实现起来也可以但Python写起来更顺手。当然如果你只是偶尔删几个repo且已经装了gh用gh确实更省事。我会在后面的章节里把gh的批量做法也写出来供你对比选择。3. 批量删除实操从拿到Token到跑完脚本下面进入正题完整走一遍用Python脚本批量删除GitHub仓库的流程。整个过程分三步准备认证凭据、编写并测试脚本、执行删除。每一步我都会说清楚原理和注意事项。3.1 创建有删除权限的Personal Access Token删除仓库需要调用GitHub的DELETE /repos/{owner}/{repo}接口而这个接口要求必须使用一个有delete_repo权限的token做认证。创建token的路径是GitHub页面右上角头像 - Settings - Developer settings - Personal access tokens - Tokens (classic) - Generate new token。注意GitHub现在也有更细粒度的Fine-grained token但为了兼容性和简单性建议直接用classic token然后在勾选权限时找到delete_repo并勾上。这一步是最关键的网上很多人说删除仓库时收到403错误原因几乎都是token权限不够。repo权限会给所有仓库的完整控制权但如果你的token是public_repo权限你只能删除公开仓库删除私有仓库时会直接403。所以要删私有仓库的话必须勾选repo这一整个大类或者至少确保delete_repo被包含在你选中的权限范围里。注意token生成后只会显示一次一定要把它复制下来保存到本地安全的地方。如果你关掉页面才发现没保存只能重新生成一个。另外这个token不要提交到GitHub仓库里也不要在公共网络里传播它等价于你账号的部分管理权限泄露了别人就能删你的仓库。如果不想用网页创建token也可以用gh CLI一步到位gh auth login gh auth token执行完gh auth login以后gh auth token会输出一个有效token这个token也可以直接用于API脚本。不过我还是建议专门创建一个仅用于删除操作、用完即删的token权限最小化别拿自己日常使用的token去跑批量删除万一脚本逻辑有bug导致误删你连追责的对象都没有。3.2 先拉取仓库列表确认你将要操作的对象脚本的第一步是拿到当前账号下的所有仓库列表。这里用到的接口是GET /user/repos它会分页返回仓库列表每页默认30条最多100条。所以如果你有200个仓库光拉取列表就需要至少2到3次请求。分页参数用per_page和page控制。下面这段代码会拉取所有仓库并组装成一份包含仓库信息、便于后续筛选的列表import requests GITHUB_TOKEN ghp_你的token HEADERS { Authorization: ftoken {GITHUB_TOKEN}, Accept: application/vnd.githubjson, } def fetch_all_repos(): repos [] page 1 while True: resp requests.get( https://api.github.com/user/repos, headersHEADERS, params{ per_page: 100, page: page, sort: updated, direction: desc, }, timeout30, ) resp.raise_for_status() data resp.json() if not data: break repos.extend(data) page 1 return repos all_repos fetch_all_repos() print(f共发现 {len(all_repos)} 个仓库)这里有个细节需要注意/user/repos接口返回的仓库是当前认证用户能看到的、自己拥有的全部仓库包括私有仓库。如果你用token访问的是别人的账号这个接口只会返回你有权限查看的仓库所以请确保token属于你本人的账号。拿到仓库列表后打印出来逐条看一眼确认数据里包含name、full_name、fork、archived、pushed_at、owner这些关键字段。full_name的格式是用户名/仓库名后面删除请求要用到这个完整名称。pushed_at字段可用于判断仓库最近一次push时间是筛选“僵尸仓库”的重要依据。3.3 编写筛选逻辑只删你想删的接下来是脚本的核心部分筛选。直接用一个列表推导式或者for循环按你的条件筛选出待删除仓库。下面这段代码展示了几种常见筛选策略import datetime def is_old_untouched(repo, months12): pushed_at repo.get(pushed_at) if not pushed_at: return False try: pushed_date datetime.datetime.strptime(pushed_at, %Y-%m-%dT%H:%M:%SZ) except ValueError: return False threshold datetime.datetime.utcnow() - datetime.timedelta(daysmonths * 30) return pushed_date threshold targets [] for repo in all_repos: name repo[name] is_fork repo[fork] is_archived repo[archived] old_and_untouched is_old_untouched(repo) # 示例筛选1删除所有名字带 test 的仓库 if test in name.lower(): targets.append(repo) continue # 示例筛选2删除超过一年没更新的 fork 仓库 if is_fork and old_and_untouched: targets.append(repo) continue # 示例筛选3删除已归档且超过两年没动的仓库 if is_archived and old_and_untouched: targets.append(repo) continue print(f筛选出 {len(targets)} 个待删除仓库) for repo in targets: print(repo[full_name])筛选项可以自由组合但请你务必有一条铁律不要直接把所有仓库一次性全选删除。即使你确定这些仓库都没用了也要分两批执行第一批选几个最不重要的仓库试水确认删除流程顺畅、没有报错之后再处理剩下的大部分。筛选逻辑执行完后先不急着删除而是把目标仓库清单保存到本地文件方便核对with open(targets.txt, w) as f: for repo in targets: f.write(repo[full_name] \n)这一步相当于给你自己留个底万一删错了至少能知道删了哪些仓库方便后续联系GitHub Support找回。虽然恢复有窗口期但前提是你能提供准确的仓库名列表。3.4 执行删除DELETE请求与错误处理筛选确认无误后就可以执行删除了。删除接口是DELETE https://api.github.com/repos/{owner}/{repo}调用这个接口不需要请求体成功会返回204 No Content失败则返回对应错误码。下面这段代码带上了完整的错误处理和速率控制import time def delete_repo(full_name, dry_runFalse): if dry_run: print(f[DRY RUN] 将删除 {full_name}) return True resp requests.delete( fhttps://api.github.com/repos/{full_name}, headersHEADERS, timeout30, ) if resp.status_code 204: print(f已删除: {full_name}) return True else: print(f删除失败: {full_name} - HTTP {resp.status_code} - {resp.text}) return False # 先干跑一遍 for repo in targets: delete_repo(repo[full_name], dry_runTrue) # 确认后正式删除 confirm input(确认删除以上仓库输入 yes 继续: ) if confirm.lower() ! yes: print(已取消) exit() success 0 failed 0 for repo in targets: ok delete_repo(repo[full_name]) if ok: success 1 else: failed 1 # 控制请求频率避免触发限速 time.sleep(0.5) print(f删除完成成功 {success} 个失败 {failed} 个)请求间隔的0.5秒是我在实际操作中调出来的一个比较稳妥的数值。GitHub官方对删除接口的配额限制没有明确写每秒几次但实测中如果连续快速发送删除请求偶尔会碰到502或403。加上0.5秒的sleep200个仓库也就多花100秒换取更低的失败率完全值得。3.5 gh CLI的替代做法如果你更喜欢命令行操作、不想写Python脚本用gh CLI也能实现批量删除而且代码更短。前提是你已经安装并登录了gh CLI。先拉取仓库列表用JSON格式输出然后通过jq筛选出目标仓库名gh repo list 你的用户名 --limit 500 \ --json name,owner,isFork,isArchived,updatedAt \ --jq .[] | select(.isFork true) | .owner.login / .name这条命令会列出所有fork仓库的完整名称。你可以把isFork true换成其他筛选条件比如(.name | test(test))匹配名字含test的仓库或者用updatedAt做时间比较。筛选结果没问题后再用循环调用gh repo delete删除gh repo list 你的用户名 --limit 500 \ --json name,owner,isFork,isArchived \ --jq .[] | select(.isFork true) | .owner.login / .name \ | while read repo; do gh repo delete $repo --yes sleep 1 done--yes参数用来跳过交互式确认因为循环里没法手动输入确认内容。这个方案跑起来很快但我的个人建议是第一次执行时先不要带--yes让命令停在一个仓库上手动确认一次确认删除流程正常后再用--yes跑剩余部分。4. 进阶场景筛选策略、fork仓库与组织仓库基础版脚本能应对大多数情况但真实世界里还有几个高频场景需要额外处理。这里挑三个最常踩坑的展开说。4.1 精准筛选别把想留的仓库也删了批量删除最怕两件事一是该删的没删二是不该删的被误删了。前者顶多算没清理干净后者才是真正的灾难。所以筛选逻辑一定要做减法。我的习惯是先限定条件再逐条排除。举例来说我想清理所有fork仓库但不想删除自己曾经为某个开源项目提交过PR的fork。那么我可以在筛选时增加一个判断查询该仓库的parent字段如果parent存在且自己仓库里有新commit就跳过。再比如我想清理一年没更新的仓库但某个仓库虽然一年没更新却是我博客的图床删了之后图片全挂。这种靠代码很难识别所以我建议在正式删除前把目标清单导出来人工浏览一遍。200个仓库的清单看起来多但扫一眼名字只需要几分钟这几分钟能救回你不少数据。更稳妥的做法是先执行一次“软删除”也就是把仓库先归档Archive而不是直接删除。归档后的仓库会变成只读不影响数据也不占活跃仓库列表。放个一两周确认没有依赖这些仓库的服务报错再执行真正的删除。这个思路在批量清理老项目时尤其好用。4.2 大量fork仓库的批量处理fork仓库是仓库列表膨胀的头号元凶。GitHub没有提供“一键取消所有fork”的功能但API层面允许你直接删除fork仓库不需要先解除fork关系。也就是说你完全可以像删除普通仓库一样对fork仓库调用DELETE /repos/{owner}/{repo}接口。不过有一件事要注意如果你删除了一个自己有过commit的fork仓库这些commit本身不会从上游项目消失但你的fork关系会中断。如果以后还想给上游项目提交PR需要重新fork。删除fork之前务必确认你不需要保留fork里的任何分支改动。批量删除fork时我建议在筛选条件里加上fork: true只处理fork仓库避免误删自己的原生项目。同时因为fork仓库数量通常很大脚本跑起来的时间会相对长记得把sleep间隔稍微拉大一点比如1秒防止中途被限速打断。4.3 组织Organization仓库的删除权限如果你要删除的仓库属于某个组织不是个人账号删除权限会有额外要求。只有组织的owner拥有者或者被授予了仓库删除权限的成员才能删除组织下的仓库。普通成员即使对仓库有push权限也没有删除权限。组织仓库的删除API跟个人仓库是同一个接口但token的权限要求更高。你需要确保token对该组织有delete_repo权限并且在OAuth App或Fine-grained token的配置中将该组织纳入授权范围。如果遇到403错误先检查是不是token对组织的权限不够而不是纠结脚本逻辑。删除组织仓库还需要注意组织有时会设置“仓库删除保护策略”要求删除操作必须经过特定流程比如先归档再删除。这种情况下API请求可能直接被拒绝你需要先去组织设置里看有没有这类限制。我在实际接手一个历史组织时就遇到过这种情况最后是联系组织owner确认策略后才顺利删除。4.4 用“清空仓库数据”替代“删除仓库”有些仓库本身还想要只是需要清空里面的内容比如要复用仓库名来开启一个新项目。这时候其实不需要删除仓库只需清空仓库内所有分支和文件。清空方式有两种。一种是直接删除默认分支下所有文件再提交一个空commit但这种方式比较麻烦尤其是分支和tag较多时。另一种更优雅的方式是用git push删除所有分支和tag然后把默认分支reset到一个空提交上。简单粗暴的方法其实是新建一个空仓库把旧仓库的内容全部推送过去确认无误后删除旧仓库、同时把新仓库改名为旧仓库的名字。但这样操作后原有仓库的issue、PR、Star、Fork等社交数据全部会丢所以只适合完全不care这些数据的仓库。如果你只是想把代码清空、保留仓库元数据可以用GitHub原生支持的“导入空仓库”技巧在本地用git init初始化一个包含一个README.md的空仓库推送到远程仓库然后强制覆盖远程分支git init git add README.md git commit -m chore: reset repo git branch -M main git remote add origin gitgithub.com:你的用户名/仓库名.git git push -f origin main这样远程仓库只剩一个main分支和空的README算是给仓库做了一次“格式化”。这个操作不会影响仓库的Star、Fork、issue等元数据适合那些还想保留社交资产、只是代码需要重建的仓库。5. 常见问题与排查实录批量删除遇坑总结批量删除过程中最容易遇到的问题集中在认证、限速和误删后的恢复上。我把实际踩过的坑和排查思路整理成速查表你可以直接对照处理。错误信息可能原因解决方案HTTP 403 资源不可用token权限不足检查token是否勾选delete_repo权限组织仓库需确认组织授权HTTP 404 资源不存在仓库名写错或不属于当前用户用/user/repos接口确认full_name拼写HTTP 451/0 网络异常请求过程中网络波动增加重试机制或调大请求间隔后重跑HTTP 502 Bad Gateway请求频率过高触发了网关错误增大sleep间隔降到每秒不到1次HTTP 422 校验失败仓库存在未处理的状态确认仓库不是归档状态先解除归档再删除5.1 403权限错误是最常见的坑几乎每位自己写脚本调GitHub API的人都会在删除接口上遇到一次403。我自己第一次写脚本时也卡在这里排查了半天才发现是token权限没勾对。403的排查方法很简单先拿token调一次GET /user如果能返回用户名信息说明token本身是有效的。再调一次DELETE /repos/{owner}/{repo}如果返回403那就基本可以确定是权限范围问题。回到token设置页面把delete_repo权限勾上重新生成token再试。还有一个容易被忽视的点token的repo大类权限包含public_repo和delete_repo但classic token里这两项是分开勾选的。如果你只勾了public_repo那么删除私有仓库时仍然会403。所以正确的勾选方式是至少勾上repo整个大类并确保delete_repo也在其中。5.2 删除后如何恢复误删的仓库GitHub官方对误删仓库支持恢复但前提是在删除后90天内提出申请且你的账号还在正常状态。恢复方式有两种一是登录GitHub网页端打开https://github.com/settings/repos页面找到最近删除的仓库列表点击Restore按钮即可二是如果列表里找不到或者数量太多可以联系GitHub Support提供仓库名和删除时间官方人员在验证账号身份后会帮你恢复。需要注意恢复操作只能恢复仓库本体恢复后仓库会失去原有的GitHub Pages、Webhook设置以及仓库的Stars、Forks等社交数据。代码本身、commit历史、分支和tag一般都能完整恢复。这个恢复窗口给了我很大的安全感但也不要因此掉以轻心。恢复操作每次都需要人工介入而且窗口只有90天超过期限就真的救不回来了。所以我的建议依然是删除前列清单、删前干跑、删后至少检查一遍清单能避免的误会尽量自己避免。5.3 网络波动导致脚本中断的处理方法批量删除过程中如果网络不稳定脚本会在某个仓库的DELETE请求上卡住超时甚至直接中断。这种情况我遇到过一次当时已经删掉了70多个仓库脚本卡在某个请求上我一度以为前面的都白干了。解决方法是在脚本里增加重试机制和断点续跑能力。重试逻辑很简单遇到网络异常或5xx状态码时等待几秒后重试同一仓库最多重试3次。断点续跑则稍复杂一点需要把待删除清单记录到文件里每次成功删除一个就标记一个脚本中断后重新运行时跳过已删除的仓库。下面这段代码演示了一个带重试和断点功能的删除逻辑import os import time def load_remaining(): if not os.path.exists(targets.txt): return [] with open(targets.txt) as f: return [line.strip() for line in f if line.strip()] def save_remaining(remaining): with open(targets.txt, w) as f: for full_name in remaining: f.write(full_name \n) def delete_with_retry(full_name, retries3): for attempt in range(retries): try: resp requests.delete( fhttps://api.github.com/repos/{full_name}, headersHEADERS, timeout30, ) if resp.status_code 204: return True # 5xx错误或限速错误才重试 if resp.status_code in (429, 500, 502, 503): time.sleep(5 * (attempt 1)) continue return False except requests.RequestException: time.sleep(3) return False remaining load_remaining() for full_name in remaining: ok delete_with_retry(full_name) if ok: print(f已删除: {full_name}) else: print(f失败: {full_name}) remaining.remove(full_name) save_remaining(remaining) time.sleep(0.5)这样即使脚本中途崩了重新运行后也会从上次未完成的仓库继续不会重复删除已经删掉的仓库也不会遗漏。6. 最后再聊几点实际体会批量删除GitHub仓库这件事技术上不难真正考验人的是流程设计和风险控制。从我处理200多个仓库的实际经历来看有几点经验值得拿出来单独说。第一先梳理清楚再动手。动手之前花半小时把仓库列表拉下来按照“必须删”“可删可不删”“绝对不删”三个分类过一遍远比删到一半发现某个仓库很重要要省心得多。我那次清理前期整理清单花了40分钟真正的删除只用了不到10分钟。第二尽量分阶段操作。不要一次性把所有目标仓库全删了先删一批明确没用的观察几天确认没有影响任何服务或本地开发环境再处理下一批。这种“小步快跑”的策略在数据删除类操作里永远是最稳妥的。第三习惯用干跑模式。脚本里保留一个dry_run开关每次改完筛选条件后先跑一遍打印待删除清单人工确认无误后再正式执行。这个习惯帮我拦下过好几次误筛比如有一次筛选条件不小心把fork false写成了fork True干跑时立刻发现了问题避免了一场数据事故。最后再说一句GitHub仓库删除以后仓库名会立刻释放别人可以马上注册同名仓库。如果你有“删除后立刻重建同名仓库”的需求尽量在删除前先把要保留的数据备份好然后快速执行删除和重建。这个操作虽然简单但需要在删除和重建之间留出足够小的间隔否则名称可能被别人抢注。批量删除仓库只是仓库日常管理的一部分。清理完之后建议顺势建立一套管理机制比如定期归档不活跃仓库、fork仓库统一收进一个archive分类或者直接清理、对命名做统一规范。这样下次再需要大规模清理时你的仓库列表本身就干净清爽得多。说白了批量删除是亡羊补牢而好的仓库管理习惯才能从根本上避免仓库数量失控。