Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions je_editor/git_client/file_staging.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,85 @@ def stage_content(file_path: str | Path, content: str) -> bool:
jeditor_logger.error(f"file_staging: could not stage {file_path}: {error!r}")
return False
return True


def unstage_file(file_path: str | Path) -> bool:
"""
把一個檔案的暫存內容還原成 HEAD 的版本
Put a file's staged content back to the way HEAD has it.

這是「取消暫存」:索引回到已提交的內容,工作區的檔案完全不動,因此使用者正在
編輯的東西不會受影響。暫存了不該暫存的一段之後,這是唯一能收回的方法。
This is unstaging: the index goes back to the committed content while the file
on disk is left exactly as it is, so nothing the user is editing is touched.
After staging a hunk that should not have been staged, this is the only way
back.

尚未提交過的新檔案沒有 HEAD 版本可回去,改為從索引移除。
A new file has no committed version to return to, so it is removed from the
index instead.

:param file_path: 檔案路徑 / the file to unstage
:return: 索引有變時為 ``True`` / ``True`` when the index changed
"""
repo = open_repository(file_path)
if repo is None:
return False
with repo:
relative = _relative_to(repo, file_path)
if relative is None:
return False
try:
blob = repo.head.commit.tree / relative
except (KeyError, ValueError, TypeError, AttributeError, GitError):
return _remove_from_index(repo, relative, file_path)
try:
repo.index.add([IndexEntry.from_blob(
Blob(repo, blob.binsha, _FILE_MODE, relative))])
repo.index.write()
except (ValueError, TypeError, AttributeError, GitError, OSError) as error:
jeditor_logger.error(f"file_staging: could not unstage {file_path}: {error!r}")
return False
return True


def commit_index(file_path: str | Path, message: str) -> bool:
"""
把索引目前的內容提交出去
Commit whatever the index currently holds.

提交的是索引而不是磁碟上的檔案,因此逐段暫存之後可以只提交挑好的那幾段,還沒
暫存的修改留在工作區。
What is committed is the index rather than the files on disk, so after staging
hunk by hunk only the chosen parts go in and the rest stays in the working
tree.

:param file_path: 儲存庫中的任一個檔案 / any file in the repository
:param message: 提交訊息 / the commit message
:return: 有提交時為 ``True`` / ``True`` when a commit was made
"""
if not message.strip():
return False
repo = open_repository(file_path)
if repo is None:
return False
with repo:
try:
repo.index.commit(message.strip())
except (ValueError, TypeError, AttributeError, GitError, OSError) as error:
jeditor_logger.error(f"file_staging: could not commit: {error!r}")
return False
return True


def _remove_from_index(repo, relative: str, file_path: str | Path) -> bool:
"""把一個還沒提交過的檔案從索引移除 / Drop a never-committed file from the index."""
try:
if (relative, 0) not in repo.index.entries:
return False
repo.index.remove([relative])
repo.index.write()
except (KeyError, ValueError, TypeError, AttributeError, GitError, OSError) as error:
jeditor_logger.error(f"file_staging: could not unstage {file_path}: {error!r}")
return False
return True
96 changes: 96 additions & 0 deletions je_editor/git_client/git_action.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,102 @@ def remotes(self) -> list[str]:
self._ensure_repo()
return [r.name for r in self.repo.remotes]

def stash_save(self, message: str = "") -> str:
"""
把目前的修改收進 stash
Put the current changes away in a stash.

要暫時放下手邊的東西去改別的地方時用這個;內容留在 stash 裡,之後可以取回。
This is for putting work down to deal with something else; the changes
stay in the stash and can be taken back afterwards.

:param message: 這次 stash 的說明 / a note describing the stash
:return: git 的輸出 / what git printed
"""
self._ensure_repo()
arguments = ["push"] + (["-m", message] if message.strip() else [])
try:
result = self.repo.git.stash(*arguments)
audit_log(self.repo_path, "stash_save", message, True)
return result
except GitCommandError as error:
audit_log(self.repo_path, "stash_save", message, False, str(error))
raise

def stash_list(self) -> list[str]:
"""
列出目前收著的 stash
The stashes currently put away.

:return: 每個 stash 的說明 / a description of each
"""
self._ensure_repo()
output = self.repo.git.stash("list")
return [line for line in output.splitlines() if line.strip()]

def stash_pop(self, index: int = 0) -> str:
"""
取回一個 stash,並把它從清單移除
Take a stash back, removing it from the list.

:param index: 要取回的 stash 編號 / which stash to take back
:return: git 的輸出 / what git printed
"""
self._ensure_repo()
reference = f"stash@{{{max(0, index)}}}"
try:
result = self.repo.git.stash("pop", reference)
audit_log(self.repo_path, "stash_pop", reference, True)
return result
except GitCommandError as error:
audit_log(self.repo_path, "stash_pop", reference, False, str(error))
raise

def conflicted_files(self) -> list[str]:
"""
列出目前處於衝突狀態的檔案
The files currently left in conflict.

合併或 pull 之後,這些檔案裡有需要人來決定的部分。
After a merge or a pull, these hold the parts someone has to decide about.

:return: 相對於儲存庫根目錄的路徑 / paths relative to the repository root
"""
self._ensure_repo()
# 索引裡同一個路徑有多個 stage 就代表衝突:2 是我們的版本,3 是對方的
# A path with more than one stage in the index is in conflict: 2 is ours
# and 3 is theirs
return sorted({
path for path, stage in self.repo.index.entries.keys() if stage != 0
})

def resolve_conflict(self, file_path: str, keep: str = "ours") -> bool:
"""
以某一邊的內容解決一個檔案的衝突
Resolve a file's conflict by keeping one side of it.

:param file_path: 相對於儲存庫根目錄的路徑 / the path, relative to the root
:param keep: ``ours`` 保留自己這邊,``theirs`` 保留對方 / which side to keep
:return: 有解決時為 ``True`` / ``True`` when it was resolved
"""
self._ensure_repo()
if keep not in ("ours", "theirs"):
return False
# 沒有衝突的檔案也吃得下 ``checkout --ours``,但那只會把它默默加進索引——
# 呼叫端要的是解決衝突,不是暫存一個沒事的檔案
# ``checkout --ours`` is accepted for a file with no conflict too, but all
# that does is quietly stage it, which is not what resolving asks for
if file_path not in self.conflicted_files():
return False
try:
self.repo.git.checkout(f"--{keep}", "--", file_path)
self.repo.git.add(file_path)
audit_log(self.repo_path, "resolve_conflict", f"{file_path} {keep}", True)
except GitCommandError as error:
audit_log(self.repo_path, "resolve_conflict", file_path, False, str(error))
return False
return True

def _ensure_repo(self) -> None:
if self.repo is None:
raise RuntimeError("Repository not opened.")
Expand Down
17 changes: 13 additions & 4 deletions je_editor/pyside_ui/code/folding/folding_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
"""
from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING

from je_editor.utils.code_folding.fold_regions import (
FoldRegion, compute_fold_regions, region_at_line
)
from je_editor.utils.code_folding.brace_regions import fold_regions_for
from je_editor.utils.code_folding.fold_regions import FoldRegion, region_at_line
from je_editor.utils.logging.loggin_instance import jeditor_logger

if TYPE_CHECKING:
Expand Down Expand Up @@ -44,10 +44,19 @@ def compute_regions(self) -> list[FoldRegion]:
依目前文字計算所有可折疊區塊
Compute every foldable region for the current text.

折疊方式依語言而定:以大括號劃分區塊的語言看括號配對,其餘看縮排。
How the regions are found depends on the language: brace-delimited ones
follow their pairs, and everything else follows indentation.

:return: 可折疊區塊清單 / The list of foldable regions
"""
text = self._editor.toPlainText()
return compute_fold_regions(text.split("\n"))
return fold_regions_for(text.split("\n"), self._suffix())

def _suffix(self) -> str:
"""目前檔案的副檔名 / The current file's suffix."""
current = getattr(self._editor, "current_file", None)
return Path(str(current)).suffix if current else ""

def foldable_header_lines(self) -> set[int]:
"""
Expand Down
Loading
Loading