aicodinggym-cli 0.3.0__tar.gz → 0.5.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.
- {aicodinggym_cli-0.3.0 → aicodinggym_cli-0.5.0}/PKG-INFO +1 -1
- {aicodinggym_cli-0.3.0 → aicodinggym_cli-0.5.0}/aicodinggym_cli.egg-info/PKG-INFO +1 -1
- {aicodinggym_cli-0.3.0 → aicodinggym_cli-0.5.0}/cli.py +88 -2
- {aicodinggym_cli-0.3.0 → aicodinggym_cli-0.5.0}/config.py +19 -2
- {aicodinggym_cli-0.3.0 → aicodinggym_cli-0.5.0}/git_ops.py +111 -28
- {aicodinggym_cli-0.3.0 → aicodinggym_cli-0.5.0}/pyproject.toml +1 -1
- {aicodinggym_cli-0.3.0 → aicodinggym_cli-0.5.0}/README.md +0 -0
- {aicodinggym_cli-0.3.0 → aicodinggym_cli-0.5.0}/__init__.py +0 -0
- {aicodinggym_cli-0.3.0 → aicodinggym_cli-0.5.0}/aicodinggym_cli.egg-info/SOURCES.txt +0 -0
- {aicodinggym_cli-0.3.0 → aicodinggym_cli-0.5.0}/aicodinggym_cli.egg-info/dependency_links.txt +0 -0
- {aicodinggym_cli-0.3.0 → aicodinggym_cli-0.5.0}/aicodinggym_cli.egg-info/entry_points.txt +0 -0
- {aicodinggym_cli-0.3.0 → aicodinggym_cli-0.5.0}/aicodinggym_cli.egg-info/requires.txt +0 -0
- {aicodinggym_cli-0.3.0 → aicodinggym_cli-0.5.0}/aicodinggym_cli.egg-info/top_level.txt +0 -0
- {aicodinggym_cli-0.3.0 → aicodinggym_cli-0.5.0}/api.py +0 -0
- {aicodinggym_cli-0.3.0 → aicodinggym_cli-0.5.0}/setup.cfg +0 -0
|
@@ -22,11 +22,13 @@ CODE REVIEW WORKFLOW:
|
|
|
22
22
|
aicodinggym cr submit sentry-0001 -f review.md
|
|
23
23
|
"""
|
|
24
24
|
|
|
25
|
+
import json
|
|
25
26
|
import os
|
|
26
27
|
import platform
|
|
27
28
|
import re
|
|
28
29
|
import subprocess
|
|
29
30
|
import sys
|
|
31
|
+
import urllib.request
|
|
30
32
|
from datetime import datetime
|
|
31
33
|
from pathlib import Path
|
|
32
34
|
|
|
@@ -78,6 +80,81 @@ def _warn(msg: str) -> None:
|
|
|
78
80
|
click.echo(f"Warning: {msg}", err=True)
|
|
79
81
|
|
|
80
82
|
|
|
83
|
+
_GYM_ENV_API = "https://api.github.com/repos/AICodingGym/gym-environment/contents"
|
|
84
|
+
_GYM_ENV_SKIP = {"README.md"}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _install_gym_environment(dest: Path) -> None:
|
|
88
|
+
"""Download gym-environment files into dest and add them to .gitignore."""
|
|
89
|
+
try:
|
|
90
|
+
req = urllib.request.Request(_GYM_ENV_API, headers={"Accept": "application/vnd.github.v3+json"})
|
|
91
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
92
|
+
entries = json.loads(resp.read())
|
|
93
|
+
except Exception as e:
|
|
94
|
+
_warn(f"Could not fetch gym-environment file list: {e}")
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
downloaded: list[str] = []
|
|
98
|
+
|
|
99
|
+
for entry in entries:
|
|
100
|
+
name = entry.get("name", "")
|
|
101
|
+
if name in _GYM_ENV_SKIP:
|
|
102
|
+
continue
|
|
103
|
+
etype = entry.get("type")
|
|
104
|
+
|
|
105
|
+
if etype == "file":
|
|
106
|
+
url = entry.get("download_url")
|
|
107
|
+
if not url:
|
|
108
|
+
continue
|
|
109
|
+
try:
|
|
110
|
+
with urllib.request.urlopen(url, timeout=15) as r:
|
|
111
|
+
(dest / name).write_bytes(r.read())
|
|
112
|
+
downloaded.append(name)
|
|
113
|
+
except Exception as e:
|
|
114
|
+
_warn(f"Failed to download {name}: {e}")
|
|
115
|
+
|
|
116
|
+
elif etype == "dir":
|
|
117
|
+
# Fetch subdirectory contents recursively (one level deep)
|
|
118
|
+
try:
|
|
119
|
+
sub_req = urllib.request.Request(
|
|
120
|
+
f"{_GYM_ENV_API}/{name}",
|
|
121
|
+
headers={"Accept": "application/vnd.github.v3+json"},
|
|
122
|
+
)
|
|
123
|
+
with urllib.request.urlopen(sub_req, timeout=15) as r:
|
|
124
|
+
sub_entries = json.loads(r.read())
|
|
125
|
+
except Exception as e:
|
|
126
|
+
_warn(f"Failed to list directory {name}: {e}")
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
sub_dir = dest / name
|
|
130
|
+
sub_dir.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
for sub in sub_entries:
|
|
132
|
+
sub_name = sub.get("name", "")
|
|
133
|
+
sub_url = sub.get("download_url")
|
|
134
|
+
if sub.get("type") != "file" or not sub_url:
|
|
135
|
+
continue
|
|
136
|
+
try:
|
|
137
|
+
with urllib.request.urlopen(sub_url, timeout=15) as r:
|
|
138
|
+
(sub_dir / sub_name).write_bytes(r.read())
|
|
139
|
+
except Exception as e:
|
|
140
|
+
_warn(f"Failed to download {name}/{sub_name}: {e}")
|
|
141
|
+
downloaded.append(name)
|
|
142
|
+
|
|
143
|
+
if not downloaded:
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
# Append to .gitignore
|
|
147
|
+
gitignore = dest / ".gitignore"
|
|
148
|
+
existing = gitignore.read_text() if gitignore.exists() else ""
|
|
149
|
+
existing_lines = set(existing.splitlines())
|
|
150
|
+
new_entries = [f for f in downloaded if f not in existing_lines and f"/{f}" not in existing_lines]
|
|
151
|
+
if new_entries:
|
|
152
|
+
block = "\n# gym-environment\n" + "\n".join(new_entries) + "\n"
|
|
153
|
+
with open(gitignore, "a") as fh:
|
|
154
|
+
fh.write(block)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
81
158
|
def _resolve_user_id(config: dict, user_id: str | None) -> str:
|
|
82
159
|
"""Resolve user_id from argument or config, with helpful error."""
|
|
83
160
|
if user_id:
|
|
@@ -334,6 +411,8 @@ def configure(user_id: str, workspace_dir: str | None):
|
|
|
334
411
|
}
|
|
335
412
|
save_config(config)
|
|
336
413
|
|
|
414
|
+
_install_gym_environment(Path(resolved_workspace))
|
|
415
|
+
|
|
337
416
|
click.echo(
|
|
338
417
|
f"\nConfiguration saved successfully!\n"
|
|
339
418
|
f"\n"
|
|
@@ -449,6 +528,8 @@ def swe_fetch(problem_id: str, user_id: str | None, workspace_dir: str | None):
|
|
|
449
528
|
if not success:
|
|
450
529
|
_error(msg)
|
|
451
530
|
|
|
531
|
+
_install_gym_environment(workspace / problem_id)
|
|
532
|
+
|
|
452
533
|
click.echo(
|
|
453
534
|
f"\nSuccessfully fetched problem: {problem_id}\n"
|
|
454
535
|
f"\n"
|
|
@@ -940,11 +1021,13 @@ def cr_fetch(problem_id: str, user_id: str | None, workspace_dir: str | None):
|
|
|
940
1021
|
if not success:
|
|
941
1022
|
_error(msg)
|
|
942
1023
|
|
|
1024
|
+
_install_gym_environment(workspace / problem_id)
|
|
1025
|
+
|
|
943
1026
|
problem_dir = workspace / problem_id
|
|
944
1027
|
|
|
945
1028
|
# Generate diff.patch
|
|
946
1029
|
diff_result = run_git_command(
|
|
947
|
-
|
|
1030
|
+
["git", "diff", f"{base_branch}..{head_branch}"], str(problem_dir)
|
|
948
1031
|
)
|
|
949
1032
|
diff_path = problem_dir / "diff.patch"
|
|
950
1033
|
diff_path.write_text(diff_result.stdout)
|
|
@@ -964,6 +1047,7 @@ def cr_fetch(problem_id: str, user_id: str | None, workspace_dir: str | None):
|
|
|
964
1047
|
"<!-- Approve / Request Changes / Comment -->\n"
|
|
965
1048
|
)
|
|
966
1049
|
|
|
1050
|
+
cat_cmd = "type" if sys.platform == "win32" else "cat"
|
|
967
1051
|
click.echo(
|
|
968
1052
|
f"\nSuccessfully fetched: {problem_id}\n"
|
|
969
1053
|
f"\n"
|
|
@@ -971,7 +1055,7 @@ def cr_fetch(problem_id: str, user_id: str | None, workspace_dir: str | None):
|
|
|
971
1055
|
f" Review template: {review_path}\n"
|
|
972
1056
|
f"\n"
|
|
973
1057
|
f"Next steps:\n"
|
|
974
|
-
f" 1. Review the diff:
|
|
1058
|
+
f" 1. Review the diff: {cat_cmd} {diff_path}\n"
|
|
975
1059
|
f" 2. Write your review in {review_path}\n"
|
|
976
1060
|
f" 3. Submit: aicodinggym cr submit {problem_id} -f review.md\n"
|
|
977
1061
|
)
|
|
@@ -1098,6 +1182,8 @@ def mle_download(competition_id: str, user_id: str | None, workspace_dir: str |
|
|
|
1098
1182
|
except APIError as e:
|
|
1099
1183
|
_error(str(e))
|
|
1100
1184
|
|
|
1185
|
+
_install_gym_environment(workspace / competition_id)
|
|
1186
|
+
|
|
1101
1187
|
click.echo(
|
|
1102
1188
|
f"\nDataset downloaded to: {dest_path}\n"
|
|
1103
1189
|
f"\nNext step: train your model and submit predictions with:\n"
|
|
@@ -6,6 +6,9 @@ SSH keys are stored in ~/.aicodinggym/{user_id}_id_rsa.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import json
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
9
12
|
from pathlib import Path
|
|
10
13
|
from typing import Any
|
|
11
14
|
|
|
@@ -19,8 +22,22 @@ _CONFIG_FIELDS = ("user_id", "repo_name", "private_key_path", "workspace_dir")
|
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
def ensure_config_dir() -> Path:
|
|
22
|
-
"""Create the config directory with secure permissions if it doesn't exist.
|
|
23
|
-
|
|
25
|
+
"""Create the config directory with secure permissions if it doesn't exist.
|
|
26
|
+
|
|
27
|
+
On Unix/macOS: mode 0o700 (owner-only access).
|
|
28
|
+
On Windows: removes inherited ACLs and grants full control only to the
|
|
29
|
+
current user via icacls.
|
|
30
|
+
"""
|
|
31
|
+
created = not CONFIG_DIR.exists()
|
|
32
|
+
CONFIG_DIR.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
33
|
+
if created and sys.platform == "win32":
|
|
34
|
+
username = os.environ.get("USERNAME", "")
|
|
35
|
+
if username:
|
|
36
|
+
subprocess.run(
|
|
37
|
+
["icacls", str(CONFIG_DIR), "/inheritance:r",
|
|
38
|
+
"/grant:r", f"{username}:(OI)(CI)(F)"],
|
|
39
|
+
capture_output=True,
|
|
40
|
+
)
|
|
24
41
|
return CONFIG_DIR
|
|
25
42
|
|
|
26
43
|
|
|
@@ -4,18 +4,69 @@ import os
|
|
|
4
4
|
import re
|
|
5
5
|
import shutil
|
|
6
6
|
import subprocess
|
|
7
|
+
import sys
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import Optional
|
|
9
10
|
|
|
10
11
|
from .config import ensure_config_dir
|
|
11
12
|
|
|
12
13
|
|
|
14
|
+
def _find_git_ssh() -> str | None:
|
|
15
|
+
"""On Windows, find Git for Windows' bundled ssh.exe.
|
|
16
|
+
|
|
17
|
+
Windows may have two SSH binaries on PATH: the built-in OpenSSH
|
|
18
|
+
(C:\\Windows\\System32\\OpenSSH\\ssh.exe) and Git for Windows' MSYS2
|
|
19
|
+
ssh (C:\\Program Files\\Git\\usr\\bin\\ssh.exe). System32 is usually
|
|
20
|
+
first on PATH, so an unqualified 'ssh' resolves to Windows OpenSSH,
|
|
21
|
+
which can trigger GUI credential dialogs or deadlock when stdout is
|
|
22
|
+
captured. This function returns the full path to Git's bundled ssh
|
|
23
|
+
so we can reference it explicitly in GIT_SSH_COMMAND.
|
|
24
|
+
"""
|
|
25
|
+
if sys.platform != "win32":
|
|
26
|
+
return None
|
|
27
|
+
git_path = shutil.which("git")
|
|
28
|
+
if not git_path:
|
|
29
|
+
return None
|
|
30
|
+
# Walk up from git.exe to find the Git root containing usr/bin/ssh.exe.
|
|
31
|
+
# Handles cmd/, bin/, and mingw64/bin/ layouts.
|
|
32
|
+
candidate = Path(git_path).resolve().parent
|
|
33
|
+
for _ in range(4):
|
|
34
|
+
ssh = candidate / "usr" / "bin" / "ssh.exe"
|
|
35
|
+
if ssh.exists():
|
|
36
|
+
return str(ssh).replace("\\", "/")
|
|
37
|
+
candidate = candidate.parent
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
13
41
|
def _validate_git_ref(name: str, label: str) -> None:
|
|
14
42
|
"""Raise ValueError if name contains suspicious shell metacharacters."""
|
|
15
43
|
if re.search(r'[;&|`$(){}]', name):
|
|
16
44
|
raise ValueError(f"Invalid {label}: {name!r}")
|
|
17
45
|
|
|
18
46
|
|
|
47
|
+
def _restrict_key_permissions(key_path: Path) -> None:
|
|
48
|
+
"""Restrict an SSH private key file to owner-only access.
|
|
49
|
+
|
|
50
|
+
On Unix/macOS: chmod 600 (read/write owner only).
|
|
51
|
+
On Windows: uses icacls to remove inherited permissions and grant
|
|
52
|
+
full control only to the current user. SSH clients on both platforms
|
|
53
|
+
refuse to use a key whose permissions are too open.
|
|
54
|
+
"""
|
|
55
|
+
if sys.platform == "win32":
|
|
56
|
+
# Remove inherited ACLs, then grant only the current user full control.
|
|
57
|
+
# (F) = Full control, matching chmod 0o600 (owner read+write).
|
|
58
|
+
key_str = str(key_path)
|
|
59
|
+
username = os.environ.get("USERNAME", "")
|
|
60
|
+
if username:
|
|
61
|
+
subprocess.run(
|
|
62
|
+
["icacls", key_str, "/inheritance:r",
|
|
63
|
+
"/grant:r", f"{username}:(F)"],
|
|
64
|
+
capture_output=True,
|
|
65
|
+
)
|
|
66
|
+
else:
|
|
67
|
+
key_path.chmod(0o600)
|
|
68
|
+
|
|
69
|
+
|
|
19
70
|
def generate_ssh_key_pair(user_id: str) -> tuple[Path, str]:
|
|
20
71
|
"""Generate an SSH key pair for the user.
|
|
21
72
|
|
|
@@ -36,8 +87,14 @@ def generate_ssh_key_pair(user_id: str) -> tuple[Path, str]:
|
|
|
36
87
|
if mcp_private.exists() and mcp_public.exists():
|
|
37
88
|
shutil.copy2(mcp_private, key_path)
|
|
38
89
|
shutil.copy2(mcp_public, Path(f"{key_path}.pub"))
|
|
39
|
-
key_path
|
|
90
|
+
_restrict_key_permissions(key_path)
|
|
40
91
|
else:
|
|
92
|
+
if not shutil.which("ssh-keygen"):
|
|
93
|
+
raise RuntimeError(
|
|
94
|
+
"ssh-keygen is not installed or not on PATH.\n"
|
|
95
|
+
"On Windows, install Git for Windows (https://git-scm.com) "
|
|
96
|
+
"which includes ssh-keygen, or use the OpenSSH optional feature."
|
|
97
|
+
)
|
|
41
98
|
result = subprocess.run(
|
|
42
99
|
["ssh-keygen", "-t", "rsa", "-b", "4096", "-f", str(key_path),
|
|
43
100
|
"-N", "", "-C", f"aicodinggym-{user_id}"],
|
|
@@ -51,13 +108,34 @@ def generate_ssh_key_pair(user_id: str) -> tuple[Path, str]:
|
|
|
51
108
|
return key_path, public_key
|
|
52
109
|
|
|
53
110
|
|
|
54
|
-
def run_git_command(cmd: str, cwd: str, key_path: Optional[Path] = None) -> subprocess.CompletedProcess:
|
|
55
|
-
"""Execute a git command with optional SSH key configuration.
|
|
111
|
+
def run_git_command(cmd: list[str], cwd: str, key_path: Optional[Path] = None) -> subprocess.CompletedProcess:
|
|
112
|
+
"""Execute a git command with optional SSH key configuration.
|
|
113
|
+
|
|
114
|
+
cmd must be a list of arguments (e.g. ["git", "status"]).
|
|
115
|
+
"""
|
|
56
116
|
env = os.environ.copy()
|
|
57
117
|
if key_path:
|
|
58
|
-
|
|
118
|
+
# Quote the key path in case it contains spaces (common on Windows).
|
|
119
|
+
# Use forward slashes — works on all platforms and avoids backslash escaping.
|
|
120
|
+
quoted_key = str(key_path).replace("\\", "/")
|
|
121
|
+
# On Windows, use Git for Windows' bundled ssh to avoid Windows native
|
|
122
|
+
# OpenSSH which can trigger GUI credential dialogs or deadlock when
|
|
123
|
+
# stdout is captured. Falls back to bare "ssh" if not found.
|
|
124
|
+
ssh_bin = _find_git_ssh() or "ssh"
|
|
125
|
+
# Always use /dev/null for UserKnownHostsFile. On macOS/Linux this is
|
|
126
|
+
# the native null device. On Windows, Git for Windows bundles MSYS2's
|
|
127
|
+
# ssh which translates /dev/null correctly. Using os.devnull ("nul")
|
|
128
|
+
# would break MSYS2's ssh which treats "nul" as a literal filename.
|
|
129
|
+
# BatchMode=yes prevents any interactive prompts (password, passphrase)
|
|
130
|
+
# that would cause a hang when stdout/stderr are captured.
|
|
131
|
+
env["GIT_SSH_COMMAND"] = (
|
|
132
|
+
f'"{ssh_bin}" -i "{quoted_key}" '
|
|
133
|
+
f"-o StrictHostKeyChecking=no "
|
|
134
|
+
f"-o UserKnownHostsFile=/dev/null "
|
|
135
|
+
f"-o BatchMode=yes"
|
|
136
|
+
)
|
|
59
137
|
|
|
60
|
-
return subprocess.run(cmd,
|
|
138
|
+
return subprocess.run(cmd, cwd=cwd, capture_output=True, text=True, env=env)
|
|
61
139
|
|
|
62
140
|
|
|
63
141
|
def clone_repo(repo_url: str, branch: str, dest_name: str,
|
|
@@ -70,9 +148,9 @@ def clone_repo(repo_url: str, branch: str, dest_name: str,
|
|
|
70
148
|
|
|
71
149
|
if problem_dir.exists():
|
|
72
150
|
# Check if already on the correct branch
|
|
73
|
-
result = run_git_command("git rev-parse --abbrev-ref HEAD", str(problem_dir))
|
|
151
|
+
result = run_git_command(["git", "rev-parse", "--abbrev-ref", "HEAD"], str(problem_dir))
|
|
74
152
|
if result.returncode == 0 and result.stdout.strip() == branch:
|
|
75
|
-
pull = run_git_command(
|
|
153
|
+
pull = run_git_command(["git", "pull", "origin", branch], str(problem_dir), key_path)
|
|
76
154
|
if pull.returncode != 0:
|
|
77
155
|
return False, f"Git pull failed:\n{pull.stderr}"
|
|
78
156
|
return True, f"Already exists. Updated to latest version.\nRepository: {problem_dir}\nBranch: {branch}"
|
|
@@ -81,7 +159,7 @@ def clone_repo(repo_url: str, branch: str, dest_name: str,
|
|
|
81
159
|
"Remove it first or use --workspace-dir to specify a different location."
|
|
82
160
|
)
|
|
83
161
|
|
|
84
|
-
cmd =
|
|
162
|
+
cmd = ["git", "clone", "--single-branch", "--branch", branch, "--depth", "1", repo_url, dest_name]
|
|
85
163
|
result = run_git_command(cmd, workspace, key_path)
|
|
86
164
|
|
|
87
165
|
if result.returncode != 0:
|
|
@@ -108,13 +186,13 @@ def clone_repo_cr(repo_url: str, base_branch: str, head_branch: str,
|
|
|
108
186
|
if problem_dir.exists():
|
|
109
187
|
# Already cloned — fetch latest for both branches
|
|
110
188
|
for branch in (base_branch, head_branch):
|
|
111
|
-
result = run_git_command(
|
|
189
|
+
result = run_git_command(["git", "fetch", "origin", branch], str(problem_dir), key_path)
|
|
112
190
|
if result.returncode != 0:
|
|
113
191
|
return False, f"Git fetch failed for {branch}:\n{result.stderr}"
|
|
114
|
-
result = run_git_command(
|
|
192
|
+
result = run_git_command(["git", "branch", "-f", branch, "FETCH_HEAD"], str(problem_dir))
|
|
115
193
|
if result.returncode != 0:
|
|
116
194
|
return False, f"Failed to update branch {branch}:\n{result.stderr}"
|
|
117
|
-
result = run_git_command(
|
|
195
|
+
result = run_git_command(["git", "checkout", head_branch], str(problem_dir))
|
|
118
196
|
if result.returncode != 0:
|
|
119
197
|
return False, f"Failed to checkout head branch '{head_branch}':\n{result.stderr}"
|
|
120
198
|
return True, (
|
|
@@ -124,24 +202,23 @@ def clone_repo_cr(repo_url: str, base_branch: str, head_branch: str,
|
|
|
124
202
|
)
|
|
125
203
|
|
|
126
204
|
# Clone base branch (shallow); depth 50 needed for diffing between branches
|
|
127
|
-
cmd =
|
|
205
|
+
cmd = ["git", "clone", "--single-branch", "--branch", base_branch, "--depth", "50", repo_url, dest_name]
|
|
128
206
|
result = run_git_command(cmd, workspace, key_path)
|
|
129
207
|
if result.returncode != 0:
|
|
130
208
|
return False, f"Git clone failed:\n{result.stderr}"
|
|
131
209
|
|
|
132
210
|
# Fetch head branch
|
|
133
|
-
|
|
134
|
-
result = run_git_command(fetch_cmd, str(problem_dir), key_path)
|
|
211
|
+
result = run_git_command(["git", "fetch", "origin", head_branch], str(problem_dir), key_path)
|
|
135
212
|
if result.returncode != 0:
|
|
136
213
|
return False, f"Failed to fetch head branch '{head_branch}':\n{result.stderr}"
|
|
137
214
|
|
|
138
215
|
# Create local head branch tracking the fetched ref
|
|
139
|
-
result = run_git_command(
|
|
216
|
+
result = run_git_command(["git", "branch", "-f", head_branch, "FETCH_HEAD"], str(problem_dir))
|
|
140
217
|
if result.returncode != 0:
|
|
141
218
|
return False, f"Failed to create branch {head_branch}:\n{result.stderr}"
|
|
142
219
|
|
|
143
220
|
# Check out head branch so the user starts on the code being reviewed
|
|
144
|
-
result = run_git_command(
|
|
221
|
+
result = run_git_command(["git", "checkout", head_branch], str(problem_dir))
|
|
145
222
|
if result.returncode != 0:
|
|
146
223
|
return False, f"Failed to checkout head branch '{head_branch}':\n{result.stderr}"
|
|
147
224
|
|
|
@@ -159,29 +236,35 @@ def add_commit_push(problem_dir: str, branch: str, key_path: Path,
|
|
|
159
236
|
"""
|
|
160
237
|
pdir = Path(problem_dir)
|
|
161
238
|
|
|
162
|
-
# Stage all changes except
|
|
163
|
-
result = run_git_command(
|
|
239
|
+
# Stage all changes except dotfiles/dotdirs and markdown files
|
|
240
|
+
result = run_git_command([
|
|
241
|
+
"git", "add", "-A", "--", ".",
|
|
242
|
+
":(exclude).*",
|
|
243
|
+
":(exclude)*.md",
|
|
244
|
+
], str(pdir))
|
|
164
245
|
if result.returncode != 0:
|
|
165
246
|
return False, f"Git add failed:\n{result.stderr}", ""
|
|
166
247
|
|
|
167
248
|
# Check for staged changes
|
|
168
|
-
status = run_git_command("git diff --cached --name-only", str(pdir))
|
|
249
|
+
status = run_git_command(["git", "diff", "--cached", "--name-only"], str(pdir))
|
|
169
250
|
if not status.stdout.strip():
|
|
170
251
|
return False, "No changes to commit. Your working directory is clean.", ""
|
|
171
252
|
|
|
172
|
-
# Commit
|
|
173
|
-
|
|
174
|
-
result = run_git_command(f'git commit -m "{safe_msg}"', str(pdir))
|
|
253
|
+
# Commit — pass message directly as a list arg; no shell escaping needed
|
|
254
|
+
result = run_git_command(["git", "commit", "-m", message], str(pdir))
|
|
175
255
|
if result.returncode != 0:
|
|
176
256
|
return False, f"Git commit failed:\n{result.stderr}", ""
|
|
177
257
|
|
|
178
258
|
# Get commit hash
|
|
179
|
-
hash_result = run_git_command("git rev-parse HEAD", str(pdir))
|
|
259
|
+
hash_result = run_git_command(["git", "rev-parse", "HEAD"], str(pdir))
|
|
180
260
|
commit_hash = hash_result.stdout.strip()
|
|
181
261
|
|
|
182
262
|
# Push
|
|
183
|
-
|
|
184
|
-
|
|
263
|
+
push_cmd = ["git", "push"]
|
|
264
|
+
if force:
|
|
265
|
+
push_cmd.append("--force-with-lease")
|
|
266
|
+
push_cmd += ["origin", branch]
|
|
267
|
+
result = run_git_command(push_cmd, str(pdir), key_path)
|
|
185
268
|
if result.returncode != 0:
|
|
186
269
|
return False, f"Git push failed:\n{result.stderr}", commit_hash
|
|
187
270
|
|
|
@@ -193,7 +276,7 @@ def reset_to_setup_commit(problem_dir: str) -> tuple[bool, str]:
|
|
|
193
276
|
|
|
194
277
|
Returns (success, message).
|
|
195
278
|
"""
|
|
196
|
-
log_result = run_git_command("git log --format=%H:%s --reverse", problem_dir)
|
|
279
|
+
log_result = run_git_command(["git", "log", "--format=%H:%s", "--reverse"], problem_dir)
|
|
197
280
|
if log_result.returncode != 0:
|
|
198
281
|
return False, f"Git log failed:\n{log_result.stderr}"
|
|
199
282
|
|
|
@@ -214,11 +297,11 @@ def reset_to_setup_commit(problem_dir: str) -> tuple[bool, str]:
|
|
|
214
297
|
f"Expected a commit message starting with '{setup_prefix}'."
|
|
215
298
|
)
|
|
216
299
|
|
|
217
|
-
reset = run_git_command(
|
|
300
|
+
reset = run_git_command(["git", "reset", "--hard", setup_commit], problem_dir)
|
|
218
301
|
if reset.returncode != 0:
|
|
219
302
|
return False, f"Git reset failed:\n{reset.stderr}"
|
|
220
303
|
|
|
221
|
-
clean = run_git_command("git clean -fd", problem_dir)
|
|
304
|
+
clean = run_git_command(["git", "clean", "-fd"], problem_dir)
|
|
222
305
|
if clean.returncode != 0:
|
|
223
306
|
return False, f"Git clean failed:\n{clean.stderr}"
|
|
224
307
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aicodinggym_cli-0.3.0 → aicodinggym_cli-0.5.0}/aicodinggym_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|