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/config.py
ADDED
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Configuration management for RepoKit.
|
|
4
|
+
|
|
5
|
+
Handles loading, validating, and merging configurations from
|
|
6
|
+
multiple sources (defaults, config files, environment, CLI).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import copy
|
|
10
|
+
import os
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Dict, Any, Optional, List, Union
|
|
15
|
+
|
|
16
|
+
from .directory_profiles import DirectoryProfileManager
|
|
17
|
+
from .defaults import (
|
|
18
|
+
DEFAULT_PRIVATE_DIRS,
|
|
19
|
+
DEFAULT_SENSITIVE_FILES,
|
|
20
|
+
DEFAULT_SENSITIVE_PATTERNS,
|
|
21
|
+
BRANCH_SPECIFIC_EXCLUDES,
|
|
22
|
+
BRANCH_STRATEGIES,
|
|
23
|
+
DEFAULT_BRANCH_STRATEGY,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ConfigManager:
|
|
28
|
+
"""
|
|
29
|
+
Manages configuration loading and validation for RepoKit.
|
|
30
|
+
|
|
31
|
+
The configuration hierarchy (from lowest to highest precedence):
|
|
32
|
+
1. Default values
|
|
33
|
+
2. Global config file (~/.repokit/config.json)
|
|
34
|
+
3. Project config file (./.repokit.json)
|
|
35
|
+
4. Environment variables (REPOKIT_*)
|
|
36
|
+
5. Command line arguments
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, verbose: int = 0):
|
|
40
|
+
"""
|
|
41
|
+
Initialize the configuration manager.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
verbose: Verbosity level (0=normal, 1=info, 2=debug)
|
|
45
|
+
"""
|
|
46
|
+
self.verbose = verbose
|
|
47
|
+
self.logger = logging.getLogger("repokit.config")
|
|
48
|
+
|
|
49
|
+
# Initialize directory profile manager
|
|
50
|
+
self.directory_manager = DirectoryProfileManager(verbose=verbose)
|
|
51
|
+
|
|
52
|
+
# Default configuration
|
|
53
|
+
self.default_config = {
|
|
54
|
+
"name": "my-project",
|
|
55
|
+
"description": "A new project repository",
|
|
56
|
+
"language": "generic",
|
|
57
|
+
"default_branch": "main",
|
|
58
|
+
# Base defaults match the default strategy; apply_branch_strategy
|
|
59
|
+
# replaces them with the chosen strategy's values
|
|
60
|
+
"branches": list(BRANCH_STRATEGIES[DEFAULT_BRANCH_STRATEGY]["branches"]),
|
|
61
|
+
"worktrees": list(BRANCH_STRATEGIES[DEFAULT_BRANCH_STRATEGY]["worktrees"]),
|
|
62
|
+
"directories": [
|
|
63
|
+
"convos",
|
|
64
|
+
"docs",
|
|
65
|
+
"logs",
|
|
66
|
+
"private",
|
|
67
|
+
"revisions",
|
|
68
|
+
"scripts",
|
|
69
|
+
"tests",
|
|
70
|
+
],
|
|
71
|
+
"private_dirs": DEFAULT_PRIVATE_DIRS.copy(),
|
|
72
|
+
"sensitive_files": DEFAULT_SENSITIVE_FILES.copy(),
|
|
73
|
+
"sensitive_patterns": DEFAULT_SENSITIVE_PATTERNS.copy(),
|
|
74
|
+
"private_branch": BRANCH_STRATEGIES[DEFAULT_BRANCH_STRATEGY]["private_branch"],
|
|
75
|
+
"github": True,
|
|
76
|
+
# The licence is REQUIRED: --license, else `license` here (or in the
|
|
77
|
+
# global file), else `license_private` for a private repository;
|
|
78
|
+
# with none, create refuses and names the catalogue (0.9.0).
|
|
79
|
+
"license": None,
|
|
80
|
+
"license_private": None,
|
|
81
|
+
# The structure's optional files, by name (0.9.0): funding,
|
|
82
|
+
# dependabot, stale, pr-template, release-template, roadmap,
|
|
83
|
+
# platform-support, vscode-settings, placeholders. Off unless
|
|
84
|
+
# the global config or --extras names them.
|
|
85
|
+
"extras": [],
|
|
86
|
+
# What --github sets up on the new repository (0.9.3): labels,
|
|
87
|
+
# issues, pin, discussions, topics, pypi-environment, traffic, all.
|
|
88
|
+
# Off unless the global config or --github-setup names them.
|
|
89
|
+
"github_setup": [],
|
|
90
|
+
"topics": [],
|
|
91
|
+
# README badges (0.9.7): a template name or a list; None picks the
|
|
92
|
+
# preset's (pypi for python/pypi, else github). badge_templates
|
|
93
|
+
# and badge_types hold a person's own; badges_ci is auto, on, off.
|
|
94
|
+
"badges": None,
|
|
95
|
+
"badge_templates": {},
|
|
96
|
+
"badge_types": {},
|
|
97
|
+
"badges_ci": "auto",
|
|
98
|
+
# The vault's AI door (0.9.4): "prompt-file" writes the oracle's
|
|
99
|
+
# first prompt to private/claude/_oracle/; "none" writes nothing.
|
|
100
|
+
"vault_init": "prompt-file",
|
|
101
|
+
"use_github_noreply": True, # Add GitHub no-reply setting
|
|
102
|
+
"user": {"name": "", "email": ""},
|
|
103
|
+
# Branch configuration enhancements
|
|
104
|
+
"branch_config": {
|
|
105
|
+
# Default mapping between branch names and directory names
|
|
106
|
+
"branch_directories": {
|
|
107
|
+
"main": "github", # main branch maps to github directory by default
|
|
108
|
+
"dev": "dev", # dev branch maps to dev directory
|
|
109
|
+
# Other branches use the branch name as directory name by default
|
|
110
|
+
},
|
|
111
|
+
# Define branch purposes and relationships
|
|
112
|
+
"branch_roles": {
|
|
113
|
+
"main": "production", # Main production branch
|
|
114
|
+
"dev": "development", # Development branch
|
|
115
|
+
"staging": "pre-release", # Pre-production testing
|
|
116
|
+
"test": "testing", # Testing branch
|
|
117
|
+
"live": "live", # Live deployment branch
|
|
118
|
+
"private": "personal", # Personal/local development
|
|
119
|
+
},
|
|
120
|
+
# Define branch flow (which branches can be merged to which)
|
|
121
|
+
"branch_flow": {
|
|
122
|
+
"private": ["dev"], # Private can merge to dev
|
|
123
|
+
"dev": ["staging", "test"], # Dev can merge to staging or test
|
|
124
|
+
"test": ["staging"], # Test can merge to staging
|
|
125
|
+
"staging": ["main"], # Staging can merge to main
|
|
126
|
+
"main": ["live"], # Main can merge to live
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
# Branch strategy templates
|
|
130
|
+
"branch_strategy": DEFAULT_BRANCH_STRATEGY,
|
|
131
|
+
# Defined once in defaults.BRANCH_STRATEGIES; copied so a config
|
|
132
|
+
# file can extend or override strategies without touching it.
|
|
133
|
+
"branch_strategies": copy.deepcopy(BRANCH_STRATEGIES),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Load configurations
|
|
137
|
+
self.global_config = self._load_global_config()
|
|
138
|
+
self.project_config = self._load_project_config()
|
|
139
|
+
self.env_config = self._load_env_config()
|
|
140
|
+
self.cli_config = {} # Will be set later
|
|
141
|
+
|
|
142
|
+
# The merged, final configuration
|
|
143
|
+
self.config = self._merge_configs()
|
|
144
|
+
|
|
145
|
+
def apply_branch_strategy(self) -> None:
|
|
146
|
+
"""
|
|
147
|
+
Apply the selected branch strategy to the configuration.
|
|
148
|
+
|
|
149
|
+
This updates branches, worktrees, and other settings based on the
|
|
150
|
+
selected branch strategy.
|
|
151
|
+
"""
|
|
152
|
+
# Get the selected strategy
|
|
153
|
+
strategy_name = self.config.get("branch_strategy", "standard")
|
|
154
|
+
strategies = self.config.get("branch_strategies", {})
|
|
155
|
+
|
|
156
|
+
if strategy_name in strategies:
|
|
157
|
+
strategy = strategies[strategy_name]
|
|
158
|
+
|
|
159
|
+
# Only update if not explicitly set in config
|
|
160
|
+
if "branches" not in self.cli_config:
|
|
161
|
+
self.config["branches"] = strategy.get("branches", ["main", "dev"])
|
|
162
|
+
|
|
163
|
+
if "worktrees" not in self.cli_config:
|
|
164
|
+
self.config["worktrees"] = strategy.get("worktrees", ["main"])
|
|
165
|
+
|
|
166
|
+
# A strategy's private branch wins unless --private-branch was
|
|
167
|
+
# given; None means the strategy has no private branch at all.
|
|
168
|
+
if "private_branch" not in self.cli_config:
|
|
169
|
+
self.config["private_branch"] = strategy.get(
|
|
170
|
+
"private_branch", "private"
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Ensure default_branch aligns
|
|
174
|
+
if "default_branch" not in self.cli_config:
|
|
175
|
+
self.config["default_branch"] = self.config.get(
|
|
176
|
+
"default_branch", "main"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if self.verbose >= 1:
|
|
180
|
+
self.logger.info(f"Applied branch strategy '{strategy_name}'")
|
|
181
|
+
|
|
182
|
+
def get_branch_directory(self, branch_name: str) -> str:
|
|
183
|
+
"""
|
|
184
|
+
Get the directory name for a branch.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
branch_name: Name of the branch
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Directory name for the branch
|
|
191
|
+
"""
|
|
192
|
+
# Check branch_directories mapping first
|
|
193
|
+
branch_dirs = self.config.get("branch_config", {}).get("branch_directories", {})
|
|
194
|
+
|
|
195
|
+
if branch_name in branch_dirs:
|
|
196
|
+
return branch_dirs[branch_name]
|
|
197
|
+
|
|
198
|
+
# Default: use branch name as directory name
|
|
199
|
+
return branch_name
|
|
200
|
+
|
|
201
|
+
def load_branch_config_file(self, path: str) -> bool:
|
|
202
|
+
"""
|
|
203
|
+
Load branch configuration from a file.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
path: Path to branch configuration file
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
True if loaded successfully, False otherwise
|
|
210
|
+
"""
|
|
211
|
+
try:
|
|
212
|
+
with open(path, "r") as f:
|
|
213
|
+
branch_config = json.load(f)
|
|
214
|
+
|
|
215
|
+
# Update branch configuration
|
|
216
|
+
if "branch_config" in branch_config:
|
|
217
|
+
self.project_config["branch_config"] = branch_config["branch_config"]
|
|
218
|
+
|
|
219
|
+
if "branch_strategy" in branch_config:
|
|
220
|
+
self.project_config["branch_strategy"] = branch_config[
|
|
221
|
+
"branch_strategy"
|
|
222
|
+
]
|
|
223
|
+
|
|
224
|
+
if "branch_strategies" in branch_config:
|
|
225
|
+
self.project_config["branch_strategies"] = branch_config[
|
|
226
|
+
"branch_strategies"
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
# Remerge configs
|
|
230
|
+
self.config = self._merge_configs()
|
|
231
|
+
|
|
232
|
+
# Apply branch strategy
|
|
233
|
+
self.apply_branch_strategy()
|
|
234
|
+
|
|
235
|
+
if self.verbose >= 1:
|
|
236
|
+
self.logger.info(f"Loaded branch configuration from {path}")
|
|
237
|
+
|
|
238
|
+
return True
|
|
239
|
+
except Exception as e:
|
|
240
|
+
self.logger.error(
|
|
241
|
+
f"Failed to load branch configuration from {path}: {str(e)}"
|
|
242
|
+
)
|
|
243
|
+
return False
|
|
244
|
+
|
|
245
|
+
def _load_global_config(self) -> Dict[str, Any]:
|
|
246
|
+
"""
|
|
247
|
+
Load global configuration from ~/.repokit/config.json.
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Global configuration or empty dict if not found
|
|
251
|
+
"""
|
|
252
|
+
home_dir = os.path.expanduser("~")
|
|
253
|
+
global_config_path = os.path.join(home_dir, ".repokit", "config.json")
|
|
254
|
+
|
|
255
|
+
if os.path.exists(global_config_path):
|
|
256
|
+
try:
|
|
257
|
+
with open(global_config_path, "r") as f:
|
|
258
|
+
config = json.load(f)
|
|
259
|
+
if self.verbose >= 1:
|
|
260
|
+
self.logger.info(f"Loaded global config from {global_config_path}")
|
|
261
|
+
return config
|
|
262
|
+
except Exception as e:
|
|
263
|
+
self.logger.warning(f"Error loading global config: {str(e)}")
|
|
264
|
+
|
|
265
|
+
return {}
|
|
266
|
+
|
|
267
|
+
def _load_project_config(self) -> Dict[str, Any]:
|
|
268
|
+
"""
|
|
269
|
+
Load project-specific configuration from ./.repokit.json.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Project configuration or empty dict if not found
|
|
273
|
+
"""
|
|
274
|
+
project_config_path = os.path.join(os.getcwd(), ".repokit.json")
|
|
275
|
+
|
|
276
|
+
if os.path.exists(project_config_path):
|
|
277
|
+
try:
|
|
278
|
+
with open(project_config_path, "r") as f:
|
|
279
|
+
config = json.load(f)
|
|
280
|
+
if self.verbose >= 1:
|
|
281
|
+
self.logger.info(
|
|
282
|
+
f"Loaded project config from {project_config_path}"
|
|
283
|
+
)
|
|
284
|
+
return config
|
|
285
|
+
except Exception as e:
|
|
286
|
+
self.logger.warning(f"Error loading project config: {str(e)}")
|
|
287
|
+
|
|
288
|
+
return {}
|
|
289
|
+
|
|
290
|
+
def _load_env_config(self) -> Dict[str, Any]:
|
|
291
|
+
"""
|
|
292
|
+
Load configuration from environment variables (REPOKIT_*).
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
Environment configuration
|
|
296
|
+
"""
|
|
297
|
+
config = {}
|
|
298
|
+
|
|
299
|
+
# Look for environment variables with REPOKIT_ prefix
|
|
300
|
+
for key, value in os.environ.items():
|
|
301
|
+
if key.startswith("REPOKIT_"):
|
|
302
|
+
# Convert REPOKIT_SNAKE_CASE to camelCase or underscore
|
|
303
|
+
config_key = key[8:].lower() # Remove REPOKIT_ and lowercase
|
|
304
|
+
|
|
305
|
+
# Handle nested keys (e.g., REPOKIT_USER_NAME -> user.name)
|
|
306
|
+
parts = config_key.split("_")
|
|
307
|
+
current = config
|
|
308
|
+
|
|
309
|
+
for i, part in enumerate(parts):
|
|
310
|
+
if i == len(parts) - 1: # Last part
|
|
311
|
+
current[part] = value
|
|
312
|
+
else:
|
|
313
|
+
if part not in current:
|
|
314
|
+
current[part] = {}
|
|
315
|
+
current = current[part]
|
|
316
|
+
|
|
317
|
+
# Special handling for list-type values
|
|
318
|
+
for key in ["branches", "worktrees", "directories", "private_dirs"]:
|
|
319
|
+
env_key = f"REPOKIT_{key.upper()}"
|
|
320
|
+
if env_key in os.environ:
|
|
321
|
+
config[key] = os.environ[env_key].split(",")
|
|
322
|
+
|
|
323
|
+
if config and self.verbose >= 1:
|
|
324
|
+
self.logger.info(f"Loaded configuration from environment variables")
|
|
325
|
+
|
|
326
|
+
return config
|
|
327
|
+
|
|
328
|
+
def set_cli_config(self, cli_config: Dict[str, Any]) -> None:
|
|
329
|
+
"""
|
|
330
|
+
Set the command-line configuration.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
cli_config: Configuration from command-line arguments
|
|
334
|
+
"""
|
|
335
|
+
self.cli_config = cli_config
|
|
336
|
+
|
|
337
|
+
# Remerge configs with the new CLI config
|
|
338
|
+
self.config = self._merge_configs()
|
|
339
|
+
|
|
340
|
+
# Apply branch strategy
|
|
341
|
+
self.apply_branch_strategy()
|
|
342
|
+
|
|
343
|
+
# Resolve directory profiles
|
|
344
|
+
self.resolve_directory_profiles()
|
|
345
|
+
|
|
346
|
+
def _merge_configs(self) -> Dict[str, Any]:
|
|
347
|
+
"""
|
|
348
|
+
Merge configurations from all sources.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
Merged configuration
|
|
352
|
+
"""
|
|
353
|
+
# Start with default config
|
|
354
|
+
config = self.default_config.copy()
|
|
355
|
+
|
|
356
|
+
# Recursively update with each config source in order of precedence
|
|
357
|
+
self._recursive_update(config, self.global_config)
|
|
358
|
+
self._recursive_update(config, self.project_config)
|
|
359
|
+
self._recursive_update(config, self.env_config)
|
|
360
|
+
self._recursive_update(config, self.cli_config)
|
|
361
|
+
|
|
362
|
+
return config
|
|
363
|
+
|
|
364
|
+
def _recursive_update(self, target: Dict[str, Any], source: Dict[str, Any]) -> None:
|
|
365
|
+
"""
|
|
366
|
+
Recursively update a target dictionary with values from a source dictionary.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
target: Target dictionary to update
|
|
370
|
+
source: Source dictionary with new values
|
|
371
|
+
"""
|
|
372
|
+
for key, value in source.items():
|
|
373
|
+
if (
|
|
374
|
+
key in target
|
|
375
|
+
and isinstance(target[key], dict)
|
|
376
|
+
and isinstance(value, dict)
|
|
377
|
+
):
|
|
378
|
+
# Recursively update nested dictionaries
|
|
379
|
+
self._recursive_update(target[key], value)
|
|
380
|
+
elif value is not None: # Only update if value is not None
|
|
381
|
+
target[key] = value
|
|
382
|
+
|
|
383
|
+
def get_config(self) -> Dict[str, Any]:
|
|
384
|
+
"""
|
|
385
|
+
Get the merged configuration.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
Merged configuration
|
|
389
|
+
"""
|
|
390
|
+
return self.config
|
|
391
|
+
|
|
392
|
+
def save_config(self, path: str) -> bool:
|
|
393
|
+
"""
|
|
394
|
+
Save the current configuration to a file.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
path: Path to save the configuration
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
True if successful, False otherwise
|
|
401
|
+
"""
|
|
402
|
+
try:
|
|
403
|
+
# Create directory if it doesn't exist
|
|
404
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
405
|
+
|
|
406
|
+
with open(path, "w") as f:
|
|
407
|
+
json.dump(self.config, f, indent=4)
|
|
408
|
+
|
|
409
|
+
if self.verbose >= 1:
|
|
410
|
+
self.logger.info(f"Saved configuration to {path}")
|
|
411
|
+
|
|
412
|
+
return True
|
|
413
|
+
except Exception as e:
|
|
414
|
+
self.logger.error(f"Failed to save configuration to {path}: {str(e)}")
|
|
415
|
+
return False
|
|
416
|
+
|
|
417
|
+
def load_config_file(self, path: str) -> bool:
|
|
418
|
+
"""
|
|
419
|
+
Load configuration from a file.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
path: Path to the configuration file
|
|
423
|
+
|
|
424
|
+
Returns:
|
|
425
|
+
True if successful, False otherwise
|
|
426
|
+
"""
|
|
427
|
+
try:
|
|
428
|
+
with open(path, "r") as f:
|
|
429
|
+
config = json.load(f)
|
|
430
|
+
|
|
431
|
+
# Update project_config with the loaded config
|
|
432
|
+
self.project_config = config
|
|
433
|
+
|
|
434
|
+
# Remerge configs with the new project config
|
|
435
|
+
self.config = self._merge_configs()
|
|
436
|
+
|
|
437
|
+
if self.verbose >= 1:
|
|
438
|
+
self.logger.info(f"Loaded configuration from {path}")
|
|
439
|
+
|
|
440
|
+
return True
|
|
441
|
+
except Exception as e:
|
|
442
|
+
self.logger.error(f"Failed to load configuration from {path}: {str(e)}")
|
|
443
|
+
return False
|
|
444
|
+
|
|
445
|
+
def validate_config(self) -> List[str]:
|
|
446
|
+
"""
|
|
447
|
+
Validate the configuration.
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
List of validation errors (empty if valid)
|
|
451
|
+
"""
|
|
452
|
+
errors = []
|
|
453
|
+
|
|
454
|
+
# Basic validation
|
|
455
|
+
if not self.config.get("name"):
|
|
456
|
+
errors.append("Project name is required")
|
|
457
|
+
|
|
458
|
+
# More validation rules can be added here
|
|
459
|
+
|
|
460
|
+
return errors
|
|
461
|
+
|
|
462
|
+
def create_bootstrap_config(
|
|
463
|
+
self,
|
|
464
|
+
name: str,
|
|
465
|
+
description: str = None,
|
|
466
|
+
language: str = "python",
|
|
467
|
+
user_info: Dict[str, str] = None,
|
|
468
|
+
) -> Dict[str, Any]:
|
|
469
|
+
"""
|
|
470
|
+
Create a configuration specifically for bootstrapping a project.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
name: Project name
|
|
474
|
+
description: Project description
|
|
475
|
+
language: Programming language
|
|
476
|
+
user_info: Dictionary with user name and email
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
Bootstrap configuration dictionary
|
|
480
|
+
"""
|
|
481
|
+
# Start with default config
|
|
482
|
+
config = self.default_config.copy()
|
|
483
|
+
|
|
484
|
+
# Update with provided values
|
|
485
|
+
config["name"] = name
|
|
486
|
+
|
|
487
|
+
if description:
|
|
488
|
+
config["description"] = description
|
|
489
|
+
|
|
490
|
+
if language:
|
|
491
|
+
config["language"] = language
|
|
492
|
+
|
|
493
|
+
# Handle user information
|
|
494
|
+
if user_info and isinstance(user_info, dict):
|
|
495
|
+
if "name" in user_info or "email" in user_info:
|
|
496
|
+
config["user"] = user_info.copy()
|
|
497
|
+
|
|
498
|
+
return config
|
|
499
|
+
|
|
500
|
+
def create_config_from_environment(self) -> Dict[str, Any]:
|
|
501
|
+
"""
|
|
502
|
+
Create a configuration from environment variables.
|
|
503
|
+
|
|
504
|
+
Environment variables:
|
|
505
|
+
REPOKIT_NAME: Project name
|
|
506
|
+
REPOKIT_DESCRIPTION: Project description
|
|
507
|
+
REPOKIT_LANGUAGE: Programming language
|
|
508
|
+
GIT_USER_NAME: Git user name
|
|
509
|
+
GIT_USER_EMAIL: Git user email
|
|
510
|
+
|
|
511
|
+
Returns:
|
|
512
|
+
Configuration dictionary from environment
|
|
513
|
+
"""
|
|
514
|
+
config = {}
|
|
515
|
+
|
|
516
|
+
# Get basic project info
|
|
517
|
+
name = os.environ.get("REPOKIT_NAME")
|
|
518
|
+
if name:
|
|
519
|
+
config["name"] = name
|
|
520
|
+
|
|
521
|
+
description = os.environ.get("REPOKIT_DESCRIPTION")
|
|
522
|
+
if description:
|
|
523
|
+
config["description"] = description
|
|
524
|
+
|
|
525
|
+
language = os.environ.get("REPOKIT_LANGUAGE")
|
|
526
|
+
if language:
|
|
527
|
+
config["language"] = language
|
|
528
|
+
|
|
529
|
+
# Get user info
|
|
530
|
+
user_name = os.environ.get("GIT_USER_NAME")
|
|
531
|
+
user_email = os.environ.get("GIT_USER_EMAIL")
|
|
532
|
+
|
|
533
|
+
if user_name or user_email:
|
|
534
|
+
config["user"] = {}
|
|
535
|
+
if user_name:
|
|
536
|
+
config["user"]["name"] = user_name
|
|
537
|
+
if user_email:
|
|
538
|
+
config["user"]["email"] = user_email
|
|
539
|
+
|
|
540
|
+
return config
|
|
541
|
+
|
|
542
|
+
def get_git_user_info(self) -> Dict[str, str]:
|
|
543
|
+
"""
|
|
544
|
+
Get Git user information from environment or global Git config.
|
|
545
|
+
|
|
546
|
+
Returns:
|
|
547
|
+
Dictionary with user name and email
|
|
548
|
+
"""
|
|
549
|
+
user_info = {"name": "", "email": ""}
|
|
550
|
+
|
|
551
|
+
# Try environment variables first
|
|
552
|
+
user_name = os.environ.get("GIT_USER_NAME")
|
|
553
|
+
user_email = os.environ.get("GIT_USER_EMAIL")
|
|
554
|
+
|
|
555
|
+
if user_name:
|
|
556
|
+
user_info["name"] = user_name
|
|
557
|
+
|
|
558
|
+
if user_email:
|
|
559
|
+
user_info["email"] = user_email
|
|
560
|
+
|
|
561
|
+
# If not in environment, try Git global config
|
|
562
|
+
if not user_name or not user_email:
|
|
563
|
+
try:
|
|
564
|
+
import subprocess
|
|
565
|
+
|
|
566
|
+
if not user_name:
|
|
567
|
+
try:
|
|
568
|
+
result = subprocess.run(
|
|
569
|
+
["git", "config", "--global", "user.name"],
|
|
570
|
+
capture_output=True,
|
|
571
|
+
text=True,
|
|
572
|
+
check=False,
|
|
573
|
+
)
|
|
574
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
575
|
+
user_info["name"] = result.stdout.strip()
|
|
576
|
+
except Exception:
|
|
577
|
+
pass
|
|
578
|
+
|
|
579
|
+
if not user_email:
|
|
580
|
+
try:
|
|
581
|
+
result = subprocess.run(
|
|
582
|
+
["git", "config", "--global", "user.email"],
|
|
583
|
+
capture_output=True,
|
|
584
|
+
text=True,
|
|
585
|
+
check=False,
|
|
586
|
+
)
|
|
587
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
588
|
+
user_info["email"] = result.stdout.strip()
|
|
589
|
+
except Exception:
|
|
590
|
+
pass
|
|
591
|
+
except Exception as e:
|
|
592
|
+
self.logger.warning(f"Could not get Git user info: {str(e)}")
|
|
593
|
+
|
|
594
|
+
return user_info
|
|
595
|
+
|
|
596
|
+
def apply_github_email_privacy(self, user_info: Dict[str, str]) -> Dict[str, str]:
|
|
597
|
+
"""
|
|
598
|
+
Apply GitHub email privacy protection to user email.
|
|
599
|
+
|
|
600
|
+
Args:
|
|
601
|
+
user_info: Dictionary with user name and email
|
|
602
|
+
|
|
603
|
+
Returns:
|
|
604
|
+
Updated user info dictionary
|
|
605
|
+
"""
|
|
606
|
+
updated_info = user_info.copy()
|
|
607
|
+
|
|
608
|
+
# Apply GitHub privacy protection if enabled and pushing to GitHub
|
|
609
|
+
if self.config.get("use_github_noreply", True) and self.config.get(
|
|
610
|
+
"github", True
|
|
611
|
+
):
|
|
612
|
+
email = updated_info.get("email", "")
|
|
613
|
+
|
|
614
|
+
# If email isn't already a no-reply format
|
|
615
|
+
if email and "@users.noreply.github.com" not in email:
|
|
616
|
+
# Extract username from email or use first part
|
|
617
|
+
username = email.split("@")[0]
|
|
618
|
+
|
|
619
|
+
# If no username from email, try to use name
|
|
620
|
+
if not username and "name" in updated_info:
|
|
621
|
+
# Convert name to a username-like format
|
|
622
|
+
username = updated_info["name"].lower().replace(" ", "-")
|
|
623
|
+
|
|
624
|
+
# Create GitHub no-reply email if we have a username
|
|
625
|
+
if username:
|
|
626
|
+
github_email = f"{username}@users.noreply.github.com"
|
|
627
|
+
self.logger.info(
|
|
628
|
+
f"Using GitHub no-reply email format: {github_email}"
|
|
629
|
+
)
|
|
630
|
+
updated_info["email"] = github_email
|
|
631
|
+
|
|
632
|
+
return updated_info
|
|
633
|
+
|
|
634
|
+
def resolve_directory_profiles(self) -> None:
|
|
635
|
+
"""
|
|
636
|
+
Resolve directory profiles into actual directory lists.
|
|
637
|
+
|
|
638
|
+
This method converts directory profile names, groups, and explicit directories
|
|
639
|
+
into a final list of directories to create.
|
|
640
|
+
"""
|
|
641
|
+
# Load custom directory profiles from config if available
|
|
642
|
+
if "directory_profiles" in self.config:
|
|
643
|
+
self.directory_manager.profiles.update(self.config["directory_profiles"])
|
|
644
|
+
|
|
645
|
+
if "directory_groups" in self.config and isinstance(self.config["directory_groups"], dict):
|
|
646
|
+
self.directory_manager.groups.update(self.config["directory_groups"])
|
|
647
|
+
|
|
648
|
+
if "directory_types" in self.config:
|
|
649
|
+
self.directory_manager.type_mapping.update(self.config["directory_types"])
|
|
650
|
+
|
|
651
|
+
# Check if we're using a directory profile
|
|
652
|
+
profile = self.config.get("directory_profile")
|
|
653
|
+
groups = self.config.get("directory_groups", [])
|
|
654
|
+
private_set = self.config.get("private_set", "standard")
|
|
655
|
+
|
|
656
|
+
# If we have a profile or groups, use the directory manager
|
|
657
|
+
if profile or groups:
|
|
658
|
+
# Get all directories from the profile manager
|
|
659
|
+
all_dirs = self.directory_manager.get_all_directories(
|
|
660
|
+
profile=profile or "standard",
|
|
661
|
+
groups=groups,
|
|
662
|
+
private_set=private_set,
|
|
663
|
+
package_name=self.config.get("name")
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
# Convert to flat lists
|
|
667
|
+
directories = all_dirs["standard"]
|
|
668
|
+
private_dirs = all_dirs["private"]
|
|
669
|
+
|
|
670
|
+
# Only add explicit additional directories from CLI, not from default config
|
|
671
|
+
if "directories" in self.cli_config:
|
|
672
|
+
existing_dirs = set(directories)
|
|
673
|
+
for d in self.cli_config["directories"]:
|
|
674
|
+
if d not in existing_dirs:
|
|
675
|
+
directories.append(d)
|
|
676
|
+
|
|
677
|
+
# Update config with resolved directories
|
|
678
|
+
self.config["directories"] = directories
|
|
679
|
+
self.config["private_dirs"] = private_dirs
|
|
680
|
+
|
|
681
|
+
if self.verbose >= 1:
|
|
682
|
+
self.logger.info(f"Resolved directory profile '{profile}' to {len(directories)} directories")
|
|
683
|
+
|
|
684
|
+
# Ensure private_dirs is always set
|
|
685
|
+
if "private_dirs" not in self.config:
|
|
686
|
+
self.config["private_dirs"] = DEFAULT_PRIVATE_DIRS.copy()
|