git-repokit 0.9.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. git_repokit-0.9.10.dist-info/METADATA +422 -0
  2. git_repokit-0.9.10.dist-info/RECORD +174 -0
  3. git_repokit-0.9.10.dist-info/WHEEL +5 -0
  4. git_repokit-0.9.10.dist-info/entry_points.txt +2 -0
  5. git_repokit-0.9.10.dist-info/licenses/LICENSE +9 -0
  6. git_repokit-0.9.10.dist-info/top_level.txt +1 -0
  7. repokit/__init__.py +8 -0
  8. repokit/__main__.py +12 -0
  9. repokit/__version__.py +4 -0
  10. repokit/_vendor/__init__.py +0 -0
  11. repokit/_vendor/ailib/__init__.py +1 -0
  12. repokit/_vendor/ailib/analyzer.py +177 -0
  13. repokit/_vendor/ailib/backend.py +95 -0
  14. repokit/_vendor/ailib/cache.py +97 -0
  15. repokit/_vendor/ailib/parsers.py +52 -0
  16. repokit/_vendor/ailib/transports/__init__.py +16 -0
  17. repokit/_vendor/ailib/transports/cli.py +312 -0
  18. repokit/_vendor/ailib/transports/openai_compat.py +383 -0
  19. repokit/_vendor/ailib/transports/prompt_file.py +41 -0
  20. repokit/_vendor/ailib/types.py +224 -0
  21. repokit/auth_integration.py +244 -0
  22. repokit/badges.py +183 -0
  23. repokit/branch_utils.py +364 -0
  24. repokit/cli.py +2997 -0
  25. repokit/common_kit.py +202 -0
  26. repokit/config.py +686 -0
  27. repokit/defaults.py +220 -0
  28. repokit/directory_analyzer.py +1244 -0
  29. repokit/directory_profiles.py +423 -0
  30. repokit/git_config.py +370 -0
  31. repokit/github_create.py +142 -0
  32. repokit/github_setup.py +248 -0
  33. repokit/guardrails.py +707 -0
  34. repokit/history_cleaner.py +449 -0
  35. repokit/history_protection.py +434 -0
  36. repokit/hooks/pre-commit-guardrails +122 -0
  37. repokit/hooks/pre-commit-template +28 -0
  38. repokit/layers.py +510 -0
  39. repokit/licenses/0bsd.txt +14 -0
  40. repokit/licenses/LICENSES.md +27 -0
  41. repokit/licenses/__init__.py +142 -0
  42. repokit/licenses/agpl-3.0.txt +661 -0
  43. repokit/licenses/all-rights-reserved.txt +10 -0
  44. repokit/licenses/apache-2.0.txt +201 -0
  45. repokit/licenses/bsd-2-clause.txt +24 -0
  46. repokit/licenses/bsd-3-clause.txt +28 -0
  47. repokit/licenses/bsl-1.0.txt +23 -0
  48. repokit/licenses/cc-by-4.0.txt +395 -0
  49. repokit/licenses/cc-by-sa-4.0.txt +427 -0
  50. repokit/licenses/cc0-1.0.txt +121 -0
  51. repokit/licenses/epl-2.0.txt +277 -0
  52. repokit/licenses/gpl-2.0.txt +339 -0
  53. repokit/licenses/gpl-3.0.txt +674 -0
  54. repokit/licenses/isc.txt +15 -0
  55. repokit/licenses/lgpl-2.1.txt +504 -0
  56. repokit/licenses/lgpl-3.0.txt +165 -0
  57. repokit/licenses/mit.txt +21 -0
  58. repokit/licenses/mpl-2.0.txt +373 -0
  59. repokit/licenses/unlicense.txt +24 -0
  60. repokit/readme_blocks.py +112 -0
  61. repokit/remote_integration.py +627 -0
  62. repokit/repo_manager.py +2796 -0
  63. repokit/template_engine.py +391 -0
  64. repokit/templates/ai/claude/CLAUDE-nested.md.template +25 -0
  65. repokit/templates/ai/claude/CLAUDE.md.template +98 -0
  66. repokit/templates/ai/claude/instructions/step1_context_rebuilder.md +33 -0
  67. repokit/templates/ai/claude/instructions/step2_dev_workflow_process.md +67 -0
  68. repokit/templates/ai/claude/instructions/step3_context_bridge.md +52 -0
  69. repokit/templates/common/CHANGELOG.md.template +15 -0
  70. repokit/templates/common/CONTRIBUTING.md.template +31 -0
  71. repokit/templates/common/README.md.template +30 -0
  72. repokit/templates/common/gitattributes.template +17 -0
  73. repokit/templates/common/gitignore.template +141 -0
  74. repokit/templates/extras/FUNDING.yml.template +13 -0
  75. repokit/templates/extras/ROADMAP.md.template +17 -0
  76. repokit/templates/extras/dependabot.yml.template +22 -0
  77. repokit/templates/extras/one-offs-README.md.template +28 -0
  78. repokit/templates/extras/platform-support.md.template +11 -0
  79. repokit/templates/extras/pull_request_template.md.template +25 -0
  80. repokit/templates/extras/release-template.md.template +34 -0
  81. repokit/templates/extras/settings.json.template +11 -0
  82. repokit/templates/extras/stale.yml.template +14 -0
  83. repokit/templates/github/CODEOWNERS.template +11 -0
  84. repokit/templates/github/ISSUE_TEMPLATE/bug-report.md.template +29 -0
  85. repokit/templates/github/ISSUE_TEMPLATE/feature-request.md.template +19 -0
  86. repokit/templates/github/issues/notes.md.template +18 -0
  87. repokit/templates/github/issues/roadmap.md.template +15 -0
  88. repokit/templates/hooks/pre-commit.template +64 -0
  89. repokit/templates/layers/archetypes/any/claude-plugin/.claude-plugin/marketplace.json +20 -0
  90. repokit/templates/layers/archetypes/any/claude-plugin/plugin/.claude-plugin/plugin.json +15 -0
  91. repokit/templates/layers/archetypes/any/claude-plugin/plugin/README.md +28 -0
  92. repokit/templates/layers/archetypes/c-cpp/cmake-app/.github/workflows/ci-c-cpp.yml +24 -0
  93. repokit/templates/layers/archetypes/c-cpp/cmake-app/.repokit-common.toml.rkappend +4 -0
  94. repokit/templates/layers/archetypes/c-cpp/cmake-app/.vscode/launch.json +29 -0
  95. repokit/templates/layers/archetypes/c-cpp/cmake-app/.vscode/tasks.json +26 -0
  96. repokit/templates/layers/archetypes/c-cpp/cmake-app/CMakeLists.txt +47 -0
  97. repokit/templates/layers/archetypes/c-cpp/cmake-app/CMakePresets.json +23 -0
  98. repokit/templates/layers/archetypes/c-cpp/cmake-app/cmake/GenerateStamp.cmake +45 -0
  99. repokit/templates/layers/archetypes/c-cpp/cmake-app/src/main.cpp +27 -0
  100. repokit/templates/layers/archetypes/c-cpp/cmake-app/tests/test_version.cpp +20 -0
  101. repokit/templates/layers/archetypes/c-cpp/cmake-app/vcpkg.json +5 -0
  102. repokit/templates/layers/archetypes/c-cpp/minimal/.vscode/launch.json +27 -0
  103. repokit/templates/layers/archetypes/c-cpp/minimal/Makefile +10 -0
  104. repokit/templates/layers/archetypes/c-cpp/minimal/build.bat +7 -0
  105. repokit/templates/layers/archetypes/c-cpp/minimal/src/__RK_SNAKE__.c +26 -0
  106. repokit/templates/layers/archetypes/c-cpp/msbuild/.vscode/launch.json +17 -0
  107. repokit/templates/layers/archetypes/c-cpp/msbuild/.vscode/tasks.json +20 -0
  108. repokit/templates/layers/archetypes/c-cpp/msbuild/__RK_PROJECT__.sln +19 -0
  109. repokit/templates/layers/archetypes/c-cpp/msbuild/__RK_PROJECT__.vcxproj +41 -0
  110. repokit/templates/layers/archetypes/c-cpp/msbuild/src/main.cpp +26 -0
  111. repokit/templates/layers/archetypes/js-ts/claude-plugin/.repokit-common.toml +28 -0
  112. repokit/templates/layers/archetypes/js-ts/claude-plugin/.vscode/launch.json +34 -0
  113. repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/hooks/hooks.json +15 -0
  114. repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/hooks/scripts/session-start.mjs +23 -0
  115. repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/tests/manifest.test.js +30 -0
  116. repokit/templates/layers/archetypes/js-ts/node/.repokit-common.toml.rkappend +6 -0
  117. repokit/templates/layers/archetypes/js-ts/node/package.json +13 -0
  118. repokit/templates/layers/archetypes/js-ts/node-package/.github/workflows/release.yml +39 -0
  119. repokit/templates/layers/archetypes/js-ts/node-package/package.json +32 -0
  120. repokit/templates/layers/archetypes/js-ts/node-package/src/index.ts +13 -0
  121. repokit/templates/layers/archetypes/js-ts/node-package/tests/index.test.ts +11 -0
  122. repokit/templates/layers/archetypes/js-ts/script/.vscode/launch.json +34 -0
  123. repokit/templates/layers/archetypes/js-ts/script/__RK_PROJECT__.js +37 -0
  124. repokit/templates/layers/archetypes/js-ts/script/tests/version.test.js +26 -0
  125. repokit/templates/layers/archetypes/js-ts/ts/package.json +18 -0
  126. repokit/templates/layers/archetypes/js-ts/ts/tsconfig.json +15 -0
  127. repokit/templates/layers/archetypes/python/claude-plugin/.github/workflows/ci-python.yml +44 -0
  128. repokit/templates/layers/archetypes/python/claude-plugin/.repokit-common.toml +21 -0
  129. repokit/templates/layers/archetypes/python/claude-plugin/.vscode/launch.json +33 -0
  130. repokit/templates/layers/archetypes/python/claude-plugin/VERSION +1 -0
  131. repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/hooks.json +15 -0
  132. repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/scripts/run-hook.mjs +42 -0
  133. repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/scripts/session_start.py +31 -0
  134. repokit/templates/layers/archetypes/python/claude-plugin/plugin/tests/test_manifest.py +42 -0
  135. repokit/templates/layers/archetypes/python/pypi/.github/workflows/release.yml +63 -0
  136. repokit/templates/layers/archetypes/python/pypi/.vscode/launch.json +43 -0
  137. repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/__init__.py +5 -0
  138. repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/__main__.py +6 -0
  139. repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/_version.py +96 -0
  140. repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/cli.py +18 -0
  141. repokit/templates/layers/archetypes/python/pypi/pyproject.toml +71 -0
  142. repokit/templates/layers/archetypes/python/pypi/tests/conftest.py +11 -0
  143. repokit/templates/layers/archetypes/python/pypi/tests/test_version.py +50 -0
  144. repokit/templates/layers/archetypes/python/script/.github/workflows/ci-python.yml +43 -0
  145. repokit/templates/layers/archetypes/python/script/.repokit-common.toml +14 -0
  146. repokit/templates/layers/archetypes/python/script/.vscode/launch.json +33 -0
  147. repokit/templates/layers/archetypes/python/script/VERSION +1 -0
  148. repokit/templates/layers/archetypes/python/script/__RK_PROJECT__.py +40 -0
  149. repokit/templates/layers/archetypes/python/script/tests/test_version.py +25 -0
  150. repokit/templates/layers/archetypes/python/setuppy/requirements.txt +8 -0
  151. repokit/templates/layers/archetypes/python/setuppy/setup.py +23 -0
  152. repokit/templates/layers/languages/c-cpp/.gitignore.rkappend +20 -0
  153. repokit/templates/layers/languages/c-cpp/.repokit-common.toml +7 -0
  154. repokit/templates/layers/languages/c-cpp/include/__RK_SNAKE__/version.h +20 -0
  155. repokit/templates/layers/languages/c-cpp/src/app.rc +25 -0
  156. repokit/templates/layers/languages/js-ts/.editorconfig +15 -0
  157. repokit/templates/layers/languages/js-ts/.github/workflows/ci-js-ts.yml +41 -0
  158. repokit/templates/layers/languages/js-ts/.gitignore.rkappend +13 -0
  159. repokit/templates/layers/languages/js-ts/.repokit-common.toml +19 -0
  160. repokit/templates/layers/languages/js-ts/.vscode/launch.json +24 -0
  161. repokit/templates/layers/languages/js-ts/VERSION +1 -0
  162. repokit/templates/layers/languages/python/.github/workflows/ci-python.yml +76 -0
  163. repokit/templates/layers/languages/python/.gitignore.rkappend +15 -0
  164. repokit/templates/layers/languages/python/.vscode/launch.json +33 -0
  165. repokit/templates/layers/manifest.toml +302 -0
  166. repokit/templates/vault/CLAUDE.md +29 -0
  167. repokit/templates/vault/claude/_maps/MOC-project.md +15 -0
  168. repokit/templates/vault/claude/_oracle/backlinks.md +3 -0
  169. repokit/templates/vault/claude/_oracle/concepts.md +12 -0
  170. repokit/templates/vault/claude/_oracle/manifest.md +10 -0
  171. repokit/templates/vault/claude/_todo/ACTIVE-WORK.md +18 -0
  172. repokit/templates/vault/claude/_todo/OPEN-LOOPS.md +14 -0
  173. repokit/utils.py +316 -0
  174. repokit/vault.py +248 -0
repokit/git_config.py ADDED
@@ -0,0 +1,370 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Git Configuration Manager for RepoKit
4
+
5
+ Enhanced git configuration detection with multi-level cascade support.
6
+ Provides comprehensive user.name and user.email detection across different
7
+ git config levels.
8
+ """
9
+
10
+ import os
11
+ import subprocess
12
+ import logging
13
+ from typing import Dict, Optional, Any
14
+
15
+
16
+ class GitConfigManager:
17
+ """
18
+ Manages git configuration detection with multi-level cascade support.
19
+
20
+ Detection levels (in order of precedence):
21
+ 1. Repository-specific config (.git/config)
22
+ 2. Global user config (~/.gitconfig)
23
+ 3. System config (/etc/gitconfig)
24
+ 4. Environment variables
25
+ 5. Interactive prompts (if enabled)
26
+ """
27
+
28
+ def __init__(self, repo_path: Optional[str] = None, verbose: int = 0):
29
+ """
30
+ Initialize GitConfigManager.
31
+
32
+ Args:
33
+ repo_path: Path to git repository (for repo-specific config)
34
+ verbose: Verbosity level for logging
35
+ """
36
+ self.repo_path = repo_path
37
+ self.verbose = verbose
38
+ self.logger = logging.getLogger("repokit.git_config")
39
+
40
+ def get_comprehensive_user_info(self, interactive: bool = True) -> Dict[str, str]:
41
+ """
42
+ Get git user information using comprehensive detection cascade.
43
+
44
+ Args:
45
+ interactive: Whether to prompt user if information is missing
46
+
47
+ Returns:
48
+ Dictionary with 'name' and 'email' keys
49
+ """
50
+ user_info = {}
51
+
52
+ # Level 1: Repository-specific config
53
+ if self.repo_path:
54
+ repo_config = self._get_repo_git_config()
55
+ user_info.update(repo_config)
56
+ if self.verbose >= 2:
57
+ self.logger.debug(f"Repository config: {repo_config}")
58
+
59
+ # Level 2: Global git config (if missing data)
60
+ if not user_info.get('name') or not user_info.get('email'):
61
+ global_config = self._get_global_git_config()
62
+ for key, value in global_config.items():
63
+ if not user_info.get(key):
64
+ user_info[key] = value
65
+ if self.verbose >= 2:
66
+ self.logger.debug(f"Global config: {global_config}")
67
+
68
+ # Level 3: System git config (if still missing data)
69
+ if not user_info.get('name') or not user_info.get('email'):
70
+ system_config = self._get_system_git_config()
71
+ for key, value in system_config.items():
72
+ if not user_info.get(key):
73
+ user_info[key] = value
74
+ if self.verbose >= 2:
75
+ self.logger.debug(f"System config: {system_config}")
76
+
77
+ # Level 4: Environment variables
78
+ if not user_info.get('name') or not user_info.get('email'):
79
+ env_config = self._get_env_git_config()
80
+ for key, value in env_config.items():
81
+ if not user_info.get(key):
82
+ user_info[key] = value
83
+ if self.verbose >= 2:
84
+ self.logger.debug(f"Environment config: {env_config}")
85
+
86
+ # Level 5: Interactive prompts (if enabled and data still missing)
87
+ missing_info = (not user_info.get('name') or
88
+ not user_info.get('email'))
89
+ if interactive and missing_info:
90
+ interactive_config = self._prompt_for_git_config(user_info)
91
+ user_info.update(interactive_config)
92
+ if self.verbose >= 2:
93
+ self.logger.debug(f"Interactive config: {interactive_config}")
94
+
95
+ # Log final result
96
+ if self.verbose >= 1:
97
+ name = user_info.get('name', 'NOT SET')
98
+ email = user_info.get('email', 'NOT SET')
99
+ self.logger.info(f"Final git user config: name='{name}', "
100
+ f"email='{email}'")
101
+
102
+ return user_info
103
+
104
+ def _get_repo_git_config(self) -> Dict[str, str]:
105
+ """Get user config from repository-specific .git/config."""
106
+ if not self.repo_path:
107
+ return {}
108
+
109
+ git_path = os.path.join(self.repo_path, ".git")
110
+ if not os.path.exists(git_path):
111
+ return {}
112
+
113
+ config = {}
114
+
115
+ # Get user.name from repo config
116
+ try:
117
+ result = subprocess.run(
118
+ ["git", "config", "--local", "user.name"],
119
+ cwd=self.repo_path,
120
+ capture_output=True,
121
+ text=True,
122
+ check=False,
123
+ )
124
+ if result.returncode == 0 and result.stdout.strip():
125
+ config["name"] = result.stdout.strip()
126
+ except Exception as e:
127
+ if self.verbose >= 3:
128
+ self.logger.debug(f"Failed to get repo user.name: {e}")
129
+
130
+ # Get user.email from repo config
131
+ try:
132
+ result = subprocess.run(
133
+ ["git", "config", "--local", "user.email"],
134
+ cwd=self.repo_path,
135
+ capture_output=True,
136
+ text=True,
137
+ check=False,
138
+ )
139
+ if result.returncode == 0 and result.stdout.strip():
140
+ config["email"] = result.stdout.strip()
141
+ except Exception as e:
142
+ if self.verbose >= 3:
143
+ self.logger.debug(f"Failed to get repo user.email: {e}")
144
+
145
+ return config
146
+
147
+ def _get_global_git_config(self) -> Dict[str, str]:
148
+ """Get user config from global ~/.gitconfig."""
149
+ config = {}
150
+
151
+ # Get user.name from global config
152
+ try:
153
+ result = subprocess.run(
154
+ ["git", "config", "--global", "user.name"],
155
+ capture_output=True,
156
+ text=True,
157
+ check=False,
158
+ )
159
+ if result.returncode == 0 and result.stdout.strip():
160
+ config["name"] = result.stdout.strip()
161
+ except Exception as e:
162
+ if self.verbose >= 3:
163
+ self.logger.debug(f"Failed to get global user.name: {e}")
164
+
165
+ # Get user.email from global config
166
+ try:
167
+ result = subprocess.run(
168
+ ["git", "config", "--global", "user.email"],
169
+ capture_output=True,
170
+ text=True,
171
+ check=False,
172
+ )
173
+ if result.returncode == 0 and result.stdout.strip():
174
+ config["email"] = result.stdout.strip()
175
+ except Exception as e:
176
+ if self.verbose >= 3:
177
+ self.logger.debug(f"Failed to get global user.email: {e}")
178
+
179
+ return config
180
+
181
+ def _get_system_git_config(self) -> Dict[str, str]:
182
+ """Get user config from system /etc/gitconfig."""
183
+ config = {}
184
+
185
+ # Get user.name from system config
186
+ try:
187
+ result = subprocess.run(
188
+ ["git", "config", "--system", "user.name"],
189
+ capture_output=True,
190
+ text=True,
191
+ check=False,
192
+ )
193
+ if result.returncode == 0 and result.stdout.strip():
194
+ config["name"] = result.stdout.strip()
195
+ except Exception as e:
196
+ if self.verbose >= 3:
197
+ self.logger.debug(f"Failed to get system user.name: {e}")
198
+
199
+ # Get user.email from system config
200
+ try:
201
+ result = subprocess.run(
202
+ ["git", "config", "--system", "user.email"],
203
+ capture_output=True,
204
+ text=True,
205
+ check=False,
206
+ )
207
+ if result.returncode == 0 and result.stdout.strip():
208
+ config["email"] = result.stdout.strip()
209
+ except Exception as e:
210
+ if self.verbose >= 3:
211
+ self.logger.debug(f"Failed to get system user.email: {e}")
212
+
213
+ return config
214
+
215
+ def _get_env_git_config(self) -> Dict[str, str]:
216
+ """Get user config from environment variables."""
217
+ config = {}
218
+
219
+ # Check for git-specific environment variables
220
+ if os.environ.get('GIT_AUTHOR_NAME'):
221
+ config["name"] = os.environ['GIT_AUTHOR_NAME']
222
+ elif os.environ.get('GIT_COMMITTER_NAME'):
223
+ config["name"] = os.environ['GIT_COMMITTER_NAME']
224
+
225
+ if os.environ.get('GIT_AUTHOR_EMAIL'):
226
+ config["email"] = os.environ['GIT_AUTHOR_EMAIL']
227
+ elif os.environ.get('GIT_COMMITTER_EMAIL'):
228
+ config["email"] = os.environ['GIT_COMMITTER_EMAIL']
229
+
230
+ # Check for RepoKit-specific environment variables
231
+ if os.environ.get('REPOKIT_USER_NAME'):
232
+ config["name"] = os.environ['REPOKIT_USER_NAME']
233
+
234
+ if os.environ.get('REPOKIT_USER_EMAIL'):
235
+ config["email"] = os.environ['REPOKIT_USER_EMAIL']
236
+
237
+ return config
238
+
239
+ def _prompt_for_git_config(self, existing_config: Dict[str, str]) -> Dict[str, str]:
240
+ """
241
+ Interactively prompt user for missing git configuration.
242
+
243
+ Args:
244
+ existing_config: Already detected configuration
245
+
246
+ Returns:
247
+ Dictionary with user-provided values
248
+ """
249
+ config = {}
250
+
251
+ try:
252
+ # Prompt for name if missing
253
+ if not existing_config.get('name'):
254
+ print("\n" + "="*60)
255
+ print("Git User Configuration Required")
256
+ print("="*60)
257
+ print("RepoKit needs your git user information for commits "
258
+ "and templates.")
259
+ print("This will be saved to your project's .repokit.json file.")
260
+ print()
261
+
262
+ name = input("Enter your full name (for git commits): ").strip()
263
+ if name:
264
+ config["name"] = name
265
+
266
+ # Prompt for email if missing
267
+ if not existing_config.get('email'):
268
+ if not existing_config.get('name'):
269
+ print() # Add spacing if we already prompted for name
270
+
271
+ email = input("Enter your email address (for git commits): ").strip()
272
+ if email:
273
+ config["email"] = email
274
+
275
+ if config:
276
+ print()
277
+ print("✓ Configuration collected. This will be saved for future use.")
278
+ print("="*60)
279
+
280
+ except (KeyboardInterrupt, EOFError):
281
+ # User cancelled input
282
+ if self.verbose >= 1:
283
+ self.logger.info("User cancelled git config input")
284
+
285
+ return config
286
+
287
+ def get_template_context(self, base_context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
288
+ """
289
+ Get enhanced template context with git user information.
290
+
291
+ Args:
292
+ base_context: Existing template context to enhance
293
+
294
+ Returns:
295
+ Enhanced context dictionary with git user info
296
+ """
297
+ context = base_context.copy() if base_context else {}
298
+
299
+ # Get comprehensive user info
300
+ user_info = self.get_comprehensive_user_info(interactive=False)
301
+
302
+ # Add git-specific template variables
303
+ context.update({
304
+ 'author': user_info.get('name', 'Unknown Author'),
305
+ 'author_email': user_info.get('email', 'unknown@example.com'),
306
+ 'git_user_name': user_info.get('name'),
307
+ 'git_user_email': user_info.get('email'),
308
+
309
+ # Backwards compatibility
310
+ 'user_name': user_info.get('name'),
311
+ 'user_email': user_info.get('email'),
312
+ })
313
+
314
+ return context
315
+
316
+ def configure_repo_git_user(self, user_info: Optional[Dict[str, str]] = None) -> bool:
317
+ """
318
+ Configure git user settings in the repository.
319
+
320
+ Args:
321
+ user_info: User info dict, or None to detect automatically
322
+
323
+ Returns:
324
+ True if configuration was successful
325
+ """
326
+ if not self.repo_path:
327
+ self.logger.warning("Cannot configure git user: no repository path")
328
+ return False
329
+
330
+ git_path = os.path.join(self.repo_path, ".git")
331
+ if not os.path.exists(git_path):
332
+ self.logger.warning("Cannot configure git user: not a git repository")
333
+ return False
334
+
335
+ if not user_info:
336
+ user_info = self.get_comprehensive_user_info(interactive=True)
337
+
338
+ success = True
339
+
340
+ # Configure user.name
341
+ if user_info.get('name'):
342
+ try:
343
+ subprocess.run(
344
+ ["git", "config", "user.name", user_info['name']],
345
+ cwd=self.repo_path,
346
+ check=True,
347
+ capture_output=True
348
+ )
349
+ if self.verbose >= 1:
350
+ self.logger.info(f"Set git user.name to: {user_info['name']}")
351
+ except subprocess.CalledProcessError as e:
352
+ self.logger.error(f"Failed to set git user.name: {e}")
353
+ success = False
354
+
355
+ # Configure user.email
356
+ if user_info.get('email'):
357
+ try:
358
+ subprocess.run(
359
+ ["git", "config", "user.email", user_info['email']],
360
+ cwd=self.repo_path,
361
+ check=True,
362
+ capture_output=True
363
+ )
364
+ if self.verbose >= 1:
365
+ self.logger.info(f"Set git user.email to: {user_info['email']}")
366
+ except subprocess.CalledProcessError as e:
367
+ self.logger.error(f"Failed to set git user.email: {e}")
368
+ success = False
369
+
370
+ return success
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ `repokit create --github`: the repository on GitHub, through the gh CLI.
4
+
5
+ gh holds the login (`gh auth login`), so git-repokit never handles a token on
6
+ this path -- the ease the GitHub template used to give, where GitHub made the
7
+ repository and the tooling only configured it (djdarcy, 2026-10-07).
8
+
9
+ Only public branches are pushed: in the private-branch model the private
10
+ branch never leaves the machine, so `gh repo create --push` (which pushes the
11
+ current branch -- the private one there) is not used; branches are pushed
12
+ explicitly.
13
+ """
14
+
15
+ import os
16
+ import shutil
17
+ import subprocess
18
+ import sys
19
+ from typing import List, Optional, Tuple
20
+
21
+ from .defaults import strategy_push_branches
22
+
23
+ GH_ENV = "REPOKIT_GH" # override the gh executable (tests use a stub)
24
+
25
+
26
+ def gh_command() -> Optional[List[str]]:
27
+ override = os.environ.get(GH_ENV)
28
+ if override:
29
+ return [sys.executable, override] if override.endswith(".py") else [override]
30
+ found = shutil.which("gh")
31
+ return [found] if found else None
32
+
33
+
34
+ def _run(cmd: List[str], cwd: str) -> Tuple[int, str]:
35
+ try:
36
+ result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True,
37
+ encoding="utf-8", errors="replace")
38
+ except OSError as e: # the executable does not exist or cannot run
39
+ return 127, str(e)
40
+ return result.returncode, (result.stdout + result.stderr).strip()
41
+
42
+
43
+ def gh_login(cwd: str) -> Optional[str]:
44
+ """The account gh is logged into, or None when gh is absent, logged out or
45
+ unreadable. Read before any file is written (0.9.6), so the project's URLs,
46
+ CODEOWNERS and FUNDING carry the real owner rather than a placeholder;
47
+ create_on_github still checks the login itself and reports any problem."""
48
+ gh = gh_command()
49
+ if gh is None:
50
+ return None
51
+ rc, out = _run(gh + ["api", "user", "-q", ".login"], cwd)
52
+ login = out.splitlines()[-1].strip() if rc == 0 and out else ""
53
+ return login or None
54
+
55
+
56
+ def branches_to_push(config: dict, private_branch: Optional[str], existing: List[str]) -> List[str]:
57
+ """The strategy's public branches that exist locally; never the private branch."""
58
+ wanted = strategy_push_branches(config.get("branch_strategy", "main-only")) or ["main"]
59
+ return [b for b in wanted if b in existing and b != private_branch]
60
+
61
+
62
+ def create_on_github(repo_root: str, name: str, config: dict, private_branch: Optional[str],
63
+ organization: Optional[str] = None, public: bool = False,
64
+ logger=None, report: Optional[List[str]] = None) -> Optional[str]:
65
+ """Create <owner>/<name> on GitHub, run the GitHub-half steps the config
66
+ names (`github_setup`, 0.9.3: labels and the two issues before the push,
67
+ the rest after), and push the public branches.
68
+
69
+ Returns None on success, or what went wrong (the local repository is
70
+ left as it is either way). The steps' report lines go into ``report``.
71
+ """
72
+ from . import github_setup
73
+ steps = github_setup.setup_names(config.get("github_setup"))
74
+ report = report if report is not None else []
75
+
76
+ gh = gh_command()
77
+ if gh is None:
78
+ return "gh (the GitHub CLI) is not installed; see https://cli.github.com"
79
+
80
+ rc, out = _run(gh + ["auth", "status"], repo_root)
81
+ if rc != 0:
82
+ return f"gh is not logged in (run `gh auth login`): {out}"
83
+
84
+ rc, out = _run(gh + ["api", "user", "-q", ".login"], repo_root)
85
+ login = out.splitlines()[-1].strip() if rc == 0 and out else None
86
+ if not organization and not login:
87
+ return f"could not read the GitHub user from gh: {out}"
88
+ # Refuse on another account: the config names the person's handle, and gh
89
+ # is logged into someone else (0.9.3).
90
+ github_cfg = config.get("github") if isinstance(config.get("github"), dict) else {}
91
+ configured = (github_cfg or {}).get("username")
92
+ if configured and login and configured.lower() != login.lower():
93
+ return (f"gh is logged into {login!r} but the config's github.username is {configured!r}; "
94
+ "switch with `gh auth switch` or fix the config")
95
+ owner = organization or login
96
+
97
+ create = gh + ["repo", "create", f"{owner}/{name}",
98
+ "--public" if public else "--private",
99
+ "--source", repo_root, "--remote", "origin"]
100
+ description = config.get("description")
101
+ if description:
102
+ create += ["--description", description]
103
+ rc, out = _run(create, repo_root)
104
+ if rc != 0:
105
+ return f"gh repo create failed: {out}"
106
+ if logger:
107
+ logger.info(f"Created {owner}/{name} on GitHub ({'public' if public else 'private'})")
108
+
109
+ # Before the first push: the labels and the two issues, so they are #1 and #2.
110
+ setup = github_setup.GhSetup(gh, f"{owner}/{name}", lambda cmd: _run(cmd, repo_root))
111
+ project = config.get("name") or name
112
+ if "labels" in steps:
113
+ report += setup.labels(github_setup.referenced_labels(repo_root))
114
+ if "issues" in steps:
115
+ drafts = os.path.join(repo_root, "private", "claude", "issues")
116
+ report += setup.issues(project, drafts if os.path.isdir(drafts) else None)
117
+ if "pin" in steps:
118
+ report += setup.pin()
119
+
120
+ rc, out = _run(["git", "for-each-ref", "--format=%(refname:short)", "refs/heads"], repo_root)
121
+ existing = out.split()
122
+ pushed = []
123
+ for branch in branches_to_push(config, private_branch, existing):
124
+ rc, out = _run(["git", "push", "-u", "origin", branch], repo_root)
125
+ if rc != 0:
126
+ return f"git push of '{branch}' failed: {out}"
127
+ pushed.append(branch)
128
+ if logger:
129
+ logger.info(f"Pushed: {', '.join(pushed) or 'nothing'}")
130
+
131
+ # After the push: the settings that need the repository's content or no ordering.
132
+ if "discussions" in steps:
133
+ report += setup.discussions()
134
+ if "topics" in steps:
135
+ report += setup.topics(list(config.get("topics") or []))
136
+ if "pypi-environment" in steps:
137
+ report += setup.pypi_environment(config.get("archetype"))
138
+ if "traffic" in steps:
139
+ report += setup.traffic(project)
140
+ if steps:
141
+ report += github_setup.reminders(f"{owner}/{name}", list(config.get("extras") or []))
142
+ return None