macOS 定时任务实现:Git 仓库状态自动同步方案
# 1. 背景
在多人协作的大型项目中,Git 权限管理通常伴随活跃度检测机制。若本地仓库长期未与远程端通信(如执行 fetch),可能导致权限被回收或访问令牌失效。
- 核心诉求:自动化、静默地维护本地所有工作仓库的活跃状态。
- 选型理由:使用 macOS 原生的
launchd(plist) 机制。相比crontab,它能更好地处理系统休眠与唤醒,且支持更完善的日志重定向和资源管理。
# 2. 环境说明
- 系统:macOS
- 工具:Python 3 + Git + LaunchAgent
# 3. 实现过程
# 3.1 编写同步脚本
编写 Python 脚本遍历指定的 <WORK_SPACE_DIR>,识别所有包含 .git 的目录,并执行 git fetch --all。
注意:脚本应包含详细的内部日志记录,以便追溯具体仓库的更新情况。
# 3.2 配置定时任务 (plist)
在 ~/Library/LaunchAgents/ 下创建任务描述文件 <TASK_LABEL>.plist。
关键配置项详解:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.placeholder.gitfetch</string>
<key>ProgramArguments</key>
<array>
<string>/Users/<USER_NAME>/.pyenv/versions/<PYTHON_VERSION>/bin/python3</string>
<string>/Users/<USER_NAME>/<PATH_TO_SCRIPT>/git_keepalive.py</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>10</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>WorkingDirectory</key>
<string>/Users/<USER_NAME>/<WORK_SPACE_DIR></string>
<key>StandardOutPath</key>
<string>/tmp/git_fetch_stdout.log</string>
<key>StandardErrorPath</key>
<string>/tmp/git_fetch_err.log</string>
</dict>
</plist>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 3.3 python 脚本
import os
import subprocess
from datetime import datetime
# ================= 配置区 =================
# 获取脚本运行时的当前目录
WORKSPACE = os.getcwd()
# 日志存放路径(放在当前目录下)
LOG_FILE = os.path.join(WORKSPACE, "git_keepalive_log.txt")
# ==========================================
def log(message):
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(f"[{now}] {message}\n")
print(message)
def keep_alive():
log(f"--- 开始唤醒 Git 权限 ---")
log(f"当前工作根目录: {WORKSPACE}")
count = 0
# os.walk 会递归遍历目录下所有子文件夹
for root, dirs, files in os.walk(WORKSPACE):
# 检查当前目录下是否有 .git 文件夹
if '.git' in dirs:
git_repo_path = root
log(f"发现仓库: {os.path.basename(git_repo_path)} -> 正在 Fetch...")
try:
# 执行 git fetch --all (更新所有远程分支状态)
# 使用 timeout 防止某个仓库因网络问题卡死脚本
result = subprocess.run(
["git", "fetch", "--all"],
cwd=git_repo_path,
capture_output=True,
text=True,
timeout=60
)
if result.returncode == 0:
log(f"成功: {git_repo_path}")
count += 1
else:
log(f"失败: {git_repo_path}\n原因: {result.stderr.strip()}")
except subprocess.TimeoutExpired:
log(f"跳过: {git_repo_path} (连接超时)")
except Exception as e:
log(f"异常: {git_repo_path} - {str(e)}")
# 性能优化:找到 .git 后,不再进入其内部子目录遍历
dirs.remove('.git')
log(f"任务完成,共更新 {count} 个仓库\n")
if __name__ == "__main__":
keep_alive()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# 3.4 任务操作指令
使用 launchctl 命令行工具管理任务生命周期:
- 加载并生效:
launchctl load ~/Library/LaunchAgents/<TASK_LABEL>.plist - 卸载任务:
launchctl unload ~/Library/LaunchAgents/<TASK_LABEL>.plist - 立即强制执行:
launchctl start <TASK_LABEL>(用于验证配置是否正确) - 停止任务:
launchctl stop <TASK_LABEL>
# 4. 问题排查与细节优化
# 4.1 常见状态码排查
运行 launchctl list | grep <TASK_LABEL> 查看第二列的退出码:
- Exit Code 0:运行成功。
- Exit Code 78:配置错误(EX_CONFIG)。通常由
plist中指定的WorkingDirectory不存在或执行文件路径写错导致。 - Exit Code 127/1:脚本执行报错或环境缺失。
# 4.2 核心踩坑与解决
- 非交互式环境下的路径失效:
launchd启动任务时不加载用户的.zshrc或.bash_profile。- 修复:不能使用
python3这种依赖 PATH 的命令。必须通过pyenv which python3获取真实物理路径并填入plist。
- 修复:不能使用
- 文件执行权限:确保 Python 脚本具备可执行位。
- 操作:
chmod +x /Users/<USER_NAME>/<PATH_TO_SCRIPT>/git_keepalive.py
- 操作:
- 日志实时审计:如果脚本没反应,首先检查
StandardErrorPath指定的日志文件。- 操作:
tail -f /tmp/git_fetch_stdout.log
- 操作:
# 5. 总结
通过 launchd 结合 Python 脚本,可以实现一套极低侵入性的 Git 权限保护机制。实现的关键在于 “绝对路径的显式声明” 以及对 “退出状态码” 的准确追踪。该方案也同样适用于其他需要在 macOS 上定时运行的数据同步或系统维护任务。
上次更新: 2026/04/26, 17:13:52