ravmem-cli 0.1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RavMem
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.4
2
+ Name: ravmem-cli
3
+ Version: 0.1.0
4
+ Summary: RavMem CLI — lightweight client for the RavMem code knowledge graph server
5
+ Author-email: RavMem <harpreet.singh@ravmem.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://www.ravmem.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
13
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
14
+ Requires-Python: >=3.10
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: typer[all]>=0.12.0
18
+ Requires-Dist: httpx>=0.27.0
19
+ Requires-Dist: pyyaml>=6.0
20
+ Requires-Dist: pydantic>=2.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
23
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
24
+ Dynamic: license-file
25
+
26
+ # ravmem-cli
27
+
28
+ Lightweight CLI client for the [RavMem](https://github.com/ravmem/ravmem) code knowledge graph server.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install ravmem-cli
34
+ ```
35
+
36
+ ## Quick start
37
+
38
+ ```bash
39
+ # Point the CLI at your RavMem server
40
+ ravmem remote config --url http://localhost:7432
41
+
42
+ # Register a repository
43
+ ravmem remote add /path/to/repo --name my-project --default-branch main
44
+
45
+ # Index a repository
46
+ ravmem analyze /path/to/repo --project <ID>
47
+
48
+ # Search symbols
49
+ ravmem search "authentication handler" --project <ID>
50
+
51
+ # Blast-radius analysis
52
+ ravmem impact MyClass.my_method --project <ID>
53
+ ```
54
+
55
+ ## Requirements
56
+
57
+ - Python 3.10+
58
+ - A running [ravmem-server](https://pypi.org/project/ravmem-server/) instance
59
+
60
+ ## License
61
+
62
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,37 @@
1
+ # ravmem-cli
2
+
3
+ Lightweight CLI client for the [RavMem](https://github.com/ravmem/ravmem) code knowledge graph server.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install ravmem-cli
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```bash
14
+ # Point the CLI at your RavMem server
15
+ ravmem remote config --url http://localhost:7432
16
+
17
+ # Register a repository
18
+ ravmem remote add /path/to/repo --name my-project --default-branch main
19
+
20
+ # Index a repository
21
+ ravmem analyze /path/to/repo --project <ID>
22
+
23
+ # Search symbols
24
+ ravmem search "authentication handler" --project <ID>
25
+
26
+ # Blast-radius analysis
27
+ ravmem impact MyClass.my_method --project <ID>
28
+ ```
29
+
30
+ ## Requirements
31
+
32
+ - Python 3.10+
33
+ - A running [ravmem-server](https://pypi.org/project/ravmem-server/) instance
34
+
35
+ ## License
36
+
37
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ravmem-cli"
7
+ version = "0.1.0"
8
+ description = "RavMem CLI — lightweight client for the RavMem code knowledge graph server"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [
14
+ { name = "RavMem", email = "harpreet.singh@ravmem.com" },
15
+ ]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Operating System :: OS Independent",
21
+ "Topic :: Software Development :: Libraries :: Python Modules",
22
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
23
+ ]
24
+ dependencies = [
25
+ "typer[all]>=0.12.0",
26
+ "httpx>=0.27.0",
27
+ "pyyaml>=6.0",
28
+ "pydantic>=2.0",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://www.ravmem.com"
33
+
34
+ [project.optional-dependencies]
35
+ dev = [
36
+ "pytest>=8.0.0",
37
+ "pytest-asyncio>=0.23.0",
38
+ ]
39
+
40
+ [project.scripts]
41
+ ravmem = "ravmem.cli.main:app"
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["."]
45
+ namespaces = true
46
+ include = ["ravmem*"]
47
+
48
+ [tool.pytest.ini_options]
49
+ asyncio_mode = "strict"
File without changes
@@ -0,0 +1,484 @@
1
+ """ravmem analyze — index a repository via the remote server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import typer
10
+
11
+ from ravmem.config import DEFAULT_LANGS
12
+
13
+ # Extensions whose content is never useful for the code-graph pipeline.
14
+ # Oversized text files (> _CLI_MAX_FILE_BYTES) are also skipped.
15
+ _BINARY_SUFFIXES = {
16
+ # Images
17
+ ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".ico", ".webp", ".tiff",
18
+ # Documents
19
+ ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
20
+ # Archives
21
+ ".zip", ".tar", ".gz", ".bz2", ".xz", ".rar", ".7z",
22
+ # Compiled / binary
23
+ ".exe", ".dll", ".so", ".dylib", ".bin", ".o", ".a", ".lib",
24
+ ".pyc", ".pyo", ".pyd", ".class", ".jar", ".wasm",
25
+ # Media
26
+ ".mp3", ".mp4", ".avi", ".mov", ".wav", ".flac", ".ogg",
27
+ # Fonts
28
+ ".ttf", ".otf", ".woff", ".woff2",
29
+ # Data / model files
30
+ ".pkl", ".joblib", ".npy", ".npz", ".h5", ".hdf5", ".onnx", ".pt", ".pth",
31
+ # Database
32
+ ".db", ".sqlite", ".sqlite3",
33
+ }
34
+
35
+ # Match server default RAVMEM_MAX_FILE_SIZE_MB=5 to avoid uploading files the
36
+ # server would skip anyway.
37
+ _CLI_MAX_FILE_BYTES = 5 * 1024 * 1024
38
+
39
+
40
+ def _get_client(remote: str | None):
41
+ from ravmem.client.api import RavMemClient
42
+ from ravmem.client.config import load_config
43
+
44
+ url = remote
45
+ api_key = None
46
+ if not url:
47
+ cfg = load_config()
48
+ url = cfg.get("server_url")
49
+ api_key = cfg.get("api_key")
50
+ if not url:
51
+ typer.echo(
52
+ "Error: No server configured. "
53
+ "Run `ravmem remote config --url <URL>` or pass --remote <URL>.",
54
+ err=True,
55
+ )
56
+ raise typer.Exit(1)
57
+ return RavMemClient(base_url=url, api_key=api_key or None)
58
+
59
+
60
+ def _print_job_result(job: dict) -> None:
61
+ status = job.get("status", "unknown")
62
+ if status == "done":
63
+ typer.echo(f"✓ Indexing complete (job {job.get('job_id', '?')}).")
64
+ elif status == "failed":
65
+ typer.echo(
66
+ f"✗ Indexing failed (job {job.get('job_id', '?')}): "
67
+ f"{job.get('failure_reason', 'unknown')}",
68
+ err=True,
69
+ )
70
+ raise typer.Exit(1)
71
+ else:
72
+ typer.echo(f"Job {job.get('job_id', '?')} ended with status: {status}")
73
+
74
+
75
+ def _analyze_upload(
76
+ client,
77
+ project_id: str,
78
+ repo_path: Path,
79
+ branch_name: str,
80
+ parent_branch: str | None,
81
+ batch_size: int,
82
+ lang_list: list[str],
83
+ wait: bool,
84
+ ) -> None:
85
+ """Upload committed files to the server in batches and trigger indexing."""
86
+ from ravmem.cli.git_local import ls_files, rev_parse, LocalGitError
87
+
88
+ typer.echo(
89
+ " ℹ ravmem indexes committed files only. "
90
+ "Uncommitted changes will not appear.\n"
91
+ " Commit your work before running analyze if you want it included."
92
+ )
93
+
94
+ # Collect committed files
95
+ try:
96
+ all_rel_paths = ls_files(repo_path)
97
+ except LocalGitError as exc:
98
+ typer.echo(f"✗ git ls-files failed: {exc}", err=True)
99
+ raise typer.Exit(1)
100
+
101
+ # Get HEAD commit SHA to record as last_indexed_commit
102
+ try:
103
+ to_commit = rev_parse(repo_path)
104
+ except LocalGitError:
105
+ to_commit = ""
106
+
107
+ # Filter binaries and oversized files
108
+ to_upload: list[str] = []
109
+ skipped = 0
110
+ for rel in all_rel_paths:
111
+ p = repo_path / rel
112
+ if p.suffix.lower() in _BINARY_SUFFIXES:
113
+ skipped += 1
114
+ continue
115
+ try:
116
+ if p.stat().st_size > _CLI_MAX_FILE_BYTES:
117
+ skipped += 1
118
+ continue
119
+ except OSError:
120
+ skipped += 1
121
+ continue
122
+ to_upload.append(rel)
123
+
124
+ if not to_upload:
125
+ typer.echo("No source files found to upload.")
126
+ return
127
+
128
+ total_files = len(to_upload)
129
+ batches = [to_upload[i : i + batch_size] for i in range(0, total_files, batch_size)]
130
+ batch_total = len(batches)
131
+
132
+ typer.echo(
133
+ f" Uploading {total_files} files"
134
+ + (f" ({skipped} skipped)" if skipped else "")
135
+ + f" in {batch_total} batch(es)…"
136
+ )
137
+
138
+ job_id: int | None = None
139
+ for batch_index, batch_paths in enumerate(batches, 1):
140
+ files: list[dict] = []
141
+ for rel in batch_paths:
142
+ p = repo_path / rel
143
+ try:
144
+ content = base64.b64encode(p.read_bytes()).decode()
145
+ files.append({"path": rel, "content": content})
146
+ except OSError:
147
+ pass # unreadable — skip silently
148
+
149
+ result = client.upload_batch(
150
+ project_id=project_id,
151
+ branch=branch_name,
152
+ batch_index=batch_index,
153
+ batch_total=batch_total,
154
+ files=files,
155
+ parent_branch=parent_branch,
156
+ to_commit=to_commit,
157
+ langs=lang_list if lang_list else None,
158
+ )
159
+
160
+ if batch_total > 1:
161
+ typer.echo(f" Batch {batch_index}/{batch_total} sent ({len(files)} files).")
162
+
163
+ if "job_id" in result:
164
+ job_id = result["job_id"]
165
+
166
+ if job_id is None:
167
+ typer.echo("✗ Server did not return a job_id.", err=True)
168
+ raise typer.Exit(1)
169
+
170
+ typer.echo(f"Indexing started on branch '{branch_name}': job_id={job_id}")
171
+
172
+ if wait:
173
+ typer.echo("Waiting for completion…")
174
+ job = client.poll_job(job_id)
175
+ _print_job_result(job)
176
+
177
+
178
+ def _analyze_incremental_upload(
179
+ client,
180
+ project_id: str,
181
+ repo_path: Path,
182
+ branch_name: str,
183
+ parent_branch: str | None,
184
+ batch_size: int,
185
+ lang_list: list[str],
186
+ wait: bool,
187
+ ) -> None:
188
+ """Upload only the files changed since the last index, plus a deletion list."""
189
+ from ravmem.cli.git_local import diff_name_status, rev_parse, LocalGitError
190
+
191
+ # 1. Find out where the server's index is anchored
192
+ try:
193
+ status = client.get_branch_status(project_id, branch_name)
194
+ last_commit = status.get("last_indexed_commit") or ""
195
+ except Exception:
196
+ last_commit = ""
197
+
198
+ if not last_commit:
199
+ typer.echo(
200
+ " ℹ No previous index found for this branch — running full upload instead."
201
+ )
202
+ _analyze_upload(
203
+ client=client,
204
+ project_id=project_id,
205
+ repo_path=repo_path,
206
+ branch_name=branch_name,
207
+ parent_branch=parent_branch,
208
+ batch_size=batch_size,
209
+ lang_list=lang_list,
210
+ wait=wait,
211
+ )
212
+ return
213
+
214
+ # 2. Diff from the last indexed commit to HEAD
215
+ try:
216
+ modified, deleted = diff_name_status(repo_path, last_commit)
217
+ to_commit = rev_parse(repo_path)
218
+ except LocalGitError as exc:
219
+ typer.echo(f"✗ git diff failed: {exc}", err=True)
220
+ raise typer.Exit(1)
221
+
222
+ if not modified and not deleted:
223
+ typer.echo(
224
+ f" ℹ No changes since last index ({last_commit[:7]}). Nothing to do."
225
+ )
226
+ return
227
+
228
+ # 3. Filter modified files (same binary/size rules as full upload)
229
+ to_upload: list[str] = []
230
+ skipped = 0
231
+ for rel in modified:
232
+ p = repo_path / rel
233
+ if p.suffix.lower() in _BINARY_SUFFIXES:
234
+ skipped += 1
235
+ continue
236
+ try:
237
+ if p.stat().st_size > _CLI_MAX_FILE_BYTES:
238
+ skipped += 1
239
+ continue
240
+ except OSError:
241
+ skipped += 1
242
+ continue
243
+ to_upload.append(rel)
244
+
245
+ typer.echo(
246
+ f" Incremental: {len(to_upload)} modified"
247
+ + (f" ({skipped} skipped)" if skipped else "")
248
+ + f", {len(deleted)} deleted, base {last_commit[:7]}…"
249
+ )
250
+
251
+ # 4. Build batches — at least one batch even if no modified files (deletions only)
252
+ batches: list[list[str]] = (
253
+ [to_upload[i : i + batch_size] for i in range(0, len(to_upload), batch_size)]
254
+ if to_upload
255
+ else [[]]
256
+ )
257
+ batch_total = len(batches)
258
+
259
+ job_id: int | None = None
260
+ for batch_index, batch_paths in enumerate(batches, 1):
261
+ files: list[dict] = []
262
+ for rel in batch_paths:
263
+ p = repo_path / rel
264
+ try:
265
+ content = base64.b64encode(p.read_bytes()).decode()
266
+ files.append({"path": rel, "content": content})
267
+ except OSError:
268
+ pass # unreadable — skip silently
269
+
270
+ result = client.upload_batch(
271
+ project_id=project_id,
272
+ branch=branch_name,
273
+ batch_index=batch_index,
274
+ batch_total=batch_total,
275
+ files=files,
276
+ parent_branch=parent_branch,
277
+ to_commit=to_commit,
278
+ from_commit=last_commit,
279
+ mode="incremental",
280
+ deleted_paths=deleted if batch_index == 1 else [],
281
+ langs=lang_list if lang_list else None,
282
+ )
283
+
284
+ if batch_total > 1:
285
+ typer.echo(f" Batch {batch_index}/{batch_total} sent ({len(files)} files).")
286
+
287
+ if "job_id" in result:
288
+ job_id = result["job_id"]
289
+
290
+ if job_id is None:
291
+ typer.echo("✗ Server did not return a job_id.", err=True)
292
+ raise typer.Exit(1)
293
+
294
+ typer.echo(f"Incremental indexing started on branch '{branch_name}': job_id={job_id}")
295
+
296
+ if wait:
297
+ typer.echo("Waiting for completion…")
298
+ job = client.poll_job(job_id)
299
+ _print_job_result(job)
300
+
301
+
302
+ def _analyze_server_side(
303
+ client,
304
+ project_id: str,
305
+ branch_name: str,
306
+ mode: str,
307
+ lang_list: list[str],
308
+ wait: bool,
309
+ yes: bool,
310
+ no_auto_sync: bool = False,
311
+ ) -> None:
312
+ """Trigger server-side indexing (mount / clone projects)."""
313
+ # Pre-flight divergence check
314
+ try:
315
+ div = client.get_divergence(project_id, branch_name)
316
+ ratio = div.get("ratio", 0.0)
317
+ recommend = div.get("recommend", "full")
318
+ parent_branch_name = div.get("parent_branch") or ""
319
+ parent_behind = div.get("parent_behind", False)
320
+
321
+ # Cross-branch guard: warn when the parent index is stale (Phase 12).
322
+ # An incremental index built on a stale baseline may silently miss changes
323
+ # that landed on the parent after its last index run.
324
+ if parent_behind and parent_branch_name and not yes:
325
+ typer.echo(
326
+ f"\nWarning: parent branch '{parent_branch_name}' index is not current.\n"
327
+ f" Re-indexing '{parent_branch_name}' first will give more accurate incremental results."
328
+ )
329
+ if not typer.confirm("Proceed anyway?", default=False):
330
+ raise typer.Exit(0)
331
+
332
+ if ratio > 0.60 and recommend == "full" and mode == "auto" and not yes:
333
+ pct = int(ratio * 100)
334
+ typer.echo(
335
+ f"\nWarning: {div.get('changed_files', '?')}/{div.get('total_files', '?')} "
336
+ f"files changed since last index ({pct}%).\n"
337
+ "This will trigger a full re-index."
338
+ )
339
+ if not typer.confirm("Proceed?", default=False):
340
+ raise typer.Exit(0)
341
+ if mode == "auto":
342
+ mode = recommend
343
+ except typer.Exit:
344
+ raise
345
+ except Exception:
346
+ pass # divergence check is best-effort
347
+
348
+ result = client.index_branch(
349
+ project_id, branch_name, mode=mode, langs=lang_list or None,
350
+ no_auto_sync=no_auto_sync,
351
+ )
352
+ job_id = result.get("job_id")
353
+ typer.echo(f"Indexing started on branch '{branch_name}': job_id={job_id}")
354
+
355
+ if wait and job_id is not None:
356
+ typer.echo("Waiting for completion…")
357
+ job = client.poll_job(job_id)
358
+ _print_job_result(job)
359
+
360
+
361
+ def analyze_cmd(
362
+ repo: Path = typer.Argument(Path("."), help="Path to the repository to index"),
363
+ langs: str = typer.Option(
364
+ ",".join(DEFAULT_LANGS),
365
+ "--langs",
366
+ help="Comma-separated list of languages to index",
367
+ ),
368
+ incremental: bool = typer.Option(
369
+ False, "--incremental", "-i",
370
+ help="Run incremental update from last indexed commit (upload mode: patches; clone/mount: git-diff)",
371
+ ),
372
+ remote: Optional[str] = typer.Option(None, "--remote", help="Server URL (overrides config)"),
373
+ project: Optional[str] = typer.Option(None, "--project", help="Project ID on the server"),
374
+ branch: Optional[str] = typer.Option(
375
+ None, "--branch", help="Branch name (auto-detected from git if omitted)"
376
+ ),
377
+ parent_branch: Optional[str] = typer.Option(
378
+ None, "--parent-branch",
379
+ help="Parent branch for copy-from-base optimisation (defaults to project default branch)",
380
+ ),
381
+ batch_size: int = typer.Option(
382
+ 50, "--batch-size",
383
+ help="Files per upload batch (upload mode only; default 50)",
384
+ min=1, max=200,
385
+ ),
386
+ default_branch: Optional[str] = typer.Option(
387
+ None, "--default-branch",
388
+ help="Set/update the project's default/parent branch",
389
+ ),
390
+ wait: bool = typer.Option(False, "--wait", "-w", help="Wait for indexing to complete"),
391
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompts (CI/scripts)"),
392
+ no_auto_sync: bool = typer.Option(
393
+ False, "--no-auto-sync",
394
+ help="Skip automatic git fetch before indexing (clone-mode only). "
395
+ "Use when you know the workspace is already up to date.",
396
+ ),
397
+ ) -> None:
398
+ """Index a repository — detects upload/clone/mount mode from the project registration."""
399
+
400
+ repo_path = repo.resolve()
401
+
402
+ client = _get_client(remote)
403
+ try:
404
+ if not project:
405
+ typer.echo(
406
+ "✗ --project is required. "
407
+ "Register first with `ravmem remote add`.",
408
+ err=True,
409
+ )
410
+ raise typer.Exit(1)
411
+
412
+ # Fetch project info to determine index_mode and default_branch
413
+ try:
414
+ project_info = client.get_project(project)
415
+ except Exception as exc:
416
+ typer.echo(f"✗ Could not fetch project '{project}': {exc}", err=True)
417
+ raise typer.Exit(1)
418
+
419
+ index_mode = project_info.get("index_mode", "mount")
420
+ project_default_branch = project_info.get("default_branch", "main")
421
+
422
+ # Update default branch if requested
423
+ if default_branch:
424
+ client.update_project(project, default_branch=default_branch)
425
+ typer.echo(f"Default branch updated to '{default_branch}'.")
426
+ project_default_branch = default_branch
427
+
428
+ # Detect branch
429
+ if branch:
430
+ branch_name = branch
431
+ elif index_mode == "upload":
432
+ from ravmem.cli.git_local import current_branch
433
+ branch_name = current_branch(repo_path) or project_default_branch
434
+ else:
435
+ from ravmem.cli.git_local import current_branch
436
+ branch_name = current_branch(repo_path) or project_default_branch
437
+
438
+ effective_parent = parent_branch or project_default_branch
439
+ lang_list = [ln.strip() for ln in langs.split(",") if ln.strip()]
440
+
441
+ typer.echo(
442
+ f"Analyzing '{project}' branch '{branch_name}' "
443
+ f"[mode: {index_mode}]…"
444
+ )
445
+
446
+ if index_mode == "upload":
447
+ if incremental:
448
+ _analyze_incremental_upload(
449
+ client=client,
450
+ project_id=project,
451
+ repo_path=repo_path,
452
+ branch_name=branch_name,
453
+ parent_branch=effective_parent,
454
+ batch_size=batch_size,
455
+ lang_list=lang_list,
456
+ wait=wait,
457
+ )
458
+ else:
459
+ _analyze_upload(
460
+ client=client,
461
+ project_id=project,
462
+ repo_path=repo_path,
463
+ branch_name=branch_name,
464
+ parent_branch=effective_parent,
465
+ batch_size=batch_size,
466
+ lang_list=lang_list,
467
+ wait=wait,
468
+ )
469
+ else:
470
+ # mount or clone — server-side indexing
471
+ mode = "incremental" if incremental else "auto"
472
+ _analyze_server_side(
473
+ client=client,
474
+ project_id=project,
475
+ branch_name=branch_name,
476
+ mode=mode,
477
+ lang_list=lang_list,
478
+ wait=wait,
479
+ yes=yes,
480
+ no_auto_sync=no_auto_sync,
481
+ )
482
+
483
+ finally:
484
+ client.close()