xiaolongmr commited on
Commit
a48700a
·
1 Parent(s): b540c69

Remove HF push tool from app project

Browse files
.gitignore CHANGED
@@ -20,7 +20,6 @@ docker-compose-local.yml
20
  # Environment variables
21
  .env
22
  .env.local
23
- scripts/hf_spaces.json
24
 
25
  # Database files
26
  *.db
 
20
  # Environment variables
21
  .env
22
  .env.local
 
23
 
24
  # Database files
25
  *.db
docs/hf-space-push-tool.md DELETED
@@ -1,88 +0,0 @@
1
- # Hugging Face Space 多账号推送工具
2
-
3
- 这个工具用于把当前项目的当前 Git 提交推送到多个 Hugging Face 账号下的多个 Space,并支持多选目标。
4
-
5
- ## 图形化界面(推荐)
6
-
7
- 双击或运行:
8
-
9
- ```powershell
10
- scripts\start_hf_push_gui.bat
11
- ```
12
-
13
- 也可以直接运行:
14
-
15
- ```powershell
16
- python scripts\hf_space_push_gui.py
17
- ```
18
-
19
- 界面里可以:
20
-
21
- - 添加多个 HF 账号,每个账号填写自己的 token
22
- - 给每个账号添加多个 Space,例如 `zlcy/li`、`GPT-IMG/2`
23
- - 在右侧列表按 `Ctrl` / `Shift` 多选 Space
24
- - 勾选“只演练,不实际推送”先确认命令
25
- - 如远端分支不是当前项目历史,可以勾选“允许覆盖远端分支(谨慎)”
26
-
27
- 账号 token 会保存到本地 `scripts\hf_spaces.json`,这个文件已经被 `.gitignore` 忽略,不会提交到仓库。
28
-
29
- ## 1. 准备配置
30
-
31
- 复制示例配置:
32
-
33
- ```powershell
34
- Copy-Item scripts\hf_spaces.example.json scripts\hf_spaces.json
35
- ```
36
-
37
- 然后编辑 `scripts\hf_spaces.json`,把不同账号的 token 和 Space 填进去。
38
-
39
- `scripts\hf_spaces.json` 是本地私有配置,不要提交到 Git。
40
-
41
- 也可以不把 token 写进文件,改用环境变量:
42
-
43
- ```json
44
- {
45
- "name": "zlcy",
46
- "token_env": "HF_TOKEN_ZLCY",
47
- "spaces": [
48
- { "name": "li", "repo": "zlcy/li" }
49
- ]
50
- }
51
- ```
52
-
53
- ## 2. 列出 Space
54
-
55
- ```powershell
56
- python scripts\hf_space_push_tool.py --list
57
- ```
58
-
59
- ## 3. 交互式多选推送
60
-
61
- ```powershell
62
- python scripts\hf_space_push_tool.py
63
- ```
64
-
65
- 输入示例:
66
-
67
- - `1,3,5`:推送第 1、3、5 个 Space
68
- - `1-3`:推送第 1 到第 3 个 Space
69
- - `all`:推送全部 Space
70
- - `q`:取消
71
-
72
- ## 4. 一键推送全部
73
-
74
- ```powershell
75
- python scripts\hf_space_push_tool.py --all --yes
76
- ```
77
-
78
- ## 5. 命令行指定多选
79
-
80
- ```powershell
81
- python scripts\hf_space_push_tool.py --select 1,3 --yes
82
- ```
83
-
84
- ## 注意
85
-
86
- - 工具只推送已经提交的内容;如果项目有未提交修改,会先停止。
87
- - 工具不会把 token 写进 Git remote,只在本次 `git push` 命令中临时使用。
88
- - 默认推送当前 `HEAD` 到目标 Space 的 `main` 分支。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/hf_space_push_gui.py DELETED
@@ -1,464 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import os
5
- import queue
6
- import subprocess
7
- import threading
8
- import tkinter as tk
9
- from pathlib import Path
10
- from tkinter import messagebox, scrolledtext, ttk
11
- from urllib.parse import quote
12
-
13
-
14
- REPO_ROOT = Path(__file__).resolve().parents[1]
15
- CONFIG_PATH = Path(__file__).with_name("hf_spaces.json")
16
- HF_SPACE_BASE_URL = "https://huggingface.co/spaces"
17
-
18
-
19
- def load_config() -> dict:
20
- if not CONFIG_PATH.exists():
21
- return {"default_branch": "main", "accounts": []}
22
- try:
23
- data = json.loads(CONFIG_PATH.read_text(encoding="utf-8-sig"))
24
- except json.JSONDecodeError as exc:
25
- messagebox.showerror("配置错误", f"配置 JSON 格式错误:\n{exc}")
26
- return {"default_branch": "main", "accounts": []}
27
- if not isinstance(data, dict):
28
- return {"default_branch": "main", "accounts": []}
29
- if not isinstance(data.get("accounts"), list):
30
- data["accounts"] = []
31
- if not str(data.get("default_branch") or "").strip():
32
- data["default_branch"] = "main"
33
- return data
34
-
35
-
36
- def save_config(config: dict) -> None:
37
- CONFIG_PATH.write_text(
38
- json.dumps(config, ensure_ascii=False, indent=2) + "\n",
39
- encoding="utf-8",
40
- )
41
-
42
-
43
- def account_token(account: dict) -> str:
44
- token = str(account.get("token") or "").strip()
45
- if token:
46
- return token
47
- token_env = str(account.get("token_env") or "").strip()
48
- return str(os.getenv(token_env) or "").strip() if token_env else ""
49
-
50
-
51
- def normalize_repo(account_name: str, space: dict) -> str:
52
- repo = str(space.get("repo") or "").strip().strip("/")
53
- if repo:
54
- return repo
55
- name = str(space.get("name") or space.get("space") or "").strip().strip("/")
56
- return f"{account_name}/{name}" if name else ""
57
-
58
-
59
- def redact(text: str, tokens: list[str]) -> str:
60
- redacted = text
61
- for token in tokens:
62
- if not token:
63
- continue
64
- redacted = redacted.replace(token, "***TOKEN***")
65
- redacted = redacted.replace(quote(token, safe=""), "***TOKEN***")
66
- return redacted
67
-
68
-
69
- def git_output(args: list[str], tokens: list[str] | None = None) -> tuple[int, str]:
70
- result = subprocess.run(
71
- ["git", *args],
72
- cwd=REPO_ROOT,
73
- text=True,
74
- encoding="utf-8",
75
- errors="replace",
76
- stdout=subprocess.PIPE,
77
- stderr=subprocess.STDOUT,
78
- )
79
- return result.returncode, redact(result.stdout, tokens or [])
80
-
81
-
82
- class HFSpacePushGUI:
83
- def __init__(self, root: tk.Tk):
84
- self.root = root
85
- self.root.title("Hugging Face Space 多账号推送工具")
86
- self.root.geometry("1100x760")
87
- self.config = load_config()
88
- self.account_var = tk.StringVar()
89
- self.token_var = tk.StringVar()
90
- self.space_account_var = tk.StringVar()
91
- self.space_repo_var = tk.StringVar()
92
- self.space_branch_var = tk.StringVar(value=str(self.config.get("default_branch") or "main"))
93
- self.force_var = tk.BooleanVar(value=False)
94
- self.dry_run_var = tk.BooleanVar(value=False)
95
- self.output_queue: queue.Queue[str] = queue.Queue()
96
- self.pushing = False
97
- self.target_rows: list[dict] = []
98
-
99
- self.build_ui()
100
- self.refresh_all()
101
- self.root.after(100, self.flush_output_queue)
102
-
103
- def build_ui(self) -> None:
104
- self.root.columnconfigure(0, weight=1)
105
- self.root.rowconfigure(1, weight=1)
106
-
107
- header = ttk.Frame(self.root, padding=10)
108
- header.grid(row=0, column=0, sticky="ew")
109
- header.columnconfigure(0, weight=1)
110
- ttk.Label(
111
- header,
112
- text=f"项目:{REPO_ROOT} 配置:{CONFIG_PATH}",
113
- foreground="#57534e",
114
- ).grid(row=0, column=0, sticky="w")
115
- ttk.Button(header, text="保存配置", command=self.save_current_config).grid(row=0, column=1, padx=(8, 0))
116
- ttk.Button(header, text="重新加载", command=self.reload_config).grid(row=0, column=2, padx=(8, 0))
117
-
118
- main = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
119
- main.grid(row=1, column=0, sticky="nsew", padx=10, pady=(0, 10))
120
-
121
- left = ttk.Frame(main, padding=8)
122
- right = ttk.Frame(main, padding=8)
123
- main.add(left, weight=1)
124
- main.add(right, weight=2)
125
-
126
- self.build_config_panel(left)
127
- self.build_push_panel(right)
128
-
129
- def build_config_panel(self, parent: ttk.Frame) -> None:
130
- parent.columnconfigure(0, weight=1)
131
- parent.rowconfigure(1, weight=1)
132
- parent.rowconfigure(4, weight=1)
133
-
134
- ttk.Label(parent, text="账号管理", font=("", 12, "bold")).grid(row=0, column=0, sticky="w")
135
- self.accounts_list = tk.Listbox(parent, height=6, exportselection=False)
136
- self.accounts_list.grid(row=1, column=0, sticky="nsew", pady=(6, 8))
137
- self.accounts_list.bind("<<ListboxSelect>>", lambda _event: self.on_account_select())
138
-
139
- form = ttk.Frame(parent)
140
- form.grid(row=2, column=0, sticky="ew")
141
- form.columnconfigure(1, weight=1)
142
- ttk.Label(form, text="账号名").grid(row=0, column=0, sticky="w", pady=3)
143
- ttk.Entry(form, textvariable=self.account_var).grid(row=0, column=1, sticky="ew", padx=(8, 0), pady=3)
144
- ttk.Label(form, text="Token").grid(row=1, column=0, sticky="w", pady=3)
145
- ttk.Entry(form, textvariable=self.token_var, show="•").grid(row=1, column=1, sticky="ew", padx=(8, 0), pady=3)
146
-
147
- account_buttons = ttk.Frame(parent)
148
- account_buttons.grid(row=3, column=0, sticky="ew", pady=(8, 12))
149
- ttk.Button(account_buttons, text="新增/更新账号", command=self.upsert_account).pack(side=tk.LEFT)
150
- ttk.Button(account_buttons, text="删除账号", command=self.delete_account).pack(side=tk.LEFT, padx=(8, 0))
151
-
152
- ttk.Label(parent, text="Space 管理", font=("", 12, "bold")).grid(row=4, column=0, sticky="nw")
153
- space_form = ttk.Frame(parent)
154
- space_form.grid(row=5, column=0, sticky="ew", pady=(8, 4))
155
- space_form.columnconfigure(1, weight=1)
156
- ttk.Label(space_form, text="账号").grid(row=0, column=0, sticky="w", pady=3)
157
- self.space_account_combo = ttk.Combobox(space_form, textvariable=self.space_account_var, state="readonly")
158
- self.space_account_combo.grid(row=0, column=1, sticky="ew", padx=(8, 0), pady=3)
159
- self.space_account_combo.bind("<<ComboboxSelected>>", lambda _event: self.refresh_spaces_list())
160
- ttk.Label(space_form, text="Repo").grid(row=1, column=0, sticky="w", pady=3)
161
- ttk.Entry(space_form, textvariable=self.space_repo_var).grid(row=1, column=1, sticky="ew", padx=(8, 0), pady=3)
162
- ttk.Label(space_form, text="分支").grid(row=2, column=0, sticky="w", pady=3)
163
- ttk.Entry(space_form, textvariable=self.space_branch_var).grid(row=2, column=1, sticky="ew", padx=(8, 0), pady=3)
164
-
165
- ttk.Label(parent, text="Repo 格式示例:zlcy/li 或 GPT-IMG/2", foreground="#78716c").grid(row=6, column=0, sticky="w")
166
- space_buttons = ttk.Frame(parent)
167
- space_buttons.grid(row=7, column=0, sticky="ew", pady=(8, 8))
168
- ttk.Button(space_buttons, text="添加 Space", command=self.add_space).pack(side=tk.LEFT)
169
- ttk.Button(space_buttons, text="删除选中 Space", command=self.delete_space).pack(side=tk.LEFT, padx=(8, 0))
170
-
171
- self.spaces_list = tk.Listbox(parent, height=7, exportselection=False)
172
- self.spaces_list.grid(row=8, column=0, sticky="nsew")
173
-
174
- def build_push_panel(self, parent: ttk.Frame) -> None:
175
- parent.columnconfigure(0, weight=1)
176
- parent.rowconfigure(1, weight=1)
177
- parent.rowconfigure(4, weight=2)
178
-
179
- top = ttk.Frame(parent)
180
- top.grid(row=0, column=0, sticky="ew")
181
- top.columnconfigure(0, weight=1)
182
- ttk.Label(top, text="选择要推送的 Space(可按 Ctrl/Shift 多选)", font=("", 12, "bold")).grid(row=0, column=0, sticky="w")
183
- ttk.Button(top, text="刷新列表", command=self.refresh_targets).grid(row=0, column=1)
184
-
185
- self.targets_list = tk.Listbox(parent, selectmode=tk.EXTENDED, height=14, exportselection=False)
186
- self.targets_list.grid(row=1, column=0, sticky="nsew", pady=(8, 8))
187
-
188
- options = ttk.Frame(parent)
189
- options.grid(row=2, column=0, sticky="ew")
190
- ttk.Checkbutton(options, text="只演练,不实际推送", variable=self.dry_run_var).pack(side=tk.LEFT)
191
- ttk.Checkbutton(options, text="允许覆盖远端分支(谨慎)", variable=self.force_var).pack(side=tk.LEFT, padx=(16, 0))
192
- ttk.Button(options, text="全选", command=self.select_all_targets).pack(side=tk.RIGHT)
193
- ttk.Button(options, text="清空选择", command=lambda: self.targets_list.selection_clear(0, tk.END)).pack(side=tk.RIGHT, padx=(0, 8))
194
-
195
- actions = ttk.Frame(parent)
196
- actions.grid(row=3, column=0, sticky="ew", pady=(10, 8))
197
- self.push_button = ttk.Button(actions, text="推送选中的 Space", command=self.push_selected)
198
- self.push_button.pack(side=tk.LEFT)
199
- ttk.Button(actions, text="查看当前提交", command=self.show_current_commit).pack(side=tk.LEFT, padx=(8, 0))
200
-
201
- self.output = scrolledtext.ScrolledText(parent, height=14, wrap=tk.WORD)
202
- self.output.grid(row=4, column=0, sticky="nsew")
203
- self.output.configure(state=tk.DISABLED)
204
-
205
- def accounts(self) -> list[dict]:
206
- accounts = self.config.setdefault("accounts", [])
207
- return accounts if isinstance(accounts, list) else []
208
-
209
- def selected_account_index(self) -> int | None:
210
- selection = self.accounts_list.curselection()
211
- return int(selection[0]) if selection else None
212
-
213
- def selected_account(self) -> dict | None:
214
- index = self.selected_account_index()
215
- accounts = self.accounts()
216
- return accounts[index] if index is not None and index < len(accounts) else None
217
-
218
- def refresh_all(self) -> None:
219
- self.refresh_accounts()
220
- self.refresh_spaces_list()
221
- self.refresh_targets()
222
-
223
- def refresh_accounts(self) -> None:
224
- self.accounts_list.delete(0, tk.END)
225
- names = []
226
- for account in self.accounts():
227
- name = str(account.get("name") or "").strip()
228
- if name:
229
- names.append(name)
230
- self.accounts_list.insert(tk.END, name)
231
- self.space_account_combo["values"] = names
232
- if names and self.space_account_var.get() not in names:
233
- self.space_account_var.set(names[0])
234
-
235
- def refresh_spaces_list(self) -> None:
236
- self.spaces_list.delete(0, tk.END)
237
- account = self.find_account(self.space_account_var.get())
238
- if not account:
239
- return
240
- spaces = account.setdefault("spaces", [])
241
- for space in spaces:
242
- repo = normalize_repo(str(account.get("name") or ""), space)
243
- branch = str(space.get("branch") or self.config.get("default_branch") or "main")
244
- self.spaces_list.insert(tk.END, f"{repo} -> {branch}")
245
-
246
- def refresh_targets(self) -> None:
247
- self.target_rows = []
248
- self.targets_list.delete(0, tk.END)
249
- for account in self.accounts():
250
- account_name = str(account.get("name") or "").strip()
251
- token = account_token(account)
252
- for space in account.get("spaces") or []:
253
- if not isinstance(space, dict):
254
- continue
255
- repo = normalize_repo(account_name, space)
256
- branch = str(space.get("branch") or self.config.get("default_branch") or "main").strip() or "main"
257
- if not account_name or not repo:
258
- continue
259
- row = {"account": account_name, "token": token, "repo": repo, "branch": branch}
260
- self.target_rows.append(row)
261
- token_hint = "已填 token" if token else "缺 token"
262
- self.targets_list.insert(tk.END, f"{account_name} / {repo} -> {branch} ({token_hint})")
263
-
264
- def on_account_select(self) -> None:
265
- account = self.selected_account()
266
- if not account:
267
- return
268
- self.account_var.set(str(account.get("name") or ""))
269
- self.token_var.set(str(account.get("token") or ""))
270
- self.space_account_var.set(str(account.get("name") or ""))
271
- self.refresh_spaces_list()
272
-
273
- def find_account(self, name: str) -> dict | None:
274
- target = name.strip()
275
- for account in self.accounts():
276
- if str(account.get("name") or "").strip() == target:
277
- return account
278
- return None
279
-
280
- def upsert_account(self) -> None:
281
- name = self.account_var.get().strip()
282
- token = self.token_var.get().strip()
283
- if not name:
284
- messagebox.showwarning("缺少账号名", "请填写 HF 账号名,例如 zlcy 或 GPT-IMG。")
285
- return
286
- account = self.find_account(name)
287
- if account is None:
288
- account = {"name": name, "token": token, "spaces": []}
289
- self.accounts().append(account)
290
- else:
291
- account["token"] = token
292
- self.save_current_config(show_message=False)
293
- self.refresh_all()
294
- self.log(f"已保存账号:{name}\n")
295
-
296
- def delete_account(self) -> None:
297
- index = self.selected_account_index()
298
- if index is None:
299
- return
300
- account = self.accounts()[index]
301
- if not messagebox.askyesno("确认删除", f"删除账号 {account.get('name')} 及其 Space 配置?"):
302
- return
303
- del self.accounts()[index]
304
- self.save_current_config(show_message=False)
305
- self.account_var.set("")
306
- self.token_var.set("")
307
- self.refresh_all()
308
-
309
- def add_space(self) -> None:
310
- account_name = self.space_account_var.get().strip()
311
- repo = self.space_repo_var.get().strip().strip("/")
312
- branch = self.space_branch_var.get().strip() or "main"
313
- account = self.find_account(account_name)
314
- if not account:
315
- messagebox.showwarning("缺少账号", "请先选择或新增账号。")
316
- return
317
- if not repo or "/" not in repo:
318
- messagebox.showwarning("Repo 格式错误", "Repo 请填写 owner/space,例如 zlcy/li。")
319
- return
320
- spaces = account.setdefault("spaces", [])
321
- existing = None
322
- for space in spaces:
323
- if isinstance(space, dict) and normalize_repo(account_name, space) == repo:
324
- existing = space
325
- break
326
- if existing is None:
327
- spaces.append({"name": repo.split("/", 1)[1], "repo": repo, "branch": branch})
328
- else:
329
- existing["branch"] = branch
330
- self.save_current_config(show_message=False)
331
- self.space_repo_var.set("")
332
- self.refresh_spaces_list()
333
- self.refresh_targets()
334
-
335
- def delete_space(self) -> None:
336
- account = self.find_account(self.space_account_var.get())
337
- selection = list(self.spaces_list.curselection())
338
- if not account or not selection:
339
- return
340
- spaces = account.get("spaces") or []
341
- for index in sorted(selection, reverse=True):
342
- if index < len(spaces):
343
- del spaces[index]
344
- self.save_current_config(show_message=False)
345
- self.refresh_spaces_list()
346
- self.refresh_targets()
347
-
348
- def save_current_config(self, show_message: bool = True) -> None:
349
- save_config(self.config)
350
- if show_message:
351
- messagebox.showinfo("已保存", f"配置已保存到:\n{CONFIG_PATH}")
352
-
353
- def reload_config(self) -> None:
354
- self.config = load_config()
355
- self.refresh_all()
356
- self.log("已重新加载配置。\n")
357
-
358
- def select_all_targets(self) -> None:
359
- self.targets_list.selection_set(0, tk.END)
360
-
361
- def selected_targets(self) -> list[dict]:
362
- return [self.target_rows[index] for index in self.targets_list.curselection() if index < len(self.target_rows)]
363
-
364
- def show_current_commit(self) -> None:
365
- code, output = git_output(["log", "--oneline", "-1"])
366
- self.log(output if code == 0 else f"读取提交失败:\n{output}")
367
-
368
- def push_selected(self) -> None:
369
- if self.pushing:
370
- return
371
- targets = self.selected_targets()
372
- if not targets:
373
- messagebox.showwarning("未选择 Space", "请先在右侧列表中选择一个或多个 Space。")
374
- return
375
- missing = [target for target in targets if not target["token"]]
376
- if missing:
377
- messagebox.showwarning("缺少 token", "有选中的账号没有 token,请先填写 token。")
378
- return
379
- if not self.dry_run_var.get():
380
- code, status = git_output(["status", "--porcelain"])
381
- if code != 0 or status.strip():
382
- messagebox.showwarning("存在未提交修改", "当前项目有未提交修改。请先提交后再推送。")
383
- self.log(status)
384
- return
385
- if not messagebox.askyesno("确认推送", f"确认推送当前 HEAD 到 {len(targets)} 个 Space?"):
386
- return
387
- self.pushing = True
388
- self.push_button.configure(state=tk.DISABLED)
389
- threading.Thread(target=self.worker_push, args=(targets,), daemon=True).start()
390
-
391
- def worker_push(self, targets: list[dict]) -> None:
392
- try:
393
- code, commit = git_output(["rev-parse", "--short", "HEAD"])
394
- self.queue_log(f"当前提交:{commit.strip() if code == 0 else '未知'}\n")
395
- for target in targets:
396
- self.push_one(target)
397
- finally:
398
- self.root.after(0, self.finish_push)
399
-
400
- def push_one(self, target: dict) -> None:
401
- account = target["account"]
402
- token = target["token"]
403
- repo = target["repo"]
404
- branch = target["branch"]
405
- safe_url = f"{HF_SPACE_BASE_URL}/{repo}"
406
- token_url = f"https://{quote(account, safe='')}:{quote(token, safe='')}@huggingface.co/spaces/{repo}"
407
- refspec = f"HEAD:{branch}"
408
- args = ["push", token_url, refspec]
409
- if self.force_var.get():
410
- args = ["push", "--force", token_url, refspec]
411
- if self.dry_run_var.get():
412
- self.queue_log(f"[演练] git {' '.join(args).replace(token_url, safe_url)}\n")
413
- return
414
- self.queue_log(f"\n推送:{account} / {repo} -> {branch}\n")
415
- process = subprocess.Popen(
416
- ["git", *args],
417
- cwd=REPO_ROOT,
418
- text=True,
419
- encoding="utf-8",
420
- errors="replace",
421
- stdout=subprocess.PIPE,
422
- stderr=subprocess.STDOUT,
423
- )
424
- assert process.stdout is not None
425
- for line in process.stdout:
426
- self.queue_log(redact(line, [token]))
427
- code = process.wait()
428
- if code == 0:
429
- self.queue_log(f"完成:{safe_url}\n")
430
- else:
431
- self.queue_log(f"失败:{safe_url}\n")
432
-
433
- def finish_push(self) -> None:
434
- self.pushing = False
435
- self.push_button.configure(state=tk.NORMAL)
436
- self.log("\n推送任务结束。\n")
437
-
438
- def queue_log(self, text: str) -> None:
439
- self.output_queue.put(text)
440
-
441
- def flush_output_queue(self) -> None:
442
- while True:
443
- try:
444
- item = self.output_queue.get_nowait()
445
- except queue.Empty:
446
- break
447
- self.log(item)
448
- self.root.after(100, self.flush_output_queue)
449
-
450
- def log(self, text: str) -> None:
451
- self.output.configure(state=tk.NORMAL)
452
- self.output.insert(tk.END, text)
453
- self.output.see(tk.END)
454
- self.output.configure(state=tk.DISABLED)
455
-
456
-
457
- def main() -> None:
458
- root = tk.Tk()
459
- HFSpacePushGUI(root)
460
- root.mainloop()
461
-
462
-
463
- if __name__ == "__main__":
464
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/hf_space_push_tool.py DELETED
@@ -1,247 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import json
5
- import os
6
- import subprocess
7
- import sys
8
- from dataclasses import dataclass
9
- from pathlib import Path
10
- from urllib.parse import quote
11
-
12
-
13
- DEFAULT_CONFIG = Path(__file__).with_name("hf_spaces.json")
14
- EXAMPLE_CONFIG = Path(__file__).with_name("hf_spaces.example.json")
15
- HF_SPACE_BASE_URL = "https://huggingface.co/spaces"
16
-
17
-
18
- @dataclass(frozen=True)
19
- class Target:
20
- index: int
21
- account: str
22
- token: str
23
- repo: str
24
- branch: str
25
- label: str
26
-
27
- @property
28
- def display(self) -> str:
29
- return f"{self.account} / {self.repo} -> {self.branch}"
30
-
31
- @property
32
- def safe_url(self) -> str:
33
- return f"{HF_SPACE_BASE_URL}/{self.repo}"
34
-
35
- @property
36
- def push_url(self) -> str:
37
- encoded_account = quote(self.account, safe="")
38
- encoded_token = quote(self.token, safe="")
39
- return f"https://{encoded_account}:{encoded_token}@huggingface.co/spaces/{self.repo}"
40
-
41
-
42
- def run_git(args: list[str], *, check: bool = True) -> subprocess.CompletedProcess[str]:
43
- return subprocess.run(
44
- ["git", *args],
45
- check=check,
46
- text=True,
47
- stdout=subprocess.PIPE,
48
- stderr=subprocess.STDOUT,
49
- )
50
-
51
-
52
- def current_commit() -> str:
53
- return run_git(["rev-parse", "--short", "HEAD"]).stdout.strip()
54
-
55
-
56
- def ensure_git_repo() -> None:
57
- result = run_git(["rev-parse", "--show-toplevel"], check=False)
58
- if result.returncode != 0:
59
- raise SystemExit("当前目录不是 Git 仓库,请在项目根目录运行。")
60
-
61
-
62
- def ensure_clean_worktree() -> None:
63
- status = run_git(["status", "--porcelain"]).stdout.strip()
64
- if status:
65
- raise SystemExit(
66
- "当前项目有未提交修改,推送前请先提交。\n"
67
- "未提交内容不会被推送,先执行 git status 查看。"
68
- )
69
-
70
-
71
- def load_config(path: Path) -> dict:
72
- if not path.exists():
73
- raise SystemExit(
74
- f"找不到配置文件:{path}\n"
75
- f"请复制示例配置:copy {EXAMPLE_CONFIG} {DEFAULT_CONFIG}"
76
- )
77
- try:
78
- return json.loads(path.read_text(encoding="utf-8-sig"))
79
- except json.JSONDecodeError as exc:
80
- raise SystemExit(f"配置 JSON 格式错误:{exc}") from exc
81
-
82
-
83
- def account_token(account: dict) -> str:
84
- token = str(account.get("token") or "").strip()
85
- token_env = str(account.get("token_env") or "").strip()
86
- if token:
87
- return token
88
- if token_env:
89
- return str(os.getenv(token_env) or "").strip()
90
- return ""
91
-
92
-
93
- def normalize_repo(owner: str, item: dict) -> str:
94
- repo = str(item.get("repo") or "").strip().strip("/")
95
- if repo:
96
- return repo
97
- space = str(item.get("space") or item.get("name") or "").strip().strip("/")
98
- if not owner or not space:
99
- raise ValueError("space 配置需要 repo 或 account.name + space/name")
100
- return f"{owner}/{space}"
101
-
102
-
103
- def build_targets(config: dict) -> list[Target]:
104
- accounts = config.get("accounts")
105
- if not isinstance(accounts, list):
106
- raise SystemExit("配置文件需要包含 accounts 数组。")
107
-
108
- targets: list[Target] = []
109
- next_index = 1
110
- for account in accounts:
111
- if not isinstance(account, dict):
112
- continue
113
- account_name = str(account.get("name") or "").strip()
114
- token = account_token(account)
115
- spaces = account.get("spaces")
116
- if not account_name:
117
- raise SystemExit("每个账号都需要填写 name。")
118
- if not token:
119
- raise SystemExit(f"账号 {account_name} 没有 token,请填写 token 或 token_env。")
120
- if not isinstance(spaces, list) or not spaces:
121
- continue
122
- for item in spaces:
123
- if not isinstance(item, dict):
124
- continue
125
- repo = normalize_repo(account_name, item)
126
- branch = str(item.get("branch") or config.get("default_branch") or "main").strip() or "main"
127
- label = str(item.get("label") or item.get("name") or repo).strip()
128
- targets.append(
129
- Target(
130
- index=next_index,
131
- account=account_name,
132
- token=token,
133
- repo=repo,
134
- branch=branch,
135
- label=label,
136
- )
137
- )
138
- next_index += 1
139
- if not targets:
140
- raise SystemExit("配置文件里没有可推送的 Space。")
141
- return targets
142
-
143
-
144
- def print_targets(targets: list[Target]) -> None:
145
- print("可推送的 Space:")
146
- for target in targets:
147
- print(f" [{target.index}] {target.display}")
148
-
149
-
150
- def parse_selection(text: str, targets: list[Target]) -> list[Target]:
151
- normalized = text.strip().lower()
152
- if normalized in {"all", "*", "a"}:
153
- return targets
154
- if normalized in {"q", "quit", "exit"}:
155
- raise SystemExit("已取消。")
156
-
157
- selected_indexes: set[int] = set()
158
- for part in normalized.replace(",", ",").split(","):
159
- part = part.strip()
160
- if not part:
161
- continue
162
- if "-" in part:
163
- start_text, end_text = part.split("-", 1)
164
- start = int(start_text)
165
- end = int(end_text)
166
- if start > end:
167
- start, end = end, start
168
- selected_indexes.update(range(start, end + 1))
169
- else:
170
- selected_indexes.add(int(part))
171
-
172
- by_index = {target.index: target for target in targets}
173
- selected = [by_index[index] for index in sorted(selected_indexes) if index in by_index]
174
- if not selected:
175
- raise ValueError("没有选中任何有效 Space。")
176
- return selected
177
-
178
-
179
- def choose_targets(targets: list[Target], selection: str | None, all_targets: bool) -> list[Target]:
180
- if all_targets:
181
- return targets
182
- if selection:
183
- return parse_selection(selection, targets)
184
- print_targets(targets)
185
- print("输入编号可多选,例如:1,3,5 或 1-3;输入 all 推送全部;输入 q 取消。")
186
- while True:
187
- try:
188
- return parse_selection(input("请选择要推送的 Space:"), targets)
189
- except ValueError as exc:
190
- print(exc)
191
-
192
-
193
- def confirm_targets(targets: list[Target], yes: bool) -> None:
194
- print("\n本次将推送当前 HEAD 到:")
195
- for target in targets:
196
- print(f" - {target.display} ({target.safe_url})")
197
- print(f"当前提交:{current_commit()}")
198
- if yes:
199
- return
200
- answer = input("确认推送?输入 y 继续:").strip().lower()
201
- if answer not in {"y", "yes"}:
202
- raise SystemExit("已取消。")
203
-
204
-
205
- def push_target(target: Target, dry_run: bool) -> None:
206
- refspec = f"HEAD:{target.branch}"
207
- command_preview = f"git push {target.safe_url} {refspec}"
208
- if dry_run:
209
- print(f"[dry-run] {command_preview}")
210
- return
211
- print(f"\n推送:{target.display}")
212
- result = run_git(["push", target.push_url, refspec], check=False)
213
- print(result.stdout.rstrip())
214
- if result.returncode != 0:
215
- raise SystemExit(f"推送失败:{target.display}")
216
-
217
-
218
- def main() -> int:
219
- parser = argparse.ArgumentParser(description="多账号 Hugging Face Space 推送工具")
220
- parser.add_argument("--config", default=str(DEFAULT_CONFIG), help="配置文件路径,默认 scripts/hf_spaces.json")
221
- parser.add_argument("--select", help="要推送的编号,例如 1,3,5 或 1-3")
222
- parser.add_argument("--all", action="store_true", help="推送配置中的全部 Space")
223
- parser.add_argument("--yes", "-y", action="store_true", help="跳过确认")
224
- parser.add_argument("--dry-run", action="store_true", help="只显示将执行的推送,不实际推送")
225
- parser.add_argument("--list", action="store_true", help="只列出配置中的 Space")
226
- args = parser.parse_args()
227
-
228
- ensure_git_repo()
229
- config_path = Path(args.config)
230
- config = load_config(config_path)
231
- targets = build_targets(config)
232
- if args.list:
233
- print_targets(targets)
234
- return 0
235
-
236
- if not args.dry_run:
237
- ensure_clean_worktree()
238
- selected = choose_targets(targets, args.select, args.all)
239
- confirm_targets(selected, args.yes or args.dry_run)
240
- for target in selected:
241
- push_target(target, args.dry_run)
242
- print("\n完成。")
243
- return 0
244
-
245
-
246
- if __name__ == "__main__":
247
- raise SystemExit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/hf_spaces.example.json DELETED
@@ -1,21 +0,0 @@
1
- {
2
- "default_branch": "main",
3
- "accounts": [
4
- {
5
- "name": "zlcy",
6
- "token": "hf_把这个账号的token填这里",
7
- "spaces": [
8
- { "name": "CHATSAM-Public", "repo": "zlcy/CHATSAM-Public" },
9
- { "name": "li", "repo": "zlcy/li" },
10
- { "name": "li-test", "repo": "zlcy/li-test" }
11
- ]
12
- },
13
- {
14
- "name": "GPT-IMG",
15
- "token": "hf_把另一个账号的token填这里",
16
- "spaces": [
17
- { "name": "2", "repo": "GPT-IMG/2" }
18
- ]
19
- }
20
- ]
21
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/start_hf_push_gui.bat DELETED
@@ -1,4 +0,0 @@
1
- @echo off
2
- cd /d "%~dp0\.."
3
- python scripts\hf_space_push_gui.py
4
- pause