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

1import json 

2import os 

3import shutil 

4import subprocess 

5import sys 

6from contextlib import suppress 

7from typing import Any 

8 

9from kwasa.libs.helper import HelperUtility 

10from kwasa.logger.log import get_logger 

11 

12logger = get_logger("update") 

13 

14 

15class GitRepoUpdater(HelperUtility): 

16 """ 

17 Handles safe update of a cloned project using metadata from the template repo. 

18 """ 

19 

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 

26 

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() 

36 

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) 

41 

42 if not os.path.exists(self.metadata_path): 

43 logger.error("❌ No metadata found. Use `kwasa clone` to initialize.") 

44 sys.exit(1) 

45 

46 def _load_template_metadata(self) -> None: 

47 with open(self.metadata_path) as f: 

48 meta = json.load(f) 

49 

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) 

54 

55 logger.info("🔄 Starting safe update...") 

56 

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 

69 

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) 

74 

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() 

84 

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}") 

93 

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 ) 

100 

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.") 

107 

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) 

115 

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) 

120 

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 ) 

133 

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.") 

153 

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) 

162 

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.") 

168 

169 with suppress(Exception): 

170 shutil.rmtree(".git", ignore_errors=True) 

171 logger.info("🧹 Removed .git directory.") 

172 

173 with suppress(Exception): 

174 self.manage_local_repository(True) 

175 

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}") 

182 

183 def _install_dependencies(self) -> None: 

184 self.install_node_dependencies() 

185 self.install_python_dependencies()