乐于分享
好东西不私藏

一个简单的GitHub项目更新软件

一个简单的GitHub项目更新软件
提到管理GitHub代码,第一个想到的就是Git,于是我们就根据Git命令写一个python编写的ui界面,实现简单的拉取和更新github项目的目的。
python代码如下
"""GitHub 项目更新工具功能:一键 Pull(同步远程到本地)或 Push(提交并推送到远程)依赖:Python 内置库(tkinter、subprocess、os、threading)打包:pyinstaller --onefile --windowed github_updater.py"""import tkinter as tkfrom tkinter import ttk, filedialog, scrolledtext, messageboximport subprocessimport osimport threadingimport jsonCONFIG_FILE = "github_updater_config.json"DEFAULT_GITIGNORE = """.Rhistory*.exe*.zip*.pyc__pycache__/.DS_Store"""# ────────────────────────────────────────────────────────────────────────────# 配置持久化# ────────────────────────────────────────────────────────────────────────────def load_config() -> dict:    if os.path.exists(CONFIG_FILE):        try:            with open(CONFIG_FILE, "r", encoding="utf-8"as f:                return json.load(f)        except Exception:            pass    return {}def save_config(data: dict):    try:        with open(CONFIG_FILE, "w", encoding="utf-8"as f:            json.dump(data, f, ensure_ascii=False, indent=2)    except Exception:        pass# ────────────────────────────────────────────────────────────────────────────# Git 工具函数# ────────────────────────────────────────────────────────────────────────────def build_remote_url(user: str, repo: str, token: str) -> str:    """构造带 token 的 HTTPS 远程地址(格式与 bash 脚本一致)。"""    repo = repo.strip().rstrip("/")    if not repo.endswith(".git"):        repo += ".git"    return f"https://{user.strip()}:{token.strip()}@github.com/{user.strip()}/{repo}"def build_clean_url(user: str, repo: str) -> str:    """不含 token 的干净远程地址(push 后用于还原,避免 token 残留)。"""    repo = repo.strip().rstrip("/")    if not repo.endswith(".git"):        repo += ".git"    return f"https://github.com/{user.strip()}/{repo}"def run_git(args: list[str], cwd: str) -> tuple[intstr]:    """执行 git 命令,返回 (returncode, output)。"""    try:        result = subprocess.run(            ["git"] + args,            cwd=cwd,            capture_output=True,            text=True,            encoding="utf-8",            errors="replace",            env=os.environ.copy(),        )        return result.returncode, result.stdout + result.stderr    except FileNotFoundError:        return -1"错误:未找到 git 命令,请确保已安装 Git 并加入 PATH。"    except Exception as e:        return -1f"执行出错:{e}"# ────────────────────────────────────────────────────────────────────────────# 主窗口# ────────────────────────────────────────────────────────────────────────────class App(tk.Tk):    def __init__(self):        super().__init__()        self.title("GitHub 项目更新工具")        self.resizable(TrueTrue)        self.minsize(660600)        self._build_ui()        self._load_saved_config()    # ── UI 构建 ──────────────────────────────────────────────────────────────    def _build_ui(self):        pad = {"padx"10"pady"4}        # ── 账号配置 ──        acct_frame = ttk.LabelFrame(self, text="GitHub 账号配置")        acct_frame.pack(fill="x", **pad)        # 用户名        ttk.Label(acct_frame, text="GitHub 用户名:").grid(row=0, column=0, sticky="w", **pad)        self.github_user = tk.StringVar()        ttk.Entry(acct_frame, textvariable=self.github_user, width=30).grid(            row=0, column=1, sticky="ew", **pad)        # 仓库名        ttk.Label(acct_frame, text="仓库名(Repo):").grid(row=1, column=0, sticky="w", **pad)        self.repo_name = tk.StringVar()        ttk.Entry(acct_frame, textvariable=self.repo_name, width=30).grid(            row=1, column=1, sticky="ew", **pad)        # Token        ttk.Label(acct_frame, text="Personal Access Token:").grid(row=2, column=0, sticky="w", **pad)        self.token = tk.StringVar()        self.token_entry = ttk.Entry(acct_frame, textvariable=self.token, width=50, show="*")        self.token_entry.grid(row=2, column=1, sticky="ew", **pad)        self.show_token = tk.BooleanVar(value=False)        ttk.Checkbutton(acct_frame, text="显示", variable=self.show_token,                        command=self._toggle_token).grid(row=2, column=2, **pad)        acct_frame.columnconfigure(1, weight=1)        # ── 本地项目配置 ──        proj_frame = ttk.LabelFrame(self, text="本地项目配置")        proj_frame.pack(fill="x", **pad)        # 本地目录        ttk.Label(proj_frame, text="本地项目目录:").grid(row=0, column=0, sticky="w", **pad)        self.local_dir = tk.StringVar()        ttk.Entry(proj_frame, textvariable=self.local_dir, width=48).grid(            row=0, column=1, sticky="ew", **pad)        ttk.Button(proj_frame, text="浏览…", command=self._browse_dir).grid(row=0, column=2, **pad)        # 分支        ttk.Label(proj_frame, text="分支名称:").grid(row=1, column=0, sticky="w", **pad)        self.branch = tk.StringVar(value="master")        ttk.Entry(proj_frame, textvariable=self.branch, width=20).grid(            row=1, column=1, sticky="w", **pad)        proj_frame.columnconfigure(1, weight=1)        # ── Push 选项 ──        push_frame = ttk.LabelFrame(self, text="Push 选项")        push_frame.pack(fill="x", **pad)        ttk.Label(push_frame, text="提交说明(Commit message):").grid(row=0, column=0, sticky="w", **pad)        self.commit_msg = tk.StringVar(value="update")        ttk.Entry(push_frame, textvariable=self.commit_msg, width=45).grid(            row=0, column=1, sticky="ew", **pad)        self.auto_init = tk.BooleanVar(value=True)        ttk.Checkbutton(push_frame, text="目录非 git 仓库时自动 git init",                        variable=self.auto_init).grid(row=1, column=1, sticky="w", **pad)        self.auto_gitignore = tk.BooleanVar(value=True)        ttk.Checkbutton(push_frame, text="不存在 .gitignore 时自动创建",                        variable=self.auto_gitignore).grid(row=2, column=1, sticky="w", **pad)        # .gitignore 内容编辑        ttk.Label(push_frame, text=".gitignore 内容:").grid(row=3, column=0, sticky="nw", **pad)        self.gitignore_text = tk.Text(push_frame, height=4, width=45, font=("Consolas"9))        self.gitignore_text.insert("1.0", DEFAULT_GITIGNORE)        self.gitignore_text.grid(row=3, column=1, sticky="ew", **pad)        self.clean_token = tk.BooleanVar(value=True)        ttk.Checkbutton(push_frame, text="Push 后从 git 配置中移除 token(推荐)",                        variable=self.clean_token).grid(row=4, column=1, sticky="w", **pad)        push_frame.columnconfigure(1, weight=1)        # ── 记住配置 ──        misc_frame = ttk.Frame(self)        misc_frame.pack(fill="x", padx=10, pady=2)        self.remember = tk.BooleanVar(value=True)        ttk.Checkbutton(misc_frame, text="记住配置(保存到 github_updater_config.json)",                        variable=self.remember).pack(side="left")        # ── 按钮区 ──        btn_frame = ttk.Frame(self)        btn_frame.pack(fill="x", padx=10, pady=6)        ttk.Button(btn_frame, text="⬆  Push(提交推送)", width=22,                   command=self._on_push).pack(side="left", padx=6)        ttk.Button(btn_frame, text="⬇  Pull(拉取更新)", width=22,                   command=self._on_pull).pack(side="left", padx=6)        ttk.Button(btn_frame, text="🔄  Clone(初次克隆)", width=22,                   command=self._on_clone).pack(side="left", padx=6)        ttk.Button(btn_frame, text="清空日志", width=10,                   command=self._clear_log).pack(side="right", padx=6)        # ── 日志区 ──        log_frame = ttk.LabelFrame(self, text="操作日志")        log_frame.pack(fill="both", expand=True, padx=10, pady=4)        self.log_box = scrolledtext.ScrolledText(            log_frame, state="disabled", height=12,            font=("Consolas"9), wrap="word"        )        self.log_box.pack(fill="both", expand=True, padx=6, pady=6)        # 状态栏        self.status_var = tk.StringVar(value="就绪")        ttk.Label(self, textvariable=self.status_var, anchor="w",                  relief="sunken").pack(fill="x", side="bottom")    # ── UI 辅助 ──────────────────────────────────────────────────────────────    def _browse_dir(self):        d = filedialog.askdirectory(title="选择本地项目目录")        if d:            self.local_dir.set(d)    def _toggle_token(self):        self.token_entry.config(show="" if self.show_token.get() else "*")    def _log(self, text: str):        def _append():            self.log_box.config(state="normal")            self.log_box.insert("end", text + "\n")            self.log_box.see("end")            self.log_box.config(state="disabled")        self.after(0, _append)    def _clear_log(self):        self.log_box.config(state="normal")        self.log_box.delete("1.0""end")        self.log_box.config(state="disabled")    def _set_status(self, msg: str):        self.after(0lambdaself.status_var.set(msg))    # ── 配置持久化 ────────────────────────────────────────────────────────────    def _load_saved_config(self):        cfg = load_config()        for key, var in [            ("github_user"self.github_user),            ("repo_name",   self.repo_name),            ("token",       self.token),            ("branch",      self.branch),            ("commit_msg",  self.commit_msg),            ("local_dir",   self.local_dir),        ]:            if cfg.get(key):                var.set(cfg[key])        if cfg.get("gitignore_content"):            self.gitignore_text.delete("1.0""end")            self.gitignore_text.insert("1.0", cfg["gitignore_content"])    def _maybe_save_config(self):        if self.remember.get():            save_config({                "github_user":      self.github_user.get(),                "repo_name":        self.repo_name.get(),                "token":            self.token.get(),                "branch":           self.branch.get(),                "commit_msg":       self.commit_msg.get(),                "local_dir":        self.local_dir.get(),                "gitignore_content"self.gitignore_text.get("1.0""end"),            })    # ── 输入校验 ──────────────────────────────────────────────────────────────    def _validate(self, need_local=True) -> bool:        if not self.github_user.get().strip():            messagebox.showwarning("提示""请填写 GitHub 用户名。")            return False        if not self.repo_name.get().strip():            messagebox.showwarning("提示""请填写仓库名(Repo)。")            return False        if not self.token.get().strip():            messagebox.showwarning("提示""请填写 Personal Access Token。")            return False        if need_local and not self.local_dir.get().strip():            messagebox.showwarning("提示""请先选择本地项目目录。")            return False        return True    # ── 共用:初始化本地仓库 ──────────────────────────────────────────────────    def _ensure_git_repo(self, local: str) -> bool:        """若目录不是 git 仓库且勾选了自动 init,则执行 git init。"""        if os.path.isdir(os.path.join(local, ".git")):            return True        if self.auto_init.get():            self._log("[init] 检测到非 git 仓库,正在执行 git init…")            code, out = run_git(["init"], cwd=local)            self._log(out.strip())            if code != 0:                self._log("❌ git init 失败。")                return False            # 设置默认分支名            branch = self.branch.get().strip() or "master"            run_git(["checkout""-b", branch], cwd=local)            return True        else:            self._log('❌ 该目录不是 git 仓库,请先使用【Clone】或勾选"自动 git init"。')            return False    def _ensure_gitignore(self, local: str):        """若不存在 .gitignore 且勾选了自动创建,则写入。"""        path = os.path.join(local, ".gitignore")        if not os.path.exists(path) and self.auto_gitignore.get():            content = self.gitignore_text.get("1.0""end")            with open(path, "w", encoding="utf-8"as f:                f.write(content)            self._log("[.gitignore] 已自动创建 .gitignore")    def _setup_remote(self, local: str, remote_url: str):        """移除旧 origin 再添加新 origin(含 token)。"""        run_git(["remote""remove""origin"], cwd=local)        code, out = run_git(["remote""add""origin", remote_url], cwd=local)        if code != 0:            self._log(f"[警告] 设置 remote 失败:{out.strip()}")    # ── 操作:Push ────────────────────────────────────────────────────────────    def _on_push(self):        if not self._validate():            return        if not self.commit_msg.get().strip():            messagebox.showwarning("提示""请填写提交说明(Commit message)。")            return        self._maybe_save_config()        threading.Thread(target=self._push_task, daemon=True).start()    def _push_task(self):        local  = self.local_dir.get().strip()        branch = self.branch.get().strip() or "master"        msg    = self.commit_msg.get().strip()        user   = self.github_user.get().strip()        repo   = self.repo_name.get().strip()        token  = self.token.get().strip()        remote_url = build_remote_url(user, repo, token)        clean_url  = build_clean_url(user, repo)        self._set_status("正在推送…")        self._log(f"\n{'='*50}")        self._log(f"[Push] 目录:{local}  分支:{branch}  提交说明:{msg}")        # 1. 确保是 git 仓库        if not self._ensure_git_repo(local):            self._set_status("Push 失败")            return        # 2. 自动创建 .gitignore        self._ensure_gitignore(local)        # 3. 设置 remote        self._setup_remote(local, remote_url)        # 4. git add .        code, out = run_git(["add""."], cwd=local)        self._log(out.strip() if out.strip() else "[add] 已暂存所有改动")        if code != 0:            self._log("❌ git add 失败。")            self._set_status("Push 失败")            return        # 5. 检查是否有改动(git diff --cached --quiet)        check_code, _ = run_git(["diff""--cached""--quiet"], cwd=local)        if check_code == 0:            self._log("⚠️ 没有新文件需要提交(工作区与最后一次提交相同)。")            self._set_status("无改动,跳过提交")        else:            # 6. git commit            code, out = run_git(["commit""-m", msg], cwd=local)            self._log(out.strip() if out.strip() else "(无 commit 输出)")            if code != 0:                self._log("❌ git commit 失败,请查看上方日志。")                self._set_status("Push 失败")                return        # 7. git push -u origin branch        code, out = run_git(["push""-u""origin", branch], cwd=local)        self._log(out.strip() if out.strip() else "(无 push 输出)")        # 8. Push 后清除 token(安全措施)        if self.clean_token.get():            run_git(["remote""set-url""origin", clean_url], cwd=local)            self._log(f"[安全] token 已从 git 配置中移除,远程地址已还原为:{clean_url}")        if code == 0:            self._log("✅ Push 成功!")            self._log("⚠️  建议在 GitHub 设置中定期轮换(regenerate)此 token。")            self._set_status("Push 完成")        else:            self._log("❌ Push 失败,请查看上方日志。")            self._set_status("Push 失败")    # ── 操作:Pull ────────────────────────────────────────────────────────────    def _on_pull(self):        if not self._validate():            return        self._maybe_save_config()        threading.Thread(target=self._pull_task, daemon=True).start()    def _pull_task(self):        local  = self.local_dir.get().strip()        branch = self.branch.get().strip() or "master"        user   = self.github_user.get().strip()        repo   = self.repo_name.get().strip()        token  = self.token.get().strip()        remote_url = build_remote_url(user, repo, token)        clean_url  = build_clean_url(user, repo)        self._set_status("正在拉取…")        self._log(f"\n{'='*50}")        self._log(f"[Pull] 目录:{local}  分支:{branch}")        if not os.path.isdir(os.path.join(local, ".git")):            self._log("❌ 该目录不是 git 仓库,请先使用【Clone】按钮初始化。")            self._set_status("Pull 失败")            return        self._setup_remote(local, remote_url)        code, out = run_git(["pull""origin", branch], cwd=local)        self._log(out.strip() if out.strip() else "(无输出)")        # 还原 remote(移除 token)        if self.clean_token.get():            run_git(["remote""set-url""origin", clean_url], cwd=local)            self._log(f"[安全] token 已从 git 配置中移除。")        if code == 0:            self._log("✅ Pull 成功!")            self._set_status("Pull 完成")        else:            self._log("❌ Pull 失败,请查看上方日志。")            self._set_status("Pull 失败")    # ── 操作:Clone ───────────────────────────────────────────────────────────    def _on_clone(self):        if not self._validate(need_local=False):            return        parent = filedialog.askdirectory(title="选择克隆到哪个父目录")        if not parent:            return        self._maybe_save_config()        threading.Thread(target=self._clone_task, args=(parent,), daemon=True).start()    def _clone_task(self, parent: str):        user   = self.github_user.get().strip()        repo   = self.repo_name.get().strip()        token  = self.token.get().strip()        branch = self.branch.get().strip()        remote_url = build_remote_url(user, repo, token)        repo_name  = repo.replace(".git""")        target_dir = os.path.join(parent, repo_name)        self._set_status("正在克隆…")        self._log(f"\n{'='*50}")        # 先尝试指定分支,失败则克隆默认分支        if branch:            self._log(f"[Clone] 目标目录:{target_dir}  分支:{branch}")            code, out = run_git(["clone""-b", branch, remote_url], cwd=parent)            if code != 0:                self._log(f"[警告] 分支 '{branch}' 不存在,改为克隆默认分支…")                code, out = run_git(["clone", remote_url], cwd=parent)        else:            self._log(f"[Clone] 目标目录:{target_dir}  使用仓库默认分支")            code, out = run_git(["clone", remote_url], cwd=parent)        self._log(out.strip() if out.strip() else "(无输出)")        if code == 0:            # 检测实际分支(symbolic-ref 在空仓库也可用)            b_code, b_out = run_git(["symbolic-ref""--short""HEAD"], cwd=target_dir)            actual = b_out.strip() if b_code == 0 and b_out.strip() else (branch or "master")            self._log(f"✅ Clone 成功!当前分支:{actual}")            self._set_status("Clone 完成")            self.after(0lambdaself.branch.set(actual))            self.after(0lambdaself.local_dir.set(target_dir))        else:            self._log("❌ Clone 失败,请查看上方日志。")            self._set_status("Clone 失败")# ────────────────────────────────────────────────────────────────────────────if __name__ == "__main__":    app = App()    app.mainloop()
然后需要exe可以使用pyinstaller --onefile --windowed --name "GitHub更新工具" github_updater.py转换成GitHub更新工具.exe
界面很简单
这里就是需要生成一个GitHub的token,方便上传
第一步,打开https://github.com/settings/tokens,进入token管理界面,然后点击Generate new token(classic),
填写你想要的名字和可以使用token的地方,我全选了
点击Generate token即可生成
记得复制保存对应的token,只显示一次
然后我们就可以填到GitHub更新工具.exe对应的token填写位置
怎么用呢,如果还没有初始化,我们点击
就在本地新建了一个.git文件,后面就在对应的文件夹下添加或修改自己的文件,代码等,修改完之后点击
就可以完成推送,如果GitHub项目中已经有内容了,我们可以使用
检测后在进行
就可以完成GitHub项目的更新了。
代码很简单,实现的结果也很简单,使用token,避免失败的情况。
关于这里,我们打开新建的GitHub项目,可以看到
这里的https://github.com/chen167777/myip.git就有我们填写的信息,按照这个来填写即可。