git-repokit 0.9.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- git_repokit-0.9.10.dist-info/METADATA +422 -0
- git_repokit-0.9.10.dist-info/RECORD +174 -0
- git_repokit-0.9.10.dist-info/WHEEL +5 -0
- git_repokit-0.9.10.dist-info/entry_points.txt +2 -0
- git_repokit-0.9.10.dist-info/licenses/LICENSE +9 -0
- git_repokit-0.9.10.dist-info/top_level.txt +1 -0
- repokit/__init__.py +8 -0
- repokit/__main__.py +12 -0
- repokit/__version__.py +4 -0
- repokit/_vendor/__init__.py +0 -0
- repokit/_vendor/ailib/__init__.py +1 -0
- repokit/_vendor/ailib/analyzer.py +177 -0
- repokit/_vendor/ailib/backend.py +95 -0
- repokit/_vendor/ailib/cache.py +97 -0
- repokit/_vendor/ailib/parsers.py +52 -0
- repokit/_vendor/ailib/transports/__init__.py +16 -0
- repokit/_vendor/ailib/transports/cli.py +312 -0
- repokit/_vendor/ailib/transports/openai_compat.py +383 -0
- repokit/_vendor/ailib/transports/prompt_file.py +41 -0
- repokit/_vendor/ailib/types.py +224 -0
- repokit/auth_integration.py +244 -0
- repokit/badges.py +183 -0
- repokit/branch_utils.py +364 -0
- repokit/cli.py +2997 -0
- repokit/common_kit.py +202 -0
- repokit/config.py +686 -0
- repokit/defaults.py +220 -0
- repokit/directory_analyzer.py +1244 -0
- repokit/directory_profiles.py +423 -0
- repokit/git_config.py +370 -0
- repokit/github_create.py +142 -0
- repokit/github_setup.py +248 -0
- repokit/guardrails.py +707 -0
- repokit/history_cleaner.py +449 -0
- repokit/history_protection.py +434 -0
- repokit/hooks/pre-commit-guardrails +122 -0
- repokit/hooks/pre-commit-template +28 -0
- repokit/layers.py +510 -0
- repokit/licenses/0bsd.txt +14 -0
- repokit/licenses/LICENSES.md +27 -0
- repokit/licenses/__init__.py +142 -0
- repokit/licenses/agpl-3.0.txt +661 -0
- repokit/licenses/all-rights-reserved.txt +10 -0
- repokit/licenses/apache-2.0.txt +201 -0
- repokit/licenses/bsd-2-clause.txt +24 -0
- repokit/licenses/bsd-3-clause.txt +28 -0
- repokit/licenses/bsl-1.0.txt +23 -0
- repokit/licenses/cc-by-4.0.txt +395 -0
- repokit/licenses/cc-by-sa-4.0.txt +427 -0
- repokit/licenses/cc0-1.0.txt +121 -0
- repokit/licenses/epl-2.0.txt +277 -0
- repokit/licenses/gpl-2.0.txt +339 -0
- repokit/licenses/gpl-3.0.txt +674 -0
- repokit/licenses/isc.txt +15 -0
- repokit/licenses/lgpl-2.1.txt +504 -0
- repokit/licenses/lgpl-3.0.txt +165 -0
- repokit/licenses/mit.txt +21 -0
- repokit/licenses/mpl-2.0.txt +373 -0
- repokit/licenses/unlicense.txt +24 -0
- repokit/readme_blocks.py +112 -0
- repokit/remote_integration.py +627 -0
- repokit/repo_manager.py +2796 -0
- repokit/template_engine.py +391 -0
- repokit/templates/ai/claude/CLAUDE-nested.md.template +25 -0
- repokit/templates/ai/claude/CLAUDE.md.template +98 -0
- repokit/templates/ai/claude/instructions/step1_context_rebuilder.md +33 -0
- repokit/templates/ai/claude/instructions/step2_dev_workflow_process.md +67 -0
- repokit/templates/ai/claude/instructions/step3_context_bridge.md +52 -0
- repokit/templates/common/CHANGELOG.md.template +15 -0
- repokit/templates/common/CONTRIBUTING.md.template +31 -0
- repokit/templates/common/README.md.template +30 -0
- repokit/templates/common/gitattributes.template +17 -0
- repokit/templates/common/gitignore.template +141 -0
- repokit/templates/extras/FUNDING.yml.template +13 -0
- repokit/templates/extras/ROADMAP.md.template +17 -0
- repokit/templates/extras/dependabot.yml.template +22 -0
- repokit/templates/extras/one-offs-README.md.template +28 -0
- repokit/templates/extras/platform-support.md.template +11 -0
- repokit/templates/extras/pull_request_template.md.template +25 -0
- repokit/templates/extras/release-template.md.template +34 -0
- repokit/templates/extras/settings.json.template +11 -0
- repokit/templates/extras/stale.yml.template +14 -0
- repokit/templates/github/CODEOWNERS.template +11 -0
- repokit/templates/github/ISSUE_TEMPLATE/bug-report.md.template +29 -0
- repokit/templates/github/ISSUE_TEMPLATE/feature-request.md.template +19 -0
- repokit/templates/github/issues/notes.md.template +18 -0
- repokit/templates/github/issues/roadmap.md.template +15 -0
- repokit/templates/hooks/pre-commit.template +64 -0
- repokit/templates/layers/archetypes/any/claude-plugin/.claude-plugin/marketplace.json +20 -0
- repokit/templates/layers/archetypes/any/claude-plugin/plugin/.claude-plugin/plugin.json +15 -0
- repokit/templates/layers/archetypes/any/claude-plugin/plugin/README.md +28 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/.github/workflows/ci-c-cpp.yml +24 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/.repokit-common.toml.rkappend +4 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/.vscode/launch.json +29 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/.vscode/tasks.json +26 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/CMakeLists.txt +47 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/CMakePresets.json +23 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/cmake/GenerateStamp.cmake +45 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/src/main.cpp +27 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/tests/test_version.cpp +20 -0
- repokit/templates/layers/archetypes/c-cpp/cmake-app/vcpkg.json +5 -0
- repokit/templates/layers/archetypes/c-cpp/minimal/.vscode/launch.json +27 -0
- repokit/templates/layers/archetypes/c-cpp/minimal/Makefile +10 -0
- repokit/templates/layers/archetypes/c-cpp/minimal/build.bat +7 -0
- repokit/templates/layers/archetypes/c-cpp/minimal/src/__RK_SNAKE__.c +26 -0
- repokit/templates/layers/archetypes/c-cpp/msbuild/.vscode/launch.json +17 -0
- repokit/templates/layers/archetypes/c-cpp/msbuild/.vscode/tasks.json +20 -0
- repokit/templates/layers/archetypes/c-cpp/msbuild/__RK_PROJECT__.sln +19 -0
- repokit/templates/layers/archetypes/c-cpp/msbuild/__RK_PROJECT__.vcxproj +41 -0
- repokit/templates/layers/archetypes/c-cpp/msbuild/src/main.cpp +26 -0
- repokit/templates/layers/archetypes/js-ts/claude-plugin/.repokit-common.toml +28 -0
- repokit/templates/layers/archetypes/js-ts/claude-plugin/.vscode/launch.json +34 -0
- repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/hooks/hooks.json +15 -0
- repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/hooks/scripts/session-start.mjs +23 -0
- repokit/templates/layers/archetypes/js-ts/claude-plugin/plugin/tests/manifest.test.js +30 -0
- repokit/templates/layers/archetypes/js-ts/node/.repokit-common.toml.rkappend +6 -0
- repokit/templates/layers/archetypes/js-ts/node/package.json +13 -0
- repokit/templates/layers/archetypes/js-ts/node-package/.github/workflows/release.yml +39 -0
- repokit/templates/layers/archetypes/js-ts/node-package/package.json +32 -0
- repokit/templates/layers/archetypes/js-ts/node-package/src/index.ts +13 -0
- repokit/templates/layers/archetypes/js-ts/node-package/tests/index.test.ts +11 -0
- repokit/templates/layers/archetypes/js-ts/script/.vscode/launch.json +34 -0
- repokit/templates/layers/archetypes/js-ts/script/__RK_PROJECT__.js +37 -0
- repokit/templates/layers/archetypes/js-ts/script/tests/version.test.js +26 -0
- repokit/templates/layers/archetypes/js-ts/ts/package.json +18 -0
- repokit/templates/layers/archetypes/js-ts/ts/tsconfig.json +15 -0
- repokit/templates/layers/archetypes/python/claude-plugin/.github/workflows/ci-python.yml +44 -0
- repokit/templates/layers/archetypes/python/claude-plugin/.repokit-common.toml +21 -0
- repokit/templates/layers/archetypes/python/claude-plugin/.vscode/launch.json +33 -0
- repokit/templates/layers/archetypes/python/claude-plugin/VERSION +1 -0
- repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/hooks.json +15 -0
- repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/scripts/run-hook.mjs +42 -0
- repokit/templates/layers/archetypes/python/claude-plugin/plugin/hooks/scripts/session_start.py +31 -0
- repokit/templates/layers/archetypes/python/claude-plugin/plugin/tests/test_manifest.py +42 -0
- repokit/templates/layers/archetypes/python/pypi/.github/workflows/release.yml +63 -0
- repokit/templates/layers/archetypes/python/pypi/.vscode/launch.json +43 -0
- repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/__init__.py +5 -0
- repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/__main__.py +6 -0
- repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/_version.py +96 -0
- repokit/templates/layers/archetypes/python/pypi/__RK_SNAKE__/cli.py +18 -0
- repokit/templates/layers/archetypes/python/pypi/pyproject.toml +71 -0
- repokit/templates/layers/archetypes/python/pypi/tests/conftest.py +11 -0
- repokit/templates/layers/archetypes/python/pypi/tests/test_version.py +50 -0
- repokit/templates/layers/archetypes/python/script/.github/workflows/ci-python.yml +43 -0
- repokit/templates/layers/archetypes/python/script/.repokit-common.toml +14 -0
- repokit/templates/layers/archetypes/python/script/.vscode/launch.json +33 -0
- repokit/templates/layers/archetypes/python/script/VERSION +1 -0
- repokit/templates/layers/archetypes/python/script/__RK_PROJECT__.py +40 -0
- repokit/templates/layers/archetypes/python/script/tests/test_version.py +25 -0
- repokit/templates/layers/archetypes/python/setuppy/requirements.txt +8 -0
- repokit/templates/layers/archetypes/python/setuppy/setup.py +23 -0
- repokit/templates/layers/languages/c-cpp/.gitignore.rkappend +20 -0
- repokit/templates/layers/languages/c-cpp/.repokit-common.toml +7 -0
- repokit/templates/layers/languages/c-cpp/include/__RK_SNAKE__/version.h +20 -0
- repokit/templates/layers/languages/c-cpp/src/app.rc +25 -0
- repokit/templates/layers/languages/js-ts/.editorconfig +15 -0
- repokit/templates/layers/languages/js-ts/.github/workflows/ci-js-ts.yml +41 -0
- repokit/templates/layers/languages/js-ts/.gitignore.rkappend +13 -0
- repokit/templates/layers/languages/js-ts/.repokit-common.toml +19 -0
- repokit/templates/layers/languages/js-ts/.vscode/launch.json +24 -0
- repokit/templates/layers/languages/js-ts/VERSION +1 -0
- repokit/templates/layers/languages/python/.github/workflows/ci-python.yml +76 -0
- repokit/templates/layers/languages/python/.gitignore.rkappend +15 -0
- repokit/templates/layers/languages/python/.vscode/launch.json +33 -0
- repokit/templates/layers/manifest.toml +302 -0
- repokit/templates/vault/CLAUDE.md +29 -0
- repokit/templates/vault/claude/_maps/MOC-project.md +15 -0
- repokit/templates/vault/claude/_oracle/backlinks.md +3 -0
- repokit/templates/vault/claude/_oracle/concepts.md +12 -0
- repokit/templates/vault/claude/_oracle/manifest.md +10 -0
- repokit/templates/vault/claude/_todo/ACTIVE-WORK.md +18 -0
- repokit/templates/vault/claude/_todo/OPEN-LOOPS.md +14 -0
- repokit/utils.py +316 -0
- repokit/vault.py +248 -0
|
@@ -0,0 +1,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
|
+
)
|