thestage 0.6.2__py3-none-any.whl → 0.6.4__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.
- thestage/.env +4 -5
- thestage/__init__.py +3 -3
- thestage/__main__.py +9 -9
- thestage/cli_command.py +56 -56
- thestage/cli_command_helper.py +51 -51
- thestage/color_scheme/color_scheme.py +7 -7
- thestage/config/__init__.py +18 -18
- thestage/config/config_storage.py +5 -5
- thestage/config/env_base.py +7 -7
- thestage/controllers/__init__.py +0 -0
- thestage/controllers/base_controller.py +67 -67
- thestage/controllers/config_controller.py +137 -137
- thestage/controllers/container_controller.py +389 -389
- thestage/controllers/instance_controller.py +183 -183
- thestage/controllers/project_controller.py +810 -810
- thestage/controllers/utils_controller.py +32 -32
- thestage/debug_main.dist.py +28 -28
- thestage/entities/__init__.py +0 -0
- thestage/entities/container.py +17 -17
- thestage/entities/enums/__init__.py +0 -0
- thestage/entities/enums/order_direction_type.py +6 -6
- thestage/entities/enums/shell_type.py +7 -7
- thestage/entities/enums/tail_output_type.py +6 -6
- thestage/entities/enums/yes_no_response.py +7 -7
- thestage/entities/file_item.py +27 -27
- thestage/entities/project_inference_simulator.py +18 -18
- thestage/entities/project_inference_simulator_model.py +17 -17
- thestage/entities/project_task.py +19 -19
- thestage/entities/rented_instance.py +19 -19
- thestage/entities/self_hosted_instance.py +18 -18
- thestage/exceptions/__init__.py +0 -0
- thestage/exceptions/auth_exception.py +6 -6
- thestage/exceptions/base_exception.py +13 -13
- thestage/exceptions/business_logic_exception.py +6 -6
- thestage/exceptions/config_exception.py +6 -6
- thestage/exceptions/file_system_exception.py +6 -6
- thestage/exceptions/git_access_exception.py +17 -17
- thestage/exceptions/remote_server_exception.py +24 -24
- thestage/git/ProgressPrinter.py +22 -22
- thestage/helpers/__init__.py +0 -0
- thestage/helpers/error_handler.py +115 -115
- thestage/helpers/exception_hook.py +14 -14
- thestage/helpers/logger/__init__.py +0 -0
- thestage/helpers/logger/app_logger.py +50 -50
- thestage/helpers/ssh_util.py +38 -38
- thestage/i18n/en_GB/messages.po +947 -947
- thestage/i18n/translation.py +9 -9
- thestage/main.py +36 -36
- thestage/services/.env +6 -6
- thestage/services/__init__.py +0 -0
- thestage/services/abstract_mapper.py +9 -9
- thestage/services/abstract_service.py +87 -87
- thestage/services/app_config_service.py +52 -52
- thestage/services/clients/__init__.py +0 -0
- thestage/services/clients/git/__init__.py +0 -0
- thestage/services/clients/git/git_client.py +436 -436
- thestage/services/clients/thestage_api/__init__.py +0 -0
- thestage/services/clients/thestage_api/api_client.py +718 -718
- thestage/services/clients/thestage_api/core/api_client_core.py +108 -108
- thestage/services/clients/thestage_api/core/http_client_exception.py +12 -12
- thestage/services/clients/thestage_api/dtos/__init__.py +0 -0
- thestage/services/clients/thestage_api/dtos/base_response.py +13 -13
- thestage/services/clients/thestage_api/dtos/cloud_provider_region.py +19 -19
- thestage/services/clients/thestage_api/dtos/container_param_request.py +11 -11
- thestage/services/clients/thestage_api/dtos/container_response.py +67 -67
- thestage/services/clients/thestage_api/dtos/docker_container_assigned_device.py +10 -10
- thestage/services/clients/thestage_api/dtos/docker_container_controller/docker_container_list_request.py +13 -13
- thestage/services/clients/thestage_api/dtos/docker_container_controller/docker_container_list_response.py +13 -13
- thestage/services/clients/thestage_api/dtos/docker_container_mapping.py +10 -10
- thestage/services/clients/thestage_api/dtos/entity_filter_request.py +14 -14
- thestage/services/clients/thestage_api/dtos/enums/__init__.py +0 -0
- thestage/services/clients/thestage_api/dtos/enums/container_pending_action.py +10 -10
- thestage/services/clients/thestage_api/dtos/enums/container_status.py +17 -17
- thestage/services/clients/thestage_api/dtos/enums/cpu_type.py +8 -8
- thestage/services/clients/thestage_api/dtos/enums/currency_type.py +10 -10
- thestage/services/clients/thestage_api/dtos/enums/daemon_status.py +9 -9
- thestage/services/clients/thestage_api/dtos/enums/disk_type.py +7 -7
- thestage/services/clients/thestage_api/dtos/enums/drive_type.py +7 -7
- thestage/services/clients/thestage_api/dtos/enums/gpu_name.py +8 -8
- thestage/services/clients/thestage_api/dtos/enums/inference_model_status.py +9 -9
- thestage/services/clients/thestage_api/dtos/enums/inference_simulator_status.py +15 -15
- thestage/services/clients/thestage_api/dtos/enums/instance_rented_status.py +17 -17
- thestage/services/clients/thestage_api/dtos/enums/instance_type.py +7 -7
- thestage/services/clients/thestage_api/dtos/enums/location_region.py +11 -11
- thestage/services/clients/thestage_api/dtos/enums/power_status.py +10 -10
- thestage/services/clients/thestage_api/dtos/enums/provider_name.py +11 -11
- thestage/services/clients/thestage_api/dtos/enums/selfhosted_status.py +10 -10
- thestage/services/clients/thestage_api/dtos/enums/task_execution_status.py +12 -12
- thestage/services/clients/thestage_api/dtos/enums/task_status.py +12 -12
- thestage/services/clients/thestage_api/dtos/frontend_status.py +10 -10
- thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_instance_request.py +13 -13
- thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_instance_response.py +13 -13
- thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_sagemaker_request.py +12 -12
- thestage/services/clients/thestage_api/dtos/inference_controller/deploy_inference_model_to_sagemaker_response.py +12 -12
- thestage/services/clients/thestage_api/dtos/inference_controller/get_inference_simulator_request.py +10 -10
- thestage/services/clients/thestage_api/dtos/inference_controller/get_inference_simulator_response.py +13 -13
- thestage/services/clients/thestage_api/dtos/inference_controller/inference_simulator_list_for_project_request.py +14 -14
- thestage/services/clients/thestage_api/dtos/inference_controller/inference_simulator_list_for_project_response.py +12 -12
- thestage/services/clients/thestage_api/dtos/inference_controller/inference_simulator_model_list_for_project_request.py +12 -12
- thestage/services/clients/thestage_api/dtos/inference_controller/inference_simulator_model_list_for_project_response.py +13 -13
- thestage/services/clients/thestage_api/dtos/inference_simulator_model_response.py +11 -11
- thestage/services/clients/thestage_api/dtos/inference_simulator_response.py +11 -11
- thestage/services/clients/thestage_api/dtos/installed_service.py +17 -17
- thestage/services/clients/thestage_api/dtos/instance_detected_gpus.py +20 -20
- thestage/services/clients/thestage_api/dtos/instance_rented_response.py +71 -71
- thestage/services/clients/thestage_api/dtos/logging_controller/docker_container_log_stream_request.py +7 -7
- thestage/services/clients/thestage_api/dtos/logging_controller/log_polling_request.py +13 -13
- thestage/services/clients/thestage_api/dtos/logging_controller/log_polling_response.py +14 -14
- thestage/services/clients/thestage_api/dtos/logging_controller/task_log_stream_request.py +7 -7
- thestage/services/clients/thestage_api/dtos/logging_controller/user_logs_query_request.py +21 -21
- thestage/services/clients/thestage_api/dtos/logging_controller/user_logs_query_response.py +14 -14
- thestage/services/clients/thestage_api/dtos/paginated_entity_list.py +11 -11
- thestage/services/clients/thestage_api/dtos/pagination_data.py +10 -10
- thestage/services/clients/thestage_api/dtos/price_definition.py +14 -14
- thestage/services/clients/thestage_api/dtos/project_controller/project_get_deploy_ssh_key_request.py +7 -7
- thestage/services/clients/thestage_api/dtos/project_controller/project_get_deploy_ssh_key_response.py +10 -10
- thestage/services/clients/thestage_api/dtos/project_controller/project_push_inference_simulator_model_request.py +8 -8
- thestage/services/clients/thestage_api/dtos/project_controller/project_push_inference_simulator_model_response.py +6 -6
- thestage/services/clients/thestage_api/dtos/project_controller/project_run_task_request.py +15 -15
- thestage/services/clients/thestage_api/dtos/project_controller/project_run_task_response.py +10 -10
- thestage/services/clients/thestage_api/dtos/project_controller/project_start_inference_simulator_request.py +13 -13
- thestage/services/clients/thestage_api/dtos/project_controller/project_start_inference_simulator_response.py +10 -10
- thestage/services/clients/thestage_api/dtos/project_response.py +32 -32
- thestage/services/clients/thestage_api/dtos/selfhosted_instance_response.py +56 -56
- thestage/services/clients/thestage_api/dtos/sftp_path_helper.py +13 -13
- thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_key_to_user_request.py +8 -8
- thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_key_to_user_response.py +11 -11
- thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_public_key_to_instance_request.py +8 -8
- thestage/services/clients/thestage_api/dtos/ssh_key_controller/add_ssh_public_key_to_instance_response.py +11 -11
- thestage/services/clients/thestage_api/dtos/ssh_key_controller/is_user_has_public_ssh_key_request.py +7 -7
- thestage/services/clients/thestage_api/dtos/ssh_key_controller/is_user_has_public_ssh_key_response.py +12 -12
- thestage/services/clients/thestage_api/dtos/task_controller/task_list_for_project_request.py +10 -10
- thestage/services/clients/thestage_api/dtos/task_controller/task_list_for_project_response.py +12 -12
- thestage/services/clients/thestage_api/dtos/task_controller/task_status_localized_map_response.py +9 -9
- thestage/services/clients/thestage_api/dtos/task_controller/task_view_response.py +12 -12
- thestage/services/clients/thestage_api/dtos/user_controller/user_profile.py +12 -12
- thestage/services/clients/thestage_api/dtos/validate_token_response.py +11 -11
- thestage/services/config_provider/__init__.py +0 -0
- thestage/services/config_provider/config_provider.py +237 -237
- thestage/services/connect/connect_service.py +193 -196
- thestage/services/connect/dto/remote_server_config.py +9 -9
- thestage/services/container/__init__.py +0 -0
- thestage/services/container/container_service.py +374 -374
- thestage/services/container/mapper/__init__.py +0 -0
- thestage/services/container/mapper/container_mapper.py +30 -30
- thestage/services/core_files/config_entity.py +26 -26
- thestage/services/filesystem_service.py +133 -133
- thestage/services/instance/__init__.py +0 -0
- thestage/services/instance/instance_service.py +303 -303
- thestage/services/instance/mapper/__init__.py +0 -0
- thestage/services/instance/mapper/instance_mapper.py +24 -24
- thestage/services/instance/mapper/selfhosted_mapper.py +33 -33
- thestage/services/logging/byte_print_style.py +5 -5
- thestage/services/logging/dto/log_message.py +15 -15
- thestage/services/logging/dto/log_type.py +6 -6
- thestage/services/logging/exception/log_polling_exception.py +6 -6
- thestage/services/logging/logging_constants.py +3 -3
- thestage/services/logging/logging_service.py +367 -367
- thestage/services/project/__init__.py +0 -0
- thestage/services/project/dto/inference_simulator_dto.py +22 -22
- thestage/services/project/dto/inference_simulator_model_dto.py +20 -20
- thestage/services/project/dto/project_config.py +14 -14
- thestage/services/project/mapper/__init__.py +0 -0
- thestage/services/project/mapper/project_inference_simulator_mapper.py +21 -21
- thestage/services/project/mapper/project_inference_simulator_model_mapper.py +21 -21
- thestage/services/project/mapper/project_task_mapper.py +22 -22
- thestage/services/project/project_service.py +1253 -1253
- thestage/services/remote_server_service.py +609 -609
- thestage/services/service_factory.py +97 -97
- thestage/services/task/dto/task_dto.py +40 -40
- thestage/services/validation_service.py +61 -61
- {thestage-0.6.2.dist-info → thestage-0.6.4.dist-info}/LICENSE.txt +12 -12
- {thestage-0.6.2.dist-info → thestage-0.6.4.dist-info}/METADATA +3 -2
- thestage-0.6.4.dist-info/RECORD +176 -0
- {thestage-0.6.2.dist-info → thestage-0.6.4.dist-info}/WHEEL +1 -1
- thestage-0.6.2.dist-info/RECORD +0 -176
- {thestage-0.6.2.dist-info → thestage-0.6.4.dist-info}/entry_points.txt +0 -0
|
@@ -1,436 +1,436 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from typing import Optional, List
|
|
7
|
-
|
|
8
|
-
import git
|
|
9
|
-
import typer
|
|
10
|
-
from git import Remote, Repo, GitCommandError, Commit
|
|
11
|
-
from gitdb.exc import BadName
|
|
12
|
-
from rich import print
|
|
13
|
-
|
|
14
|
-
from thestage.color_scheme.color_scheme import ColorScheme
|
|
15
|
-
from thestage.config import THESTAGE_CONFIG_DIR
|
|
16
|
-
from thestage.exceptions.git_access_exception import GitAccessException
|
|
17
|
-
from thestage.git.ProgressPrinter import ProgressPrinter
|
|
18
|
-
from thestage.services.filesystem_service import FileSystemService
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class GitLocalClient:
|
|
22
|
-
__base_name_remote: str = 'origin'
|
|
23
|
-
__base_name_local: str = 'main'
|
|
24
|
-
__git_ignore_thestage_line: str = f'/{THESTAGE_CONFIG_DIR}/'
|
|
25
|
-
|
|
26
|
-
__special_main_branches = ['main', 'master']
|
|
27
|
-
|
|
28
|
-
__base_git_url: str = 'https://github.com/'
|
|
29
|
-
|
|
30
|
-
def __init__(
|
|
31
|
-
self,
|
|
32
|
-
file_system_service: FileSystemService,
|
|
33
|
-
):
|
|
34
|
-
self.__file_system_service = file_system_service
|
|
35
|
-
|
|
36
|
-
# todo delete this fuckery
|
|
37
|
-
def __get_repo(self, path: str) -> Repo:
|
|
38
|
-
return git.Repo(path)
|
|
39
|
-
|
|
40
|
-
def is_present_local_git(self, path: str) -> bool:
|
|
41
|
-
git_path = self.__file_system_service.get_path(path)
|
|
42
|
-
if not git_path.exists():
|
|
43
|
-
return False
|
|
44
|
-
|
|
45
|
-
git_path = git_path.joinpath('.git')
|
|
46
|
-
if not git_path.exists():
|
|
47
|
-
return False
|
|
48
|
-
|
|
49
|
-
result = git.repo.base.is_git_dir(git_path)
|
|
50
|
-
return result
|
|
51
|
-
|
|
52
|
-
def get_remote(self, path: str) -> Optional[List[Remote]]:
|
|
53
|
-
is_git_repo = self.is_present_local_git(path=path)
|
|
54
|
-
if is_git_repo:
|
|
55
|
-
repo = git.Repo(path)
|
|
56
|
-
remotes: Optional[List[Remote]] = list(repo.remotes) if repo.remotes else []
|
|
57
|
-
return remotes
|
|
58
|
-
return None
|
|
59
|
-
|
|
60
|
-
def has_remote(self, path: str) -> bool:
|
|
61
|
-
remotes: Optional[List[Remote]] = self.get_remote(path)
|
|
62
|
-
return True if remotes is not None and len(remotes) > 0 else False
|
|
63
|
-
|
|
64
|
-
def has_changes_with_untracked(self, path: str) -> bool:
|
|
65
|
-
repo = self.__get_repo(path=path)
|
|
66
|
-
return repo.is_dirty(untracked_files=True)
|
|
67
|
-
|
|
68
|
-
def init_repository(
|
|
69
|
-
self,
|
|
70
|
-
path: str,
|
|
71
|
-
) -> Optional[Repo]:
|
|
72
|
-
|
|
73
|
-
repo = git.Repo.init(path)
|
|
74
|
-
if repo:
|
|
75
|
-
# default git name master, rename to main - sync wih github
|
|
76
|
-
repo.git.branch("-M", self.__base_name_local)
|
|
77
|
-
return repo
|
|
78
|
-
|
|
79
|
-
def add_remote_to_repo(
|
|
80
|
-
self,
|
|
81
|
-
path: str,
|
|
82
|
-
remote_url: str,
|
|
83
|
-
remote_name: str,
|
|
84
|
-
) -> bool:
|
|
85
|
-
repo = self.__get_repo(path=path)
|
|
86
|
-
remotes: List[Remote] = repo.remotes
|
|
87
|
-
not_present = True
|
|
88
|
-
if remotes:
|
|
89
|
-
item = list(filter(lambda x: x.name == remote_name, remotes))
|
|
90
|
-
if len(item) > 0:
|
|
91
|
-
not_present = False
|
|
92
|
-
|
|
93
|
-
if not_present:
|
|
94
|
-
remote: Remote = repo.create_remote(
|
|
95
|
-
name=self.__base_name_remote,
|
|
96
|
-
url=remote_url,
|
|
97
|
-
)
|
|
98
|
-
if remote:
|
|
99
|
-
return True
|
|
100
|
-
else:
|
|
101
|
-
return False
|
|
102
|
-
else:
|
|
103
|
-
return True
|
|
104
|
-
|
|
105
|
-
def git_fetch(self, path: str, deploy_key_path: str):
|
|
106
|
-
repo = self.__get_repo(path=path)
|
|
107
|
-
git_ssh_cmd = 'ssh -F /dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i %s' % deploy_key_path
|
|
108
|
-
|
|
109
|
-
with repo.git.custom_environment(GIT_SSH_COMMAND=git_ssh_cmd):
|
|
110
|
-
remote: Remote = repo.remote(self.__base_name_remote)
|
|
111
|
-
if remote:
|
|
112
|
-
progress = ProgressPrinter()
|
|
113
|
-
try:
|
|
114
|
-
remote.fetch(progress=progress)
|
|
115
|
-
except GitCommandError as ex:
|
|
116
|
-
for line in progress.allDroppedLines():
|
|
117
|
-
# returning the whole output if failed - so that user have any idea what's going on
|
|
118
|
-
print(f'>> {line}')
|
|
119
|
-
raise ex
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def git_pull(self, path: str, deploy_key_path: str):
|
|
123
|
-
repo = self.__get_repo(path=path)
|
|
124
|
-
git_ssh_cmd = 'ssh -F /dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i %s' % deploy_key_path
|
|
125
|
-
|
|
126
|
-
with repo.git.custom_environment(GIT_SSH_COMMAND=git_ssh_cmd):
|
|
127
|
-
local_branch = self.__base_name_local
|
|
128
|
-
if repo.active_branch.name:
|
|
129
|
-
local_branch = repo.active_branch.name
|
|
130
|
-
|
|
131
|
-
origin = repo.remote(self.__base_name_remote)
|
|
132
|
-
|
|
133
|
-
if origin:
|
|
134
|
-
progress = ProgressPrinter()
|
|
135
|
-
try:
|
|
136
|
-
origin.pull(refspec=local_branch, progress=progress)
|
|
137
|
-
typer.echo(f"Pulled remote changes to branch '{local_branch}'")
|
|
138
|
-
except GitCommandError as ex:
|
|
139
|
-
for line in progress.allDroppedLines():
|
|
140
|
-
# returning the whole output if failed - so that user have any idea what's going on
|
|
141
|
-
print(f'>> {line}')
|
|
142
|
-
raise ex
|
|
143
|
-
|
|
144
|
-
def find_main_branch_name(self, path: str) -> Optional[str]:
|
|
145
|
-
repo = self.__get_repo(path=path)
|
|
146
|
-
if repo:
|
|
147
|
-
for branch in [head.name for head in repo.heads]:
|
|
148
|
-
if branch in self.__special_main_branches:
|
|
149
|
-
return branch
|
|
150
|
-
return None
|
|
151
|
-
|
|
152
|
-
def get_active_branch_name(self, path: str) -> Optional[str]:
|
|
153
|
-
repo = self.__get_repo(path=path)
|
|
154
|
-
if repo:
|
|
155
|
-
if repo.head.is_detached:
|
|
156
|
-
return None
|
|
157
|
-
return repo.active_branch.name
|
|
158
|
-
return None
|
|
159
|
-
|
|
160
|
-
def git_checkout_to_branch(self, path: str, branch: str):
|
|
161
|
-
repo = self.__get_repo(path=path)
|
|
162
|
-
if repo:
|
|
163
|
-
repo.git.checkout(branch.strip())
|
|
164
|
-
|
|
165
|
-
def git_checkout_to_commit(self, path: str, commit_hash: str = None) -> bool:
|
|
166
|
-
repo = self.__get_repo(path=path)
|
|
167
|
-
if repo:
|
|
168
|
-
if is_commit_exists(repo, commit_hash):
|
|
169
|
-
repo.git.checkout(commit_hash.strip())
|
|
170
|
-
return True
|
|
171
|
-
else:
|
|
172
|
-
typer.echo(f"Could not checkout to commit {commit_hash} - reference not found in repository")
|
|
173
|
-
return False
|
|
174
|
-
|
|
175
|
-
def build_http_repo_url(self, git_path: str) -> str:
|
|
176
|
-
start_path_pos = git_path.find(":")
|
|
177
|
-
pre_url = git_path[start_path_pos + 1:]
|
|
178
|
-
url = pre_url.replace('.git', '')
|
|
179
|
-
return self.__base_git_url + url
|
|
180
|
-
|
|
181
|
-
def clone(self, url: str, path: str, deploy_key_path: str) -> Optional[Repo]:
|
|
182
|
-
try:
|
|
183
|
-
git_ssh_cmd = 'ssh -F /dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i %s' % deploy_key_path
|
|
184
|
-
return Repo.clone_from(url=url, to_path=path, env={"GIT_SSH_COMMAND": git_ssh_cmd})
|
|
185
|
-
except GitCommandError as base_ex:
|
|
186
|
-
msg = base_ex.stderr
|
|
187
|
-
if msg and 'Repository not found' in msg and 'correct access rights' in msg:
|
|
188
|
-
raise GitAccessException(
|
|
189
|
-
message='You dont have access to repository, or repository not found.',
|
|
190
|
-
url=self.build_http_repo_url(git_path=url),
|
|
191
|
-
dop_message=msg,
|
|
192
|
-
)
|
|
193
|
-
else:
|
|
194
|
-
raise base_ex
|
|
195
|
-
|
|
196
|
-
def commit_local_changes(
|
|
197
|
-
self,
|
|
198
|
-
path: str,
|
|
199
|
-
name: Optional[str] = None,
|
|
200
|
-
) -> Optional[str]:
|
|
201
|
-
repo = self.__get_repo(path=path)
|
|
202
|
-
if repo.head.is_detached:
|
|
203
|
-
line_color = ColorScheme.GIT_HEADLESS
|
|
204
|
-
print(f'[{line_color}]Committing in detached head state at {repo.head.commit.hexsha}[/{line_color}]')
|
|
205
|
-
commit_name = name if name else f"Auto commit {str(datetime.datetime.now().date())}"
|
|
206
|
-
commit = repo.git.commit('--allow-empty', '-m', commit_name, )
|
|
207
|
-
return commit
|
|
208
|
-
|
|
209
|
-
def push_changes(
|
|
210
|
-
self,
|
|
211
|
-
path: str,
|
|
212
|
-
deploy_key_path: str
|
|
213
|
-
):
|
|
214
|
-
repo = self.__get_repo(path=path)
|
|
215
|
-
git_ssh_cmd = 'ssh -F /dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i %s' % deploy_key_path
|
|
216
|
-
|
|
217
|
-
with repo.git.custom_environment(GIT_SSH_COMMAND=git_ssh_cmd):
|
|
218
|
-
origin = repo.remote(self.__base_name_remote)
|
|
219
|
-
if origin:
|
|
220
|
-
progress = ProgressPrinter()
|
|
221
|
-
# repo.git.push(origin.name, repo.active_branch.name)
|
|
222
|
-
try:
|
|
223
|
-
origin.push(refspec=repo.active_branch.name, progress=progress).raise_if_error()
|
|
224
|
-
except GitCommandError as ex:
|
|
225
|
-
for line in progress.allDroppedLines():
|
|
226
|
-
# returning the whole output if failed - so that user have any idea what's going on
|
|
227
|
-
print(f'>> {line}')
|
|
228
|
-
raise ex
|
|
229
|
-
|
|
230
|
-
def get_current_commit(self, path: str,) -> Optional[Commit]:
|
|
231
|
-
repo = self.__get_repo(path=path)
|
|
232
|
-
if repo:
|
|
233
|
-
return repo.head.commit
|
|
234
|
-
else:
|
|
235
|
-
return None
|
|
236
|
-
|
|
237
|
-
def get_commit_by_hash(self, path: str, commit_hash: str) -> Optional[Commit]:
|
|
238
|
-
repo = self.__get_repo(path=path)
|
|
239
|
-
if repo:
|
|
240
|
-
try:
|
|
241
|
-
return repo.commit(commit_hash)
|
|
242
|
-
except BadName as ex:
|
|
243
|
-
return None
|
|
244
|
-
else:
|
|
245
|
-
return None
|
|
246
|
-
|
|
247
|
-
def _get_gitignore_path(self, path: str) -> Path:
|
|
248
|
-
git_path = self.__file_system_service.get_path(path)
|
|
249
|
-
return git_path.joinpath('.gitignore')
|
|
250
|
-
|
|
251
|
-
def git_add_by_path(self, repo_path: str, file_path: str):
|
|
252
|
-
repo = self.__get_repo(path=repo_path)
|
|
253
|
-
if repo:
|
|
254
|
-
abs_path = os.path.join(repo_path, file_path)
|
|
255
|
-
if os.path.isfile(abs_path):
|
|
256
|
-
repo.index.add([file_path])
|
|
257
|
-
else:
|
|
258
|
-
repo.index.remove([file_path])
|
|
259
|
-
|
|
260
|
-
def git_add_all(self, repo_path: str):
|
|
261
|
-
repo = self.__get_repo(path=repo_path)
|
|
262
|
-
if repo:
|
|
263
|
-
repo.git.add(all=True)
|
|
264
|
-
|
|
265
|
-
def git_diff_stat(self, repo_path: str) -> str:
|
|
266
|
-
repo = self.__get_repo(path=repo_path)
|
|
267
|
-
if repo:
|
|
268
|
-
try:
|
|
269
|
-
diff: str = repo.git.diff("--cached", "--stat")
|
|
270
|
-
if diff:
|
|
271
|
-
return diff.splitlines()[-1]
|
|
272
|
-
else:
|
|
273
|
-
return None
|
|
274
|
-
except ValueError as e:
|
|
275
|
-
return str(e)
|
|
276
|
-
|
|
277
|
-
def add_files_with_size_limit_or_warn(
|
|
278
|
-
self,
|
|
279
|
-
repo_path: str,
|
|
280
|
-
files_to_add: Optional[str] = None,
|
|
281
|
-
max_file_size: int = 500 * 1024, # default 500KB
|
|
282
|
-
) -> bool:
|
|
283
|
-
"""
|
|
284
|
-
Adds to git only files <= max_file_size.
|
|
285
|
-
If files_to_add is not provided, adds all changed/new files except those that are too large.
|
|
286
|
-
Returns True if files were added, False if operation was aborted due to large files.
|
|
287
|
-
"""
|
|
288
|
-
if files_to_add:
|
|
289
|
-
return self.add_files_by_path(repo_path=repo_path, files_to_add=files_to_add, max_file_size=max_file_size)
|
|
290
|
-
|
|
291
|
-
return self.add_all_files(repo_path=repo_path, max_file_size=max_file_size)
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
def add_files_by_path(
|
|
295
|
-
self,
|
|
296
|
-
repo_path: str,
|
|
297
|
-
files_to_add: str,
|
|
298
|
-
max_file_size: int = 500 * 1024, # default 500KB
|
|
299
|
-
) -> bool:
|
|
300
|
-
repo = self.__get_repo(path=repo_path)
|
|
301
|
-
|
|
302
|
-
if files_to_add.strip().endswith(","):
|
|
303
|
-
space_warning = f"[{ColorScheme.WARNING.value}][WARNING] Use only commas to separate files, without spaces[/{ColorScheme.WARNING.value}]"
|
|
304
|
-
print(space_warning)
|
|
305
|
-
return False
|
|
306
|
-
|
|
307
|
-
files = [f.strip() for f in files_to_add.split(",") if f.strip()]
|
|
308
|
-
|
|
309
|
-
rejected_file_paths = []
|
|
310
|
-
missing_file_paths = []
|
|
311
|
-
|
|
312
|
-
if repo.head.is_valid():
|
|
313
|
-
staged_files = [item.a_path for item in repo.index.diff('HEAD')]
|
|
314
|
-
else:
|
|
315
|
-
staged_files = []
|
|
316
|
-
|
|
317
|
-
deleted_files = [item.a_path for item in repo.index.diff(None) if item.change_type == 'D']
|
|
318
|
-
|
|
319
|
-
for file_path in files:
|
|
320
|
-
if file_path not in deleted_files and file_path not in staged_files:
|
|
321
|
-
abs_path = os.path.join(repo_path, file_path)
|
|
322
|
-
|
|
323
|
-
if not os.path.isfile(abs_path):
|
|
324
|
-
missing_file_paths.append(file_path)
|
|
325
|
-
elif os.path.getsize(abs_path) > max_file_size:
|
|
326
|
-
rejected_file_paths.append(file_path)
|
|
327
|
-
|
|
328
|
-
if missing_file_paths:
|
|
329
|
-
not_found_file_warning = f"[{ColorScheme.WARNING.value}][WARNING] The following files do not exist and cannot be added: {', '.join(missing_file_paths)}[{ColorScheme.WARNING.value}]"
|
|
330
|
-
print(not_found_file_warning)
|
|
331
|
-
|
|
332
|
-
if rejected_file_paths:
|
|
333
|
-
size_kb = max_file_size // 1024
|
|
334
|
-
wrong_size_warning = f"[{ColorScheme.WARNING.value}][WARNING] The following files exceed {size_kb}KB and cannot be added: {', '.join(rejected_file_paths)}[{ColorScheme.WARNING.value}]"
|
|
335
|
-
print(wrong_size_warning)
|
|
336
|
-
|
|
337
|
-
if rejected_file_paths or missing_file_paths:
|
|
338
|
-
return False
|
|
339
|
-
|
|
340
|
-
for file_path in files:
|
|
341
|
-
if file_path not in staged_files:
|
|
342
|
-
self.git_add_by_path(repo_path=repo_path, file_path=file_path)
|
|
343
|
-
|
|
344
|
-
return True
|
|
345
|
-
|
|
346
|
-
def add_all_files(
|
|
347
|
-
self,
|
|
348
|
-
repo_path: str,
|
|
349
|
-
max_file_size: int = 500 * 1024, # default 500KB
|
|
350
|
-
) -> bool:
|
|
351
|
-
repo = self.__get_repo(path=repo_path)
|
|
352
|
-
|
|
353
|
-
files = [item.a_path for item in repo.index.diff(None)] + repo.untracked_files
|
|
354
|
-
|
|
355
|
-
rejected_file_paths = []
|
|
356
|
-
deleted_files = [item.a_path for item in repo.index.diff(None) if item.change_type == 'D']
|
|
357
|
-
|
|
358
|
-
for file_path in files:
|
|
359
|
-
abs_path = os.path.join(repo_path, file_path)
|
|
360
|
-
if file_path not in deleted_files and os.path.getsize(abs_path) > max_file_size:
|
|
361
|
-
rejected_file_paths.append(file_path)
|
|
362
|
-
|
|
363
|
-
if rejected_file_paths:
|
|
364
|
-
size_kb = max_file_size // 1024
|
|
365
|
-
wrong_size_warning = f"[{ColorScheme.WARNING.value}][WARNING] The following files exceed {size_kb}KB and cannot be added: {', '.join(rejected_file_paths)}[{ColorScheme.WARNING.value}]"
|
|
366
|
-
print(wrong_size_warning)
|
|
367
|
-
return False
|
|
368
|
-
|
|
369
|
-
self.git_add_all(repo_path=repo_path)
|
|
370
|
-
|
|
371
|
-
return True
|
|
372
|
-
|
|
373
|
-
def init_gitignore(self, path: str):
|
|
374
|
-
gitignore_path = self._get_gitignore_path(path=path)
|
|
375
|
-
if not gitignore_path.exists():
|
|
376
|
-
self.__file_system_service.create_if_not_exists_file(gitignore_path)
|
|
377
|
-
self.git_add_by_path(repo_path=path, file_path=str(gitignore_path))
|
|
378
|
-
|
|
379
|
-
is_present_tsr = self.__file_system_service.find_line_in_text_file(file=str(gitignore_path),
|
|
380
|
-
find=self.__git_ignore_thestage_line)
|
|
381
|
-
if not is_present_tsr:
|
|
382
|
-
self.__file_system_service.add_line_to_text_file(file=str(gitignore_path),
|
|
383
|
-
new_line=self.__git_ignore_thestage_line)
|
|
384
|
-
|
|
385
|
-
def is_head_detached(self, path: str) -> bool:
|
|
386
|
-
repo = self.__get_repo(path=path)
|
|
387
|
-
if repo:
|
|
388
|
-
return repo.head.is_detached
|
|
389
|
-
|
|
390
|
-
def reset_hard(self, path: str, deploy_key_path: str, reset_to_origin: bool):
|
|
391
|
-
repo = self.__get_repo(path=path)
|
|
392
|
-
if repo:
|
|
393
|
-
git_ssh_cmd = 'ssh -F /dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i %s' % deploy_key_path
|
|
394
|
-
with repo.git.custom_environment(GIT_SSH_COMMAND=git_ssh_cmd):
|
|
395
|
-
if reset_to_origin:
|
|
396
|
-
repo.git.reset('--hard', f'origin/{repo.active_branch.name}')
|
|
397
|
-
typer.echo(f'Branch "{repo.active_branch.name}" is now synced to its remote counterpart')
|
|
398
|
-
else:
|
|
399
|
-
typer.echo('simple branch reset is not implemented')
|
|
400
|
-
|
|
401
|
-
# refers to a "headless commit" where something was committed while in detached head state and head is pointing at that commit
|
|
402
|
-
def is_head_committed_in_headless_state(self, path: str) -> bool:
|
|
403
|
-
repo = self.__get_repo(path=path)
|
|
404
|
-
if repo:
|
|
405
|
-
commit = repo.head.commit
|
|
406
|
-
for branch in repo.heads:
|
|
407
|
-
for commit_item in repo.iter_commits(branch):
|
|
408
|
-
if commit_item.hexsha == commit.hexsha:
|
|
409
|
-
return False
|
|
410
|
-
return True
|
|
411
|
-
|
|
412
|
-
def is_branch_exists(self, path: str, branch_name: str) -> bool:
|
|
413
|
-
repo = self.__get_repo(path=path)
|
|
414
|
-
if repo:
|
|
415
|
-
for branch in repo.heads:
|
|
416
|
-
if branch.name == branch_name:
|
|
417
|
-
return True
|
|
418
|
-
|
|
419
|
-
for ref in repo.remotes.origin.refs:
|
|
420
|
-
if ref.remote_head == branch_name:
|
|
421
|
-
typer.echo(f'Found remote branch "{branch_name}"')
|
|
422
|
-
return True
|
|
423
|
-
return False
|
|
424
|
-
|
|
425
|
-
def checkout_to_new_branch(self, path: str, branch_name: str):
|
|
426
|
-
repo = self.__get_repo(path=path)
|
|
427
|
-
if repo:
|
|
428
|
-
repo.git.checkout("-b", branch_name)
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
def is_commit_exists(repo, commit_sha) -> bool:
|
|
432
|
-
try:
|
|
433
|
-
repo.commit(commit_sha)
|
|
434
|
-
return True
|
|
435
|
-
except ValueError:
|
|
436
|
-
return False
|
|
1
|
+
import datetime
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional, List
|
|
7
|
+
|
|
8
|
+
import git
|
|
9
|
+
import typer
|
|
10
|
+
from git import Remote, Repo, GitCommandError, Commit
|
|
11
|
+
from gitdb.exc import BadName
|
|
12
|
+
from rich import print
|
|
13
|
+
|
|
14
|
+
from thestage.color_scheme.color_scheme import ColorScheme
|
|
15
|
+
from thestage.config import THESTAGE_CONFIG_DIR
|
|
16
|
+
from thestage.exceptions.git_access_exception import GitAccessException
|
|
17
|
+
from thestage.git.ProgressPrinter import ProgressPrinter
|
|
18
|
+
from thestage.services.filesystem_service import FileSystemService
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GitLocalClient:
|
|
22
|
+
__base_name_remote: str = 'origin'
|
|
23
|
+
__base_name_local: str = 'main'
|
|
24
|
+
__git_ignore_thestage_line: str = f'/{THESTAGE_CONFIG_DIR}/'
|
|
25
|
+
|
|
26
|
+
__special_main_branches = ['main', 'master']
|
|
27
|
+
|
|
28
|
+
__base_git_url: str = 'https://github.com/'
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
file_system_service: FileSystemService,
|
|
33
|
+
):
|
|
34
|
+
self.__file_system_service = file_system_service
|
|
35
|
+
|
|
36
|
+
# todo delete this fuckery
|
|
37
|
+
def __get_repo(self, path: str) -> Repo:
|
|
38
|
+
return git.Repo(path)
|
|
39
|
+
|
|
40
|
+
def is_present_local_git(self, path: str) -> bool:
|
|
41
|
+
git_path = self.__file_system_service.get_path(path)
|
|
42
|
+
if not git_path.exists():
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
git_path = git_path.joinpath('.git')
|
|
46
|
+
if not git_path.exists():
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
result = git.repo.base.is_git_dir(git_path)
|
|
50
|
+
return result
|
|
51
|
+
|
|
52
|
+
def get_remote(self, path: str) -> Optional[List[Remote]]:
|
|
53
|
+
is_git_repo = self.is_present_local_git(path=path)
|
|
54
|
+
if is_git_repo:
|
|
55
|
+
repo = git.Repo(path)
|
|
56
|
+
remotes: Optional[List[Remote]] = list(repo.remotes) if repo.remotes else []
|
|
57
|
+
return remotes
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
def has_remote(self, path: str) -> bool:
|
|
61
|
+
remotes: Optional[List[Remote]] = self.get_remote(path)
|
|
62
|
+
return True if remotes is not None and len(remotes) > 0 else False
|
|
63
|
+
|
|
64
|
+
def has_changes_with_untracked(self, path: str) -> bool:
|
|
65
|
+
repo = self.__get_repo(path=path)
|
|
66
|
+
return repo.is_dirty(untracked_files=True)
|
|
67
|
+
|
|
68
|
+
def init_repository(
|
|
69
|
+
self,
|
|
70
|
+
path: str,
|
|
71
|
+
) -> Optional[Repo]:
|
|
72
|
+
|
|
73
|
+
repo = git.Repo.init(path)
|
|
74
|
+
if repo:
|
|
75
|
+
# default git name master, rename to main - sync wih github
|
|
76
|
+
repo.git.branch("-M", self.__base_name_local)
|
|
77
|
+
return repo
|
|
78
|
+
|
|
79
|
+
def add_remote_to_repo(
|
|
80
|
+
self,
|
|
81
|
+
path: str,
|
|
82
|
+
remote_url: str,
|
|
83
|
+
remote_name: str,
|
|
84
|
+
) -> bool:
|
|
85
|
+
repo = self.__get_repo(path=path)
|
|
86
|
+
remotes: List[Remote] = repo.remotes
|
|
87
|
+
not_present = True
|
|
88
|
+
if remotes:
|
|
89
|
+
item = list(filter(lambda x: x.name == remote_name, remotes))
|
|
90
|
+
if len(item) > 0:
|
|
91
|
+
not_present = False
|
|
92
|
+
|
|
93
|
+
if not_present:
|
|
94
|
+
remote: Remote = repo.create_remote(
|
|
95
|
+
name=self.__base_name_remote,
|
|
96
|
+
url=remote_url,
|
|
97
|
+
)
|
|
98
|
+
if remote:
|
|
99
|
+
return True
|
|
100
|
+
else:
|
|
101
|
+
return False
|
|
102
|
+
else:
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
def git_fetch(self, path: str, deploy_key_path: str):
|
|
106
|
+
repo = self.__get_repo(path=path)
|
|
107
|
+
git_ssh_cmd = 'ssh -F /dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i %s' % deploy_key_path
|
|
108
|
+
|
|
109
|
+
with repo.git.custom_environment(GIT_SSH_COMMAND=git_ssh_cmd):
|
|
110
|
+
remote: Remote = repo.remote(self.__base_name_remote)
|
|
111
|
+
if remote:
|
|
112
|
+
progress = ProgressPrinter()
|
|
113
|
+
try:
|
|
114
|
+
remote.fetch(progress=progress)
|
|
115
|
+
except GitCommandError as ex:
|
|
116
|
+
for line in progress.allDroppedLines():
|
|
117
|
+
# returning the whole output if failed - so that user have any idea what's going on
|
|
118
|
+
print(f'>> {line}')
|
|
119
|
+
raise ex
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def git_pull(self, path: str, deploy_key_path: str):
|
|
123
|
+
repo = self.__get_repo(path=path)
|
|
124
|
+
git_ssh_cmd = 'ssh -F /dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i %s' % deploy_key_path
|
|
125
|
+
|
|
126
|
+
with repo.git.custom_environment(GIT_SSH_COMMAND=git_ssh_cmd):
|
|
127
|
+
local_branch = self.__base_name_local
|
|
128
|
+
if repo.active_branch.name:
|
|
129
|
+
local_branch = repo.active_branch.name
|
|
130
|
+
|
|
131
|
+
origin = repo.remote(self.__base_name_remote)
|
|
132
|
+
|
|
133
|
+
if origin:
|
|
134
|
+
progress = ProgressPrinter()
|
|
135
|
+
try:
|
|
136
|
+
origin.pull(refspec=local_branch, progress=progress)
|
|
137
|
+
typer.echo(f"Pulled remote changes to branch '{local_branch}'")
|
|
138
|
+
except GitCommandError as ex:
|
|
139
|
+
for line in progress.allDroppedLines():
|
|
140
|
+
# returning the whole output if failed - so that user have any idea what's going on
|
|
141
|
+
print(f'>> {line}')
|
|
142
|
+
raise ex
|
|
143
|
+
|
|
144
|
+
def find_main_branch_name(self, path: str) -> Optional[str]:
|
|
145
|
+
repo = self.__get_repo(path=path)
|
|
146
|
+
if repo:
|
|
147
|
+
for branch in [head.name for head in repo.heads]:
|
|
148
|
+
if branch in self.__special_main_branches:
|
|
149
|
+
return branch
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
def get_active_branch_name(self, path: str) -> Optional[str]:
|
|
153
|
+
repo = self.__get_repo(path=path)
|
|
154
|
+
if repo:
|
|
155
|
+
if repo.head.is_detached:
|
|
156
|
+
return None
|
|
157
|
+
return repo.active_branch.name
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
def git_checkout_to_branch(self, path: str, branch: str):
|
|
161
|
+
repo = self.__get_repo(path=path)
|
|
162
|
+
if repo:
|
|
163
|
+
repo.git.checkout(branch.strip())
|
|
164
|
+
|
|
165
|
+
def git_checkout_to_commit(self, path: str, commit_hash: str = None) -> bool:
|
|
166
|
+
repo = self.__get_repo(path=path)
|
|
167
|
+
if repo:
|
|
168
|
+
if is_commit_exists(repo, commit_hash):
|
|
169
|
+
repo.git.checkout(commit_hash.strip())
|
|
170
|
+
return True
|
|
171
|
+
else:
|
|
172
|
+
typer.echo(f"Could not checkout to commit {commit_hash} - reference not found in repository")
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
def build_http_repo_url(self, git_path: str) -> str:
|
|
176
|
+
start_path_pos = git_path.find(":")
|
|
177
|
+
pre_url = git_path[start_path_pos + 1:]
|
|
178
|
+
url = pre_url.replace('.git', '')
|
|
179
|
+
return self.__base_git_url + url
|
|
180
|
+
|
|
181
|
+
def clone(self, url: str, path: str, deploy_key_path: str) -> Optional[Repo]:
|
|
182
|
+
try:
|
|
183
|
+
git_ssh_cmd = 'ssh -F /dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i %s' % deploy_key_path
|
|
184
|
+
return Repo.clone_from(url=url, to_path=path, env={"GIT_SSH_COMMAND": git_ssh_cmd})
|
|
185
|
+
except GitCommandError as base_ex:
|
|
186
|
+
msg = base_ex.stderr
|
|
187
|
+
if msg and 'Repository not found' in msg and 'correct access rights' in msg:
|
|
188
|
+
raise GitAccessException(
|
|
189
|
+
message='You dont have access to repository, or repository not found.',
|
|
190
|
+
url=self.build_http_repo_url(git_path=url),
|
|
191
|
+
dop_message=msg,
|
|
192
|
+
)
|
|
193
|
+
else:
|
|
194
|
+
raise base_ex
|
|
195
|
+
|
|
196
|
+
def commit_local_changes(
|
|
197
|
+
self,
|
|
198
|
+
path: str,
|
|
199
|
+
name: Optional[str] = None,
|
|
200
|
+
) -> Optional[str]:
|
|
201
|
+
repo = self.__get_repo(path=path)
|
|
202
|
+
if repo.head.is_detached:
|
|
203
|
+
line_color = ColorScheme.GIT_HEADLESS
|
|
204
|
+
print(f'[{line_color}]Committing in detached head state at {repo.head.commit.hexsha}[/{line_color}]')
|
|
205
|
+
commit_name = name if name else f"Auto commit {str(datetime.datetime.now().date())}"
|
|
206
|
+
commit = repo.git.commit('--allow-empty', '-m', commit_name, )
|
|
207
|
+
return commit
|
|
208
|
+
|
|
209
|
+
def push_changes(
|
|
210
|
+
self,
|
|
211
|
+
path: str,
|
|
212
|
+
deploy_key_path: str
|
|
213
|
+
):
|
|
214
|
+
repo = self.__get_repo(path=path)
|
|
215
|
+
git_ssh_cmd = 'ssh -F /dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i %s' % deploy_key_path
|
|
216
|
+
|
|
217
|
+
with repo.git.custom_environment(GIT_SSH_COMMAND=git_ssh_cmd):
|
|
218
|
+
origin = repo.remote(self.__base_name_remote)
|
|
219
|
+
if origin:
|
|
220
|
+
progress = ProgressPrinter()
|
|
221
|
+
# repo.git.push(origin.name, repo.active_branch.name)
|
|
222
|
+
try:
|
|
223
|
+
origin.push(refspec=repo.active_branch.name, progress=progress).raise_if_error()
|
|
224
|
+
except GitCommandError as ex:
|
|
225
|
+
for line in progress.allDroppedLines():
|
|
226
|
+
# returning the whole output if failed - so that user have any idea what's going on
|
|
227
|
+
print(f'>> {line}')
|
|
228
|
+
raise ex
|
|
229
|
+
|
|
230
|
+
def get_current_commit(self, path: str,) -> Optional[Commit]:
|
|
231
|
+
repo = self.__get_repo(path=path)
|
|
232
|
+
if repo:
|
|
233
|
+
return repo.head.commit
|
|
234
|
+
else:
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
def get_commit_by_hash(self, path: str, commit_hash: str) -> Optional[Commit]:
|
|
238
|
+
repo = self.__get_repo(path=path)
|
|
239
|
+
if repo:
|
|
240
|
+
try:
|
|
241
|
+
return repo.commit(commit_hash)
|
|
242
|
+
except BadName as ex:
|
|
243
|
+
return None
|
|
244
|
+
else:
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
def _get_gitignore_path(self, path: str) -> Path:
|
|
248
|
+
git_path = self.__file_system_service.get_path(path)
|
|
249
|
+
return git_path.joinpath('.gitignore')
|
|
250
|
+
|
|
251
|
+
def git_add_by_path(self, repo_path: str, file_path: str):
|
|
252
|
+
repo = self.__get_repo(path=repo_path)
|
|
253
|
+
if repo:
|
|
254
|
+
abs_path = os.path.join(repo_path, file_path)
|
|
255
|
+
if os.path.isfile(abs_path):
|
|
256
|
+
repo.index.add([file_path])
|
|
257
|
+
else:
|
|
258
|
+
repo.index.remove([file_path])
|
|
259
|
+
|
|
260
|
+
def git_add_all(self, repo_path: str):
|
|
261
|
+
repo = self.__get_repo(path=repo_path)
|
|
262
|
+
if repo:
|
|
263
|
+
repo.git.add(all=True)
|
|
264
|
+
|
|
265
|
+
def git_diff_stat(self, repo_path: str) -> str:
|
|
266
|
+
repo = self.__get_repo(path=repo_path)
|
|
267
|
+
if repo:
|
|
268
|
+
try:
|
|
269
|
+
diff: str = repo.git.diff("--cached", "--stat")
|
|
270
|
+
if diff:
|
|
271
|
+
return diff.splitlines()[-1]
|
|
272
|
+
else:
|
|
273
|
+
return None
|
|
274
|
+
except ValueError as e:
|
|
275
|
+
return str(e)
|
|
276
|
+
|
|
277
|
+
def add_files_with_size_limit_or_warn(
|
|
278
|
+
self,
|
|
279
|
+
repo_path: str,
|
|
280
|
+
files_to_add: Optional[str] = None,
|
|
281
|
+
max_file_size: int = 500 * 1024, # default 500KB
|
|
282
|
+
) -> bool:
|
|
283
|
+
"""
|
|
284
|
+
Adds to git only files <= max_file_size.
|
|
285
|
+
If files_to_add is not provided, adds all changed/new files except those that are too large.
|
|
286
|
+
Returns True if files were added, False if operation was aborted due to large files.
|
|
287
|
+
"""
|
|
288
|
+
if files_to_add:
|
|
289
|
+
return self.add_files_by_path(repo_path=repo_path, files_to_add=files_to_add, max_file_size=max_file_size)
|
|
290
|
+
|
|
291
|
+
return self.add_all_files(repo_path=repo_path, max_file_size=max_file_size)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def add_files_by_path(
|
|
295
|
+
self,
|
|
296
|
+
repo_path: str,
|
|
297
|
+
files_to_add: str,
|
|
298
|
+
max_file_size: int = 500 * 1024, # default 500KB
|
|
299
|
+
) -> bool:
|
|
300
|
+
repo = self.__get_repo(path=repo_path)
|
|
301
|
+
|
|
302
|
+
if files_to_add.strip().endswith(","):
|
|
303
|
+
space_warning = f"[{ColorScheme.WARNING.value}][WARNING] Use only commas to separate files, without spaces[/{ColorScheme.WARNING.value}]"
|
|
304
|
+
print(space_warning)
|
|
305
|
+
return False
|
|
306
|
+
|
|
307
|
+
files = [f.strip() for f in files_to_add.split(",") if f.strip()]
|
|
308
|
+
|
|
309
|
+
rejected_file_paths = []
|
|
310
|
+
missing_file_paths = []
|
|
311
|
+
|
|
312
|
+
if repo.head.is_valid():
|
|
313
|
+
staged_files = [item.a_path for item in repo.index.diff('HEAD')]
|
|
314
|
+
else:
|
|
315
|
+
staged_files = []
|
|
316
|
+
|
|
317
|
+
deleted_files = [item.a_path for item in repo.index.diff(None) if item.change_type == 'D']
|
|
318
|
+
|
|
319
|
+
for file_path in files:
|
|
320
|
+
if file_path not in deleted_files and file_path not in staged_files:
|
|
321
|
+
abs_path = os.path.join(repo_path, file_path)
|
|
322
|
+
|
|
323
|
+
if not os.path.isfile(abs_path):
|
|
324
|
+
missing_file_paths.append(file_path)
|
|
325
|
+
elif os.path.getsize(abs_path) > max_file_size:
|
|
326
|
+
rejected_file_paths.append(file_path)
|
|
327
|
+
|
|
328
|
+
if missing_file_paths:
|
|
329
|
+
not_found_file_warning = f"[{ColorScheme.WARNING.value}][WARNING] The following files do not exist and cannot be added: {', '.join(missing_file_paths)}[{ColorScheme.WARNING.value}]"
|
|
330
|
+
print(not_found_file_warning)
|
|
331
|
+
|
|
332
|
+
if rejected_file_paths:
|
|
333
|
+
size_kb = max_file_size // 1024
|
|
334
|
+
wrong_size_warning = f"[{ColorScheme.WARNING.value}][WARNING] The following files exceed {size_kb}KB and cannot be added: {', '.join(rejected_file_paths)}[{ColorScheme.WARNING.value}]"
|
|
335
|
+
print(wrong_size_warning)
|
|
336
|
+
|
|
337
|
+
if rejected_file_paths or missing_file_paths:
|
|
338
|
+
return False
|
|
339
|
+
|
|
340
|
+
for file_path in files:
|
|
341
|
+
if file_path not in staged_files:
|
|
342
|
+
self.git_add_by_path(repo_path=repo_path, file_path=file_path)
|
|
343
|
+
|
|
344
|
+
return True
|
|
345
|
+
|
|
346
|
+
def add_all_files(
|
|
347
|
+
self,
|
|
348
|
+
repo_path: str,
|
|
349
|
+
max_file_size: int = 500 * 1024, # default 500KB
|
|
350
|
+
) -> bool:
|
|
351
|
+
repo = self.__get_repo(path=repo_path)
|
|
352
|
+
|
|
353
|
+
files = [item.a_path for item in repo.index.diff(None)] + repo.untracked_files
|
|
354
|
+
|
|
355
|
+
rejected_file_paths = []
|
|
356
|
+
deleted_files = [item.a_path for item in repo.index.diff(None) if item.change_type == 'D']
|
|
357
|
+
|
|
358
|
+
for file_path in files:
|
|
359
|
+
abs_path = os.path.join(repo_path, file_path)
|
|
360
|
+
if file_path not in deleted_files and os.path.getsize(abs_path) > max_file_size:
|
|
361
|
+
rejected_file_paths.append(file_path)
|
|
362
|
+
|
|
363
|
+
if rejected_file_paths:
|
|
364
|
+
size_kb = max_file_size // 1024
|
|
365
|
+
wrong_size_warning = f"[{ColorScheme.WARNING.value}][WARNING] The following files exceed {size_kb}KB and cannot be added: {', '.join(rejected_file_paths)}[{ColorScheme.WARNING.value}]"
|
|
366
|
+
print(wrong_size_warning)
|
|
367
|
+
return False
|
|
368
|
+
|
|
369
|
+
self.git_add_all(repo_path=repo_path)
|
|
370
|
+
|
|
371
|
+
return True
|
|
372
|
+
|
|
373
|
+
def init_gitignore(self, path: str):
|
|
374
|
+
gitignore_path = self._get_gitignore_path(path=path)
|
|
375
|
+
if not gitignore_path.exists():
|
|
376
|
+
self.__file_system_service.create_if_not_exists_file(gitignore_path)
|
|
377
|
+
self.git_add_by_path(repo_path=path, file_path=str(gitignore_path))
|
|
378
|
+
|
|
379
|
+
is_present_tsr = self.__file_system_service.find_line_in_text_file(file=str(gitignore_path),
|
|
380
|
+
find=self.__git_ignore_thestage_line)
|
|
381
|
+
if not is_present_tsr:
|
|
382
|
+
self.__file_system_service.add_line_to_text_file(file=str(gitignore_path),
|
|
383
|
+
new_line=self.__git_ignore_thestage_line)
|
|
384
|
+
|
|
385
|
+
def is_head_detached(self, path: str) -> bool:
|
|
386
|
+
repo = self.__get_repo(path=path)
|
|
387
|
+
if repo:
|
|
388
|
+
return repo.head.is_detached
|
|
389
|
+
|
|
390
|
+
def reset_hard(self, path: str, deploy_key_path: str, reset_to_origin: bool):
|
|
391
|
+
repo = self.__get_repo(path=path)
|
|
392
|
+
if repo:
|
|
393
|
+
git_ssh_cmd = 'ssh -F /dev/null -o StrictHostKeyChecking=no -o IdentitiesOnly=yes -i %s' % deploy_key_path
|
|
394
|
+
with repo.git.custom_environment(GIT_SSH_COMMAND=git_ssh_cmd):
|
|
395
|
+
if reset_to_origin:
|
|
396
|
+
repo.git.reset('--hard', f'origin/{repo.active_branch.name}')
|
|
397
|
+
typer.echo(f'Branch "{repo.active_branch.name}" is now synced to its remote counterpart')
|
|
398
|
+
else:
|
|
399
|
+
typer.echo('simple branch reset is not implemented')
|
|
400
|
+
|
|
401
|
+
# refers to a "headless commit" where something was committed while in detached head state and head is pointing at that commit
|
|
402
|
+
def is_head_committed_in_headless_state(self, path: str) -> bool:
|
|
403
|
+
repo = self.__get_repo(path=path)
|
|
404
|
+
if repo:
|
|
405
|
+
commit = repo.head.commit
|
|
406
|
+
for branch in repo.heads:
|
|
407
|
+
for commit_item in repo.iter_commits(branch):
|
|
408
|
+
if commit_item.hexsha == commit.hexsha:
|
|
409
|
+
return False
|
|
410
|
+
return True
|
|
411
|
+
|
|
412
|
+
def is_branch_exists(self, path: str, branch_name: str) -> bool:
|
|
413
|
+
repo = self.__get_repo(path=path)
|
|
414
|
+
if repo:
|
|
415
|
+
for branch in repo.heads:
|
|
416
|
+
if branch.name == branch_name:
|
|
417
|
+
return True
|
|
418
|
+
|
|
419
|
+
for ref in repo.remotes.origin.refs:
|
|
420
|
+
if ref.remote_head == branch_name:
|
|
421
|
+
typer.echo(f'Found remote branch "{branch_name}"')
|
|
422
|
+
return True
|
|
423
|
+
return False
|
|
424
|
+
|
|
425
|
+
def checkout_to_new_branch(self, path: str, branch_name: str):
|
|
426
|
+
repo = self.__get_repo(path=path)
|
|
427
|
+
if repo:
|
|
428
|
+
repo.git.checkout("-b", branch_name)
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def is_commit_exists(repo, commit_sha) -> bool:
|
|
432
|
+
try:
|
|
433
|
+
repo.commit(commit_sha)
|
|
434
|
+
return True
|
|
435
|
+
except ValueError:
|
|
436
|
+
return False
|