Jacky's blog
首页
  • 学习笔记

    • web
    • android
    • iOS
    • vue
  • 分类
  • 标签
  • 归档
收藏
  • tool
  • algo
  • python
  • java
  • server
  • growth
  • frida
  • blog
  • SP
  • more
GitHub (opens new window)

Jack Yang

编程; 随笔
首页
  • 学习笔记

    • web
    • android
    • iOS
    • vue
  • 分类
  • 标签
  • 归档
收藏
  • tool
  • algo
  • python
  • java
  • server
  • growth
  • frida
  • blog
  • SP
  • more
GitHub (opens new window)
  • 服务器tutorial
  • spring

  • 数据库

  • 运维

    • SSH 进行本地文件的交互
    • SSH Tunnel结合本地代理案例
    • 查询本地电脑外网 IP
    • dns查询
    • 内网穿透
    • macOS 定时任务实现:Git 仓库状态自动同步方案
      • 1. 背景
      • 2. 环境说明
      • 3. 实现过程
        • 3.1 编写同步脚本
        • 3.2 配置定时任务 (plist)
        • 3.3 python 脚本
        • 3.4 任务操作指令
      • 4. 问题排查与细节优化
        • 4.1 常见状态码排查
        • 4.2 核心踩坑与解决
      • 5. 总结
  • 运营

  • other

  • 《server》
  • 运维
Jacky
2026-04-26
目录

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

# 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

# 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 核心踩坑与解决

  1. 非交互式环境下的路径失效:launchd 启动任务时不加载用户的 .zshrc 或 .bash_profile。
    • 修复:不能使用 python3 这种依赖 PATH 的命令。必须通过 pyenv which python3 获取真实物理路径并填入 plist。
  2. 文件执行权限:确保 Python 脚本具备可执行位。
    • 操作:chmod +x /Users/<USER_NAME>/<PATH_TO_SCRIPT>/git_keepalive.py
  3. 日志实时审计:如果脚本没反应,首先检查 StandardErrorPath 指定的日志文件。
    • 操作:tail -f /tmp/git_fetch_stdout.log

# 5. 总结

通过 launchd 结合 Python 脚本,可以实现一套极低侵入性的 Git 权限保护机制。实现的关键在于 “绝对路径的显式声明” 以及对 “退出状态码” 的准确追踪。该方案也同样适用于其他需要在 macOS 上定时运行的数据同步或系统维护任务。

#运维#mac
上次更新: 2026/04/26, 17:13:52
内网穿透
电商运营核心指标图表

← 内网穿透 电商运营核心指标图表→

最近更新
01
fzf
04-11
02
tmux
03-30
03
AI TOP10基础问题
03-19
更多文章>
Theme by Vdoing | Copyright © 2019-2026 Jacky | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式