git-repokit 0.9.10__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.
- git_repokit-0.9.10.dist-info/METADATA +422 -0
- git_repokit-0.9.10.dist-info/RECORD +174 -0
- git_repokit-0.9.10.dist-info/WHEEL +5 -0
- git_repokit-0.9.10.dist-info/entry_points.txt +2 -0
- git_repokit-0.9.10.dist-info/licenses/LICENSE +9 -0
- git_repokit-0.9.10.dist-info/top_level.txt +1 -0
- repokit/__init__.py +8 -0
- repokit/__main__.py +12 -0
- repokit/__version__.py +4 -0
- repokit/_vendor/__init__.py +0 -0
- repokit/_vendor/ailib/__init__.py +1 -0
- repokit/_vendor/ailib/analyzer.py +177 -0
- repokit/_vendor/ailib/backend.py +95 -0
- repokit/_vendor/ailib/cache.py +97 -0
- repokit/_vendor/ailib/parsers.py +52 -0
- repokit/_vendor/ailib/transports/__init__.py +16 -0
- repokit/_vendor/ailib/transports/cli.py +312 -0
- repokit/_vendor/ailib/transports/openai_compat.py +383 -0
- repokit/_vendor/ailib/transports/prompt_file.py +41 -0
- repokit/_vendor/ailib/types.py +224 -0
- repokit/auth_integration.py +244 -0
- repokit/badges.py +183 -0
- repokit/branch_utils.py +364 -0
- repokit/cli.py +2997 -0
- repokit/common_kit.py +202 -0
- repokit/config.py +686 -0
- repokit/defaults.py +220 -0
- repokit/directory_analyzer.py +1244 -0
- repokit/directory_profiles.py +423 -0
- repokit/git_config.py +370 -0
- repokit/github_create.py +142 -0
- repokit/github_setup.py +248 -0
- repokit/guardrails.py +707 -0
- repokit/history_cleaner.py +449 -0
- repokit/history_protection.py +434 -0
- repokit/hooks/pre-commit-guardrails +122 -0
- repokit/hooks/pre-commit-template +28 -0
- repokit/layers.py +510 -0
- repokit/licenses/0bsd.txt +14 -0
- repokit/licenses/LICENSES.md +27 -0
- repokit/licenses/__init__.py +142 -0
- repokit/licenses/agpl-3.0.txt +661 -0
- repokit/licenses/all-rights-reserved.txt +10 -0
- repokit/licenses/apache-2.0.txt +201 -0
- repokit/licenses/bsd-2-clause.txt +24 -0
- repokit/licenses/bsd-3-clause.txt +28 -0
- repokit/licenses/bsl-1.0.txt +23 -0
- repokit/licenses/cc-by-4.0.txt +395 -0
- repokit/licenses/cc-by-sa-4.0.txt +427 -0
- repokit/licenses/cc0-1.0.txt +121 -0
- repokit/licenses/epl-2.0.txt +277 -0
- repokit/licenses/gpl-2.0.txt +339 -0
- repokit/licenses/gpl-3.0.txt +674 -0
- repokit/licenses/isc.txt +15 -0
- repokit/licenses/lgpl-2.1.txt +504 -0
- repokit/licenses/lgpl-3.0.txt +165 -0
- repokit/licenses/mit.txt +21 -0
- repokit/licenses/mpl-2.0.txt +373 -0
- repokit/licenses/unlicense.txt +24 -0
- repokit/readme_blocks.py +112 -0
- repokit/remote_integration.py +627 -0
- repokit/repo_manager.py +2796 -0
- repokit/template_engine.py +391 -0
- repokit/templates/ai/claude/CLAUDE-nested.md.template +25 -0
- repokit/templates/ai/claude/CLAUDE.md.template +98 -0
- repokit/templates/ai/claude/instructions/step1_context_rebuilder.md +33 -0
- repokit/templates/ai/claude/instructions/step2_dev_workflow_process.md +67 -0
- repokit/templates/ai/claude/instructions/step3_context_bridge.md +52 -0
- repokit/templates/common/CHANGELOG.md.template +15 -0
- repokit/templates/common/CONTRIBUTING.md.template +31 -0
- repokit/templates/common/README.md.template +30 -0
- repokit/templates/common/gitattributes.template +17 -0
- repokit/templates/common/gitignore.template +141 -0
- repokit/templates/extras/FUNDING.yml.template +13 -0
- repokit/templates/extras/ROADMAP.md.template +17 -0
- repokit/templates/extras/dependabot.yml.template +22 -0
- repokit/templates/extras/one-offs-README.md.template +28 -0
- repokit/templates/extras/platform-support.md.template +11 -0
- repokit/templates/extras/pull_request_template.md.template +25 -0
- repokit/templates/extras/release-template.md.template +34 -0
- repokit/templates/extras/settings.json.template +11 -0
- repokit/templates/extras/stale.yml.template +14 -0
- repokit/templates/github/CODEOWNERS.template +11 -0
- repokit/templates/github/ISSUE_TEMPLATE/bug-report.md.template +29 -0
- repokit/templates/github/ISSUE_TEMPLATE/feature-request.md.template +19 -0
- repokit/templates/github/issues/notes.md.template +18 -0
- repokit/templates/github/issues/roadmap.md.template +15 -0
- repokit/templates/hooks/pre-commit.template +64 -0
- repokit/templates/layers/archetypes/any/claude-plugin/.claude-plugin/marketplace.json +20 -0
- repokit/templates/layers/archetypes/any/claude-plugin/plugin/.claude-plugin/plugin.json +15 -0
- repokit/templates/layers/archetypes/any/claude-plugin/plugin/README.md +28 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/.github/workflows/ci-c-cpp.yml +24 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/.repokit-common.toml.rkappend +4 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/.vscode/launch.json +29 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/.vscode/tasks.json +26 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/CMakeLists.txt +47 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/CMakePresets.json +23 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/cmake/GenerateStamp.cmake +45 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/src/main.cpp +27 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/tests/test_version.cpp +20 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/vcpkg.json +5 -0
- repokit/templates/layers/archetypes/c-cpp/minimal/.vscode/launch.json +27 -0
- repokit/templates/layers/archetypes/c-cpp/minimal/Makefile +10 -0
- repokit/templates/layers/archetypes/c-cpp/minimal/build.bat +7 -0
- repokit/templates/layers/archetypes/c-cpp/minimal/src/__RK_SNAKE__.c +26 -0
- repokit/templates/layers/archetypes/c-cpp/msbuild/.vscode/launch.json +17 -0
- repokit/templates/layers/archetypes/c-cpp/msbuild/.vscode/tasks.json +20 -0
- repokit/templates/layers/archetypes/c-cpp/msbuild/__RK_PROJECT__.sln +19 -0
- repokit/templates/layers/archetypes/c-cpp/msbuild/__RK_PROJECT__.vcxproj +41 -0
- repokit/templates/layers/archetypes/c-cpp/msbuild/src/main.cpp +26 -0
- repokit/templates/layers/archetypes/js-ts/claude-plugin/.repokit-common.toml +28 -0
- repokit/templates/layers/archetypes/js-ts/claude-plugin/.vscode/launch.json +34 -0
- repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/hooks/hooks.json +15 -0
- repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/hooks/scripts/session-start.mjs +23 -0
- repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/tests/manifest.test.js +30 -0
- repokit/templates/layers/archetypes/js-ts/node/.repokit-common.toml.rkappend +6 -0
- repokit/templates/layers/archetypes/js-ts/node/package.json +13 -0
- repokit/templates/layers/archetypes/js-ts/node-package/.github/workflows/release.yml +39 -0
- repokit/templates/layers/archetypes/js-ts/node-package/package.json +32 -0
- repokit/templates/layers/archetypes/js-ts/node-package/src/index.ts +13 -0
- repokit/templates/layers/archetypes/js-ts/node-package/tests/index.test.ts +11 -0
- repokit/templates/layers/archetypes/js-ts/script/.vscode/launch.json +34 -0
- repokit/templates/layers/archetypes/js-ts/script/__RK_PROJECT__.js +37 -0
- repokit/templates/layers/archetypes/js-ts/script/tests/version.test.js +26 -0
- repokit/templates/layers/archetypes/js-ts/ts/package.json +18 -0
- repokit/templates/layers/archetypes/js-ts/ts/tsconfig.json +15 -0
- repokit/templates/layers/archetypes/python/claude-plugin/.github/workflows/ci-python.yml +44 -0
- repokit/templates/layers/archetypes/python/claude-plugin/.repokit-common.toml +21 -0
- repokit/templates/layers/archetypes/python/claude-plugin/.vscode/launch.json +33 -0
- repokit/templates/layers/archetypes/python/claude-plugin/VERSION +1 -0
- repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/hooks.json +15 -0
- repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/scripts/run-hook.mjs +42 -0
- repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/scripts/session_start.py +31 -0
- repokit/templates/layers/archetypes/python/claude-plugin/plugin/tests/test_manifest.py +42 -0
- repokit/templates/layers/archetypes/python/pypi/.github/workflows/release.yml +63 -0
- repokit/templates/layers/archetypes/python/pypi/.vscode/launch.json +43 -0
- repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/__init__.py +5 -0
- repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/__main__.py +6 -0
- repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/_version.py +96 -0
- repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/cli.py +18 -0
- repokit/templates/layers/archetypes/python/pypi/pyproject.toml +71 -0
- repokit/templates/layers/archetypes/python/pypi/tests/conftest.py +11 -0
- repokit/templates/layers/archetypes/python/pypi/tests/test_version.py +50 -0
- repokit/templates/layers/archetypes/python/script/.github/workflows/ci-python.yml +43 -0
- repokit/templates/layers/archetypes/python/script/.repokit-common.toml +14 -0
- repokit/templates/layers/archetypes/python/script/.vscode/launch.json +33 -0
- repokit/templates/layers/archetypes/python/script/VERSION +1 -0
- repokit/templates/layers/archetypes/python/script/__RK_PROJECT__.py +40 -0
- repokit/templates/layers/archetypes/python/script/tests/test_version.py +25 -0
- repokit/templates/layers/archetypes/python/setuppy/requirements.txt +8 -0
- repokit/templates/layers/archetypes/python/setuppy/setup.py +23 -0
- repokit/templates/layers/languages/c-cpp/.gitignore.rkappend +20 -0
- repokit/templates/layers/languages/c-cpp/.repokit-common.toml +7 -0
- repokit/templates/layers/languages/c-cpp/include/__RK_SNAKE__/version.h +20 -0
- repokit/templates/layers/languages/c-cpp/src/app.rc +25 -0
- repokit/templates/layers/languages/js-ts/.editorconfig +15 -0
- repokit/templates/layers/languages/js-ts/.github/workflows/ci-js-ts.yml +41 -0
- repokit/templates/layers/languages/js-ts/.gitignore.rkappend +13 -0
- repokit/templates/layers/languages/js-ts/.repokit-common.toml +19 -0
- repokit/templates/layers/languages/js-ts/.vscode/launch.json +24 -0
- repokit/templates/layers/languages/js-ts/VERSION +1 -0
- repokit/templates/layers/languages/python/.github/workflows/ci-python.yml +76 -0
- repokit/templates/layers/languages/python/.gitignore.rkappend +15 -0
- repokit/templates/layers/languages/python/.vscode/launch.json +33 -0
- repokit/templates/layers/manifest.toml +302 -0
- repokit/templates/vault/CLAUDE.md +29 -0
- repokit/templates/vault/claude/_maps/MOC-project.md +15 -0
- repokit/templates/vault/claude/_oracle/backlinks.md +3 -0
- repokit/templates/vault/claude/_oracle/concepts.md +12 -0
- repokit/templates/vault/claude/_oracle/manifest.md +10 -0
- repokit/templates/vault/claude/_todo/ACTIVE-WORK.md +18 -0
- repokit/templates/vault/claude/_todo/OPEN-LOOPS.md +14 -0
- repokit/utils.py +316 -0
- repokit/vault.py +248 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
History Cleaner for RepoKit.
|
|
4
|
+
|
|
5
|
+
Provides safe, user-friendly tools to clean sensitive data from git history
|
|
6
|
+
using git filter-repo. Includes pre-built recipes for common scenarios and
|
|
7
|
+
comprehensive safety features.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import shutil
|
|
13
|
+
import logging
|
|
14
|
+
import subprocess
|
|
15
|
+
import tempfile
|
|
16
|
+
import json
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Dict, List, Optional, Tuple, Any, Callable
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from enum import Enum
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CleaningRecipe(Enum):
|
|
25
|
+
"""Pre-built recipes for common cleaning scenarios."""
|
|
26
|
+
PRE_OPEN_SOURCE = "pre-open-source"
|
|
27
|
+
REMOVE_SECRETS = "remove-secrets"
|
|
28
|
+
WINDOWS_SAFE = "windows-safe"
|
|
29
|
+
CUTOFF_DATE = "cutoff-date"
|
|
30
|
+
CUSTOM = "custom"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class CleaningConfig:
|
|
35
|
+
"""Configuration for a cleaning operation."""
|
|
36
|
+
recipe: CleaningRecipe
|
|
37
|
+
paths_to_remove: List[str] = None
|
|
38
|
+
patterns_to_remove: List[str] = None
|
|
39
|
+
cutoff_sha: Optional[str] = None
|
|
40
|
+
cutoff_date: Optional[str] = None
|
|
41
|
+
preserve_recent: bool = True
|
|
42
|
+
backup_location: Optional[str] = None
|
|
43
|
+
force: bool = False
|
|
44
|
+
dry_run: bool = False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class HistoryCleanerError(Exception):
|
|
48
|
+
"""Base exception for history cleaner errors."""
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class GitFilterRepoNotFound(HistoryCleanerError):
|
|
53
|
+
"""Raised when git filter-repo is not available."""
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class HistoryCleaner:
|
|
58
|
+
"""
|
|
59
|
+
Manages safe history cleaning operations using git filter-repo.
|
|
60
|
+
|
|
61
|
+
This class provides:
|
|
62
|
+
- Pre-built recipes for common scenarios
|
|
63
|
+
- Safety checks and backups
|
|
64
|
+
- Windows compatibility
|
|
65
|
+
- Dry-run previews
|
|
66
|
+
- Progress tracking
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
# Default paths to remove for pre-open-source recipe
|
|
70
|
+
DEFAULT_PRIVATE_PATHS = [
|
|
71
|
+
'private/',
|
|
72
|
+
'revisions/',
|
|
73
|
+
'convos/',
|
|
74
|
+
'logs/',
|
|
75
|
+
'credentials/',
|
|
76
|
+
'secrets/',
|
|
77
|
+
'.env',
|
|
78
|
+
'CLAUDE.md',
|
|
79
|
+
'.claude',
|
|
80
|
+
'__private__',
|
|
81
|
+
'*~', # Vim backup files
|
|
82
|
+
'*.*~', # Vim backup files (multiple extensions)
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
# Windows reserved names that cause issues
|
|
86
|
+
WINDOWS_RESERVED_NAMES = [
|
|
87
|
+
'CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4',
|
|
88
|
+
'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2',
|
|
89
|
+
'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
# Common secret patterns
|
|
93
|
+
SECRET_PATTERNS = [
|
|
94
|
+
r'["\']?api[_-]?key["\']?\s*[:=]\s*["\'][^"\']+["\']',
|
|
95
|
+
r'["\']?secret[_-]?key["\']?\s*[:=]\s*["\'][^"\']+["\']',
|
|
96
|
+
r'["\']?password["\']?\s*[:=]\s*["\'][^"\']+["\']',
|
|
97
|
+
r'["\']?token["\']?\s*[:=]\s*["\'][^"\']+["\']',
|
|
98
|
+
r'["\']?auth["\']?\s*[:=]\s*["\'][^"\']+["\']',
|
|
99
|
+
r'BEGIN (RSA|DSA|EC|OPENSSH) PRIVATE KEY',
|
|
100
|
+
r'ssh-rsa AAAA[0-9A-Za-z+/]+[=]{0,2}',
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
def __init__(self, repo_path: str = ".", verbose: int = 0, check_filter_repo: bool = True):
|
|
104
|
+
"""
|
|
105
|
+
Initialize the history cleaner.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
repo_path: Path to the git repository
|
|
109
|
+
verbose: Verbosity level
|
|
110
|
+
check_filter_repo: Whether to check for git filter-repo availability
|
|
111
|
+
"""
|
|
112
|
+
self.repo_path = os.path.abspath(repo_path)
|
|
113
|
+
self.verbose = verbose
|
|
114
|
+
self.logger = logging.getLogger("repokit.history_cleaner")
|
|
115
|
+
self._filter_repo_checked = False
|
|
116
|
+
|
|
117
|
+
# Only check if git filter-repo is available when needed
|
|
118
|
+
if check_filter_repo:
|
|
119
|
+
self._ensure_filter_repo_available()
|
|
120
|
+
|
|
121
|
+
# Ensure we're in a git repository if path is not current directory
|
|
122
|
+
if repo_path != "." and not os.path.exists(os.path.join(self.repo_path, '.git')):
|
|
123
|
+
raise HistoryCleanerError(f"Not a git repository: {self.repo_path}")
|
|
124
|
+
|
|
125
|
+
def _ensure_filter_repo_available(self):
|
|
126
|
+
"""Ensure git filter-repo is available, checking only once."""
|
|
127
|
+
if self._filter_repo_checked:
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
self._check_filter_repo_availability()
|
|
131
|
+
self._filter_repo_checked = True
|
|
132
|
+
|
|
133
|
+
def _check_filter_repo_availability(self):
|
|
134
|
+
"""Check if git filter-repo is installed and available."""
|
|
135
|
+
try:
|
|
136
|
+
result = subprocess.run(
|
|
137
|
+
['git', 'filter-repo', '--version'],
|
|
138
|
+
capture_output=True,
|
|
139
|
+
text=True,
|
|
140
|
+
check=False
|
|
141
|
+
)
|
|
142
|
+
if result.returncode != 0:
|
|
143
|
+
raise GitFilterRepoNotFound()
|
|
144
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
145
|
+
raise GitFilterRepoNotFound(
|
|
146
|
+
"git filter-repo is not installed. "
|
|
147
|
+
"Install it with: pip install git-filter-repo"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def run_command(self, cmd: List[str], check: bool = True) -> subprocess.CompletedProcess:
|
|
151
|
+
"""Run a command and return the result."""
|
|
152
|
+
if self.verbose >= 2:
|
|
153
|
+
self.logger.debug(f"Running: {' '.join(cmd)}")
|
|
154
|
+
|
|
155
|
+
result = subprocess.run(
|
|
156
|
+
cmd,
|
|
157
|
+
cwd=self.repo_path,
|
|
158
|
+
capture_output=True,
|
|
159
|
+
text=True,
|
|
160
|
+
check=False
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if check and result.returncode != 0:
|
|
164
|
+
raise HistoryCleanerError(
|
|
165
|
+
f"Command failed: {' '.join(cmd)}\n"
|
|
166
|
+
f"Error: {result.stderr}"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return result
|
|
170
|
+
|
|
171
|
+
def create_backup(self, backup_location: Optional[str] = None) -> str:
|
|
172
|
+
"""
|
|
173
|
+
Create a backup of the repository.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
backup_location: Where to create the backup
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Path to the backup
|
|
180
|
+
"""
|
|
181
|
+
if not backup_location:
|
|
182
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
183
|
+
backup_location = f"{self.repo_path}_backup_{timestamp}"
|
|
184
|
+
|
|
185
|
+
self.logger.info(f"Creating backup at: {backup_location}")
|
|
186
|
+
|
|
187
|
+
# Use git clone --mirror for a complete backup
|
|
188
|
+
self.run_command([
|
|
189
|
+
'git', 'clone', '--mirror',
|
|
190
|
+
self.repo_path, backup_location
|
|
191
|
+
])
|
|
192
|
+
|
|
193
|
+
return backup_location
|
|
194
|
+
|
|
195
|
+
def analyze_repository(self) -> Dict[str, Any]:
|
|
196
|
+
"""
|
|
197
|
+
Analyze repository for potential issues and cleaning candidates.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Analysis results
|
|
201
|
+
"""
|
|
202
|
+
analysis = {
|
|
203
|
+
'private_paths': [],
|
|
204
|
+
'large_files': [],
|
|
205
|
+
'windows_issues': [],
|
|
206
|
+
'potential_secrets': [],
|
|
207
|
+
'branches': [],
|
|
208
|
+
'total_commits': 0,
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
# Get all file paths in history
|
|
212
|
+
result = self.run_command([
|
|
213
|
+
'git', 'log', '--all', '--name-only', '--format='
|
|
214
|
+
])
|
|
215
|
+
|
|
216
|
+
all_paths = set(result.stdout.strip().split('\n'))
|
|
217
|
+
all_paths.discard('') # Remove empty strings
|
|
218
|
+
|
|
219
|
+
# Check for private paths
|
|
220
|
+
for path in all_paths:
|
|
221
|
+
for private_pattern in self.DEFAULT_PRIVATE_PATHS:
|
|
222
|
+
if path.startswith(private_pattern) or f"/{private_pattern}" in path:
|
|
223
|
+
analysis['private_paths'].append(path)
|
|
224
|
+
break
|
|
225
|
+
|
|
226
|
+
# Check for Windows reserved names
|
|
227
|
+
for path in all_paths:
|
|
228
|
+
filename = os.path.basename(path).upper()
|
|
229
|
+
base_name = filename.split('.')[0] if '.' in filename else filename
|
|
230
|
+
if base_name in self.WINDOWS_RESERVED_NAMES:
|
|
231
|
+
analysis['windows_issues'].append(path)
|
|
232
|
+
|
|
233
|
+
# Get branch list
|
|
234
|
+
result = self.run_command(['git', 'branch', '-a'])
|
|
235
|
+
analysis['branches'] = [
|
|
236
|
+
line.strip().lstrip('* ')
|
|
237
|
+
for line in result.stdout.strip().split('\n')
|
|
238
|
+
if line.strip()
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
# Count commits
|
|
242
|
+
result = self.run_command(['git', 'rev-list', '--all', '--count'])
|
|
243
|
+
analysis['total_commits'] = int(result.stdout.strip())
|
|
244
|
+
|
|
245
|
+
return analysis
|
|
246
|
+
|
|
247
|
+
def get_recipe_config(self, recipe: CleaningRecipe) -> CleaningConfig:
|
|
248
|
+
"""
|
|
249
|
+
Get default configuration for a recipe.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
recipe: The recipe to get config for
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
CleaningConfig for the recipe
|
|
256
|
+
"""
|
|
257
|
+
if recipe == CleaningRecipe.PRE_OPEN_SOURCE:
|
|
258
|
+
return CleaningConfig(
|
|
259
|
+
recipe=recipe,
|
|
260
|
+
paths_to_remove=self.DEFAULT_PRIVATE_PATHS.copy(),
|
|
261
|
+
preserve_recent=True
|
|
262
|
+
)
|
|
263
|
+
elif recipe == CleaningRecipe.WINDOWS_SAFE:
|
|
264
|
+
# Will be determined by analysis
|
|
265
|
+
return CleaningConfig(
|
|
266
|
+
recipe=recipe,
|
|
267
|
+
paths_to_remove=[],
|
|
268
|
+
preserve_recent=True
|
|
269
|
+
)
|
|
270
|
+
elif recipe == CleaningRecipe.REMOVE_SECRETS:
|
|
271
|
+
return CleaningConfig(
|
|
272
|
+
recipe=recipe,
|
|
273
|
+
patterns_to_remove=self.SECRET_PATTERNS.copy(),
|
|
274
|
+
preserve_recent=True
|
|
275
|
+
)
|
|
276
|
+
else:
|
|
277
|
+
return CleaningConfig(recipe=recipe)
|
|
278
|
+
|
|
279
|
+
def preview_cleaning(self, config: CleaningConfig) -> Dict[str, Any]:
|
|
280
|
+
"""
|
|
281
|
+
Preview what would be cleaned without making changes.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
config: Cleaning configuration
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Preview information
|
|
288
|
+
"""
|
|
289
|
+
preview = {
|
|
290
|
+
'recipe': config.recipe.value,
|
|
291
|
+
'paths_to_remove': config.paths_to_remove or [],
|
|
292
|
+
'patterns_to_remove': config.patterns_to_remove or [],
|
|
293
|
+
'estimated_impact': {},
|
|
294
|
+
'warnings': [],
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
# Analyze repository
|
|
298
|
+
analysis = self.analyze_repository()
|
|
299
|
+
|
|
300
|
+
# Calculate impact
|
|
301
|
+
if config.paths_to_remove:
|
|
302
|
+
affected_files = []
|
|
303
|
+
for path in analysis['private_paths']:
|
|
304
|
+
for remove_path in config.paths_to_remove:
|
|
305
|
+
if path.startswith(remove_path) or f"/{remove_path}" in path:
|
|
306
|
+
affected_files.append(path)
|
|
307
|
+
break
|
|
308
|
+
|
|
309
|
+
preview['estimated_impact']['files_to_remove'] = len(set(affected_files))
|
|
310
|
+
|
|
311
|
+
# Add warnings
|
|
312
|
+
if analysis['windows_issues'] and config.recipe != CleaningRecipe.WINDOWS_SAFE:
|
|
313
|
+
preview['warnings'].append(
|
|
314
|
+
f"Found {len(analysis['windows_issues'])} Windows reserved names. "
|
|
315
|
+
"Consider using --recipe windows-safe"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
if len(analysis['branches']) > 5:
|
|
319
|
+
preview['warnings'].append(
|
|
320
|
+
f"Repository has {len(analysis['branches'])} branches. "
|
|
321
|
+
"Cleaning will affect all branches."
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
return preview
|
|
325
|
+
|
|
326
|
+
def clean_history(self, config: CleaningConfig) -> bool:
|
|
327
|
+
"""
|
|
328
|
+
Clean repository history according to configuration.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
config: Cleaning configuration
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
True if successful
|
|
335
|
+
"""
|
|
336
|
+
# Ensure git filter-repo is available
|
|
337
|
+
self._ensure_filter_repo_available()
|
|
338
|
+
|
|
339
|
+
# Safety checks
|
|
340
|
+
if not config.force and not config.dry_run:
|
|
341
|
+
# Check for uncommitted changes
|
|
342
|
+
result = self.run_command(['git', 'status', '--porcelain'], check=False)
|
|
343
|
+
if result.stdout.strip():
|
|
344
|
+
raise HistoryCleanerError(
|
|
345
|
+
"Uncommitted changes detected. Commit or stash them first."
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# Confirm with user
|
|
349
|
+
print("\n⚠️ WARNING: This will rewrite repository history!")
|
|
350
|
+
print("This operation:")
|
|
351
|
+
print("- Cannot be undone (except by restoring from backup)")
|
|
352
|
+
print("- Will require force-push to update remotes")
|
|
353
|
+
print("- Will break existing clones")
|
|
354
|
+
|
|
355
|
+
response = input("\nContinue? [y/N]: ")
|
|
356
|
+
if response.lower() != 'y':
|
|
357
|
+
print("Operation cancelled.")
|
|
358
|
+
return False
|
|
359
|
+
|
|
360
|
+
# Create backup unless skipped
|
|
361
|
+
if not config.dry_run and config.backup_location != 'skip':
|
|
362
|
+
backup_path = self.create_backup(config.backup_location)
|
|
363
|
+
print(f"✓ Backup created at: {backup_path}")
|
|
364
|
+
|
|
365
|
+
# Execute the appropriate recipe
|
|
366
|
+
if config.recipe == CleaningRecipe.PRE_OPEN_SOURCE:
|
|
367
|
+
return self._clean_pre_open_source(config)
|
|
368
|
+
elif config.recipe == CleaningRecipe.WINDOWS_SAFE:
|
|
369
|
+
return self._clean_windows_safe(config)
|
|
370
|
+
elif config.recipe == CleaningRecipe.REMOVE_SECRETS:
|
|
371
|
+
return self._clean_remove_secrets(config)
|
|
372
|
+
elif config.recipe == CleaningRecipe.CUTOFF_DATE:
|
|
373
|
+
return self._clean_cutoff_date(config)
|
|
374
|
+
else:
|
|
375
|
+
raise HistoryCleanerError(f"Recipe not implemented: {config.recipe}")
|
|
376
|
+
|
|
377
|
+
def _clean_pre_open_source(self, config: CleaningConfig) -> bool:
|
|
378
|
+
"""Execute pre-open-source cleaning recipe."""
|
|
379
|
+
self.logger.info("Executing pre-open-source recipe")
|
|
380
|
+
|
|
381
|
+
# Build filter-repo command
|
|
382
|
+
cmd = ['git', 'filter-repo', '--force']
|
|
383
|
+
|
|
384
|
+
if config.dry_run:
|
|
385
|
+
cmd.append('--dry-run')
|
|
386
|
+
|
|
387
|
+
# Add paths to remove
|
|
388
|
+
for path in config.paths_to_remove:
|
|
389
|
+
cmd.extend(['--path', path, '--invert-paths'])
|
|
390
|
+
|
|
391
|
+
# Execute
|
|
392
|
+
result = self.run_command(cmd, check=False)
|
|
393
|
+
|
|
394
|
+
if result.returncode != 0:
|
|
395
|
+
self.logger.error(f"Cleaning failed: {result.stderr}")
|
|
396
|
+
return False
|
|
397
|
+
|
|
398
|
+
if not config.dry_run:
|
|
399
|
+
self.logger.info("Successfully cleaned repository")
|
|
400
|
+
print("\n✓ Repository cleaned successfully!")
|
|
401
|
+
print("\nNext steps:")
|
|
402
|
+
print("1. Review the changes with: git log --oneline")
|
|
403
|
+
print("2. Force push to remote: git push --force-with-lease origin <branch>")
|
|
404
|
+
print("3. Notify team members to re-clone the repository")
|
|
405
|
+
|
|
406
|
+
return True
|
|
407
|
+
|
|
408
|
+
def _clean_windows_safe(self, config: CleaningConfig) -> bool:
|
|
409
|
+
"""Execute Windows-safe cleaning recipe."""
|
|
410
|
+
self.logger.info("Executing Windows-safe recipe")
|
|
411
|
+
|
|
412
|
+
# Find Windows reserved names
|
|
413
|
+
analysis = self.analyze_repository()
|
|
414
|
+
windows_issues = analysis['windows_issues']
|
|
415
|
+
|
|
416
|
+
if not windows_issues:
|
|
417
|
+
print("No Windows reserved names found.")
|
|
418
|
+
return True
|
|
419
|
+
|
|
420
|
+
print(f"\nFound {len(windows_issues)} Windows reserved names:")
|
|
421
|
+
for path in windows_issues[:10]:
|
|
422
|
+
print(f" - {path}")
|
|
423
|
+
if len(windows_issues) > 10:
|
|
424
|
+
print(f" ... and {len(windows_issues) - 10} more")
|
|
425
|
+
|
|
426
|
+
# Build filter-repo command
|
|
427
|
+
cmd = ['git', 'filter-repo', '--force']
|
|
428
|
+
|
|
429
|
+
if config.dry_run:
|
|
430
|
+
cmd.append('--dry-run')
|
|
431
|
+
|
|
432
|
+
# Remove each problematic file
|
|
433
|
+
for path in windows_issues:
|
|
434
|
+
cmd.extend(['--path', path, '--invert-paths'])
|
|
435
|
+
|
|
436
|
+
# Execute
|
|
437
|
+
result = self.run_command(cmd, check=False)
|
|
438
|
+
|
|
439
|
+
return result.returncode == 0
|
|
440
|
+
|
|
441
|
+
def _clean_remove_secrets(self, config: CleaningConfig) -> bool:
|
|
442
|
+
"""Execute remove-secrets cleaning recipe."""
|
|
443
|
+
# TODO: Implement secret removal using regex patterns
|
|
444
|
+
raise NotImplementedError("Remove secrets recipe not yet implemented")
|
|
445
|
+
|
|
446
|
+
def _clean_cutoff_date(self, config: CleaningConfig) -> bool:
|
|
447
|
+
"""Execute cutoff-date cleaning recipe."""
|
|
448
|
+
# TODO: Implement cutoff date cleaning
|
|
449
|
+
raise NotImplementedError("Cutoff date recipe not yet implemented")
|