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,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
|