bingo-light 2.1.2 → 2.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +13 -7
- package/README.md +21 -4
- package/bingo-light +269 -11
- package/bingo_core/__init__.py +3 -1
- package/bingo_core/dep.py +385 -25
- package/bingo_core/dep_fork.py +268 -0
- package/bingo_core/repo.py +795 -8
- package/bingo_core/state.py +1 -1
- package/bingo_core/team.py +170 -0
- package/completions/bingo-light.bash +14 -4
- package/completions/bingo-light.fish +23 -2
- package/completions/bingo-light.zsh +18 -2
- package/mcp-server.py +241 -6
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
|
11
11
|
<a href="https://github.com/DanOps-1/bingo-light/releases"><img src="https://img.shields.io/github/v/release/DanOps-1/bingo-light?label=Release&color=orange" alt="Release"></a>
|
|
12
12
|
<br>
|
|
13
|
-
<a href="#for-ai-agents"><img src="https://img.shields.io/badge/MCP_Server-
|
|
13
|
+
<a href="#for-ai-agents"><img src="https://img.shields.io/badge/MCP_Server-49_tools-blueviolet.svg" alt="MCP: 49 tools"></a>
|
|
14
14
|
<a href="https://www.python.org/"><img src="https://img.shields.io/badge/Python-3.8+-3776ab.svg" alt="Python 3.8+"></a>
|
|
15
15
|
<img src="https://img.shields.io/badge/Dependencies-Zero-brightgreen.svg" alt="Zero deps">
|
|
16
16
|
<a href="https://github.com/DanOps-1/bingo-light/stargazers"><img src="https://img.shields.io/github/stars/DanOps-1/bingo-light?style=social" alt="Stars"></a>
|
|
@@ -25,7 +25,7 @@ GitHub's "Sync fork" button breaks the moment you have customizations. `git reba
|
|
|
25
25
|
|
|
26
26
|
Your patches live as a clean, named stack on top of upstream. Syncing is `bingo-light sync`. Conflicts get remembered so you never solve the same one twice. And if something goes sideways, `bingo-light undo` puts everything back in one second.
|
|
27
27
|
|
|
28
|
-
Every command speaks JSON. The built-in MCP server gives AI agents
|
|
28
|
+
Every command speaks JSON. The built-in MCP server gives AI agents 49 tools to manage your fork autonomously -- from init through conflict resolution. No human in the loop required.
|
|
29
29
|
|
|
30
30
|
---
|
|
31
31
|
|
|
@@ -69,6 +69,12 @@ That's it. Three commands and your fork stays in sync forever.
|
|
|
69
69
|
|
|
70
70
|
> The AI calls `conflict-analyze --json`, reads the structured ours/theirs data, writes the merged file, and the rebase continues. No human needed.
|
|
71
71
|
|
|
72
|
+
During a rebase, `conflict-analyze` also returns:
|
|
73
|
+
- **`patch_intent`** — patch name, subject, full commit message, original SHA, original diff, metadata (reason/tags/upstream_pr/status/owner), and position in the patch stack.
|
|
74
|
+
- **`verify`** — configured `test.command` plus per-file syntax/parse commands by extension (`.py/.json/.yml/.yaml/.toml/.sh`).
|
|
75
|
+
|
|
76
|
+
`conflict-resolve --verify` (CLI) or `verify: true` (MCP) runs `test.command` after the final `git rebase --continue`; the result is attached as `verify_result`.
|
|
77
|
+
|
|
72
78
|
---
|
|
73
79
|
|
|
74
80
|
## Key Features
|
|
@@ -93,7 +99,7 @@ That's it. Three commands and your fork stays in sync forever.
|
|
|
93
99
|
|
|
94
100
|
### For AI Agents
|
|
95
101
|
|
|
96
|
-
- :electric_plug: **MCP server (
|
|
102
|
+
- :electric_plug: **MCP server (49 tools)** -- full fork management from init through conflict resolution.
|
|
97
103
|
- :bar_chart: **`--json` on everything** -- every command returns structured JSON. Parse, don't scrape.
|
|
98
104
|
- :mute: **`--yes` flag** -- fully non-interactive. No TTY required. No prompts. Ever.
|
|
99
105
|
- :gear: **Auto-detect non-TTY** -- pipes and subprocesses trigger non-interactive mode automatically.
|
|
@@ -200,7 +206,7 @@ make install && bingo-light setup
|
|
|
200
206
|
|
|
201
207
|
## For AI Agents
|
|
202
208
|
|
|
203
|
-
bingo-light was designed from day one for AI agents. Every command speaks JSON. The MCP server exposes
|
|
209
|
+
bingo-light was designed from day one for AI agents. Every command speaks JSON. The MCP server exposes 49 tools covering the full lifecycle from `init` to `conflict-resolve`. Non-interactive mode is the default when stdin is not a TTY.
|
|
204
210
|
|
|
205
211
|
### MCP setup -- Claude Code
|
|
206
212
|
|
|
@@ -234,7 +240,7 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
|
234
240
|
|
|
235
241
|
**Any MCP client** (VS Code Copilot, Cursor, custom agents): connect via stdio to `python3 mcp-server.py`.
|
|
236
242
|
|
|
237
|
-
###
|
|
243
|
+
### 49 MCP Tools
|
|
238
244
|
|
|
239
245
|
| Tool | Purpose |
|
|
240
246
|
|------|---------|
|
|
@@ -424,7 +430,7 @@ StGit (649 stars) manages patch stacks but has no AI integration, no MCP server,
|
|
|
424
430
|
| Handles customizations | **Yes** | **No** | Manual | Manual | Manual |
|
|
425
431
|
| Conflict memory (rerere) | **Auto** | No | Manual | No | No |
|
|
426
432
|
| Conflict prediction | **Yes** | No | No | No | No |
|
|
427
|
-
| AI / MCP integration | **
|
|
433
|
+
| AI / MCP integration | **49 tools** | No | No | No | No |
|
|
428
434
|
| JSON output | **All commands** | No | No | No | No |
|
|
429
435
|
| Non-interactive mode | **Native** | No | Partial | Partial | Partial |
|
|
430
436
|
| Undo sync | **One command** | No | git reflog | Manual | Manual |
|
|
@@ -490,7 +496,7 @@ Yes. bingo-light uses standard git operations (fetch, rebase, push). It works wi
|
|
|
490
496
|
```
|
|
491
497
|
bingo-light CLI tool (Python 3, zero deps)
|
|
492
498
|
bingo_core/ Core library package (all business logic)
|
|
493
|
-
mcp-server.py MCP server (zero-dep Python 3,
|
|
499
|
+
mcp-server.py MCP server (zero-dep Python 3, 49 tools, JSON-RPC 2.0)
|
|
494
500
|
contrib/agent.py Advisor agent (monitors drift, auto-syncs when safe)
|
|
495
501
|
contrib/tui.py Terminal dashboard (curses TUI, real-time monitoring)
|
|
496
502
|
install.sh Installer (--yes for CI, --help for options)
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<a href="https://github.com/DanOps-1/bingo-light/actions"><img src="https://github.com/DanOps-1/bingo-light/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
10
10
|
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
|
11
11
|
<a href="https://github.com/DanOps-1/bingo-light/releases"><img src="https://img.shields.io/github/v/release/DanOps-1/bingo-light?label=Release&color=orange" alt="Release"></a>
|
|
12
|
-
<a href="#mcp-服务器"><img src="https://img.shields.io/badge/MCP_Server-
|
|
12
|
+
<a href="#mcp-服务器"><img src="https://img.shields.io/badge/MCP_Server-49_tools-blueviolet.svg" alt="MCP: 49 tools"></a>
|
|
13
13
|
<a href="https://www.python.org/"><img src="https://img.shields.io/badge/Python-3.8+-3776ab.svg" alt="Python 3.8+"></a>
|
|
14
14
|
<img src="https://img.shields.io/badge/Dependencies-Zero-brightgreen.svg" alt="Zero deps">
|
|
15
15
|
<a href="https://github.com/DanOps-1/bingo-light/stargazers"><img src="https://img.shields.io/github/stars/DanOps-1/bingo-light?style=social" alt="Stars"></a>
|
|
@@ -168,10 +168,27 @@ cd bingo-light && make install && bingo-light setup
|
|
|
168
168
|
"theirs": "... 你的补丁版本 ...",
|
|
169
169
|
"hint": "上游重构了调度器核心;补丁需要适配新结构。"
|
|
170
170
|
}
|
|
171
|
-
]
|
|
171
|
+
],
|
|
172
|
+
"patch_intent": {
|
|
173
|
+
"name": "custom-scheduler",
|
|
174
|
+
"subject": "...",
|
|
175
|
+
"message": "完整 commit 消息",
|
|
176
|
+
"original_sha": "a1b2c3d...",
|
|
177
|
+
"original_diff": "diff --git ...",
|
|
178
|
+
"meta": {"reason": "...", "tags": [], "status": "permanent"},
|
|
179
|
+
"stack_position": {"index": 3, "total": 7}
|
|
180
|
+
},
|
|
181
|
+
"verify": {
|
|
182
|
+
"test_command": "make test",
|
|
183
|
+
"file_hints": [
|
|
184
|
+
{"file": "kernel/sched/core.c", "command": "bash -n ...", "kind": "syntax"}
|
|
185
|
+
]
|
|
186
|
+
}
|
|
172
187
|
}
|
|
173
188
|
```
|
|
174
189
|
|
|
190
|
+
`conflict-analyze` 在 rebase 中额外附带 `patch_intent`(补丁意图:原始 commit、diff、metadata、栈位置)与 `verify`(配置的 `test.command` + 按扩展名的逐文件校验命令)。`conflict-resolve --verify` 在最终 `git rebase --continue` 完成后自动跑 `test.command`,结果挂在 `verify_result` 字段。
|
|
191
|
+
|
|
175
192
|
</details>
|
|
176
193
|
|
|
177
194
|
<details>
|
|
@@ -197,7 +214,7 @@ Claude Code:
|
|
|
197
214
|
```console
|
|
198
215
|
$ bingo-light setup
|
|
199
216
|
|
|
200
|
-
◆ bingo-light setup v2.
|
|
217
|
+
◆ bingo-light setup v2.x.x
|
|
201
218
|
│
|
|
202
219
|
◆ MCP Server
|
|
203
220
|
│ Connect bingo-light tools to your AI coding assistants
|
|
@@ -434,7 +451,7 @@ bingo-light help 打印帮助
|
|
|
434
451
|
|
|
435
452
|
| 集成方式 | 适用场景 | 示例 |
|
|
436
453
|
|---------|---------|------|
|
|
437
|
-
| **MCP** (
|
|
454
|
+
| **MCP** (49 tools) | Claude Code / Cursor / Windsurf 等 | `bingo-light setup` 自动配 |
|
|
438
455
|
| **CLI `--json`** | 任何能跑 shell 的 AI | `bingo-light sync --json --yes` |
|
|
439
456
|
| **Skill** | Claude Code / Continue / Gemini 等 | `/bingo` 教 AI 用法 |
|
|
440
457
|
|
package/bingo-light
CHANGED
|
@@ -234,6 +234,7 @@ def _format_conflict_analyze(result: dict) -> str:
|
|
|
234
234
|
conflicts = result.get("conflicts", [])
|
|
235
235
|
if not conflicts:
|
|
236
236
|
return f"{_c(GREEN, 'OK')} No conflicts detected."
|
|
237
|
+
|
|
237
238
|
lines = [f"{_c(YELLOW, '!')} {len(conflicts)} conflicting file(s):"]
|
|
238
239
|
for c in conflicts:
|
|
239
240
|
f = c.get("file", "?")
|
|
@@ -241,6 +242,33 @@ def _format_conflict_analyze(result: dict) -> str:
|
|
|
241
242
|
lines.append(f" {_c(RED, f)}")
|
|
242
243
|
if hint:
|
|
243
244
|
lines.append(f" hint: {hint}")
|
|
245
|
+
|
|
246
|
+
intent = result.get("patch_intent")
|
|
247
|
+
if intent and intent.get("name"):
|
|
248
|
+
lines.append("")
|
|
249
|
+
lines.append(
|
|
250
|
+
f"{_c(BOLD, 'Patch intent:')} "
|
|
251
|
+
f"{intent['name']} \u2014 {intent.get('subject', '')}"
|
|
252
|
+
)
|
|
253
|
+
meta = intent.get("meta") or {}
|
|
254
|
+
if meta.get("reason"):
|
|
255
|
+
lines.append(f" reason: {meta['reason']}")
|
|
256
|
+
if meta.get("tags"):
|
|
257
|
+
lines.append(f" tags: {', '.join(meta['tags'])}")
|
|
258
|
+
sp = intent.get("stack_position")
|
|
259
|
+
if sp:
|
|
260
|
+
lines.append(f" position: {sp['index']}/{sp['total']}")
|
|
261
|
+
|
|
262
|
+
verify = result.get("verify")
|
|
263
|
+
if verify:
|
|
264
|
+
lines.append("")
|
|
265
|
+
lines.append(f"{_c(BOLD, 'Verify suggestions:')}")
|
|
266
|
+
tc = verify.get("test_command")
|
|
267
|
+
if tc:
|
|
268
|
+
lines.append(f" test.command: {tc}")
|
|
269
|
+
for h in verify.get("file_hints", []):
|
|
270
|
+
lines.append(f" [{h['kind']}] {h['command']}")
|
|
271
|
+
|
|
244
272
|
return "\n".join(lines)
|
|
245
273
|
|
|
246
274
|
|
|
@@ -261,10 +289,18 @@ def _format_conflict_resolve(result: dict) -> str:
|
|
|
261
289
|
)
|
|
262
290
|
|
|
263
291
|
if result.get("sync_complete"):
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
292
|
+
out = f"{_c(GREEN, 'OK')} Resolved {resolved} \u2014 sync complete!"
|
|
293
|
+
vr = result.get("verify_result")
|
|
294
|
+
if vr:
|
|
295
|
+
if vr.get("skipped"):
|
|
296
|
+
out += f"\n verify: skipped ({vr.get('reason', '')})"
|
|
297
|
+
elif vr.get("test") == "pass":
|
|
298
|
+
out += f"\n {_c(GREEN, 'verify: PASS')} ({vr.get('command', '')})"
|
|
299
|
+
elif vr.get("test") == "fail":
|
|
300
|
+
out += f"\n {_c(RED, 'verify: FAIL')} ({vr.get('command', '')})"
|
|
301
|
+
else:
|
|
302
|
+
out += f"\n verify: {vr.get('test', '?')}"
|
|
303
|
+
return out
|
|
268
304
|
|
|
269
305
|
if result.get("conflict"):
|
|
270
306
|
patch = result.get("current_patch", "")
|
|
@@ -284,10 +320,18 @@ def _format_conflict_resolve(result: dict) -> str:
|
|
|
284
320
|
return "\n".join(lines)
|
|
285
321
|
|
|
286
322
|
if result.get("rebase_continued"):
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
323
|
+
out = f"{_c(GREEN, 'OK')} Resolved {resolved} \u2014 rebase continued."
|
|
324
|
+
vr = result.get("verify_result")
|
|
325
|
+
if vr:
|
|
326
|
+
if vr.get("skipped"):
|
|
327
|
+
out += f"\n verify: skipped ({vr.get('reason', '')})"
|
|
328
|
+
elif vr.get("test") == "pass":
|
|
329
|
+
out += f"\n {_c(GREEN, 'verify: PASS')} ({vr.get('command', '')})"
|
|
330
|
+
elif vr.get("test") == "fail":
|
|
331
|
+
out += f"\n {_c(RED, 'verify: FAIL')} ({vr.get('command', '')})"
|
|
332
|
+
else:
|
|
333
|
+
out += f"\n verify: {vr.get('test', '?')}"
|
|
334
|
+
return out
|
|
291
335
|
|
|
292
336
|
return f"{_c(GREEN, 'OK')} Resolved {resolved}."
|
|
293
337
|
|
|
@@ -500,6 +544,118 @@ def _format_patch_meta(result: dict) -> str:
|
|
|
500
544
|
return f" {k}: {v}" if k else f"{_c(GREEN, 'OK')} Done."
|
|
501
545
|
|
|
502
546
|
|
|
547
|
+
def _format_patch_lock(result: dict) -> str:
|
|
548
|
+
"""Format patch lock/unlock result."""
|
|
549
|
+
if result.get("ok") is False:
|
|
550
|
+
return f"{_c(RED, 'x')} {result.get('error', 'Failed.')}"
|
|
551
|
+
patch = result.get("patch", "")
|
|
552
|
+
owner = result.get("owner", "")
|
|
553
|
+
if result.get("was_locked") is False:
|
|
554
|
+
return f"{_c(GREEN, 'OK')} {patch} was not locked."
|
|
555
|
+
if "locked_at" in result:
|
|
556
|
+
reason = result.get("reason", "")
|
|
557
|
+
msg = f"{_c(GREEN, 'OK')} Locked {_c(BOLD, patch)} by {owner}"
|
|
558
|
+
if reason:
|
|
559
|
+
msg += f" ({reason})"
|
|
560
|
+
return msg
|
|
561
|
+
prev = result.get("previous_owner", "")
|
|
562
|
+
if prev and prev != owner:
|
|
563
|
+
return f"{_c(GREEN, 'OK')} Unlocked {_c(BOLD, patch)} (was locked by {prev})"
|
|
564
|
+
return f"{_c(GREEN, 'OK')} Unlocked {_c(BOLD, patch)}"
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def _format_patch_check(result: dict) -> str:
|
|
568
|
+
"""Format patch check (obsolescence detection) result."""
|
|
569
|
+
if result.get("ok") is False:
|
|
570
|
+
return f"{_c(RED, 'x')} {result.get('error', 'Failed.')}"
|
|
571
|
+
patches = result.get("patches", [])
|
|
572
|
+
if not patches:
|
|
573
|
+
return "No patches to check."
|
|
574
|
+
lines = []
|
|
575
|
+
for p in patches:
|
|
576
|
+
status = p.get("status", "unknown")
|
|
577
|
+
name = p.get("name", "?")
|
|
578
|
+
reason = p.get("reason", "")
|
|
579
|
+
if status == "obsolete":
|
|
580
|
+
lines.append(f" {_c(YELLOW, 'OBSOLETE')} {_c(BOLD, name)} — {reason}")
|
|
581
|
+
elif status == "active":
|
|
582
|
+
lines.append(f" {_c(GREEN, 'ACTIVE')} {_c(BOLD, name)} — {reason}")
|
|
583
|
+
else:
|
|
584
|
+
lines.append(f" {_c(DIM, 'UNKNOWN')} {name} — {reason}")
|
|
585
|
+
header = f"Checked {len(patches)} patch(es):"
|
|
586
|
+
return header + "\n" + "\n".join(lines)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _format_patch_upstream(result: dict) -> str:
|
|
590
|
+
"""Format patch upstream (PR-ready export) result."""
|
|
591
|
+
if result.get("ok") is False:
|
|
592
|
+
return f"{_c(RED, 'x')} {result.get('error', 'Failed.')}"
|
|
593
|
+
name = result.get("patch", "")
|
|
594
|
+
desc = result.get("description", "")
|
|
595
|
+
files = result.get("files", [])
|
|
596
|
+
stats = result.get("stats", "")
|
|
597
|
+
diff = result.get("diff", "")
|
|
598
|
+
lines = [
|
|
599
|
+
f"{_c(BOLD, 'PR-ready export:')} {name}",
|
|
600
|
+
f" Description: {desc}",
|
|
601
|
+
f" Files: {', '.join(files)}",
|
|
602
|
+
]
|
|
603
|
+
if stats:
|
|
604
|
+
lines.append(f" Stats: {stats}")
|
|
605
|
+
lines.append("")
|
|
606
|
+
lines.append(diff)
|
|
607
|
+
return "\n".join(lines)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _format_patch_expire(result: dict) -> str:
|
|
611
|
+
"""Format patch expire result."""
|
|
612
|
+
if result.get("ok") is False:
|
|
613
|
+
return f"{_c(RED, 'x')} {result.get('error', 'Failed.')}"
|
|
614
|
+
expired = result.get("expired", [])
|
|
615
|
+
expiring = result.get("expiring_soon", [])
|
|
616
|
+
if not expired and not expiring:
|
|
617
|
+
return f"{_c(GREEN, 'OK')} No expired or expiring patches."
|
|
618
|
+
lines = []
|
|
619
|
+
if expired:
|
|
620
|
+
lines.append(f"{_c(RED, 'Expired')} ({len(expired)}):")
|
|
621
|
+
for e in expired:
|
|
622
|
+
lines.append(f" {_c(RED, 'x')} {_c(BOLD, e['name'])} — expired {e['expires']} ({abs(e['days_left'])}d ago)")
|
|
623
|
+
if expiring:
|
|
624
|
+
lines.append(f"{_c(YELLOW, 'Expiring soon')} ({len(expiring)}):")
|
|
625
|
+
for e in expiring:
|
|
626
|
+
lines.append(f" {_c(YELLOW, '!')} {_c(BOLD, e['name'])} — expires {e['expires']} ({e['days_left']}d left)")
|
|
627
|
+
return "\n".join(lines)
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _format_patch_stats(result: dict) -> str:
|
|
631
|
+
"""Format patch stats result."""
|
|
632
|
+
if result.get("ok") is False:
|
|
633
|
+
return f"{_c(RED, 'x')} {result.get('error', 'Failed.')}"
|
|
634
|
+
patches = result.get("patches", [])
|
|
635
|
+
if not patches:
|
|
636
|
+
return "No patches."
|
|
637
|
+
lines = [f"{'#':>3} {'Name':<20} {'Age':>6} {'Size':>10} {'Syncs':>6} {'Status':<12} {'Owner':<15}"]
|
|
638
|
+
lines.append("─" * 80)
|
|
639
|
+
for i, p in enumerate(patches, 1):
|
|
640
|
+
age = f"{p['age_days']}d" if p.get("age_days", -1) >= 0 else "?"
|
|
641
|
+
size = f"+{p.get('insertions', 0)}/-{p.get('deletions', 0)}"
|
|
642
|
+
syncs = str(p.get("syncs_survived", 0))
|
|
643
|
+
status = p.get("status", "")
|
|
644
|
+
owner = p.get("owner", "") or p.get("locked_by", "") or ""
|
|
645
|
+
lines.append(f"{i:>3} {p['name']:<20} {age:>6} {size:>10} {syncs:>6} {status:<12} {owner:<15}")
|
|
646
|
+
return "\n".join(lines)
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _format_report(result: dict) -> str:
|
|
650
|
+
"""Format fork health report."""
|
|
651
|
+
if result.get("ok") is False:
|
|
652
|
+
return f"{_c(RED, 'x')} {result.get('error', 'Failed.')}"
|
|
653
|
+
report = result.get("report", "")
|
|
654
|
+
if report:
|
|
655
|
+
return report
|
|
656
|
+
return f"{_c(GREEN, 'OK')} Report generated."
|
|
657
|
+
|
|
658
|
+
|
|
503
659
|
def _format_undo(result: dict) -> str:
|
|
504
660
|
"""Format undo result."""
|
|
505
661
|
if result.get("ok") is False:
|
|
@@ -740,7 +896,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
740
896
|
sub.add_parser("diff", aliases=["d"], add_help=False)
|
|
741
897
|
|
|
742
898
|
# doctor
|
|
743
|
-
sub.add_parser("doctor", add_help=False)
|
|
899
|
+
p_doctor = sub.add_parser("doctor", add_help=False)
|
|
900
|
+
p_doctor.add_argument("--report", action="store_true")
|
|
901
|
+
|
|
902
|
+
# report
|
|
903
|
+
sub.add_parser("report", add_help=False)
|
|
744
904
|
|
|
745
905
|
# log
|
|
746
906
|
sub.add_parser("log", add_help=False)
|
|
@@ -755,6 +915,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
755
915
|
cr = sub.add_parser("conflict-resolve", add_help=False)
|
|
756
916
|
cr.add_argument("resolve_file", nargs="?", default="")
|
|
757
917
|
cr.add_argument("--content-stdin", action="store_true")
|
|
918
|
+
cr.add_argument("--verify", action="store_true",
|
|
919
|
+
help="Run test.command after the final rebase continues")
|
|
758
920
|
|
|
759
921
|
# session
|
|
760
922
|
p_session = sub.add_parser("session", add_help=False)
|
|
@@ -811,6 +973,23 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
811
973
|
pp_meta.add_argument("meta_key", nargs="?", default="")
|
|
812
974
|
pp_meta.add_argument("meta_value", nargs="?", default="")
|
|
813
975
|
|
|
976
|
+
pp_lock = patch_sub.add_parser("lock", add_help=False)
|
|
977
|
+
pp_lock.add_argument("target")
|
|
978
|
+
pp_lock.add_argument("--reason", default="")
|
|
979
|
+
|
|
980
|
+
pp_unlock = patch_sub.add_parser("unlock", add_help=False)
|
|
981
|
+
pp_unlock.add_argument("target")
|
|
982
|
+
pp_unlock.add_argument("--force", action="store_true")
|
|
983
|
+
|
|
984
|
+
pp_check = patch_sub.add_parser("check", add_help=False)
|
|
985
|
+
pp_check.add_argument("target", nargs="?", default="")
|
|
986
|
+
|
|
987
|
+
pp_upstream = patch_sub.add_parser("upstream", add_help=False)
|
|
988
|
+
pp_upstream.add_argument("target")
|
|
989
|
+
|
|
990
|
+
patch_sub.add_parser("expire", add_help=False)
|
|
991
|
+
patch_sub.add_parser("stats", add_help=False)
|
|
992
|
+
|
|
814
993
|
# workspace
|
|
815
994
|
p_ws = sub.add_parser("workspace", aliases=["ws"], add_help=False)
|
|
816
995
|
ws_sub = p_ws.add_subparsers(dest="ws_command")
|
|
@@ -853,6 +1032,26 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
853
1032
|
dp_drop.add_argument("dep_package")
|
|
854
1033
|
dp_drop.add_argument("dep_patch_name", nargs="?", default="")
|
|
855
1034
|
|
|
1035
|
+
# dep override subcommands
|
|
1036
|
+
dp_override = dep_sub.add_parser("override", add_help=False)
|
|
1037
|
+
override_sub = dp_override.add_subparsers(dest="override_command")
|
|
1038
|
+
override_sub.add_parser("list", add_help=False)
|
|
1039
|
+
override_sub.add_parser("check", add_help=False)
|
|
1040
|
+
ov_add = override_sub.add_parser("add", add_help=False)
|
|
1041
|
+
ov_add.add_argument("override_package")
|
|
1042
|
+
ov_add.add_argument("override_version")
|
|
1043
|
+
ov_add.add_argument("--reason", dest="override_reason", default="")
|
|
1044
|
+
ov_drop = override_sub.add_parser("drop", add_help=False)
|
|
1045
|
+
ov_drop.add_argument("override_package")
|
|
1046
|
+
|
|
1047
|
+
# dep fork subcommands
|
|
1048
|
+
dp_fork = dep_sub.add_parser("fork", add_help=False)
|
|
1049
|
+
fork_sub = dp_fork.add_subparsers(dest="fork_command")
|
|
1050
|
+
fork_sub.add_parser("list", add_help=False)
|
|
1051
|
+
fork_sub.add_parser("check", add_help=False)
|
|
1052
|
+
fk_sync = fork_sub.add_parser("sync", add_help=False)
|
|
1053
|
+
fk_sync.add_argument("fork_package")
|
|
1054
|
+
|
|
856
1055
|
# help
|
|
857
1056
|
sub.add_parser("help", add_help=False)
|
|
858
1057
|
|
|
@@ -884,6 +1083,31 @@ def dispatch(args: argparse.Namespace, json_mode: bool) -> Optional[dict]:
|
|
|
884
1083
|
return dm.sync()
|
|
885
1084
|
if dcmd == "drop":
|
|
886
1085
|
return dm.drop(args.dep_package, getattr(args, "dep_patch_name", ""))
|
|
1086
|
+
if dcmd == "override":
|
|
1087
|
+
ocmd = getattr(args, "override_command", "")
|
|
1088
|
+
if ocmd == "list":
|
|
1089
|
+
return dm.override_list()
|
|
1090
|
+
if ocmd == "check":
|
|
1091
|
+
return dm.override_check()
|
|
1092
|
+
if ocmd == "add":
|
|
1093
|
+
return dm.override_add(
|
|
1094
|
+
args.override_package, args.override_version,
|
|
1095
|
+
getattr(args, "override_reason", ""),
|
|
1096
|
+
)
|
|
1097
|
+
if ocmd == "drop":
|
|
1098
|
+
return dm.override_drop(args.override_package)
|
|
1099
|
+
return {"ok": False, "error": f"Unknown override subcommand: {ocmd}"}
|
|
1100
|
+
if dcmd == "fork":
|
|
1101
|
+
from bingo_core.dep_fork import ForkTracker
|
|
1102
|
+
ft = ForkTracker()
|
|
1103
|
+
fcmd = getattr(args, "fork_command", "")
|
|
1104
|
+
if fcmd == "list":
|
|
1105
|
+
return ft.fork_list()
|
|
1106
|
+
if fcmd == "check":
|
|
1107
|
+
return ft.fork_check()
|
|
1108
|
+
if fcmd == "sync":
|
|
1109
|
+
return ft.fork_sync(args.fork_package)
|
|
1110
|
+
return {"ok": False, "error": f"Unknown fork subcommand: {fcmd}"}
|
|
887
1111
|
return {"ok": False, "error": f"Unknown dep subcommand: {dcmd}"}
|
|
888
1112
|
|
|
889
1113
|
# setup doesn't need a Repo (works outside git repos)
|
|
@@ -919,7 +1143,10 @@ def dispatch(args: argparse.Namespace, json_mode: bool) -> Optional[dict]:
|
|
|
919
1143
|
return repo.diff()
|
|
920
1144
|
|
|
921
1145
|
if cmd == "doctor":
|
|
922
|
-
return repo.doctor()
|
|
1146
|
+
return repo.doctor(report=getattr(args, "report", False))
|
|
1147
|
+
|
|
1148
|
+
if cmd == "report":
|
|
1149
|
+
return repo.report()
|
|
923
1150
|
|
|
924
1151
|
if cmd == "log":
|
|
925
1152
|
return repo.history()
|
|
@@ -935,7 +1162,9 @@ def dispatch(args: argparse.Namespace, json_mode: bool) -> Optional[dict]:
|
|
|
935
1162
|
if args.content_stdin:
|
|
936
1163
|
import sys as _sys
|
|
937
1164
|
content = _sys.stdin.read()
|
|
938
|
-
return repo.conflict_resolve(
|
|
1165
|
+
return repo.conflict_resolve(
|
|
1166
|
+
args.resolve_file, content, verify=args.verify
|
|
1167
|
+
)
|
|
939
1168
|
|
|
940
1169
|
if cmd == "session":
|
|
941
1170
|
update = (args.session_action == "update")
|
|
@@ -1018,6 +1247,24 @@ def _dispatch_patch(args: argparse.Namespace, repo: Repo) -> dict:
|
|
|
1018
1247
|
value=meta_value,
|
|
1019
1248
|
)
|
|
1020
1249
|
|
|
1250
|
+
if pcmd == "lock":
|
|
1251
|
+
return repo.patch_lock(args.target, reason=getattr(args, "reason", ""))
|
|
1252
|
+
|
|
1253
|
+
if pcmd == "unlock":
|
|
1254
|
+
return repo.patch_unlock(args.target, force=getattr(args, "force", False))
|
|
1255
|
+
|
|
1256
|
+
if pcmd == "check":
|
|
1257
|
+
return repo.patch_check(getattr(args, "target", ""))
|
|
1258
|
+
|
|
1259
|
+
if pcmd == "upstream":
|
|
1260
|
+
return repo.patch_upstream(args.target)
|
|
1261
|
+
|
|
1262
|
+
if pcmd == "expire":
|
|
1263
|
+
return repo.patch_expire()
|
|
1264
|
+
|
|
1265
|
+
if pcmd == "stats":
|
|
1266
|
+
return repo.patch_stats()
|
|
1267
|
+
|
|
1021
1268
|
raise BingoError(f"Unknown patch subcommand: {pcmd}")
|
|
1022
1269
|
|
|
1023
1270
|
|
|
@@ -1075,6 +1322,7 @@ _FORMATTERS: Dict[str, Any] = {
|
|
|
1075
1322
|
"smart-sync": _format_smart_sync,
|
|
1076
1323
|
"setup": _format_setup,
|
|
1077
1324
|
"dep": _format_dep,
|
|
1325
|
+
"report": _format_report,
|
|
1078
1326
|
}
|
|
1079
1327
|
|
|
1080
1328
|
|
|
@@ -1099,6 +1347,16 @@ def _get_formatter(args: argparse.Namespace):
|
|
|
1099
1347
|
return _format_patch_import
|
|
1100
1348
|
if pcmd == "meta":
|
|
1101
1349
|
return _format_patch_meta
|
|
1350
|
+
if pcmd in ("lock", "unlock"):
|
|
1351
|
+
return _format_patch_lock
|
|
1352
|
+
if pcmd == "check":
|
|
1353
|
+
return _format_patch_check
|
|
1354
|
+
if pcmd == "upstream":
|
|
1355
|
+
return _format_patch_upstream
|
|
1356
|
+
if pcmd == "expire":
|
|
1357
|
+
return _format_patch_expire
|
|
1358
|
+
if pcmd == "stats":
|
|
1359
|
+
return _format_patch_stats
|
|
1102
1360
|
return _format_generic
|
|
1103
1361
|
|
|
1104
1362
|
# workspace subcommands
|
package/bingo_core/__init__.py
CHANGED
|
@@ -14,7 +14,7 @@ import re
|
|
|
14
14
|
|
|
15
15
|
# --- Constants ---
|
|
16
16
|
|
|
17
|
-
VERSION = "2.1.
|
|
17
|
+
VERSION = "2.1.3"
|
|
18
18
|
PATCH_PREFIX = "[bl]"
|
|
19
19
|
CONFIG_FILE = ".bingolight"
|
|
20
20
|
BINGO_DIR = ".bingo"
|
|
@@ -42,6 +42,7 @@ from bingo_core.models import PatchInfo, ConflictInfo # noqa: E402
|
|
|
42
42
|
from bingo_core.git import Git # noqa: E402
|
|
43
43
|
from bingo_core.config import Config # noqa: E402
|
|
44
44
|
from bingo_core.state import State # noqa: E402
|
|
45
|
+
from bingo_core.team import TeamState # noqa: E402
|
|
45
46
|
from bingo_core.repo import Repo # noqa: E402
|
|
46
47
|
|
|
47
48
|
__all__ = [
|
|
@@ -73,5 +74,6 @@ __all__ = [
|
|
|
73
74
|
"Git",
|
|
74
75
|
"Config",
|
|
75
76
|
"State",
|
|
77
|
+
"TeamState",
|
|
76
78
|
"Repo",
|
|
77
79
|
]
|