repofuse 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,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: repofuse
3
+ Version: 0.1.0
4
+ Summary: Monorepo management: multi-repo isolation and documentation generation
5
+ Author-email: deepstrain <dev@massiron.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/massiron/repofuse
8
+ Project-URL: Repository, https://github.com/massiron/repofuse
9
+ Project-URL: Documentation, https://github.com/massiron/repofuse#readme
10
+ Keywords: monorepo,git,repository,documentation
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: gitpython>=3.1
22
+ Requires-Dist: typer>=0.9
23
+ Requires-Dist: rich>=13.0
24
+ Requires-Dist: pyyaml>=6.0
25
+
26
+ # repofuse
27
+
28
+ **Monorepo management: multi-repo isolation and documentation generation.**
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install repofuse
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ```bash
39
+ # Initialize a monorepo
40
+ repofuse init
41
+
42
+ # Add a sub-repository
43
+ repofuse add https://github.com/user/project.git
44
+
45
+ # Add with custom prefix
46
+ repofuse add https://github.com/user/project.git --prefix libs/project
47
+
48
+ # List registered repos
49
+ repofuse list
50
+
51
+ # Isolate a sub-repo as standalone
52
+ repofuse iso project-name -o _isolated
53
+
54
+ # Generate documentation
55
+ repofuse docs -o docs/
56
+ ```
57
+
58
+ ## API
59
+
60
+ ```python
61
+ from repofuse.core import add_repo, list_repos, remove_repo
62
+ from repofuse.isolate import isolate
63
+ from repofuse.docs import generate_docs
64
+ ```
65
+
66
+ ## License
67
+
68
+ MIT
@@ -0,0 +1,43 @@
1
+ # repofuse
2
+
3
+ **Monorepo management: multi-repo isolation and documentation generation.**
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install repofuse
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ # Initialize a monorepo
15
+ repofuse init
16
+
17
+ # Add a sub-repository
18
+ repofuse add https://github.com/user/project.git
19
+
20
+ # Add with custom prefix
21
+ repofuse add https://github.com/user/project.git --prefix libs/project
22
+
23
+ # List registered repos
24
+ repofuse list
25
+
26
+ # Isolate a sub-repo as standalone
27
+ repofuse iso project-name -o _isolated
28
+
29
+ # Generate documentation
30
+ repofuse docs -o docs/
31
+ ```
32
+
33
+ ## API
34
+
35
+ ```python
36
+ from repofuse.core import add_repo, list_repos, remove_repo
37
+ from repofuse.isolate import isolate
38
+ from repofuse.docs import generate_docs
39
+ ```
40
+
41
+ ## License
42
+
43
+ MIT
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=75.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "repofuse"
7
+ version = "0.1.0"
8
+ description = "Monorepo management: multi-repo isolation and documentation generation"
9
+ readme = "README.md"
10
+ license = {text = "MIT"}
11
+ requires-python = ">=3.10"
12
+ authors = [{name = "deepstrain", email = "dev@massiron.com"}]
13
+ keywords = ["monorepo", "git", "repository", "documentation"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ ]
24
+
25
+ dependencies = [
26
+ "gitpython>=3.1",
27
+ "typer>=0.9",
28
+ "rich>=13.0",
29
+ "pyyaml>=6.0",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://github.com/massiron/repofuse"
34
+ Repository = "https://github.com/massiron/repofuse"
35
+ Documentation = "https://github.com/massiron/repofuse#readme"
36
+
37
+ [project.scripts]
38
+ repofuse = "repofuse.cli:app"
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """repofuse — Monorepo management: multi-repo isolation and documentation generation."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,69 @@
1
+ """repofuse CLI — Typer-based command line interface."""
2
+
3
+ import typer
4
+ from pathlib import Path
5
+ from typing import Optional
6
+ from rich.console import Console
7
+ from repofuse.core import add_repo, list_repos, remove_repo, init_monorepo
8
+ from repofuse.isolate import isolate
9
+ from repofuse.docs import generate_docs
10
+
11
+ app = typer.Typer(help="repofuse — Monorepo management tool")
12
+ console = Console()
13
+
14
+ @app.command()
15
+ def init(
16
+ path: Path = typer.Argument(".", help="Project root"),
17
+ remote: Optional[str] = typer.Option(None, "--remote", "-r", help="Remote URL for config sync"),
18
+ ):
19
+ """Initialize a monorepo structure."""
20
+ init_monorepo(path, remote)
21
+ console.print(f"[green]OK[/green] Monorepo initialized at [bold]{path.resolve()}[/bold]")
22
+
23
+ @app.command()
24
+ def add(
25
+ url: str = typer.Argument(..., help="Git repository URL"),
26
+ prefix: str = typer.Option("", "--prefix", "-p", help="Subdirectory prefix (auto-detected if empty)"),
27
+ ref: str = typer.Option("main", "--ref", help="Branch/tag/commit to track"),
28
+ ):
29
+ """Add a sub-repository to the monorepo."""
30
+ result = add_repo(url, prefix=prefix or None, ref=ref)
31
+ console.print(f"[green]OK[/green] Added [bold]{result['name']}[/bold] -> [dim]{result['path']}[/dim]")
32
+
33
+ @app.command()
34
+ def list():
35
+ """List all registered sub-repositories."""
36
+ repos = list_repos()
37
+ if not repos:
38
+ console.print("[yellow]No sub-repositories registered.[/yellow]")
39
+ return
40
+ for r in repos:
41
+ console.print(f" [cyan]{r['name']}[/cyan] [dim]{r['path']}[/dim] ({r['ref']})")
42
+
43
+ @app.command()
44
+ def remove(
45
+ name: str = typer.Argument(..., help="Repository name to remove"),
46
+ keep_files: bool = typer.Option(False, "--keep-files", help="Keep files on disk"),
47
+ ):
48
+ """Remove a sub-repository from the monorepo."""
49
+ remove_repo(name, keep_files=keep_files)
50
+ console.print(f"[green]OK[/green] Removed [bold]{name}[/bold]")
51
+
52
+ @app.command()
53
+ def iso(
54
+ name: str = typer.Argument(..., help="Repository name to isolate"),
55
+ output: Path = typer.Option(Path("_isolated"), "--output", "-o", help="Output directory"),
56
+ ):
57
+ """Isolate a sub-repository as a standalone repo with full history."""
58
+ path = isolate(name, output)
59
+ console.print(f"[green]OK[/green] Isolated to [bold]{path}[/bold]")
60
+
61
+ @app.command()
62
+ def docs(
63
+ path: Path = typer.Argument(".", help="Repository path"),
64
+ output: Path = typer.Option(Path("docs"), "--output", "-o", help="Output directory"),
65
+ format: str = typer.Option("markdown", "--format", "-f", help="Output format (markdown/json)"),
66
+ ):
67
+ """Generate documentation from repository source code."""
68
+ result = generate_docs(path, output, fmt=format)
69
+ console.print(f"[green]OK[/green] Generated {result['file_count']} docs -> [bold]{output.resolve()}[/bold]")
@@ -0,0 +1,91 @@
1
+ """repofuse core — Monorepo initialization, add/remove/list sub-repos."""
2
+
3
+ import json
4
+ import subprocess
5
+ from pathlib import Path
6
+ from typing import Optional
7
+ from repofuse.utils import git, repo_name_from_url, CONFIG_FILE
8
+
9
+
10
+ def init_monorepo(path: Path, remote: Optional[str] = None) -> dict:
11
+ path = Path(path).resolve()
12
+ path.mkdir(parents=True, exist_ok=True)
13
+
14
+ config_path = path / CONFIG_FILE
15
+ if config_path.exists():
16
+ raise FileExistsError(f"Monorepo already initialized at {path}")
17
+
18
+ config = {"version": "0.1.0", "remote": remote, "repos": {}}
19
+ config_path.write_text(json.dumps(config, indent=2))
20
+
21
+ if not (path / ".git").exists():
22
+ git(["init"], cwd=path)
23
+ if remote:
24
+ git(["remote", "add", "origin", remote], cwd=path)
25
+
26
+ return config
27
+
28
+
29
+ def add_repo(url: str, prefix: Optional[str] = None, ref: str = "main") -> dict:
30
+ config = _load_config()
31
+
32
+ name = repo_name_from_url(url)
33
+ target = prefix or name
34
+ dest = Path(target)
35
+
36
+ if name in config["repos"]:
37
+ raise ValueError(f"Repository '{name}' already registered")
38
+
39
+ if dest.exists():
40
+ raise FileExistsError(f"Path '{dest}' already exists")
41
+
42
+ dest.mkdir(parents=True, exist_ok=True)
43
+ git(["clone", "--depth", "1", "--branch", ref, url, str(dest)])
44
+ if (dest / ".git").exists():
45
+ _remove_git_history(dest)
46
+
47
+ entry = {"url": url, "path": str(dest), "ref": ref}
48
+ config["repos"][name] = entry
49
+ _save_config(config)
50
+ return {"name": name, **entry}
51
+
52
+
53
+ def list_repos() -> list[dict]:
54
+ config = _load_config()
55
+ result = []
56
+ for name, entry in config["repos"].items():
57
+ result.append({"name": name, **entry})
58
+ return result
59
+
60
+
61
+ def remove_repo(name: str, keep_files: bool = False) -> None:
62
+ config = _load_config()
63
+ if name not in config["repos"]:
64
+ raise KeyError(f"Repository '{name}' not found")
65
+
66
+ entry = config["repos"][name]
67
+ if not keep_files:
68
+ import shutil
69
+ shutil.rmtree(entry["path"], ignore_errors=True)
70
+
71
+ del config["repos"][name]
72
+ _save_config(config)
73
+
74
+
75
+ def _load_config() -> dict:
76
+ from repofuse.utils import find_config
77
+ config_path = find_config()
78
+ return json.loads(config_path.read_text("utf-8"))
79
+
80
+
81
+ def _save_config(config: dict) -> None:
82
+ from repofuse.utils import find_config
83
+ config_path = find_config()
84
+ config_path.write_text(json.dumps(config, indent=2, ensure_ascii=False))
85
+
86
+
87
+ def _remove_git_history(path: Path) -> None:
88
+ import shutil
89
+ git_dir = path / ".git"
90
+ if git_dir.exists():
91
+ shutil.rmtree(git_dir)
@@ -0,0 +1,81 @@
1
+ """repofuse docs — Generate documentation from repository source code."""
2
+
3
+ import ast
4
+ import json
5
+ from pathlib import Path
6
+
7
+
8
+ def generate_docs(path: Path, output: Path, fmt: str = "markdown") -> dict:
9
+ path = Path(path).resolve()
10
+ output = Path(output).resolve()
11
+ output.mkdir(parents=True, exist_ok=True)
12
+
13
+ files = sorted(path.rglob("*.py"))
14
+ generated = []
15
+
16
+ for filepath in files:
17
+ if any(part.startswith(".") or part == "__pycache__" for part in filepath.parts):
18
+ continue
19
+ rel = filepath.relative_to(path)
20
+ doc = _parse_python_file(filepath, rel)
21
+ if doc["functions"] or doc["classes"]:
22
+ generated.append(doc)
23
+
24
+ if fmt == "json":
25
+ (output / "docs.json").write_text(
26
+ json.dumps(generated, indent=2, ensure_ascii=False)
27
+ )
28
+ else:
29
+ _write_markdown(generated, output)
30
+
31
+ return {"file_count": len(generated), "output": str(output)}
32
+
33
+
34
+ def _parse_python_file(filepath: Path, rel: Path) -> dict:
35
+ try:
36
+ tree = ast.parse(filepath.read_text("utf-8"))
37
+ except SyntaxError:
38
+ return {"file": str(rel), "functions": [], "classes": []}
39
+
40
+ functions = []
41
+ classes = []
42
+
43
+ for node in ast.walk(tree):
44
+ if isinstance(node, ast.FunctionDef):
45
+ functions.append({
46
+ "name": node.name,
47
+ "docstring": ast.get_docstring(node) or "",
48
+ "line": node.lineno,
49
+ })
50
+ elif isinstance(node, ast.ClassDef):
51
+ classes.append({
52
+ "name": node.name,
53
+ "docstring": ast.get_docstring(node) or "",
54
+ "methods": [
55
+ {"name": n.name, "docstring": ast.get_docstring(n) or ""}
56
+ for n in node.body if isinstance(n, ast.FunctionDef)
57
+ ],
58
+ "line": node.lineno,
59
+ })
60
+
61
+ return {"file": str(rel), "functions": functions, "classes": classes}
62
+
63
+
64
+ def _write_markdown(docs: list[dict], output: Path) -> None:
65
+ for doc in docs:
66
+ md = output / Path(doc["file"]).with_suffix(".md")
67
+ md.parent.mkdir(parents=True, exist_ok=True)
68
+ lines = [f"# {doc['file']}\n"]
69
+ for cls in doc["classes"]:
70
+ lines.append(f"\n## Class: `{cls['name']}`\n")
71
+ if cls["docstring"]:
72
+ lines.append(f"{cls['docstring']}\n")
73
+ for m in cls["methods"]:
74
+ lines.append(f"### `{m['name']}()`\n")
75
+ if m["docstring"]:
76
+ lines.append(f"{m['docstring']}\n")
77
+ for fn in doc["functions"]:
78
+ lines.append(f"\n## `{fn['name']}()`\n")
79
+ if fn["docstring"]:
80
+ lines.append(f"{fn['docstring']}\n")
81
+ md.write_text("\n".join(lines), encoding="utf-8")
@@ -0,0 +1,31 @@
1
+ """repofuse isolate — Extract a sub-repo as standalone with isolated files."""
2
+
3
+ import json
4
+ import shutil
5
+ from pathlib import Path
6
+ from repofuse.utils import find_config
7
+
8
+
9
+ def isolate(name: str, output: Path) -> Path:
10
+ config_path = find_config()
11
+ config = json.loads(config_path.read_text("utf-8"))
12
+
13
+ if name not in config["repos"]:
14
+ raise KeyError(f"Repository '{name}' not found in monorepo config")
15
+
16
+ entry = config["repos"][name]
17
+ source = (config_path.parent / entry["path"]).resolve()
18
+ dest = Path(output).resolve()
19
+
20
+ if dest.exists():
21
+ shutil.rmtree(dest)
22
+
23
+ shutil.copytree(source, dest)
24
+
25
+ init_file = dest / "__init__.py"
26
+ if not init_file.exists():
27
+ parent_init = source / "__init__.py"
28
+ if parent_init.exists():
29
+ shutil.copy2(parent_init, init_file)
30
+
31
+ return dest
@@ -0,0 +1,38 @@
1
+ """repofuse utilities."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ CONFIG_FILE = ".repofuse.json"
7
+
8
+
9
+ def find_config() -> Path:
10
+ cwd = Path.cwd()
11
+ for parent in [cwd] + list(cwd.parents):
12
+ config = parent / CONFIG_FILE
13
+ if config.exists():
14
+ return config
15
+ raise FileNotFoundError(
16
+ f"No {CONFIG_FILE} found in any parent directory. "
17
+ "Run 'repofuse init' first."
18
+ )
19
+
20
+
21
+ def repo_name_from_url(url: str) -> str:
22
+ name = url.rstrip("/").split("/")[-1]
23
+ if name.endswith(".git"):
24
+ name = name[:-4]
25
+ return name
26
+
27
+
28
+ def git(args: list[str], cwd: Path) -> str:
29
+ result = subprocess.run(
30
+ ["git"] + args,
31
+ capture_output=True,
32
+ text=True,
33
+ cwd=str(cwd),
34
+ timeout=120,
35
+ )
36
+ if result.returncode != 0:
37
+ raise RuntimeError(f"git {' '.join(args)} failed: {result.stderr.strip()}")
38
+ return result.stdout.strip()
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: repofuse
3
+ Version: 0.1.0
4
+ Summary: Monorepo management: multi-repo isolation and documentation generation
5
+ Author-email: deepstrain <dev@massiron.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/massiron/repofuse
8
+ Project-URL: Repository, https://github.com/massiron/repofuse
9
+ Project-URL: Documentation, https://github.com/massiron/repofuse#readme
10
+ Keywords: monorepo,git,repository,documentation
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+ Requires-Dist: gitpython>=3.1
22
+ Requires-Dist: typer>=0.9
23
+ Requires-Dist: rich>=13.0
24
+ Requires-Dist: pyyaml>=6.0
25
+
26
+ # repofuse
27
+
28
+ **Monorepo management: multi-repo isolation and documentation generation.**
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install repofuse
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ```bash
39
+ # Initialize a monorepo
40
+ repofuse init
41
+
42
+ # Add a sub-repository
43
+ repofuse add https://github.com/user/project.git
44
+
45
+ # Add with custom prefix
46
+ repofuse add https://github.com/user/project.git --prefix libs/project
47
+
48
+ # List registered repos
49
+ repofuse list
50
+
51
+ # Isolate a sub-repo as standalone
52
+ repofuse iso project-name -o _isolated
53
+
54
+ # Generate documentation
55
+ repofuse docs -o docs/
56
+ ```
57
+
58
+ ## API
59
+
60
+ ```python
61
+ from repofuse.core import add_repo, list_repos, remove_repo
62
+ from repofuse.isolate import isolate
63
+ from repofuse.docs import generate_docs
64
+ ```
65
+
66
+ ## License
67
+
68
+ MIT
@@ -0,0 +1,15 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/repofuse/__init__.py
4
+ src/repofuse/cli.py
5
+ src/repofuse/core.py
6
+ src/repofuse/docs.py
7
+ src/repofuse/isolate.py
8
+ src/repofuse/utils.py
9
+ src/repofuse.egg-info/PKG-INFO
10
+ src/repofuse.egg-info/SOURCES.txt
11
+ src/repofuse.egg-info/dependency_links.txt
12
+ src/repofuse.egg-info/entry_points.txt
13
+ src/repofuse.egg-info/requires.txt
14
+ src/repofuse.egg-info/top_level.txt
15
+ tests/test_repofuse.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ repofuse = repofuse.cli:app
@@ -0,0 +1,4 @@
1
+ gitpython>=3.1
2
+ typer>=0.9
3
+ rich>=13.0
4
+ pyyaml>=6.0
@@ -0,0 +1 @@
1
+ repofuse
@@ -0,0 +1,50 @@
1
+ """Tests for repofuse."""
2
+
3
+ import json
4
+ import tempfile
5
+ from pathlib import Path
6
+
7
+ from repofuse.core import init_monorepo, add_repo, list_repos, remove_repo
8
+ from repofuse.utils import CONFIG_FILE
9
+
10
+
11
+ def test_init_monorepo():
12
+ with tempfile.TemporaryDirectory() as tmp:
13
+ path = Path(tmp) / "mono"
14
+ config = init_monorepo(path)
15
+ assert config["version"] == "0.1.0"
16
+ assert (path / CONFIG_FILE).exists()
17
+ assert (path / ".git").exists()
18
+
19
+
20
+ def test_init_twice_fails():
21
+ with tempfile.TemporaryDirectory() as tmp:
22
+ path = Path(tmp) / "mono"
23
+ init_monorepo(path)
24
+ import pytest
25
+ with pytest.raises(FileExistsError):
26
+ init_monorepo(path)
27
+
28
+
29
+ def test_roundtrip():
30
+ with tempfile.TemporaryDirectory() as tmp:
31
+ path = Path(tmp) / "mono"
32
+ init_monorepo(path)
33
+
34
+ old_cwd = Path.cwd()
35
+ import os
36
+ os.chdir(str(path))
37
+ try:
38
+ repos = list_repos()
39
+ assert repos == []
40
+ finally:
41
+ os.chdir(str(old_cwd))
42
+
43
+
44
+ def test_config_format():
45
+ with tempfile.TemporaryDirectory() as tmp:
46
+ path = Path(tmp) / "mono"
47
+ init_monorepo(path)
48
+ raw = json.loads((path / CONFIG_FILE).read_text())
49
+ assert "repos" in raw
50
+ assert "version" in raw