evo-cli 0.18.0__tar.gz → 0.19.0__tar.gz
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.
- {evo_cli-0.18.0 → evo_cli-0.19.0}/PKG-INFO +17 -1
- {evo_cli-0.18.0 → evo_cli-0.19.0}/README.md +16 -0
- evo_cli-0.19.0/evo_cli/VERSION +1 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/__init__.py +3 -1
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/_paths.py +4 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/_render.py +10 -0
- evo_cli-0.19.0/evo_cli/commands/harness/clone.py +123 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/pull.py +3 -7
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/views.py +4 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli.egg-info/PKG-INFO +17 -1
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli.egg-info/SOURCES.txt +2 -0
- evo_cli-0.19.0/tests/test_harness_clone.py +270 -0
- evo_cli-0.18.0/evo_cli/VERSION +0 -1
- {evo_cli-0.18.0 → evo_cli-0.19.0}/Containerfile +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/HISTORY.md +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/LICENSE +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/MANIFEST.in +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/__init__.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/__main__.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/base.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/cli.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/__init__.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/agent_toy.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/claude_code.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/cloudflare.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/cred.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/download.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/fix_claude.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/gdrive.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/gh.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/_dag.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/_git.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/_model.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/_mutate.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/_server.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/check.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/edit.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/serve.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/show.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/index-CsALFg16.css +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/index-DwEXL5pV.js +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/harness/web/index.html +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/hwid.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/hwid_reset.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/localproxy.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/mcp.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/miniconda.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/netcheck.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/opencode.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/plantuml.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/serp.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/site2s.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/ssh.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/sysmon.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/tts.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/update.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/commands/wifi.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/console.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/credentials/__init__.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/credentials/doctor.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/credentials/google_oauth.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/credentials/migrate.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/credentials/oauth_flow.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/credentials/registry.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/credentials/store.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/credentials/sync.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/mcp_registry.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/serp/__init__.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/serp/client.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/serp/creds.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/serp/errors.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/serp/install.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/serp/render.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/tts/__init__.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/tts/chunking.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/tts/core.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/tts/creds.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/tts/errors.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/tts/http.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/tts/openai.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/tts/player.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli/tts/vbee.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli.egg-info/dependency_links.txt +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli.egg-info/entry_points.txt +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli.egg-info/requires.txt +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/evo_cli.egg-info/top_level.txt +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/pyproject.toml +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/setup.cfg +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/tests/__init__.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/tests/test_agent_toy.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/tests/test_claude_code.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/tests/test_cli.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/tests/test_console.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/tests/test_cred.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/tests/test_download.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/tests/test_fix_claude.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/tests/test_gh.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/tests/test_harness.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/tests/test_harness_dag.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/tests/test_mcp.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/tests/test_opencode.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/tests/test_plantuml.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/tests/test_serp.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/tests/test_sysmon.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/tests/test_tts.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/tests/test_update.py +0 -0
- {evo_cli-0.18.0 → evo_cli-0.19.0}/tests/test_wifi.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: evo_cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.19.0
|
|
4
4
|
Summary: Evolution CLI - a developer toolbox for setting up dev machines
|
|
5
5
|
Author: maycuatroi
|
|
6
6
|
Project-URL: Homepage, https://github.com/maycuatroi/evo-cli
|
|
@@ -159,6 +159,21 @@ else has the flat file open.
|
|
|
159
159
|
|
|
160
160
|
#### Harness Repositories
|
|
161
161
|
|
|
162
|
+
Clone every repository a harness manifest declares but this machine does not have yet:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
evo harness clone
|
|
166
|
+
evo harness clone --dry-run # show what lands where, change nothing
|
|
167
|
+
evo harness clone --repo backend --depth 1
|
|
168
|
+
evo harness clone --all # include repos marked present: false
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Each repo lands on its declared `path`, falling back to `<workspace>/<name>`, so a fresh machine
|
|
172
|
+
ends up with the layout the manifest, the contract seams and the exec-plans already assume. A repo
|
|
173
|
+
already on disk is left alone; if its `origin` is not the remote the manifest names, the summary
|
|
174
|
+
says so instead of touching it. When the manifest names a `branch`, the clone moves onto it, and a
|
|
175
|
+
branch nobody has pushed yet is reported rather than treated as a failure.
|
|
176
|
+
|
|
162
177
|
Fast-forward every available repository declared in a harness manifest:
|
|
163
178
|
|
|
164
179
|
```bash
|
|
@@ -299,6 +314,7 @@ evo harness plans # progress across every exec-plan
|
|
|
299
314
|
evo harness show <plan>
|
|
300
315
|
evo harness check <plan> [--fetch] # what the plan claims vs what git says
|
|
301
316
|
evo harness graph <plan>:steps # the same DAG as an adjacency list
|
|
317
|
+
evo harness clone # clone every declared repo into its declared path
|
|
302
318
|
evo harness pull # fast-forward every repo
|
|
303
319
|
```
|
|
304
320
|
|
|
@@ -131,6 +131,21 @@ else has the flat file open.
|
|
|
131
131
|
|
|
132
132
|
#### Harness Repositories
|
|
133
133
|
|
|
134
|
+
Clone every repository a harness manifest declares but this machine does not have yet:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
evo harness clone
|
|
138
|
+
evo harness clone --dry-run # show what lands where, change nothing
|
|
139
|
+
evo harness clone --repo backend --depth 1
|
|
140
|
+
evo harness clone --all # include repos marked present: false
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Each repo lands on its declared `path`, falling back to `<workspace>/<name>`, so a fresh machine
|
|
144
|
+
ends up with the layout the manifest, the contract seams and the exec-plans already assume. A repo
|
|
145
|
+
already on disk is left alone; if its `origin` is not the remote the manifest names, the summary
|
|
146
|
+
says so instead of touching it. When the manifest names a `branch`, the clone moves onto it, and a
|
|
147
|
+
branch nobody has pushed yet is reported rather than treated as a failure.
|
|
148
|
+
|
|
134
149
|
Fast-forward every available repository declared in a harness manifest:
|
|
135
150
|
|
|
136
151
|
```bash
|
|
@@ -271,6 +286,7 @@ evo harness plans # progress across every exec-plan
|
|
|
271
286
|
evo harness show <plan>
|
|
272
287
|
evo harness check <plan> [--fetch] # what the plan claims vs what git says
|
|
273
288
|
evo harness graph <plan>:steps # the same DAG as an adjacency list
|
|
289
|
+
evo harness clone # clone every declared repo into its declared path
|
|
274
290
|
evo harness pull # fast-forward every repo
|
|
275
291
|
```
|
|
276
292
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.19.0
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import rich_click as click
|
|
2
2
|
|
|
3
3
|
from evo_cli.commands.harness.check import check
|
|
4
|
+
from evo_cli.commands.harness.clone import clone
|
|
4
5
|
from evo_cli.commands.harness.edit import debt, question, repo, step
|
|
5
6
|
from evo_cli.commands.harness.pull import pull
|
|
6
7
|
from evo_cli.commands.harness.serve import serve
|
|
@@ -21,9 +22,10 @@ def harness_group():
|
|
|
21
22
|
evo harness graph <plan> print a DAG as an adjacency list
|
|
22
23
|
evo harness check <plan> check what the plan claims against real git
|
|
23
24
|
evo harness step <plan> 3 done mark a step
|
|
25
|
+
evo harness clone clone every declared repo into its declared path
|
|
24
26
|
evo harness pull fast-forward every repo
|
|
25
27
|
"""
|
|
26
28
|
|
|
27
29
|
|
|
28
|
-
for command in (serve, repos, seams, plans, show, graph, check, step, debt, question, repo, pull):
|
|
30
|
+
for command in (serve, repos, seams, plans, show, graph, check, step, debt, question, repo, clone, pull):
|
|
29
31
|
harness_group.add_command(command)
|
|
@@ -62,6 +62,10 @@ def load_repos(manifest_path):
|
|
|
62
62
|
{
|
|
63
63
|
"name": name,
|
|
64
64
|
"path": path,
|
|
65
|
+
# Where the manifest says the repo belongs, whether or not it is there yet.
|
|
66
|
+
# `path` falls back to the workspace when the declared path is missing, so a
|
|
67
|
+
# command that creates the checkout has to read this one instead.
|
|
68
|
+
"declared_path": declared or fallback.resolve(),
|
|
65
69
|
"present": bool(present) and path.is_dir(),
|
|
66
70
|
"declared_present": bool(present),
|
|
67
71
|
"role": str(entry.get("role") or ""),
|
|
@@ -85,6 +85,16 @@ def print_graph(title: str, graph: dict) -> None:
|
|
|
85
85
|
print_warnings(graph["warnings"])
|
|
86
86
|
|
|
87
87
|
|
|
88
|
+
def print_summary(results: list) -> None:
|
|
89
|
+
"""One `name status` line per repo, coloured by the verb the status starts with."""
|
|
90
|
+
click.echo()
|
|
91
|
+
click.secho("Summary", bold=True)
|
|
92
|
+
width = max((len(name) for name, _ in results), default=0)
|
|
93
|
+
for name, status in results:
|
|
94
|
+
color = "red" if status.startswith("failed") else "yellow" if status.startswith("skipped") else "green"
|
|
95
|
+
click.secho(f" {name:<{width}} {status}", fg=color)
|
|
96
|
+
|
|
97
|
+
|
|
88
98
|
def fail_if(condition: bool) -> None:
|
|
89
99
|
if condition:
|
|
90
100
|
raise click.exceptions.Exit(1)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import shutil
|
|
3
|
+
|
|
4
|
+
import rich_click as click
|
|
5
|
+
|
|
6
|
+
from evo_cli.commands.harness._paths import find_manifest, git, harness_option, load_repos
|
|
7
|
+
from evo_cli.commands.harness._render import print_summary
|
|
8
|
+
|
|
9
|
+
SCHEME_RE = re.compile(r"^\w+://")
|
|
10
|
+
USER_RE = re.compile(r"^[^@/]+@")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def normalize_remote(url):
|
|
14
|
+
"""Reduce a remote URL to host + path so the ssh and https forms of one repo compare equal."""
|
|
15
|
+
text = str(url).strip().rstrip("/")
|
|
16
|
+
if text.endswith(".git"):
|
|
17
|
+
text = text[: -len(".git")]
|
|
18
|
+
text = USER_RE.sub("", SCHEME_RE.sub("", text))
|
|
19
|
+
return text.replace(":", "/", 1).lower()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def detail_of(result, fallback):
|
|
23
|
+
lines = ((result.stderr or "") + (result.stdout or "")).strip().splitlines()
|
|
24
|
+
return lines[-1] if lines else fallback
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def already_cloned(entry, target):
|
|
28
|
+
"""A checkout is already there. Say so, and say if it came from a different remote."""
|
|
29
|
+
declared = entry["origin"]
|
|
30
|
+
actual = git(target, "remote", "get-url", "origin").stdout.strip()
|
|
31
|
+
if declared and actual and normalize_remote(declared) != normalize_remote(actual):
|
|
32
|
+
return f"skipped (already cloned, but origin is {actual})"
|
|
33
|
+
return "skipped (already cloned)"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def checkout_declared_branch(entry, target):
|
|
37
|
+
"""Move onto the branch the manifest names. A branch that is not on origin is not an error.
|
|
38
|
+
|
|
39
|
+
Cloning with `--branch` would fail the whole clone for a branch that has not been pushed yet,
|
|
40
|
+
which is a normal state for a plan in progress. Clone the default branch, then try to move.
|
|
41
|
+
"""
|
|
42
|
+
branch = entry["branch"]
|
|
43
|
+
if not branch or branch.upper() == "TBD":
|
|
44
|
+
return ""
|
|
45
|
+
current = git(target, "rev-parse", "--abbrev-ref", "HEAD").stdout.strip()
|
|
46
|
+
if current == branch:
|
|
47
|
+
return ""
|
|
48
|
+
if git(target, "checkout", branch, timeout=120).returncode != 0:
|
|
49
|
+
return f" (branch {branch!r} is not on origin, left on {current or 'HEAD'})"
|
|
50
|
+
return f" (on {branch})"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@click.command("clone", help="Clone the repositories a harness declares into their declared paths.")
|
|
54
|
+
@harness_option
|
|
55
|
+
@click.option("--repo", "repos", multiple=True, help="Clone only this repo name. Repeat for multiple repos.")
|
|
56
|
+
@click.option("--all", "include_absent", is_flag=True, help="Also clone repos the manifest marks present: false.")
|
|
57
|
+
@click.option("--depth", type=int, help="Shallow clone with this many commits of history.")
|
|
58
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be cloned, and where.")
|
|
59
|
+
def clone(harness_path, repos, include_absent, depth, dry_run):
|
|
60
|
+
"""Clone every repository the harness manifest declares but this machine does not have.
|
|
61
|
+
|
|
62
|
+
Each repo lands on its declared `path`, falling back to `<workspace>/<name>`, so a fresh
|
|
63
|
+
machine ends up with the layout the manifest, the contract seams and the exec-plans already
|
|
64
|
+
assume. Repos already on disk are left alone.
|
|
65
|
+
"""
|
|
66
|
+
if shutil.which("git") is None:
|
|
67
|
+
raise click.ClickException("git is not installed or is not on PATH")
|
|
68
|
+
|
|
69
|
+
manifest_path = find_manifest(harness_path)
|
|
70
|
+
manifest, entries = load_repos(manifest_path)
|
|
71
|
+
available = {entry["name"] for entry in entries}
|
|
72
|
+
unknown = sorted(set(repos) - available)
|
|
73
|
+
if unknown:
|
|
74
|
+
raise click.ClickException(f"Unknown repo name(s): {', '.join(unknown)}")
|
|
75
|
+
selected = [entry for entry in entries if not repos or entry["name"] in repos]
|
|
76
|
+
|
|
77
|
+
click.echo(f"Harness: {manifest.get('name') or manifest_path.parent.name}")
|
|
78
|
+
click.echo(f"Manifest: {manifest_path}")
|
|
79
|
+
results = []
|
|
80
|
+
failures = 0
|
|
81
|
+
|
|
82
|
+
for entry in selected:
|
|
83
|
+
name = entry["name"]
|
|
84
|
+
target = entry["declared_path"]
|
|
85
|
+
|
|
86
|
+
# `present: false` means the manifest knows about the repo and deliberately does not want
|
|
87
|
+
# it here. Naming it with --repo, or asking for --all, overrides that.
|
|
88
|
+
if not entry["declared_present"] and not include_absent and not repos:
|
|
89
|
+
results.append((name, "skipped (present: false)"))
|
|
90
|
+
continue
|
|
91
|
+
if (target / ".git").is_dir():
|
|
92
|
+
results.append((name, already_cloned(entry, target)))
|
|
93
|
+
continue
|
|
94
|
+
if entry["path"] != target and (entry["path"] / ".git").is_dir():
|
|
95
|
+
results.append((name, f"skipped (already cloned at {entry['path']}, manifest says {target})"))
|
|
96
|
+
continue
|
|
97
|
+
if target.exists() and (not target.is_dir() or any(target.iterdir())):
|
|
98
|
+
results.append((name, f"failed (path is taken and is not an empty directory: {target})"))
|
|
99
|
+
failures += 1
|
|
100
|
+
continue
|
|
101
|
+
if not entry["origin"]:
|
|
102
|
+
results.append((name, "failed (no origin declared in the manifest)"))
|
|
103
|
+
failures += 1
|
|
104
|
+
continue
|
|
105
|
+
if dry_run:
|
|
106
|
+
results.append((name, f"would clone {entry['origin']} into {target}"))
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
args = ["clone"]
|
|
111
|
+
if depth:
|
|
112
|
+
args += ["--depth", str(depth)]
|
|
113
|
+
args += [entry["origin"], str(target)]
|
|
114
|
+
result = git(target.parent, *args, timeout=900)
|
|
115
|
+
if result.returncode != 0:
|
|
116
|
+
results.append((name, f"failed ({detail_of(result, 'git clone failed')})"))
|
|
117
|
+
failures += 1
|
|
118
|
+
continue
|
|
119
|
+
results.append((name, f"cloned into {target}{checkout_declared_branch(entry, target)}"))
|
|
120
|
+
|
|
121
|
+
print_summary(results)
|
|
122
|
+
if failures:
|
|
123
|
+
raise click.exceptions.Exit(1)
|
|
@@ -3,6 +3,7 @@ import shutil
|
|
|
3
3
|
import rich_click as click
|
|
4
4
|
|
|
5
5
|
from evo_cli.commands.harness._paths import find_manifest, git, harness_option, load_repos
|
|
6
|
+
from evo_cli.commands.harness._render import print_summary
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
@click.command("pull", help="Fast-forward all available repositories in a harness.")
|
|
@@ -34,7 +35,7 @@ def pull(harness_path, repos, prune, dry_run):
|
|
|
34
35
|
results.append((name, "skipped (present: false)"))
|
|
35
36
|
continue
|
|
36
37
|
if not path.is_dir():
|
|
37
|
-
results.append((name, f"failed (missing: {path})"))
|
|
38
|
+
results.append((name, f"failed (missing: {path} - run `evo harness clone`)"))
|
|
38
39
|
incomplete += 1
|
|
39
40
|
continue
|
|
40
41
|
|
|
@@ -64,11 +65,6 @@ def pull(harness_path, repos, prune, dry_run):
|
|
|
64
65
|
head = git(path, "log", "-1", "--oneline")
|
|
65
66
|
results.append((name, head.stdout.strip() or "updated"))
|
|
66
67
|
|
|
67
|
-
|
|
68
|
-
click.secho("Summary", bold=True)
|
|
69
|
-
width = max((len(name) for name, _ in results), default=0)
|
|
70
|
-
for name, status in results:
|
|
71
|
-
color = "red" if status.startswith("failed") else "yellow" if status.startswith("skipped") else "green"
|
|
72
|
-
click.secho(f" {name:<{width}} {status}", fg=color)
|
|
68
|
+
print_summary(results)
|
|
73
69
|
if incomplete:
|
|
74
70
|
raise click.exceptions.Exit(1)
|
|
@@ -71,6 +71,10 @@ def repos(harness_path, with_git):
|
|
|
71
71
|
grid.add_row(f"[bold]{entry['name']}[/]", entry["role"] or "-", branch, state, one_line(entry["note"], width))
|
|
72
72
|
console.print(grid)
|
|
73
73
|
|
|
74
|
+
missing = [entry["name"] for entry in info["repos"] if not entry["present"]]
|
|
75
|
+
if missing:
|
|
76
|
+
console.print(f"\n[red]{len(missing)} repo(s) missing[/] - run [accent]evo harness clone[/] to fetch them")
|
|
77
|
+
|
|
74
78
|
if info["proposals_pending"]:
|
|
75
79
|
console.print(f"\n[yellow]{len(info['proposals_pending'])} pending proposal(s)[/] in proposals/_pending")
|
|
76
80
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: evo_cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.19.0
|
|
4
4
|
Summary: Evolution CLI - a developer toolbox for setting up dev machines
|
|
5
5
|
Author: maycuatroi
|
|
6
6
|
Project-URL: Homepage, https://github.com/maycuatroi/evo-cli
|
|
@@ -159,6 +159,21 @@ else has the flat file open.
|
|
|
159
159
|
|
|
160
160
|
#### Harness Repositories
|
|
161
161
|
|
|
162
|
+
Clone every repository a harness manifest declares but this machine does not have yet:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
evo harness clone
|
|
166
|
+
evo harness clone --dry-run # show what lands where, change nothing
|
|
167
|
+
evo harness clone --repo backend --depth 1
|
|
168
|
+
evo harness clone --all # include repos marked present: false
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Each repo lands on its declared `path`, falling back to `<workspace>/<name>`, so a fresh machine
|
|
172
|
+
ends up with the layout the manifest, the contract seams and the exec-plans already assume. A repo
|
|
173
|
+
already on disk is left alone; if its `origin` is not the remote the manifest names, the summary
|
|
174
|
+
says so instead of touching it. When the manifest names a `branch`, the clone moves onto it, and a
|
|
175
|
+
branch nobody has pushed yet is reported rather than treated as a failure.
|
|
176
|
+
|
|
162
177
|
Fast-forward every available repository declared in a harness manifest:
|
|
163
178
|
|
|
164
179
|
```bash
|
|
@@ -299,6 +314,7 @@ evo harness plans # progress across every exec-plan
|
|
|
299
314
|
evo harness show <plan>
|
|
300
315
|
evo harness check <plan> [--fetch] # what the plan claims vs what git says
|
|
301
316
|
evo harness graph <plan>:steps # the same DAG as an adjacency list
|
|
317
|
+
evo harness clone # clone every declared repo into its declared path
|
|
302
318
|
evo harness pull # fast-forward every repo
|
|
303
319
|
```
|
|
304
320
|
|
|
@@ -50,6 +50,7 @@ evo_cli/commands/harness/_paths.py
|
|
|
50
50
|
evo_cli/commands/harness/_render.py
|
|
51
51
|
evo_cli/commands/harness/_server.py
|
|
52
52
|
evo_cli/commands/harness/check.py
|
|
53
|
+
evo_cli/commands/harness/clone.py
|
|
53
54
|
evo_cli/commands/harness/edit.py
|
|
54
55
|
evo_cli/commands/harness/pull.py
|
|
55
56
|
evo_cli/commands/harness/serve.py
|
|
@@ -103,6 +104,7 @@ tests/test_download.py
|
|
|
103
104
|
tests/test_fix_claude.py
|
|
104
105
|
tests/test_gh.py
|
|
105
106
|
tests/test_harness.py
|
|
107
|
+
tests/test_harness_clone.py
|
|
106
108
|
tests/test_harness_dag.py
|
|
107
109
|
tests/test_mcp.py
|
|
108
110
|
tests/test_opencode.py
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
from click.testing import CliRunner
|
|
5
|
+
|
|
6
|
+
from evo_cli.cli import cli
|
|
7
|
+
|
|
8
|
+
# harness/__init__ binds the command object to the name `clone`, shadowing the submodule,
|
|
9
|
+
# so the module has to be fetched by path rather than imported by name.
|
|
10
|
+
clone_command = importlib.import_module("evo_cli.commands.harness.clone")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _result(returncode=0, stdout="", stderr=""):
|
|
14
|
+
return subprocess.CompletedProcess([], returncode, stdout, stderr)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _write_harness(root, workspace, extra=""):
|
|
18
|
+
"""alpha declares a path outside <workspace>/<name>; beta relies on the fallback."""
|
|
19
|
+
root.mkdir()
|
|
20
|
+
(root / "harness.yaml").write_text(
|
|
21
|
+
f"name: test-cluster\nworkspace: {workspace.as_posix()}\nrepos:\n"
|
|
22
|
+
f"- name: alpha\n path: {(workspace / 'nested' / 'alpha').as_posix()}\n"
|
|
23
|
+
" origin: https://github.com/acme/alpha.git\n"
|
|
24
|
+
"- name: beta\n origin: git@github.com:acme/beta.git\n" + extra,
|
|
25
|
+
encoding="utf-8",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _checkout(path, origin="https://github.com/acme/alpha.git"):
|
|
30
|
+
(path / ".git").mkdir(parents=True)
|
|
31
|
+
return origin
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_clone_command_is_registered():
|
|
35
|
+
assert "clone" in cli.commands["harness"].commands
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_clone_dry_run_targets_the_declared_path_not_the_workspace_fallback(tmp_path, monkeypatch):
|
|
39
|
+
workspace = tmp_path / "repos"
|
|
40
|
+
workspace.mkdir()
|
|
41
|
+
root = tmp_path / "cluster"
|
|
42
|
+
_write_harness(root, workspace)
|
|
43
|
+
calls = []
|
|
44
|
+
monkeypatch.setattr(clone_command, "git", lambda path, *args, **kwargs: calls.append((path, args)) or _result())
|
|
45
|
+
|
|
46
|
+
result = CliRunner().invoke(cli, ["harness", "clone", "--harness", str(root), "--dry-run"])
|
|
47
|
+
|
|
48
|
+
assert result.exit_code == 0
|
|
49
|
+
assert f"would clone https://github.com/acme/alpha.git into {(workspace / 'nested' / 'alpha').resolve()}" in (
|
|
50
|
+
result.output
|
|
51
|
+
)
|
|
52
|
+
assert f"would clone git@github.com:acme/beta.git into {(workspace / 'beta').resolve()}" in result.output
|
|
53
|
+
assert calls == []
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_clone_runs_git_clone_from_the_parent_directory(tmp_path, monkeypatch):
|
|
57
|
+
workspace = tmp_path / "repos"
|
|
58
|
+
workspace.mkdir()
|
|
59
|
+
root = tmp_path / "cluster"
|
|
60
|
+
_write_harness(root, workspace)
|
|
61
|
+
calls = []
|
|
62
|
+
|
|
63
|
+
def fake_git(path, *args, **kwargs):
|
|
64
|
+
calls.append((path, args))
|
|
65
|
+
return _result()
|
|
66
|
+
|
|
67
|
+
monkeypatch.setattr(clone_command, "git", fake_git)
|
|
68
|
+
result = CliRunner().invoke(cli, ["harness", "clone", "--harness", str(root), "--repo", "beta", "--depth", "1"])
|
|
69
|
+
|
|
70
|
+
target = (workspace / "beta").resolve()
|
|
71
|
+
assert result.exit_code == 0
|
|
72
|
+
assert calls == [(target.parent, ("clone", "--depth", "1", "git@github.com:acme/beta.git", str(target)))]
|
|
73
|
+
assert f"cloned into {target}" in result.output
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_clone_creates_the_parent_of_a_nested_declared_path(tmp_path, monkeypatch):
|
|
77
|
+
workspace = tmp_path / "repos"
|
|
78
|
+
workspace.mkdir()
|
|
79
|
+
root = tmp_path / "cluster"
|
|
80
|
+
_write_harness(root, workspace)
|
|
81
|
+
monkeypatch.setattr(clone_command, "git", lambda path, *args, **kwargs: _result())
|
|
82
|
+
|
|
83
|
+
result = CliRunner().invoke(cli, ["harness", "clone", "--harness", str(root), "--repo", "alpha"])
|
|
84
|
+
|
|
85
|
+
assert result.exit_code == 0
|
|
86
|
+
assert (workspace / "nested").is_dir()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_clone_leaves_an_existing_checkout_alone(tmp_path, monkeypatch):
|
|
90
|
+
workspace = tmp_path / "repos"
|
|
91
|
+
workspace.mkdir()
|
|
92
|
+
root = tmp_path / "cluster"
|
|
93
|
+
_write_harness(root, workspace)
|
|
94
|
+
_checkout(workspace / "nested" / "alpha")
|
|
95
|
+
# The ssh form of the same remote must not read as a different repository.
|
|
96
|
+
monkeypatch.setattr(
|
|
97
|
+
clone_command, "git", lambda path, *args, **kwargs: _result(stdout="git@github.com:acme/alpha.git\n")
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
result = CliRunner().invoke(cli, ["harness", "clone", "--harness", str(root), "--repo", "alpha"])
|
|
101
|
+
|
|
102
|
+
assert result.exit_code == 0
|
|
103
|
+
assert "alpha skipped (already cloned)" in result.output
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_clone_reports_a_checkout_whose_origin_is_a_different_repo(tmp_path, monkeypatch):
|
|
107
|
+
workspace = tmp_path / "repos"
|
|
108
|
+
workspace.mkdir()
|
|
109
|
+
root = tmp_path / "cluster"
|
|
110
|
+
_write_harness(root, workspace)
|
|
111
|
+
_checkout(workspace / "nested" / "alpha")
|
|
112
|
+
monkeypatch.setattr(
|
|
113
|
+
clone_command, "git", lambda path, *args, **kwargs: _result(stdout="https://github.com/other/fork.git\n")
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
result = CliRunner().invoke(cli, ["harness", "clone", "--harness", str(root), "--repo", "alpha"])
|
|
117
|
+
|
|
118
|
+
assert result.exit_code == 0
|
|
119
|
+
assert "skipped (already cloned, but origin is https://github.com/other/fork.git)" in result.output
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_clone_reports_a_checkout_sitting_somewhere_other_than_the_declared_path(tmp_path, monkeypatch):
|
|
123
|
+
workspace = tmp_path / "repos"
|
|
124
|
+
workspace.mkdir()
|
|
125
|
+
root = tmp_path / "cluster"
|
|
126
|
+
_write_harness(root, workspace)
|
|
127
|
+
_checkout(workspace / "alpha")
|
|
128
|
+
monkeypatch.setattr(clone_command, "git", lambda path, *args, **kwargs: _result())
|
|
129
|
+
|
|
130
|
+
result = CliRunner().invoke(cli, ["harness", "clone", "--harness", str(root), "--repo", "alpha"])
|
|
131
|
+
|
|
132
|
+
assert result.exit_code == 0
|
|
133
|
+
assert f"already cloned at {(workspace / 'alpha').resolve()}" in result.output
|
|
134
|
+
assert f"manifest says {(workspace / 'nested' / 'alpha').resolve()}" in result.output
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_clone_refuses_to_write_into_an_occupied_path(tmp_path, monkeypatch):
|
|
138
|
+
workspace = tmp_path / "repos"
|
|
139
|
+
workspace.mkdir()
|
|
140
|
+
(workspace / "beta").mkdir()
|
|
141
|
+
(workspace / "beta" / "notes.txt").write_text("mine", encoding="utf-8")
|
|
142
|
+
root = tmp_path / "cluster"
|
|
143
|
+
_write_harness(root, workspace)
|
|
144
|
+
monkeypatch.setattr(clone_command, "git", lambda path, *args, **kwargs: _result())
|
|
145
|
+
|
|
146
|
+
result = CliRunner().invoke(cli, ["harness", "clone", "--harness", str(root), "--repo", "beta"])
|
|
147
|
+
|
|
148
|
+
assert result.exit_code == 1
|
|
149
|
+
assert "failed (path is taken and is not an empty directory" in result.output
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_clone_reports_the_git_failure_and_exits_nonzero(tmp_path, monkeypatch):
|
|
153
|
+
workspace = tmp_path / "repos"
|
|
154
|
+
workspace.mkdir()
|
|
155
|
+
root = tmp_path / "cluster"
|
|
156
|
+
_write_harness(root, workspace)
|
|
157
|
+
monkeypatch.setattr(
|
|
158
|
+
clone_command,
|
|
159
|
+
"git",
|
|
160
|
+
lambda path, *args, **kwargs: _result(returncode=128, stderr="fatal: repository not found\n"),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
result = CliRunner().invoke(cli, ["harness", "clone", "--harness", str(root), "--repo", "beta"])
|
|
164
|
+
|
|
165
|
+
assert result.exit_code == 1
|
|
166
|
+
assert "failed (fatal: repository not found)" in result.output
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_clone_skips_present_false_unless_asked(tmp_path, monkeypatch):
|
|
170
|
+
workspace = tmp_path / "repos"
|
|
171
|
+
workspace.mkdir()
|
|
172
|
+
root = tmp_path / "cluster"
|
|
173
|
+
_write_harness(root, workspace, extra="- name: gamma\n present: false\n origin: https://x.test/gamma.git\n")
|
|
174
|
+
monkeypatch.setattr(clone_command, "git", lambda path, *args, **kwargs: _result())
|
|
175
|
+
|
|
176
|
+
skipped = CliRunner().invoke(cli, ["harness", "clone", "--harness", str(root), "--dry-run"])
|
|
177
|
+
forced = CliRunner().invoke(cli, ["harness", "clone", "--harness", str(root), "--dry-run", "--all"])
|
|
178
|
+
named = CliRunner().invoke(cli, ["harness", "clone", "--harness", str(root), "--dry-run", "--repo", "gamma"])
|
|
179
|
+
|
|
180
|
+
assert "gamma skipped (present: false)" in skipped.output
|
|
181
|
+
assert f"would clone https://x.test/gamma.git into {(workspace / 'gamma').resolve()}" in forced.output
|
|
182
|
+
assert f"would clone https://x.test/gamma.git into {(workspace / 'gamma').resolve()}" in named.output
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_clone_fails_a_repo_with_no_origin(tmp_path, monkeypatch):
|
|
186
|
+
workspace = tmp_path / "repos"
|
|
187
|
+
workspace.mkdir()
|
|
188
|
+
root = tmp_path / "cluster"
|
|
189
|
+
root.mkdir()
|
|
190
|
+
(root / "harness.yaml").write_text(
|
|
191
|
+
f"name: test-cluster\nworkspace: {workspace.as_posix()}\nrepos:\n- name: alpha\n",
|
|
192
|
+
encoding="utf-8",
|
|
193
|
+
)
|
|
194
|
+
monkeypatch.setattr(clone_command, "git", lambda path, *args, **kwargs: _result())
|
|
195
|
+
|
|
196
|
+
result = CliRunner().invoke(cli, ["harness", "clone", "--harness", str(root)])
|
|
197
|
+
|
|
198
|
+
assert result.exit_code == 1
|
|
199
|
+
assert "failed (no origin declared in the manifest)" in result.output
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_clone_moves_onto_the_declared_branch_when_it_exists(tmp_path, monkeypatch):
|
|
203
|
+
workspace = tmp_path / "repos"
|
|
204
|
+
workspace.mkdir()
|
|
205
|
+
root = tmp_path / "cluster"
|
|
206
|
+
root.mkdir()
|
|
207
|
+
(root / "harness.yaml").write_text(
|
|
208
|
+
f"name: test-cluster\nworkspace: {workspace.as_posix()}\nrepos:\n"
|
|
209
|
+
"- name: beta\n origin: git@github.com:acme/beta.git\n branch: feat/new-intent-flow\n",
|
|
210
|
+
encoding="utf-8",
|
|
211
|
+
)
|
|
212
|
+
calls = []
|
|
213
|
+
|
|
214
|
+
def fake_git(path, *args, **kwargs):
|
|
215
|
+
calls.append(args)
|
|
216
|
+
if args[0] == "rev-parse":
|
|
217
|
+
return _result(stdout="main\n")
|
|
218
|
+
return _result()
|
|
219
|
+
|
|
220
|
+
monkeypatch.setattr(clone_command, "git", fake_git)
|
|
221
|
+
result = CliRunner().invoke(cli, ["harness", "clone", "--harness", str(root)])
|
|
222
|
+
|
|
223
|
+
assert result.exit_code == 0
|
|
224
|
+
assert ("checkout", "feat/new-intent-flow") in calls
|
|
225
|
+
assert "(on feat/new-intent-flow)" in result.output
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_clone_keeps_a_clone_whose_declared_branch_is_not_pushed_yet(tmp_path, monkeypatch):
|
|
229
|
+
workspace = tmp_path / "repos"
|
|
230
|
+
workspace.mkdir()
|
|
231
|
+
root = tmp_path / "cluster"
|
|
232
|
+
root.mkdir()
|
|
233
|
+
(root / "harness.yaml").write_text(
|
|
234
|
+
f"name: test-cluster\nworkspace: {workspace.as_posix()}\nrepos:\n"
|
|
235
|
+
"- name: beta\n origin: git@github.com:acme/beta.git\n branch: feat/unpushed\n",
|
|
236
|
+
encoding="utf-8",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def fake_git(path, *args, **kwargs):
|
|
240
|
+
if args[0] == "rev-parse":
|
|
241
|
+
return _result(stdout="main\n")
|
|
242
|
+
if args[0] == "checkout":
|
|
243
|
+
return _result(returncode=1, stderr="error: pathspec did not match\n")
|
|
244
|
+
return _result()
|
|
245
|
+
|
|
246
|
+
monkeypatch.setattr(clone_command, "git", fake_git)
|
|
247
|
+
result = CliRunner().invoke(cli, ["harness", "clone", "--harness", str(root)])
|
|
248
|
+
|
|
249
|
+
assert result.exit_code == 0
|
|
250
|
+
assert "cloned into" in result.output
|
|
251
|
+
assert "branch 'feat/unpushed' is not on origin, left on main" in result.output
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_clone_rejects_unknown_repo(tmp_path):
|
|
255
|
+
workspace = tmp_path / "repos"
|
|
256
|
+
workspace.mkdir()
|
|
257
|
+
root = tmp_path / "cluster"
|
|
258
|
+
_write_harness(root, workspace)
|
|
259
|
+
|
|
260
|
+
result = CliRunner().invoke(cli, ["harness", "clone", "--harness", str(root), "--repo", "nope"])
|
|
261
|
+
|
|
262
|
+
assert result.exit_code == 1
|
|
263
|
+
assert "Unknown repo name(s): nope" in result.output
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def test_normalize_remote_matches_ssh_and_https_forms():
|
|
267
|
+
assert clone_command.normalize_remote("git@github.com:acme/repo.git") == "github.com/acme/repo"
|
|
268
|
+
assert clone_command.normalize_remote("https://github.com/acme/repo.git") == "github.com/acme/repo"
|
|
269
|
+
assert clone_command.normalize_remote("ssh://git@github.com/acme/repo/") == "github.com/acme/repo"
|
|
270
|
+
assert clone_command.normalize_remote("https://github.com/acme/other") != "github.com/acme/repo"
|
evo_cli-0.18.0/evo_cli/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.18.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|