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.
Files changed (174) hide show
  1. git_repokit-0.9.10.dist-info/METADATA +422 -0
  2. git_repokit-0.9.10.dist-info/RECORD +174 -0
  3. git_repokit-0.9.10.dist-info/WHEEL +5 -0
  4. git_repokit-0.9.10.dist-info/entry_points.txt +2 -0
  5. git_repokit-0.9.10.dist-info/licenses/LICENSE +9 -0
  6. git_repokit-0.9.10.dist-info/top_level.txt +1 -0
  7. repokit/__init__.py +8 -0
  8. repokit/__main__.py +12 -0
  9. repokit/__version__.py +4 -0
  10. repokit/_vendor/__init__.py +0 -0
  11. repokit/_vendor/ailib/__init__.py +1 -0
  12. repokit/_vendor/ailib/analyzer.py +177 -0
  13. repokit/_vendor/ailib/backend.py +95 -0
  14. repokit/_vendor/ailib/cache.py +97 -0
  15. repokit/_vendor/ailib/parsers.py +52 -0
  16. repokit/_vendor/ailib/transports/__init__.py +16 -0
  17. repokit/_vendor/ailib/transports/cli.py +312 -0
  18. repokit/_vendor/ailib/transports/openai_compat.py +383 -0
  19. repokit/_vendor/ailib/transports/prompt_file.py +41 -0
  20. repokit/_vendor/ailib/types.py +224 -0
  21. repokit/auth_integration.py +244 -0
  22. repokit/badges.py +183 -0
  23. repokit/branch_utils.py +364 -0
  24. repokit/cli.py +2997 -0
  25. repokit/common_kit.py +202 -0
  26. repokit/config.py +686 -0
  27. repokit/defaults.py +220 -0
  28. repokit/directory_analyzer.py +1244 -0
  29. repokit/directory_profiles.py +423 -0
  30. repokit/git_config.py +370 -0
  31. repokit/github_create.py +142 -0
  32. repokit/github_setup.py +248 -0
  33. repokit/guardrails.py +707 -0
  34. repokit/history_cleaner.py +449 -0
  35. repokit/history_protection.py +434 -0
  36. repokit/hooks/pre-commit-guardrails +122 -0
  37. repokit/hooks/pre-commit-template +28 -0
  38. repokit/layers.py +510 -0
  39. repokit/licenses/0bsd.txt +14 -0
  40. repokit/licenses/LICENSES.md +27 -0
  41. repokit/licenses/__init__.py +142 -0
  42. repokit/licenses/agpl-3.0.txt +661 -0
  43. repokit/licenses/all-rights-reserved.txt +10 -0
  44. repokit/licenses/apache-2.0.txt +201 -0
  45. repokit/licenses/bsd-2-clause.txt +24 -0
  46. repokit/licenses/bsd-3-clause.txt +28 -0
  47. repokit/licenses/bsl-1.0.txt +23 -0
  48. repokit/licenses/cc-by-4.0.txt +395 -0
  49. repokit/licenses/cc-by-sa-4.0.txt +427 -0
  50. repokit/licenses/cc0-1.0.txt +121 -0
  51. repokit/licenses/epl-2.0.txt +277 -0
  52. repokit/licenses/gpl-2.0.txt +339 -0
  53. repokit/licenses/gpl-3.0.txt +674 -0
  54. repokit/licenses/isc.txt +15 -0
  55. repokit/licenses/lgpl-2.1.txt +504 -0
  56. repokit/licenses/lgpl-3.0.txt +165 -0
  57. repokit/licenses/mit.txt +21 -0
  58. repokit/licenses/mpl-2.0.txt +373 -0
  59. repokit/licenses/unlicense.txt +24 -0
  60. repokit/readme_blocks.py +112 -0
  61. repokit/remote_integration.py +627 -0
  62. repokit/repo_manager.py +2796 -0
  63. repokit/template_engine.py +391 -0
  64. repokit/templates/ai/claude/CLAUDE-nested.md.template +25 -0
  65. repokit/templates/ai/claude/CLAUDE.md.template +98 -0
  66. repokit/templates/ai/claude/instructions/step1_context_rebuilder.md +33 -0
  67. repokit/templates/ai/claude/instructions/step2_dev_workflow_process.md +67 -0
  68. repokit/templates/ai/claude/instructions/step3_context_bridge.md +52 -0
  69. repokit/templates/common/CHANGELOG.md.template +15 -0
  70. repokit/templates/common/CONTRIBUTING.md.template +31 -0
  71. repokit/templates/common/README.md.template +30 -0
  72. repokit/templates/common/gitattributes.template +17 -0
  73. repokit/templates/common/gitignore.template +141 -0
  74. repokit/templates/extras/FUNDING.yml.template +13 -0
  75. repokit/templates/extras/ROADMAP.md.template +17 -0
  76. repokit/templates/extras/dependabot.yml.template +22 -0
  77. repokit/templates/extras/one-offs-README.md.template +28 -0
  78. repokit/templates/extras/platform-support.md.template +11 -0
  79. repokit/templates/extras/pull_request_template.md.template +25 -0
  80. repokit/templates/extras/release-template.md.template +34 -0
  81. repokit/templates/extras/settings.json.template +11 -0
  82. repokit/templates/extras/stale.yml.template +14 -0
  83. repokit/templates/github/CODEOWNERS.template +11 -0
  84. repokit/templates/github/ISSUE_TEMPLATE/bug-report.md.template +29 -0
  85. repokit/templates/github/ISSUE_TEMPLATE/feature-request.md.template +19 -0
  86. repokit/templates/github/issues/notes.md.template +18 -0
  87. repokit/templates/github/issues/roadmap.md.template +15 -0
  88. repokit/templates/hooks/pre-commit.template +64 -0
  89. repokit/templates/layers/archetypes/any/claude-plugin/.claude-plugin/marketplace.json +20 -0
  90. repokit/templates/layers/archetypes/any/claude-plugin/plugin/.claude-plugin/plugin.json +15 -0
  91. repokit/templates/layers/archetypes/any/claude-plugin/plugin/README.md +28 -0
  92. repokit/templates/layers/archetypes/c-cpp/cmake-app/.github/workflows/ci-c-cpp.yml +24 -0
  93. repokit/templates/layers/archetypes/c-cpp/cmake-app/.repokit-common.toml.rkappend +4 -0
  94. repokit/templates/layers/archetypes/c-cpp/cmake-app/.vscode/launch.json +29 -0
  95. repokit/templates/layers/archetypes/c-cpp/cmake-app/.vscode/tasks.json +26 -0
  96. repokit/templates/layers/archetypes/c-cpp/cmake-app/CMakeLists.txt +47 -0
  97. repokit/templates/layers/archetypes/c-cpp/cmake-app/CMakePresets.json +23 -0
  98. repokit/templates/layers/archetypes/c-cpp/cmake-app/cmake/GenerateStamp.cmake +45 -0
  99. repokit/templates/layers/archetypes/c-cpp/cmake-app/src/main.cpp +27 -0
  100. repokit/templates/layers/archetypes/c-cpp/cmake-app/tests/test_version.cpp +20 -0
  101. repokit/templates/layers/archetypes/c-cpp/cmake-app/vcpkg.json +5 -0
  102. repokit/templates/layers/archetypes/c-cpp/minimal/.vscode/launch.json +27 -0
  103. repokit/templates/layers/archetypes/c-cpp/minimal/Makefile +10 -0
  104. repokit/templates/layers/archetypes/c-cpp/minimal/build.bat +7 -0
  105. repokit/templates/layers/archetypes/c-cpp/minimal/src/__RK_SNAKE__.c +26 -0
  106. repokit/templates/layers/archetypes/c-cpp/msbuild/.vscode/launch.json +17 -0
  107. repokit/templates/layers/archetypes/c-cpp/msbuild/.vscode/tasks.json +20 -0
  108. repokit/templates/layers/archetypes/c-cpp/msbuild/__RK_PROJECT__.sln +19 -0
  109. repokit/templates/layers/archetypes/c-cpp/msbuild/__RK_PROJECT__.vcxproj +41 -0
  110. repokit/templates/layers/archetypes/c-cpp/msbuild/src/main.cpp +26 -0
  111. repokit/templates/layers/archetypes/js-ts/claude-plugin/.repokit-common.toml +28 -0
  112. repokit/templates/layers/archetypes/js-ts/claude-plugin/.vscode/launch.json +34 -0
  113. repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/hooks/hooks.json +15 -0
  114. repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/hooks/scripts/session-start.mjs +23 -0
  115. repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/tests/manifest.test.js +30 -0
  116. repokit/templates/layers/archetypes/js-ts/node/.repokit-common.toml.rkappend +6 -0
  117. repokit/templates/layers/archetypes/js-ts/node/package.json +13 -0
  118. repokit/templates/layers/archetypes/js-ts/node-package/.github/workflows/release.yml +39 -0
  119. repokit/templates/layers/archetypes/js-ts/node-package/package.json +32 -0
  120. repokit/templates/layers/archetypes/js-ts/node-package/src/index.ts +13 -0
  121. repokit/templates/layers/archetypes/js-ts/node-package/tests/index.test.ts +11 -0
  122. repokit/templates/layers/archetypes/js-ts/script/.vscode/launch.json +34 -0
  123. repokit/templates/layers/archetypes/js-ts/script/__RK_PROJECT__.js +37 -0
  124. repokit/templates/layers/archetypes/js-ts/script/tests/version.test.js +26 -0
  125. repokit/templates/layers/archetypes/js-ts/ts/package.json +18 -0
  126. repokit/templates/layers/archetypes/js-ts/ts/tsconfig.json +15 -0
  127. repokit/templates/layers/archetypes/python/claude-plugin/.github/workflows/ci-python.yml +44 -0
  128. repokit/templates/layers/archetypes/python/claude-plugin/.repokit-common.toml +21 -0
  129. repokit/templates/layers/archetypes/python/claude-plugin/.vscode/launch.json +33 -0
  130. repokit/templates/layers/archetypes/python/claude-plugin/VERSION +1 -0
  131. repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/hooks.json +15 -0
  132. repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/scripts/run-hook.mjs +42 -0
  133. repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/scripts/session_start.py +31 -0
  134. repokit/templates/layers/archetypes/python/claude-plugin/plugin/tests/test_manifest.py +42 -0
  135. repokit/templates/layers/archetypes/python/pypi/.github/workflows/release.yml +63 -0
  136. repokit/templates/layers/archetypes/python/pypi/.vscode/launch.json +43 -0
  137. repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/__init__.py +5 -0
  138. repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/__main__.py +6 -0
  139. repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/_version.py +96 -0
  140. repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/cli.py +18 -0
  141. repokit/templates/layers/archetypes/python/pypi/pyproject.toml +71 -0
  142. repokit/templates/layers/archetypes/python/pypi/tests/conftest.py +11 -0
  143. repokit/templates/layers/archetypes/python/pypi/tests/test_version.py +50 -0
  144. repokit/templates/layers/archetypes/python/script/.github/workflows/ci-python.yml +43 -0
  145. repokit/templates/layers/archetypes/python/script/.repokit-common.toml +14 -0
  146. repokit/templates/layers/archetypes/python/script/.vscode/launch.json +33 -0
  147. repokit/templates/layers/archetypes/python/script/VERSION +1 -0
  148. repokit/templates/layers/archetypes/python/script/__RK_PROJECT__.py +40 -0
  149. repokit/templates/layers/archetypes/python/script/tests/test_version.py +25 -0
  150. repokit/templates/layers/archetypes/python/setuppy/requirements.txt +8 -0
  151. repokit/templates/layers/archetypes/python/setuppy/setup.py +23 -0
  152. repokit/templates/layers/languages/c-cpp/.gitignore.rkappend +20 -0
  153. repokit/templates/layers/languages/c-cpp/.repokit-common.toml +7 -0
  154. repokit/templates/layers/languages/c-cpp/include/__RK_SNAKE__/version.h +20 -0
  155. repokit/templates/layers/languages/c-cpp/src/app.rc +25 -0
  156. repokit/templates/layers/languages/js-ts/.editorconfig +15 -0
  157. repokit/templates/layers/languages/js-ts/.github/workflows/ci-js-ts.yml +41 -0
  158. repokit/templates/layers/languages/js-ts/.gitignore.rkappend +13 -0
  159. repokit/templates/layers/languages/js-ts/.repokit-common.toml +19 -0
  160. repokit/templates/layers/languages/js-ts/.vscode/launch.json +24 -0
  161. repokit/templates/layers/languages/js-ts/VERSION +1 -0
  162. repokit/templates/layers/languages/python/.github/workflows/ci-python.yml +76 -0
  163. repokit/templates/layers/languages/python/.gitignore.rkappend +15 -0
  164. repokit/templates/layers/languages/python/.vscode/launch.json +33 -0
  165. repokit/templates/layers/manifest.toml +302 -0
  166. repokit/templates/vault/CLAUDE.md +29 -0
  167. repokit/templates/vault/claude/_maps/MOC-project.md +15 -0
  168. repokit/templates/vault/claude/_oracle/backlinks.md +3 -0
  169. repokit/templates/vault/claude/_oracle/concepts.md +12 -0
  170. repokit/templates/vault/claude/_oracle/manifest.md +10 -0
  171. repokit/templates/vault/claude/_todo/ACTIVE-WORK.md +18 -0
  172. repokit/templates/vault/claude/_todo/OPEN-LOOPS.md +14 -0
  173. repokit/utils.py +316 -0
  174. 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")