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,434 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ History Protection for RepoKit.
4
+
5
+ Provides tools to protect sensitive commit history when merging development
6
+ branches, preventing detailed implementation history from leaking to public
7
+ branches.
8
+ """
9
+
10
+ import os
11
+ import re
12
+ import logging
13
+ import subprocess
14
+ from typing import Dict, List, Optional, Tuple, Any
15
+ from dataclasses import dataclass
16
+ from enum import Enum
17
+
18
+
19
+ class MergeAction(Enum):
20
+ """Actions for handling branch merges."""
21
+ SQUASH = "squash"
22
+ PRESERVE = "preserve"
23
+ INTERACTIVE = "interactive"
24
+ SMART_SQUASH = "smart_squash"
25
+
26
+
27
+ @dataclass
28
+ class BranchRule:
29
+ """Rule for handling a branch pattern."""
30
+ pattern: str
31
+ action: MergeAction
32
+ auto: bool = False
33
+ preserve_last: int = 0
34
+ message_template: Optional[str] = None
35
+
36
+
37
+ @dataclass
38
+ class CommitInfo:
39
+ """Information about a commit."""
40
+ hash: str
41
+ author: str
42
+ date: str
43
+ message: str
44
+ files_changed: List[str]
45
+
46
+
47
+ class HistoryProtectionManager:
48
+ """
49
+ Manages history protection when merging development branches.
50
+
51
+ This class provides functionality to:
52
+ - Categorize branches based on patterns
53
+ - Squash or preserve commits based on rules
54
+ - Sanitize commit messages
55
+ - Generate meaningful merge commit messages
56
+ """
57
+
58
+ # Default sensitive patterns to remove from commit messages
59
+ DEFAULT_SENSITIVE_PATTERNS = [
60
+ r'private/',
61
+ r'secret',
62
+ r'password',
63
+ r'token',
64
+ r'TODO:\s*hack',
65
+ r'FIXME:\s*security',
66
+ r'XXX:',
67
+ r'HACK:',
68
+ r'TEMP:',
69
+ r'DO NOT COMMIT',
70
+ r'@nocommit',
71
+ ]
72
+
73
+ # Default branch rules
74
+ DEFAULT_BRANCH_RULES = {
75
+ 'prototype/*': BranchRule('prototype/*', MergeAction.SQUASH, auto=True),
76
+ 'experiment/*': BranchRule('experiment/*', MergeAction.SQUASH, auto=True),
77
+ 'spike/*': BranchRule('spike/*', MergeAction.SQUASH, auto=True),
78
+ 'feature/*': BranchRule('feature/*', MergeAction.INTERACTIVE, auto=False),
79
+ 'bugfix/*': BranchRule('bugfix/*', MergeAction.PRESERVE, auto=True),
80
+ 'hotfix/*': BranchRule('hotfix/*', MergeAction.PRESERVE, auto=True),
81
+ }
82
+
83
+ def __init__(self, repo_path: str = ".", config: Optional[Dict[str, Any]] = None, verbose: int = 0):
84
+ """
85
+ Initialize the history protection manager.
86
+
87
+ Args:
88
+ repo_path: Path to the git repository
89
+ config: Configuration dictionary
90
+ verbose: Verbosity level
91
+ """
92
+ self.repo_path = os.path.abspath(repo_path)
93
+ self.verbose = verbose
94
+ self.logger = logging.getLogger("repokit.history_protection")
95
+
96
+ # Load configuration
97
+ self.config = config or {}
98
+ self.history_config = self.config.get('history_protection', {})
99
+
100
+ # Set up branch rules
101
+ self.branch_rules = self._load_branch_rules()
102
+
103
+ # Set up sensitive patterns
104
+ self.sensitive_patterns = self._load_sensitive_patterns()
105
+
106
+ def _load_branch_rules(self) -> Dict[str, BranchRule]:
107
+ """Load branch rules from configuration."""
108
+ rules = {}
109
+
110
+ # Start with defaults
111
+ for pattern, rule in self.DEFAULT_BRANCH_RULES.items():
112
+ rules[pattern] = rule
113
+
114
+ # Override with config
115
+ config_rules = self.history_config.get('branch_rules', {})
116
+ for pattern, rule_config in config_rules.items():
117
+ action = MergeAction(rule_config.get('action', 'interactive'))
118
+ auto = rule_config.get('auto', False)
119
+ preserve_last = rule_config.get('preserve_last', 0)
120
+ message_template = rule_config.get('message_template')
121
+
122
+ rules[pattern] = BranchRule(
123
+ pattern=pattern,
124
+ action=action,
125
+ auto=auto,
126
+ preserve_last=preserve_last,
127
+ message_template=message_template
128
+ )
129
+
130
+ return rules
131
+
132
+ def _load_sensitive_patterns(self) -> List[re.Pattern]:
133
+ """Load and compile sensitive patterns."""
134
+ patterns = []
135
+
136
+ # Start with defaults
137
+ pattern_strings = self.DEFAULT_SENSITIVE_PATTERNS.copy()
138
+
139
+ # Add config patterns
140
+ config_patterns = self.history_config.get('sensitive_patterns', [])
141
+ pattern_strings.extend(config_patterns)
142
+
143
+ # Compile patterns
144
+ for pattern in pattern_strings:
145
+ try:
146
+ patterns.append(re.compile(pattern, re.IGNORECASE))
147
+ except re.error as e:
148
+ self.logger.warning(f"Invalid regex pattern '{pattern}': {e}")
149
+
150
+ return patterns
151
+
152
+ def get_branch_rule(self, branch_name: str) -> BranchRule:
153
+ """
154
+ Get the rule for a given branch name.
155
+
156
+ Args:
157
+ branch_name: Name of the branch
158
+
159
+ Returns:
160
+ BranchRule for the branch
161
+ """
162
+ # Check each pattern
163
+ for pattern, rule in self.branch_rules.items():
164
+ # Convert glob pattern to regex
165
+ regex_pattern = re.escape(pattern).replace('\\*', '.*')
166
+ if re.match(f'^{regex_pattern}$', branch_name):
167
+ return rule
168
+
169
+ # Default rule
170
+ return BranchRule(
171
+ pattern='*',
172
+ action=MergeAction.INTERACTIVE,
173
+ auto=False
174
+ )
175
+
176
+ def run_git(self, args: List[str], check: bool = True) -> Optional[str]:
177
+ """Run a git command and return output."""
178
+ cmd = ['git'] + args
179
+
180
+ if self.verbose >= 2:
181
+ self.logger.debug(f"Running: {' '.join(cmd)}")
182
+
183
+ try:
184
+ result = subprocess.run(
185
+ cmd,
186
+ cwd=self.repo_path,
187
+ check=check,
188
+ capture_output=True,
189
+ text=True
190
+ )
191
+ return result.stdout.strip()
192
+ except subprocess.CalledProcessError as e:
193
+ if check:
194
+ raise
195
+ return None
196
+
197
+ def get_branch_commits(self, branch: str, base_branch: str = "HEAD") -> List[CommitInfo]:
198
+ """
199
+ Get all commits in a branch that aren't in the base branch.
200
+
201
+ Args:
202
+ branch: Branch to get commits from
203
+ base_branch: Base branch to compare against
204
+
205
+ Returns:
206
+ List of CommitInfo objects
207
+ """
208
+ # Get commit hashes
209
+ commit_range = f"{base_branch}..{branch}"
210
+ hashes = self.run_git(['rev-list', '--reverse', commit_range])
211
+
212
+ if not hashes:
213
+ return []
214
+
215
+ commits = []
216
+ for hash in hashes.split('\n'):
217
+ if not hash:
218
+ continue
219
+
220
+ # Get commit info
221
+ info = self.run_git([
222
+ 'show',
223
+ '--no-patch',
224
+ '--format=%an|%ad|%s',
225
+ '--date=short',
226
+ hash
227
+ ])
228
+
229
+ if info:
230
+ author, date, message = info.split('|', 2)
231
+
232
+ # Get files changed
233
+ files = self.run_git([
234
+ 'diff-tree',
235
+ '--no-commit-id',
236
+ '--name-only',
237
+ '-r',
238
+ hash
239
+ ])
240
+
241
+ files_list = files.split('\n') if files else []
242
+
243
+ commits.append(CommitInfo(
244
+ hash=hash,
245
+ author=author,
246
+ date=date,
247
+ message=message,
248
+ files_changed=files_list
249
+ ))
250
+
251
+ return commits
252
+
253
+ def sanitize_message(self, message: str) -> str:
254
+ """
255
+ Remove sensitive patterns from a commit message.
256
+
257
+ Args:
258
+ message: Original message
259
+
260
+ Returns:
261
+ Sanitized message
262
+ """
263
+ sanitized = message
264
+
265
+ for pattern in self.sensitive_patterns:
266
+ sanitized = pattern.sub('[REDACTED]', sanitized)
267
+
268
+ return sanitized
269
+
270
+ def generate_squash_message(self, commits: List[CommitInfo], branch_name: str) -> str:
271
+ """
272
+ Generate a meaningful squash commit message from multiple commits.
273
+
274
+ Args:
275
+ commits: List of commits to squash
276
+ branch_name: Name of the branch being merged
277
+
278
+ Returns:
279
+ Generated commit message
280
+ """
281
+ if not commits:
282
+ return f"Merge {branch_name}"
283
+
284
+ # Extract key information
285
+ authors = list(set(c.author for c in commits))
286
+ file_changes = {}
287
+ key_messages = []
288
+
289
+ for commit in commits:
290
+ # Track file changes
291
+ for file in commit.files_changed:
292
+ if file not in file_changes:
293
+ file_changes[file] = 0
294
+ file_changes[file] += 1
295
+
296
+ # Extract key messages (skip merge commits and trivial ones)
297
+ msg = commit.message.lower()
298
+ if not any(skip in msg for skip in ['merge', 'wip', 'temp', 'fix typo']):
299
+ sanitized = self.sanitize_message(commit.message)
300
+ if sanitized and sanitized != '[REDACTED]':
301
+ key_messages.append(sanitized)
302
+
303
+ # Build the message
304
+ lines = []
305
+
306
+ # Summary line
307
+ feature_name = branch_name.split('/')[-1].replace('-', ' ').replace('_', ' ')
308
+ lines.append(f"feat: {feature_name}")
309
+ lines.append("")
310
+
311
+ # Key changes
312
+ if key_messages:
313
+ lines.append("Key changes:")
314
+ # Deduplicate and take top 5
315
+ seen = set()
316
+ for msg in key_messages[:5]:
317
+ if msg not in seen:
318
+ lines.append(f"- {msg}")
319
+ seen.add(msg)
320
+ lines.append("")
321
+
322
+ # Statistics
323
+ lines.append(f"Squashed {len(commits)} commits from {branch_name}")
324
+ lines.append(f"Authors: {', '.join(authors[:3])}" + (" and others" if len(authors) > 3 else ""))
325
+ lines.append(f"Files changed: {len(file_changes)}")
326
+
327
+ return '\n'.join(lines)
328
+
329
+ def preview_squash(self, branch: str, base_branch: str = "HEAD") -> Tuple[List[CommitInfo], str]:
330
+ """
331
+ Preview what would happen if we squashed a branch.
332
+
333
+ Args:
334
+ branch: Branch to squash
335
+ base_branch: Base branch to merge into
336
+
337
+ Returns:
338
+ Tuple of (commits that would be squashed, generated message)
339
+ """
340
+ commits = self.get_branch_commits(branch, base_branch)
341
+ message = self.generate_squash_message(commits, branch)
342
+
343
+ return commits, message
344
+
345
+ def safe_merge_dev(
346
+ self,
347
+ branch: str,
348
+ target_branch: Optional[str] = None,
349
+ squash: Optional[bool] = None,
350
+ message: Optional[str] = None,
351
+ preserve_last: Optional[int] = None,
352
+ dry_run: bool = False
353
+ ) -> bool:
354
+ """
355
+ Safely merge a development branch with history protection.
356
+
357
+ Args:
358
+ branch: Branch to merge
359
+ target_branch: Target branch (default: current branch)
360
+ squash: Whether to squash (None = use rules)
361
+ message: Custom merge message
362
+ preserve_last: Number of recent commits to preserve
363
+ dry_run: Preview without merging
364
+
365
+ Returns:
366
+ True if successful
367
+ """
368
+ # Get branch rule
369
+ rule = self.get_branch_rule(branch)
370
+
371
+ # Determine action
372
+ if squash is not None:
373
+ action = MergeAction.SQUASH if squash else MergeAction.PRESERVE
374
+ else:
375
+ action = rule.action
376
+
377
+ # Get commits
378
+ commits = self.get_branch_commits(branch, target_branch or "HEAD")
379
+
380
+ if not commits:
381
+ self.logger.info(f"No commits to merge from {branch}")
382
+ return True
383
+
384
+ # Handle based on action
385
+ if action == MergeAction.PRESERVE:
386
+ self.logger.info(f"Preserving all {len(commits)} commits from {branch}")
387
+ if not dry_run:
388
+ self.run_git(['merge', branch, '--no-ff'])
389
+
390
+ elif action == MergeAction.SQUASH:
391
+ # Generate message if not provided
392
+ if not message:
393
+ message = self.generate_squash_message(commits, branch)
394
+
395
+ self.logger.info(f"Squashing {len(commits)} commits from {branch}")
396
+ if self.verbose >= 1:
397
+ print(f"\nGenerated commit message:\n{message}\n")
398
+
399
+ if not dry_run:
400
+ # Perform squash merge
401
+ self.run_git(['merge', branch, '--squash'])
402
+ self.run_git(['commit', '-m', message])
403
+
404
+ elif action == MergeAction.INTERACTIVE:
405
+ # Show preview and ask user
406
+ preview_message = self.generate_squash_message(commits, branch)
407
+
408
+ print(f"\nBranch: {branch}")
409
+ print(f"Commits: {len(commits)}")
410
+ print(f"Rule: {rule.pattern} ({action.value})")
411
+ print(f"\nCommits to be merged:")
412
+ for commit in commits[-10:]: # Show last 10
413
+ print(f" {commit.hash[:8]} {commit.message}")
414
+ if len(commits) > 10:
415
+ print(f" ... and {len(commits) - 10} more")
416
+
417
+ print(f"\nSuggested squash message:")
418
+ print(preview_message)
419
+
420
+ if not dry_run:
421
+ response = input("\nSquash these commits? [Y/n/e(dit)]: ").lower()
422
+ if response == 'n':
423
+ print("Performing regular merge...")
424
+ self.run_git(['merge', branch, '--no-ff'])
425
+ elif response == 'e':
426
+ # TODO: Implement message editing
427
+ print("Message editing not yet implemented")
428
+ return False
429
+ else:
430
+ print("Performing squash merge...")
431
+ self.run_git(['merge', branch, '--squash'])
432
+ self.run_git(['commit', '-m', message or preview_message])
433
+
434
+ return True
@@ -0,0 +1,122 @@
1
+ #!/bin/sh
2
+ # RepoKit Cross-Environment Pre-Commit Hook
3
+ # This wrapper script detects the environment and calls Python appropriately
4
+
5
+ # Function to detect Python command
6
+ find_python() {
7
+ # Try to detect if we're on Windows
8
+ if [ -n "$WINDIR" ] || [ "$OSTYPE" = "msys" ] || [ "$OSTYPE" = "cygwin" ]; then
9
+ # Windows environment - try various Python commands
10
+ if command -v py >/dev/null 2>&1; then
11
+ echo "py"
12
+ elif command -v python >/dev/null 2>&1; then
13
+ echo "python"
14
+ elif command -v python3 >/dev/null 2>&1; then
15
+ echo "python3"
16
+ else
17
+ echo ""
18
+ fi
19
+ else
20
+ # Unix/Linux/WSL environment
21
+ if command -v python3 >/dev/null 2>&1; then
22
+ echo "python3"
23
+ elif command -v python >/dev/null 2>&1; then
24
+ echo "python"
25
+ else
26
+ echo ""
27
+ fi
28
+ fi
29
+ }
30
+
31
+ # Find Python command
32
+ PYTHON_CMD=$(find_python)
33
+
34
+ # If Python not found, exit gracefully but block commit
35
+ if [ -z "$PYTHON_CMD" ]; then
36
+ echo "⚠️ Python not found - cannot run RepoKit pre-commit checks"
37
+ echo " To enable: Install Python and ensure it's in PATH"
38
+ echo " To bypass: use 'git commit --no-verify' (not recommended)"
39
+ exit 1
40
+ fi
41
+
42
+ # Execute the Python script. Its messages contain emoji; when output is not a
43
+ # UTF-8 console (Windows, or piped from a GUI client) Python would raise
44
+ # UnicodeEncodeError and block the commit, so force UTF-8 output.
45
+ PYTHONIOENCODING=utf-8 $PYTHON_CMD - << 'EOF'
46
+ """
47
+ RepoKit Pre-Commit Hook with Private Content Guardrails.
48
+
49
+ This hook prevents accidental commits of private content to public branches.
50
+ """
51
+
52
+ import sys
53
+ import os
54
+ import subprocess
55
+
56
+ # Add parent directory to Python path for imports
57
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
58
+
59
+ try:
60
+ from repokit.branch_utils import BranchContext
61
+ except ImportError:
62
+ print("ERROR: Could not import RepoKit branch utilities.")
63
+ print("Make sure RepoKit is installed: pip install -e .")
64
+ sys.exit(1)
65
+
66
+
67
+ def main():
68
+ """Main pre-commit hook logic."""
69
+ # Get branch context
70
+ context = BranchContext()
71
+
72
+ # Show current branch info
73
+ print(f"🔍 RepoKit Pre-Commit Check")
74
+ print(f" Branch: {context.current_branch} ({context.branch_type})")
75
+
76
+ # Validate commit
77
+ is_valid, errors = context.validate_commit()
78
+
79
+ if not is_valid:
80
+ print("\n❌ COMMIT BLOCKED - Private Content Protection\n")
81
+ for error in errors:
82
+ print(error)
83
+ return 1
84
+
85
+ # Additional checks for public branches
86
+ if context.is_public_branch():
87
+ staged_files = context.get_staged_files()
88
+
89
+ # Warn about sensitive patterns even if not explicitly private
90
+ warnings = []
91
+ for file in staged_files:
92
+ if any(pattern in file.lower() for pattern in ['password', 'secret', 'token', 'credential']):
93
+ warnings.append(f" ⚠️ {file} - filename suggests sensitive content")
94
+
95
+ if warnings:
96
+ print("\n⚠️ WARNING - Potentially Sensitive Files:")
97
+ for warning in warnings:
98
+ print(warning)
99
+ print("\nMake sure these files don't contain actual secrets!")
100
+ print("Press Ctrl+C to cancel, or wait 3 seconds to continue...")
101
+
102
+ try:
103
+ import time
104
+ time.sleep(3)
105
+ except KeyboardInterrupt:
106
+ print("\n\nCommit cancelled by user.")
107
+ return 1
108
+
109
+ print("\n✅ Pre-commit checks passed!")
110
+
111
+ # Check if we should set up branch excludes
112
+ exclude_file = os.path.join('.git', 'info', 'exclude')
113
+ if not os.path.exists(exclude_file) or os.path.getsize(exclude_file) == 0:
114
+ print("\n📝 Setting up branch-specific excludes...")
115
+ context.setup_branch_excludes()
116
+
117
+ return 0
118
+
119
+
120
+ if __name__ == "__main__":
121
+ sys.exit(main())
122
+ EOF
@@ -0,0 +1,28 @@
1
+ #!/bin/sh
2
+ # Pre-commit hook to prevent committing private content
3
+
4
+ # Check for private directories
5
+ for file in $(git diff --cached --name-only); do
6
+ if echo "$file" | grep -q -E "^private/|^convos/|^logs/|__private__"; then
7
+ echo "ERROR: Attempting to commit private content: $file"
8
+ echo "Private content should not be committed to the repository."
9
+ exit 1
10
+ fi
11
+ done
12
+
13
+ # Check specific private directories from config
14
+ PRIVATE_PATTERNS=(
15
+ $private_dirs_patterns
16
+ )
17
+
18
+ for file in $(git diff --cached --name-only); do
19
+ for pattern in "${PRIVATE_PATTERNS[@]}"; do
20
+ if echo "$file" | grep -q -E "$pattern"; then
21
+ echo "ERROR: Attempting to commit private content: $file"
22
+ echo "This file matches a protected pattern: $pattern"
23
+ exit 1
24
+ fi
25
+ done
26
+ done
27
+
28
+ exit 0