Coverage for kwasa\functions\_update.py: 0%
114 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-14 18:06 +0300
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-14 18:06 +0300
1import json
2import os
3import shutil
4import subprocess
5import sys
6from contextlib import suppress
7from typing import Any
9from kwasa.libs.helper import HelperUtility
10from kwasa.logger.log import get_logger
12logger = get_logger("update")
15class GitRepoUpdater(HelperUtility):
16 """
17 Handles safe update of a cloned project using metadata from the template repo.
18 """
20 def __init__(self) -> None:
21 self.project_path = os.getcwd()
22 self.kwasa_meta = os.path.join(self.project_path, ".git", ".kwasa")
23 self.metadata_path = os.path.join(self.kwasa_meta, "metadata.json")
24 self.template_repo = None
25 self.user_remote = None
27 def run(self) -> None:
28 self._validate_git_repo()
29 self._load_template_metadata()
30 self._stage_untracked_files()
31 self._swap_origin_and_add_template()
32 self._fetch_and_merge_template()
33 self._cleanup_and_re_init_git()
34 self._restore_template_metadata()
35 self._install_dependencies()
37 def _validate_git_repo(self) -> None:
38 if not os.path.exists(".git"):
39 logger.error("❌ Not a git repo. Run this inside a cloned directory.")
40 sys.exit(1)
42 if not os.path.exists(self.metadata_path):
43 logger.error("❌ No metadata found. Use `kwasa clone` to initialize.")
44 sys.exit(1)
46 def _load_template_metadata(self) -> None:
47 with open(self.metadata_path) as f:
48 meta = json.load(f)
50 self.template_repo = meta.get("origin")
51 if not self.template_repo:
52 logger.error("❌ No 'origin' found in metadata.json.")
53 sys.exit(1)
55 logger.info("🔄 Starting safe update...")
57 def _get_git_remote(self, name: str = "origin") -> str | None:
58 try:
59 result = subprocess.run(
60 ["git", "-C", self.project_path, "remote", "get-url", name],
61 capture_output=True,
62 text=True,
63 check=True,
64 )
65 print(result.stdout.strip())
66 return result.stdout.strip()
67 except subprocess.CalledProcessError:
68 return None
70 def _save_metadata(self, filename: str, data: Any) -> None:
71 os.makedirs(self.kwasa_meta, exist_ok=True)
72 with open(os.path.join(self.kwasa_meta, filename), "w") as f:
73 json.dump(data, f)
75 def _stage_untracked_files(self) -> None:
76 try:
77 result = subprocess.run(
78 ["git", "ls-files", "--others", "--exclude-standard"],
79 capture_output=True,
80 text=True,
81 check=True,
82 )
83 untracked_files = result.stdout.splitlines()
85 if untracked_files:
86 logger.warning(f"⚠️ Untracked files: {untracked_files}")
87 subprocess.run(["git", "add"] + untracked_files, check=True)
88 subprocess.run(
89 ["git", "commit", "-m", "updating untracked files"], check=True
90 )
91 except subprocess.CalledProcessError as e:
92 logger.warning(f"⚠️ Could not stage untracked files: {e}")
94 def _swap_origin_and_add_template(self) -> None:
95 try:
96 if self.user_remote:
97 subprocess.run(
98 ["git", "remote", "rename", "origin", "user"], check=True
99 )
101 result = subprocess.run(
102 ["git", "remote", "get-url", "template"], capture_output=True, text=True
103 )
104 if result.returncode == 0:
105 subprocess.run(["git", "remote", "remove", "template"], check=True)
106 logger.info("🧹 Removed existing 'template' remote.")
108 subprocess.run(
109 ["git", "remote", "add", "template", self.template_repo], check=True
110 )
111 subprocess.run(["git", "fetch", "template"], check=True)
112 except subprocess.CalledProcessError as e:
113 logger.error(f"❌ Remote setup failed. {e}")
114 sys.exit(1)
116 def _fetch_and_merge_template(self) -> None:
117 try:
118 # Stash local changes before merge
119 subprocess.run(["git", "stash", "--include-untracked"], check=True)
121 # Merge from template remote
122 subprocess.run(
123 [
124 "git",
125 "merge",
126 "template/main",
127 "--allow-unrelated-histories",
128 "--no-edit",
129 "--strategy-option=theirs",
130 ],
131 check=True,
132 )
134 # Pop stashed changes
135 stash_list = subprocess.run(
136 ["git", "stash", "list"], capture_output=True, text=True
137 )
138 if stash_list.stdout.strip():
139 try:
140 subprocess.run(["git", "stash", "pop"], check=True)
141 logger.info(
142 "✅ Merged updates and restored local changes from stash."
143 )
144 except subprocess.CalledProcessError:
145 logger.warning(
146 "⚠️ Stash pop failed. You may need to resolve conflicts manually."
147 )
148 logger.warning(
149 " Run: `git stash list` then `git stash apply` to retry."
150 )
151 else:
152 logger.info("✅ Merged updates; no stash to pop.")
154 except subprocess.CalledProcessError as e:
155 if e.stderr and "unmerged files" in e.stderr:
156 logger.error("❌ Merge conflicts detected. Resolve manually.")
157 with suppress(Exception):
158 subprocess.run(["git", "merge", "--abort"], check=True)
159 else:
160 logger.error(f"❌ Merge failed. {e}")
161 sys.exit(1)
163 def _cleanup_and_re_init_git(self) -> None:
164 try:
165 subprocess.run(["git", "remote", "remove", "template"], check=True)
166 except subprocess.CalledProcessError:
167 logger.warning("⚠️ Failed to remove template remote.")
169 with suppress(Exception):
170 shutil.rmtree(".git", ignore_errors=True)
171 logger.info("🧹 Removed .git directory.")
173 with suppress(Exception):
174 self.manage_local_repository(True)
176 def _restore_template_metadata(self) -> None:
177 try:
178 self._save_metadata("metadata.json", {"origin": self.template_repo})
179 logger.info(f"🔁 Restored and saved remote: {self.user_remote}")
180 except subprocess.CalledProcessError as e:
181 logger.error(f"❌ Failed to restore original remote. {e}")
183 def _install_dependencies(self) -> None:
184 self.install_node_dependencies()
185 self.install_python_dependencies()