je-editor 0.0.215__py3-none-any.whl → 0.0.217__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. je_editor/code_scan/watchdog_thread.py +1 -1
  2. je_editor/git_client/__init__.py +0 -0
  3. je_editor/git_client/commit_graph.py +88 -0
  4. je_editor/git_client/git.py +181 -0
  5. je_editor/git_client/git_cli.py +66 -0
  6. je_editor/git_client/github.py +50 -0
  7. je_editor/pyside_ui/browser/browser_view.py +0 -3
  8. je_editor/pyside_ui/git_ui/__init__.py +0 -0
  9. je_editor/pyside_ui/git_ui/commit_table.py +27 -0
  10. je_editor/pyside_ui/git_ui/git_branch_tree_widget.py +119 -0
  11. je_editor/pyside_ui/git_ui/git_client_gui.py +291 -0
  12. je_editor/pyside_ui/git_ui/graph_view.py +174 -0
  13. je_editor/pyside_ui/main_ui/ai_widget/__init__.py +0 -0
  14. je_editor/pyside_ui/main_ui/ai_widget/ai_config.py +19 -0
  15. je_editor/pyside_ui/main_ui/ai_widget/ask_thread.py +17 -0
  16. je_editor/pyside_ui/main_ui/ai_widget/chat_ui.py +130 -0
  17. je_editor/pyside_ui/main_ui/ai_widget/langchain_interface.py +45 -0
  18. je_editor/pyside_ui/main_ui/main_editor.py +1 -1
  19. je_editor/pyside_ui/main_ui/menu/check_style_menu/__init__.py +0 -0
  20. je_editor/pyside_ui/main_ui/menu/check_style_menu/build_check_style_menu.py +81 -0
  21. je_editor/pyside_ui/main_ui/menu/dock_menu/build_dock_menu.py +2 -2
  22. je_editor/pyside_ui/main_ui/menu/tab_menu/build_tab_menu.py +2 -2
  23. je_editor/utils/multi_language/english.py +1 -1
  24. je_editor/utils/multi_language/traditional_chinese.py +1 -1
  25. {je_editor-0.0.215.dist-info → je_editor-0.0.217.dist-info}/METADATA +2 -1
  26. {je_editor-0.0.215.dist-info → je_editor-0.0.217.dist-info}/RECORD +29 -12
  27. {je_editor-0.0.215.dist-info → je_editor-0.0.217.dist-info}/WHEEL +0 -0
  28. {je_editor-0.0.215.dist-info → je_editor-0.0.217.dist-info}/licenses/LICENSE +0 -0
  29. {je_editor-0.0.215.dist-info → je_editor-0.0.217.dist-info}/top_level.txt +0 -0
@@ -26,7 +26,7 @@ class WatchdogThread(threading.Thread):
26
26
 
27
27
 
28
28
  if __name__ == '__main__':
29
- watchdog_thread = WatchdogThread(".")
29
+ watchdog_thread = WatchdogThread("")
30
30
  watchdog_thread.start()
31
31
  while True:
32
32
  time.sleep(1)
File without changes
@@ -0,0 +1,88 @@
1
+ import logging
2
+ from dataclasses import dataclass, field
3
+ from typing import List, Dict
4
+
5
+ log = logging.getLogger(__name__)
6
+
7
+
8
+ @dataclass
9
+ class CommitNode:
10
+ commit_sha: str
11
+ author_name: str
12
+ commit_date: str
13
+ commit_message: str
14
+ parent_shas: List[str]
15
+ lane_index: int = -1 # assigned later
16
+
17
+ @dataclass
18
+ class CommitGraph:
19
+ nodes: List[CommitNode] = field(default_factory=list)
20
+ index: Dict[str, int] = field(default_factory=dict) # sha -> row
21
+
22
+ def build(self, commits: List[Dict], refs: Dict[str, str]) -> None:
23
+ # commits are topo-ordered by git_client log --topo-order; we keep it.
24
+ self.nodes = [
25
+ CommitNode(
26
+ commit_sha=c["sha"],
27
+ author_name=c["author"],
28
+ commit_date=c["date"],
29
+ commit_message=c["message"],
30
+ parent_shas=c["parents"],
31
+ ) for c in commits
32
+ ]
33
+ self.index = {n.commit_sha: i for i, n in enumerate(self.nodes)}
34
+ self._assign_lanes()
35
+
36
+ def _assign_lanes(self) -> None:
37
+ """
38
+ Simple lane assignment similar to 'git_client log --graph' lanes.
39
+ Greedy: reuse freed lanes; parents may create new lanes.
40
+ """
41
+ active: Dict[int, str] = {} # lane -> sha
42
+ free_lanes: List[int] = []
43
+
44
+ for i, node in enumerate(self.nodes):
45
+ # If any active lane points to this commit, use that lane
46
+ lane_found = None
47
+ for lane, sha in list(active.items()):
48
+ if sha == node.commit_sha:
49
+ lane_found = lane
50
+ break
51
+
52
+ if lane_found is None:
53
+ if free_lanes:
54
+ node.lane_index = free_lanes.pop(0)
55
+ else:
56
+ node.lane_index = 0 if not active else max(active.keys()) + 1
57
+ else:
58
+ node.lane_index = lane_found
59
+
60
+ # Update active: current node consumes its lane, parents occupy lanes
61
+ # Remove the current sha from any lane that pointed to it
62
+ for lane, sha in list(active.items()):
63
+ if sha == node.commit_sha:
64
+ del active[lane]
65
+
66
+ # First parent continues in the same lane; others go to free/new lanes
67
+ if node.parent_shas:
68
+ first = node.parent_shas[0]
69
+ active[node.lane_index] = first
70
+ # Side branches
71
+ for p in node.parent_shas[1:]:
72
+ # Pick a free lane or new one
73
+ if free_lanes:
74
+ pl = free_lanes.pop(0)
75
+ else:
76
+ pl = 0 if not active else max(active.keys()) + 1
77
+ active[pl] = p
78
+
79
+ # Any lane whose target no longer appears in the future will be freed later
80
+ # We approximate by freeing lanes when a target didn't appear in the next rows;
81
+ # but for minimal viable, free when lane not reassigned by parents this row.
82
+ used_lanes = set(active.keys())
83
+ # Collect gaps below max lane as free lanes to reuse
84
+ max_lane = max(used_lanes) if used_lanes else -1
85
+ present = set(range(max_lane + 1))
86
+ missing = sorted(list(present - used_lanes))
87
+ # Merge missing into free_lanes maintaining order
88
+ free_lanes = sorted(set(free_lanes + missing))
@@ -0,0 +1,181 @@
1
+ import os
2
+ from datetime import datetime
3
+
4
+ from PySide6.QtCore import QThread, Signal
5
+ from git import Repo, GitCommandError, InvalidGitRepositoryError, NoSuchPathError
6
+
7
+
8
+ # -----------------------
9
+ # Simple audit logger
10
+ # -----------------------
11
+ def audit_log(repo_path: str, action: str, detail: str, ok: bool, err: str = ""):
12
+ """
13
+ Append an audit log entry to 'audit.log' in the repo directory.
14
+ This is useful for compliance and traceability.
15
+ """
16
+ try:
17
+ path = os.path.join(repo_path if repo_path else ".", "audit.log")
18
+ with open(path, "a", encoding="utf-8") as f:
19
+ ts = datetime.now().isoformat(timespec="seconds")
20
+ f.write(f"{ts}\taction={action}\tok={ok}\tdetail={detail}\terr={err}\n")
21
+ except Exception:
22
+ pass # Never let audit logging failure break the UI
23
+
24
+
25
+ # -----------------------
26
+ # Git service layer
27
+ # -----------------------
28
+ class GitService:
29
+ """
30
+ Encapsulates Git operations using GitPython.
31
+ Keeps UI logic separate from Git logic.
32
+ """
33
+
34
+ def __init__(self):
35
+ self.repo: Repo | None = None
36
+ self.repo_path: str | None = None
37
+
38
+ def open_repo(self, path: str):
39
+ try:
40
+ self.repo = Repo(path)
41
+ self.repo_path = path
42
+ audit_log(path, "open_repo", path, True)
43
+ except (InvalidGitRepositoryError, NoSuchPathError) as e:
44
+ audit_log(path, "open_repo", path, False, str(e))
45
+ raise
46
+
47
+ def list_branches(self):
48
+ self._ensure_repo()
49
+ branches = [head.name for head in self.repo.heads]
50
+ audit_log(self.repo_path, "list_branches", ",".join(branches), True)
51
+ return branches
52
+
53
+ def current_branch(self):
54
+ self._ensure_repo()
55
+ try:
56
+ return self.repo.active_branch.name
57
+ except TypeError:
58
+ return "(detached HEAD)"
59
+
60
+ def checkout(self, branch: str):
61
+ self._ensure_repo()
62
+ try:
63
+ self.repo.git.checkout(branch)
64
+ audit_log(self.repo_path, "checkout", branch, True)
65
+ except GitCommandError as e:
66
+ audit_log(self.repo_path, "checkout", branch, False, str(e))
67
+ raise
68
+
69
+ def list_commits(self, branch: str, max_count: int = 100):
70
+ self._ensure_repo()
71
+ commits = list(self.repo.iter_commits(branch, max_count=max_count))
72
+ data = [
73
+ {
74
+ "hexsha": c.hexsha,
75
+ "summary": c.summary,
76
+ "author": c.author.name if c.author else "",
77
+ "date": datetime.fromtimestamp(c.committed_date).isoformat(sep=" ", timespec="seconds"),
78
+ }
79
+ for c in commits
80
+ ]
81
+ audit_log(self.repo_path, "list_commits", f"{branch}:{len(data)}", True)
82
+ return data
83
+
84
+ def show_diff_of_commit(self, commit_sha: str) -> str:
85
+ self._ensure_repo()
86
+ commit = self.repo.commit(commit_sha)
87
+ parent = commit.parents[0] if commit.parents else None
88
+ if parent is None:
89
+ null_tree = self.repo.tree(NULL_TREE)
90
+ diffs = commit.diff(null_tree, create_patch=True)
91
+ else:
92
+ diffs = commit.diff(parent, create_patch=True)
93
+ text = []
94
+ for d in diffs:
95
+ try:
96
+ text.append(d.diff.decode("utf-8", errors="replace"))
97
+ except Exception:
98
+ pass
99
+ out = "".join(text) if text else "(No patch content)"
100
+ audit_log(self.repo_path, "show_diff", commit_sha, True)
101
+ return out
102
+
103
+ def stage_all(self):
104
+ self._ensure_repo()
105
+ try:
106
+ self.repo.git.add(all=True)
107
+ audit_log(self.repo_path, "stage_all", "git_client add -A", True)
108
+ except GitCommandError as e:
109
+ audit_log(self.repo_path, "stage_all", "git_client add -A", False, str(e))
110
+ raise
111
+
112
+ def commit(self, message: str):
113
+ self._ensure_repo()
114
+ if not message.strip():
115
+ raise ValueError("Commit message is empty.")
116
+ try:
117
+ self.repo.index.commit(message)
118
+ audit_log(self.repo_path, "commit", message, True)
119
+ except Exception as e:
120
+ audit_log(self.repo_path, "commit", message, False, str(e))
121
+ raise
122
+
123
+ def pull(self, remote: str = "origin", branch: str | None = None):
124
+ self._ensure_repo()
125
+ if branch is None:
126
+ branch = self.current_branch()
127
+ try:
128
+ res = self.repo.git.pull(remote, branch)
129
+ audit_log(self.repo_path, "pull", f"{remote}/{branch}", True)
130
+ return res
131
+ except GitCommandError as e:
132
+ audit_log(self.repo_path, "pull", f"{remote}/{branch}", False, str(e))
133
+ raise
134
+
135
+ def push(self, remote: str = "origin", branch: str | None = None):
136
+ self._ensure_repo()
137
+ if branch is None:
138
+ branch = self.current_branch()
139
+ try:
140
+ res = self.repo.git.push(remote, branch)
141
+ audit_log(self.repo_path, "push", f"{remote}/{branch}", True)
142
+ return res
143
+ except GitCommandError as e:
144
+ audit_log(self.repo_path, "push", f"{remote}/{branch}", False, str(e))
145
+ raise
146
+
147
+ def remotes(self):
148
+ self._ensure_repo()
149
+ return [r.name for r in self.repo.remotes]
150
+
151
+ def _ensure_repo(self):
152
+ if self.repo is None:
153
+ raise RuntimeError("Repository not opened.")
154
+
155
+
156
+ # Null tree constant for initial commit diff
157
+ NULL_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
158
+
159
+
160
+ # -----------------------
161
+ # Worker thread wrapper
162
+ # -----------------------
163
+ class Worker(QThread):
164
+ """
165
+ Runs a function in a separate thread to avoid blocking the UI.
166
+ Emits (result, error) when done.
167
+ """
168
+ done = Signal(object, object)
169
+
170
+ def __init__(self, fn, *args, **kwargs):
171
+ super().__init__()
172
+ self.fn = fn
173
+ self.args = args
174
+ self.kwargs = kwargs
175
+
176
+ def run(self):
177
+ try:
178
+ res = self.fn(*self.args, **self.kwargs)
179
+ self.done.emit(res, None)
180
+ except Exception as e:
181
+ self.done.emit(None, e)
@@ -0,0 +1,66 @@
1
+ import logging
2
+ import subprocess
3
+ from pathlib import Path
4
+ from typing import List, Dict
5
+
6
+ log = logging.getLogger(__name__)
7
+
8
+
9
+ class GitCLI:
10
+ def __init__(self, repo_path: Path):
11
+ self.repo_path = Path(repo_path)
12
+
13
+ def is_git_repo(self) -> bool:
14
+ return (self.repo_path / ".git_client").exists()
15
+
16
+ def _run(self, args: List[str]) -> str:
17
+ log.debug("git_client %s", " ".join(args))
18
+ res = subprocess.run(
19
+ ["git_client"] + args,
20
+ cwd=self.repo_path,
21
+ stdout=subprocess.PIPE,
22
+ stderr=subprocess.PIPE,
23
+ text=True,
24
+ encoding="utf-8",
25
+ )
26
+ if res.returncode != 0:
27
+ log.error("Git failed: %s", res.stderr.strip())
28
+ raise RuntimeError(res.stderr.strip())
29
+ return res.stdout
30
+
31
+ def get_all_refs(self) -> Dict[str, str]:
32
+ # returns refname -> commit hash
33
+ out = self._run(["show-ref", "--heads", "--tags"])
34
+ refs = {}
35
+ for line in out.splitlines():
36
+ if not line.strip():
37
+ continue
38
+ sha, ref = line.split(" ", 1)
39
+ refs[ref.strip()] = sha.strip()
40
+ return refs
41
+
42
+ def get_commits(self, max_count: int = 500) -> List[Dict]:
43
+ """
44
+ Return recent commits across all refs, with parents.
45
+ """
46
+ fmt = "%H%x01%P%x01%an%x01%ad%x01%s"
47
+ out = self._run([
48
+ "log", "--date=short", f"--format={fmt}", "--all",
49
+ f"--max-count={max_count}", "--topo-order"
50
+ ])
51
+ commits = []
52
+ for line in out.splitlines():
53
+ if not line.strip():
54
+ continue
55
+ parts = line.split("\x01")
56
+ if len(parts) != 5:
57
+ continue
58
+ sha, parents, author, date, msg = parts
59
+ commits.append({
60
+ "sha": sha,
61
+ "parents": [p for p in parents.strip().split() if p],
62
+ "author": author,
63
+ "date": date,
64
+ "message": msg,
65
+ })
66
+ return commits
@@ -0,0 +1,50 @@
1
+ import datetime
2
+ import traceback
3
+
4
+ from git import Repo, GitCommandError, InvalidGitRepositoryError, NoSuchPathError
5
+
6
+
7
+ class GitCloneHandler:
8
+ """
9
+ Handles cloning of remote Git repositories with audit logging.
10
+ Can be reused in UI or CLI contexts.
11
+ """
12
+
13
+ def __init__(self, audit_log_path: str = "git_clone_audit.log"):
14
+ self.audit_log_path = audit_log_path
15
+
16
+ def clone_repo(self, remote_url: str, local_path: str) -> str:
17
+ """
18
+ Clone a remote repository to a local path.
19
+
20
+ :param remote_url: The Git repository URL (e.g., https://github.com/user/repo.git)
21
+ :param local_path: The local directory to clone into
22
+ :return: The path to the cloned repository
23
+ :raises: Exception if cloning fails
24
+ """
25
+ try:
26
+ self._log_audit(f"Cloning started: {remote_url} -> {local_path}")
27
+ Repo.clone_from(remote_url, local_path)
28
+ self._log_audit(f"Cloning completed: {remote_url} -> {local_path}")
29
+ return local_path
30
+ except (GitCommandError, InvalidGitRepositoryError, NoSuchPathError) as e:
31
+ self._log_audit(
32
+ f"ERROR: Git operation failed: {remote_url} -> {local_path}\n{str(e)}\nTraceback:\n{traceback.format_exc()}")
33
+ raise RuntimeError(f"Git operation failed: {str(e)}") from e
34
+ except Exception as e:
35
+ self._log_audit(
36
+ f"ERROR: Unexpected error during clone: {remote_url} -> {local_path}\n{str(e)}\nTraceback:\n{traceback.format_exc()}")
37
+ raise RuntimeError(f"Unexpected error during clone: {str(e)}") from e
38
+
39
+ def _log_audit(self, message: str):
40
+ """
41
+ Append an audit log entry with timestamp.
42
+ """
43
+ timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
44
+ log_entry = f"[{timestamp}] {message}\n"
45
+ try:
46
+ with open(self.audit_log_path, "a", encoding="utf-8") as f:
47
+ f.write(log_entry)
48
+ except Exception:
49
+ # Never let audit logging failure break the flow
50
+ pass
@@ -1,8 +1,5 @@
1
- import pathlib
2
1
  from typing import List
3
2
 
4
- from PySide6.QtCore import Qt
5
- from PySide6.QtNetwork import QNetworkCookie
6
3
  from PySide6.QtWebEngineCore import QWebEngineDownloadRequest
7
4
  from PySide6.QtWebEngineWidgets import QWebEngineView
8
5
 
File without changes
@@ -0,0 +1,27 @@
1
+ from PySide6.QtGui import QStandardItemModel, QStandardItem
2
+ from PySide6.QtWidgets import QTableView
3
+
4
+
5
+ class CommitTable(QTableView):
6
+ def __init__(self, parent=None):
7
+ super().__init__(parent)
8
+ self.model_data = QStandardItemModel(0, 5, self) # 多一欄
9
+ self.model_data.setHorizontalHeaderLabels(["#", "SHA", "Message", "Author", "Date"])
10
+ self.setModel(self.model_data)
11
+ self.setSelectionBehavior(QTableView.SelectionBehavior.SelectRows)
12
+ self.setEditTriggers(QTableView.EditTrigger.NoEditTriggers)
13
+ self.horizontalHeader().setStretchLastSection(True)
14
+
15
+ def set_commits(self, commits):
16
+ self.model_data.setRowCount(0)
17
+ for idx, c in enumerate(commits, start=1):
18
+ row = [
19
+ QStandardItem(str(idx)), # 行號
20
+ QStandardItem(c["sha"][:7]),
21
+ QStandardItem(c["message"]),
22
+ QStandardItem(c["author"]),
23
+ QStandardItem(c["date"]),
24
+ ]
25
+ for item in row:
26
+ item.setEditable(False)
27
+ self.model_data.appendRow(row)
@@ -0,0 +1,119 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ from PySide6.QtCore import QTimer, QFileSystemWatcher, Qt
5
+ from PySide6.QtGui import QAction
6
+ from PySide6.QtWidgets import (
7
+ QFileDialog, QToolBar, QMessageBox, QStatusBar,
8
+ QSplitter, QWidget, QVBoxLayout
9
+ )
10
+
11
+ from je_editor.git_client.commit_graph import CommitGraph
12
+ from je_editor.git_client.git_cli import GitCLI
13
+ from je_editor.pyside_ui.git_ui.commit_table import CommitTable
14
+ from je_editor.pyside_ui.git_ui.graph_view import CommitGraphView
15
+ from je_editor.utils.multi_language.multi_language_wrapper import language_wrapper
16
+
17
+ logging.basicConfig(level=logging.INFO)
18
+ log = logging.getLogger(__name__)
19
+
20
+
21
+ class GitTreeViewGUI(QWidget):
22
+ def __init__(self):
23
+ super().__init__()
24
+ self.language_wrapper_get = language_wrapper.language_word_dict.get
25
+ self.setWindowTitle(self.language_wrapper_get("git_graph_title"))
26
+
27
+ main_layout = QVBoxLayout(self)
28
+ main_layout.setContentsMargins(0, 0, 0, 0)
29
+ main_layout.setSpacing(0)
30
+
31
+ toolbar = QToolBar()
32
+ act_open = QAction(self.language_wrapper_get("git_graph_toolbar_open"), self)
33
+ act_open.triggered.connect(self.open_repo)
34
+ toolbar.addAction(act_open)
35
+
36
+ act_refresh = QAction(self.language_wrapper_get("git_graph_toolbar_refresh"), self)
37
+ act_refresh.triggered.connect(self.refresh_graph)
38
+ toolbar.addAction(act_refresh)
39
+
40
+ main_layout.addWidget(toolbar)
41
+
42
+ splitter = QSplitter(Qt.Orientation.Horizontal)
43
+ self.graph_view = CommitGraphView()
44
+ self.commit_table = CommitTable()
45
+ splitter.addWidget(self.graph_view)
46
+ splitter.addWidget(self.commit_table)
47
+ splitter.setSizes([600, 400])
48
+ main_layout.addWidget(splitter, 1) # 1 表示可伸縮
49
+
50
+ self.status = QStatusBar()
51
+ main_layout.addWidget(self.status)
52
+
53
+ self.repo_path = None
54
+ self.git = None
55
+
56
+ self.watcher = QFileSystemWatcher()
57
+ self.watcher.directoryChanged.connect(self._on_git_changed)
58
+ self.watcher.fileChanged.connect(self._on_git_changed)
59
+
60
+ self.refresh_timer = QTimer()
61
+ self.refresh_timer.setSingleShot(True)
62
+ self.refresh_timer.timeout.connect(self.refresh_graph)
63
+
64
+ self.commit_table.selectionModel().selectionChanged.connect(self._on_table_selection)
65
+
66
+ def _on_table_selection(self, selected, _):
67
+ if not selected.indexes():
68
+ return
69
+ row = selected.indexes()[0].row()
70
+ self.graph_view.focus_row(row)
71
+
72
+ def open_repo(self):
73
+ path = QFileDialog.getExistingDirectory(self, self.language_wrapper_get("git_graph_menu_open_repo"))
74
+ if not path:
75
+ return
76
+ repo_path = Path(path)
77
+ git = GitCLI(repo_path)
78
+ if not git.is_git_repo():
79
+ QMessageBox.warning(self, self.language_wrapper_get("git_graph_title"),
80
+ self.language_wrapper_get("git_graph_error_not_git"))
81
+ return
82
+ self.repo_path = repo_path
83
+ self.git = git
84
+ self._setup_watcher()
85
+ self.refresh_graph()
86
+
87
+ def _setup_watcher(self):
88
+ self.watcher.removePaths(self.watcher.files())
89
+ self.watcher.removePaths(self.watcher.directories())
90
+ if not self.repo_path:
91
+ return
92
+ git_dir = self.repo_path / ".git_client"
93
+ if git_dir.exists():
94
+ self.watcher.addPath(str(git_dir))
95
+ for f in ["HEAD", "packed-refs"]:
96
+ fp = git_dir / f
97
+ if fp.exists():
98
+ self.watcher.addPath(str(fp))
99
+ refs_dir = git_dir / "refs"
100
+ if refs_dir.exists():
101
+ self.watcher.addPath(str(refs_dir))
102
+
103
+ def _on_git_changed(self, path):
104
+ self.refresh_timer.start(500)
105
+
106
+ def refresh_graph(self):
107
+ if not self.git:
108
+ return
109
+ try:
110
+ refs = self.git.get_all_refs()
111
+ commits = self.git.get_commits(max_count=500)
112
+ graph = CommitGraph()
113
+ graph.build(commits, refs)
114
+ self.graph_view.set_graph(graph)
115
+ self.commit_table.set_commits(commits)
116
+ except Exception as e:
117
+ QMessageBox.critical(self, self.language_wrapper_get("git_graph_title"),
118
+ f"{self.language_wrapper_get('git_graph_error_exec_failed')}\n{e}")
119
+