git-repokit 0.9.10__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. git_repokit-0.9.10.dist-info/METADATA +422 -0
  2. git_repokit-0.9.10.dist-info/RECORD +174 -0
  3. git_repokit-0.9.10.dist-info/WHEEL +5 -0
  4. git_repokit-0.9.10.dist-info/entry_points.txt +2 -0
  5. git_repokit-0.9.10.dist-info/licenses/LICENSE +9 -0
  6. git_repokit-0.9.10.dist-info/top_level.txt +1 -0
  7. repokit/__init__.py +8 -0
  8. repokit/__main__.py +12 -0
  9. repokit/__version__.py +4 -0
  10. repokit/_vendor/__init__.py +0 -0
  11. repokit/_vendor/ailib/__init__.py +1 -0
  12. repokit/_vendor/ailib/analyzer.py +177 -0
  13. repokit/_vendor/ailib/backend.py +95 -0
  14. repokit/_vendor/ailib/cache.py +97 -0
  15. repokit/_vendor/ailib/parsers.py +52 -0
  16. repokit/_vendor/ailib/transports/__init__.py +16 -0
  17. repokit/_vendor/ailib/transports/cli.py +312 -0
  18. repokit/_vendor/ailib/transports/openai_compat.py +383 -0
  19. repokit/_vendor/ailib/transports/prompt_file.py +41 -0
  20. repokit/_vendor/ailib/types.py +224 -0
  21. repokit/auth_integration.py +244 -0
  22. repokit/badges.py +183 -0
  23. repokit/branch_utils.py +364 -0
  24. repokit/cli.py +2997 -0
  25. repokit/common_kit.py +202 -0
  26. repokit/config.py +686 -0
  27. repokit/defaults.py +220 -0
  28. repokit/directory_analyzer.py +1244 -0
  29. repokit/directory_profiles.py +423 -0
  30. repokit/git_config.py +370 -0
  31. repokit/github_create.py +142 -0
  32. repokit/github_setup.py +248 -0
  33. repokit/guardrails.py +707 -0
  34. repokit/history_cleaner.py +449 -0
  35. repokit/history_protection.py +434 -0
  36. repokit/hooks/pre-commit-guardrails +122 -0
  37. repokit/hooks/pre-commit-template +28 -0
  38. repokit/layers.py +510 -0
  39. repokit/licenses/0bsd.txt +14 -0
  40. repokit/licenses/LICENSES.md +27 -0
  41. repokit/licenses/__init__.py +142 -0
  42. repokit/licenses/agpl-3.0.txt +661 -0
  43. repokit/licenses/all-rights-reserved.txt +10 -0
  44. repokit/licenses/apache-2.0.txt +201 -0
  45. repokit/licenses/bsd-2-clause.txt +24 -0
  46. repokit/licenses/bsd-3-clause.txt +28 -0
  47. repokit/licenses/bsl-1.0.txt +23 -0
  48. repokit/licenses/cc-by-4.0.txt +395 -0
  49. repokit/licenses/cc-by-sa-4.0.txt +427 -0
  50. repokit/licenses/cc0-1.0.txt +121 -0
  51. repokit/licenses/epl-2.0.txt +277 -0
  52. repokit/licenses/gpl-2.0.txt +339 -0
  53. repokit/licenses/gpl-3.0.txt +674 -0
  54. repokit/licenses/isc.txt +15 -0
  55. repokit/licenses/lgpl-2.1.txt +504 -0
  56. repokit/licenses/lgpl-3.0.txt +165 -0
  57. repokit/licenses/mit.txt +21 -0
  58. repokit/licenses/mpl-2.0.txt +373 -0
  59. repokit/licenses/unlicense.txt +24 -0
  60. repokit/readme_blocks.py +112 -0
  61. repokit/remote_integration.py +627 -0
  62. repokit/repo_manager.py +2796 -0
  63. repokit/template_engine.py +391 -0
  64. repokit/templates/ai/claude/CLAUDE-nested.md.template +25 -0
  65. repokit/templates/ai/claude/CLAUDE.md.template +98 -0
  66. repokit/templates/ai/claude/instructions/step1_context_rebuilder.md +33 -0
  67. repokit/templates/ai/claude/instructions/step2_dev_workflow_process.md +67 -0
  68. repokit/templates/ai/claude/instructions/step3_context_bridge.md +52 -0
  69. repokit/templates/common/CHANGELOG.md.template +15 -0
  70. repokit/templates/common/CONTRIBUTING.md.template +31 -0
  71. repokit/templates/common/README.md.template +30 -0
  72. repokit/templates/common/gitattributes.template +17 -0
  73. repokit/templates/common/gitignore.template +141 -0
  74. repokit/templates/extras/FUNDING.yml.template +13 -0
  75. repokit/templates/extras/ROADMAP.md.template +17 -0
  76. repokit/templates/extras/dependabot.yml.template +22 -0
  77. repokit/templates/extras/one-offs-README.md.template +28 -0
  78. repokit/templates/extras/platform-support.md.template +11 -0
  79. repokit/templates/extras/pull_request_template.md.template +25 -0
  80. repokit/templates/extras/release-template.md.template +34 -0
  81. repokit/templates/extras/settings.json.template +11 -0
  82. repokit/templates/extras/stale.yml.template +14 -0
  83. repokit/templates/github/CODEOWNERS.template +11 -0
  84. repokit/templates/github/ISSUE_TEMPLATE/bug-report.md.template +29 -0
  85. repokit/templates/github/ISSUE_TEMPLATE/feature-request.md.template +19 -0
  86. repokit/templates/github/issues/notes.md.template +18 -0
  87. repokit/templates/github/issues/roadmap.md.template +15 -0
  88. repokit/templates/hooks/pre-commit.template +64 -0
  89. repokit/templates/layers/archetypes/any/claude-plugin/.claude-plugin/marketplace.json +20 -0
  90. repokit/templates/layers/archetypes/any/claude-plugin/plugin/.claude-plugin/plugin.json +15 -0
  91. repokit/templates/layers/archetypes/any/claude-plugin/plugin/README.md +28 -0
  92. repokit/templates/layers/archetypes/c-cpp/cmake-app/.github/workflows/ci-c-cpp.yml +24 -0
  93. repokit/templates/layers/archetypes/c-cpp/cmake-app/.repokit-common.toml.rkappend +4 -0
  94. repokit/templates/layers/archetypes/c-cpp/cmake-app/.vscode/launch.json +29 -0
  95. repokit/templates/layers/archetypes/c-cpp/cmake-app/.vscode/tasks.json +26 -0
  96. repokit/templates/layers/archetypes/c-cpp/cmake-app/CMakeLists.txt +47 -0
  97. repokit/templates/layers/archetypes/c-cpp/cmake-app/CMakePresets.json +23 -0
  98. repokit/templates/layers/archetypes/c-cpp/cmake-app/cmake/GenerateStamp.cmake +45 -0
  99. repokit/templates/layers/archetypes/c-cpp/cmake-app/src/main.cpp +27 -0
  100. repokit/templates/layers/archetypes/c-cpp/cmake-app/tests/test_version.cpp +20 -0
  101. repokit/templates/layers/archetypes/c-cpp/cmake-app/vcpkg.json +5 -0
  102. repokit/templates/layers/archetypes/c-cpp/minimal/.vscode/launch.json +27 -0
  103. repokit/templates/layers/archetypes/c-cpp/minimal/Makefile +10 -0
  104. repokit/templates/layers/archetypes/c-cpp/minimal/build.bat +7 -0
  105. repokit/templates/layers/archetypes/c-cpp/minimal/src/__RK_SNAKE__.c +26 -0
  106. repokit/templates/layers/archetypes/c-cpp/msbuild/.vscode/launch.json +17 -0
  107. repokit/templates/layers/archetypes/c-cpp/msbuild/.vscode/tasks.json +20 -0
  108. repokit/templates/layers/archetypes/c-cpp/msbuild/__RK_PROJECT__.sln +19 -0
  109. repokit/templates/layers/archetypes/c-cpp/msbuild/__RK_PROJECT__.vcxproj +41 -0
  110. repokit/templates/layers/archetypes/c-cpp/msbuild/src/main.cpp +26 -0
  111. repokit/templates/layers/archetypes/js-ts/claude-plugin/.repokit-common.toml +28 -0
  112. repokit/templates/layers/archetypes/js-ts/claude-plugin/.vscode/launch.json +34 -0
  113. repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/hooks/hooks.json +15 -0
  114. repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/hooks/scripts/session-start.mjs +23 -0
  115. repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/tests/manifest.test.js +30 -0
  116. repokit/templates/layers/archetypes/js-ts/node/.repokit-common.toml.rkappend +6 -0
  117. repokit/templates/layers/archetypes/js-ts/node/package.json +13 -0
  118. repokit/templates/layers/archetypes/js-ts/node-package/.github/workflows/release.yml +39 -0
  119. repokit/templates/layers/archetypes/js-ts/node-package/package.json +32 -0
  120. repokit/templates/layers/archetypes/js-ts/node-package/src/index.ts +13 -0
  121. repokit/templates/layers/archetypes/js-ts/node-package/tests/index.test.ts +11 -0
  122. repokit/templates/layers/archetypes/js-ts/script/.vscode/launch.json +34 -0
  123. repokit/templates/layers/archetypes/js-ts/script/__RK_PROJECT__.js +37 -0
  124. repokit/templates/layers/archetypes/js-ts/script/tests/version.test.js +26 -0
  125. repokit/templates/layers/archetypes/js-ts/ts/package.json +18 -0
  126. repokit/templates/layers/archetypes/js-ts/ts/tsconfig.json +15 -0
  127. repokit/templates/layers/archetypes/python/claude-plugin/.github/workflows/ci-python.yml +44 -0
  128. repokit/templates/layers/archetypes/python/claude-plugin/.repokit-common.toml +21 -0
  129. repokit/templates/layers/archetypes/python/claude-plugin/.vscode/launch.json +33 -0
  130. repokit/templates/layers/archetypes/python/claude-plugin/VERSION +1 -0
  131. repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/hooks.json +15 -0
  132. repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/scripts/run-hook.mjs +42 -0
  133. repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/scripts/session_start.py +31 -0
  134. repokit/templates/layers/archetypes/python/claude-plugin/plugin/tests/test_manifest.py +42 -0
  135. repokit/templates/layers/archetypes/python/pypi/.github/workflows/release.yml +63 -0
  136. repokit/templates/layers/archetypes/python/pypi/.vscode/launch.json +43 -0
  137. repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/__init__.py +5 -0
  138. repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/__main__.py +6 -0
  139. repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/_version.py +96 -0
  140. repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/cli.py +18 -0
  141. repokit/templates/layers/archetypes/python/pypi/pyproject.toml +71 -0
  142. repokit/templates/layers/archetypes/python/pypi/tests/conftest.py +11 -0
  143. repokit/templates/layers/archetypes/python/pypi/tests/test_version.py +50 -0
  144. repokit/templates/layers/archetypes/python/script/.github/workflows/ci-python.yml +43 -0
  145. repokit/templates/layers/archetypes/python/script/.repokit-common.toml +14 -0
  146. repokit/templates/layers/archetypes/python/script/.vscode/launch.json +33 -0
  147. repokit/templates/layers/archetypes/python/script/VERSION +1 -0
  148. repokit/templates/layers/archetypes/python/script/__RK_PROJECT__.py +40 -0
  149. repokit/templates/layers/archetypes/python/script/tests/test_version.py +25 -0
  150. repokit/templates/layers/archetypes/python/setuppy/requirements.txt +8 -0
  151. repokit/templates/layers/archetypes/python/setuppy/setup.py +23 -0
  152. repokit/templates/layers/languages/c-cpp/.gitignore.rkappend +20 -0
  153. repokit/templates/layers/languages/c-cpp/.repokit-common.toml +7 -0
  154. repokit/templates/layers/languages/c-cpp/include/__RK_SNAKE__/version.h +20 -0
  155. repokit/templates/layers/languages/c-cpp/src/app.rc +25 -0
  156. repokit/templates/layers/languages/js-ts/.editorconfig +15 -0
  157. repokit/templates/layers/languages/js-ts/.github/workflows/ci-js-ts.yml +41 -0
  158. repokit/templates/layers/languages/js-ts/.gitignore.rkappend +13 -0
  159. repokit/templates/layers/languages/js-ts/.repokit-common.toml +19 -0
  160. repokit/templates/layers/languages/js-ts/.vscode/launch.json +24 -0
  161. repokit/templates/layers/languages/js-ts/VERSION +1 -0
  162. repokit/templates/layers/languages/python/.github/workflows/ci-python.yml +76 -0
  163. repokit/templates/layers/languages/python/.gitignore.rkappend +15 -0
  164. repokit/templates/layers/languages/python/.vscode/launch.json +33 -0
  165. repokit/templates/layers/manifest.toml +302 -0
  166. repokit/templates/vault/CLAUDE.md +29 -0
  167. repokit/templates/vault/claude/_maps/MOC-project.md +15 -0
  168. repokit/templates/vault/claude/_oracle/backlinks.md +3 -0
  169. repokit/templates/vault/claude/_oracle/concepts.md +12 -0
  170. repokit/templates/vault/claude/_oracle/manifest.md +10 -0
  171. repokit/templates/vault/claude/_todo/ACTIVE-WORK.md +18 -0
  172. repokit/templates/vault/claude/_todo/OPEN-LOOPS.md +14 -0
  173. repokit/utils.py +316 -0
  174. repokit/vault.py +248 -0
@@ -0,0 +1,627 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Remote integration for RepoKit.
4
+
5
+ Handles creating and configuring remote repositories on GitHub, GitLab, etc.
6
+ """
7
+
8
+ import os
9
+ import logging
10
+ import json
11
+ import subprocess
12
+ import requests
13
+ from typing import Dict, Any, Optional, List, Union, Tuple
14
+
15
+ from .auth_integration import AuthenticationHandler
16
+ from .defaults import strategy_push_branches
17
+
18
+
19
+ class RemoteIntegration:
20
+ """
21
+ Manages integration with remote Git hosting services like GitHub, GitLab, etc.
22
+ """
23
+
24
+ def __init__(
25
+ self, repo_manager, credentials_file: Optional[str] = None, verbose: int = 0
26
+ ):
27
+ """
28
+ Initialize the remote integration manager.
29
+
30
+ Args:
31
+ repo_manager: Reference to the repository manager
32
+ credentials_file: Optional path to credentials file
33
+ verbose: Verbosity level (0=normal, 1=info, 2=debug)
34
+ """
35
+ self.repo_manager = repo_manager
36
+ self.verbose = verbose
37
+ self.logger = logging.getLogger("repokit.remote")
38
+
39
+ # Initialize authentication handler
40
+ self.auth_handler = AuthenticationHandler(verbose=verbose)
41
+ if credentials_file:
42
+ self.auth_handler.load_credentials(credentials_file)
43
+ else:
44
+ self.auth_handler.load_credentials()
45
+
46
+ def create_github_repository(
47
+ self,
48
+ repo_name: str,
49
+ description: str = "",
50
+ private: bool = False,
51
+ organization: Optional[str] = None,
52
+ token_command: Optional[str] = None,
53
+ ) -> Tuple[bool, str]:
54
+ """
55
+ Create a new repository on GitHub.
56
+
57
+ Args:
58
+ repo_name: Name of the repository
59
+ description: Repository description
60
+ private: Whether the repository should be private
61
+ organization: GitHub organization name (or None for personal account)
62
+ token_command: Command to retrieve token from password manager
63
+
64
+ Returns:
65
+ Tuple of (success, url or error message)
66
+ """
67
+ # Get token from auth handler
68
+ token = self.auth_handler.get_token("github", token_command)
69
+ if not token:
70
+ return (
71
+ False,
72
+ "GitHub token not found. See Authentication Guide for setup instructions.",
73
+ )
74
+
75
+ # Get organization from auth handler if not provided
76
+ if not organization:
77
+ organization = self.auth_handler.get_organization("github")
78
+
79
+ # Get API URL
80
+ api_url = self.auth_handler.get_api_url("github")
81
+
82
+ headers = {
83
+ "Authorization": f"token {token}",
84
+ "Accept": "application/vnd.github.v3+json",
85
+ }
86
+
87
+ default_branch = self.repo_manager.config.get("default_branch", "main")
88
+ data = {
89
+ "name": repo_name,
90
+ "description": description,
91
+ "private": private,
92
+ "auto_init": False, # Don't initialize with README
93
+ "default_branch": default_branch,
94
+ }
95
+
96
+ # API endpoint - either user repos or organization repos
97
+ if organization:
98
+ url = f"{api_url}/orgs/{organization}/repos"
99
+ else:
100
+ url = f"{api_url}/user/repos"
101
+
102
+ try:
103
+ response = requests.post(url, headers=headers, json=data)
104
+ response.raise_for_status()
105
+
106
+ repo_info = response.json()
107
+ repo_url = repo_info["html_url"]
108
+
109
+ self.logger.info(f"Created GitHub repository: {repo_url}")
110
+
111
+ # Set default branch to main if not already set
112
+ # This is necessary because GitHub doesn't let you set default branch at creation
113
+ # if the repo is empty
114
+ try:
115
+ # The repository's real owner (an organization, or the token's
116
+ # user); the setting lives on the repository itself
117
+ owner = repo_info.get("owner", {}).get("login") or organization
118
+ default_branch_url = f"{api_url}/repos/{owner}/{repo_name}"
119
+ default_branch_data = {"default_branch": "main"}
120
+ default_branch_response = requests.patch(
121
+ default_branch_url, headers=headers, json=default_branch_data
122
+ )
123
+ if default_branch_response.status_code == 200:
124
+ self.logger.info("Set default branch to 'main'")
125
+ except Exception as e:
126
+ self.logger.warning(f"Could not set default branch: {str(e)}")
127
+
128
+ return True, repo_url
129
+ except requests.RequestException as e:
130
+ error_msg = f"Failed to create GitHub repository: {str(e)}"
131
+ if hasattr(e, "response") and e.response:
132
+ try:
133
+ error_details = e.response.json()
134
+ error_msg += f" - {error_details.get('message', '')}"
135
+ except ValueError:
136
+ pass
137
+
138
+ self.logger.error(error_msg)
139
+ return False, error_msg
140
+
141
+ def create_gitlab_repository(
142
+ self,
143
+ repo_name: str,
144
+ description: str = "",
145
+ private: bool = False,
146
+ group: Optional[str] = None,
147
+ token_command: Optional[str] = None,
148
+ ) -> Tuple[bool, str]:
149
+ """
150
+ Create a new repository on GitLab.
151
+
152
+ Args:
153
+ repo_name: Name of the repository
154
+ description: Repository description
155
+ private: Whether the repository should be private
156
+ group: GitLab group path (or None for personal account)
157
+ token_command: Command to retrieve token from password manager
158
+
159
+ Returns:
160
+ Tuple of (success, url or error message)
161
+ """
162
+ # Get token from auth handler
163
+ token = self.auth_handler.get_token("gitlab", token_command)
164
+ if not token:
165
+ return (
166
+ False,
167
+ "GitLab token not found. See Authentication Guide for setup instructions.",
168
+ )
169
+
170
+ # Get group from auth handler if not provided
171
+ if not group:
172
+ group = self.auth_handler.get_organization("gitlab")
173
+
174
+ # Get API URL
175
+ api_url = self.auth_handler.get_api_url("gitlab")
176
+
177
+ headers = {"PRIVATE-TOKEN": token, "Content-Type": "application/json"}
178
+
179
+ # Convert private flag to GitLab's visibility level
180
+ visibility = "private" if private else "public"
181
+
182
+ data = {
183
+ "name": repo_name,
184
+ "description": description,
185
+ "visibility": visibility,
186
+ "initialize_with_readme": False,
187
+ }
188
+
189
+ # API endpoint - either user projects or group projects
190
+ if group:
191
+ data["namespace_id"] = group
192
+ url = f"{api_url}/projects"
193
+ else:
194
+ url = f"{api_url}/projects"
195
+
196
+ try:
197
+ response = requests.post(url, headers=headers, json=data)
198
+ response.raise_for_status()
199
+
200
+ repo_info = response.json()
201
+ repo_url = repo_info["web_url"]
202
+
203
+ self.logger.info(f"Created GitLab repository: {repo_url}")
204
+ return True, repo_url
205
+ except requests.RequestException as e:
206
+ error_msg = f"Failed to create GitLab repository: {str(e)}"
207
+ if hasattr(e, "response") and e.response:
208
+ try:
209
+ error_details = e.response.json()
210
+ error_msg += f" - {error_details.get('message', '')}"
211
+ except ValueError:
212
+ pass
213
+
214
+ self.logger.error(error_msg)
215
+ return False, error_msg
216
+
217
+ def add_remote(self, remote_url: str, remote_name: str = "origin") -> bool:
218
+ """
219
+ Add a remote to the local repository.
220
+
221
+ Args:
222
+ remote_url: URL of the remote repository
223
+ remote_name: Name of the remote (default: origin)
224
+
225
+ Returns:
226
+ True if successful, False otherwise
227
+ """
228
+ try:
229
+ # Check if remote already exists
230
+ existing_remote = self.repo_manager.run_git(
231
+ ["remote", "get-url", remote_name], check=False
232
+ )
233
+
234
+ if existing_remote:
235
+ # Remote exists, update URL
236
+ self.repo_manager.run_git(
237
+ ["remote", "set-url", remote_name, remote_url]
238
+ )
239
+ self.logger.info(f"Updated remote '{remote_name}' to {remote_url}")
240
+ else:
241
+ # Add new remote
242
+ self.repo_manager.run_git(["remote", "add", remote_name, remote_url])
243
+ self.logger.info(f"Added remote '{remote_name}' at {remote_url}")
244
+
245
+ return True
246
+ except Exception as e:
247
+ self.logger.error(f"Failed to add remote: {str(e)}")
248
+ return False
249
+
250
+ def push_to_remote(
251
+ self, branch_name: str, remote_name: str = "origin", set_upstream: bool = True
252
+ ) -> bool:
253
+ """
254
+ Push a branch to the remote repository.
255
+
256
+ Args:
257
+ branch_name: Name of the branch to push
258
+ remote_name: Name of the remote (default: origin)
259
+ set_upstream: Whether to set up branch tracking
260
+
261
+ Returns:
262
+ True if successful, False otherwise
263
+ """
264
+ try:
265
+ # First ensure we're on the correct branch or this will fail
266
+ current_branch = self.repo_manager.run_git(["branch", "--show-current"])
267
+ current_branch = current_branch.strip() if current_branch else ""
268
+
269
+ if current_branch != branch_name:
270
+ self.logger.warning(
271
+ f"Not on branch {branch_name}, currently on {current_branch}"
272
+ )
273
+ # Instead of checking out, use push with refspec
274
+ # Make sure there are no spaces in the branch name and that it's properly formatted
275
+ # Remove the "+" that was mistakenly being added before the branch name
276
+ branch_name = branch_name.strip().replace(" ", "_")
277
+ push_cmd = [
278
+ "push",
279
+ remote_name,
280
+ f"refs/heads/{branch_name}:refs/heads/{branch_name}",
281
+ ]
282
+ else:
283
+ # Standard push with optional upstream setting
284
+ push_cmd = ["push"]
285
+ if set_upstream:
286
+ push_cmd.append("--set-upstream")
287
+ push_cmd.extend([remote_name, branch_name])
288
+
289
+ self.repo_manager.run_git(push_cmd)
290
+ self.logger.info(f"Pushed branch '{branch_name}' to {remote_name}")
291
+ return True
292
+ except Exception as e:
293
+ self.logger.error(f"Failed to push branch '{branch_name}': {str(e)}")
294
+ return False
295
+
296
+ def push_all_branches(
297
+ self, exclude_branches: Optional[List[str]] = None, remote_name: str = "origin"
298
+ ) -> Tuple[List[str], List[str]]:
299
+ """
300
+ Push all branches to the remote repository, except those explicitly excluded.
301
+
302
+ Args:
303
+ exclude_branches: List of branch names to exclude (default: ["private"])
304
+ remote_name: Name of the remote (default: origin)
305
+
306
+ Returns:
307
+ Tuple of (successful_branches, failed_branches)
308
+ """
309
+ if exclude_branches is None:
310
+ exclude_branches = ["private"]
311
+
312
+ # Get all branches - use a better approach to avoid plus signs
313
+ try:
314
+ branches_output = self.repo_manager.run_git(["branch", "--list"])
315
+ branches = []
316
+
317
+ for branch in branches_output.split("\n"):
318
+ # Strip the leading "* " or " " and any "+" from each branch name
319
+ branch_name = branch.strip().lstrip("*+").strip()
320
+
321
+ # Make sure the branch name is valid and not empty
322
+ if branch_name and branch_name not in exclude_branches:
323
+ branches.append(branch_name)
324
+ except Exception as e:
325
+ self.logger.error(f"Failed to get branches: {str(e)}")
326
+ return [], []
327
+
328
+ # Prioritize main branch first for initial push (important for GitHub)
329
+ if "main" in branches:
330
+ branches.remove("main")
331
+ branches.insert(0, "main")
332
+
333
+ successful_branches = []
334
+ failed_branches = []
335
+
336
+ # Get the current branch
337
+ current_branch = self.repo_manager.run_git(["branch", "--show-current"])
338
+
339
+ for branch in branches:
340
+ try:
341
+ # For branches that are currently checked out in worktrees, we need a different approach
342
+ if self.is_branch_checked_out(branch) and branch != current_branch:
343
+ # For branches checked out in worktrees, we use a different approach:
344
+ # 1. First check if the remote exists and branch is tracked
345
+ remote_check = self.repo_manager.run_git(
346
+ ["config", "--get", f"branch.{branch}.remote"], check=False
347
+ )
348
+
349
+ if remote_check:
350
+ # Branch is tracked, push it
351
+ self.repo_manager.run_git(["push", remote_name, branch])
352
+ else:
353
+ # Branch is not tracked, set up tracking first
354
+ # This is trickier - we need to use the git command with the refspec
355
+ self.repo_manager.run_git(
356
+ [
357
+ "push",
358
+ "-u",
359
+ remote_name,
360
+ f"refs/heads/{branch}:refs/heads/{branch}",
361
+ ],
362
+ check=True,
363
+ )
364
+ else:
365
+ # Normal push for branches that aren't checked out in worktrees
366
+ # or if we're on this branch
367
+ if current_branch == branch:
368
+ # We're on this branch, regular push
369
+ self.repo_manager.run_git(["push", "-u", remote_name, branch])
370
+ else:
371
+ # We need to checkout the branch first
372
+ self.repo_manager.run_git(["checkout", branch])
373
+ self.repo_manager.run_git(["push", "-u", remote_name, branch])
374
+ # Go back to the original branch
375
+ self.repo_manager.run_git(["checkout", current_branch])
376
+
377
+ self.logger.info(f"Pushed branch '{branch}' to {remote_name}")
378
+ successful_branches.append(branch)
379
+ except Exception as e:
380
+ self.logger.error(f"Failed to push branch '{branch}': {str(e)}")
381
+ failed_branches.append(branch)
382
+
383
+ if successful_branches:
384
+ self.logger.info(
385
+ f"Successfully pushed branches: {', '.join(successful_branches)}"
386
+ )
387
+
388
+ if failed_branches:
389
+ self.logger.warning(
390
+ f"Failed to push branches: {', '.join(failed_branches)}"
391
+ )
392
+
393
+ return successful_branches, failed_branches
394
+
395
+ def push_allowed_branches(
396
+ self, allowed_branches: List[str], exclude_branches: Optional[List[str]] = None, remote_name: str = "origin"
397
+ ) -> Tuple[List[str], List[str]]:
398
+ """
399
+ Push only the allowed branches to the remote repository.
400
+
401
+ Args:
402
+ allowed_branches: List of branch names that are allowed to be pushed
403
+ exclude_branches: List of branch names to exclude (additional safety check)
404
+ remote_name: Name of the remote (default: origin)
405
+
406
+ Returns:
407
+ Tuple of (successful_branches, failed_branches)
408
+ """
409
+ if exclude_branches is None:
410
+ exclude_branches = ["private"]
411
+
412
+ # Get all existing local branches
413
+ try:
414
+ branches_output = self.repo_manager.run_git(["branch", "--list"])
415
+ existing_branches = []
416
+
417
+ for branch in branches_output.split("\n"):
418
+ # Strip the leading "* " or " " and any "+" from each branch name
419
+ branch_name = branch.strip().lstrip("*+").strip()
420
+ if branch_name:
421
+ existing_branches.append(branch_name)
422
+ except Exception as e:
423
+ self.logger.error(f"Failed to get branches: {str(e)}")
424
+ return [], []
425
+
426
+ # Only push branches that exist locally AND are in the allowed list AND not excluded
427
+ branches_to_push = []
428
+ for branch in allowed_branches:
429
+ if branch in existing_branches and branch not in exclude_branches:
430
+ branches_to_push.append(branch)
431
+ elif branch not in existing_branches:
432
+ self.logger.info(f"Branch '{branch}' not found locally, skipping")
433
+
434
+ self.logger.info(f"Pushing allowed branches: {branches_to_push}")
435
+
436
+ # Prioritize main branch first for initial push (important for GitHub)
437
+ if "main" in branches_to_push:
438
+ branches_to_push.remove("main")
439
+ branches_to_push.insert(0, "main")
440
+
441
+ successful_branches = []
442
+ failed_branches = []
443
+
444
+ # Get the current branch
445
+ current_branch = self.repo_manager.run_git(["branch", "--show-current"])
446
+
447
+ for branch in branches_to_push:
448
+ try:
449
+ # For branches that are currently checked out in worktrees, we need a different approach
450
+ if self.is_branch_checked_out(branch) and branch != current_branch:
451
+ # Use refspec to push without checkout
452
+ self.repo_manager.run_git(
453
+ [
454
+ "push",
455
+ "-u",
456
+ remote_name,
457
+ f"refs/heads/{branch}:refs/heads/{branch}",
458
+ ],
459
+ check=True,
460
+ )
461
+ else:
462
+ # Normal push for branches that aren't checked out in worktrees
463
+ if current_branch == branch:
464
+ # We're on this branch, regular push
465
+ self.repo_manager.run_git(["push", "-u", remote_name, branch])
466
+ else:
467
+ # We need to checkout the branch first
468
+ self.repo_manager.run_git(["checkout", branch])
469
+ self.repo_manager.run_git(["push", "-u", remote_name, branch])
470
+ # Go back to the original branch
471
+ self.repo_manager.run_git(["checkout", current_branch])
472
+
473
+ self.logger.info(f"Pushed branch '{branch}' to {remote_name}")
474
+ successful_branches.append(branch)
475
+ except Exception as e:
476
+ self.logger.error(f"Failed to push branch '{branch}': {str(e)}")
477
+ failed_branches.append(branch)
478
+
479
+ if successful_branches:
480
+ self.logger.info(
481
+ f"Successfully pushed branches: {', '.join(successful_branches)}"
482
+ )
483
+
484
+ if failed_branches:
485
+ self.logger.warning(
486
+ f"Failed to push branches: {', '.join(failed_branches)}"
487
+ )
488
+
489
+ return successful_branches, failed_branches
490
+
491
+ def is_branch_checked_out(self, branch: str) -> bool:
492
+ """
493
+ Check if a branch is currently checked out in a worktree.
494
+
495
+ Args:
496
+ branch: Branch name to check
497
+
498
+ Returns:
499
+ True if the branch is checked out, False otherwise
500
+ """
501
+ try:
502
+ # Get all worktrees
503
+ worktree_output = self.repo_manager.run_git(
504
+ ["worktree", "list"], check=False
505
+ )
506
+
507
+ # Check if the branch appears as checked out in any worktree
508
+ for line in worktree_output.split("\n"):
509
+ if branch in line and "[" in line and "]" in line:
510
+ # Format is typically: "/path/to/worktree abcd1234 [branch]"
511
+ return True
512
+
513
+ return False
514
+ except Exception:
515
+ # If we can't determine, assume it's not checked out to be safe
516
+ return False
517
+
518
+ def setup_remote_repository(
519
+ self,
520
+ service: str = "github",
521
+ remote_name: str = "origin",
522
+ private: bool = False,
523
+ push_branches: bool = True,
524
+ organization: Optional[str] = None,
525
+ token_command: Optional[str] = None,
526
+ include_branches: Optional[List[str]] = None,
527
+ exclude_branches: Optional[List[str]] = None,
528
+ ) -> bool:
529
+ """
530
+ Create a remote repository and push local branches.
531
+
532
+ Args:
533
+ service: Remote service ("github" or "gitlab")
534
+ remote_name: Name to use for the remote (default: origin)
535
+ private: Whether the repository should be private
536
+ push_branches: Whether to push branches after setup
537
+ organization: Organization or group name (optional)
538
+ token_command: Command to retrieve token (optional)
539
+ include_branches: Specific branches to push (overrides strategy defaults)
540
+ exclude_branches: Additional branches to exclude (beyond private)
541
+
542
+ Returns:
543
+ True if successful, False otherwise
544
+ """
545
+ repo_name = self.repo_manager.project_name
546
+ description = self.repo_manager.config.get("description", "")
547
+
548
+ # Create the remote repository
549
+ if service.lower() == "github":
550
+ success, result = self.create_github_repository(
551
+ repo_name, description, private, organization, token_command
552
+ )
553
+ elif service.lower() == "gitlab":
554
+ success, result = self.create_gitlab_repository(
555
+ repo_name, description, private, organization, token_command
556
+ )
557
+ else:
558
+ success = False
559
+ result = f"Unsupported service: {service}"
560
+
561
+ if not success:
562
+ self.logger.error(f"Failed to create remote repository: {result}")
563
+ return False
564
+
565
+ # Add the remote
566
+ remote_url = result
567
+ if not self.add_remote(remote_url, remote_name):
568
+ return False
569
+
570
+ # Push branches if requested
571
+ if push_branches:
572
+ # Determine which branches to push
573
+ branch_strategy = self.repo_manager.config.get("branch_strategy", "simple")
574
+ strategy_push = strategy_push_branches(branch_strategy)
575
+
576
+ if include_branches:
577
+ # User explicitly specified branches to include
578
+ allowed_branches = include_branches
579
+ self.logger.info(f"Using user-specified branches to push: {allowed_branches}")
580
+ else:
581
+ allowed_branches = strategy_push or ["main", "dev"]
582
+ self.logger.info(f"Using {branch_strategy} strategy branches: {allowed_branches}")
583
+
584
+ # Build exclude list: the private branch + user-specified exclusions.
585
+ # A strategy whose "private" branch is one of its public branches
586
+ # (minimal: main) still pushes that branch.
587
+ private_branch = self.repo_manager.config.get("private_branch", "private")
588
+ exclude_list = []
589
+ if private_branch and private_branch not in strategy_push:
590
+ exclude_list.append(private_branch)
591
+ if exclude_branches:
592
+ exclude_list.extend(exclude_branches)
593
+ self.logger.info(f"Additional excluded branches: {exclude_branches}")
594
+
595
+ successful, failed = self.push_allowed_branches(allowed_branches, exclude_list, remote_name)
596
+
597
+ if failed:
598
+ self.logger.warning(
599
+ f"Some branches could not be pushed: {', '.join(failed)}"
600
+ )
601
+ return False
602
+
603
+ self.logger.info(f"Successfully set up remote repository at {remote_url}")
604
+ return True
605
+
606
+ def store_credentials(
607
+ self,
608
+ service: str,
609
+ token: str,
610
+ organization: Optional[str] = None,
611
+ credentials_file: Optional[str] = None,
612
+ ) -> bool:
613
+ """
614
+ Store credentials for a service.
615
+
616
+ Args:
617
+ service: Service name ('github' or 'gitlab')
618
+ token: Authentication token
619
+ organization: Organization or group name (optional)
620
+ credentials_file: Path to credentials file (optional)
621
+
622
+ Returns:
623
+ True if stored successfully, False otherwise
624
+ """
625
+ return self.auth_handler.store_credentials(
626
+ service, token, organization, credentials_file
627
+ )