repofix 0.1.0__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.
repofix/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """RepoFix — run any GitHub repo locally with one command."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1 @@
1
+ """Branch-aware dependency caching for repofix."""
@@ -0,0 +1,152 @@
1
+ """Branch-aware dependency cache utilities.
2
+
3
+ Each git branch tracks its own dependency fingerprint so repofix can skip
4
+ reinstalling packages when switching back to a previously-set-up branch.
5
+
6
+ Isolation strategy by runtime:
7
+ Python → branch-specific venv: .venv-<branch-slug> (full isolation)
8
+ Node → shared node_modules; dep hash decides whether npm/yarn/pnpm re-runs
9
+ Go/Rust → module caches are global; dep hash decides whether to re-run tidy/fetch
10
+ Ruby → shared vendor/bundle; dep hash controls bundle install
11
+ Others → dep hash tracked; no extra isolation needed
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ from pathlib import Path
18
+
19
+
20
+ # Dependency manifest files — any change here invalidates the branch cache.
21
+ DEP_FILE_NAMES: list[str] = [
22
+ # Python
23
+ "requirements.txt",
24
+ "requirements-dev.txt",
25
+ "requirements-prod.txt",
26
+ "pyproject.toml",
27
+ "setup.py",
28
+ "setup.cfg",
29
+ "Pipfile",
30
+ "Pipfile.lock",
31
+ "uv.lock",
32
+ # Node
33
+ "package.json",
34
+ "package-lock.json",
35
+ "yarn.lock",
36
+ "pnpm-lock.yaml",
37
+ # Go
38
+ "go.mod",
39
+ "go.sum",
40
+ # Rust
41
+ "Cargo.toml",
42
+ "Cargo.lock",
43
+ # Ruby
44
+ "Gemfile",
45
+ "Gemfile.lock",
46
+ # PHP
47
+ "composer.json",
48
+ "composer.lock",
49
+ # Dart / Flutter
50
+ "pubspec.yaml",
51
+ "pubspec.lock",
52
+ # Java / Kotlin
53
+ "pom.xml",
54
+ "build.gradle",
55
+ "build.gradle.kts",
56
+ "gradle.lockfile",
57
+ ]
58
+
59
+
60
+ def get_current_branch(repo_path: Path) -> str:
61
+ """Return the active git branch name, or 'HEAD' for detached/non-git repos."""
62
+ try:
63
+ from git import InvalidGitRepositoryError, Repo # type: ignore[import]
64
+ repo = Repo(repo_path, search_parent_directories=True)
65
+ return repo.active_branch.name
66
+ except Exception:
67
+ return "HEAD"
68
+
69
+
70
+ def compute_dep_hash(repo_path: Path) -> tuple[str, list[str]]:
71
+ """
72
+ SHA-256 hash of the contents of every present dependency manifest.
73
+
74
+ Returns:
75
+ (hex_digest, list_of_found_filenames)
76
+
77
+ An empty repo (no dep files at all) returns a stable sentinel hash so it
78
+ can still be cached and compared correctly.
79
+ """
80
+ hasher = hashlib.sha256()
81
+ found: list[str] = []
82
+ for name in DEP_FILE_NAMES:
83
+ dep_file = repo_path / name
84
+ if dep_file.exists() and dep_file.is_file():
85
+ found.append(name)
86
+ hasher.update(name.encode())
87
+ hasher.update(dep_file.read_bytes())
88
+
89
+ if not found:
90
+ # Stable sentinel so a no-dep-file repo still gets a consistent hash
91
+ hasher.update(b"__no_dep_files__")
92
+
93
+ return hasher.hexdigest(), found
94
+
95
+
96
+ def normalize_repo_key(source: str, repo_path: Path) -> str:
97
+ """Stable, lowercase string key identifying a repo (URL or absolute path)."""
98
+ if source.startswith(("http://", "https://", "git@")):
99
+ key = source.rstrip("/")
100
+ if key.endswith(".git"):
101
+ key = key[:-4]
102
+ return key.lower()
103
+ return str(repo_path.resolve())
104
+
105
+
106
+ def branch_slug(branch: str) -> str:
107
+ """Convert a branch name to a safe, short filesystem slug.
108
+
109
+ Examples:
110
+ "main" → "main"
111
+ "feature/my-work" → "feature-my-work"
112
+ "HEAD" → "HEAD"
113
+ """
114
+ safe = (
115
+ branch
116
+ .replace("/", "-")
117
+ .replace("\\", "-")
118
+ .replace(" ", "-")
119
+ .replace(":", "-")
120
+ )
121
+ # Keep only alphanumeric, dash, underscore, dot
122
+ safe = "".join(c for c in safe if c.isalnum() or c in "-_.")
123
+ return safe[:48] or "branch"
124
+
125
+
126
+ def branch_venv_name(branch: str) -> str:
127
+ """Return the .venv directory name to use for a given branch."""
128
+ slug = branch_slug(branch)
129
+ # main / master keep the canonical name for backwards compatibility
130
+ if slug in ("main", "master"):
131
+ return ".venv"
132
+ return f".venv-{slug}"
133
+
134
+
135
+ def is_env_valid(repo_path: Path, runtime: str, env_dir: str) -> bool:
136
+ """
137
+ Sanity-check that the cached isolated environment is still usable.
138
+
139
+ For Python: the branch-specific venv python binary must exist.
140
+ For Node: node_modules directory must exist.
141
+ For others: assume valid (global caches are outside the repo).
142
+ """
143
+ rt = runtime.lower()
144
+ if rt in ("python", "pip"):
145
+ venv = Path(env_dir) if env_dir else repo_path / ".venv"
146
+ return (venv / "bin" / "python").exists()
147
+ if rt in ("node", "npm"):
148
+ return (repo_path / "node_modules").exists()
149
+ if rt == "ruby":
150
+ return (repo_path / "vendor" / "bundle").exists()
151
+ # Go, Rust, Java, PHP, Docker — their caches live in global dirs; trust them
152
+ return True