genarena 0.0.1__py3-none-any.whl → 0.1.1__py3-none-any.whl
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.
- genarena/__init__.py +49 -2
- genarena/__main__.py +10 -0
- genarena/arena.py +1685 -0
- genarena/battle.py +337 -0
- genarena/bt_elo.py +507 -0
- genarena/cli.py +1581 -0
- genarena/data.py +476 -0
- genarena/deploy/Dockerfile +22 -0
- genarena/deploy/README.md +55 -0
- genarena/deploy/__init__.py +5 -0
- genarena/deploy/app.py +84 -0
- genarena/experiments.py +121 -0
- genarena/leaderboard.py +270 -0
- genarena/logs.py +409 -0
- genarena/models.py +412 -0
- genarena/prompts/__init__.py +127 -0
- genarena/prompts/mmrb2.py +373 -0
- genarena/sampling.py +336 -0
- genarena/state.py +656 -0
- genarena/sync/__init__.py +105 -0
- genarena/sync/auto_commit.py +118 -0
- genarena/sync/deploy_ops.py +543 -0
- genarena/sync/git_ops.py +422 -0
- genarena/sync/hf_ops.py +891 -0
- genarena/sync/init_ops.py +431 -0
- genarena/sync/packer.py +587 -0
- genarena/sync/submit.py +837 -0
- genarena/utils.py +103 -0
- genarena/validation/__init__.py +19 -0
- genarena/validation/schema.py +327 -0
- genarena/validation/validator.py +329 -0
- genarena/visualize/README.md +148 -0
- genarena/visualize/__init__.py +14 -0
- genarena/visualize/app.py +938 -0
- genarena/visualize/data_loader.py +2430 -0
- genarena/visualize/static/app.js +3762 -0
- genarena/visualize/static/model_aliases.json +86 -0
- genarena/visualize/static/style.css +4104 -0
- genarena/visualize/templates/index.html +413 -0
- genarena/vlm.py +519 -0
- genarena-0.1.1.dist-info/METADATA +178 -0
- genarena-0.1.1.dist-info/RECORD +44 -0
- {genarena-0.0.1.dist-info → genarena-0.1.1.dist-info}/WHEEL +1 -2
- genarena-0.1.1.dist-info/entry_points.txt +2 -0
- genarena-0.0.1.dist-info/METADATA +0 -26
- genarena-0.0.1.dist-info/RECORD +0 -5
- genarena-0.0.1.dist-info/top_level.txt +0 -1
genarena/sync/git_ops.py
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
# Copyright 2026 Ruihang Li.
|
|
2
|
+
# Licensed under the Apache License, Version 2.0.
|
|
3
|
+
# See LICENSE file in the project root for details.
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
Git operations module for GenArena.
|
|
7
|
+
|
|
8
|
+
This module provides Git version control functionality for arena data,
|
|
9
|
+
including initialization, commit, remote configuration, and push operations.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import subprocess
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
# Patterns to exclude from Git tracking (models directories contain large image files)
|
|
22
|
+
GITIGNORE_PATTERNS = [
|
|
23
|
+
"# GenArena: Exclude model output images (large files)",
|
|
24
|
+
"*/models/",
|
|
25
|
+
"",
|
|
26
|
+
"# Python cache",
|
|
27
|
+
"__pycache__/",
|
|
28
|
+
"*.pyc",
|
|
29
|
+
"",
|
|
30
|
+
"# OS files",
|
|
31
|
+
".DS_Store",
|
|
32
|
+
"Thumbs.db",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _run_git_command(
|
|
37
|
+
arena_dir: str,
|
|
38
|
+
args: list,
|
|
39
|
+
check: bool = True,
|
|
40
|
+
capture_output: bool = True,
|
|
41
|
+
) -> subprocess.CompletedProcess:
|
|
42
|
+
"""
|
|
43
|
+
Run a git command in the arena directory.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
arena_dir: Path to the arena directory
|
|
47
|
+
args: Git command arguments (without 'git' prefix)
|
|
48
|
+
check: If True, raise exception on non-zero exit code
|
|
49
|
+
capture_output: If True, capture stdout and stderr
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
CompletedProcess instance
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
subprocess.CalledProcessError: If check=True and command fails
|
|
56
|
+
"""
|
|
57
|
+
cmd = ["git"] + args
|
|
58
|
+
return subprocess.run(
|
|
59
|
+
cmd,
|
|
60
|
+
cwd=arena_dir,
|
|
61
|
+
check=check,
|
|
62
|
+
capture_output=capture_output,
|
|
63
|
+
text=True,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def is_git_initialized(arena_dir: str) -> bool:
|
|
68
|
+
"""
|
|
69
|
+
Check if the arena directory is a Git repository.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
arena_dir: Path to the arena directory
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
True if Git is initialized, False otherwise
|
|
76
|
+
"""
|
|
77
|
+
git_dir = os.path.join(arena_dir, ".git")
|
|
78
|
+
return os.path.isdir(git_dir)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def git_init(arena_dir: str) -> tuple[bool, str]:
|
|
82
|
+
"""
|
|
83
|
+
Initialize a Git repository in the arena directory.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
arena_dir: Path to the arena directory
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
Tuple of (success, message)
|
|
90
|
+
"""
|
|
91
|
+
if is_git_initialized(arena_dir):
|
|
92
|
+
return True, "Git repository already initialized"
|
|
93
|
+
|
|
94
|
+
# Ensure directory exists
|
|
95
|
+
os.makedirs(arena_dir, exist_ok=True)
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
result = _run_git_command(arena_dir, ["init"])
|
|
99
|
+
logger.info(f"Initialized Git repository in {arena_dir}")
|
|
100
|
+
|
|
101
|
+
# Ensure .gitignore is set up
|
|
102
|
+
ensure_gitignore(arena_dir)
|
|
103
|
+
|
|
104
|
+
return True, "Git repository initialized successfully"
|
|
105
|
+
except subprocess.CalledProcessError as e:
|
|
106
|
+
error_msg = f"Failed to initialize Git repository: {e.stderr}"
|
|
107
|
+
logger.error(error_msg)
|
|
108
|
+
return False, error_msg
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def ensure_gitignore(arena_dir: str) -> tuple[bool, str]:
|
|
112
|
+
"""
|
|
113
|
+
Create or update .gitignore file to exclude models directories.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
arena_dir: Path to the arena directory
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Tuple of (success, message)
|
|
120
|
+
"""
|
|
121
|
+
gitignore_path = os.path.join(arena_dir, ".gitignore")
|
|
122
|
+
|
|
123
|
+
existing_content = ""
|
|
124
|
+
if os.path.isfile(gitignore_path):
|
|
125
|
+
with open(gitignore_path, "r", encoding="utf-8") as f:
|
|
126
|
+
existing_content = f.read()
|
|
127
|
+
|
|
128
|
+
# Check if the key pattern already exists
|
|
129
|
+
key_pattern = "*/models/"
|
|
130
|
+
if key_pattern in existing_content:
|
|
131
|
+
return True, ".gitignore already contains required patterns"
|
|
132
|
+
|
|
133
|
+
# Append patterns to existing content
|
|
134
|
+
new_content = existing_content
|
|
135
|
+
if new_content and not new_content.endswith("\n"):
|
|
136
|
+
new_content += "\n"
|
|
137
|
+
|
|
138
|
+
if new_content:
|
|
139
|
+
new_content += "\n"
|
|
140
|
+
|
|
141
|
+
new_content += "\n".join(GITIGNORE_PATTERNS)
|
|
142
|
+
|
|
143
|
+
with open(gitignore_path, "w", encoding="utf-8") as f:
|
|
144
|
+
f.write(new_content)
|
|
145
|
+
|
|
146
|
+
logger.info(f"Updated .gitignore in {arena_dir}")
|
|
147
|
+
return True, ".gitignore updated successfully"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def git_add_all(arena_dir: str) -> tuple[bool, str]:
|
|
151
|
+
"""
|
|
152
|
+
Stage all changes in the arena directory (respecting .gitignore).
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
arena_dir: Path to the arena directory
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Tuple of (success, message)
|
|
159
|
+
"""
|
|
160
|
+
if not is_git_initialized(arena_dir):
|
|
161
|
+
return False, "Git repository not initialized"
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
_run_git_command(arena_dir, ["add", "-A"])
|
|
165
|
+
return True, "All changes staged"
|
|
166
|
+
except subprocess.CalledProcessError as e:
|
|
167
|
+
error_msg = f"Failed to stage changes: {e.stderr}"
|
|
168
|
+
logger.error(error_msg)
|
|
169
|
+
return False, error_msg
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def has_uncommitted_changes(arena_dir: str) -> bool:
|
|
173
|
+
"""
|
|
174
|
+
Check if there are uncommitted changes in the repository.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
arena_dir: Path to the arena directory
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
True if there are uncommitted changes, False otherwise
|
|
181
|
+
"""
|
|
182
|
+
if not is_git_initialized(arena_dir):
|
|
183
|
+
return False
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
# Check for staged changes
|
|
187
|
+
result = _run_git_command(arena_dir, ["diff", "--cached", "--quiet"], check=False)
|
|
188
|
+
if result.returncode != 0:
|
|
189
|
+
return True
|
|
190
|
+
|
|
191
|
+
# Check for unstaged changes
|
|
192
|
+
result = _run_git_command(arena_dir, ["diff", "--quiet"], check=False)
|
|
193
|
+
if result.returncode != 0:
|
|
194
|
+
return True
|
|
195
|
+
|
|
196
|
+
# Check for untracked files (that aren't ignored)
|
|
197
|
+
result = _run_git_command(
|
|
198
|
+
arena_dir,
|
|
199
|
+
["ls-files", "--others", "--exclude-standard"],
|
|
200
|
+
check=False
|
|
201
|
+
)
|
|
202
|
+
if result.stdout.strip():
|
|
203
|
+
return True
|
|
204
|
+
|
|
205
|
+
return False
|
|
206
|
+
except Exception as e:
|
|
207
|
+
logger.warning(f"Error checking for uncommitted changes: {e}")
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def git_commit(
|
|
212
|
+
arena_dir: str,
|
|
213
|
+
message: Optional[str] = None,
|
|
214
|
+
command_name: Optional[str] = None,
|
|
215
|
+
) -> tuple[bool, str]:
|
|
216
|
+
"""
|
|
217
|
+
Commit staged changes.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
arena_dir: Path to the arena directory
|
|
221
|
+
message: Custom commit message (optional)
|
|
222
|
+
command_name: Name of the command that triggered this commit (for auto-commit)
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Tuple of (success, message)
|
|
226
|
+
"""
|
|
227
|
+
if not is_git_initialized(arena_dir):
|
|
228
|
+
return False, "Git repository not initialized"
|
|
229
|
+
|
|
230
|
+
# Stage all changes first
|
|
231
|
+
success, msg = git_add_all(arena_dir)
|
|
232
|
+
if not success:
|
|
233
|
+
return False, msg
|
|
234
|
+
|
|
235
|
+
# Check if there's anything to commit
|
|
236
|
+
result = _run_git_command(arena_dir, ["diff", "--cached", "--quiet"], check=False)
|
|
237
|
+
if result.returncode == 0:
|
|
238
|
+
return True, "Nothing to commit, working tree clean"
|
|
239
|
+
|
|
240
|
+
# Generate commit message
|
|
241
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
242
|
+
if message:
|
|
243
|
+
commit_msg = message
|
|
244
|
+
elif command_name:
|
|
245
|
+
commit_msg = f"[genarena] Auto commit after {command_name} at {timestamp}"
|
|
246
|
+
else:
|
|
247
|
+
commit_msg = f"[genarena] Auto commit at {timestamp}"
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
_run_git_command(arena_dir, ["commit", "-m", commit_msg])
|
|
251
|
+
logger.info(f"Committed changes: {commit_msg}")
|
|
252
|
+
return True, f"Committed: {commit_msg}"
|
|
253
|
+
except subprocess.CalledProcessError as e:
|
|
254
|
+
error_msg = f"Failed to commit: {e.stderr}"
|
|
255
|
+
logger.error(error_msg)
|
|
256
|
+
return False, error_msg
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def git_remote_get_url(arena_dir: str, remote_name: str = "origin") -> Optional[str]:
|
|
260
|
+
"""
|
|
261
|
+
Get the URL of a remote repository.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
arena_dir: Path to the arena directory
|
|
265
|
+
remote_name: Name of the remote (default: origin)
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Remote URL or None if not configured
|
|
269
|
+
"""
|
|
270
|
+
if not is_git_initialized(arena_dir):
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
result = _run_git_command(
|
|
275
|
+
arena_dir,
|
|
276
|
+
["remote", "get-url", remote_name],
|
|
277
|
+
check=False
|
|
278
|
+
)
|
|
279
|
+
if result.returncode == 0:
|
|
280
|
+
return result.stdout.strip()
|
|
281
|
+
return None
|
|
282
|
+
except Exception:
|
|
283
|
+
return None
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def git_remote_add(
|
|
287
|
+
arena_dir: str,
|
|
288
|
+
url: str,
|
|
289
|
+
remote_name: str = "origin",
|
|
290
|
+
force: bool = False,
|
|
291
|
+
) -> tuple[bool, str]:
|
|
292
|
+
"""
|
|
293
|
+
Configure a remote repository.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
arena_dir: Path to the arena directory
|
|
297
|
+
url: Remote repository URL
|
|
298
|
+
remote_name: Name of the remote (default: origin)
|
|
299
|
+
force: If True, overwrite existing remote URL
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
Tuple of (success, message)
|
|
303
|
+
"""
|
|
304
|
+
if not is_git_initialized(arena_dir):
|
|
305
|
+
return False, "Git repository not initialized"
|
|
306
|
+
|
|
307
|
+
existing_url = git_remote_get_url(arena_dir, remote_name)
|
|
308
|
+
|
|
309
|
+
if existing_url:
|
|
310
|
+
if existing_url == url:
|
|
311
|
+
return True, f"Remote '{remote_name}' already configured with this URL"
|
|
312
|
+
|
|
313
|
+
if not force:
|
|
314
|
+
return False, (
|
|
315
|
+
f"Remote '{remote_name}' already exists with URL: {existing_url}. "
|
|
316
|
+
f"Use --force to overwrite."
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Remove existing remote
|
|
320
|
+
try:
|
|
321
|
+
_run_git_command(arena_dir, ["remote", "remove", remote_name])
|
|
322
|
+
except subprocess.CalledProcessError as e:
|
|
323
|
+
return False, f"Failed to remove existing remote: {e.stderr}"
|
|
324
|
+
|
|
325
|
+
# Add remote
|
|
326
|
+
try:
|
|
327
|
+
_run_git_command(arena_dir, ["remote", "add", remote_name, url])
|
|
328
|
+
logger.info(f"Added remote '{remote_name}': {url}")
|
|
329
|
+
return True, f"Remote '{remote_name}' configured: {url}"
|
|
330
|
+
except subprocess.CalledProcessError as e:
|
|
331
|
+
error_msg = f"Failed to add remote: {e.stderr}"
|
|
332
|
+
logger.error(error_msg)
|
|
333
|
+
return False, error_msg
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def git_push(
|
|
337
|
+
arena_dir: str,
|
|
338
|
+
remote_name: str = "origin",
|
|
339
|
+
branch: Optional[str] = None,
|
|
340
|
+
set_upstream: bool = True,
|
|
341
|
+
) -> tuple[bool, str]:
|
|
342
|
+
"""
|
|
343
|
+
Push commits to the remote repository.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
arena_dir: Path to the arena directory
|
|
347
|
+
remote_name: Name of the remote (default: origin)
|
|
348
|
+
branch: Branch name (default: current branch)
|
|
349
|
+
set_upstream: If True, set upstream tracking
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Tuple of (success, message)
|
|
353
|
+
"""
|
|
354
|
+
if not is_git_initialized(arena_dir):
|
|
355
|
+
return False, "Git repository not initialized"
|
|
356
|
+
|
|
357
|
+
# Check if remote is configured
|
|
358
|
+
remote_url = git_remote_get_url(arena_dir, remote_name)
|
|
359
|
+
if not remote_url:
|
|
360
|
+
return False, f"Remote '{remote_name}' not configured. Use 'genarena git remote --url <url>' first."
|
|
361
|
+
|
|
362
|
+
# Get current branch if not specified
|
|
363
|
+
if not branch:
|
|
364
|
+
try:
|
|
365
|
+
result = _run_git_command(arena_dir, ["branch", "--show-current"])
|
|
366
|
+
branch = result.stdout.strip()
|
|
367
|
+
if not branch:
|
|
368
|
+
# Might be on a detached HEAD, try to get default branch
|
|
369
|
+
branch = "main"
|
|
370
|
+
except subprocess.CalledProcessError:
|
|
371
|
+
branch = "main"
|
|
372
|
+
|
|
373
|
+
# Push
|
|
374
|
+
try:
|
|
375
|
+
push_args = ["push"]
|
|
376
|
+
if set_upstream:
|
|
377
|
+
push_args.extend(["-u", remote_name, branch])
|
|
378
|
+
else:
|
|
379
|
+
push_args.extend([remote_name, branch])
|
|
380
|
+
|
|
381
|
+
_run_git_command(arena_dir, push_args)
|
|
382
|
+
logger.info(f"Pushed to {remote_name}/{branch}")
|
|
383
|
+
return True, f"Pushed to {remote_name}/{branch}"
|
|
384
|
+
except subprocess.CalledProcessError as e:
|
|
385
|
+
error_msg = f"Failed to push: {e.stderr}"
|
|
386
|
+
logger.error(error_msg)
|
|
387
|
+
return False, error_msg
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def git_sync(arena_dir: str) -> tuple[bool, str]:
|
|
391
|
+
"""
|
|
392
|
+
Commit all changes and push to remote (one-click sync).
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
arena_dir: Path to the arena directory
|
|
396
|
+
|
|
397
|
+
Returns:
|
|
398
|
+
Tuple of (success, message)
|
|
399
|
+
"""
|
|
400
|
+
if not is_git_initialized(arena_dir):
|
|
401
|
+
return False, "Git repository not initialized"
|
|
402
|
+
|
|
403
|
+
messages = []
|
|
404
|
+
|
|
405
|
+
# Commit changes
|
|
406
|
+
success, msg = git_commit(arena_dir)
|
|
407
|
+
messages.append(msg)
|
|
408
|
+
|
|
409
|
+
if not success and "Nothing to commit" not in msg:
|
|
410
|
+
return False, msg
|
|
411
|
+
|
|
412
|
+
# Push to remote
|
|
413
|
+
success, msg = git_push(arena_dir)
|
|
414
|
+
messages.append(msg)
|
|
415
|
+
|
|
416
|
+
if not success:
|
|
417
|
+
# If push fails due to no remote, still return partial success
|
|
418
|
+
if "not configured" in msg:
|
|
419
|
+
return True, f"{messages[0]} (push skipped: {msg})"
|
|
420
|
+
return False, msg
|
|
421
|
+
|
|
422
|
+
return True, " | ".join(messages)
|