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
repokit/cli.py
ADDED
|
@@ -0,0 +1,2997 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
RepoKit: Repository Template Generator
|
|
4
|
+
|
|
5
|
+
Command-line interface for creating standardized Git repositories.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import json
|
|
11
|
+
import argparse
|
|
12
|
+
import logging
|
|
13
|
+
import subprocess
|
|
14
|
+
from typing import Dict, Any, Optional
|
|
15
|
+
|
|
16
|
+
from .config import ConfigManager
|
|
17
|
+
from .defaults import BRANCH_STRATEGIES, DEFAULT_ADOPT_STRATEGY, PRIVATE_BRANCH_STRATEGIES
|
|
18
|
+
from .repo_manager import RepoManager
|
|
19
|
+
from .template_engine import TemplateEngine
|
|
20
|
+
from .directory_analyzer import (
|
|
21
|
+
analyze_directory,
|
|
22
|
+
plan_migration,
|
|
23
|
+
migrate_directory,
|
|
24
|
+
analyze_project,
|
|
25
|
+
adopt_project,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# Set UTF-8 encoding for subprocess on Windows to prevent Unicode errors
|
|
29
|
+
if sys.platform == 'win32':
|
|
30
|
+
import locale
|
|
31
|
+
# Force UTF-8 for subprocess communication
|
|
32
|
+
os.environ['PYTHONIOENCODING'] = 'utf-8'
|
|
33
|
+
os.environ['PYTHONUTF8'] = '1'
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_arguments() -> argparse.Namespace:
|
|
37
|
+
"""
|
|
38
|
+
Parse command-line arguments.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Parsed arguments
|
|
42
|
+
"""
|
|
43
|
+
parser = argparse.ArgumentParser(
|
|
44
|
+
prog="repokit",
|
|
45
|
+
description="RepoKit - A Git repository template generator with standardized structures",
|
|
46
|
+
epilog="""
|
|
47
|
+
Examples:
|
|
48
|
+
# Create a new Python project with standard structure
|
|
49
|
+
repokit create myproject --language python --dir-profile standard
|
|
50
|
+
|
|
51
|
+
# Adopt an existing project with RepoKit structure
|
|
52
|
+
repokit adopt ./existing-project --strategy safe
|
|
53
|
+
|
|
54
|
+
# Create with custom branch strategy
|
|
55
|
+
repokit create webapp --branch-strategy gitflow --publish-to github
|
|
56
|
+
|
|
57
|
+
For detailed help on any command, use: repokit <command> --help
|
|
58
|
+
""",
|
|
59
|
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Version argument
|
|
63
|
+
from . import __version__
|
|
64
|
+
parser.add_argument("--version", action="version", version=f"RepoKit {__version__}")
|
|
65
|
+
|
|
66
|
+
# Global options that apply to all commands
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--config", "-c",
|
|
69
|
+
help="Path to configuration file (JSON)",
|
|
70
|
+
metavar="FILE"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"--save-config",
|
|
75
|
+
help="Save the final configuration to the specified file",
|
|
76
|
+
metavar="FILE"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Verbosity control
|
|
80
|
+
verbosity_group = parser.add_mutually_exclusive_group()
|
|
81
|
+
verbosity_group.add_argument(
|
|
82
|
+
"--verbose",
|
|
83
|
+
"-v",
|
|
84
|
+
action="count",
|
|
85
|
+
default=0,
|
|
86
|
+
help="Increase verbosity (-v for INFO, -vv for DEBUG, -vvv for detailed DEBUG)"
|
|
87
|
+
)
|
|
88
|
+
verbosity_group.add_argument(
|
|
89
|
+
"--quiet", "-q", action="store_true", help="Suppress all output except errors"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Create subparsers for commands
|
|
93
|
+
subparsers = parser.add_subparsers(
|
|
94
|
+
title="Commands",
|
|
95
|
+
description="Available commands",
|
|
96
|
+
dest="command",
|
|
97
|
+
help="Use 'repokit <command> --help' for command-specific help",
|
|
98
|
+
required=True
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# CREATE command
|
|
102
|
+
create_parser = subparsers.add_parser(
|
|
103
|
+
"create",
|
|
104
|
+
help="Create a new repository with RepoKit structure",
|
|
105
|
+
description="""
|
|
106
|
+
Create a new Git repository with standardized structure, branches, and templates.
|
|
107
|
+
|
|
108
|
+
This command initializes a complete project structure including:
|
|
109
|
+
- Directory hierarchy based on profiles or custom specification
|
|
110
|
+
- Git setup for the chosen branch strategy
|
|
111
|
+
- Template files for documentation, CI/CD, and language-specific needs
|
|
112
|
+
- Pre-commit hooks for private content protection
|
|
113
|
+
- Optional remote repository creation and pushing
|
|
114
|
+
|
|
115
|
+
Directory Profiles:
|
|
116
|
+
minimal - Basic structure: src, tests, docs
|
|
117
|
+
standard - Common structure with config, logs, private directories
|
|
118
|
+
complete - Full structure including examples, assets, resources
|
|
119
|
+
|
|
120
|
+
Branch Strategies:
|
|
121
|
+
main-only - one branch, main; the repository is <name>/ itself and
|
|
122
|
+
private/ stays out of git
|
|
123
|
+
|
|
124
|
+
Private-branch model (work on a local-only 'private' branch, move finished
|
|
125
|
+
work to the public branches; repository in <name>/local/, main checked out
|
|
126
|
+
in <name>/github/):
|
|
127
|
+
standard - private, main, dev, staging, test, live
|
|
128
|
+
simple - private, main, dev
|
|
129
|
+
gitflow - private, main, develop, release, hotfix
|
|
130
|
+
github-flow - private, main
|
|
131
|
+
minimal - private, main (no worktrees)
|
|
132
|
+
""",
|
|
133
|
+
epilog="""
|
|
134
|
+
Examples:
|
|
135
|
+
# Basic Python project
|
|
136
|
+
repokit create myapp --language python
|
|
137
|
+
|
|
138
|
+
# Web project with standard profile
|
|
139
|
+
repokit create webapp --dir-profile standard --language javascript
|
|
140
|
+
|
|
141
|
+
# Enterprise project with custom directories
|
|
142
|
+
repokit create enterprise-app --dir-profile complete --directories "analytics,ml-models"
|
|
143
|
+
|
|
144
|
+
# Create and publish to GitHub
|
|
145
|
+
repokit create myproject --publish-to github --private-repo
|
|
146
|
+
|
|
147
|
+
Recipe: Data Science Project
|
|
148
|
+
repokit create ml-project \\
|
|
149
|
+
--language python \\
|
|
150
|
+
--dir-profile standard \\
|
|
151
|
+
--directories "data,models,notebooks" \\
|
|
152
|
+
--branch-strategy simple
|
|
153
|
+
|
|
154
|
+
Recipe: Microservice
|
|
155
|
+
repokit create user-service \\
|
|
156
|
+
--dir-profile minimal \\
|
|
157
|
+
--branch-strategy github-flow \\
|
|
158
|
+
--publish-to github \\
|
|
159
|
+
--organization mycompany
|
|
160
|
+
""",
|
|
161
|
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
create_parser.add_argument(
|
|
165
|
+
"name",
|
|
166
|
+
help="Name of the repository to create"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Language and description. The names come from the code-type layers'
|
|
170
|
+
# manifest (repokit/templates/layers/manifest.toml) and are validated
|
|
171
|
+
# against it before anything is created, so the list here is the help only.
|
|
172
|
+
create_parser.add_argument(
|
|
173
|
+
"--language", "-l",
|
|
174
|
+
action="append",
|
|
175
|
+
metavar="LANG[,LANG...]",
|
|
176
|
+
help=f"The project's language(s), in the order their layers apply; a list or repeated. "
|
|
177
|
+
f"Known: {', '.join(_known_language_names())} (default: generic, no code-type layers)"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
create_parser.add_argument(
|
|
181
|
+
"--with",
|
|
182
|
+
dest="with_layers",
|
|
183
|
+
action="append",
|
|
184
|
+
metavar="NAME[,NAME...]",
|
|
185
|
+
help="What the project is, below its language: a preset such as python/pypi or "
|
|
186
|
+
"js-ts/claude-plugin, a layer such as c-cpp/qt, or bare names when unique "
|
|
187
|
+
"(claude-plugin; cmake-lib,qt). Comma-separated or repeated; each implies its "
|
|
188
|
+
"language; a list that equals a preset is that preset. See `repokit list`."
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
create_parser.add_argument(
|
|
192
|
+
"--license",
|
|
193
|
+
metavar="ID|file=PATH|none",
|
|
194
|
+
help="The LICENSE to write: a catalogue id (mit, gpl-3.0, gpl-3.0-or-later, apache-2.0, "
|
|
195
|
+
"all-rights-reserved, ...; `repokit list` shows them), file=PATH to copy a text as is, or "
|
|
196
|
+
"none. Required unless the global config names `license` (or `license_private` for "
|
|
197
|
+
"--private-repo); holder and year come from the git identity and the clock."
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
create_parser.add_argument(
|
|
201
|
+
"--extras",
|
|
202
|
+
dest="extras",
|
|
203
|
+
action="append",
|
|
204
|
+
metavar="NAME[,NAME...]",
|
|
205
|
+
help="The structure's optional files, by name: funding, dependabot, stale, pr-template, "
|
|
206
|
+
"release-template, roadmap, platform-support, vscode-settings, placeholders, or all. "
|
|
207
|
+
"Off unless named here or in the global config's `extras` list."
|
|
208
|
+
)
|
|
209
|
+
create_parser.add_argument(
|
|
210
|
+
"--no-extras",
|
|
211
|
+
dest="no_extras",
|
|
212
|
+
action="store_true",
|
|
213
|
+
help="Write none of the extras, whatever the global config says"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
create_parser.add_argument(
|
|
217
|
+
"--github-setup",
|
|
218
|
+
dest="github_setup",
|
|
219
|
+
action="append",
|
|
220
|
+
metavar="STEP[,STEP...]",
|
|
221
|
+
help="With --github, what to set up on the new repository: labels, issues (the Roadmap and "
|
|
222
|
+
"Quick Notes as #1 and #2, before the first push), pin, discussions, topics, "
|
|
223
|
+
"pypi-environment, traffic (prints ghtraf's commands), or all. Off unless named here "
|
|
224
|
+
"or in the global config's `github_setup` list."
|
|
225
|
+
)
|
|
226
|
+
create_parser.add_argument(
|
|
227
|
+
"--topics",
|
|
228
|
+
metavar="TOPIC[,TOPIC...]",
|
|
229
|
+
help="Repository topics for --github-setup topics"
|
|
230
|
+
)
|
|
231
|
+
create_parser.add_argument(
|
|
232
|
+
"--badges",
|
|
233
|
+
metavar="TEMPLATE|BADGE[,BADGE...]",
|
|
234
|
+
help="README badges: a template (pypi, github, comfyui, minimal, or one saved as "
|
|
235
|
+
"badge_templates in the config) or a list of badges (pypi-version, release-date, "
|
|
236
|
+
"downloads, release-version, ci, python, node, comfyui-registry, license, discussions, "
|
|
237
|
+
"installs, views, platform). Default: the preset's template"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
create_parser.add_argument(
|
|
241
|
+
"--description", "-d",
|
|
242
|
+
help="Short description of the repository"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Directory configuration - NEW PROFILE SUPPORT
|
|
246
|
+
dir_group = create_parser.add_argument_group("Directory Configuration")
|
|
247
|
+
dir_group.add_argument(
|
|
248
|
+
"--dir-profile",
|
|
249
|
+
choices=["minimal", "standard", "complete"],
|
|
250
|
+
help="Use a predefined directory profile"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
dir_group.add_argument(
|
|
254
|
+
"--dir-groups",
|
|
255
|
+
help="Comma-separated list of directory groups (development,documentation,operations,privacy)",
|
|
256
|
+
metavar="GROUPS"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
dir_group.add_argument(
|
|
260
|
+
"--directories", "-dir",
|
|
261
|
+
help="Comma-separated list of additional directories to create",
|
|
262
|
+
metavar="DIRS"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
dir_group.add_argument(
|
|
266
|
+
"--private-dirs", "-pd",
|
|
267
|
+
help="Comma-separated list of directories to mark as private",
|
|
268
|
+
metavar="DIRS"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
dir_group.add_argument(
|
|
272
|
+
"--private-set",
|
|
273
|
+
choices=["standard", "enhanced"],
|
|
274
|
+
default="standard",
|
|
275
|
+
help="Private directory set to use (default: standard)"
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
dir_group.add_argument(
|
|
279
|
+
"--sensitive-files", "-sf",
|
|
280
|
+
help="Comma-separated list of sensitive files to exclude from public branches",
|
|
281
|
+
metavar="FILES"
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
dir_group.add_argument(
|
|
285
|
+
"--sensitive-patterns", "-sp",
|
|
286
|
+
help="Comma-separated list of file patterns to exclude from public branches (supports glob)",
|
|
287
|
+
metavar="PATTERNS"
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Branch configuration
|
|
291
|
+
branch_group = create_parser.add_argument_group("Branch Configuration")
|
|
292
|
+
branch_group.add_argument(
|
|
293
|
+
"--branches", "-b",
|
|
294
|
+
help="Comma-separated list of branches to create",
|
|
295
|
+
metavar="BRANCHES"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
branch_group.add_argument(
|
|
299
|
+
"--worktrees", "-w",
|
|
300
|
+
help="Comma-separated list of branches to create worktrees for",
|
|
301
|
+
metavar="BRANCHES"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
branch_group.add_argument(
|
|
305
|
+
"--private-branch",
|
|
306
|
+
default=None,
|
|
307
|
+
help="Name of the private branch (default: the strategy's; 'private' in the private-branch strategies)"
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
branch_group.add_argument(
|
|
311
|
+
"--default-branch",
|
|
312
|
+
help="Name of the default branch (default: main)"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
branch_group.add_argument(
|
|
316
|
+
"--branch-strategy",
|
|
317
|
+
choices=list(BRANCH_STRATEGIES),
|
|
318
|
+
help="Predefined branch strategy to use (default: main-only; or a private-branch strategy)"
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
branch_group.add_argument(
|
|
322
|
+
"--private-mode",
|
|
323
|
+
choices=["nested", "branch"],
|
|
324
|
+
help="How private work is kept: 'nested' makes private/ its own git repository "
|
|
325
|
+
"(linked into every worktree); 'branch' keeps it on the private branch only. "
|
|
326
|
+
"Default: nested for main-only, branch for the private-branch strategies"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
common_group = branch_group.add_mutually_exclusive_group()
|
|
330
|
+
common_group.add_argument(
|
|
331
|
+
"--with-common", dest="with_common", action="store_true", default=None,
|
|
332
|
+
help="Vendor git-repokit-common (hooks, version sync, tools) at scripts/repokit-common/ "
|
|
333
|
+
"and install its hooks (default for main-only)"
|
|
334
|
+
)
|
|
335
|
+
common_group.add_argument(
|
|
336
|
+
"--no-common", dest="with_common", action="store_false",
|
|
337
|
+
help="Do not vendor git-repokit-common; git-repokit's own pre-commit guard is installed instead "
|
|
338
|
+
"(default for the private-branch strategies)"
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
branch_group.add_argument(
|
|
342
|
+
"--branch-config",
|
|
343
|
+
help="Path to branch configuration file (JSON)",
|
|
344
|
+
metavar="FILE"
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
branch_group.add_argument(
|
|
348
|
+
"--branch-dir-main",
|
|
349
|
+
help="Directory name for main branch worktree (default: github)"
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
branch_group.add_argument(
|
|
353
|
+
"--branch-dir-dev",
|
|
354
|
+
help="Directory name for dev branch worktree (default: dev)"
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# Git configuration
|
|
358
|
+
git_group = create_parser.add_argument_group("Git Configuration")
|
|
359
|
+
git_group.add_argument(
|
|
360
|
+
"--user-name",
|
|
361
|
+
help="Git user name for commits"
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
git_group.add_argument(
|
|
365
|
+
"--user-email",
|
|
366
|
+
help="Git user email for commits"
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Remote/Publishing options
|
|
370
|
+
remote_group = create_parser.add_argument_group("Remote Repository")
|
|
371
|
+
remote_group.add_argument(
|
|
372
|
+
"--github",
|
|
373
|
+
action="store_true",
|
|
374
|
+
help="Create the repository on GitHub with the gh CLI (its login is used; private unless "
|
|
375
|
+
"--public) and push the public branches. With --no-push nothing is pushed"
|
|
376
|
+
)
|
|
377
|
+
remote_group.add_argument(
|
|
378
|
+
"--public",
|
|
379
|
+
action="store_true",
|
|
380
|
+
help="With --github: make the GitHub repository public"
|
|
381
|
+
)
|
|
382
|
+
remote_group.add_argument(
|
|
383
|
+
"--publish-to",
|
|
384
|
+
choices=["github", "gitlab"],
|
|
385
|
+
help="Publish repository to a remote service through its REST API and a token"
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
remote_group.add_argument(
|
|
389
|
+
"--remote-name",
|
|
390
|
+
default="origin",
|
|
391
|
+
help="Name for the remote (default: origin)"
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
remote_group.add_argument(
|
|
395
|
+
"--private-repo",
|
|
396
|
+
action="store_true",
|
|
397
|
+
help="Create a private repository on remote service"
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
remote_group.add_argument(
|
|
401
|
+
"--organization", "--org",
|
|
402
|
+
help="GitHub organization or GitLab group name"
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
remote_group.add_argument(
|
|
406
|
+
"--no-push",
|
|
407
|
+
action="store_true",
|
|
408
|
+
help="Don't push branches after creating remote repository"
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
# Authentication
|
|
412
|
+
auth_group = create_parser.add_argument_group("Authentication")
|
|
413
|
+
auth_group.add_argument(
|
|
414
|
+
"--token",
|
|
415
|
+
help="Authentication token for remote service"
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
auth_group.add_argument(
|
|
419
|
+
"--token-command",
|
|
420
|
+
help="Command to retrieve token (e.g., 'pass show github/token')"
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
auth_group.add_argument(
|
|
424
|
+
"--credentials-file",
|
|
425
|
+
help="Path to credentials file",
|
|
426
|
+
metavar="FILE"
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
# Template options
|
|
430
|
+
template_group = create_parser.add_argument_group("Template Configuration")
|
|
431
|
+
template_group.add_argument(
|
|
432
|
+
"--templates-dir",
|
|
433
|
+
help="Path to custom templates directory",
|
|
434
|
+
metavar="DIR"
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
template_group.add_argument(
|
|
438
|
+
"--ai",
|
|
439
|
+
choices=["none", "claude"],
|
|
440
|
+
default="claude",
|
|
441
|
+
help="AI tool integration to include (default: claude)"
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# ANALYZE command
|
|
445
|
+
analyze_parser = subparsers.add_parser(
|
|
446
|
+
"analyze",
|
|
447
|
+
help="Analyze an existing directory for RepoKit compatibility",
|
|
448
|
+
description="""
|
|
449
|
+
Analyze an existing directory to understand its structure and provide recommendations
|
|
450
|
+
for RepoKit adoption or migration.
|
|
451
|
+
|
|
452
|
+
This command examines:
|
|
453
|
+
- Current directory structure and how it maps to RepoKit profiles
|
|
454
|
+
- Git repository state and branch configuration
|
|
455
|
+
- Existing configuration files and potential conflicts
|
|
456
|
+
- Project type detection (language, framework)
|
|
457
|
+
- Migration complexity assessment
|
|
458
|
+
|
|
459
|
+
The analysis provides:
|
|
460
|
+
- Recommended migration strategy (safe, replace, merge)
|
|
461
|
+
- Suggested branch strategy based on current setup
|
|
462
|
+
- List of actions that would be taken during migration
|
|
463
|
+
- Potential issues or conflicts to resolve
|
|
464
|
+
""",
|
|
465
|
+
epilog="""
|
|
466
|
+
Examples:
|
|
467
|
+
# Analyze current directory
|
|
468
|
+
repokit analyze .
|
|
469
|
+
|
|
470
|
+
# Analyze a specific project
|
|
471
|
+
repokit analyze /path/to/project
|
|
472
|
+
|
|
473
|
+
# Verbose analysis with detailed information
|
|
474
|
+
repokit analyze ./myproject -vv
|
|
475
|
+
""",
|
|
476
|
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
analyze_parser.add_argument(
|
|
480
|
+
"name",
|
|
481
|
+
help="Path to directory to analyze"
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
# MIGRATE command
|
|
485
|
+
migrate_parser = subparsers.add_parser(
|
|
486
|
+
"migrate",
|
|
487
|
+
help="Migrate an existing directory to RepoKit structure",
|
|
488
|
+
description="""
|
|
489
|
+
Migrate an existing directory to use RepoKit's standardized structure.
|
|
490
|
+
|
|
491
|
+
Migration Strategies:
|
|
492
|
+
safe - Preserve all existing files, only add missing RepoKit elements
|
|
493
|
+
replace - Replace conflicting files with RepoKit templates
|
|
494
|
+
merge - Attempt to merge existing content with RepoKit templates
|
|
495
|
+
|
|
496
|
+
This command will:
|
|
497
|
+
- Create missing standard directories
|
|
498
|
+
- Add RepoKit configuration files
|
|
499
|
+
- Set up Git hooks for private content protection
|
|
500
|
+
- Generate appropriate .gitignore entries
|
|
501
|
+
- Create template files where needed
|
|
502
|
+
""",
|
|
503
|
+
epilog="""
|
|
504
|
+
Examples:
|
|
505
|
+
# Safe migration (preserves all existing files)
|
|
506
|
+
repokit migrate ./myproject
|
|
507
|
+
|
|
508
|
+
# Replace strategy for clean template files
|
|
509
|
+
repokit migrate ./myproject --migration-strategy replace
|
|
510
|
+
|
|
511
|
+
# Dry run to see what would happen
|
|
512
|
+
repokit migrate ./myproject --dry-run
|
|
513
|
+
|
|
514
|
+
# Migrate and publish to GitHub
|
|
515
|
+
repokit migrate ./myproject --publish-to github
|
|
516
|
+
""",
|
|
517
|
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
migrate_parser.add_argument(
|
|
521
|
+
"name",
|
|
522
|
+
help="Path to directory to migrate"
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
migrate_parser.add_argument(
|
|
526
|
+
"--migration-strategy",
|
|
527
|
+
choices=["safe", "replace", "merge"],
|
|
528
|
+
default="safe",
|
|
529
|
+
help="Migration strategy (default: safe)"
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
migrate_parser.add_argument(
|
|
533
|
+
"--dry-run",
|
|
534
|
+
action="store_true",
|
|
535
|
+
help="Show what would be done without making changes"
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
migrate_parser.add_argument(
|
|
539
|
+
"--backup",
|
|
540
|
+
action="store_true",
|
|
541
|
+
help="Create a backup of the directory before making changes"
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
migrate_parser.add_argument(
|
|
545
|
+
"--backup-location",
|
|
546
|
+
help="Custom backup location (default: <dir>_backup_<timestamp>)",
|
|
547
|
+
metavar="PATH"
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
migrate_parser.add_argument(
|
|
551
|
+
"--publish-to",
|
|
552
|
+
choices=["github", "gitlab"],
|
|
553
|
+
help="Publish to remote after migration"
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
# ADOPT command
|
|
557
|
+
adopt_parser = subparsers.add_parser(
|
|
558
|
+
"adopt",
|
|
559
|
+
help="Adopt RepoKit in an existing Git repository",
|
|
560
|
+
description="""
|
|
561
|
+
Adopt RepoKit structure in an existing Git repository without disrupting
|
|
562
|
+
current development workflow.
|
|
563
|
+
|
|
564
|
+
This is the recommended approach for active projects. It:
|
|
565
|
+
- Preserves all existing Git history
|
|
566
|
+
- Adds RepoKit structure incrementally
|
|
567
|
+
- Sets up branches according to detected or specified strategy
|
|
568
|
+
- Maintains existing remote connections
|
|
569
|
+
- Can optionally publish to a new remote
|
|
570
|
+
|
|
571
|
+
The adopt command is safer than migrate for repositories with:
|
|
572
|
+
- Active development
|
|
573
|
+
- Existing CI/CD pipelines
|
|
574
|
+
- Multiple contributors
|
|
575
|
+
- Production deployments
|
|
576
|
+
""",
|
|
577
|
+
epilog="""
|
|
578
|
+
Examples:
|
|
579
|
+
|
|
580
|
+
BASIC ADOPTION:
|
|
581
|
+
# Adopt current directory with minimal setup
|
|
582
|
+
repokit adopt
|
|
583
|
+
|
|
584
|
+
# Adopt specific directory with standard profile
|
|
585
|
+
repokit adopt ./my-project --dir-profile standard
|
|
586
|
+
|
|
587
|
+
PYTHON PROJECT ADOPTION (Real Example):
|
|
588
|
+
# Adopt a Python project with comprehensive setup and GitHub publishing
|
|
589
|
+
# This is the command used for the UNCtools project deployment
|
|
590
|
+
repokit adopt ./my-python-project \\
|
|
591
|
+
--branch-strategy simple \\
|
|
592
|
+
--migration-strategy safe \\
|
|
593
|
+
--dir-profile standard \\
|
|
594
|
+
--language python \\
|
|
595
|
+
--description "Universal Naming Convention (UNC) path tools for Python" \\
|
|
596
|
+
--publish-to github \\
|
|
597
|
+
--repo-name MyPythonTools \\
|
|
598
|
+
--private-repo \\
|
|
599
|
+
--ai claude \\
|
|
600
|
+
--backup \\
|
|
601
|
+
--backup-location ../backup-before-adoption
|
|
602
|
+
|
|
603
|
+
GITHUB DEPLOYMENT RECIPES:
|
|
604
|
+
# Public GitHub repository
|
|
605
|
+
repokit adopt ./open-source-project \\
|
|
606
|
+
--publish-to github \\
|
|
607
|
+
--repo-name awesome-open-source \\
|
|
608
|
+
--language javascript \\
|
|
609
|
+
--dir-profile complete
|
|
610
|
+
|
|
611
|
+
# Private GitHub repository with organization
|
|
612
|
+
repokit adopt ./company-project \\
|
|
613
|
+
--publish-to github \\
|
|
614
|
+
--repo-name internal-tools \\
|
|
615
|
+
--organization mycompany \\
|
|
616
|
+
--private-repo \\
|
|
617
|
+
--language python
|
|
618
|
+
|
|
619
|
+
# Personal project with full configuration
|
|
620
|
+
repokit adopt ./personal-project \\
|
|
621
|
+
--publish-to github \\
|
|
622
|
+
--repo-name my-awesome-project \\
|
|
623
|
+
--private-repo \\
|
|
624
|
+
--description "My personal coding project" \\
|
|
625
|
+
--user-name "John Doe" \\
|
|
626
|
+
--user-email "john@example.com"
|
|
627
|
+
|
|
628
|
+
BRANCH STRATEGY EXAMPLES:
|
|
629
|
+
# Simple strategy (main, dev, private)
|
|
630
|
+
repokit adopt --branch-strategy simple --language python
|
|
631
|
+
|
|
632
|
+
# GitFlow strategy (main, dev, feature, release, hotfix branches)
|
|
633
|
+
repokit adopt --branch-strategy gitflow --dir-profile complete
|
|
634
|
+
|
|
635
|
+
# GitHub Flow (main, feature branches)
|
|
636
|
+
repokit adopt --branch-strategy github-flow
|
|
637
|
+
|
|
638
|
+
# Custom branches
|
|
639
|
+
repokit adopt --branches "main,staging,production" --default-branch main
|
|
640
|
+
|
|
641
|
+
DIRECTORY PROFILE EXAMPLES:
|
|
642
|
+
# Minimal profile (docs, tests, scripts)
|
|
643
|
+
repokit adopt --dir-profile minimal
|
|
644
|
+
|
|
645
|
+
# Standard profile (docs, tests, scripts, examples, config)
|
|
646
|
+
repokit adopt --dir-profile standard
|
|
647
|
+
|
|
648
|
+
# Complete profile (all directories including logs, private, revisions)
|
|
649
|
+
repokit adopt --dir-profile complete
|
|
650
|
+
|
|
651
|
+
# Custom directories
|
|
652
|
+
repokit adopt --directories "data,models,notebooks,experiments"
|
|
653
|
+
|
|
654
|
+
SENSITIVE FILE PROTECTION:
|
|
655
|
+
# Specify sensitive files to exclude from public branches
|
|
656
|
+
repokit adopt --sensitive-files ".env,secrets.json,api_keys.txt"
|
|
657
|
+
|
|
658
|
+
# Specify sensitive patterns (vim backups, logs, private dirs)
|
|
659
|
+
repokit adopt --sensitive-patterns "*.log,*~,private/*,secrets/*"
|
|
660
|
+
|
|
661
|
+
# Use enhanced privacy protection
|
|
662
|
+
repokit adopt --private-set enhanced --private-dirs "experiments,drafts"
|
|
663
|
+
|
|
664
|
+
BACKUP AND SAFETY:
|
|
665
|
+
# Create backup before adoption
|
|
666
|
+
repokit adopt --backup --backup-location ../project-backup-$(date +%Y%m%d)
|
|
667
|
+
|
|
668
|
+
# Dry run to preview all changes
|
|
669
|
+
repokit adopt --dry-run -vv
|
|
670
|
+
|
|
671
|
+
# Clean git history during adoption
|
|
672
|
+
repokit adopt --clean-history --cleaning-recipe pre-open-source
|
|
673
|
+
|
|
674
|
+
AI INTEGRATION:
|
|
675
|
+
# Add Claude AI integration files
|
|
676
|
+
repokit adopt --ai claude
|
|
677
|
+
|
|
678
|
+
# Skip AI integration
|
|
679
|
+
repokit adopt --ai none
|
|
680
|
+
|
|
681
|
+
COMMON WORKFLOWS:
|
|
682
|
+
1. Test adoption (recommended first step):
|
|
683
|
+
repokit adopt ./my-project --dry-run -vv
|
|
684
|
+
|
|
685
|
+
2. Safe adoption with backup:
|
|
686
|
+
repokit adopt ./my-project --backup --backup-location ../backup
|
|
687
|
+
|
|
688
|
+
3. GitHub deployment:
|
|
689
|
+
repokit adopt ./my-project --publish-to github --repo-name my-project --private-repo
|
|
690
|
+
|
|
691
|
+
4. Complete adoption (full features):
|
|
692
|
+
repokit adopt ./my-project \\
|
|
693
|
+
--dir-profile complete \\
|
|
694
|
+
--branch-strategy simple \\
|
|
695
|
+
--language python \\
|
|
696
|
+
--ai claude \\
|
|
697
|
+
--publish-to github \\
|
|
698
|
+
--private-repo
|
|
699
|
+
|
|
700
|
+
For more detailed documentation, see: docs/Adoption-Guide.md
|
|
701
|
+
""",
|
|
702
|
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
adopt_parser.add_argument(
|
|
706
|
+
"name",
|
|
707
|
+
nargs="?",
|
|
708
|
+
help="Path to repository (default: current directory)"
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
# Language and description (the same names as create; the layers are laid
|
|
712
|
+
# down without overwriting the adopted project's own files)
|
|
713
|
+
adopt_parser.add_argument(
|
|
714
|
+
"--language", "-l",
|
|
715
|
+
action="append",
|
|
716
|
+
metavar="LANG[,LANG...]",
|
|
717
|
+
help=f"The project's language(s). Known: {', '.join(_known_language_names())} (default: generic)"
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
adopt_parser.add_argument(
|
|
721
|
+
"--with",
|
|
722
|
+
dest="with_layers",
|
|
723
|
+
action="append",
|
|
724
|
+
metavar="NAME[,NAME...]",
|
|
725
|
+
help="Presets and layers to lay down without overwriting the project's own files, "
|
|
726
|
+
"as for create (see `repokit list`)"
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
adopt_parser.add_argument(
|
|
730
|
+
"--license",
|
|
731
|
+
metavar="ID|file=PATH|none",
|
|
732
|
+
help="The LICENSE to write when the project has none, as for create; an existing LICENSE is kept"
|
|
733
|
+
)
|
|
734
|
+
|
|
735
|
+
adopt_parser.add_argument(
|
|
736
|
+
"--description", "-d",
|
|
737
|
+
help="Short description of the repository"
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
# Directory configuration - NEW PROFILE SUPPORT
|
|
741
|
+
adopt_dir_group = adopt_parser.add_argument_group("Directory Configuration")
|
|
742
|
+
adopt_dir_group.add_argument(
|
|
743
|
+
"--dir-profile",
|
|
744
|
+
choices=["minimal", "standard", "complete"],
|
|
745
|
+
help="Use a predefined directory profile"
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
adopt_dir_group.add_argument(
|
|
749
|
+
"--dir-groups",
|
|
750
|
+
help="Comma-separated list of directory groups (development,documentation,operations,privacy)",
|
|
751
|
+
metavar="GROUPS"
|
|
752
|
+
)
|
|
753
|
+
|
|
754
|
+
adopt_dir_group.add_argument(
|
|
755
|
+
"--directories", "-dir",
|
|
756
|
+
help="Comma-separated list of additional directories to create",
|
|
757
|
+
metavar="DIRS"
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
adopt_dir_group.add_argument(
|
|
761
|
+
"--private-dirs", "-pd",
|
|
762
|
+
help="Comma-separated list of directories to mark as private",
|
|
763
|
+
metavar="DIRS"
|
|
764
|
+
)
|
|
765
|
+
|
|
766
|
+
adopt_dir_group.add_argument(
|
|
767
|
+
"--private-set",
|
|
768
|
+
choices=["standard", "enhanced"],
|
|
769
|
+
default="standard",
|
|
770
|
+
help="Private directory set to use (default: standard)"
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
adopt_dir_group.add_argument(
|
|
774
|
+
"--sensitive-files", "-sf",
|
|
775
|
+
help="Comma-separated list of sensitive files to exclude from public branches",
|
|
776
|
+
metavar="FILES"
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
adopt_dir_group.add_argument(
|
|
780
|
+
"--sensitive-patterns", "-sp",
|
|
781
|
+
help="Comma-separated list of file patterns to exclude from public branches (supports glob)",
|
|
782
|
+
metavar="PATTERNS"
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
# Branch configuration
|
|
786
|
+
adopt_branch_group = adopt_parser.add_argument_group("Branch Configuration")
|
|
787
|
+
adopt_branch_group.add_argument(
|
|
788
|
+
"--branches", "-b",
|
|
789
|
+
help="Comma-separated list of branches to create",
|
|
790
|
+
metavar="BRANCHES"
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
adopt_branch_group.add_argument(
|
|
794
|
+
"--worktrees", "-w",
|
|
795
|
+
help="Comma-separated list of branches to create worktrees for",
|
|
796
|
+
metavar="BRANCHES"
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
adopt_branch_group.add_argument(
|
|
800
|
+
"--private-branch",
|
|
801
|
+
default=None,
|
|
802
|
+
help="Name of the private branch (default: the strategy's; 'private' in the private-branch strategies)"
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
adopt_branch_group.add_argument(
|
|
806
|
+
"--default-branch",
|
|
807
|
+
help="Name of the default branch (default: main)"
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
# Adoption builds the private-branch model only; main-only adoption is not
|
|
811
|
+
# implemented yet.
|
|
812
|
+
adopt_branch_group.add_argument(
|
|
813
|
+
"--branch-strategy",
|
|
814
|
+
choices=PRIVATE_BRANCH_STRATEGIES,
|
|
815
|
+
help="Branch strategy to implement"
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
adopt_branch_group.add_argument(
|
|
819
|
+
"--branch-config",
|
|
820
|
+
help="Path to branch configuration file (JSON)",
|
|
821
|
+
metavar="FILE"
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
adopt_branch_group.add_argument(
|
|
825
|
+
"--branch-dir-main",
|
|
826
|
+
help="Directory name for main branch worktree (default: github)"
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
adopt_branch_group.add_argument(
|
|
830
|
+
"--branch-dir-dev",
|
|
831
|
+
help="Directory name for dev branch worktree (default: dev)"
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
# Git configuration
|
|
835
|
+
adopt_git_group = adopt_parser.add_argument_group("Git Configuration")
|
|
836
|
+
adopt_git_group.add_argument(
|
|
837
|
+
"--user-name",
|
|
838
|
+
help="Git user name for commits"
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
adopt_git_group.add_argument(
|
|
842
|
+
"--user-email",
|
|
843
|
+
help="Git user email for commits"
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
# Migration strategy
|
|
847
|
+
adopt_parser.add_argument(
|
|
848
|
+
"--migration-strategy",
|
|
849
|
+
choices=["safe", "replace", "merge"],
|
|
850
|
+
default="safe",
|
|
851
|
+
help="File handling strategy (default: safe)"
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
adopt_parser.add_argument(
|
|
855
|
+
"--dry-run",
|
|
856
|
+
action="store_true",
|
|
857
|
+
help="Preview changes without modifying repository"
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
adopt_parser.add_argument(
|
|
861
|
+
"--backup",
|
|
862
|
+
action="store_true",
|
|
863
|
+
help="Create a backup of the repository before making changes"
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
adopt_parser.add_argument(
|
|
867
|
+
"--backup-location",
|
|
868
|
+
help="Custom backup location (default: <repo>_backup_<timestamp>)",
|
|
869
|
+
metavar="PATH"
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
adopt_parser.add_argument(
|
|
873
|
+
"--clean-history",
|
|
874
|
+
action="store_true",
|
|
875
|
+
help="Clean private content from git history before creating public branches"
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
adopt_parser.add_argument(
|
|
879
|
+
"--cleaning-recipe",
|
|
880
|
+
choices=["pre-open-source", "windows-safe", "remove-secrets"],
|
|
881
|
+
default="pre-open-source",
|
|
882
|
+
help="Recipe for cleaning history (default: pre-open-source)"
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
# Remote/Publishing options
|
|
886
|
+
adopt_remote_group = adopt_parser.add_argument_group("Remote Repository")
|
|
887
|
+
adopt_remote_group.add_argument(
|
|
888
|
+
"--publish-to",
|
|
889
|
+
choices=["github", "gitlab"],
|
|
890
|
+
help="Publish repository to a remote service"
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
adopt_remote_group.add_argument(
|
|
894
|
+
"--repo-name",
|
|
895
|
+
help="Name for the remote repository (default: directory name)"
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
adopt_remote_group.add_argument(
|
|
899
|
+
"--remote-name",
|
|
900
|
+
default="origin",
|
|
901
|
+
help="Name for the remote (default: origin)"
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
adopt_remote_group.add_argument(
|
|
905
|
+
"--private-repo",
|
|
906
|
+
action="store_true",
|
|
907
|
+
help="Create a private repository on remote service"
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
adopt_remote_group.add_argument(
|
|
911
|
+
"--organization",
|
|
912
|
+
help="GitHub organization or GitLab group name"
|
|
913
|
+
)
|
|
914
|
+
|
|
915
|
+
adopt_remote_group.add_argument(
|
|
916
|
+
"--no-push",
|
|
917
|
+
action="store_true",
|
|
918
|
+
help="Don't push branches after creating remote repository"
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
# Authentication
|
|
922
|
+
adopt_auth_group = adopt_parser.add_argument_group("Authentication")
|
|
923
|
+
adopt_auth_group.add_argument(
|
|
924
|
+
"--token",
|
|
925
|
+
help="Authentication token for remote service"
|
|
926
|
+
)
|
|
927
|
+
|
|
928
|
+
adopt_auth_group.add_argument(
|
|
929
|
+
"--token-command",
|
|
930
|
+
help="Command to retrieve token (e.g., 'pass show github/token')"
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
adopt_auth_group.add_argument(
|
|
934
|
+
"--credentials-file",
|
|
935
|
+
help="Path to credentials file",
|
|
936
|
+
metavar="FILE"
|
|
937
|
+
)
|
|
938
|
+
|
|
939
|
+
# Template options
|
|
940
|
+
adopt_template_group = adopt_parser.add_argument_group("Template Configuration")
|
|
941
|
+
adopt_template_group.add_argument(
|
|
942
|
+
"--templates-dir",
|
|
943
|
+
help="Path to custom templates directory",
|
|
944
|
+
metavar="DIR"
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
adopt_template_group.add_argument(
|
|
948
|
+
"--ai",
|
|
949
|
+
choices=["none", "claude"],
|
|
950
|
+
default="claude",
|
|
951
|
+
help="AI tool integration to include (default: claude)"
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
# PUBLISH command
|
|
955
|
+
publish_parser = subparsers.add_parser(
|
|
956
|
+
"publish",
|
|
957
|
+
help="Publish an existing repository to GitHub/GitLab",
|
|
958
|
+
description="""
|
|
959
|
+
Publish an existing RepoKit repository to a remote service.
|
|
960
|
+
|
|
961
|
+
This command:
|
|
962
|
+
- Creates a remote repository on GitHub or GitLab
|
|
963
|
+
- Sets up authentication using tokens or stored credentials
|
|
964
|
+
- Configures remote tracking for all branches
|
|
965
|
+
- Pushes all branches and tags
|
|
966
|
+
- Can work with organization/group repositories
|
|
967
|
+
""",
|
|
968
|
+
epilog="""
|
|
969
|
+
Examples:
|
|
970
|
+
# Publish current repository to GitHub
|
|
971
|
+
repokit publish . --publish-to github
|
|
972
|
+
|
|
973
|
+
# Publish to organization with private visibility
|
|
974
|
+
repokit publish ./myproject --publish-to github --organization myorg --private-repo
|
|
975
|
+
|
|
976
|
+
# Publish to GitLab group
|
|
977
|
+
repokit publish . --publish-to gitlab --organization mygroup
|
|
978
|
+
|
|
979
|
+
# Publish without pushing branches
|
|
980
|
+
repokit publish . --publish-to github --no-push
|
|
981
|
+
""",
|
|
982
|
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
publish_parser.add_argument(
|
|
986
|
+
"name",
|
|
987
|
+
help="Path to repository to publish"
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
publish_parser.add_argument(
|
|
991
|
+
"--repo-name",
|
|
992
|
+
help="Name for the remote repository (default: directory name)"
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
publish_parser.add_argument(
|
|
996
|
+
"--publish-to",
|
|
997
|
+
choices=["github", "gitlab"],
|
|
998
|
+
required=True,
|
|
999
|
+
help="Remote service to publish to"
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
publish_parser.add_argument(
|
|
1003
|
+
"--remote-name",
|
|
1004
|
+
default="origin",
|
|
1005
|
+
help="Name for the remote (default: origin)"
|
|
1006
|
+
)
|
|
1007
|
+
|
|
1008
|
+
publish_parser.add_argument(
|
|
1009
|
+
"--private-repo",
|
|
1010
|
+
action="store_true",
|
|
1011
|
+
help="Create as private repository"
|
|
1012
|
+
)
|
|
1013
|
+
|
|
1014
|
+
publish_parser.add_argument(
|
|
1015
|
+
"--organization",
|
|
1016
|
+
help="Organization/group name"
|
|
1017
|
+
)
|
|
1018
|
+
|
|
1019
|
+
publish_parser.add_argument(
|
|
1020
|
+
"--no-push",
|
|
1021
|
+
action="store_true",
|
|
1022
|
+
help="Create remote but don't push"
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
publish_parser.add_argument(
|
|
1026
|
+
"--token",
|
|
1027
|
+
help="Authentication token"
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
publish_parser.add_argument(
|
|
1031
|
+
"--token-command",
|
|
1032
|
+
help="Command to retrieve token"
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
publish_parser.add_argument(
|
|
1036
|
+
"--credentials-file",
|
|
1037
|
+
help="Path to credentials file"
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
publish_parser.add_argument(
|
|
1041
|
+
"--templates-dir",
|
|
1042
|
+
help="Path to custom templates directory",
|
|
1043
|
+
metavar="DIR"
|
|
1044
|
+
)
|
|
1045
|
+
|
|
1046
|
+
publish_parser.add_argument(
|
|
1047
|
+
"--include-branches",
|
|
1048
|
+
nargs="+",
|
|
1049
|
+
help="Only push these specific branches (overrides strategy defaults)"
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
publish_parser.add_argument(
|
|
1053
|
+
"--exclude-branches",
|
|
1054
|
+
nargs="+",
|
|
1055
|
+
help="Exclude these branches from publishing (in addition to private)"
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
# BOOTSTRAP command
|
|
1059
|
+
bootstrap_parser = subparsers.add_parser(
|
|
1060
|
+
"bootstrap",
|
|
1061
|
+
help="Generate a bootstrap script for RepoKit setup",
|
|
1062
|
+
description="""
|
|
1063
|
+
Generate a self-contained Python script that can bootstrap a complete
|
|
1064
|
+
RepoKit project with a single command.
|
|
1065
|
+
|
|
1066
|
+
The bootstrap script:
|
|
1067
|
+
- Checks for Git installation
|
|
1068
|
+
- Configures Git user if needed
|
|
1069
|
+
- Creates the repository structure
|
|
1070
|
+
- Sets up all branches and worktrees
|
|
1071
|
+
- Can optionally publish to remote
|
|
1072
|
+
- Includes all RepoKit functionality in one file
|
|
1073
|
+
|
|
1074
|
+
This is useful for:
|
|
1075
|
+
- Quick project initialization
|
|
1076
|
+
- CI/CD pipelines
|
|
1077
|
+
- Containerized environments
|
|
1078
|
+
- Sharing project templates
|
|
1079
|
+
""",
|
|
1080
|
+
epilog="""
|
|
1081
|
+
Examples:
|
|
1082
|
+
# Generate basic bootstrap script
|
|
1083
|
+
repokit bootstrap --name myproject
|
|
1084
|
+
|
|
1085
|
+
# Bootstrap with GitHub publishing
|
|
1086
|
+
repokit bootstrap --name webapp --service github --private
|
|
1087
|
+
|
|
1088
|
+
# Custom output location
|
|
1089
|
+
repokit bootstrap --name api --output setup_api.py
|
|
1090
|
+
|
|
1091
|
+
# For organization project
|
|
1092
|
+
repokit bootstrap --name service --service github --organization mycompany
|
|
1093
|
+
""",
|
|
1094
|
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
bootstrap_parser.add_argument(
|
|
1098
|
+
"--name",
|
|
1099
|
+
help="Project name for the bootstrap script"
|
|
1100
|
+
)
|
|
1101
|
+
|
|
1102
|
+
bootstrap_parser.add_argument(
|
|
1103
|
+
"--output", "-o",
|
|
1104
|
+
default="bootstrap_repokit.py",
|
|
1105
|
+
help="Output file path (default: bootstrap_repokit.py)"
|
|
1106
|
+
)
|
|
1107
|
+
|
|
1108
|
+
bootstrap_parser.add_argument(
|
|
1109
|
+
"--service",
|
|
1110
|
+
choices=["github", "gitlab"],
|
|
1111
|
+
help="Include remote publishing in bootstrap"
|
|
1112
|
+
)
|
|
1113
|
+
|
|
1114
|
+
bootstrap_parser.add_argument(
|
|
1115
|
+
"--private",
|
|
1116
|
+
action="store_true",
|
|
1117
|
+
help="Create private repository"
|
|
1118
|
+
)
|
|
1119
|
+
|
|
1120
|
+
bootstrap_parser.add_argument(
|
|
1121
|
+
"--organization",
|
|
1122
|
+
help="Organization/group name"
|
|
1123
|
+
)
|
|
1124
|
+
|
|
1125
|
+
bootstrap_parser.add_argument(
|
|
1126
|
+
"--no-publish",
|
|
1127
|
+
action="store_true",
|
|
1128
|
+
help="Skip publishing step in bootstrap"
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1131
|
+
bootstrap_parser.add_argument(
|
|
1132
|
+
"--git-user-name",
|
|
1133
|
+
help="Git user name for bootstrap"
|
|
1134
|
+
)
|
|
1135
|
+
|
|
1136
|
+
bootstrap_parser.add_argument(
|
|
1137
|
+
"--git-user-email",
|
|
1138
|
+
help="Git user email for bootstrap"
|
|
1139
|
+
)
|
|
1140
|
+
|
|
1141
|
+
# Other simple commands
|
|
1142
|
+
list_parser = subparsers.add_parser(
|
|
1143
|
+
"list",
|
|
1144
|
+
help="List the languages, presets and layers that --language and --with accept",
|
|
1145
|
+
description="Per language: its presets (name = layers; planned ones marked), then each "
|
|
1146
|
+
"facet with its choices and whether one or any may be picked."
|
|
1147
|
+
)
|
|
1148
|
+
list_parser.add_argument("language", nargs="?", help="One language only")
|
|
1149
|
+
|
|
1150
|
+
subparsers.add_parser(
|
|
1151
|
+
"list-templates",
|
|
1152
|
+
help="List all available templates",
|
|
1153
|
+
description="Display all available template files that RepoKit can use."
|
|
1154
|
+
)
|
|
1155
|
+
|
|
1156
|
+
init_config_parser = subparsers.add_parser(
|
|
1157
|
+
"init-config",
|
|
1158
|
+
help="Initialize a .repokit.json configuration file",
|
|
1159
|
+
description="""
|
|
1160
|
+
Create a default .repokit.json configuration file in the current directory.
|
|
1161
|
+
|
|
1162
|
+
This file can be customized to set project-specific defaults for:
|
|
1163
|
+
- Directory structure and profiles
|
|
1164
|
+
- Branch strategies and names
|
|
1165
|
+
- Git user configuration
|
|
1166
|
+
- Remote service preferences
|
|
1167
|
+
- Template directories
|
|
1168
|
+
"""
|
|
1169
|
+
)
|
|
1170
|
+
|
|
1171
|
+
store_creds_parser = subparsers.add_parser(
|
|
1172
|
+
"store-credentials",
|
|
1173
|
+
help="Store credentials for GitHub/GitLab",
|
|
1174
|
+
description="""
|
|
1175
|
+
Securely store authentication credentials for GitHub or GitLab.
|
|
1176
|
+
|
|
1177
|
+
Credentials are stored in:
|
|
1178
|
+
- ~/.repokit/credentials.json (default)
|
|
1179
|
+
- Or a custom location specified by --credentials-file
|
|
1180
|
+
|
|
1181
|
+
The stored credentials are used automatically when publishing repositories.
|
|
1182
|
+
"""
|
|
1183
|
+
)
|
|
1184
|
+
|
|
1185
|
+
store_creds_parser.add_argument(
|
|
1186
|
+
"--publish-to",
|
|
1187
|
+
choices=["github", "gitlab"],
|
|
1188
|
+
required=True,
|
|
1189
|
+
help="Service to store credentials for"
|
|
1190
|
+
)
|
|
1191
|
+
|
|
1192
|
+
store_creds_parser.add_argument(
|
|
1193
|
+
"--token",
|
|
1194
|
+
help="Authentication token to store"
|
|
1195
|
+
)
|
|
1196
|
+
|
|
1197
|
+
store_creds_parser.add_argument(
|
|
1198
|
+
"--token-command",
|
|
1199
|
+
help="Command to retrieve token"
|
|
1200
|
+
)
|
|
1201
|
+
|
|
1202
|
+
store_creds_parser.add_argument(
|
|
1203
|
+
"--organization",
|
|
1204
|
+
help="Default organization/group"
|
|
1205
|
+
)
|
|
1206
|
+
|
|
1207
|
+
store_creds_parser.add_argument(
|
|
1208
|
+
"--credentials-file",
|
|
1209
|
+
help="Custom credentials file path"
|
|
1210
|
+
)
|
|
1211
|
+
|
|
1212
|
+
# Add guardrails commands
|
|
1213
|
+
guardrails_parser = subparsers.add_parser(
|
|
1214
|
+
"setup-guardrails",
|
|
1215
|
+
help="Set up private content protection guardrails",
|
|
1216
|
+
description="""
|
|
1217
|
+
Set up guardrails that keep private content off public branches.
|
|
1218
|
+
|
|
1219
|
+
The hooks are git-repokit-common's: this installs them from the project's
|
|
1220
|
+
vendored copy (scripts/repokit-common/), writes the settings the private-branch
|
|
1221
|
+
model needs, and configures branch-specific exclusions.
|
|
1222
|
+
""",
|
|
1223
|
+
epilog="""
|
|
1224
|
+
Examples:
|
|
1225
|
+
# Install git-repokit-common's hooks from the vendored copy
|
|
1226
|
+
repokit setup-guardrails
|
|
1227
|
+
|
|
1228
|
+
# No copy yet: vendor git-repokit-common first (makes two commits)
|
|
1229
|
+
repokit setup-guardrails --vendor
|
|
1230
|
+
|
|
1231
|
+
# The previous git-repokit hooks (pre-commit + post-checkout restore)
|
|
1232
|
+
repokit setup-guardrails --legacy --force
|
|
1233
|
+
"""
|
|
1234
|
+
)
|
|
1235
|
+
|
|
1236
|
+
guardrails_parser.add_argument(
|
|
1237
|
+
"--force",
|
|
1238
|
+
action="store_true",
|
|
1239
|
+
help="Overwrite existing hooks (--legacy only; install-hooks.sh always rewrites its stubs)"
|
|
1240
|
+
)
|
|
1241
|
+
guardrails_parser.add_argument(
|
|
1242
|
+
"--vendor",
|
|
1243
|
+
action="store_true",
|
|
1244
|
+
help="Add git-repokit-common at scripts/repokit-common/ first if the project has no copy "
|
|
1245
|
+
"(git subtree; needs a clean working tree)"
|
|
1246
|
+
)
|
|
1247
|
+
guardrails_parser.add_argument(
|
|
1248
|
+
"--legacy",
|
|
1249
|
+
action="store_true",
|
|
1250
|
+
help="Install git-repokit's previous hooks instead (its own pre-commit and the post-checkout "
|
|
1251
|
+
"that restores private files from the private branch)"
|
|
1252
|
+
)
|
|
1253
|
+
|
|
1254
|
+
check_guardrails_parser = subparsers.add_parser(
|
|
1255
|
+
"check-guardrails",
|
|
1256
|
+
help="Check guardrails status and validate current state",
|
|
1257
|
+
description="Check if guardrails are properly installed and validate staged changes."
|
|
1258
|
+
)
|
|
1259
|
+
|
|
1260
|
+
safe_merge_parser = subparsers.add_parser(
|
|
1261
|
+
"safe-merge",
|
|
1262
|
+
help="Merge branches with private content protection",
|
|
1263
|
+
description="""
|
|
1264
|
+
Safely merge branches while automatically excluding private content when
|
|
1265
|
+
merging from private to public branches.
|
|
1266
|
+
""",
|
|
1267
|
+
epilog="""
|
|
1268
|
+
Examples:
|
|
1269
|
+
# Preview merge from private to current branch
|
|
1270
|
+
repokit safe-merge private --preview
|
|
1271
|
+
|
|
1272
|
+
# Perform safe merge
|
|
1273
|
+
repokit safe-merge private --no-ff --no-commit
|
|
1274
|
+
"""
|
|
1275
|
+
)
|
|
1276
|
+
|
|
1277
|
+
safe_merge_parser.add_argument(
|
|
1278
|
+
"source_branch",
|
|
1279
|
+
help="Branch to merge from"
|
|
1280
|
+
)
|
|
1281
|
+
|
|
1282
|
+
safe_merge_parser.add_argument(
|
|
1283
|
+
"--no-commit",
|
|
1284
|
+
action="store_true",
|
|
1285
|
+
default=True,
|
|
1286
|
+
help="Perform merge without committing (default)"
|
|
1287
|
+
)
|
|
1288
|
+
|
|
1289
|
+
safe_merge_parser.add_argument(
|
|
1290
|
+
"--no-ff",
|
|
1291
|
+
action="store_true",
|
|
1292
|
+
default=True,
|
|
1293
|
+
help="Create merge commit even if fast-forward (default)"
|
|
1294
|
+
)
|
|
1295
|
+
|
|
1296
|
+
safe_merge_parser.add_argument(
|
|
1297
|
+
"--preview",
|
|
1298
|
+
action="store_true",
|
|
1299
|
+
help="Preview what would be merged without doing it"
|
|
1300
|
+
)
|
|
1301
|
+
|
|
1302
|
+
# Add safe-merge-dev command
|
|
1303
|
+
safe_merge_dev_parser = subparsers.add_parser(
|
|
1304
|
+
"safe-merge-dev",
|
|
1305
|
+
help="Merge development branches with history protection",
|
|
1306
|
+
description="""
|
|
1307
|
+
Merge development branches while protecting sensitive commit history.
|
|
1308
|
+
|
|
1309
|
+
This command helps prevent detailed implementation history from leaking
|
|
1310
|
+
to public branches by intelligently squashing or preserving commits based
|
|
1311
|
+
on branch patterns and configurable rules.
|
|
1312
|
+
|
|
1313
|
+
Branch categories and default behaviors:
|
|
1314
|
+
- prototype/*, experiment/*, spike/*: Always squash (development/exploration)
|
|
1315
|
+
- feature/*: Interactive mode (ask whether to squash)
|
|
1316
|
+
- bugfix/*, hotfix/*: Preserve history (important for tracking)
|
|
1317
|
+
|
|
1318
|
+
The command will:
|
|
1319
|
+
- Analyze commits in the branch
|
|
1320
|
+
- Remove sensitive patterns from messages
|
|
1321
|
+
- Generate meaningful squash commit messages
|
|
1322
|
+
- Provide interactive options for feature branches
|
|
1323
|
+
""",
|
|
1324
|
+
epilog="""
|
|
1325
|
+
Examples:
|
|
1326
|
+
# Merge a prototype branch (auto-squashes)
|
|
1327
|
+
repokit safe-merge-dev prototype/new-feature
|
|
1328
|
+
|
|
1329
|
+
# Merge a feature branch (interactive)
|
|
1330
|
+
repokit safe-merge-dev feature/oauth-integration
|
|
1331
|
+
|
|
1332
|
+
# Force squash with custom message
|
|
1333
|
+
repokit safe-merge-dev feature/api --squash --message "Add REST API"
|
|
1334
|
+
|
|
1335
|
+
# Preserve full history
|
|
1336
|
+
repokit safe-merge-dev feature/refactor --no-squash
|
|
1337
|
+
|
|
1338
|
+
# Preview without merging
|
|
1339
|
+
repokit safe-merge-dev experiment/ml-model --preview
|
|
1340
|
+
""",
|
|
1341
|
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
1342
|
+
)
|
|
1343
|
+
|
|
1344
|
+
safe_merge_dev_parser.add_argument(
|
|
1345
|
+
"branch",
|
|
1346
|
+
help="Development branch to merge"
|
|
1347
|
+
)
|
|
1348
|
+
|
|
1349
|
+
safe_merge_dev_parser.add_argument(
|
|
1350
|
+
"--target",
|
|
1351
|
+
help="Target branch (default: current branch)"
|
|
1352
|
+
)
|
|
1353
|
+
|
|
1354
|
+
safe_merge_dev_parser.add_argument(
|
|
1355
|
+
"--squash",
|
|
1356
|
+
action="store_true",
|
|
1357
|
+
help="Force squash merge regardless of branch rules"
|
|
1358
|
+
)
|
|
1359
|
+
|
|
1360
|
+
safe_merge_dev_parser.add_argument(
|
|
1361
|
+
"--no-squash",
|
|
1362
|
+
action="store_true",
|
|
1363
|
+
help="Preserve all commits regardless of branch rules"
|
|
1364
|
+
)
|
|
1365
|
+
|
|
1366
|
+
safe_merge_dev_parser.add_argument(
|
|
1367
|
+
"--message", "-m",
|
|
1368
|
+
help="Custom commit message for squashed merge"
|
|
1369
|
+
)
|
|
1370
|
+
|
|
1371
|
+
safe_merge_dev_parser.add_argument(
|
|
1372
|
+
"--preserve-last",
|
|
1373
|
+
type=int,
|
|
1374
|
+
help="Preserve the last N commits (not yet implemented)"
|
|
1375
|
+
)
|
|
1376
|
+
|
|
1377
|
+
safe_merge_dev_parser.add_argument(
|
|
1378
|
+
"--preview",
|
|
1379
|
+
action="store_true",
|
|
1380
|
+
help="Preview merge without executing"
|
|
1381
|
+
)
|
|
1382
|
+
|
|
1383
|
+
# Add clean-history command
|
|
1384
|
+
clean_history_parser = subparsers.add_parser(
|
|
1385
|
+
"clean-history",
|
|
1386
|
+
help="Clean sensitive data from repository history",
|
|
1387
|
+
description="""
|
|
1388
|
+
Clean sensitive data from repository history using git filter-repo.
|
|
1389
|
+
|
|
1390
|
+
This command provides safe, user-friendly tools to remove private files,
|
|
1391
|
+
secrets, and other sensitive data from git history. It includes pre-built
|
|
1392
|
+
recipes for common scenarios and comprehensive safety features.
|
|
1393
|
+
|
|
1394
|
+
WARNING: This rewrites repository history and requires force-push!
|
|
1395
|
+
|
|
1396
|
+
Pre-built recipes:
|
|
1397
|
+
- pre-open-source: Remove common private files (private/, CLAUDE.md, etc.)
|
|
1398
|
+
- windows-safe: Fix Windows reserved names (nul, con, aux, etc.)
|
|
1399
|
+
- remove-secrets: Remove API keys and passwords (coming soon)
|
|
1400
|
+
- cutoff-date: Remove old private data before a date (coming soon)
|
|
1401
|
+
|
|
1402
|
+
Safety features:
|
|
1403
|
+
- Automatic backup before cleaning
|
|
1404
|
+
- Dry-run preview mode
|
|
1405
|
+
- Confirmation prompts
|
|
1406
|
+
- Clear warnings and instructions
|
|
1407
|
+
""",
|
|
1408
|
+
epilog="""
|
|
1409
|
+
Examples:
|
|
1410
|
+
# Interactive mode - analyze and suggest cleaning
|
|
1411
|
+
repokit clean-history
|
|
1412
|
+
|
|
1413
|
+
# Remove common private files before open-sourcing
|
|
1414
|
+
repokit clean-history --recipe pre-open-source
|
|
1415
|
+
|
|
1416
|
+
# Preview what would be removed
|
|
1417
|
+
repokit clean-history --recipe pre-open-source --dry-run
|
|
1418
|
+
|
|
1419
|
+
# Fix Windows compatibility issues
|
|
1420
|
+
repokit clean-history --recipe windows-safe
|
|
1421
|
+
|
|
1422
|
+
# Remove specific paths
|
|
1423
|
+
repokit clean-history --remove-paths private/ logs/ secrets/
|
|
1424
|
+
|
|
1425
|
+
# Skip backup (not recommended)
|
|
1426
|
+
repokit clean-history --recipe pre-open-source --no-backup
|
|
1427
|
+
""",
|
|
1428
|
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
1429
|
+
)
|
|
1430
|
+
|
|
1431
|
+
clean_history_parser.add_argument(
|
|
1432
|
+
"--recipe",
|
|
1433
|
+
choices=["pre-open-source", "windows-safe", "remove-secrets", "cutoff-date"],
|
|
1434
|
+
help="Use a pre-built cleaning recipe"
|
|
1435
|
+
)
|
|
1436
|
+
|
|
1437
|
+
clean_history_parser.add_argument(
|
|
1438
|
+
"--remove-paths",
|
|
1439
|
+
nargs="+",
|
|
1440
|
+
help="Specific paths to remove from history"
|
|
1441
|
+
)
|
|
1442
|
+
|
|
1443
|
+
clean_history_parser.add_argument(
|
|
1444
|
+
"--cutoff-sha",
|
|
1445
|
+
help="Remove private data only before this commit"
|
|
1446
|
+
)
|
|
1447
|
+
|
|
1448
|
+
clean_history_parser.add_argument(
|
|
1449
|
+
"--cutoff-date",
|
|
1450
|
+
help="Remove private data only before this date (YYYY-MM-DD)"
|
|
1451
|
+
)
|
|
1452
|
+
|
|
1453
|
+
clean_history_parser.add_argument(
|
|
1454
|
+
"--dry-run",
|
|
1455
|
+
action="store_true",
|
|
1456
|
+
help="Preview changes without modifying repository"
|
|
1457
|
+
)
|
|
1458
|
+
|
|
1459
|
+
clean_history_parser.add_argument(
|
|
1460
|
+
"--no-backup",
|
|
1461
|
+
action="store_true",
|
|
1462
|
+
help="Skip creating backup (not recommended)"
|
|
1463
|
+
)
|
|
1464
|
+
|
|
1465
|
+
clean_history_parser.add_argument(
|
|
1466
|
+
"--backup-location",
|
|
1467
|
+
help="Custom backup location (default: <repo>_backup_<timestamp>)"
|
|
1468
|
+
)
|
|
1469
|
+
|
|
1470
|
+
clean_history_parser.add_argument(
|
|
1471
|
+
"--force",
|
|
1472
|
+
action="store_true",
|
|
1473
|
+
help="Skip confirmation prompts"
|
|
1474
|
+
)
|
|
1475
|
+
|
|
1476
|
+
# Add manage-gitkeep command
|
|
1477
|
+
gitkeep_parser = subparsers.add_parser(
|
|
1478
|
+
"manage-gitkeep",
|
|
1479
|
+
help="Manage .gitkeep files in directories",
|
|
1480
|
+
description="""
|
|
1481
|
+
Manage .gitkeep files based on directory contents.
|
|
1482
|
+
|
|
1483
|
+
This command automatically:
|
|
1484
|
+
- Adds .gitkeep files to empty directories to ensure they are tracked by Git
|
|
1485
|
+
- Removes .gitkeep files from directories that contain other files
|
|
1486
|
+
|
|
1487
|
+
This is useful for maintaining clean Git history while ensuring empty directories
|
|
1488
|
+
are properly tracked.
|
|
1489
|
+
""",
|
|
1490
|
+
epilog="""
|
|
1491
|
+
Examples:
|
|
1492
|
+
# Manage .gitkeep files in current directory
|
|
1493
|
+
repokit manage-gitkeep
|
|
1494
|
+
|
|
1495
|
+
# Manage .gitkeep files in specific directory
|
|
1496
|
+
repokit manage-gitkeep --directory ./src
|
|
1497
|
+
|
|
1498
|
+
# Process only top-level directories (no recursion)
|
|
1499
|
+
repokit manage-gitkeep --no-recursive
|
|
1500
|
+
|
|
1501
|
+
# Preview changes without applying them
|
|
1502
|
+
repokit manage-gitkeep --dry-run
|
|
1503
|
+
""",
|
|
1504
|
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
1505
|
+
)
|
|
1506
|
+
|
|
1507
|
+
gitkeep_parser.add_argument(
|
|
1508
|
+
"--directory",
|
|
1509
|
+
"-d",
|
|
1510
|
+
default=".",
|
|
1511
|
+
help="Directory to process (default: current directory)"
|
|
1512
|
+
)
|
|
1513
|
+
|
|
1514
|
+
gitkeep_parser.add_argument(
|
|
1515
|
+
"--recursive",
|
|
1516
|
+
action="store_true",
|
|
1517
|
+
default=True,
|
|
1518
|
+
help="Process subdirectories recursively (default: True)"
|
|
1519
|
+
)
|
|
1520
|
+
|
|
1521
|
+
gitkeep_parser.add_argument(
|
|
1522
|
+
"--no-recursive",
|
|
1523
|
+
action="store_true",
|
|
1524
|
+
help="Do not process subdirectories recursively"
|
|
1525
|
+
)
|
|
1526
|
+
|
|
1527
|
+
gitkeep_parser.add_argument(
|
|
1528
|
+
"--dry-run",
|
|
1529
|
+
action="store_true",
|
|
1530
|
+
help="Show what would be changed without actually making changes"
|
|
1531
|
+
)
|
|
1532
|
+
|
|
1533
|
+
# Add analyze-history command
|
|
1534
|
+
analyze_history_parser = subparsers.add_parser(
|
|
1535
|
+
"analyze-history",
|
|
1536
|
+
help="Analyze repository history for sensitive data",
|
|
1537
|
+
description="""
|
|
1538
|
+
Analyze repository history to identify sensitive data that should be cleaned.
|
|
1539
|
+
|
|
1540
|
+
This command scans your repository history and reports:
|
|
1541
|
+
- Private directories (private/, logs/, etc.)
|
|
1542
|
+
- Windows compatibility issues (nul, con, aux files)
|
|
1543
|
+
- Large files that might not belong
|
|
1544
|
+
- Potential secrets (coming soon)
|
|
1545
|
+
- Branch structure and commit count
|
|
1546
|
+
|
|
1547
|
+
Use this before clean-history to understand what needs cleaning.
|
|
1548
|
+
""",
|
|
1549
|
+
epilog="""
|
|
1550
|
+
Examples:
|
|
1551
|
+
# Analyze current repository
|
|
1552
|
+
repokit analyze-history
|
|
1553
|
+
|
|
1554
|
+
# Analyze with detailed output
|
|
1555
|
+
repokit analyze-history -v
|
|
1556
|
+
""",
|
|
1557
|
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
1558
|
+
)
|
|
1559
|
+
|
|
1560
|
+
return parser.parse_args()
|
|
1561
|
+
|
|
1562
|
+
|
|
1563
|
+
def setup_logging(verbosity: int, quiet: bool = False) -> None:
|
|
1564
|
+
"""
|
|
1565
|
+
Set up logging based on verbosity level.
|
|
1566
|
+
|
|
1567
|
+
Args:
|
|
1568
|
+
verbosity: Verbosity level (0-3+)
|
|
1569
|
+
quiet: Whether to suppress all output except errors
|
|
1570
|
+
"""
|
|
1571
|
+
if quiet:
|
|
1572
|
+
log_level = logging.ERROR
|
|
1573
|
+
else:
|
|
1574
|
+
# Map verbosity levels to logging levels
|
|
1575
|
+
log_levels = {
|
|
1576
|
+
0: logging.WARNING, # Default - only warnings and errors
|
|
1577
|
+
1: logging.INFO, # -v - basic operations
|
|
1578
|
+
2: logging.DEBUG, # -vv - file operations and detailed flow
|
|
1579
|
+
3: logging.DEBUG, # -vvv - pattern matching and cleanup details
|
|
1580
|
+
}
|
|
1581
|
+
# Cap at maximum level
|
|
1582
|
+
capped_verbosity = min(verbosity, 3)
|
|
1583
|
+
log_level = log_levels[capped_verbosity]
|
|
1584
|
+
|
|
1585
|
+
# Configure basic logging
|
|
1586
|
+
logging.basicConfig(
|
|
1587
|
+
level=log_level,
|
|
1588
|
+
format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
|
|
1589
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
1590
|
+
)
|
|
1591
|
+
|
|
1592
|
+
# For highest verbosity, adjust formatter to include more details
|
|
1593
|
+
if verbosity >= 3:
|
|
1594
|
+
for handler in logging.root.handlers:
|
|
1595
|
+
handler.setFormatter(
|
|
1596
|
+
logging.Formatter(
|
|
1597
|
+
"%(asctime)s - %(levelname)s - %(name)s - %(filename)s:%(lineno)d - %(message)s",
|
|
1598
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
1599
|
+
)
|
|
1600
|
+
)
|
|
1601
|
+
|
|
1602
|
+
# Get logger for repokit
|
|
1603
|
+
logger = logging.getLogger("repokit")
|
|
1604
|
+
|
|
1605
|
+
# Set up verbosity-specific loggers for detailed debugging
|
|
1606
|
+
if verbosity >= 2:
|
|
1607
|
+
# Enable detailed file operation logging
|
|
1608
|
+
logging.getLogger("repokit.utils").setLevel(logging.DEBUG)
|
|
1609
|
+
logging.getLogger("repokit.repo_manager").setLevel(logging.DEBUG)
|
|
1610
|
+
|
|
1611
|
+
if verbosity >= 3:
|
|
1612
|
+
# Enable pattern matching and cleanup debugging
|
|
1613
|
+
logging.getLogger("repokit.cleanup").setLevel(logging.DEBUG)
|
|
1614
|
+
logging.getLogger("repokit.patterns").setLevel(logging.DEBUG)
|
|
1615
|
+
|
|
1616
|
+
# Log verbosity level for debugging
|
|
1617
|
+
if verbosity >= 1:
|
|
1618
|
+
logger.info(f"Verbosity level: {verbosity} (-{'v' * verbosity})")
|
|
1619
|
+
if verbosity >= 2:
|
|
1620
|
+
logger.info("File operations debugging enabled")
|
|
1621
|
+
if verbosity >= 3:
|
|
1622
|
+
logger.info("Pattern matching and cleanup debugging enabled")
|
|
1623
|
+
|
|
1624
|
+
|
|
1625
|
+
def _known_language_names():
|
|
1626
|
+
"""The languages the layers' manifest names (for help text); generic alone
|
|
1627
|
+
when the manifest cannot be read."""
|
|
1628
|
+
try:
|
|
1629
|
+
from . import layers
|
|
1630
|
+
root = layers.layers_root()
|
|
1631
|
+
return layers.known_languages(layers.load_manifest(root)) if root else [layers.GENERIC]
|
|
1632
|
+
except Exception:
|
|
1633
|
+
return ["generic"]
|
|
1634
|
+
|
|
1635
|
+
|
|
1636
|
+
def args_to_config(args: argparse.Namespace) -> Dict[str, Any]:
|
|
1637
|
+
"""
|
|
1638
|
+
Convert command-line arguments to configuration dictionary.
|
|
1639
|
+
|
|
1640
|
+
Args:
|
|
1641
|
+
args: Parsed command-line arguments
|
|
1642
|
+
|
|
1643
|
+
Returns:
|
|
1644
|
+
Configuration dictionary
|
|
1645
|
+
"""
|
|
1646
|
+
# --language is a list (repeated, or comma-separated); `language` keeps the
|
|
1647
|
+
# first for everything that reads one value, `languages` carries them all.
|
|
1648
|
+
languages = []
|
|
1649
|
+
if getattr(args, "language", None):
|
|
1650
|
+
from .layers import parse_languages
|
|
1651
|
+
languages = parse_languages(args.language)
|
|
1652
|
+
|
|
1653
|
+
# Base configuration from CLI arguments
|
|
1654
|
+
cli_config = {
|
|
1655
|
+
"name": getattr(args, "name", None),
|
|
1656
|
+
"language": languages[0] if languages else None,
|
|
1657
|
+
"description": getattr(args, "description", None),
|
|
1658
|
+
"private_branch": getattr(args, "private_branch", None),
|
|
1659
|
+
"user": (
|
|
1660
|
+
{"name": args.user_name, "email": args.user_email}
|
|
1661
|
+
if getattr(args, "user_name", None) or getattr(args, "user_email", None)
|
|
1662
|
+
else None
|
|
1663
|
+
),
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
if languages:
|
|
1667
|
+
cli_config["languages"] = languages
|
|
1668
|
+
if getattr(args, "with_layers", None):
|
|
1669
|
+
from .layers import parse_languages
|
|
1670
|
+
cli_config["with"] = parse_languages(args.with_layers)
|
|
1671
|
+
if getattr(args, "license", None):
|
|
1672
|
+
cli_config["license"] = args.license
|
|
1673
|
+
cli_config["license_cli"] = True # the flag beats license_private; a config value does not
|
|
1674
|
+
if getattr(args, "organization", None):
|
|
1675
|
+
cli_config["organization"] = args.organization # the owner in the extras' URLs
|
|
1676
|
+
if getattr(args, "github_setup", None):
|
|
1677
|
+
from .layers import parse_languages as _split_steps
|
|
1678
|
+
cli_config["github_setup"] = _split_steps(args.github_setup)
|
|
1679
|
+
if getattr(args, "topics", None):
|
|
1680
|
+
cli_config["topics"] = [t.strip() for t in args.topics.split(",") if t.strip()]
|
|
1681
|
+
if getattr(args, "badges", None):
|
|
1682
|
+
cli_config["badges"] = args.badges.strip()
|
|
1683
|
+
if getattr(args, "no_extras", False):
|
|
1684
|
+
cli_config["extras"] = []
|
|
1685
|
+
elif getattr(args, "extras", None):
|
|
1686
|
+
from .layers import parse_languages as _split
|
|
1687
|
+
cli_config["extras"] = _split(args.extras)
|
|
1688
|
+
if getattr(args, "private_repo", False):
|
|
1689
|
+
cli_config["private_repo"] = True
|
|
1690
|
+
|
|
1691
|
+
# Handle directory profile configuration
|
|
1692
|
+
if hasattr(args, "dir_profile") and args.dir_profile:
|
|
1693
|
+
cli_config["directory_profile"] = args.dir_profile
|
|
1694
|
+
|
|
1695
|
+
if hasattr(args, "dir_groups") and args.dir_groups:
|
|
1696
|
+
cli_config["directory_groups"] = [
|
|
1697
|
+
group.strip() for group in args.dir_groups.split(",")
|
|
1698
|
+
]
|
|
1699
|
+
|
|
1700
|
+
if hasattr(args, "private_set") and args.private_set:
|
|
1701
|
+
cli_config["private_set"] = args.private_set
|
|
1702
|
+
|
|
1703
|
+
# Handle list-type arguments
|
|
1704
|
+
if hasattr(args, "branches") and args.branches:
|
|
1705
|
+
cli_config["branches"] = [branch.strip() for branch in args.branches.split(",")]
|
|
1706
|
+
|
|
1707
|
+
if hasattr(args, "worktrees") and args.worktrees:
|
|
1708
|
+
cli_config["worktrees"] = [
|
|
1709
|
+
worktree.strip() for worktree in args.worktrees.split(",")
|
|
1710
|
+
]
|
|
1711
|
+
|
|
1712
|
+
if hasattr(args, "directories") and args.directories:
|
|
1713
|
+
cli_config["directories"] = [
|
|
1714
|
+
directory.strip() for directory in args.directories.split(",")
|
|
1715
|
+
]
|
|
1716
|
+
|
|
1717
|
+
if hasattr(args, "private_dirs") and args.private_dirs:
|
|
1718
|
+
cli_config["private_dirs"] = [
|
|
1719
|
+
dir.strip() for dir in args.private_dirs.split(",")
|
|
1720
|
+
]
|
|
1721
|
+
|
|
1722
|
+
if hasattr(args, "sensitive_files") and args.sensitive_files:
|
|
1723
|
+
cli_config["sensitive_files"] = [
|
|
1724
|
+
file.strip() for file in args.sensitive_files.split(",")
|
|
1725
|
+
]
|
|
1726
|
+
|
|
1727
|
+
if hasattr(args, "sensitive_patterns") and args.sensitive_patterns:
|
|
1728
|
+
cli_config["sensitive_patterns"] = [
|
|
1729
|
+
pattern.strip() for pattern in args.sensitive_patterns.split(",")
|
|
1730
|
+
]
|
|
1731
|
+
|
|
1732
|
+
# Handle branch strategy
|
|
1733
|
+
if hasattr(args, "branch_strategy") and args.branch_strategy:
|
|
1734
|
+
cli_config["branch_strategy"] = args.branch_strategy
|
|
1735
|
+
elif getattr(args, "command", None) in ("adopt", "migrate"):
|
|
1736
|
+
# Adoption builds the private-branch model only (see defaults)
|
|
1737
|
+
cli_config["branch_strategy"] = DEFAULT_ADOPT_STRATEGY
|
|
1738
|
+
|
|
1739
|
+
if getattr(args, "private_mode", None):
|
|
1740
|
+
cli_config["private_mode"] = args.private_mode
|
|
1741
|
+
|
|
1742
|
+
if getattr(args, "with_common", None) is not None:
|
|
1743
|
+
cli_config["with_common"] = args.with_common
|
|
1744
|
+
|
|
1745
|
+
# Handle AI integration
|
|
1746
|
+
if hasattr(args, "ai") and args.ai and args.ai != "none":
|
|
1747
|
+
cli_config["ai_integration"] = args.ai
|
|
1748
|
+
|
|
1749
|
+
# Handle backup configuration
|
|
1750
|
+
if hasattr(args, "backup") and args.backup:
|
|
1751
|
+
cli_config["backup"] = True
|
|
1752
|
+
|
|
1753
|
+
if hasattr(args, "backup_location") and args.backup_location:
|
|
1754
|
+
cli_config["backup_location"] = args.backup_location
|
|
1755
|
+
|
|
1756
|
+
# Handle migration strategy
|
|
1757
|
+
if hasattr(args, "migration_strategy") and args.migration_strategy:
|
|
1758
|
+
cli_config["migration_strategy"] = args.migration_strategy
|
|
1759
|
+
|
|
1760
|
+
# Handle remote publishing configuration
|
|
1761
|
+
if hasattr(args, "publish_to") and args.publish_to:
|
|
1762
|
+
cli_config["publish_to"] = args.publish_to
|
|
1763
|
+
|
|
1764
|
+
if hasattr(args, "repo_name") and args.repo_name:
|
|
1765
|
+
cli_config["repo_name"] = args.repo_name
|
|
1766
|
+
|
|
1767
|
+
if hasattr(args, "private_repo") and args.private_repo:
|
|
1768
|
+
cli_config["private_repo"] = True
|
|
1769
|
+
|
|
1770
|
+
# Handle branch directory mappings
|
|
1771
|
+
branch_directories = {}
|
|
1772
|
+
|
|
1773
|
+
if hasattr(args, "branch_dir_main") and args.branch_dir_main:
|
|
1774
|
+
branch_directories["main"] = args.branch_dir_main
|
|
1775
|
+
|
|
1776
|
+
if hasattr(args, "branch_dir_dev") and args.branch_dir_dev:
|
|
1777
|
+
branch_directories["dev"] = args.branch_dir_dev
|
|
1778
|
+
|
|
1779
|
+
if branch_directories:
|
|
1780
|
+
cli_config["branch_config"] = {"branch_directories": branch_directories}
|
|
1781
|
+
|
|
1782
|
+
# default branch from CLI
|
|
1783
|
+
if hasattr(args, "default_branch") and args.default_branch:
|
|
1784
|
+
cli_config["default_branch"] = args.default_branch
|
|
1785
|
+
|
|
1786
|
+
# Remove None values from config
|
|
1787
|
+
return {k: v for k, v in cli_config.items() if v is not None}
|
|
1788
|
+
|
|
1789
|
+
|
|
1790
|
+
def main() -> int:
|
|
1791
|
+
"""
|
|
1792
|
+
Main entry point.
|
|
1793
|
+
|
|
1794
|
+
Returns:
|
|
1795
|
+
Exit code (0 for success, non-zero for failure)
|
|
1796
|
+
"""
|
|
1797
|
+
# Status output uses symbols a non-UTF-8 console (Windows' default code
|
|
1798
|
+
# page, output piped to a tool) cannot encode; replace them rather than
|
|
1799
|
+
# crash after the work is done.
|
|
1800
|
+
for stream in (sys.stdout, sys.stderr):
|
|
1801
|
+
try:
|
|
1802
|
+
stream.reconfigure(errors="replace")
|
|
1803
|
+
except (AttributeError, ValueError):
|
|
1804
|
+
pass
|
|
1805
|
+
|
|
1806
|
+
args = parse_arguments()
|
|
1807
|
+
|
|
1808
|
+
# Set up logging based on verbosity
|
|
1809
|
+
setup_logging(args.verbose, args.quiet)
|
|
1810
|
+
logger = logging.getLogger("repokit")
|
|
1811
|
+
|
|
1812
|
+
# Initialize configuration manager
|
|
1813
|
+
config_manager = ConfigManager(verbose=args.verbose)
|
|
1814
|
+
|
|
1815
|
+
# Load config file if provided
|
|
1816
|
+
if args.config and os.path.exists(args.config):
|
|
1817
|
+
config_manager.load_config_file(args.config)
|
|
1818
|
+
|
|
1819
|
+
# Load branch configuration file if provided
|
|
1820
|
+
if hasattr(args, 'branch_config') and args.branch_config and os.path.exists(args.branch_config):
|
|
1821
|
+
config_manager.load_branch_config_file(args.branch_config)
|
|
1822
|
+
|
|
1823
|
+
# Add CLI arguments to configuration
|
|
1824
|
+
cli_config = args_to_config(args)
|
|
1825
|
+
config_manager.set_cli_config(cli_config)
|
|
1826
|
+
|
|
1827
|
+
# Get the final, merged configuration
|
|
1828
|
+
config = config_manager.get_config()
|
|
1829
|
+
|
|
1830
|
+
# Add remote integration configuration
|
|
1831
|
+
if hasattr(args, 'publish_to') and args.publish_to:
|
|
1832
|
+
service_config = config.get(args.publish_to, {})
|
|
1833
|
+
# Ensure service_config is a dict, not a boolean
|
|
1834
|
+
if not isinstance(service_config, dict):
|
|
1835
|
+
service_config = {}
|
|
1836
|
+
if hasattr(args, 'organization') and args.organization:
|
|
1837
|
+
if args.publish_to == "github":
|
|
1838
|
+
service_config["organization"] = args.organization
|
|
1839
|
+
else:
|
|
1840
|
+
service_config["group"] = args.organization
|
|
1841
|
+
config[args.publish_to] = service_config
|
|
1842
|
+
|
|
1843
|
+
# Validate configuration
|
|
1844
|
+
errors = config_manager.validate_config()
|
|
1845
|
+
if errors:
|
|
1846
|
+
for error in errors:
|
|
1847
|
+
logger.error(f"Configuration error: {error}")
|
|
1848
|
+
return 1
|
|
1849
|
+
|
|
1850
|
+
# Save configuration if requested
|
|
1851
|
+
if hasattr(args, 'save_config') and args.save_config:
|
|
1852
|
+
config_manager.save_config(args.save_config)
|
|
1853
|
+
|
|
1854
|
+
# Handle commands
|
|
1855
|
+
if args.command == "list":
|
|
1856
|
+
from . import layers as _layers
|
|
1857
|
+
from .template_engine import TemplateEngine as _TemplateEngine
|
|
1858
|
+
_root = _layers.layers_root(_TemplateEngine(templates_dir=getattr(args, "templates_dir", None)))
|
|
1859
|
+
if _root is None:
|
|
1860
|
+
logger.error("no code-type layers found with the templates")
|
|
1861
|
+
return 1
|
|
1862
|
+
try:
|
|
1863
|
+
for line in _layers.catalogue_lines(_layers.load_manifest(_root), args.language):
|
|
1864
|
+
print(line)
|
|
1865
|
+
except _layers.LayerError as e:
|
|
1866
|
+
logger.error(str(e))
|
|
1867
|
+
return 1
|
|
1868
|
+
return 0
|
|
1869
|
+
|
|
1870
|
+
elif args.command == "list-templates":
|
|
1871
|
+
template_engine = TemplateEngine(
|
|
1872
|
+
templates_dir=args.templates_dir, verbose=args.verbose
|
|
1873
|
+
)
|
|
1874
|
+
templates = template_engine.list_templates()
|
|
1875
|
+
print("Available templates:")
|
|
1876
|
+
for template in sorted(templates):
|
|
1877
|
+
print(f" {template}")
|
|
1878
|
+
return 0
|
|
1879
|
+
|
|
1880
|
+
elif args.command == "init-config":
|
|
1881
|
+
# Create a default configuration file in the current directory
|
|
1882
|
+
config_path = os.path.join(os.getcwd(), ".repokit.json")
|
|
1883
|
+
if os.path.exists(config_path):
|
|
1884
|
+
logger.error(f"Configuration file already exists at {config_path}")
|
|
1885
|
+
return 1
|
|
1886
|
+
|
|
1887
|
+
config_manager.save_config(config_path)
|
|
1888
|
+
logger.info(f"Created default configuration at {config_path}")
|
|
1889
|
+
print(f"Created default configuration at {config_path}")
|
|
1890
|
+
print("Edit this file to customize your repository settings.")
|
|
1891
|
+
return 0
|
|
1892
|
+
|
|
1893
|
+
elif args.command == "store-credentials":
|
|
1894
|
+
from .remote_integration import RemoteIntegration
|
|
1895
|
+
from .auth_integration import AuthenticationHandler
|
|
1896
|
+
|
|
1897
|
+
if not args.publish_to:
|
|
1898
|
+
logger.error(
|
|
1899
|
+
"Please specify a service with --publish-to (github or gitlab)"
|
|
1900
|
+
)
|
|
1901
|
+
return 1
|
|
1902
|
+
|
|
1903
|
+
if not args.token and not args.token_command:
|
|
1904
|
+
logger.error("Please provide either --token or --token-command")
|
|
1905
|
+
return 1
|
|
1906
|
+
|
|
1907
|
+
auth_handler = AuthenticationHandler(verbose=args.verbose)
|
|
1908
|
+
|
|
1909
|
+
# If token_command is provided, use it to get the token
|
|
1910
|
+
if args.token_command:
|
|
1911
|
+
token = auth_handler.get_token(args.publish_to, args.token_command)
|
|
1912
|
+
if not token:
|
|
1913
|
+
logger.error(
|
|
1914
|
+
f"Failed to retrieve token using command: {args.token_command}"
|
|
1915
|
+
)
|
|
1916
|
+
return 1
|
|
1917
|
+
else:
|
|
1918
|
+
token = args.token
|
|
1919
|
+
|
|
1920
|
+
# Store the credentials
|
|
1921
|
+
success = auth_handler.store_credentials(
|
|
1922
|
+
args.publish_to, token, args.organization, args.credentials_file
|
|
1923
|
+
)
|
|
1924
|
+
|
|
1925
|
+
if success:
|
|
1926
|
+
logger.info(f"Successfully stored credentials for {args.publish_to}")
|
|
1927
|
+
print(f"Credentials stored successfully for {args.publish_to}")
|
|
1928
|
+
return 0
|
|
1929
|
+
else:
|
|
1930
|
+
logger.error(f"Failed to store credentials for {args.publish_to}")
|
|
1931
|
+
return 1
|
|
1932
|
+
|
|
1933
|
+
elif args.command == "setup-guardrails":
|
|
1934
|
+
from .guardrails import setup_guardrails_command
|
|
1935
|
+
return setup_guardrails_command(args)
|
|
1936
|
+
|
|
1937
|
+
elif args.command == "check-guardrails":
|
|
1938
|
+
from .guardrails import check_guardrails_command
|
|
1939
|
+
return check_guardrails_command(args)
|
|
1940
|
+
|
|
1941
|
+
elif args.command == "safe-merge":
|
|
1942
|
+
from .guardrails import GuardrailsManager
|
|
1943
|
+
|
|
1944
|
+
manager = GuardrailsManager()
|
|
1945
|
+
|
|
1946
|
+
if args.preview:
|
|
1947
|
+
success = manager.safe_merge(args.source_branch, preview=True)
|
|
1948
|
+
else:
|
|
1949
|
+
success = manager.safe_merge(
|
|
1950
|
+
args.source_branch,
|
|
1951
|
+
no_commit=args.no_commit,
|
|
1952
|
+
no_ff=args.no_ff
|
|
1953
|
+
)
|
|
1954
|
+
|
|
1955
|
+
return 0 if success else 1
|
|
1956
|
+
|
|
1957
|
+
elif args.command == "safe-merge-dev":
|
|
1958
|
+
from .history_protection import HistoryProtectionManager
|
|
1959
|
+
|
|
1960
|
+
# Get configuration for history protection
|
|
1961
|
+
history_config = config.get('history_protection', {})
|
|
1962
|
+
|
|
1963
|
+
# Initialize manager
|
|
1964
|
+
manager = HistoryProtectionManager(
|
|
1965
|
+
repo_path=os.getcwd(),
|
|
1966
|
+
config=config,
|
|
1967
|
+
verbose=args.verbose
|
|
1968
|
+
)
|
|
1969
|
+
|
|
1970
|
+
# Determine squash behavior
|
|
1971
|
+
squash = None
|
|
1972
|
+
if args.squash:
|
|
1973
|
+
squash = True
|
|
1974
|
+
elif args.no_squash:
|
|
1975
|
+
squash = False
|
|
1976
|
+
|
|
1977
|
+
# Perform the merge
|
|
1978
|
+
success = manager.safe_merge_dev(
|
|
1979
|
+
branch=args.branch,
|
|
1980
|
+
target_branch=args.target,
|
|
1981
|
+
squash=squash,
|
|
1982
|
+
message=args.message,
|
|
1983
|
+
preserve_last=args.preserve_last,
|
|
1984
|
+
dry_run=args.preview
|
|
1985
|
+
)
|
|
1986
|
+
|
|
1987
|
+
return 0 if success else 1
|
|
1988
|
+
|
|
1989
|
+
elif args.command == "clean-history":
|
|
1990
|
+
from .history_cleaner import HistoryCleaner, CleaningConfig, CleaningRecipe, GitFilterRepoNotFound
|
|
1991
|
+
|
|
1992
|
+
try:
|
|
1993
|
+
# Initialize cleaner
|
|
1994
|
+
cleaner = HistoryCleaner(repo_path=os.getcwd(), verbose=args.verbose)
|
|
1995
|
+
|
|
1996
|
+
# Interactive mode if no recipe specified
|
|
1997
|
+
if not args.recipe and not args.remove_paths:
|
|
1998
|
+
# Analyze repository
|
|
1999
|
+
analysis = cleaner.analyze_repository()
|
|
2000
|
+
|
|
2001
|
+
print("\n=== Repository Analysis ===")
|
|
2002
|
+
print(f"Total commits: {analysis['total_commits']}")
|
|
2003
|
+
print(f"Branches: {len(analysis['branches'])}")
|
|
2004
|
+
|
|
2005
|
+
if analysis['private_paths']:
|
|
2006
|
+
print(f"\nFound {len(analysis['private_paths'])} private files:")
|
|
2007
|
+
for path in analysis['private_paths'][:10]:
|
|
2008
|
+
print(f" - {path}")
|
|
2009
|
+
if len(analysis['private_paths']) > 10:
|
|
2010
|
+
print(f" ... and {len(analysis['private_paths']) - 10} more")
|
|
2011
|
+
|
|
2012
|
+
if analysis['windows_issues']:
|
|
2013
|
+
print(f"\nFound {len(analysis['windows_issues'])} Windows compatibility issues:")
|
|
2014
|
+
for path in analysis['windows_issues'][:5]:
|
|
2015
|
+
print(f" - {path}")
|
|
2016
|
+
if len(analysis['windows_issues']) > 5:
|
|
2017
|
+
print(f" ... and {len(analysis['windows_issues']) - 5} more")
|
|
2018
|
+
|
|
2019
|
+
# Suggest recipe
|
|
2020
|
+
if analysis['private_paths']:
|
|
2021
|
+
print("\n💡 Suggestion: Use --recipe pre-open-source to remove private files")
|
|
2022
|
+
if analysis['windows_issues']:
|
|
2023
|
+
print("💡 Suggestion: Use --recipe windows-safe to fix Windows issues")
|
|
2024
|
+
|
|
2025
|
+
return 0
|
|
2026
|
+
|
|
2027
|
+
# Build configuration
|
|
2028
|
+
if args.recipe:
|
|
2029
|
+
recipe = CleaningRecipe(args.recipe)
|
|
2030
|
+
config = cleaner.get_recipe_config(recipe)
|
|
2031
|
+
else:
|
|
2032
|
+
config = CleaningConfig(recipe=CleaningRecipe.CUSTOM)
|
|
2033
|
+
|
|
2034
|
+
# Override with command-line options
|
|
2035
|
+
if args.remove_paths:
|
|
2036
|
+
config.paths_to_remove = args.remove_paths
|
|
2037
|
+
if args.cutoff_sha:
|
|
2038
|
+
config.cutoff_sha = args.cutoff_sha
|
|
2039
|
+
if args.cutoff_date:
|
|
2040
|
+
config.cutoff_date = args.cutoff_date
|
|
2041
|
+
|
|
2042
|
+
config.dry_run = args.dry_run
|
|
2043
|
+
config.force = args.force
|
|
2044
|
+
if args.no_backup:
|
|
2045
|
+
config.backup_location = 'skip'
|
|
2046
|
+
elif args.backup_location:
|
|
2047
|
+
config.backup_location = args.backup_location
|
|
2048
|
+
|
|
2049
|
+
# Preview if requested
|
|
2050
|
+
if args.dry_run:
|
|
2051
|
+
preview = cleaner.preview_cleaning(config)
|
|
2052
|
+
print("\n=== Cleaning Preview ===")
|
|
2053
|
+
print(f"Recipe: {preview['recipe']}")
|
|
2054
|
+
if preview['paths_to_remove']:
|
|
2055
|
+
print(f"Paths to remove: {', '.join(preview['paths_to_remove'])}")
|
|
2056
|
+
if preview['estimated_impact']:
|
|
2057
|
+
print(f"Estimated impact: {preview['estimated_impact']}")
|
|
2058
|
+
if preview['warnings']:
|
|
2059
|
+
print("\nWarnings:")
|
|
2060
|
+
for warning in preview['warnings']:
|
|
2061
|
+
print(f" ⚠️ {warning}")
|
|
2062
|
+
|
|
2063
|
+
# Execute cleaning
|
|
2064
|
+
success = cleaner.clean_history(config)
|
|
2065
|
+
return 0 if success else 1
|
|
2066
|
+
|
|
2067
|
+
except GitFilterRepoNotFound as e:
|
|
2068
|
+
logger.error(str(e))
|
|
2069
|
+
print("\nTo install git filter-repo:")
|
|
2070
|
+
print(" pip install git-filter-repo")
|
|
2071
|
+
print("\nOr see: https://github.com/newren/git-filter-repo")
|
|
2072
|
+
return 1
|
|
2073
|
+
except Exception as e:
|
|
2074
|
+
logger.error(f"Error cleaning history: {str(e)}")
|
|
2075
|
+
if args.verbose >= 2:
|
|
2076
|
+
import traceback
|
|
2077
|
+
traceback.print_exc()
|
|
2078
|
+
return 1
|
|
2079
|
+
|
|
2080
|
+
elif args.command == "manage-gitkeep":
|
|
2081
|
+
from .directory_profiles import DirectoryProfileManager
|
|
2082
|
+
|
|
2083
|
+
try:
|
|
2084
|
+
# Resolve directory path
|
|
2085
|
+
directory = os.path.abspath(args.directory)
|
|
2086
|
+
if not os.path.exists(directory):
|
|
2087
|
+
logger.error(f"Directory does not exist: {directory}")
|
|
2088
|
+
return 1
|
|
2089
|
+
|
|
2090
|
+
if not os.path.isdir(directory):
|
|
2091
|
+
logger.error(f"Path is not a directory: {directory}")
|
|
2092
|
+
return 1
|
|
2093
|
+
|
|
2094
|
+
# Determine recursiveness (--no-recursive overrides default)
|
|
2095
|
+
recursive = args.recursive and not args.no_recursive
|
|
2096
|
+
|
|
2097
|
+
# Initialize directory profile manager
|
|
2098
|
+
profile_manager = DirectoryProfileManager(verbose=args.verbose)
|
|
2099
|
+
|
|
2100
|
+
if args.dry_run:
|
|
2101
|
+
logger.info(f"DRY RUN: Analyzing .gitkeep files in {directory} (recursive: {recursive})")
|
|
2102
|
+
|
|
2103
|
+
# Create a temporary function to simulate the changes
|
|
2104
|
+
def simulate_manage_gitkeep(base_path: str, recursive: bool = True):
|
|
2105
|
+
result = {"added": [], "removed": []}
|
|
2106
|
+
|
|
2107
|
+
def process_directory(dir_path: str, relative_path: str = ""):
|
|
2108
|
+
if not os.path.isdir(dir_path):
|
|
2109
|
+
return
|
|
2110
|
+
|
|
2111
|
+
gitkeep_path = os.path.join(dir_path, ".gitkeep")
|
|
2112
|
+
gitkeep_exists = os.path.exists(gitkeep_path)
|
|
2113
|
+
|
|
2114
|
+
try:
|
|
2115
|
+
items = [item for item in os.listdir(dir_path) if item != ".gitkeep"]
|
|
2116
|
+
except PermissionError:
|
|
2117
|
+
return
|
|
2118
|
+
|
|
2119
|
+
non_hidden_items = [item for item in items if not item.startswith(".")]
|
|
2120
|
+
is_empty = len(non_hidden_items) == 0
|
|
2121
|
+
|
|
2122
|
+
if is_empty and not gitkeep_exists:
|
|
2123
|
+
result["added"].append(os.path.join(relative_path, ".gitkeep") if relative_path else ".gitkeep")
|
|
2124
|
+
elif not is_empty and gitkeep_exists:
|
|
2125
|
+
result["removed"].append(os.path.join(relative_path, ".gitkeep") if relative_path else ".gitkeep")
|
|
2126
|
+
|
|
2127
|
+
if recursive:
|
|
2128
|
+
for item in items:
|
|
2129
|
+
item_path = os.path.join(dir_path, item)
|
|
2130
|
+
if os.path.isdir(item_path) and not item.startswith("."):
|
|
2131
|
+
item_relative = os.path.join(relative_path, item) if relative_path else item
|
|
2132
|
+
process_directory(item_path, item_relative)
|
|
2133
|
+
|
|
2134
|
+
process_directory(base_path)
|
|
2135
|
+
return result
|
|
2136
|
+
|
|
2137
|
+
result = simulate_manage_gitkeep(directory, recursive)
|
|
2138
|
+
else:
|
|
2139
|
+
# Actually manage the .gitkeep files
|
|
2140
|
+
logger.info(f"Managing .gitkeep files in {directory} (recursive: {recursive})")
|
|
2141
|
+
result = profile_manager.manage_gitkeep_files(directory, recursive)
|
|
2142
|
+
|
|
2143
|
+
# Report results
|
|
2144
|
+
if result["added"]:
|
|
2145
|
+
if args.dry_run:
|
|
2146
|
+
print(f"\nWould ADD .gitkeep files to {len(result['added'])} empty directories:")
|
|
2147
|
+
else:
|
|
2148
|
+
print(f"\nADDED .gitkeep files to {len(result['added'])} empty directories:")
|
|
2149
|
+
for file_path in result["added"]:
|
|
2150
|
+
print(f" + {file_path}")
|
|
2151
|
+
|
|
2152
|
+
if result["removed"]:
|
|
2153
|
+
if args.dry_run:
|
|
2154
|
+
print(f"\nWould REMOVE .gitkeep files from {len(result['removed'])} non-empty directories:")
|
|
2155
|
+
else:
|
|
2156
|
+
print(f"\nREMOVED .gitkeep files from {len(result['removed'])} non-empty directories:")
|
|
2157
|
+
for file_path in result["removed"]:
|
|
2158
|
+
print(f" - {file_path}")
|
|
2159
|
+
|
|
2160
|
+
if not result["added"] and not result["removed"]:
|
|
2161
|
+
print("\nNo .gitkeep file changes needed.")
|
|
2162
|
+
else:
|
|
2163
|
+
total_changes = len(result["added"]) + len(result["removed"])
|
|
2164
|
+
if args.dry_run:
|
|
2165
|
+
print(f"\nDry run complete. {total_changes} changes would be made.")
|
|
2166
|
+
else:
|
|
2167
|
+
print(f"\nManagement complete. {total_changes} changes made.")
|
|
2168
|
+
|
|
2169
|
+
return 0
|
|
2170
|
+
|
|
2171
|
+
except Exception as e:
|
|
2172
|
+
logger.error(f"Error managing .gitkeep files: {str(e)}")
|
|
2173
|
+
if args.verbose >= 2:
|
|
2174
|
+
import traceback
|
|
2175
|
+
traceback.print_exc()
|
|
2176
|
+
return 1
|
|
2177
|
+
|
|
2178
|
+
elif args.command == "analyze-history":
|
|
2179
|
+
from .history_cleaner import HistoryCleaner, GitFilterRepoNotFound
|
|
2180
|
+
|
|
2181
|
+
try:
|
|
2182
|
+
# Initialize cleaner
|
|
2183
|
+
cleaner = HistoryCleaner(repo_path=os.getcwd(), verbose=args.verbose)
|
|
2184
|
+
|
|
2185
|
+
# Analyze repository
|
|
2186
|
+
analysis = cleaner.analyze_repository()
|
|
2187
|
+
|
|
2188
|
+
print("\n=== Repository History Analysis ===")
|
|
2189
|
+
print(f"Repository: {os.getcwd()}")
|
|
2190
|
+
print(f"Total commits: {analysis['total_commits']}")
|
|
2191
|
+
print(f"Branches: {', '.join(analysis['branches'][:5])}")
|
|
2192
|
+
if len(analysis['branches']) > 5:
|
|
2193
|
+
print(f" ... and {len(analysis['branches']) - 5} more")
|
|
2194
|
+
|
|
2195
|
+
# Private paths
|
|
2196
|
+
if analysis['private_paths']:
|
|
2197
|
+
print(f"\n🔒 Private/Sensitive Paths ({len(analysis['private_paths'])} found):")
|
|
2198
|
+
by_type = {}
|
|
2199
|
+
for path in analysis['private_paths']:
|
|
2200
|
+
if path.startswith('private/'):
|
|
2201
|
+
by_type.setdefault('private/', []).append(path)
|
|
2202
|
+
elif path.startswith('logs/'):
|
|
2203
|
+
by_type.setdefault('logs/', []).append(path)
|
|
2204
|
+
elif path == 'CLAUDE.md':
|
|
2205
|
+
by_type.setdefault('AI files', []).append(path)
|
|
2206
|
+
else:
|
|
2207
|
+
by_type.setdefault('other', []).append(path)
|
|
2208
|
+
|
|
2209
|
+
for type_name, paths in by_type.items():
|
|
2210
|
+
print(f"\n {type_name}: {len(paths)} files")
|
|
2211
|
+
for path in paths[:3]:
|
|
2212
|
+
print(f" - {path}")
|
|
2213
|
+
if len(paths) > 3:
|
|
2214
|
+
print(f" ... and {len(paths) - 3} more")
|
|
2215
|
+
|
|
2216
|
+
# Windows issues
|
|
2217
|
+
if analysis['windows_issues']:
|
|
2218
|
+
print(f"\n⚠️ Windows Compatibility Issues ({len(analysis['windows_issues'])} found):")
|
|
2219
|
+
for path in analysis['windows_issues'][:5]:
|
|
2220
|
+
print(f" - {path}")
|
|
2221
|
+
if len(analysis['windows_issues']) > 5:
|
|
2222
|
+
print(f" ... and {len(analysis['windows_issues']) - 5} more")
|
|
2223
|
+
|
|
2224
|
+
# Recommendations
|
|
2225
|
+
print("\n📋 Recommendations:")
|
|
2226
|
+
if analysis['private_paths']:
|
|
2227
|
+
print(" 1. Remove private files with: repokit clean-history --recipe pre-open-source")
|
|
2228
|
+
if analysis['windows_issues']:
|
|
2229
|
+
print(" 2. Fix Windows issues with: repokit clean-history --recipe windows-safe")
|
|
2230
|
+
if not analysis['private_paths'] and not analysis['windows_issues']:
|
|
2231
|
+
print(" ✅ No sensitive data detected!")
|
|
2232
|
+
|
|
2233
|
+
print("\n💡 Tip: Use --dry-run to preview changes before cleaning")
|
|
2234
|
+
|
|
2235
|
+
return 0
|
|
2236
|
+
|
|
2237
|
+
except GitFilterRepoNotFound as e:
|
|
2238
|
+
logger.error(str(e))
|
|
2239
|
+
print("\nTo install git filter-repo:")
|
|
2240
|
+
print(" pip install git-filter-repo")
|
|
2241
|
+
return 1
|
|
2242
|
+
except Exception as e:
|
|
2243
|
+
logger.error(f"Error analyzing history: {str(e)}")
|
|
2244
|
+
if args.verbose >= 2:
|
|
2245
|
+
import traceback
|
|
2246
|
+
traceback.print_exc()
|
|
2247
|
+
return 1
|
|
2248
|
+
|
|
2249
|
+
elif args.command == "bootstrap":
|
|
2250
|
+
bootstrap_script = os.path.join(
|
|
2251
|
+
os.path.dirname(os.path.dirname(__file__)), "scripts", "bootstrap.py"
|
|
2252
|
+
)
|
|
2253
|
+
|
|
2254
|
+
if not os.path.exists(bootstrap_script):
|
|
2255
|
+
logger.error(f"Bootstrap script not found at {bootstrap_script}")
|
|
2256
|
+
return 1
|
|
2257
|
+
|
|
2258
|
+
# Build arguments for bootstrap script
|
|
2259
|
+
bootstrap_args = [sys.executable, bootstrap_script]
|
|
2260
|
+
|
|
2261
|
+
# Pass through relevant arguments
|
|
2262
|
+
if args.name:
|
|
2263
|
+
bootstrap_args.extend(["--name", args.name])
|
|
2264
|
+
|
|
2265
|
+
if args.publish_to:
|
|
2266
|
+
bootstrap_args.extend(["--service", args.publish_to])
|
|
2267
|
+
|
|
2268
|
+
if args.private_repo:
|
|
2269
|
+
bootstrap_args.append("--private")
|
|
2270
|
+
|
|
2271
|
+
if args.organization:
|
|
2272
|
+
bootstrap_args.extend(["--organization", args.organization])
|
|
2273
|
+
|
|
2274
|
+
if args.output:
|
|
2275
|
+
bootstrap_args.extend(["--config-path", args.output])
|
|
2276
|
+
|
|
2277
|
+
if args.no_push:
|
|
2278
|
+
bootstrap_args.append("--no-publish")
|
|
2279
|
+
|
|
2280
|
+
if args.user_name:
|
|
2281
|
+
bootstrap_args.extend(["--git-user-name", args.user_name])
|
|
2282
|
+
|
|
2283
|
+
if args.user_email:
|
|
2284
|
+
bootstrap_args.extend(["--git-user-email", args.user_email])
|
|
2285
|
+
|
|
2286
|
+
# Add verbosity flags
|
|
2287
|
+
for _ in range(args.verbose):
|
|
2288
|
+
bootstrap_args.append("-v")
|
|
2289
|
+
|
|
2290
|
+
# Execute the bootstrap script
|
|
2291
|
+
logger.info(f"Running bootstrap script: {' '.join(bootstrap_args)}")
|
|
2292
|
+
|
|
2293
|
+
try:
|
|
2294
|
+
# Use subprocess.run for better control over encoding
|
|
2295
|
+
result = subprocess.run(bootstrap_args, encoding='utf-8', errors='replace')
|
|
2296
|
+
exit_code = result.returncode
|
|
2297
|
+
return exit_code
|
|
2298
|
+
except Exception as e:
|
|
2299
|
+
logger.error(f"Error running bootstrap script: {str(e)}")
|
|
2300
|
+
return 1
|
|
2301
|
+
|
|
2302
|
+
elif args.command == "analyze":
|
|
2303
|
+
if not args.name:
|
|
2304
|
+
logger.error("Directory path is required for 'analyze' command")
|
|
2305
|
+
return 1
|
|
2306
|
+
|
|
2307
|
+
try:
|
|
2308
|
+
# Analyze the directory
|
|
2309
|
+
dir_path = os.path.abspath(args.name)
|
|
2310
|
+
if not os.path.exists(dir_path):
|
|
2311
|
+
logger.error(f"Directory not found: {dir_path}")
|
|
2312
|
+
return 1
|
|
2313
|
+
|
|
2314
|
+
# Use comprehensive project analysis
|
|
2315
|
+
summary = analyze_project(dir_path, verbose=args.verbose)
|
|
2316
|
+
|
|
2317
|
+
# Display the enhanced analysis results
|
|
2318
|
+
print(f"\n=== PROJECT ANALYSIS: {dir_path} ===")
|
|
2319
|
+
print(f" Project type: {summary['project_type']}")
|
|
2320
|
+
print(f" Detected language: {summary['detected_language']}")
|
|
2321
|
+
print(f" Migration complexity: {summary['migration_complexity']}")
|
|
2322
|
+
print(f" Recommended strategy: {summary['recommended_strategy']}")
|
|
2323
|
+
print(
|
|
2324
|
+
f" Recommended branch strategy: {summary['recommended_branch_strategy']}"
|
|
2325
|
+
)
|
|
2326
|
+
|
|
2327
|
+
# Git state information
|
|
2328
|
+
git_state = summary.get("git_state", {})
|
|
2329
|
+
if git_state.get("is_repo"):
|
|
2330
|
+
print(f"\n Git repository:")
|
|
2331
|
+
print(
|
|
2332
|
+
f" Current branch: {git_state.get('current_branch', 'Unknown')}"
|
|
2333
|
+
)
|
|
2334
|
+
print(f" Branches: {', '.join(git_state.get('branches', []))}")
|
|
2335
|
+
print(
|
|
2336
|
+
f" Has uncommitted changes: {'Yes' if git_state.get('has_uncommitted_changes') else 'No'}"
|
|
2337
|
+
)
|
|
2338
|
+
print(
|
|
2339
|
+
f" Remotes: {', '.join(git_state.get('remotes', {}).keys()) or 'None'}"
|
|
2340
|
+
)
|
|
2341
|
+
print(f" Branch strategy: {summary['branch_strategy']}")
|
|
2342
|
+
else:
|
|
2343
|
+
print(f" Git repository: No")
|
|
2344
|
+
|
|
2345
|
+
# Directory structure
|
|
2346
|
+
print(f"\n Directory structure:")
|
|
2347
|
+
print(
|
|
2348
|
+
f" Existing standard directories: {', '.join(summary['existing_dirs']) or 'None'}"
|
|
2349
|
+
)
|
|
2350
|
+
print(
|
|
2351
|
+
f" Missing standard directories: {', '.join(summary['missing_dirs']) or 'None'}"
|
|
2352
|
+
)
|
|
2353
|
+
print(
|
|
2354
|
+
f" Special directories: {', '.join(summary['special_dirs']) or 'None'}"
|
|
2355
|
+
)
|
|
2356
|
+
print(
|
|
2357
|
+
f" Has template conflicts: {'Yes' if summary['has_template_conflicts'] else 'No'}"
|
|
2358
|
+
)
|
|
2359
|
+
|
|
2360
|
+
# File information
|
|
2361
|
+
print(f"\n File counts:")
|
|
2362
|
+
for category, count in summary["file_counts"].items():
|
|
2363
|
+
if count > 0:
|
|
2364
|
+
print(f" {category}: {count}")
|
|
2365
|
+
print(f" Total: {summary['total_files']}")
|
|
2366
|
+
|
|
2367
|
+
# Migration recommendations
|
|
2368
|
+
print(f"\n Migration steps:")
|
|
2369
|
+
for i, step in enumerate(summary["migration_steps"], 1):
|
|
2370
|
+
print(f" {i}. {step}")
|
|
2371
|
+
|
|
2372
|
+
# Command suggestions
|
|
2373
|
+
print(f"\n=== NEXT STEPS ===")
|
|
2374
|
+
if summary["project_type"] == "repokit":
|
|
2375
|
+
print(" Project is already using RepoKit structure.")
|
|
2376
|
+
elif summary["project_type"] == "empty":
|
|
2377
|
+
print(" Create new RepoKit project:")
|
|
2378
|
+
print(
|
|
2379
|
+
f" repokit create {os.path.basename(dir_path)} --language {summary['detected_language']}"
|
|
2380
|
+
)
|
|
2381
|
+
else:
|
|
2382
|
+
print(" Migrate to RepoKit structure:")
|
|
2383
|
+
print(
|
|
2384
|
+
f" repokit migrate {args.name} --migration-strategy {summary['recommended_strategy']}"
|
|
2385
|
+
)
|
|
2386
|
+
print(" Or adopt in-place:")
|
|
2387
|
+
print(
|
|
2388
|
+
f" repokit adopt {args.name} --strategy {summary['recommended_strategy']}"
|
|
2389
|
+
)
|
|
2390
|
+
if getattr(args, "publish_to", None):
|
|
2391
|
+
print(f" With publishing:")
|
|
2392
|
+
print(
|
|
2393
|
+
f" repokit adopt {args.name} --publish-to {args.publish_to}"
|
|
2394
|
+
)
|
|
2395
|
+
|
|
2396
|
+
return 0
|
|
2397
|
+
except Exception as e:
|
|
2398
|
+
logger.error(f"Error analyzing directory: {str(e)}")
|
|
2399
|
+
if args.verbose >= 2:
|
|
2400
|
+
import traceback
|
|
2401
|
+
|
|
2402
|
+
traceback.print_exc()
|
|
2403
|
+
return 1
|
|
2404
|
+
|
|
2405
|
+
elif args.command == "migrate":
|
|
2406
|
+
if not args.name:
|
|
2407
|
+
logger.error("Directory path is required for 'migrate' command")
|
|
2408
|
+
return 1
|
|
2409
|
+
|
|
2410
|
+
try:
|
|
2411
|
+
# Analyze and migrate the directory
|
|
2412
|
+
dir_path = os.path.abspath(args.name)
|
|
2413
|
+
if not os.path.exists(dir_path):
|
|
2414
|
+
logger.error(f"Directory not found: {dir_path}")
|
|
2415
|
+
return 1
|
|
2416
|
+
|
|
2417
|
+
# First show the analysis
|
|
2418
|
+
summary = analyze_directory(dir_path, verbose=args.verbose)
|
|
2419
|
+
|
|
2420
|
+
print(f"\nAnalysis of {dir_path}:")
|
|
2421
|
+
print(
|
|
2422
|
+
f" Existing standard directories: {', '.join(summary['existing_dirs']) or 'None'}"
|
|
2423
|
+
)
|
|
2424
|
+
print(
|
|
2425
|
+
f" Missing standard directories: {', '.join(summary['missing_dirs']) or 'None'}"
|
|
2426
|
+
)
|
|
2427
|
+
print(
|
|
2428
|
+
f" Special directories: {', '.join(summary['special_dirs']) or 'None'}"
|
|
2429
|
+
)
|
|
2430
|
+
print(
|
|
2431
|
+
f" Has Git repository: {'Yes' if summary['has_git_repository'] else 'No'}"
|
|
2432
|
+
)
|
|
2433
|
+
print(
|
|
2434
|
+
f" Has template conflicts: {'Yes' if summary['has_template_conflicts'] else 'No'}"
|
|
2435
|
+
)
|
|
2436
|
+
|
|
2437
|
+
# Get migration plan
|
|
2438
|
+
plan = plan_migration(
|
|
2439
|
+
dir_path, strategy=args.migration_strategy, verbose=args.verbose
|
|
2440
|
+
)
|
|
2441
|
+
|
|
2442
|
+
# Show migration plan
|
|
2443
|
+
print("\nMigration plan:")
|
|
2444
|
+
print(f" Strategy: {plan['strategy']}")
|
|
2445
|
+
print(
|
|
2446
|
+
f" Directories to create: {', '.join(plan['create_dirs']) or 'None'}"
|
|
2447
|
+
)
|
|
2448
|
+
|
|
2449
|
+
for dirname, info in plan["special_dirs"].items():
|
|
2450
|
+
print(f" Special directory '{dirname}': {info['action']}")
|
|
2451
|
+
|
|
2452
|
+
for template_dir, info in plan["template_conflicts"].items():
|
|
2453
|
+
print(
|
|
2454
|
+
f" Template conflicts in '{template_dir}' ({info['conflicts']} files): {info['action']}"
|
|
2455
|
+
)
|
|
2456
|
+
|
|
2457
|
+
# Create backup if requested
|
|
2458
|
+
if hasattr(args, 'backup') and args.backup and not args.dry_run:
|
|
2459
|
+
import shutil
|
|
2460
|
+
import datetime
|
|
2461
|
+
|
|
2462
|
+
# Determine backup location
|
|
2463
|
+
if hasattr(args, 'backup_location') and args.backup_location:
|
|
2464
|
+
backup_path = os.path.abspath(args.backup_location)
|
|
2465
|
+
else:
|
|
2466
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
2467
|
+
backup_path = os.path.abspath(f"{os.path.basename(dir_path)}_backup_{timestamp}")
|
|
2468
|
+
|
|
2469
|
+
try:
|
|
2470
|
+
print(f"\nCreating backup at: {backup_path}")
|
|
2471
|
+
shutil.copytree(dir_path, backup_path, symlinks=True)
|
|
2472
|
+
print(f"✓ Backup created successfully")
|
|
2473
|
+
except Exception as e:
|
|
2474
|
+
logger.error(f"Failed to create backup: {str(e)}")
|
|
2475
|
+
return 1
|
|
2476
|
+
|
|
2477
|
+
# Confirm migration
|
|
2478
|
+
if not args.dry_run and not args.quiet:
|
|
2479
|
+
confirm = input("\nProceed with migration? [y/N]: ")
|
|
2480
|
+
if confirm.lower() != "y":
|
|
2481
|
+
print("Migration cancelled.")
|
|
2482
|
+
return 0
|
|
2483
|
+
|
|
2484
|
+
# Execute migration
|
|
2485
|
+
success = migrate_directory(
|
|
2486
|
+
dir_path,
|
|
2487
|
+
strategy=args.migration_strategy,
|
|
2488
|
+
dry_run=args.dry_run,
|
|
2489
|
+
verbose=args.verbose,
|
|
2490
|
+
)
|
|
2491
|
+
|
|
2492
|
+
if success:
|
|
2493
|
+
msg = (
|
|
2494
|
+
"Migration analysis completed (dry run)"
|
|
2495
|
+
if args.dry_run
|
|
2496
|
+
else "Migration completed successfully"
|
|
2497
|
+
)
|
|
2498
|
+
print(f"\n{msg}")
|
|
2499
|
+
print("\nNext steps:")
|
|
2500
|
+
|
|
2501
|
+
if summary["has_git_repository"]:
|
|
2502
|
+
print(" - Your directory already has a Git repository.")
|
|
2503
|
+
print(" - You may want to update templates and configuration.")
|
|
2504
|
+
else:
|
|
2505
|
+
print(" - Initialize a Git repository (if not already done):")
|
|
2506
|
+
print(" git init")
|
|
2507
|
+
print(" - Set up your initial branches:")
|
|
2508
|
+
print(" git branch dev")
|
|
2509
|
+
print(" git branch staging")
|
|
2510
|
+
print(" git branch test")
|
|
2511
|
+
print(" git branch live")
|
|
2512
|
+
|
|
2513
|
+
if args.publish_to:
|
|
2514
|
+
print(f" - Publish to {args.publish_to}:")
|
|
2515
|
+
print(
|
|
2516
|
+
f" repokit publish {dir_path} --publish-to {args.publish_to}"
|
|
2517
|
+
)
|
|
2518
|
+
|
|
2519
|
+
return 0
|
|
2520
|
+
else:
|
|
2521
|
+
logger.error("Migration failed.")
|
|
2522
|
+
return 1
|
|
2523
|
+
except Exception as e:
|
|
2524
|
+
logger.error(f"Error migrating directory: {str(e)}")
|
|
2525
|
+
if args.verbose >= 2:
|
|
2526
|
+
import traceback
|
|
2527
|
+
|
|
2528
|
+
traceback.print_exc()
|
|
2529
|
+
return 1
|
|
2530
|
+
|
|
2531
|
+
elif args.command == "adopt":
|
|
2532
|
+
# Use current directory if no name provided
|
|
2533
|
+
dir_path = os.path.abspath(args.name) if args.name else os.getcwd()
|
|
2534
|
+
|
|
2535
|
+
if not os.path.exists(dir_path):
|
|
2536
|
+
logger.error(f"Directory not found: {dir_path}")
|
|
2537
|
+
return 1
|
|
2538
|
+
|
|
2539
|
+
try:
|
|
2540
|
+
# Update configuration with adopt-specific options
|
|
2541
|
+
adopt_config = config.copy()
|
|
2542
|
+
|
|
2543
|
+
# Set repo name based on --repo-name or directory name
|
|
2544
|
+
repo_name = getattr(args, 'repo_name', None) or os.path.basename(dir_path)
|
|
2545
|
+
adopt_config["name"] = repo_name
|
|
2546
|
+
|
|
2547
|
+
# Override language if specified
|
|
2548
|
+
if hasattr(args, 'language') and args.language:
|
|
2549
|
+
adopt_config["language"] = args.language
|
|
2550
|
+
if getattr(args, "with_layers", None):
|
|
2551
|
+
from .layers import parse_languages as _parse
|
|
2552
|
+
adopt_config["with"] = _parse(args.with_layers)
|
|
2553
|
+
if getattr(args, "license", None):
|
|
2554
|
+
adopt_config["license"] = args.license
|
|
2555
|
+
|
|
2556
|
+
# Override description if specified
|
|
2557
|
+
if hasattr(args, 'description') and args.description:
|
|
2558
|
+
adopt_config["description"] = args.description
|
|
2559
|
+
|
|
2560
|
+
# Merge adoption-specific sensitive patterns (CLI args + config file)
|
|
2561
|
+
if hasattr(args, 'sensitive_patterns') and args.sensitive_patterns:
|
|
2562
|
+
# Get existing patterns from config file
|
|
2563
|
+
existing_patterns = adopt_config.get("sensitive_patterns", [])
|
|
2564
|
+
# Add CLI patterns
|
|
2565
|
+
cli_patterns = [pattern.strip() for pattern in args.sensitive_patterns.split(",")]
|
|
2566
|
+
# Merge and deduplicate
|
|
2567
|
+
adopt_config["sensitive_patterns"] = list(set(existing_patterns + cli_patterns))
|
|
2568
|
+
|
|
2569
|
+
# Merge adoption-specific private directories (CLI args + config file)
|
|
2570
|
+
if hasattr(args, 'private_dirs') and args.private_dirs:
|
|
2571
|
+
# Get existing private dirs from config file
|
|
2572
|
+
existing_dirs = adopt_config.get("private_dirs", [])
|
|
2573
|
+
# Add CLI dirs
|
|
2574
|
+
cli_dirs = [dir_name.strip() for dir_name in args.private_dirs.split(",")]
|
|
2575
|
+
# Merge and deduplicate
|
|
2576
|
+
adopt_config["private_dirs"] = list(set(existing_dirs + cli_dirs))
|
|
2577
|
+
|
|
2578
|
+
# First analyze the project
|
|
2579
|
+
summary = analyze_project(dir_path, verbose=args.verbose)
|
|
2580
|
+
|
|
2581
|
+
print(f"\n=== ADOPTING PROJECT: {dir_path} ===")
|
|
2582
|
+
print(f" Project type: {summary['project_type']}")
|
|
2583
|
+
print(f" Detected language: {summary['detected_language']}")
|
|
2584
|
+
print(f" Migration complexity: {summary['migration_complexity']}")
|
|
2585
|
+
|
|
2586
|
+
# Check if already RepoKit
|
|
2587
|
+
if summary["project_type"] == "repokit":
|
|
2588
|
+
print(" Project is already using RepoKit structure.")
|
|
2589
|
+
if not args.publish_to:
|
|
2590
|
+
return 0
|
|
2591
|
+
|
|
2592
|
+
# Warn about uncommitted changes
|
|
2593
|
+
git_state = summary.get("git_state", {})
|
|
2594
|
+
if git_state.get("has_uncommitted_changes"):
|
|
2595
|
+
print(" WARNING: Uncommitted changes detected!")
|
|
2596
|
+
if not args.dry_run and not args.quiet:
|
|
2597
|
+
confirm = input(
|
|
2598
|
+
" Continue with adoption? Changes should be committed first. [y/N]: "
|
|
2599
|
+
)
|
|
2600
|
+
if confirm.lower() != "y":
|
|
2601
|
+
print(" Adoption cancelled.")
|
|
2602
|
+
return 0
|
|
2603
|
+
|
|
2604
|
+
# Determine strategies
|
|
2605
|
+
strategy = (
|
|
2606
|
+
args.migration_strategy
|
|
2607
|
+
if hasattr(args, "migration_strategy")
|
|
2608
|
+
else summary["recommended_strategy"]
|
|
2609
|
+
)
|
|
2610
|
+
branch_strategy = getattr(args, "branch_strategy", None)
|
|
2611
|
+
if not branch_strategy:
|
|
2612
|
+
branch_strategy = summary["recommended_branch_strategy"]
|
|
2613
|
+
|
|
2614
|
+
print(f" Using migration strategy: {strategy}")
|
|
2615
|
+
print(f" Using branch strategy: {branch_strategy}")
|
|
2616
|
+
|
|
2617
|
+
if args.dry_run:
|
|
2618
|
+
print(f" DRY RUN - No changes will be made")
|
|
2619
|
+
|
|
2620
|
+
# Create backup if requested
|
|
2621
|
+
if hasattr(args, 'backup') and args.backup and not args.dry_run:
|
|
2622
|
+
import shutil
|
|
2623
|
+
import datetime
|
|
2624
|
+
|
|
2625
|
+
# Determine backup location
|
|
2626
|
+
if hasattr(args, 'backup_location') and args.backup_location:
|
|
2627
|
+
backup_path = os.path.abspath(args.backup_location)
|
|
2628
|
+
else:
|
|
2629
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
2630
|
+
backup_path = os.path.abspath(f"{os.path.basename(dir_path)}_backup_{timestamp}")
|
|
2631
|
+
|
|
2632
|
+
try:
|
|
2633
|
+
print(f" Creating backup at: {backup_path}")
|
|
2634
|
+
shutil.copytree(dir_path, backup_path, symlinks=True)
|
|
2635
|
+
print(f" ✓ Backup created successfully")
|
|
2636
|
+
except Exception as e:
|
|
2637
|
+
logger.error(f"Failed to create backup: {str(e)}")
|
|
2638
|
+
return 1
|
|
2639
|
+
|
|
2640
|
+
# Clean history if requested
|
|
2641
|
+
if hasattr(args, 'clean_history') and args.clean_history and not args.dry_run:
|
|
2642
|
+
try:
|
|
2643
|
+
from .history_cleaner import HistoryCleaner, CleaningConfig, CleaningRecipe, GitFilterRepoNotFound
|
|
2644
|
+
|
|
2645
|
+
print(f" Cleaning repository history using '{args.cleaning_recipe}' recipe...")
|
|
2646
|
+
|
|
2647
|
+
# Initialize cleaner
|
|
2648
|
+
cleaner = HistoryCleaner(repo_path=dir_path, verbose=args.verbose)
|
|
2649
|
+
|
|
2650
|
+
# Build cleaning config using default settings
|
|
2651
|
+
recipe_map = {
|
|
2652
|
+
"pre-open-source": CleaningRecipe.PRE_OPEN_SOURCE,
|
|
2653
|
+
"windows-safe": CleaningRecipe.WINDOWS_SAFE,
|
|
2654
|
+
"remove-secrets": CleaningRecipe.REMOVE_SECRETS
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
# Get default config for the recipe
|
|
2658
|
+
recipe_enum = recipe_map[args.cleaning_recipe]
|
|
2659
|
+
config = cleaner.get_recipe_config(recipe_enum)
|
|
2660
|
+
|
|
2661
|
+
# Override with adopt-specific settings
|
|
2662
|
+
config.backup_location = f"{backup_path}_pre_clean" if 'backup_path' in locals() else None
|
|
2663
|
+
config.dry_run = False
|
|
2664
|
+
config.force = True # Skip confirmation prompts during adopt
|
|
2665
|
+
|
|
2666
|
+
# Execute history cleaning
|
|
2667
|
+
success = cleaner.clean_history(config)
|
|
2668
|
+
if not success:
|
|
2669
|
+
logger.error("History cleaning failed - aborting adoption")
|
|
2670
|
+
return 1
|
|
2671
|
+
|
|
2672
|
+
print(f" ✓ Repository history cleaned successfully")
|
|
2673
|
+
|
|
2674
|
+
except GitFilterRepoNotFound as e:
|
|
2675
|
+
logger.error(str(e))
|
|
2676
|
+
print("\nTo install git filter-repo:")
|
|
2677
|
+
print(" pip install git-filter-repo")
|
|
2678
|
+
print("\nOr see: https://github.com/newren/git-filter-repo")
|
|
2679
|
+
return 1
|
|
2680
|
+
except Exception as e:
|
|
2681
|
+
logger.error(f"Failed to clean history: {str(e)}")
|
|
2682
|
+
return 1
|
|
2683
|
+
|
|
2684
|
+
# Execute adoption using enhanced RepoManager approach
|
|
2685
|
+
# Use RepoManager for non-repokit projects OR when publishing/templates are needed
|
|
2686
|
+
if (summary["project_type"] != "repokit" or args.publish_to):
|
|
2687
|
+
if args.dry_run:
|
|
2688
|
+
print(f"\n=== DRY RUN MODE - No changes will be made ===")
|
|
2689
|
+
print(f"Would execute RepoManager setup_repository() for:")
|
|
2690
|
+
print(f" Project: {repo_name}")
|
|
2691
|
+
print(f" Path: {dir_path}")
|
|
2692
|
+
print(f" Language: {adopt_config.get('language', 'generic')}")
|
|
2693
|
+
print(f" Branch strategy: {adopt_config.get('branch_strategy', 'standard')}")
|
|
2694
|
+
publish_to = getattr(args, 'publish_to', None) or adopt_config.get('publish_to')
|
|
2695
|
+
print(f" Would publish to: {publish_to if publish_to else 'none'}")
|
|
2696
|
+
print(f" Would create backup: {hasattr(args, 'backup') and args.backup}")
|
|
2697
|
+
print(f" Would clean history: {hasattr(args, 'clean_history') and args.clean_history}")
|
|
2698
|
+
|
|
2699
|
+
# Show what sensitive file cleanup would do
|
|
2700
|
+
from .defaults import DEFAULT_SENSITIVE_PATTERNS
|
|
2701
|
+
print(f"\nWould clean files matching these patterns:")
|
|
2702
|
+
for pattern in DEFAULT_SENSITIVE_PATTERNS[:5]:
|
|
2703
|
+
print(f" - {pattern}")
|
|
2704
|
+
if len(DEFAULT_SENSITIVE_PATTERNS) > 5:
|
|
2705
|
+
print(f" ... and {len(DEFAULT_SENSITIVE_PATTERNS) - 5} more patterns")
|
|
2706
|
+
|
|
2707
|
+
print(f"\n=== DRY RUN COMPLETE - No actual changes made ===")
|
|
2708
|
+
return 0
|
|
2709
|
+
# Initialize RepoManager with the merged configuration for in-place adoption
|
|
2710
|
+
adopt_config["name"] = repo_name
|
|
2711
|
+
# Set preserve_existing flag for adoption to avoid overwriting project files
|
|
2712
|
+
adopt_config["preserve_existing"] = True
|
|
2713
|
+
adopt_config["is_adoption"] = True # Flag to indicate this is an adoption
|
|
2714
|
+
|
|
2715
|
+
# Debug: Log final adopt_config
|
|
2716
|
+
logger.debug(f"Final adopt_config before RepoManager: {adopt_config}")
|
|
2717
|
+
logger.debug(f"Sensitive patterns in adopt_config: {adopt_config.get('sensitive_patterns', 'NOT FOUND')}")
|
|
2718
|
+
|
|
2719
|
+
repo_manager = RepoManager(
|
|
2720
|
+
adopt_config, templates_dir=getattr(args, 'templates_dir', None), verbose=args.verbose
|
|
2721
|
+
)
|
|
2722
|
+
|
|
2723
|
+
# Override the project_root and repo_root to adopt in-place
|
|
2724
|
+
repo_manager.project_root = os.path.dirname(dir_path)
|
|
2725
|
+
repo_manager.repo_root = dir_path
|
|
2726
|
+
# Fix github_root path after override to ensure proper template deployment
|
|
2727
|
+
repo_manager.github_root = os.path.join(repo_manager.project_root, "github")
|
|
2728
|
+
|
|
2729
|
+
# Set up repository structure (this will add missing RepoKit structure)
|
|
2730
|
+
success = repo_manager.setup_repository()
|
|
2731
|
+
if not success:
|
|
2732
|
+
logger.error("Failed to set up RepoKit structure")
|
|
2733
|
+
return 1
|
|
2734
|
+
else:
|
|
2735
|
+
# Use the simple adopt_project for dry runs or already RepoKit projects
|
|
2736
|
+
success = adopt_project(
|
|
2737
|
+
directory=dir_path,
|
|
2738
|
+
strategy=strategy,
|
|
2739
|
+
branch_strategy=branch_strategy,
|
|
2740
|
+
publish_to=args.publish_to,
|
|
2741
|
+
dry_run=args.dry_run,
|
|
2742
|
+
verbose=args.verbose,
|
|
2743
|
+
)
|
|
2744
|
+
if not success:
|
|
2745
|
+
logger.error("Project adoption failed.")
|
|
2746
|
+
return 1
|
|
2747
|
+
|
|
2748
|
+
# Handle remote publishing if requested (from CLI args or config file)
|
|
2749
|
+
publish_to = getattr(args, 'publish_to', None) or adopt_config.get('publish_to')
|
|
2750
|
+
if publish_to and not args.dry_run:
|
|
2751
|
+
from .remote_integration import RemoteIntegration
|
|
2752
|
+
|
|
2753
|
+
# Update configuration for remote publishing
|
|
2754
|
+
organization = getattr(args, 'organization', None) or adopt_config.get('organization')
|
|
2755
|
+
if organization:
|
|
2756
|
+
if publish_to == "github":
|
|
2757
|
+
adopt_config.setdefault("github", {})["organization"] = organization
|
|
2758
|
+
else:
|
|
2759
|
+
adopt_config.setdefault("gitlab", {})["group"] = organization
|
|
2760
|
+
|
|
2761
|
+
# Initialize RepoManager for publishing (if not already done)
|
|
2762
|
+
if summary["project_type"] == "repokit":
|
|
2763
|
+
repo_manager = RepoManager(
|
|
2764
|
+
adopt_config, templates_dir=getattr(args, 'templates_dir', None), verbose=args.verbose
|
|
2765
|
+
)
|
|
2766
|
+
repo_manager.project_root = os.path.dirname(dir_path)
|
|
2767
|
+
repo_manager.repo_root = dir_path
|
|
2768
|
+
|
|
2769
|
+
remote_integration = RemoteIntegration(
|
|
2770
|
+
repo_manager,
|
|
2771
|
+
credentials_file=getattr(args, 'credentials_file', None),
|
|
2772
|
+
verbose=args.verbose,
|
|
2773
|
+
)
|
|
2774
|
+
|
|
2775
|
+
# Handle token - if direct token provided, create a command that echoes it
|
|
2776
|
+
token_cmd = getattr(args, 'token_command', None)
|
|
2777
|
+
if hasattr(args, 'token') and args.token and not token_cmd:
|
|
2778
|
+
token_cmd = f"echo {args.token}"
|
|
2779
|
+
|
|
2780
|
+
publish_success = remote_integration.setup_remote_repository(
|
|
2781
|
+
service=publish_to,
|
|
2782
|
+
remote_name=getattr(args, 'remote_name', None) or adopt_config.get('remote_name', 'origin'),
|
|
2783
|
+
private=getattr(args, 'private_repo', None) or adopt_config.get('private_repo', False),
|
|
2784
|
+
push_branches=not getattr(args, 'no_push', False),
|
|
2785
|
+
organization=organization,
|
|
2786
|
+
token_command=token_cmd,
|
|
2787
|
+
)
|
|
2788
|
+
|
|
2789
|
+
if not publish_success:
|
|
2790
|
+
logger.error(f"Failed to publish repository to {publish_to}")
|
|
2791
|
+
return 1
|
|
2792
|
+
|
|
2793
|
+
logger.info(f"Successfully published repository to {publish_to}")
|
|
2794
|
+
|
|
2795
|
+
if args.dry_run:
|
|
2796
|
+
print(f"\nAdoption analysis completed (dry run)")
|
|
2797
|
+
else:
|
|
2798
|
+
print(f"\nProject adopted successfully!")
|
|
2799
|
+
|
|
2800
|
+
print("\nNext steps:")
|
|
2801
|
+
print(f" - Your project now uses RepoKit structure")
|
|
2802
|
+
print(f" - Check the generated templates and configuration")
|
|
2803
|
+
print(f" - Commit any new files to your repository")
|
|
2804
|
+
|
|
2805
|
+
if publish_to:
|
|
2806
|
+
print(f" - Repository has been published to {publish_to}")
|
|
2807
|
+
elif not git_state.get("remotes"):
|
|
2808
|
+
print(f" - Consider publishing to a remote service:")
|
|
2809
|
+
print(f" repokit publish {dir_path} --publish-to github")
|
|
2810
|
+
|
|
2811
|
+
return 0
|
|
2812
|
+
|
|
2813
|
+
except Exception as e:
|
|
2814
|
+
logger.error(f"Error adopting project: {str(e)}")
|
|
2815
|
+
if args.verbose >= 2:
|
|
2816
|
+
import traceback
|
|
2817
|
+
|
|
2818
|
+
traceback.print_exc()
|
|
2819
|
+
return 1
|
|
2820
|
+
|
|
2821
|
+
elif args.command == "create":
|
|
2822
|
+
if not args.name:
|
|
2823
|
+
logger.error("Repository name is required for 'create' command")
|
|
2824
|
+
return 1
|
|
2825
|
+
|
|
2826
|
+
# The code-type names are checked here, before a folder or a repository
|
|
2827
|
+
# exists: an unknown language or archetype, or a stub that is not built
|
|
2828
|
+
# yet, stops with the known names.
|
|
2829
|
+
from . import layers as _layers
|
|
2830
|
+
from .template_engine import TemplateEngine as _TemplateEngine
|
|
2831
|
+
try:
|
|
2832
|
+
_root = _layers.layers_root(_TemplateEngine(templates_dir=args.templates_dir))
|
|
2833
|
+
if _root is not None:
|
|
2834
|
+
_layers.resolve(_layers.load_manifest(_root),
|
|
2835
|
+
config.get("languages") or [config.get("language") or _layers.GENERIC],
|
|
2836
|
+
config.get("with"))
|
|
2837
|
+
elif config.get("with"):
|
|
2838
|
+
raise _layers.LayerError("no code-type layers found with the templates, so --with cannot be used")
|
|
2839
|
+
except _layers.LayerError as e:
|
|
2840
|
+
logger.error(str(e))
|
|
2841
|
+
return 1
|
|
2842
|
+
|
|
2843
|
+
# The GitHub login, before any file is written: the files' URLs,
|
|
2844
|
+
# CODEOWNERS and FUNDING name it (0.9.6; before, they carried the
|
|
2845
|
+
# YOUR_GITHUB_USERNAME placeholder even with --github)
|
|
2846
|
+
if getattr(args, "github", False) and not args.no_push:
|
|
2847
|
+
from .github_create import gh_login
|
|
2848
|
+
config["github_create"] = True
|
|
2849
|
+
login = gh_login(os.getcwd())
|
|
2850
|
+
if login:
|
|
2851
|
+
config["github_login"] = login
|
|
2852
|
+
|
|
2853
|
+
try:
|
|
2854
|
+
# Initialize RepoManager with the merged configuration
|
|
2855
|
+
repo_manager = RepoManager(
|
|
2856
|
+
config, templates_dir=args.templates_dir, verbose=args.verbose
|
|
2857
|
+
)
|
|
2858
|
+
|
|
2859
|
+
# Set up repository
|
|
2860
|
+
success = repo_manager.setup_repository()
|
|
2861
|
+
if not success:
|
|
2862
|
+
return 1
|
|
2863
|
+
|
|
2864
|
+
# GitHub through gh (its login, no token here)
|
|
2865
|
+
if getattr(args, "github", False):
|
|
2866
|
+
if args.no_push:
|
|
2867
|
+
logger.info("--github with --no-push: nothing created on GitHub")
|
|
2868
|
+
else:
|
|
2869
|
+
from .github_create import create_on_github
|
|
2870
|
+
setup_report: list = []
|
|
2871
|
+
# repo_manager.config carries what the create settled
|
|
2872
|
+
# (the archetype, the extras) beside the person's settings
|
|
2873
|
+
problem = create_on_github(
|
|
2874
|
+
repo_manager.repo_root, repo_manager.project_name, repo_manager.config,
|
|
2875
|
+
repo_manager.private_branch, organization=args.organization,
|
|
2876
|
+
public=getattr(args, "public", False), logger=logger,
|
|
2877
|
+
report=setup_report,
|
|
2878
|
+
)
|
|
2879
|
+
for line in setup_report:
|
|
2880
|
+
print(f"GitHub: {line}")
|
|
2881
|
+
if problem:
|
|
2882
|
+
logger.error(f"GitHub: {problem}")
|
|
2883
|
+
logger.error("The local repository is complete; retry with gh once fixed.")
|
|
2884
|
+
return 1
|
|
2885
|
+
|
|
2886
|
+
# Publish to remote if requested
|
|
2887
|
+
if args.publish_to:
|
|
2888
|
+
# --token reaches the token lookup through its environment variable
|
|
2889
|
+
if getattr(args, "token", None):
|
|
2890
|
+
os.environ[f"{args.publish_to.upper()}_TOKEN"] = args.token
|
|
2891
|
+
from .remote_integration import RemoteIntegration
|
|
2892
|
+
|
|
2893
|
+
remote_integration = RemoteIntegration(
|
|
2894
|
+
repo_manager,
|
|
2895
|
+
credentials_file=args.credentials_file,
|
|
2896
|
+
verbose=args.verbose,
|
|
2897
|
+
)
|
|
2898
|
+
|
|
2899
|
+
publish_success = remote_integration.setup_remote_repository(
|
|
2900
|
+
service=args.publish_to,
|
|
2901
|
+
remote_name=args.remote_name,
|
|
2902
|
+
private=args.private_repo,
|
|
2903
|
+
push_branches=not args.no_push,
|
|
2904
|
+
organization=args.organization,
|
|
2905
|
+
token_command=args.token_command,
|
|
2906
|
+
)
|
|
2907
|
+
|
|
2908
|
+
if not publish_success:
|
|
2909
|
+
logger.error(f"Failed to publish repository to {args.publish_to}")
|
|
2910
|
+
return 1
|
|
2911
|
+
|
|
2912
|
+
logger.info(f"Successfully published repository to {args.publish_to}")
|
|
2913
|
+
|
|
2914
|
+
return 0
|
|
2915
|
+
except Exception as e:
|
|
2916
|
+
logger.error(f"Error: {str(e)}")
|
|
2917
|
+
if args.verbose >= 2:
|
|
2918
|
+
import traceback
|
|
2919
|
+
|
|
2920
|
+
traceback.print_exc()
|
|
2921
|
+
return 1
|
|
2922
|
+
|
|
2923
|
+
elif args.command == "publish":
|
|
2924
|
+
if not args.name:
|
|
2925
|
+
logger.error("Repository path is required for 'publish' command")
|
|
2926
|
+
return 1
|
|
2927
|
+
|
|
2928
|
+
if not args.publish_to:
|
|
2929
|
+
logger.error("--publish-to is required for 'publish' command")
|
|
2930
|
+
return 1
|
|
2931
|
+
|
|
2932
|
+
try:
|
|
2933
|
+
# Use the provided path as the repository root
|
|
2934
|
+
repo_path = os.path.abspath(args.name)
|
|
2935
|
+
if not os.path.exists(repo_path):
|
|
2936
|
+
logger.error(f"Repository not found at {repo_path}")
|
|
2937
|
+
return 1
|
|
2938
|
+
|
|
2939
|
+
# Update configuration with the correct path and name
|
|
2940
|
+
repo_name = args.repo_name if args.repo_name else os.path.basename(repo_path)
|
|
2941
|
+
config["name"] = repo_name
|
|
2942
|
+
|
|
2943
|
+
# Initialize RepoManager with the repository path
|
|
2944
|
+
repo_manager = RepoManager(
|
|
2945
|
+
config, templates_dir=args.templates_dir, verbose=args.verbose
|
|
2946
|
+
)
|
|
2947
|
+
|
|
2948
|
+
# Override the project_root and repo_root
|
|
2949
|
+
repo_manager.project_root = os.path.dirname(repo_path)
|
|
2950
|
+
repo_manager.repo_root = repo_path
|
|
2951
|
+
|
|
2952
|
+
# Publish to remote
|
|
2953
|
+
from .remote_integration import RemoteIntegration
|
|
2954
|
+
|
|
2955
|
+
remote_integration = RemoteIntegration(
|
|
2956
|
+
repo_manager,
|
|
2957
|
+
credentials_file=args.credentials_file,
|
|
2958
|
+
verbose=args.verbose,
|
|
2959
|
+
)
|
|
2960
|
+
|
|
2961
|
+
# Handle token - if direct token provided, create a command that echoes it
|
|
2962
|
+
token_cmd = args.token_command
|
|
2963
|
+
if args.token and not token_cmd:
|
|
2964
|
+
token_cmd = f"echo {args.token}"
|
|
2965
|
+
|
|
2966
|
+
publish_success = remote_integration.setup_remote_repository(
|
|
2967
|
+
service=args.publish_to,
|
|
2968
|
+
remote_name=args.remote_name,
|
|
2969
|
+
private=args.private_repo,
|
|
2970
|
+
push_branches=not args.no_push,
|
|
2971
|
+
organization=args.organization,
|
|
2972
|
+
token_command=token_cmd,
|
|
2973
|
+
include_branches=getattr(args, 'include_branches', None),
|
|
2974
|
+
exclude_branches=getattr(args, 'exclude_branches', None),
|
|
2975
|
+
)
|
|
2976
|
+
|
|
2977
|
+
if not publish_success:
|
|
2978
|
+
logger.error(f"Failed to publish repository to {args.publish_to}")
|
|
2979
|
+
return 1
|
|
2980
|
+
|
|
2981
|
+
logger.info(f"Successfully published repository to {args.publish_to}")
|
|
2982
|
+
return 0
|
|
2983
|
+
except Exception as e:
|
|
2984
|
+
logger.error(f"Error: {str(e)}")
|
|
2985
|
+
if args.verbose >= 2:
|
|
2986
|
+
import traceback
|
|
2987
|
+
|
|
2988
|
+
traceback.print_exc()
|
|
2989
|
+
return 1
|
|
2990
|
+
|
|
2991
|
+
# Unknown command (should not happen due to choices in argparse)
|
|
2992
|
+
logger.error(f"Unknown command: {args.command}")
|
|
2993
|
+
return 1
|
|
2994
|
+
|
|
2995
|
+
|
|
2996
|
+
if __name__ == "__main__":
|
|
2997
|
+
sys.exit(main())
|