From 99d1d69d4d68c452fb02bd4b9275929dbc4c6fde Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 13 Oct 2025 16:25:35 +0800 Subject: [PATCH 01/72] ux: main tab bar style Signed-off-by: leo --- src/Resources/Themes.axaml | 2 +- src/Views/LauncherTabBar.axaml | 11 ++++++----- src/Views/LauncherTabBar.axaml.cs | 15 +++++++++++++-- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/Resources/Themes.axaml b/src/Resources/Themes.axaml index 3b4637331..b5a0d61fa 100644 --- a/src/Resources/Themes.axaml +++ b/src/Resources/Themes.axaml @@ -32,7 +32,7 @@ #FF252525 #FF444444 #FF1F1F1F - #FF2C2C2C + #FF2F2F2F #FF2B2B2B #FF1C1C1C #FF8F8F8F diff --git a/src/Views/LauncherTabBar.axaml b/src/Views/LauncherTabBar.axaml index 7ea6bd400..74cda1daf 100644 --- a/src/Views/LauncherTabBar.axaml +++ b/src/Views/LauncherTabBar.axaml @@ -42,7 +42,8 @@ - - + @@ -129,7 +130,7 @@ - @@ -107,7 +116,7 @@ HorizontalAlignment="Center" VerticalAlignment="Center" IsVisible="{Binding IsBinary}"> - + From 6e70b7421277ef87707eca31c493a812be63e595 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 20 Oct 2025 03:57:13 +0000 Subject: [PATCH 31/72] doc: Update translation status and sort locale files --- TRANSLATION.md | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index dff27b046..ca9c88be8 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -6,11 +6,12 @@ This document shows the translation status of each locale file in the repository ### ![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen) -### ![de__DE](https://img.shields.io/badge/de__DE-99.44%25-yellow) +### ![de__DE](https://img.shields.io/badge/de__DE-99.33%25-yellow)
Missing keys in de_DE.axaml +- Text.Blame.BlameOnPreviousRevision - Text.BranchCM.CreatePR - Text.BranchCM.CreatePRForUpstream - Text.Configure.CommitMessageTemplate.BuiltinVars @@ -19,11 +20,12 @@ This document shows the translation status of each locale file in the repository
-### ![es__ES](https://img.shields.io/badge/es__ES-99.44%25-yellow) +### ![es__ES](https://img.shields.io/badge/es__ES-99.33%25-yellow)
Missing keys in es_ES.axaml +- Text.Blame.BlameOnPreviousRevision - Text.BranchCM.CreatePR - Text.BranchCM.CreatePRForUpstream - Text.Configure.CommitMessageTemplate.BuiltinVars @@ -32,7 +34,7 @@ This document shows the translation status of each locale file in the repository
-### ![fr__FR](https://img.shields.io/badge/fr__FR-77.40%25-yellow) +### ![fr__FR](https://img.shields.io/badge/fr__FR-77.32%25-yellow)
Missing keys in fr_FR.axaml @@ -52,6 +54,7 @@ This document shows the translation status of each locale file in the repository - Text.Bisect.Good - Text.Bisect.Skip - Text.Bisect.WaitingForRange +- Text.Blame.BlameOnPreviousRevision - Text.BranchCM.CreatePR - Text.BranchCM.CreatePRForUpstream - Text.BranchCM.ResetToSelectedCommit @@ -242,12 +245,13 @@ This document shows the translation status of each locale file in the repository
-### ![id__ID](https://img.shields.io/badge/id__ID-98.88%25-yellow) +### ![id__ID](https://img.shields.io/badge/id__ID-98.77%25-yellow)
Missing keys in id_ID.axaml - Text.About.ReleaseNotes +- Text.Blame.BlameOnPreviousRevision - Text.BranchCM.CreatePR - Text.BranchCM.CreatePRForUpstream - Text.CommitCM.Drop @@ -260,12 +264,13 @@ This document shows the translation status of each locale file in the repository
-### ![it__IT](https://img.shields.io/badge/it__IT-96.09%25-yellow) +### ![it__IT](https://img.shields.io/badge/it__IT-95.98%25-yellow)
Missing keys in it_IT.axaml - Text.About.ReleaseNotes +- Text.Blame.BlameOnPreviousRevision - Text.BranchCM.CreatePR - Text.BranchCM.CreatePRForUpstream - Text.BranchCM.SwitchToWorktree @@ -303,7 +308,7 @@ This document shows the translation status of each locale file in the repository
-### ![ja__JP](https://img.shields.io/badge/ja__JP-77.40%25-yellow) +### ![ja__JP](https://img.shields.io/badge/ja__JP-77.32%25-yellow)
Missing keys in ja_JP.axaml @@ -323,6 +328,7 @@ This document shows the translation status of each locale file in the repository - Text.Bisect.Good - Text.Bisect.Skip - Text.Bisect.WaitingForRange +- Text.Blame.BlameOnPreviousRevision - Text.BranchCM.CompareWithCurrent - Text.BranchCM.CreatePR - Text.BranchCM.CreatePRForUpstream @@ -513,7 +519,7 @@ This document shows the translation status of each locale file in the repository
-### ![pt__BR](https://img.shields.io/badge/pt__BR-71.03%25-red) +### ![pt__BR](https://img.shields.io/badge/pt__BR-70.95%25-red)
Missing keys in pt_BR.axaml @@ -539,6 +545,7 @@ This document shows the translation status of each locale file in the repository - Text.Bisect.Good - Text.Bisect.Skip - Text.Bisect.WaitingForRange +- Text.Blame.BlameOnPreviousRevision - Text.BranchCM.CreatePR - Text.BranchCM.CreatePRForUpstream - Text.BranchCM.CustomAction @@ -780,9 +787,16 @@ This document shows the translation status of each locale file in the repository
-### ![ru__RU](https://img.shields.io/badge/ru__RU-%E2%88%9A-brightgreen) +### ![ru__RU](https://img.shields.io/badge/ru__RU-99.89%25-yellow) -### ![ta__IN](https://img.shields.io/badge/ta__IN-77.52%25-yellow) +
+Missing keys in ru_RU.axaml + +- Text.Blame.BlameOnPreviousRevision + +
+ +### ![ta__IN](https://img.shields.io/badge/ta__IN-77.43%25-yellow)
Missing keys in ta_IN.axaml @@ -802,6 +816,7 @@ This document shows the translation status of each locale file in the repository - Text.Bisect.Good - Text.Bisect.Skip - Text.Bisect.WaitingForRange +- Text.Blame.BlameOnPreviousRevision - Text.BranchCM.CompareWithCurrent - Text.BranchCM.CreatePR - Text.BranchCM.CreatePRForUpstream @@ -991,7 +1006,7 @@ This document shows the translation status of each locale file in the repository
-### ![uk__UA](https://img.shields.io/badge/uk__UA-78.64%25-yellow) +### ![uk__UA](https://img.shields.io/badge/uk__UA-78.55%25-yellow)
Missing keys in uk_UA.axaml @@ -1011,6 +1026,7 @@ This document shows the translation status of each locale file in the repository - Text.Bisect.Good - Text.Bisect.Skip - Text.Bisect.WaitingForRange +- Text.Blame.BlameOnPreviousRevision - Text.BranchCM.CreatePR - Text.BranchCM.CreatePRForUpstream - Text.BranchCM.ResetToSelectedCommit From 0c34c54e5c563342ba4d8de942a2475b8922f9b8 Mon Sep 17 00:00:00 2001 From: Junhyung Choi Date: Mon, 20 Oct 2025 14:55:16 +0900 Subject: [PATCH 32/72] localization: add Korean(ko_KR) support (#1864) * localization: add Korean(ko_KR) support * [FIX] typo --- src/App.axaml | 1 + src/Models/Locales.cs | 1 + src/Resources/Locales/ko_KR.axaml | 903 ++++++++++++++++++++++++++++++ 3 files changed, 905 insertions(+) create mode 100644 src/Resources/Locales/ko_KR.axaml diff --git a/src/App.axaml b/src/App.axaml index 36c17decd..a7a0c17f2 100644 --- a/src/App.axaml +++ b/src/App.axaml @@ -24,6 +24,7 @@ + diff --git a/src/Models/Locales.cs b/src/Models/Locales.cs index 42b878208..027433336 100644 --- a/src/Models/Locales.cs +++ b/src/Models/Locales.cs @@ -21,6 +21,7 @@ public class Locale new Locale("繁體中文", "zh_TW"), new Locale("日本語", "ja_JP"), new Locale("தமிழ் (Tamil)", "ta_IN"), + new Locale("한국어", "ko_KR"), }; public Locale(string name, string key) diff --git a/src/Resources/Locales/ko_KR.axaml b/src/Resources/Locales/ko_KR.axaml new file mode 100644 index 000000000..ea262f0b0 --- /dev/null +++ b/src/Resources/Locales/ko_KR.axaml @@ -0,0 +1,903 @@ + +정보 + SourceGit 정보 + 릴리스 노트 + 오픈소스 & 무료 Git GUI 클라이언트 + 무시할 파일 추가 + 패턴: + 저장 파일: + 워크트리 추가 + 위치: + 이 워크트리의 경로입니다. 상대 경로를 지원합니다. + 브랜치 이름: + 선택 사항. 기본값은 대상 폴더 이름입니다. + 추적할 브랜치: + 원격 브랜치 추적 + 체크아웃할 대상: + 새 브랜치 생성 + 기존 브랜치 + AI 어시스턴트 + 재생성 + AI를 사용하여 커밋 메시지 생성 + 커밋 메시지로 적용 + SourceGit 숨기기 + 모두 보기 + 패치 + 패치 파일: + 적용할 .patch 파일을 선택하세요 + 공백 변경 사항 무시 + 패치 적용 + 공백: + 스태시 적용 + 적용 후 삭제 + 인덱스의 변경 사항 복원 + 스태시: + 아카이브... + 아카이브 저장 위치: + 아카이브 파일 경로 선택 + 리비전: + 아카이브 + SourceGit Askpass + 암호 입력: + 변경되지 않음으로 간주된 파일 + 변경되지 않음으로 간주된 파일 없음 + 이미지 불러오기... + 새로 고침 + 바이너리 파일은 지원되지 않습니다!!! + 이진 탐색 + 중단 + 나쁨 + 이진 탐색 중. 현재 HEAD가 '좋음' 상태입니까, '나쁨' 상태입니까? + 좋음 + 건너뛰기 + 이진 탐색 중. 현재 커밋을 '좋음' 또는 '나쁨'으로 표시하고 다른 커밋을 체크아웃하세요. + 블레임 + 이 파일은 블레임을 지원하지 않습니다!!! + ${0}$ 체크아웃... + ${0}$와(과) 비교 + 워크트리와 비교 + 브랜치 이름 복사 + 사용자 지정 작업 + ${0}$ 삭제... + 선택한 {0}개의 브랜치 삭제 + ${0}$(으)로 Fast-Forward + ${0}$에서 ${1}$(으)로 Fetch... + Git Flow - ${0}$ 완료 + ${0}$을(를) ${1}$(으)로 병합... + 선택한 {0}개의 브랜치를 현재 브랜치로 병합 + ${0}$ Pull + ${0}$에서 ${1}$(으)로 Pull... + ${0}$ Push + ${1}$을(를) 기반으로 ${0}$ 리베이스... + ${0}$ 이름 바꾸기... + ${0}$을(를) ${1}$(으)로 리셋... + ${0}$(워크트리)로 전환 + 추적 브랜치 설정... + 브랜치 비교 + {0}개 커밋 앞섬 + {0}개 커밋 앞섬, {1}개 커밋 뒤처짐 + {0}개 커밋 뒤처짐 + 유효하지 않음 + 원격 + 상태 + 추적 중 + URL + 워크트리 + 취소 + 부모 리비전으로 리셋 + 이 리비전으로 리셋 + 커밋 메시지 생성 + 표시 모드 변경 + 파일 및 디렉터리 목록으로 보기 + 경로 목록으로 보기 + 파일 시스템 트리로 보기 + 서브모듈 URL 변경 + 서브모듈: + URL: + 브랜치 체크아웃 + 커밋 체크아웃 + 커밋: + 경고: 커밋 체크아웃을 하면, HEAD가 분리됩니다(detached) + 로컬 변경 사항: + 폐기 + 스태시 & 재적용 + 모든 서브모듈 업데이트 + 브랜치: + 현재 HEAD에 브랜치/태그에 연결되지 않은 커밋이 있습니다! 계속하시겠습니까? + 체크아웃 & Fast-Forward + Fast-Forward 대상: + 체리픽 + 커밋 메시지에 원본 추가 + 커밋: + 모든 변경 사항 커밋 + 메인라인: + 어느 쪽을 메인라인으로 간주해야 할지 알 수 없기 때문에 일반적으로 병합(merge)을 체리픽할 수 없습니다. 이 옵션을 사용하면 지정된 부모를 기준으로 변경 사항을 다시 적용할 수 있습니다. + 모든 스태시 지우기 + 모든 스태시를 지우려고 합니다. 계속하시겠습니까? + 원격 저장소 복제 + 추가 파라미터: + 저장소 복제 시 추가 인수. 선택 사항. + 로컬 이름: + 저장소 이름. 선택 사항. + 상위 폴더: + 서브모듈 초기화 & 업데이트 + 저장소 URL: + 닫기 + 에디터 + 커밋 체크아웃 + 커밋 체리픽 + 체리픽... + HEAD와 비교 + 워크트리와 비교 + 작성자 + 메시지 + 커밋터 + SHA + 제목 + 사용자 지정 작업 + 커밋 삭제 + 대화형 리베이스 + 삭제(Drop)... + 수정(Edit)... + 부모에 합치기(Fixup)... + ${1}$을(를) 기반으로 ${0}$ 대화형 리베이스 + 메시지 수정(Reword)... + 부모에 합치기(Squash)... + ${0}$(으)로 병합 + 병합... + ${0}$을(를) ${1}$(으)로 푸시 + ${1}$을(를) 기반으로 ${0}$ 리베이스 + ${0}$을(를) ${1}$(으)로 리셋 + 커밋 되돌리기 + 메시지 수정 + 패치로 저장... + 부모에 합치기 + 변경 사항 + 변경된 파일 + 변경 사항 검색... + 파일 + LFS 파일 + 파일 검색... + 서브모듈 + 정보 + 작성자 + 자식 + 커밋터 + 이 커밋을 포함하는 ref 확인 + 커밋 포함 REF + 이메일 복사 + 이름 복사 + 이름 & 이메일 복사 + 처음 100개의 변경 사항만 표시합니다. 모든 변경 사항은 '변경 사항' 탭에서 확인하세요. + 키: + 메시지 + 부모 + REFS + SHA + 서명자: + 브라우저에서 열기 + 설명 + 붙여넣기 (모두 바꾸기) + 제목 + 커밋 제목 입력 + 저장소 설정 + 커밋 템플릿 + ${files_num}, ${branch_name}, ${files} 및 ${files:N} (N은 출력할 최대 파일 경로 수)을(를) 사용할 수 있습니다. + 템플릿 내용: + 템플릿 이름: + 사용자 지정 작업 + 인수: + 내장 파라미터: + + ${REPO} 저장소 경로 + ${REMOTE} 선택한 원격 또는 선택한 브랜치의 원격 + ${BRANCH} 선택한 브랜치 (원격 브랜치의 경우 ${REMOTE} 부분 제외) + ${BRANCH_FRIENDLY_NAME} 선택한 브랜치의 식별하기 쉬운 이름 (원격 브랜치의 경우 ${REMOTE} 부분 포함) + ${SHA} 선택한 커밋의 해시 + ${TAG} 선택한 태그 + $1, $2 ... 입력 컨트롤 값 + 실행 파일: + 입력 컨트롤: + 편집 + 이름: + 범위: + 브랜치 + 커밋 + 원격 + 저장소 + 태그 + 작업이 끝날 때까지 대기 + 이메일 주소 + 이메일 주소 + GIT + 원격 자동 Fetch + + 기본 원격 + 선호하는 병합 모드 + 이슈 트래커 + Azure DevOps 규칙 추가 + Gerrit Change-Id 커밋 규칙 추가 + Gitee 이슈 규칙 추가 + Gitee Pull Request 규칙 추가 + GitHub 규칙 추가 + GitLab 이슈 규칙 추가 + GitLab Merge Request 규칙 추가 + Jira 규칙 추가 + 새 규칙 + 이슈 정규식: + 규칙 이름: + .issuetracker 파일에 이 규칙 공유 + 결과 URL: + 정규식 그룹 값에 접근하려면 $1, $2를 사용하세요. + AI + 선호하는 서비스: + '선호하는 서비스'가 설정되면, SourceGit은 이 저장소에서 해당 서비스만 사용합니다. 그렇지 않고 사용 가능한 서비스가 두 개 이상인 경우, 하나를 선택할 수 있는 컨텍스트 메뉴가 표시됩니다. + HTTP 프록시 + 이 저장소에서 사용하는 HTTP 프록시 + 사용자 이름 + 이 저장소의 사용자 이름 + 사용자 지정 작업 컨트롤 편집 + 선택 시 값: + 선택 시, 이 값이 명령줄 인수로 사용됩니다 + 설명: + 기본값: + 폴더 여부: + 레이블: + 옵션: + 옵션 구분자로 '|'를 사용하세요 + 유형: + 작업 공간 + 색상 + 이름 + 시작 시 탭 복원 + 계속 + 빈 커밋이 감지되었습니다! 계속하시겠습니까 (--allow-empty)? + 모두 스테이징 & 커밋 + 빈 커밋이 감지되었습니다! 계속하시겠습니까 (--allow-empty) 아니면 모두 스테이징 후 커밋하시겠습니까? + 재시작 필요 + 변경 사항을 적용하려면 앱을 다시 시작해야 합니다. + Conventional Commit 도우미 + 주요 변경 사항(Breaking Change): + 종료된 이슈: + 상세 변경 내역: + 범위: + 간단한 설명: + 변경 유형: + 복사 + 전체 텍스트 복사 + 전체 경로 복사 + 경로 복사 + 브랜치 생성... + 기준: + 생성된 브랜치로 체크아웃 + 로컬 변경 사항: + 폐기 + 스태시 & 재적용 + 새 브랜치 이름: + 브랜치 이름을 입력하세요. + 로컬 브랜치 생성 + 기존 브랜치 덮어쓰기 + 태그 생성... + 태그 생성 위치: + GPG 서명 + 태그 메시지: + 선택 사항. + 태그 이름: + 권장 형식: v1.0.0-alpha + 생성 후 모든 원격에 푸시 + 새 태그 생성 + 종류: + 주석 태그 + 경량 태그 + Ctrl을 누른 채 클릭하면 바로 시작합니다 + 잘라내기 + 서브모듈 초기화 해제 + 로컬 변경 사항이 있어도 강제로 초기화 해제합니다. + 서브모듈: + 브랜치 삭제 + 브랜치: + 원격 브랜치를 삭제하려고 합니다!!! + 원격 브랜치 ${0}$도 함께 삭제 + 여러 브랜치 삭제 + 한 번에 여러 브랜치를 삭제하려고 합니다. 실행하기 전에 다시 한번 확인하세요! + 여러 태그 삭제 + 원격 저장소에서도 삭제 + 한 번에 여러 태그를 삭제하려고 합니다. 실행하기 전에 다시 한번 확인하세요! + 원격 삭제 + 원격: + 경로: + 대상: + 모든 하위 항목이 목록에서 제거됩니다. + 목록에서만 제거되며, 디스크에서 삭제되지 않습니다! + 그룹 삭제 확인 + 저장소 삭제 확인 + 서브모듈 삭제 + 서브모듈 경로: + 태그 삭제 + 태그: + 원격 저장소에서도 삭제 + 바이너리 비교 + 파일 모드 변경됨 + 첫 번째 차이점 + 모든 공백 변경 사항 무시 + 혼합 + 차이점 + 나란히 보기 + 스와이프 + 마지막 차이점 + LFS 객체 변경 + 신규 + 다음 차이점 + 변경 사항 없음 또는 줄바꿈(EOL) 변경만 있음 + 기존 + 이전 차이점 + 패치로 저장 + 숨겨진 기호 표시 + 나란히 비교 + 서브모듈 + 삭제됨 + 신규 + 전환 + 구문 강조 + 줄 바꿈 + 병합 도구에서 열기 + 모든 줄 표시 + 표시 줄 수 줄이기 + 표시 줄 수 늘리기 + 파일을 선택하여 변경 사항 보기 + 디렉터리 히스토리 + 로컬 변경 사항 있음 + 업스트림과 불일치 + 이미 최신 상태 + 변경 사항 폐기 + 작업 사본의 모든 로컬 변경 사항. + 변경 사항: + 무시된 파일 포함 + 추적하지 않는 파일 포함 + {0}개의 변경 사항이 폐기됩니다 + 이 작업은 되돌릴 수 없습니다!!! + 커밋 삭제 + 커밋: + 새 HEAD: + 북마크: + 새 이름: + 대상: + 선택한 그룹 편집 + 선택한 저장소 편집 + 대상: + 이 저장소 + Fetch + 모든 원격 Fetch + 로컬 ref 강제 덮어쓰기 + 태그 없이 Fetch + 원격: + 원격 변경 사항 Fetch + 변경되지 않음으로 간주 + 폐기... + {0}개 파일 폐기... + ${0}$을(를) 사용하여 해결 + 패치로 저장... + 스테이지 + {0}개 파일 스테이지 + 스태시... + {0}개 파일 스태시... + 언스테이지 + {0}개 파일 언스테이지 + 내 것 사용 (checkout --ours) + 상대방 것 사용 (checkout --theirs) + 파일 히스토리 + 변경 사항 + 내용 + Git-Flow + 개발 브랜치: + Feature: + Feature 접두사: + FLOW - Feature 완료 + FLOW - Hotfix 완료 + FLOW - Release 완료 + 대상: + 완료 후 원격(들)에 푸시 + 병합 시 스쿼시 + 핫픽스: + Hotfix 접두사: + Git-Flow 초기화 + 브랜치 유지 + 운영 브랜치: + 릴리스: + Release 접두사: + Feature 시작... + FLOW - Feature 시작 + Hotfix 시작... + FLOW - Hotfix 시작 + 이름 입력 + Release 시작... + FLOW - Release 시작 + 버전 태그 접두사: + Git LFS + 추적 패턴 추가... + 패턴이 파일 이름임 + 사용자 정의 패턴: + Git LFS에 추적 패턴 추가 + Fetch + Git LFS 객체를 다운로드하려면 `git lfs fetch`를 실행하세요. 이 작업은 작업 사본을 업데이트하지 않습니다. + LFS 객체 Fetch + Git LFS 훅(hook) 설치 + 잠금 보기 + 잠긴 파일 없음 + 잠금 + 내 잠금만 보기 + LFS 잠금 + 잠금 해제 + 강제 잠금 해제 + 정리 + 로컬 저장소에서 오래된 LFS 파일을 삭제하려면 `git lfs prune`을 실행하세요 + Pull + 현재 ref 및 체크아웃에 대한 모든 Git LFS 파일을 다운로드하려면 `git lfs pull`을 실행하세요 + LFS 객체 Pull + 푸시 + 대기 중인 대용량 파일을 Git LFS 엔드포인트로 푸시합니다 + LFS 객체 푸시 + 원격: + '{0}' 이름의 파일 추적 + 모든 *{0} 파일 추적 + 히스토리 + 작성자 + 작성 시간 + 그래프 & 제목 + SHA + 커밋 시간 + {0}개 커밋 선택됨 + 'Ctrl' 또는 'Shift' 키를 누른 채로 여러 커밋을 선택하세요. + ⌘ 또는 ⇧ 키를 누른 채로 여러 커밋을 선택하세요. + 팁: + 키보드 단축키 참조 + 전역 + 새 저장소 복제 + 현재 탭 닫기 + 다음 탭으로 이동 + 이전 탭으로 이동 + 새 탭 만들기 + 환경설정 대화상자 열기 + 활성 작업 공간 전환 + 활성 탭 전환 + 저장소 + 스테이징된 변경 사항 커밋 + 스테이징된 변경 사항 커밋 및 푸시 + 모든 변경 사항 스테이징 후 커밋 + Fetch (바로 시작) + 대시보드 모드 (기본) + 커밋 검색 모드 열기 + Pull (바로 시작) + 푸시 (바로 시작) + 이 저장소 강제 새로고침 + '변경 사항'으로 전환 + '히스토리'로 전환 + '스태시'로 전환 + 텍스트 에디터 + 검색 패널 닫기 + 다음 일치 항목 찾기 + 이전 일치 항목 찾기 + 외부 diff/merge 도구로 열기 + 검색 패널 열기 + 폐기 + 스테이지 + 언스테이지 + 저장소 초기화 + 경로: + 체리픽 진행 중. + 커밋 처리 중 + 병합 진행 중. + 병합 중 + 리베이스 진행 중. + 중단 지점 + 되돌리기 진행 중. + 커밋 되돌리는 중 + 대화형 리베이스 + 로컬 변경 사항 스태시 & 재적용 + 기준: + 드래그 앤 드롭으로 커밋 순서 변경 + 대상 브랜치: + 링크 복사 + 브라우저에서 열기 + 오류 + 알림 + + 작업 공간 + 브랜치 병합 + 병합 메시지 수정 + 대상: + 병합 옵션: + 소스: + 병합 (다중) + 모든 변경 사항 커밋 + 전략: + 대상: + 서브모듈 이동 + 이동 위치: + 서브모듈: + 저장소 노드 이동 + 상위 노드 선택: + 이름: + Git이 구성되지 않았습니다. [환경설정]으로 이동하여 먼저 구성하세요. + 데이터 저장 디렉터리 열기 + 병합 도구에서 열기 + 다음으로 열기... + 선택 사항. + 새 탭 만들기 + 북마크 + 탭 닫기 + 다른 탭 닫기 + 오른쪽 탭 닫기 + 저장소 경로 복사 + 저장소 + 붙여넣기 + {0}일 전 + 1시간 전 + {0}시간 전 + 방금 전 + 지난 달 + 작년 + {0}분 전 + {0}개월 전 + {0}년 전 + 어제 + 환경설정 + AI + Diff 분석 프롬프트 + API 키 + 제목 생성 프롬프트 + 모델 + 이름 + 입력된 값은 환경변수(ENV)에서 API 키를 불러올 이름입니다 + 서버 + 스트리밍 활성화 + 모양 + 기본 글꼴 + 에디터 탭 너비 + 글꼴 크기 + 기본 + 에디터 + 고정폭 글꼴 + 텍스트 에디터에서만 고정폭 글꼴 사용 + 테마 + 테마 재정의 + 스크롤바 자동 숨기기 사용 + 제목 표시줄에 고정폭 탭 사용 + 네이티브 윈도우 프레임 사용 + DIFF/MERGE 도구 + 설치 경로 + diff/merge 도구 경로 입력 + 도구 + 일반 + 시작 시 업데이트 확인 + 날짜 형식 + 변경 사항 트리에서 폴더 압축 활성화 + 언어 + 히스토리 커밋 수 + 그래프에 커밋 시간 대신 작성자 시간 표시 + 기본으로 `로컬 변경 사항` 페이지 표시 + 커밋 세부 정보에서 기본으로 `변경 사항` 탭 표시 + 커밋 세부 정보에 자식 커밋 표시 + 커밋 그래프에 태그 표시 + 제목 가이드 길이 + GitHub 스타일 기본 아바타 생성 + GIT + 자동 CRLF 활성화 + 기본 복제 디렉터리 + 사용자 이메일 + 전역 git 사용자 이메일 + Fetch 시 --prune 활성화 + diff 시 --ignore-cr-at-eol 활성화 + 이 앱은 Git (>= 2.25.1)을(를) 필요로 합니다 + 설치 경로 + HTTP SSL 검증 활성화 + git-credential-manager 대신 git-credential-libsecret 사용 + 사용자 이름 + 전역 git 사용자 이름 + Git 버전 + GPG 서명 + 커밋 GPG 서명 + GPG 형식 + 프로그램 설치 경로 + 설치된 gpg 프로그램 경로 입력 + 태그 GPG 서명 + 사용자 서명 키 + 사용자의 gpg 서명 키 + 연동 + 셸/터미널 + 경로 + 셸/터미널 + 원격 정리 + 대상: + 워크트리 정리 + `$GIT_COMMON_DIR/worktrees`의 워크트리 정보 정리 + Pull + 원격 브랜치: + 대상: + 로컬 변경 사항: + 폐기 + 스태시 & 재적용 + 모든 서브모듈 업데이트 + 원격: + Pull (Fetch & 병합) + 병합 대신 리베이스 사용 + 푸시 + 서브모듈이 푸시되었는지 확인 + 강제 푸시 + 로컬 브랜치: + 신규 + 원격: + 리비전: + 리비전을 원격에 푸시 + 변경 사항을 원격에 푸시 + 원격 브랜치: + 추적 브랜치로 설정 + 모든 태그 푸시 + 태그를 원격에 푸시 + 모든 원격에 푸시 + 원격: + 태그: + 종료 + 현재 브랜치 리베이스 + 로컬 변경 사항 스태시 & 재적용 + 기준: + 원격 추가 + 원격 편집 + 이름: + 원격 이름 + 저장소 URL: + 원격 git 저장소 URL + URL 복사 + 사용자 지정 작업 + 삭제... + 편집... + Fetch + 브라우저에서 열기 + 정리 + 워크트리 제거 확인 + `--force` 옵션 활성화 + 대상: + 브랜치 이름 변경 + 새 이름: + 이 브랜치의 고유한 이름 + 브랜치: + 중단 + 원격에서 변경 사항 자동 Fetch 중... + 정렬 + 커밋 날짜 순 + 이름 순 + 정리 (GC & Prune) + 이 저장소에 대해 `git gc` 명령을 실행합니다. + 모두 지우기 + 지우기 + 이 저장소 설정 + 계속 + 사용자 지정 작업 + 사용자 지정 작업 없음 + 대시보드 + 모든 변경 사항 폐기 + 파일 탐색기에서 열기 + 브랜치/태그/서브모듈 검색 + 그래프에 표시 여부 + 설정 안 함 + 커밋 그래프에서 숨기기 + 커밋 그래프에서 필터링 + 레이아웃 + 수평 + 수직 + 커밋 순서 + 커밋 날짜 + 위상 정렬 + 로컬 브랜치 + 추가 옵션... + HEAD로 이동 + 브랜치 생성 + 알림 지우기 + 현재 브랜치만 강조 + {0}에서 열기 + 외부 도구에서 열기 + 원격 + 원격 추가 + 커밋 검색 + 작성자 + 커밋터 + 내용 + 메시지 + 경로 + SHA + 현재 브랜치 + 장식된(Decorated) 커밋만 + 첫 번째 부모만 + 플래그 표시 + 유실된(Lost) 커밋 표시 + 서브모듈을 트리로 표시 + 태그를 트리로 표시 + 건너뛰기 + 통계 + 서브모듈 + 서브모듈 추가 + 서브모듈 업데이트 + 태그 + 새 태그 + 생성 날짜 순 + 이름 순 + 정렬 + 터미널에서 열기 + 상대 시간 사용 + 로그 보기 + 브라우저에서 '{0}' 방문 + 워크트리 + 워크트리 추가 + 정리 + Git 저장소 URL + 현재 브랜치를 리비전으로 리셋 + 리셋 모드: + 이동 대상: + 현재 브랜치: + 브랜치 리셋 (체크아웃 없음) + 이동 대상: + 브랜치: + 파일 탐색기에서 보기 + 커밋 되돌리기 + 커밋: + 되돌린 변경 사항 커밋 + 커밋 메시지 수정 + 실행 중. 잠시만 기다려주세요... + 저장 + 다른 이름으로 저장... + 패치가 성공적으로 저장되었습니다! + 저장소 스캔 + 루트 디렉터리: + 다른 사용자 정의 디렉터리 스캔 + 업데이트 확인... + 이 소프트웨어의 새 버전을 사용할 수 있습니다: + 업데이트 확인 실패! + 다운로드 + 이 버전 건너뛰기 + 소프트웨어 업데이트 + 현재 사용 가능한 업데이트가 없습니다. + 서브모듈 브랜치 설정 + 서브모듈: + 현재: + 변경: + 선택 사항. 비어 있으면 기본값으로 설정됩니다. + 추적 브랜치 설정 + 브랜치: + 업스트림 설정 해제 + 업스트림: + SHA 복사 + 이동 + 커밋 스쿼시 + 대상: + SSH 개인 키: + 개인 SSH 키 저장 경로 + 시작 + 스태시 + 추적하지 않는 파일 포함 + 메시지: + 선택 사항. 이 스태시의 메시지 + 모드: + 스테이징된 변경 사항만 + 선택한 파일의 스테이징된 변경 사항과 스테이징되지 않은 변경 사항이 모두 스태시됩니다!!! + 로컬 변경 사항 스태시 + 적용 + 메시지 복사 + 삭제 + 패치로 저장... + 스태시 삭제 + 삭제: + 스태시 + 변경 사항 + 스태시 + 통계 + 개요 + 이번 달 + 이번 주 + 작성자: + 커밋: + 서브모듈 + 서브모듈 추가 + 브랜치 + 브랜치 + 상대 경로 + 초기화 해제 + 중첩된 서브모듈 Fetch + 히스토리 + 이동 + 저장소 열기 + 상대 경로: + 이 모듈을 저장할 상대 폴더입니다. + 삭제 + 브랜치 설정 + URL 변경 + 상태 + 수정됨 + 초기화 안 됨 + 리비전 변경됨 + 병합 안 됨 + 업데이트 + URL + 확인 + 태그 생성자 + 시간 + 메시지 + 이름 + 태그 생성자 + 태그 이름 복사 + 사용자 지정 작업 + ${0}$ 삭제... + 선택한 {0}개의 태그 삭제... + ${0}$을(를) ${1}$(으)로 병합... + ${0}$ 푸시... + 서브모듈 업데이트 + 모든 서브모듈 + 필요시 초기화 + 서브모듈 재귀적으로 탐색 + 서브모듈: + 서브모듈의 원격 추적 브랜치로 업데이트 + URL: + 로그 + 모두 지우기 + 복사 + 삭제 + 경고 + 시작 페이지 + 그룹 생성 + 하위 그룹 생성 + 저장소 복제 + 삭제 + 폴더 끌어다 놓기 지원. 사용자 정의 그룹화 지원. + 편집 + 다른 그룹으로 이동 + 모든 저장소 열기 + 저장소 열기 + 터미널 열기 + 기본 복제 디렉터리의 저장소 다시 스캔 + 저장소 검색... + 로컬 변경 사항 + Git 무시 + 모든 *{0} 파일 무시 + 같은 폴더의 *{0} 파일 무시 + 이 폴더의 추적하지 않는 파일 무시 + 이 파일만 무시 + 수정 + 이제 이 파일을 스테이징할 수 있습니다. + 히스토리 지우기 + 모든 커밋 메시지 히스토리를 지우시겠습니까? 이 작업은 되돌릴 수 없습니다. + 커밋 + 커밋 & 푸시 + 템플릿/히스토리 + 클릭 이벤트 트리거 + 커밋 (수정) + 모든 변경 사항 스테이징 후 커밋 + 분리된(detached) HEAD에 커밋을 생성하고 있습니다. 계속하시겠습니까? + {0}개의 파일을 스테이징했지만 {1}개의 파일만 표시됩니다 ({2}개의 파일은 필터링됨). 계속하시겠습니까? + 충돌 감지됨 + 외부 병합 도구 열기 + 모든 충돌을 외부 병합 도구에서 열기 + 파일 충돌 해결됨 + 내 것 사용 + 상대방 것 사용 + 추적하지 않는 파일 포함 + 최근 입력한 메시지 없음 + 커밋 템플릿 없음 + 검증 안 함 + 작성자 리셋 + 서명(SignOff) + 스테이징됨 + 언스테이지 + 모두 언스테이지 + 스테이징 안 됨 + 스테이지 + 모두 스테이지 + 변경되지 않음으로 간주된 파일 보기 + 템플릿: ${0}$ + 작업 공간: + 작업 공간 설정... + 워크트리 + 경로 복사 + 잠금 + 열기 + 제거 + 잠금 해제 + From c963473219a0de60f62fcaececc985aa9f422171 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 20 Oct 2025 05:55:36 +0000 Subject: [PATCH 33/72] doc: Update translation status and sort locale files --- TRANSLATION.md | 15 +++++++++++++++ src/Resources/Locales/ko_KR.axaml | 8 ++------ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index ca9c88be8..b91e2c84d 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -519,6 +519,21 @@ This document shows the translation status of each locale file in the repository
+### ![ko__KR](https://img.shields.io/badge/ko__KR-99.22%25-yellow) + +
+Missing keys in ko_KR.axaml + +- Text.Blame.BlameOnPreviousRevision +- Text.Blame.TypeNotSupported +- Text.BranchCM.CreatePR +- Text.BranchCM.CreatePRForUpstream +- Text.PushToNewBranch +- Text.PushToNewBranch.Title +- Text.Submodule.Status.Unmerged + +
+ ### ![pt__BR](https://img.shields.io/badge/pt__BR-70.95%25-red)
diff --git a/src/Resources/Locales/ko_KR.axaml b/src/Resources/Locales/ko_KR.axaml index ea262f0b0..35769b8bf 100644 --- a/src/Resources/Locales/ko_KR.axaml +++ b/src/Resources/Locales/ko_KR.axaml @@ -1,5 +1,6 @@ -정보 + + 정보 SourceGit 정보 릴리스 노트 오픈소스 & 무료 Git GUI 클라이언트 @@ -52,7 +53,6 @@ 건너뛰기 이진 탐색 중. 현재 커밋을 '좋음' 또는 '나쁨'으로 표시하고 다른 커밋을 체크아웃하세요. 블레임 - 이 파일은 블레임을 지원하지 않습니다!!! ${0}$ 체크아웃... ${0}$와(과) 비교 워크트리와 비교 @@ -458,7 +458,6 @@ 이전 탭으로 이동 새 탭 만들기 환경설정 대화상자 열기 - 활성 작업 공간 전환 활성 탭 전환 저장소 스테이징된 변경 사항 커밋 @@ -558,11 +557,9 @@ 기본 에디터 고정폭 글꼴 - 텍스트 에디터에서만 고정폭 글꼴 사용 테마 테마 재정의 스크롤바 자동 숨기기 사용 - 제목 표시줄에 고정폭 탭 사용 네이티브 윈도우 프레임 사용 DIFF/MERGE 도구 설치 경로 @@ -814,7 +811,6 @@ 수정됨 초기화 안 됨 리비전 변경됨 - 병합 안 됨 업데이트 URL 확인 From 6907e2220cc4c225120b78572e35dfdd9fbe540a Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 20 Oct 2025 13:58:11 +0800 Subject: [PATCH 34/72] =?UTF-8?q?doc:=20add=20`=ED=95=9C=EA=B5=AD=EC=96=B4?= =?UTF-8?q?`=20translations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: leo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f0c037bd..25de6dd87 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ * Supports Windows/macOS/Linux * Opensource/Free * Fast -* Deutsch/English/Español/Bahasa Indonesia/Français/Italiano/Português/Русский/Українська/简体中文/繁體中文/日本語/தமிழ் (Tamil) +* Deutsch/English/Español/Bahasa Indonesia/Français/Italiano/Português/Русский/Українська/简体中文/繁體中文/日本語/தமிழ் (Tamil)/한국어 * Built-in light/dark themes * Customize theme * Visual commit graph From 11d66c67118b84b541c74931b999e8444f2bdbca Mon Sep 17 00:00:00 2001 From: leo Date: Mon, 20 Oct 2025 15:06:09 +0800 Subject: [PATCH 35/72] ux: main tab close icon style Signed-off-by: leo --- src/Views/LauncherTabBar.axaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Views/LauncherTabBar.axaml b/src/Views/LauncherTabBar.axaml index c82656edc..d3f23b91b 100644 --- a/src/Views/LauncherTabBar.axaml +++ b/src/Views/LauncherTabBar.axaml @@ -107,7 +107,7 @@ + + + + - - - + diff --git a/src/Views/RepositoryConfigure.axaml.cs b/src/Views/RepositoryConfigure.axaml.cs index 3ce581d76..2c5622df1 100644 --- a/src/Views/RepositoryConfigure.axaml.cs +++ b/src/Views/RepositoryConfigure.axaml.cs @@ -21,6 +21,21 @@ protected override async void OnClosing(WindowClosingEventArgs e) await configure.SaveAsync(); } + private async void SelectConventionalTypesFile(object sender, RoutedEventArgs e) + { + var options = new FilePickerOpenOptions() + { + FileTypeFilter = [new FilePickerFileType("Conventional Commit Types") { Patterns = ["*.json"] }], + AllowMultiple = false, + }; + + var selected = await StorageProvider.OpenFilePickerAsync(options); + if (selected.Count == 1 && DataContext is ViewModels.RepositoryConfigure vm) + vm.ConventionalTypesOverride = selected[0].Path.LocalPath; + + e.Handled = true; + } + private async void SelectExecutableForCustomAction(object sender, RoutedEventArgs e) { var options = new FilePickerOpenOptions() From 33c6d9d0ef29b21635f276da0d0dccd3fc4f06d1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 20 Oct 2025 13:24:57 +0000 Subject: [PATCH 37/72] doc: Update translation status and sort locale files --- TRANSLATION.md | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index b91e2c84d..ca6bac8d0 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -6,7 +6,7 @@ This document shows the translation status of each locale file in the repository ### ![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen) -### ![de__DE](https://img.shields.io/badge/de__DE-99.33%25-yellow) +### ![de__DE](https://img.shields.io/badge/de__DE-99.22%25-yellow)
Missing keys in de_DE.axaml @@ -15,12 +15,13 @@ This document shows the translation status of each locale file in the repository - Text.BranchCM.CreatePR - Text.BranchCM.CreatePRForUpstream - Text.Configure.CommitMessageTemplate.BuiltinVars +- Text.Configure.Git.ConventionalTypesOverride - Text.PushToNewBranch - Text.PushToNewBranch.Title
-### ![es__ES](https://img.shields.io/badge/es__ES-99.33%25-yellow) +### ![es__ES](https://img.shields.io/badge/es__ES-99.22%25-yellow)
Missing keys in es_ES.axaml @@ -29,12 +30,13 @@ This document shows the translation status of each locale file in the repository - Text.BranchCM.CreatePR - Text.BranchCM.CreatePRForUpstream - Text.Configure.CommitMessageTemplate.BuiltinVars +- Text.Configure.Git.ConventionalTypesOverride - Text.PushToNewBranch - Text.PushToNewBranch.Title
-### ![fr__FR](https://img.shields.io/badge/fr__FR-77.32%25-yellow) +### ![fr__FR](https://img.shields.io/badge/fr__FR-77.23%25-yellow)
Missing keys in fr_FR.axaml @@ -104,6 +106,7 @@ This document shows the translation status of each locale file in the repository - Text.Configure.CustomAction.InputControls.Edit - Text.Configure.CustomAction.Scope.Remote - Text.Configure.CustomAction.Scope.Tag +- Text.Configure.Git.ConventionalTypesOverride - Text.Configure.Git.PreferredMergeMode - Text.Configure.IssueTracker.AddSampleGerritChangeIdCommit - Text.Configure.IssueTracker.Share @@ -245,7 +248,7 @@ This document shows the translation status of each locale file in the repository
-### ![id__ID](https://img.shields.io/badge/id__ID-98.77%25-yellow) +### ![id__ID](https://img.shields.io/badge/id__ID-98.66%25-yellow)
Missing keys in id_ID.axaml @@ -256,6 +259,7 @@ This document shows the translation status of each locale file in the repository - Text.BranchCM.CreatePRForUpstream - Text.CommitCM.Drop - Text.Configure.CommitMessageTemplate.BuiltinVars +- Text.Configure.Git.ConventionalTypesOverride - Text.DropHead - Text.DropHead.Commit - Text.DropHead.NewHead @@ -264,7 +268,7 @@ This document shows the translation status of each locale file in the repository
-### ![it__IT](https://img.shields.io/badge/it__IT-95.98%25-yellow) +### ![it__IT](https://img.shields.io/badge/it__IT-95.87%25-yellow)
Missing keys in it_IT.axaml @@ -285,6 +289,7 @@ This document shows the translation status of each locale file in the repository - Text.CommitDetail.Info.CopyNameAndEmail - Text.CommitMessageTextBox.PasteAndReplaceAll - Text.Configure.CommitMessageTemplate.BuiltinVars +- Text.Configure.Git.ConventionalTypesOverride - Text.Diff.Image.Difference - Text.DirtyState.HasLocalChanges - Text.DirtyState.HasPendingPullOrPush @@ -308,7 +313,7 @@ This document shows the translation status of each locale file in the repository
-### ![ja__JP](https://img.shields.io/badge/ja__JP-77.32%25-yellow) +### ![ja__JP](https://img.shields.io/badge/ja__JP-77.23%25-yellow)
Missing keys in ja_JP.axaml @@ -379,6 +384,7 @@ This document shows the translation status of each locale file in the repository - Text.Configure.CustomAction.InputControls.Edit - Text.Configure.CustomAction.Scope.Remote - Text.Configure.CustomAction.Scope.Tag +- Text.Configure.Git.ConventionalTypesOverride - Text.Configure.Git.PreferredMergeMode - Text.Configure.IssueTracker.AddSampleGerritChangeIdCommit - Text.Configure.IssueTracker.Share @@ -519,7 +525,7 @@ This document shows the translation status of each locale file in the repository
-### ![ko__KR](https://img.shields.io/badge/ko__KR-99.22%25-yellow) +### ![ko__KR](https://img.shields.io/badge/ko__KR-99.11%25-yellow)
Missing keys in ko_KR.axaml @@ -528,13 +534,14 @@ This document shows the translation status of each locale file in the repository - Text.Blame.TypeNotSupported - Text.BranchCM.CreatePR - Text.BranchCM.CreatePRForUpstream +- Text.Configure.Git.ConventionalTypesOverride - Text.PushToNewBranch - Text.PushToNewBranch.Title - Text.Submodule.Status.Unmerged
-### ![pt__BR](https://img.shields.io/badge/pt__BR-70.95%25-red) +### ![pt__BR](https://img.shields.io/badge/pt__BR-70.87%25-red)
Missing keys in pt_BR.axaml @@ -619,6 +626,7 @@ This document shows the translation status of each locale file in the repository - Text.Configure.CustomAction.Scope.Remote - Text.Configure.CustomAction.Scope.Tag - Text.Configure.CustomAction.WaitForExit +- Text.Configure.Git.ConventionalTypesOverride - Text.Configure.Git.PreferredMergeMode - Text.Configure.IssueTracker.AddSampleGerritChangeIdCommit - Text.Configure.IssueTracker.AddSampleGiteeIssue @@ -802,16 +810,17 @@ This document shows the translation status of each locale file in the repository
-### ![ru__RU](https://img.shields.io/badge/ru__RU-99.89%25-yellow) +### ![ru__RU](https://img.shields.io/badge/ru__RU-99.78%25-yellow)
Missing keys in ru_RU.axaml - Text.Blame.BlameOnPreviousRevision +- Text.Configure.Git.ConventionalTypesOverride
-### ![ta__IN](https://img.shields.io/badge/ta__IN-77.43%25-yellow) +### ![ta__IN](https://img.shields.io/badge/ta__IN-77.34%25-yellow)
Missing keys in ta_IN.axaml @@ -882,6 +891,7 @@ This document shows the translation status of each locale file in the repository - Text.Configure.CustomAction.InputControls.Edit - Text.Configure.CustomAction.Scope.Remote - Text.Configure.CustomAction.Scope.Tag +- Text.Configure.Git.ConventionalTypesOverride - Text.Configure.Git.PreferredMergeMode - Text.Configure.IssueTracker.AddSampleGerritChangeIdCommit - Text.Configure.IssueTracker.Share @@ -1021,7 +1031,7 @@ This document shows the translation status of each locale file in the repository
-### ![uk__UA](https://img.shields.io/badge/uk__UA-78.55%25-yellow) +### ![uk__UA](https://img.shields.io/badge/uk__UA-78.46%25-yellow)
Missing keys in uk_UA.axaml @@ -1091,6 +1101,7 @@ This document shows the translation status of each locale file in the repository - Text.Configure.CustomAction.InputControls.Edit - Text.Configure.CustomAction.Scope.Remote - Text.Configure.CustomAction.Scope.Tag +- Text.Configure.Git.ConventionalTypesOverride - Text.Configure.IssueTracker.AddSampleGerritChangeIdCommit - Text.Configure.IssueTracker.Share - Text.ConfigureCustomActionControls From d9b2b45922ef53ba09e357ec7db08b45d6f192a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Javier=20J=2E=20Mart=C3=ADnez=20M=2E?= <56406225+jjesus-dev@users.noreply.github.com> Date: Mon, 20 Oct 2025 20:31:25 -0600 Subject: [PATCH 38/72] localization: update Spanish translation (#1863) Add missing strings. --- src/Resources/Locales/es_ES.axaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml index 16131682a..44634488a 100644 --- a/src/Resources/Locales/es_ES.axaml +++ b/src/Resources/Locales/es_ES.axaml @@ -61,6 +61,8 @@ Comparar con ${0}$ Comparar con Worktree Copiar Nombre de la Rama + Crear PR... + Crear PR para upstream ${0}$... Acción personalizada Eliminar ${0}$... Eliminar {0} ramas seleccionadas @@ -637,6 +639,8 @@ Push a todos los remotos Remoto: Etiqueta: + Push a una NUEVA rama + Nombre de entrada de la nueva rama remota: Salir Rebase Rama Actual Stash & reaplicar cambios locales From 2925ae06066545f6cc98c81ebdeb515e3188d5bc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Oct 2025 02:31:49 +0000 Subject: [PATCH 39/72] doc: Update translation status and sort locale files --- TRANSLATION.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index ca6bac8d0..b0bd02cf1 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -21,18 +21,14 @@ This document shows the translation status of each locale file in the repository
-### ![es__ES](https://img.shields.io/badge/es__ES-99.22%25-yellow) +### ![es__ES](https://img.shields.io/badge/es__ES-99.67%25-yellow)
Missing keys in es_ES.axaml - Text.Blame.BlameOnPreviousRevision -- Text.BranchCM.CreatePR -- Text.BranchCM.CreatePRForUpstream - Text.Configure.CommitMessageTemplate.BuiltinVars - Text.Configure.Git.ConventionalTypesOverride -- Text.PushToNewBranch -- Text.PushToNewBranch.Title
From 27e3a97d7e93f0631efbae17d1c9b9969f3ec8db Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 21 Oct 2025 10:49:48 +0800 Subject: [PATCH 40/72] ux: use another label for string values in custom input control because $1, $2... is not available here Signed-off-by: leo --- src/Resources/Locales/en_US.axaml | 1 + src/Resources/Locales/zh_CN.axaml | 1 + src/Resources/Locales/zh_TW.axaml | 1 + src/Views/ConfigureCustomActionControls.axaml | 2 +- 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 0e4e87330..011f26c7f 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -256,6 +256,7 @@ Label: Options: Use '|' as delimiter for options + The built-in variables ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA}, and ${TAG} remain available here Type: Workspaces Color diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 8a31a35a6..88f020e4a 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -260,6 +260,7 @@ 名称 : 选项列表 : 选项之间请使用英文 '|' 作为分隔符 + 内置变量 ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA} 与 ${TAG} 在这里仍然可用 类型 : 工作区 颜色 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 9944c41d0..e329f3525 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -260,6 +260,7 @@ 名稱: 選項列表: 請使用英文「|」符號分隔選項 + 內建變數 ${REPO}、${REMOTE}、${BRANCH}、${BRANCH_FRIENDLY_NAME}、${SHA} 及 ${TAG} 在此處仍可使用 類型: 工作區 顏色 diff --git a/src/Views/ConfigureCustomActionControls.axaml b/src/Views/ConfigureCustomActionControls.axaml index 1e7c9fa83..e24bc0101 100644 --- a/src/Views/ConfigureCustomActionControls.axaml +++ b/src/Views/ConfigureCustomActionControls.axaml @@ -177,7 +177,7 @@ From a70182e7b7d59a0d6e3dbbaefdd50aee1737ca2f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 21 Oct 2025 02:50:11 +0000 Subject: [PATCH 41/72] doc: Update translation status and sort locale files --- TRANSLATION.md | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index b0bd02cf1..c02202014 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -6,7 +6,7 @@ This document shows the translation status of each locale file in the repository ### ![en_US](https://img.shields.io/badge/en__US-%E2%88%9A-brightgreen) -### ![de__DE](https://img.shields.io/badge/de__DE-99.22%25-yellow) +### ![de__DE](https://img.shields.io/badge/de__DE-99.11%25-yellow)
Missing keys in de_DE.axaml @@ -16,12 +16,13 @@ This document shows the translation status of each locale file in the repository - Text.BranchCM.CreatePRForUpstream - Text.Configure.CommitMessageTemplate.BuiltinVars - Text.Configure.Git.ConventionalTypesOverride +- Text.ConfigureCustomActionControls.StringValue.Tip - Text.PushToNewBranch - Text.PushToNewBranch.Title
-### ![es__ES](https://img.shields.io/badge/es__ES-99.67%25-yellow) +### ![es__ES](https://img.shields.io/badge/es__ES-99.55%25-yellow)
Missing keys in es_ES.axaml @@ -29,10 +30,11 @@ This document shows the translation status of each locale file in the repository - Text.Blame.BlameOnPreviousRevision - Text.Configure.CommitMessageTemplate.BuiltinVars - Text.Configure.Git.ConventionalTypesOverride +- Text.ConfigureCustomActionControls.StringValue.Tip
-### ![fr__FR](https://img.shields.io/badge/fr__FR-77.23%25-yellow) +### ![fr__FR](https://img.shields.io/badge/fr__FR-77.15%25-yellow)
Missing keys in fr_FR.axaml @@ -115,6 +117,7 @@ This document shows the translation status of each locale file in the repository - Text.ConfigureCustomActionControls.Label - Text.ConfigureCustomActionControls.Options - Text.ConfigureCustomActionControls.Options.Tip +- Text.ConfigureCustomActionControls.StringValue.Tip - Text.ConfigureCustomActionControls.Type - Text.ConfirmEmptyCommit.Continue - Text.ConfirmEmptyCommit.NoLocalChanges @@ -244,7 +247,7 @@ This document shows the translation status of each locale file in the repository
-### ![id__ID](https://img.shields.io/badge/id__ID-98.66%25-yellow) +### ![id__ID](https://img.shields.io/badge/id__ID-98.55%25-yellow)
Missing keys in id_ID.axaml @@ -256,6 +259,7 @@ This document shows the translation status of each locale file in the repository - Text.CommitCM.Drop - Text.Configure.CommitMessageTemplate.BuiltinVars - Text.Configure.Git.ConventionalTypesOverride +- Text.ConfigureCustomActionControls.StringValue.Tip - Text.DropHead - Text.DropHead.Commit - Text.DropHead.NewHead @@ -264,7 +268,7 @@ This document shows the translation status of each locale file in the repository
-### ![it__IT](https://img.shields.io/badge/it__IT-95.87%25-yellow) +### ![it__IT](https://img.shields.io/badge/it__IT-95.76%25-yellow)
Missing keys in it_IT.axaml @@ -286,6 +290,7 @@ This document shows the translation status of each locale file in the repository - Text.CommitMessageTextBox.PasteAndReplaceAll - Text.Configure.CommitMessageTemplate.BuiltinVars - Text.Configure.Git.ConventionalTypesOverride +- Text.ConfigureCustomActionControls.StringValue.Tip - Text.Diff.Image.Difference - Text.DirtyState.HasLocalChanges - Text.DirtyState.HasPendingPullOrPush @@ -309,7 +314,7 @@ This document shows the translation status of each locale file in the repository
-### ![ja__JP](https://img.shields.io/badge/ja__JP-77.23%25-yellow) +### ![ja__JP](https://img.shields.io/badge/ja__JP-77.15%25-yellow)
Missing keys in ja_JP.axaml @@ -393,6 +398,7 @@ This document shows the translation status of each locale file in the repository - Text.ConfigureCustomActionControls.Label - Text.ConfigureCustomActionControls.Options - Text.ConfigureCustomActionControls.Options.Tip +- Text.ConfigureCustomActionControls.StringValue.Tip - Text.ConfigureCustomActionControls.Type - Text.ConfirmEmptyCommit.Continue - Text.ConfirmEmptyCommit.NoLocalChanges @@ -521,7 +527,7 @@ This document shows the translation status of each locale file in the repository
-### ![ko__KR](https://img.shields.io/badge/ko__KR-99.11%25-yellow) +### ![ko__KR](https://img.shields.io/badge/ko__KR-99.00%25-yellow)
Missing keys in ko_KR.axaml @@ -531,13 +537,14 @@ This document shows the translation status of each locale file in the repository - Text.BranchCM.CreatePR - Text.BranchCM.CreatePRForUpstream - Text.Configure.Git.ConventionalTypesOverride +- Text.ConfigureCustomActionControls.StringValue.Tip - Text.PushToNewBranch - Text.PushToNewBranch.Title - Text.Submodule.Status.Unmerged
-### ![pt__BR](https://img.shields.io/badge/pt__BR-70.87%25-red) +### ![pt__BR](https://img.shields.io/badge/pt__BR-70.79%25-red)
Missing keys in pt_BR.axaml @@ -637,6 +644,7 @@ This document shows the translation status of each locale file in the repository - Text.ConfigureCustomActionControls.Label - Text.ConfigureCustomActionControls.Options - Text.ConfigureCustomActionControls.Options.Tip +- Text.ConfigureCustomActionControls.StringValue.Tip - Text.ConfigureCustomActionControls.Type - Text.ConfirmEmptyCommit.Continue - Text.ConfirmEmptyCommit.NoLocalChanges @@ -806,17 +814,18 @@ This document shows the translation status of each locale file in the repository
-### ![ru__RU](https://img.shields.io/badge/ru__RU-99.78%25-yellow) +### ![ru__RU](https://img.shields.io/badge/ru__RU-99.67%25-yellow)
Missing keys in ru_RU.axaml - Text.Blame.BlameOnPreviousRevision - Text.Configure.Git.ConventionalTypesOverride +- Text.ConfigureCustomActionControls.StringValue.Tip
-### ![ta__IN](https://img.shields.io/badge/ta__IN-77.34%25-yellow) +### ![ta__IN](https://img.shields.io/badge/ta__IN-77.26%25-yellow)
Missing keys in ta_IN.axaml @@ -900,6 +909,7 @@ This document shows the translation status of each locale file in the repository - Text.ConfigureCustomActionControls.Label - Text.ConfigureCustomActionControls.Options - Text.ConfigureCustomActionControls.Options.Tip +- Text.ConfigureCustomActionControls.StringValue.Tip - Text.ConfigureCustomActionControls.Type - Text.ConfirmEmptyCommit.Continue - Text.ConfirmEmptyCommit.NoLocalChanges @@ -1027,7 +1037,7 @@ This document shows the translation status of each locale file in the repository
-### ![uk__UA](https://img.shields.io/badge/uk__UA-78.46%25-yellow) +### ![uk__UA](https://img.shields.io/badge/uk__UA-78.37%25-yellow)
Missing keys in uk_UA.axaml @@ -1109,6 +1119,7 @@ This document shows the translation status of each locale file in the repository - Text.ConfigureCustomActionControls.Label - Text.ConfigureCustomActionControls.Options - Text.ConfigureCustomActionControls.Options.Tip +- Text.ConfigureCustomActionControls.StringValue.Tip - Text.ConfigureCustomActionControls.Type - Text.ConfigureWorkspace.Name - Text.ConfirmRestart.Title From fe471ac89d5cb2efea6851e2850a86c96b749731 Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 21 Oct 2025 16:05:11 +0800 Subject: [PATCH 42/72] feature: enable multiple-selection for changes collection view in `Changes Detail`/`Stash Changes`/`Branch Compare`/`Revision Compare` (#1826) Signed-off-by: leo --- src/Commands/Checkout.cs | 15 ++ src/ViewModels/BranchCompare.cs | 7 + src/ViewModels/CommitDetail.cs | 37 ++++- src/ViewModels/RevisionCompare.cs | 11 +- src/ViewModels/StashesPage.cs | 38 ++++- src/Views/BranchCompare.axaml.cs | 169 +++++++++++++++------- src/Views/ChangeCollectionView.axaml | 6 +- src/Views/ChangeCollectionView.axaml.cs | 9 -- src/Views/CommitChanges.axaml | 3 +- src/Views/CommitChanges.axaml.cs | 42 +++--- src/Views/CommitDetail.axaml.cs | 93 ++++++++++++ src/Views/RevisionCompare.axaml.cs | 175 ++++++++++++++++------- src/Views/StashesPage.axaml.cs | 179 ++++++++++++++++-------- src/Views/WorkingCopy.axaml | 2 - 14 files changed, 585 insertions(+), 201 deletions(-) diff --git a/src/Commands/Checkout.cs b/src/Commands/Checkout.cs index 23cbd413b..024636bf9 100644 --- a/src/Commands/Checkout.cs +++ b/src/Commands/Checkout.cs @@ -72,5 +72,20 @@ public async Task FileWithRevisionAsync(string file, string revision) Args = $"checkout --no-overlay {revision} -- {file.Quoted()}"; return await ExecAsync().ConfigureAwait(false); } + + public async Task MultipleFilesWithRevisionAsync(List files, string revision) + { + var builder = new StringBuilder(); + builder + .Append("checkout --no-overlay ") + .Append(revision) + .Append(" --"); + + foreach (var f in files) + builder.Append(' ').Append(f.Quoted()); + + Args = builder.ToString(); + return await ExecAsync().ConfigureAwait(false); + } } } diff --git a/src/ViewModels/BranchCompare.cs b/src/ViewModels/BranchCompare.cs index bbe26390e..e7f17e490 100644 --- a/src/ViewModels/BranchCompare.cs +++ b/src/ViewModels/BranchCompare.cs @@ -128,6 +128,13 @@ public string GetAbsPath(string path) return Native.OS.GetAbsPath(_repo, path); } + public async Task SaveChangesAsPatchAsync(List changes, string saveTo) + { + var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo, changes, _based.Head, _to.Head, saveTo); + if (succ) + App.SendNotification(_repo, App.Text("SaveAsPatchSuccess")); + } + private void Refresh() { IsLoading = true; diff --git a/src/ViewModels/CommitDetail.cs b/src/ViewModels/CommitDetail.cs index 1501f6b4d..c272689dc 100644 --- a/src/ViewModels/CommitDetail.cs +++ b/src/ViewModels/CommitDetail.cs @@ -239,7 +239,7 @@ public async Task SaveChangesAsPatchAsync(List changes, string sa public async Task ResetToThisRevisionAsync(string path) { var log = _repo.CreateLog($"Reset File to '{_commit.SHA}'"); - await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(path, $"{_commit.SHA}"); + await new Commands.Checkout(_repo.FullPath).Use(log).FileWithRevisionAsync(path, _commit.SHA); log.Complete(); } @@ -254,6 +254,41 @@ public async Task ResetToParentRevisionAsync(Models.Change change) log.Complete(); } + public async Task ResetMultipleToThisRevisionAsync(List changes) + { + var files = new List(); + foreach (var c in changes) + files.Add(c.Path); + + var log = _repo.CreateLog($"Reset Files to '{_commit.SHA}'"); + await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(files, _commit.SHA); + log.Complete(); + } + + public async Task ResetMultipleToParentRevisionAsync(List changes) + { + var renamed = new List(); + var modified = new List(); + + foreach (var c in changes) + { + if (c.Index == Models.ChangeState.Renamed) + renamed.Add(c.OriginalPath); + else + modified.Add(c.Path); + } + + var log = _repo.CreateLog($"Reset Files to '{_commit.SHA}~1'"); + + if (modified.Count > 0) + await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(modified, $"{_commit.SHA}~1"); + + if (renamed.Count > 0) + await new Commands.Checkout(_repo.FullPath).Use(log).MultipleFilesWithRevisionAsync(renamed, $"{_commit.SHA}~1"); + + log.Complete(); + } + public async Task> GetRevisionFilesUnderFolderAsync(string parentFolder) { return await new Commands.QueryRevisionObjects(_repo.FullPath, _commit.SHA, parentFolder) diff --git a/src/ViewModels/RevisionCompare.cs b/src/ViewModels/RevisionCompare.cs index ca81a0dc7..4de7b16d6 100644 --- a/src/ViewModels/RevisionCompare.cs +++ b/src/ViewModels/RevisionCompare.cs @@ -127,14 +127,11 @@ public string GetAbsPath(string path) return Native.OS.GetAbsPath(_repo, path); } - public void SaveAsPatch(string saveTo) + public async Task SaveChangesAsPatchAsync(List changes, string saveTo) { - Task.Run(async () => - { - var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo, _changes, GetSHA(_startPoint), GetSHA(_endPoint), saveTo); - if (succ) - App.SendNotification(_repo, App.Text("SaveAsPatchSuccess")); - }); + var succ = await Commands.SaveChangesAsPatch.ProcessRevisionCompareChangesAsync(_repo, changes ?? _changes, GetSHA(_startPoint), GetSHA(_endPoint), saveTo); + if (succ) + App.SendNotification(_repo, App.Text("SaveAsPatchSuccess")); } public void ClearSearchFilter() diff --git a/src/ViewModels/StashesPage.cs b/src/ViewModels/StashesPage.cs index de34ecab0..96f01c7a0 100644 --- a/src/ViewModels/StashesPage.cs +++ b/src/ViewModels/StashesPage.cs @@ -158,7 +158,7 @@ public void Drop(Models.Stash stash) _repo.ShowPopup(new DropStash(_repo, stash)); } - public async Task SaveStashAsPathAsync(Models.Stash stash, string saveTo) + public async Task SaveStashAsPatchAsync(Models.Stash stash, string saveTo) { var opts = new List(); var changes = await new Commands.CompareRevisions(_repo.FullPath, $"{stash.SHA}^", stash.SHA) @@ -211,6 +211,42 @@ public async Task CheckoutSingleFileAsync(Models.Change change) log.Complete(); } + public async Task CheckoutMultipleFileAsync(List changes) + { + var untracked = new List(); + var added = new List(); + var modified = new List(); + + foreach (var c in changes) + { + if (_untracked.Contains(c) && _selectedStash.Parents.Count == 3) + untracked.Add(c.Path); + else if (c.Index == Models.ChangeState.Added && _selectedStash.Parents.Count > 1) + added.Add(c.Path); + else + modified.Add(c.Path); + } + + var log = _repo.CreateLog($"Reset File to '{_selectedStash.Name}'"); + + if (untracked.Count > 0) + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .MultipleFilesWithRevisionAsync(untracked, _selectedStash.Parents[2]); + + if (added.Count > 0) + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .MultipleFilesWithRevisionAsync(added, _selectedStash.Parents[1]); + + if (modified.Count > 0) + await new Commands.Checkout(_repo.FullPath) + .Use(log) + .MultipleFilesWithRevisionAsync(modified, _selectedStash.SHA); + + log.Complete(); + } + private void RefreshVisible() { if (string.IsNullOrEmpty(_searchFilter)) diff --git a/src/Views/BranchCompare.axaml.cs b/src/Views/BranchCompare.axaml.cs index 886825b14..08083ccca 100644 --- a/src/Views/BranchCompare.axaml.cs +++ b/src/Views/BranchCompare.axaml.cs @@ -1,8 +1,10 @@ using System; using System.IO; +using System.Text; using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Platform.Storage; namespace SourceGit.Views { @@ -15,61 +17,127 @@ public BranchCompare() private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e) { - if (DataContext is ViewModels.BranchCompare { SelectedChanges: { Count: 1 } selected } vm && + if (DataContext is ViewModels.BranchCompare { SelectedChanges: { Count: > 0 } selected } vm && sender is ChangeCollectionView view) { - var repo = vm.RepositoryPath; - var change = selected[0]; var menu = new ContextMenu(); + var repo = vm.RepositoryPath; - var openWithMerger = new MenuItem(); - openWithMerger.Header = App.Text("OpenInExternalMergeTool"); - openWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith"); - openWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D"; - openWithMerger.Click += (_, ev) => + var patch = new MenuItem(); + patch.Header = App.Text("FileCM.SaveAsPatch"); + patch.Icon = App.CreateMenuIcon("Icons.Diff"); + patch.Click += async (_, e) => { - new Commands.DiffTool(repo, new Models.DiffOption(vm.Base.Head, vm.To.Head, change)).Open(); - ev.Handled = true; + var storageProvider = this.StorageProvider; + if (storageProvider == null) + return; + + var options = new FilePickerSaveOptions(); + options.Title = App.Text("FileCM.SaveAsPatch"); + options.DefaultExtension = ".patch"; + options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }]; + + var storageFile = await storageProvider.SaveFilePickerAsync(options); + if (storageFile != null) + { + var saveTo = storageFile.Path.LocalPath; + await vm.SaveChangesAsPatchAsync(selected, saveTo); + } + + e.Handled = true; }; - menu.Items.Add(openWithMerger); - if (change.Index != Models.ChangeState.Deleted) + if (selected.Count == 1) { - var full = Path.GetFullPath(Path.Combine(repo, change.Path)); - var explore = new MenuItem(); - explore.Header = App.Text("RevealFile"); - explore.Icon = App.CreateMenuIcon("Icons.Explore"); - explore.IsEnabled = File.Exists(full); - explore.Click += (_, ev) => + var change = selected[0]; + var openWithMerger = new MenuItem(); + openWithMerger.Header = App.Text("OpenInExternalMergeTool"); + openWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith"); + openWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D"; + openWithMerger.Click += (_, ev) => { - Native.OS.OpenInFileManager(full, true); + new Commands.DiffTool(repo, new Models.DiffOption(vm.Base.Head, vm.To.Head, change)).Open(); ev.Handled = true; }; - menu.Items.Add(explore); - } + menu.Items.Add(openWithMerger); - var copyPath = new MenuItem(); - copyPath.Header = App.Text("CopyPath"); - copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; - copyPath.Click += async (_, ev) => - { - await App.CopyTextAsync(change.Path); - ev.Handled = true; - }; - menu.Items.Add(new MenuItem() { Header = "-" }); - menu.Items.Add(copyPath); - - var copyFullPath = new MenuItem(); - copyFullPath.Header = App.Text("CopyFullPath"); - copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; - copyFullPath.Click += async (_, ev) => + if (change.Index != Models.ChangeState.Deleted) + { + var full = Path.GetFullPath(Path.Combine(repo, change.Path)); + var explore = new MenuItem(); + explore.Header = App.Text("RevealFile"); + explore.Icon = App.CreateMenuIcon("Icons.Explore"); + explore.IsEnabled = File.Exists(full); + explore.Click += (_, ev) => + { + Native.OS.OpenInFileManager(full, true); + ev.Handled = true; + }; + menu.Items.Add(explore); + } + + var copyPath = new MenuItem(); + copyPath.Header = App.Text("CopyPath"); + copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); + copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; + copyPath.Click += async (_, ev) => + { + await App.CopyTextAsync(change.Path); + ev.Handled = true; + }; + + var copyFullPath = new MenuItem(); + copyFullPath.Header = App.Text("CopyFullPath"); + copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); + copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; + copyFullPath.Click += async (_, ev) => + { + await App.CopyTextAsync(Native.OS.GetAbsPath(repo, change.Path)); + ev.Handled = true; + }; + + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(patch); + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(copyPath); + menu.Items.Add(copyFullPath); + } + else { - await App.CopyTextAsync(Native.OS.GetAbsPath(repo, change.Path)); - ev.Handled = true; - }; - menu.Items.Add(copyFullPath); + var copyPath = new MenuItem(); + copyPath.Header = App.Text("CopyPath"); + copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); + copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; + copyPath.Click += async (_, ev) => + { + var builder = new StringBuilder(); + foreach (var c in selected) + builder.AppendLine(c.Path); + + await App.CopyTextAsync(builder.ToString()); + ev.Handled = true; + }; + + var copyFullPath = new MenuItem(); + copyFullPath.Header = App.Text("CopyFullPath"); + copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); + copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; + copyFullPath.Click += async (_, ev) => + { + var builder = new StringBuilder(); + foreach (var c in selected) + builder.AppendLine(Native.OS.GetAbsPath(repo, c.Path)); + + await App.CopyTextAsync(builder.ToString()); + ev.Handled = true; + }; + + menu.Items.Add(patch); + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(copyPath); + menu.Items.Add(copyFullPath); + } + menu.Open(view); } @@ -89,17 +157,24 @@ private async void OnChangeCollectionViewKeyDown(object sender, KeyEventArgs e) if (DataContext is not ViewModels.BranchCompare vm) return; - if (sender is not ChangeCollectionView { SelectedChanges: { Count: 1 } selectedChanges }) + if (sender is not ChangeCollectionView { SelectedChanges: { Count: > 0 } selectedChanges }) return; - var change = selectedChanges[0]; if (e.KeyModifiers.HasFlag(OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control) && e.Key == Key.C) { - if (e.KeyModifiers.HasFlag(KeyModifiers.Shift)) - await App.CopyTextAsync(vm.GetAbsPath(change.Path)); + var builder = new StringBuilder(); + var copyAbsPath = e.KeyModifiers.HasFlag(KeyModifiers.Shift); + if (selectedChanges.Count == 1) + { + builder.Append(copyAbsPath ? vm.GetAbsPath(selectedChanges[0].Path) : selectedChanges[0].Path); + } else - await App.CopyTextAsync(change.Path); + { + foreach (var c in selectedChanges) + builder.AppendLine(copyAbsPath ? vm.GetAbsPath(c.Path) : c.Path); + } + await App.CopyTextAsync(builder.ToString()); e.Handled = true; } } diff --git a/src/Views/ChangeCollectionView.axaml b/src/Views/ChangeCollectionView.axaml index e7cf63882..a00570f50 100644 --- a/src/Views/ChangeCollectionView.axaml +++ b/src/Views/ChangeCollectionView.axaml @@ -33,7 +33,7 @@ @@ -78,7 +78,7 @@ @@ -110,7 +110,7 @@ diff --git a/src/Views/ChangeCollectionView.axaml.cs b/src/Views/ChangeCollectionView.axaml.cs index 4e3092fa8..237a69da4 100644 --- a/src/Views/ChangeCollectionView.axaml.cs +++ b/src/Views/ChangeCollectionView.axaml.cs @@ -61,15 +61,6 @@ public bool IsUnstagedChange set => SetValue(IsUnstagedChangeProperty, value); } - public static readonly StyledProperty SelectionModeProperty = - AvaloniaProperty.Register(nameof(SelectionMode)); - - public SelectionMode SelectionMode - { - get => GetValue(SelectionModeProperty); - set => SetValue(SelectionModeProperty, value); - } - public static readonly StyledProperty ViewModeProperty = AvaloniaProperty.Register(nameof(ViewMode), Models.ChangeViewMode.Tree); diff --git a/src/Views/CommitChanges.axaml b/src/Views/CommitChanges.axaml index 4656b6b48..e03a50ad2 100644 --- a/src/Views/CommitChanges.axaml +++ b/src/Views/CommitChanges.axaml @@ -45,8 +45,7 @@ - 0 } changes } view) return; var detailView = this.FindAncestorOfType(); if (detailView == null) return; - var changes = view.SelectedChanges ?? []; var container = view.FindDescendantOfType(); if (container is { SelectedItems.Count: 1, SelectedItem: ViewModels.ChangeTreeNode { IsFolder: true } node }) - { - var menu = detailView.CreateChangeContextMenuByFolder(node, changes); - menu.Open(view); - return; - } - - if (changes.Count == 1) - { - var menu = detailView.CreateChangeContextMenu(changes[0]); - menu.Open(view); - } + detailView.CreateChangeContextMenuByFolder(node, changes)?.Open(view); + else if (changes.Count > 1) + detailView.CreateMultipleChangesContextMenu(changes)?.Open(view); + else + detailView.CreateChangeContextMenu(changes[0])?.Open(view); } private async void OnChangeCollectionViewKeyDown(object sender, KeyEventArgs e) @@ -45,18 +39,30 @@ private async void OnChangeCollectionViewKeyDown(object sender, KeyEventArgs e) if (DataContext is not ViewModels.CommitDetail vm) return; - if (sender is not ChangeCollectionView { SelectedChanges: { Count: 1 } selectedChanges }) + if (sender is not ChangeCollectionView { SelectedChanges: { Count: > 0 } selectedChanges } view) return; - var change = selectedChanges[0]; if (e.Key == Key.C && e.KeyModifiers.HasFlag(OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control)) { - if (e.KeyModifiers.HasFlag(KeyModifiers.Shift)) - await App.CopyTextAsync(vm.GetAbsPath(change.Path)); + var builder = new StringBuilder(); + var copyAbsPath = e.KeyModifiers.HasFlag(KeyModifiers.Shift); + var container = view.FindDescendantOfType(); + if (container is { SelectedItems.Count: 1, SelectedItem: ViewModels.ChangeTreeNode { IsFolder: true } node }) + { + builder.Append(copyAbsPath ? vm.GetAbsPath(node.FullPath) : node.FullPath); + } + else if (selectedChanges.Count == 1) + { + builder.Append(copyAbsPath ? vm.GetAbsPath(selectedChanges[0].Path) : selectedChanges[0].Path); + } else - await App.CopyTextAsync(change.Path); + { + foreach (var c in selectedChanges) + builder.AppendLine(copyAbsPath ? vm.GetAbsPath(c.Path) : c.Path); + } + await App.CopyTextAsync(builder.ToString()); e.Handled = true; } } diff --git a/src/Views/CommitDetail.axaml.cs b/src/Views/CommitDetail.axaml.cs index 1d028ca8b..ee570e64f 100644 --- a/src/Views/CommitDetail.axaml.cs +++ b/src/Views/CommitDetail.axaml.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Text; using Avalonia.Controls; using Avalonia.Input; @@ -97,6 +98,98 @@ public ContextMenu CreateChangeContextMenuByFolder(ViewModels.ChangeTreeNode nod return menu; } + public ContextMenu CreateMultipleChangesContextMenu(List changes) + { + if (DataContext is not ViewModels.CommitDetail { Repository: { } repo, Commit: { } commit } vm) + return null; + + var patch = new MenuItem(); + patch.Header = App.Text("FileCM.SaveAsPatch"); + patch.Icon = App.CreateMenuIcon("Icons.Diff"); + patch.Click += async (_, e) => + { + var storageProvider = TopLevel.GetTopLevel(this)?.StorageProvider; + if (storageProvider == null) + return; + + var options = new FilePickerSaveOptions(); + options.Title = App.Text("FileCM.SaveAsPatch"); + options.DefaultExtension = ".patch"; + options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }]; + + var storageFile = await storageProvider.SaveFilePickerAsync(options); + if (storageFile != null) + { + var saveTo = storageFile.Path.LocalPath; + await vm.SaveChangesAsPatchAsync(changes, saveTo); + } + + e.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(patch); + menu.Items.Add(new MenuItem() { Header = "-" }); + + if (!repo.IsBare) + { + var resetToThisRevision = new MenuItem(); + resetToThisRevision.Header = App.Text("ChangeCM.CheckoutThisRevision"); + resetToThisRevision.Icon = App.CreateMenuIcon("Icons.File.Checkout"); + resetToThisRevision.Click += async (_, ev) => + { + await vm.ResetMultipleToThisRevisionAsync(changes); + ev.Handled = true; + }; + + var resetToFirstParent = new MenuItem(); + resetToFirstParent.Header = App.Text("ChangeCM.CheckoutFirstParentRevision"); + resetToFirstParent.Icon = App.CreateMenuIcon("Icons.File.Checkout"); + resetToFirstParent.IsEnabled = commit.Parents.Count > 0; + resetToFirstParent.Click += async (_, ev) => + { + await vm.ResetMultipleToParentRevisionAsync(changes); + ev.Handled = true; + }; + + menu.Items.Add(resetToThisRevision); + menu.Items.Add(resetToFirstParent); + menu.Items.Add(new MenuItem { Header = "-" }); + } + + var copyPath = new MenuItem(); + copyPath.Header = App.Text("CopyPath"); + copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); + copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; + copyPath.Click += async (_, ev) => + { + var builder = new StringBuilder(); + foreach (var c in changes) + builder.AppendLine(c.Path); + + await App.CopyTextAsync(builder.ToString()); + ev.Handled = true; + }; + + var copyFullPath = new MenuItem(); + copyFullPath.Header = App.Text("CopyFullPath"); + copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); + copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; + copyFullPath.Click += async (_, e) => + { + var builder = new StringBuilder(); + foreach (var c in changes) + builder.AppendLine(Native.OS.GetAbsPath(repo.FullPath, c.Path)); + + await App.CopyTextAsync(builder.ToString()); + e.Handled = true; + }; + + menu.Items.Add(copyPath); + menu.Items.Add(copyFullPath); + return menu; + } + public ContextMenu CreateChangeContextMenu(Models.Change change) { if (DataContext is not ViewModels.CommitDetail { Repository: { } repo, Commit: { } commit } vm) diff --git a/src/Views/RevisionCompare.axaml.cs b/src/Views/RevisionCompare.axaml.cs index bb27e7095..3a84baca1 100644 --- a/src/Views/RevisionCompare.axaml.cs +++ b/src/Views/RevisionCompare.axaml.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Text; using Avalonia.Controls; using Avalonia.Input; @@ -17,59 +18,126 @@ public RevisionCompare() private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e) { - if (DataContext is ViewModels.RevisionCompare { SelectedChanges: { Count: 1 } selected } vm && + if (DataContext is ViewModels.RevisionCompare { SelectedChanges: { Count: > 0 } selected } vm && sender is ChangeCollectionView view) { - var change = selected[0]; - var changeFullPath = vm.GetAbsPath(change.Path); - var menu = new ContextMenu(); - - var openWithMerger = new MenuItem(); - openWithMerger.Header = App.Text("OpenInExternalMergeTool"); - openWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith"); - openWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D"; - openWithMerger.Click += (_, ev) => + var patch = new MenuItem(); + patch.Header = App.Text("FileCM.SaveAsPatch"); + patch.Icon = App.CreateMenuIcon("Icons.Diff"); + patch.Click += async (_, e) => { - vm.OpenChangeWithExternalDiffTool(change); - ev.Handled = true; + var storageProvider = TopLevel.GetTopLevel(this)?.StorageProvider; + if (storageProvider == null) + return; + + var options = new FilePickerSaveOptions(); + options.Title = App.Text("FileCM.SaveAsPatch"); + options.DefaultExtension = ".patch"; + options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }]; + + var storageFile = await storageProvider.SaveFilePickerAsync(options); + if (storageFile != null) + { + var saveTo = storageFile.Path.LocalPath; + await vm.SaveChangesAsPatchAsync(selected, saveTo); + } + + e.Handled = true; }; - menu.Items.Add(openWithMerger); - if (change.Index != Models.ChangeState.Deleted) + var menu = new ContextMenu(); + if (selected.Count == 1) { - var explore = new MenuItem(); - explore.Header = App.Text("RevealFile"); - explore.Icon = App.CreateMenuIcon("Icons.Explore"); - explore.IsEnabled = File.Exists(changeFullPath); - explore.Click += (_, ev) => + var change = selected[0]; + var changeFullPath = vm.GetAbsPath(change.Path); + + var openWithMerger = new MenuItem(); + openWithMerger.Header = App.Text("OpenInExternalMergeTool"); + openWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith"); + openWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D"; + openWithMerger.Click += (_, ev) => { - Native.OS.OpenInFileManager(changeFullPath, true); + vm.OpenChangeWithExternalDiffTool(change); ev.Handled = true; }; - menu.Items.Add(explore); - } + menu.Items.Add(openWithMerger); - var copyPath = new MenuItem(); - copyPath.Header = App.Text("CopyPath"); - copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; - copyPath.Click += async (_, ev) => - { - await App.CopyTextAsync(change.Path); - ev.Handled = true; - }; - menu.Items.Add(copyPath); + if (change.Index != Models.ChangeState.Deleted) + { + var explore = new MenuItem(); + explore.Header = App.Text("RevealFile"); + explore.Icon = App.CreateMenuIcon("Icons.Explore"); + explore.IsEnabled = File.Exists(changeFullPath); + explore.Click += (_, ev) => + { + Native.OS.OpenInFileManager(changeFullPath, true); + ev.Handled = true; + }; + menu.Items.Add(explore); + } + + var copyPath = new MenuItem(); + copyPath.Header = App.Text("CopyPath"); + copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); + copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; + copyPath.Click += async (_, ev) => + { + await App.CopyTextAsync(change.Path); + ev.Handled = true; + }; - var copyFullPath = new MenuItem(); - copyFullPath.Header = App.Text("CopyFullPath"); - copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; - copyFullPath.Click += async (_, ev) => + var copyFullPath = new MenuItem(); + copyFullPath.Header = App.Text("CopyFullPath"); + copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); + copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; + copyFullPath.Click += async (_, ev) => + { + await App.CopyTextAsync(changeFullPath); + ev.Handled = true; + }; + + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(patch); + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(copyPath); + menu.Items.Add(copyFullPath); + } + else { - await App.CopyTextAsync(changeFullPath); - ev.Handled = true; - }; - menu.Items.Add(copyFullPath); + var copyPath = new MenuItem(); + copyPath.Header = App.Text("CopyPath"); + copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); + copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; + copyPath.Click += async (_, ev) => + { + var builder = new StringBuilder(); + foreach (var c in selected) + builder.AppendLine(c.Path); + + await App.CopyTextAsync(builder.ToString()); + ev.Handled = true; + }; + + var copyFullPath = new MenuItem(); + copyFullPath.Header = App.Text("CopyFullPath"); + copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); + copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; + copyFullPath.Click += async (_, ev) => + { + var builder = new StringBuilder(); + foreach (var c in selected) + builder.AppendLine(vm.GetAbsPath(c.Path)); + + await App.CopyTextAsync(builder.ToString()); + ev.Handled = true; + }; + + menu.Items.Add(patch); + menu.Items.Add(new MenuItem() { Header = "-" }); + menu.Items.Add(copyPath); + menu.Items.Add(copyFullPath); + } + menu.Open(view); } @@ -86,8 +154,8 @@ private void OnPressedSHA(object sender, PointerPressedEventArgs e) private async void OnSaveAsPatch(object sender, RoutedEventArgs e) { - var topLevel = TopLevel.GetTopLevel(this); - if (topLevel == null) + var storage = TopLevel.GetTopLevel(this)?.StorageProvider; + if (storage == null) return; if (DataContext is not ViewModels.RevisionCompare vm) @@ -98,9 +166,9 @@ private async void OnSaveAsPatch(object sender, RoutedEventArgs e) options.DefaultExtension = ".patch"; options.FileTypeChoices = [new FilePickerFileType("Patch File") { Patterns = ["*.patch"] }]; - var storageFile = await topLevel.StorageProvider.SaveFilePickerAsync(options); + var storageFile = await storage.SaveFilePickerAsync(options); if (storageFile != null) - vm.SaveAsPatch(storageFile.Path.LocalPath); + await vm.SaveChangesAsPatchAsync(null, storageFile.Path.LocalPath); e.Handled = true; } @@ -110,17 +178,24 @@ private async void OnChangeCollectionViewKeyDown(object sender, KeyEventArgs e) if (DataContext is not ViewModels.RevisionCompare vm) return; - if (sender is not ChangeCollectionView { SelectedChanges: { Count: 1 } selectedChanges }) + if (sender is not ChangeCollectionView { SelectedChanges: { Count: > 0 } selectedChanges }) return; - var change = selectedChanges[0]; if (e.KeyModifiers.HasFlag(OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control) && e.Key == Key.C) { - if (e.KeyModifiers.HasFlag(KeyModifiers.Shift)) - await App.CopyTextAsync(vm.GetAbsPath(change.Path)); + var builder = new StringBuilder(); + var copyAbsPath = e.KeyModifiers.HasFlag(KeyModifiers.Shift); + if (selectedChanges.Count == 1) + { + builder.Append(copyAbsPath ? vm.GetAbsPath(selectedChanges[0].Path) : selectedChanges[0].Path); + } else - await App.CopyTextAsync(change.Path); + { + foreach (var c in selectedChanges) + builder.AppendLine(copyAbsPath ? vm.GetAbsPath(c.Path) : c.Path); + } + await App.CopyTextAsync(builder.ToString()); e.Handled = true; } } diff --git a/src/Views/StashesPage.axaml.cs b/src/Views/StashesPage.axaml.cs index 6dc74f174..ec19e27ae 100644 --- a/src/Views/StashesPage.axaml.cs +++ b/src/Views/StashesPage.axaml.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using System.Text; using Avalonia.Controls; using Avalonia.Input; @@ -84,7 +85,7 @@ private void OnStashContextRequested(object sender, ContextRequestedEventArgs e) var storageFile = await storageProvider.SaveFilePickerAsync(options); if (storageFile != null) - await vm.SaveStashAsPathAsync(stash, storageFile.Path.LocalPath); + await vm.SaveStashAsPatchAsync(stash, storageFile.Path.LocalPath); ev.Handled = true; }; @@ -123,70 +124,119 @@ private void OnStashDoubleTapped(object sender, TappedEventArgs e) private void OnChangeContextRequested(object sender, ContextRequestedEventArgs e) { - if (DataContext is ViewModels.StashesPage { SelectedChanges: { Count: 1 } selected } vm && + if (DataContext is ViewModels.StashesPage { SelectedChanges: { Count: > 0 } selected } vm && sender is ChangeCollectionView view) { - var change = selected[0]; - var fullPath = vm.GetAbsPath(change.Path); - - var openWithMerger = new MenuItem(); - openWithMerger.Header = App.Text("OpenInExternalMergeTool"); - openWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith"); - openWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D"; - openWithMerger.Click += (_, ev) => + if (selected.Count == 1) { - vm.OpenChangeWithExternalDiffTool(change); - ev.Handled = true; - }; + var change = selected[0]; + var fullPath = vm.GetAbsPath(change.Path); - var explore = new MenuItem(); - explore.Header = App.Text("RevealFile"); - explore.Icon = App.CreateMenuIcon("Icons.Explore"); - explore.IsEnabled = File.Exists(fullPath); - explore.Click += (_, ev) => - { - Native.OS.OpenInFileManager(fullPath, true); - ev.Handled = true; - }; + var openWithMerger = new MenuItem(); + openWithMerger.Header = App.Text("OpenInExternalMergeTool"); + openWithMerger.Icon = App.CreateMenuIcon("Icons.OpenWith"); + openWithMerger.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+D" : "Ctrl+Shift+D"; + openWithMerger.Click += (_, ev) => + { + vm.OpenChangeWithExternalDiffTool(change); + ev.Handled = true; + }; - var resetToThisRevision = new MenuItem(); - resetToThisRevision.Header = App.Text("ChangeCM.CheckoutThisRevision"); - resetToThisRevision.Icon = App.CreateMenuIcon("Icons.File.Checkout"); - resetToThisRevision.Click += async (_, ev) => - { - await vm.CheckoutSingleFileAsync(change); - ev.Handled = true; - }; + var explore = new MenuItem(); + explore.Header = App.Text("RevealFile"); + explore.Icon = App.CreateMenuIcon("Icons.Explore"); + explore.IsEnabled = File.Exists(fullPath); + explore.Click += (_, ev) => + { + Native.OS.OpenInFileManager(fullPath, true); + ev.Handled = true; + }; - var copyPath = new MenuItem(); - copyPath.Header = App.Text("CopyPath"); - copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; - copyPath.Click += async (_, ev) => - { - await App.CopyTextAsync(change.Path); - ev.Handled = true; - }; + var resetToThisRevision = new MenuItem(); + resetToThisRevision.Header = App.Text("ChangeCM.CheckoutThisRevision"); + resetToThisRevision.Icon = App.CreateMenuIcon("Icons.File.Checkout"); + resetToThisRevision.Click += async (_, ev) => + { + await vm.CheckoutSingleFileAsync(change); + ev.Handled = true; + }; + + var copyPath = new MenuItem(); + copyPath.Header = App.Text("CopyPath"); + copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); + copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; + copyPath.Click += async (_, ev) => + { + await App.CopyTextAsync(change.Path); + ev.Handled = true; + }; - var copyFullPath = new MenuItem(); - copyFullPath.Header = App.Text("CopyFullPath"); - copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); - copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; - copyFullPath.Click += async (_, ev) => + var copyFullPath = new MenuItem(); + copyFullPath.Header = App.Text("CopyFullPath"); + copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); + copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; + copyFullPath.Click += async (_, ev) => + { + await App.CopyTextAsync(fullPath); + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(openWithMerger); + menu.Items.Add(explore); + menu.Items.Add(new MenuItem { Header = "-" }); + menu.Items.Add(resetToThisRevision); + menu.Items.Add(new MenuItem { Header = "-" }); + menu.Items.Add(copyPath); + menu.Items.Add(copyFullPath); + menu.Open(view); + } + else { - await App.CopyTextAsync(fullPath); - ev.Handled = true; - }; + var resetToThisRevision = new MenuItem(); + resetToThisRevision.Header = App.Text("ChangeCM.CheckoutThisRevision"); + resetToThisRevision.Icon = App.CreateMenuIcon("Icons.File.Checkout"); + resetToThisRevision.Click += async (_, ev) => + { + await vm.CheckoutMultipleFileAsync(selected); + ev.Handled = true; + }; - var menu = new ContextMenu(); - menu.Items.Add(openWithMerger); - menu.Items.Add(explore); - menu.Items.Add(new MenuItem { Header = "-" }); - menu.Items.Add(resetToThisRevision); - menu.Items.Add(new MenuItem { Header = "-" }); - menu.Items.Add(copyPath); - menu.Items.Add(copyFullPath); - menu.Open(view); + var copyPath = new MenuItem(); + copyPath.Header = App.Text("CopyPath"); + copyPath.Icon = App.CreateMenuIcon("Icons.Copy"); + copyPath.Tag = OperatingSystem.IsMacOS() ? "⌘+C" : "Ctrl+C"; + copyPath.Click += async (_, ev) => + { + var builder = new StringBuilder(); + foreach (var c in selected) + builder.AppendLine(c.Path); + + await App.CopyTextAsync(builder.ToString()); + ev.Handled = true; + }; + + var copyFullPath = new MenuItem(); + copyFullPath.Header = App.Text("CopyFullPath"); + copyFullPath.Icon = App.CreateMenuIcon("Icons.Copy"); + copyFullPath.Tag = OperatingSystem.IsMacOS() ? "⌘+⇧+C" : "Ctrl+Shift+C"; + copyFullPath.Click += async (_, ev) => + { + var builder = new StringBuilder(); + foreach (var c in selected) + builder.AppendLine(vm.GetAbsPath(c.Path)); + + await App.CopyTextAsync(builder.ToString()); + ev.Handled = true; + }; + + var menu = new ContextMenu(); + menu.Items.Add(resetToThisRevision); + menu.Items.Add(new MenuItem { Header = "-" }); + menu.Items.Add(copyPath); + menu.Items.Add(copyFullPath); + menu.Open(view); + } } e.Handled = true; @@ -197,17 +247,24 @@ private async void OnChangeCollectionViewKeyDown(object sender, KeyEventArgs e) if (DataContext is not ViewModels.StashesPage vm) return; - if (sender is not ChangeCollectionView { SelectedChanges: { Count: 1 } selectedChanges }) + if (sender is not ChangeCollectionView { SelectedChanges: { Count: > 0 } selectedChanges }) return; - var change = selectedChanges[0]; if (e.KeyModifiers.HasFlag(OperatingSystem.IsMacOS() ? KeyModifiers.Meta : KeyModifiers.Control) && e.Key == Key.C) { - if (e.KeyModifiers.HasFlag(KeyModifiers.Shift)) - await App.CopyTextAsync(vm.GetAbsPath(change.Path)); + var builder = new StringBuilder(); + var copyAbsPath = e.KeyModifiers.HasFlag(KeyModifiers.Shift); + if (selectedChanges.Count == 1) + { + builder.Append(copyAbsPath ? vm.GetAbsPath(selectedChanges[0].Path) : selectedChanges[0].Path); + } else - await App.CopyTextAsync(change.Path); + { + foreach (var c in selectedChanges) + builder.AppendLine(copyAbsPath ? vm.GetAbsPath(c.Path) : c.Path); + } + await App.CopyTextAsync(builder.ToString()); e.Handled = true; } } diff --git a/src/Views/WorkingCopy.axaml b/src/Views/WorkingCopy.axaml index e5eed5e71..ce940299f 100644 --- a/src/Views/WorkingCopy.axaml +++ b/src/Views/WorkingCopy.axaml @@ -126,7 +126,6 @@ Date: Wed, 22 Oct 2025 15:45:42 +0800 Subject: [PATCH 43/72] feature: show repo status in `Welcome` page (#1867) Signed-off-by: leo --- ...thoutUntracked.cs => CountLocalChanges.cs} | 7 ++- src/ViewModels/Checkout.cs | 2 +- src/ViewModels/CheckoutAndFastForward.cs | 2 +- src/ViewModels/CheckoutCommit.cs | 2 +- src/ViewModels/CreateBranch.cs | 2 +- src/ViewModels/Pull.cs | 2 +- src/ViewModels/RepositoryNode.cs | 49 ++++++++++++++- src/ViewModels/Welcome.cs | 6 ++ src/Views/Welcome.axaml | 59 ++++++++++++++++--- src/Views/Welcome.axaml.cs | 6 ++ 10 files changed, 121 insertions(+), 16 deletions(-) rename src/Commands/{CountLocalChangesWithoutUntracked.cs => CountLocalChanges.cs} (66%) diff --git a/src/Commands/CountLocalChangesWithoutUntracked.cs b/src/Commands/CountLocalChanges.cs similarity index 66% rename from src/Commands/CountLocalChangesWithoutUntracked.cs rename to src/Commands/CountLocalChanges.cs index 769d732e9..17916926d 100644 --- a/src/Commands/CountLocalChangesWithoutUntracked.cs +++ b/src/Commands/CountLocalChanges.cs @@ -3,13 +3,14 @@ namespace SourceGit.Commands { - public class CountLocalChangesWithoutUntracked : Command + public class CountLocalChanges : Command { - public CountLocalChangesWithoutUntracked(string repo) + public CountLocalChanges(string repo, bool includeUntracked) { + var option = includeUntracked ? "-uall" : "-uno"; WorkingDirectory = repo; Context = repo; - Args = "--no-optional-locks status -uno --ignore-submodules=all --porcelain"; + Args = $"--no-optional-locks status {option} --ignore-submodules=all --porcelain"; } public async Task GetResultAsync() diff --git a/src/ViewModels/Checkout.cs b/src/ViewModels/Checkout.cs index 98b2a21ff..25f3e05db 100644 --- a/src/ViewModels/Checkout.cs +++ b/src/ViewModels/Checkout.cs @@ -58,7 +58,7 @@ public override async Task Sure() if (!DiscardLocalChanges) { - var changes = await new Commands.CountLocalChangesWithoutUntracked(_repo.FullPath).GetResultAsync(); + var changes = await new Commands.CountLocalChanges(_repo.FullPath, false).GetResultAsync(); if (changes > 0) { succ = await new Commands.Stash(_repo.FullPath) diff --git a/src/ViewModels/CheckoutAndFastForward.cs b/src/ViewModels/CheckoutAndFastForward.cs index 550245fe7..32ce28798 100644 --- a/src/ViewModels/CheckoutAndFastForward.cs +++ b/src/ViewModels/CheckoutAndFastForward.cs @@ -63,7 +63,7 @@ public override async Task Sure() if (!DiscardLocalChanges) { - var changes = await new Commands.CountLocalChangesWithoutUntracked(_repo.FullPath).GetResultAsync(); + var changes = await new Commands.CountLocalChanges(_repo.FullPath, false).GetResultAsync(); if (changes > 0) { succ = await new Commands.Stash(_repo.FullPath) diff --git a/src/ViewModels/CheckoutCommit.cs b/src/ViewModels/CheckoutCommit.cs index 459fba4dd..b4d4438c7 100644 --- a/src/ViewModels/CheckoutCommit.cs +++ b/src/ViewModels/CheckoutCommit.cs @@ -58,7 +58,7 @@ public override async Task Sure() if (!DiscardLocalChanges) { - var changes = await new Commands.CountLocalChangesWithoutUntracked(_repo.FullPath).GetResultAsync(); + var changes = await new Commands.CountLocalChanges(_repo.FullPath, false).GetResultAsync(); if (changes > 0) { succ = await new Commands.Stash(_repo.FullPath) diff --git a/src/ViewModels/CreateBranch.cs b/src/ViewModels/CreateBranch.cs index d213f1ecb..3958e0d56 100644 --- a/src/ViewModels/CreateBranch.cs +++ b/src/ViewModels/CreateBranch.cs @@ -142,7 +142,7 @@ public override async Task Sure() var needPopStash = false; if (!DiscardLocalChanges) { - var changes = await new Commands.CountLocalChangesWithoutUntracked(_repo.FullPath).GetResultAsync(); + var changes = await new Commands.CountLocalChanges(_repo.FullPath, false).GetResultAsync(); if (changes > 0) { succ = await new Commands.Stash(_repo.FullPath) diff --git a/src/ViewModels/Pull.cs b/src/ViewModels/Pull.cs index 2a976a060..378d63f40 100644 --- a/src/ViewModels/Pull.cs +++ b/src/ViewModels/Pull.cs @@ -119,7 +119,7 @@ public override async Task Sure() Use(log); var updateSubmodules = IsRecurseSubmoduleVisible && RecurseSubmodules; - var changes = await new Commands.CountLocalChangesWithoutUntracked(_repo.FullPath).GetResultAsync(); + var changes = await new Commands.CountLocalChanges(_repo.FullPath, false).GetResultAsync(); var needPopStash = false; if (changes > 0) { diff --git a/src/ViewModels/RepositoryNode.cs b/src/ViewModels/RepositoryNode.cs index d69f9d9dc..ea9c3fbe3 100644 --- a/src/ViewModels/RepositoryNode.cs +++ b/src/ViewModels/RepositoryNode.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.IO; using System.Text.Json.Serialization; - +using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels @@ -62,6 +62,20 @@ public int Depth set; } = 0; + [JsonIgnore] + public Models.Branch CurrentBranch + { + get => _currentBranch; + private set => SetProperty(ref _currentBranch, value); + } + + [JsonIgnore] + public int LocalChanges + { + get => _localChanges; + private set => SetProperty(ref _localChanges, value); + } + public List SubNodes { get; @@ -122,11 +136,44 @@ public void Delete() activePage.Popup = new DeleteRepositoryNode(this); } + public async Task UpdateStatusAsync() + { + if (!_isRepository || !Directory.Exists(_id)) + { + CurrentBranch = null; + LocalChanges = 0; + + if (SubNodes.Count > 0) + { + foreach (var subNode in SubNodes) + await subNode.UpdateStatusAsync(); + } + + return; + } + + LocalChanges = await new Commands.CountLocalChanges(_id, true) { RaiseError = false }.GetResultAsync(); + + var branches = await new Commands.QueryBranches(_id) { RaiseError = false }.GetResultAsync(); + foreach (var branch in branches) + { + if (branch.IsCurrent) + { + CurrentBranch = branch; + return; + } + } + + CurrentBranch = null; + } + private string _id = string.Empty; private string _name = string.Empty; private bool _isRepository = false; private int _bookmark = 0; private bool _isExpanded = false; private bool _isVisible = true; + private Models.Branch _currentBranch = null; + private int _localChanges = 0; } } diff --git a/src/ViewModels/Welcome.cs b/src/ViewModels/Welcome.cs index 2e6b3a6a3..c020cdda5 100644 --- a/src/ViewModels/Welcome.cs +++ b/src/ViewModels/Welcome.cs @@ -52,6 +52,12 @@ public void Refresh() Rows.AddRange(rows); } + public async Task UpdateStatusAsync() + { + foreach (var node in Preferences.Instance.RepositoryNodes) + await node.UpdateStatusAsync(); + } + public void ToggleNodeIsExpanded(RepositoryNode node) { node.IsExpanded = !node.IsExpanded; diff --git a/src/Views/Welcome.axaml b/src/Views/Welcome.axaml index 0f625e7af..d31bdb55e 100644 --- a/src/Views/Welcome.axaml +++ b/src/Views/Welcome.axaml @@ -3,6 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:c="using:SourceGit.Converters" + xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" xmlns:v="using:SourceGit.Views" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" @@ -106,7 +107,7 @@ + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/Welcome.axaml.cs b/src/Views/Welcome.axaml.cs index 365a7cbee..b9010bfe2 100644 --- a/src/Views/Welcome.axaml.cs +++ b/src/Views/Welcome.axaml.cs @@ -61,6 +61,12 @@ public Welcome() InitializeComponent(); } + protected override async void OnLoaded(RoutedEventArgs e) + { + base.OnLoaded(e); + await ViewModels.Welcome.Instance.UpdateStatusAsync(); + } + protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); From f88f920f57f62916e3cbe2a37348d342fd586744 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 22 Oct 2025 16:11:05 +0800 Subject: [PATCH 44/72] enhance: limit the rate to call `UpdateStatusAsync` and add hotkey `F5` to call it forcely Signed-off-by: leo --- src/ViewModels/Welcome.cs | 11 ++++++++++- src/Views/Launcher.axaml.cs | 6 ++++++ src/Views/Welcome.axaml.cs | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/ViewModels/Welcome.cs b/src/ViewModels/Welcome.cs index c020cdda5..3cb83b6ba 100644 --- a/src/ViewModels/Welcome.cs +++ b/src/ViewModels/Welcome.cs @@ -52,8 +52,16 @@ public void Refresh() Rows.AddRange(rows); } - public async Task UpdateStatusAsync() + public async Task UpdateStatusAsync(bool force) { + if (!force) + { + var passed = DateTime.Now - _lastUpdateStatus; + if (passed.TotalSeconds < 10.0) + return; + } + + _lastUpdateStatus = DateTime.Now; foreach (var node in Preferences.Instance.RepositoryNodes) await node.UpdateStatusAsync(); } @@ -275,6 +283,7 @@ private void MakeTreeRows(List rows, List nodes, } } + private DateTime _lastUpdateStatus = DateTime.UnixEpoch.ToLocalTime(); private string _searchFilter = string.Empty; } } diff --git a/src/Views/Launcher.axaml.cs b/src/Views/Launcher.axaml.cs index a8329dd01..008b98f72 100644 --- a/src/Views/Launcher.axaml.cs +++ b/src/Views/Launcher.axaml.cs @@ -266,6 +266,12 @@ protected override async void OnKeyDown(KeyEventArgs e) e.Handled = true; return; } + else if (vm.ActivePage.Data is ViewModels.Welcome welcome) + { + e.Handled = true; + await welcome.UpdateStatusAsync(true); + return; + } } base.OnKeyDown(e); diff --git a/src/Views/Welcome.axaml.cs b/src/Views/Welcome.axaml.cs index b9010bfe2..fd00a8995 100644 --- a/src/Views/Welcome.axaml.cs +++ b/src/Views/Welcome.axaml.cs @@ -64,7 +64,7 @@ public Welcome() protected override async void OnLoaded(RoutedEventArgs e) { base.OnLoaded(e); - await ViewModels.Welcome.Instance.UpdateStatusAsync(); + await ViewModels.Welcome.Instance.UpdateStatusAsync(false); } protected override void OnKeyDown(KeyEventArgs e) From c651e71229347d8c926f855cbee439c69aa2a8e5 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 22 Oct 2025 16:34:32 +0800 Subject: [PATCH 45/72] refactor: move rate limitation from `Welcome` to `RepositoryNode` Signed-off-by: leo --- src/ViewModels/RepositoryNode.cs | 16 +++++++++++++--- src/ViewModels/Welcome.cs | 11 +---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/ViewModels/RepositoryNode.cs b/src/ViewModels/RepositoryNode.cs index ea9c3fbe3..eb813d830 100644 --- a/src/ViewModels/RepositoryNode.cs +++ b/src/ViewModels/RepositoryNode.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Text.Json.Serialization; using System.Threading.Tasks; @@ -136,7 +137,7 @@ public void Delete() activePage.Popup = new DeleteRepositoryNode(this); } - public async Task UpdateStatusAsync() + public async Task UpdateStatusAsync(bool force) { if (!_isRepository || !Directory.Exists(_id)) { @@ -146,12 +147,20 @@ public async Task UpdateStatusAsync() if (SubNodes.Count > 0) { foreach (var subNode in SubNodes) - await subNode.UpdateStatusAsync(); + await subNode.UpdateStatusAsync(force); } return; } + if (!force) + { + var passed = DateTime.Now - _lastUpdateStatus; + if (passed.TotalSeconds < 10.0) + return; + } + + _lastUpdateStatus = DateTime.Now; LocalChanges = await new Commands.CountLocalChanges(_id, true) { RaiseError = false }.GetResultAsync(); var branches = await new Commands.QueryBranches(_id) { RaiseError = false }.GetResultAsync(); @@ -175,5 +184,6 @@ public async Task UpdateStatusAsync() private bool _isVisible = true; private Models.Branch _currentBranch = null; private int _localChanges = 0; + private DateTime _lastUpdateStatus = DateTime.UnixEpoch.ToLocalTime(); } } diff --git a/src/ViewModels/Welcome.cs b/src/ViewModels/Welcome.cs index 3cb83b6ba..014f748fb 100644 --- a/src/ViewModels/Welcome.cs +++ b/src/ViewModels/Welcome.cs @@ -54,16 +54,8 @@ public void Refresh() public async Task UpdateStatusAsync(bool force) { - if (!force) - { - var passed = DateTime.Now - _lastUpdateStatus; - if (passed.TotalSeconds < 10.0) - return; - } - - _lastUpdateStatus = DateTime.Now; foreach (var node in Preferences.Instance.RepositoryNodes) - await node.UpdateStatusAsync(); + await node.UpdateStatusAsync(force); } public void ToggleNodeIsExpanded(RepositoryNode node) @@ -283,7 +275,6 @@ private void MakeTreeRows(List rows, List nodes, } } - private DateTime _lastUpdateStatus = DateTime.UnixEpoch.ToLocalTime(); private string _searchFilter = string.Empty; } } From 02ecd581c75485b75b6ac36518edd82a849ec5a0 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 22 Oct 2025 17:42:51 +0800 Subject: [PATCH 46/72] refactor: use new command `QueryRepositoryStatus` to update repository status in `Welcome` page Signed-off-by: leo --- src/Commands/QueryRepositoryStatus.cs | 59 ++++++++++++++++++++ src/Models/Branch.cs | 2 +- src/Models/RepositoryStatus.cs | 29 ++++++++++ src/ViewModels/RepositoryNode.cs | 33 +++--------- src/Views/Welcome.axaml | 78 +++++++++++++-------------- 5 files changed, 133 insertions(+), 68 deletions(-) create mode 100644 src/Commands/QueryRepositoryStatus.cs create mode 100644 src/Models/RepositoryStatus.cs diff --git a/src/Commands/QueryRepositoryStatus.cs b/src/Commands/QueryRepositoryStatus.cs new file mode 100644 index 000000000..32c2489a6 --- /dev/null +++ b/src/Commands/QueryRepositoryStatus.cs @@ -0,0 +1,59 @@ +using System; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace SourceGit.Commands +{ + public partial class QueryRepositoryStatus : Command + { + [GeneratedRegex(@"ahead\s(\d+)")] + private static partial Regex REG_AHEAD(); + + [GeneratedRegex(@"behind\s(\d+)")] + private static partial Regex REG_BEHIND(); + + public QueryRepositoryStatus(string repo) + { + WorkingDirectory = repo; + RaiseError = false; + } + + public async Task GetResultAsync() + { + Args = "branch -l -v --format=\"%(refname:short)%00%(HEAD)%00%(upstream:track,nobracket)\""; + var rs = await ReadToEndAsync().ConfigureAwait(false); + if (!rs.IsSuccess) + return null; + + var status = new Models.RepositoryStatus(); + var lines = rs.StdOut.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + var parts = line.Split('\0'); + if (parts.Length != 3 || !parts[1].Equals("*", StringComparison.Ordinal)) + continue; + + status.CurrentBranch = parts[0]; + if (!string.IsNullOrEmpty(parts[2])) + ParseTrackStatus(status, parts[2]); + } + + status.LocalChanges = await new CountLocalChanges(WorkingDirectory, true) { RaiseError = false } + .GetResultAsync() + .ConfigureAwait(false); + + return status; + } + + private void ParseTrackStatus(Models.RepositoryStatus status, string input) + { + var aheadMatch = REG_AHEAD().Match(input); + if (aheadMatch.Success) + status.Ahead = int.Parse(aheadMatch.Groups[1].Value); + + var behindMatch = REG_BEHIND().Match(input); + if (behindMatch.Success) + status.Behind = int.Parse(behindMatch.Groups[1].Value); + } + } +} diff --git a/src/Models/Branch.cs b/src/Models/Branch.cs index f2c593d31..47aa2153a 100644 --- a/src/Models/Branch.cs +++ b/src/Models/Branch.cs @@ -26,7 +26,7 @@ public class Branch public bool HasWorktree => !IsCurrent && !string.IsNullOrEmpty(WorktreePath); public string FriendlyName => IsLocal ? Name : $"{Remote}/{Name}"; - public bool IsTrackStatusVisible => Ahead.Count + Behind.Count > 0; + public bool IsTrackStatusVisible => Ahead.Count > 0 || Behind.Count > 0; public string TrackStatusDescription { diff --git a/src/Models/RepositoryStatus.cs b/src/Models/RepositoryStatus.cs new file mode 100644 index 000000000..c7c498ae5 --- /dev/null +++ b/src/Models/RepositoryStatus.cs @@ -0,0 +1,29 @@ +namespace SourceGit.Models +{ + public class RepositoryStatus + { + public string CurrentBranch { get; set; } = string.Empty; + public int Ahead { get; set; } = 0; + public int Behind { get; set; } = 0; + public int LocalChanges { get; set; } = 0; + + public bool IsTrackingStatusVisible + { + get + { + return Ahead > 0 || Behind > 0; + } + } + + public string TrackingDescription + { + get + { + if (Ahead > 0) + return Behind > 0 ? $"{Ahead}↑ {Behind}↓" : $"{Ahead}↑"; + + return Behind > 0 ? $"{Behind}↓" : string.Empty; + } + } + } +} diff --git a/src/ViewModels/RepositoryNode.cs b/src/ViewModels/RepositoryNode.cs index eb813d830..5dc23ed3f 100644 --- a/src/ViewModels/RepositoryNode.cs +++ b/src/ViewModels/RepositoryNode.cs @@ -64,17 +64,10 @@ public int Depth } = 0; [JsonIgnore] - public Models.Branch CurrentBranch + public Models.RepositoryStatus Status { - get => _currentBranch; - private set => SetProperty(ref _currentBranch, value); - } - - [JsonIgnore] - public int LocalChanges - { - get => _localChanges; - private set => SetProperty(ref _localChanges, value); + get => _status; + private set => SetProperty(ref _status, value); } public List SubNodes @@ -141,8 +134,7 @@ public async Task UpdateStatusAsync(bool force) { if (!_isRepository || !Directory.Exists(_id)) { - CurrentBranch = null; - LocalChanges = 0; + Status = null; if (SubNodes.Count > 0) { @@ -161,19 +153,7 @@ public async Task UpdateStatusAsync(bool force) } _lastUpdateStatus = DateTime.Now; - LocalChanges = await new Commands.CountLocalChanges(_id, true) { RaiseError = false }.GetResultAsync(); - - var branches = await new Commands.QueryBranches(_id) { RaiseError = false }.GetResultAsync(); - foreach (var branch in branches) - { - if (branch.IsCurrent) - { - CurrentBranch = branch; - return; - } - } - - CurrentBranch = null; + Status = await new Commands.QueryRepositoryStatus(_id).GetResultAsync(); } private string _id = string.Empty; @@ -182,8 +162,7 @@ public async Task UpdateStatusAsync(bool force) private int _bookmark = 0; private bool _isExpanded = false; private bool _isVisible = true; - private Models.Branch _currentBranch = null; - private int _localChanges = 0; + private Models.RepositoryStatus _status = null; private DateTime _lastUpdateStatus = DateTime.UnixEpoch.ToLocalTime(); } } diff --git a/src/Views/Welcome.axaml b/src/Views/Welcome.axaml index d31bdb55e..8bff914eb 100644 --- a/src/Views/Welcome.axaml +++ b/src/Views/Welcome.axaml @@ -158,48 +158,46 @@ IsVisible="{Binding IsInvalid}"/> - - - - - - - - - - + + + + + + FontSize="11" + Foreground="{DynamicResource Brush.Window}"/> + - - - - - - - - - - - + + + + + + + + + + + + From b412c9a13f0996515a003e8188dcd61feb07d0a3 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 22 Oct 2025 17:53:22 +0800 Subject: [PATCH 47/72] enhance: cancel updating repository status when `Welcome` page is unloaded Signed-off-by: leo --- src/ViewModels/RepositoryNode.cs | 8 ++++++-- src/ViewModels/Welcome.cs | 5 +++-- src/Views/Launcher.axaml.cs | 2 +- src/Views/Welcome.axaml.cs | 10 +++++++++- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/ViewModels/RepositoryNode.cs b/src/ViewModels/RepositoryNode.cs index 5dc23ed3f..24f5f66e2 100644 --- a/src/ViewModels/RepositoryNode.cs +++ b/src/ViewModels/RepositoryNode.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Text.Json.Serialization; +using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; @@ -130,8 +131,11 @@ public void Delete() activePage.Popup = new DeleteRepositoryNode(this); } - public async Task UpdateStatusAsync(bool force) + public async Task UpdateStatusAsync(bool force, CancellationToken? token) { + if (token is { IsCancellationRequested: true }) + return; + if (!_isRepository || !Directory.Exists(_id)) { Status = null; @@ -139,7 +143,7 @@ public async Task UpdateStatusAsync(bool force) if (SubNodes.Count > 0) { foreach (var subNode in SubNodes) - await subNode.UpdateStatusAsync(force); + await subNode.UpdateStatusAsync(force, token); } return; diff --git a/src/ViewModels/Welcome.cs b/src/ViewModels/Welcome.cs index 014f748fb..d3416788e 100644 --- a/src/ViewModels/Welcome.cs +++ b/src/ViewModels/Welcome.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; using System.Threading.Tasks; using Avalonia.Collections; @@ -52,10 +53,10 @@ public void Refresh() Rows.AddRange(rows); } - public async Task UpdateStatusAsync(bool force) + public async Task UpdateStatusAsync(bool force, CancellationToken? token) { foreach (var node in Preferences.Instance.RepositoryNodes) - await node.UpdateStatusAsync(force); + await node.UpdateStatusAsync(force, token); } public void ToggleNodeIsExpanded(RepositoryNode node) diff --git a/src/Views/Launcher.axaml.cs b/src/Views/Launcher.axaml.cs index 008b98f72..6998fb8ae 100644 --- a/src/Views/Launcher.axaml.cs +++ b/src/Views/Launcher.axaml.cs @@ -269,7 +269,7 @@ protected override async void OnKeyDown(KeyEventArgs e) else if (vm.ActivePage.Data is ViewModels.Welcome welcome) { e.Handled = true; - await welcome.UpdateStatusAsync(true); + await welcome.UpdateStatusAsync(true, null); return; } } diff --git a/src/Views/Welcome.axaml.cs b/src/Views/Welcome.axaml.cs index fd00a8995..6a5ea2df1 100644 --- a/src/Views/Welcome.axaml.cs +++ b/src/Views/Welcome.axaml.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -64,7 +65,13 @@ public Welcome() protected override async void OnLoaded(RoutedEventArgs e) { base.OnLoaded(e); - await ViewModels.Welcome.Instance.UpdateStatusAsync(false); + await ViewModels.Welcome.Instance.UpdateStatusAsync(false, _cancellation.Token); + } + + protected override void OnUnloaded(RoutedEventArgs e) + { + _cancellation.Cancel(); + base.OnUnloaded(e); } protected override void OnKeyDown(KeyEventArgs e) @@ -361,5 +368,6 @@ private void OnDoubleTappedTreeNode(object sender, TappedEventArgs e) private Point _pressedTreeNodePosition = new Point(); private bool _startDragTreeNode = false; private readonly DataFormat _dndRepoNode = DataFormat.CreateStringApplicationFormat("sourcegit-dnd-repo-node"); + private CancellationTokenSource _cancellation = new CancellationTokenSource(); } } From fb046d551f52837544d1d5ece77c64c0e99267ae Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 22 Oct 2025 17:59:21 +0800 Subject: [PATCH 48/72] enhance: cache last status of repository for better experience Signed-off-by: leo --- src/ViewModels/RepositoryNode.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ViewModels/RepositoryNode.cs b/src/ViewModels/RepositoryNode.cs index 24f5f66e2..770e1b86e 100644 --- a/src/ViewModels/RepositoryNode.cs +++ b/src/ViewModels/RepositoryNode.cs @@ -64,11 +64,10 @@ public int Depth set; } = 0; - [JsonIgnore] public Models.RepositoryStatus Status { get => _status; - private set => SetProperty(ref _status, value); + set => SetProperty(ref _status, value); } public List SubNodes From b15d714b849e679121e7d7de2299f7fc20ce36d9 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 22 Oct 2025 18:15:50 +0800 Subject: [PATCH 49/72] enhance: update repository status immediately on the first time that it is added Signed-off-by: leo --- src/ViewModels/Clone.cs | 2 ++ src/ViewModels/Init.cs | 4 +++- src/ViewModels/RepositoryNode.cs | 17 ++++++++++++++--- src/ViewModels/ScanRepositories.cs | 6 ++++-- src/ViewModels/Welcome.cs | 18 ++++++++++++++++-- src/Views/Welcome.axaml.cs | 4 ++-- src/Views/WelcomeToolbar.axaml.cs | 2 +- 7 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/ViewModels/Clone.cs b/src/ViewModels/Clone.cs index cfcf51bf2..73033682e 100644 --- a/src/ViewModels/Clone.cs +++ b/src/ViewModels/Clone.cs @@ -150,6 +150,8 @@ public override async Task Sure() log.Complete(); var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(path, null, true); + await node.UpdateStatusAsync(false, null); + var launcher = App.GetLauncher(); LauncherPage page = null; foreach (var one in launcher.Pages) diff --git a/src/ViewModels/Init.cs b/src/ViewModels/Init.cs index 7f349917f..f6740d0f6 100644 --- a/src/ViewModels/Init.cs +++ b/src/ViewModels/Init.cs @@ -39,7 +39,9 @@ public override async Task Sure() if (succ) { - Preferences.Instance.FindOrAddNodeByRepositoryPath(_targetPath, _parentNode, true); + var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(_targetPath, _parentNode, true); + await node.UpdateStatusAsync(false, null); + Welcome.Instance.Refresh(); } return succ; diff --git a/src/ViewModels/RepositoryNode.cs b/src/ViewModels/RepositoryNode.cs index 770e1b86e..1f5515b9d 100644 --- a/src/ViewModels/RepositoryNode.cs +++ b/src/ViewModels/RepositoryNode.cs @@ -135,19 +135,30 @@ public async Task UpdateStatusAsync(bool force, CancellationToken? token) if (token is { IsCancellationRequested: true }) return; - if (!_isRepository || !Directory.Exists(_id)) + if (!_isRepository) { Status = null; if (SubNodes.Count > 0) { - foreach (var subNode in SubNodes) - await subNode.UpdateStatusAsync(force, token); + // avoid collection was modified while enumerating. + var nodes = new List(); + nodes.AddRange(SubNodes); + + foreach (var node in nodes) + await node.UpdateStatusAsync(force, token); } return; } + if (!Directory.Exists(_id)) + { + _lastUpdateStatus = DateTime.Now; + Status = null; + return; + } + if (!force) { var passed = DateTime.Now - _lastUpdateStatus; diff --git a/src/ViewModels/ScanRepositories.cs b/src/ViewModels/ScanRepositories.cs index 6802897aa..e7316dba5 100644 --- a/src/ViewModels/ScanRepositories.cs +++ b/src/ViewModels/ScanRepositories.cs @@ -97,13 +97,15 @@ public override async Task Sure() var parent = new DirectoryInfo(f).Parent!.FullName.Replace('\\', '/').TrimEnd('/'); if (parent.Equals(normalizedRoot, StringComparison.Ordinal)) { - Preferences.Instance.FindOrAddNodeByRepositoryPath(f, null, false, false); + var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(f, null, false, false); + await node.UpdateStatusAsync(false, null); } else if (parent.StartsWith(normalizedRoot, StringComparison.Ordinal)) { var relative = parent.Substring(normalizedRoot.Length).TrimStart('/'); var group = FindOrCreateGroupRecursive(Preferences.Instance.RepositoryNodes, relative); - Preferences.Instance.FindOrAddNodeByRepositoryPath(f, group, false, false); + var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(f, group, false, false); + await node.UpdateStatusAsync(false, null); } } diff --git a/src/ViewModels/Welcome.cs b/src/ViewModels/Welcome.cs index d3416788e..1fa1797a8 100644 --- a/src/ViewModels/Welcome.cs +++ b/src/ViewModels/Welcome.cs @@ -55,8 +55,19 @@ public void Refresh() public async Task UpdateStatusAsync(bool force, CancellationToken? token) { - foreach (var node in Preferences.Instance.RepositoryNodes) + if (_isUpdatingStatus) + return; + + _isUpdatingStatus = true; + + // avoid collection was modified while enumerating. + var nodes = new List(); + nodes.AddRange(Preferences.Instance.RepositoryNodes); + + foreach (var node in nodes) await node.UpdateStatusAsync(force, token); + + _isUpdatingStatus = false; } public void ToggleNodeIsExpanded(RepositoryNode node) @@ -130,9 +141,11 @@ public void InitRepository(string path, RepositoryNode parent, string reason) activePage.Popup = new Init(activePage.Node.Id, path, parent, reason); } - public void AddRepository(string path, RepositoryNode parent, bool moveNode, bool open) + public async Task AddRepositoryAsync(string path, RepositoryNode parent, bool moveNode, bool open) { var node = Preferences.Instance.FindOrAddNodeByRepositoryPath(path, parent, moveNode); + await node.UpdateStatusAsync(false, null); + if (open) node.Open(); } @@ -277,5 +290,6 @@ private void MakeTreeRows(List rows, List nodes, } private string _searchFilter = string.Empty; + private bool _isUpdatingStatus = false; } } diff --git a/src/Views/Welcome.axaml.cs b/src/Views/Welcome.axaml.cs index 6a5ea2df1..7a3565c02 100644 --- a/src/Views/Welcome.axaml.cs +++ b/src/Views/Welcome.axaml.cs @@ -279,7 +279,7 @@ private async void DropOnTreeView(object sender, DragEventArgs e) var path = await ViewModels.Welcome.Instance.GetRepositoryRootAsync(item.Path.LocalPath); if (!string.IsNullOrEmpty(path)) { - ViewModels.Welcome.Instance.AddRepository(path, null, true, false); + await ViewModels.Welcome.Instance.AddRepositoryAsync(path, null, true, false); refresh = true; } } @@ -338,7 +338,7 @@ private async void DropOnTreeNode(object sender, DragEventArgs e) var path = await ViewModels.Welcome.Instance.GetRepositoryRootAsync(item.Path.LocalPath); if (!string.IsNullOrEmpty(path)) { - ViewModels.Welcome.Instance.AddRepository(path, to, true, false); + await ViewModels.Welcome.Instance.AddRepositoryAsync(path, to, true, false); refresh = true; } } diff --git a/src/Views/WelcomeToolbar.axaml.cs b/src/Views/WelcomeToolbar.axaml.cs index 65b4ca129..7d901e35c 100644 --- a/src/Views/WelcomeToolbar.axaml.cs +++ b/src/Views/WelcomeToolbar.axaml.cs @@ -47,7 +47,7 @@ private async void OpenLocalRepository(object _1, RoutedEventArgs e) var repoPath = await ViewModels.Welcome.Instance.GetRepositoryRootAsync(folderPath); if (!string.IsNullOrEmpty(repoPath)) { - ViewModels.Welcome.Instance.AddRepository(repoPath, null, false, true); + await ViewModels.Welcome.Instance.AddRepositoryAsync(repoPath, null, false, true); ViewModels.Welcome.Instance.Refresh(); } else if (Directory.Exists(folderPath)) From c4929dddc86ea24213f46cf91ca748c855db12b7 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 22 Oct 2025 19:46:48 +0800 Subject: [PATCH 50/72] refactor: remove unused `ConterPresenter` Signed-off-by: leo --- src/Views/Repository.axaml | 42 ++++++++++------ src/Views/Repository.axaml.cs | 93 ----------------------------------- 2 files changed, 26 insertions(+), 109 deletions(-) diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index 18c5c06a3..59512aba2 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -131,14 +131,19 @@ - + + + - + + + - - - - diff --git a/src/Views/RepositoryConfigure.axaml b/src/Views/RepositoryConfigure.axaml index 91af9c447..b56925617 100644 --- a/src/Views/RepositoryConfigure.axaml +++ b/src/Views/RepositoryConfigure.axaml @@ -242,13 +242,9 @@ - - - - @@ -347,13 +343,10 @@ - - - From a14ef05641f57c853cb0088e44bf75f8f2c46f58 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 22 Oct 2025 20:15:34 +0800 Subject: [PATCH 52/72] enhance: supports to find `zed` or `zeditor` from `PATH` and `~/.local/bin/zed` (#1869) Signed-off-by: leo --- src/Native/Linux.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Native/Linux.cs b/src/Native/Linux.cs index f6eb4ebf3..38d3da8b2 100644 --- a/src/Native/Linux.cs +++ b/src/Native/Linux.cs @@ -57,7 +57,18 @@ public string FindTerminal(Models.ShellOrTerminal shell) finder.Fleet(() => FindJetBrainsFleet(localAppDataDir)); finder.FindJetBrainsFromToolbox(() => Path.Combine(localAppDataDir, "JetBrains/Toolbox")); finder.SublimeText(() => FindExecutable("subl")); - finder.Zed(() => FindExecutable("zeditor")); + finder.Zed(() => + { + var exec = FindExecutable("zeditor"); + if (string.IsNullOrEmpty(exec)) + { + exec = FindExecutable("zed"); + if (string.IsNullOrEmpty(exec)) + exec = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", "zed"); + } + + return exec; + }); return finder.Tools; } From ba06f83f1dca3a483f50b7adc73d486dcd90b954 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 22 Oct 2025 20:29:28 +0800 Subject: [PATCH 53/72] refactor: `FindExecutable` on Linux will always try to find file under `~/.local/bin` Signed-off-by: leo --- src/Native/Linux.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/Native/Linux.cs b/src/Native/Linux.cs index 38d3da8b2..9cae29f46 100644 --- a/src/Native/Linux.cs +++ b/src/Native/Linux.cs @@ -60,14 +60,7 @@ public string FindTerminal(Models.ShellOrTerminal shell) finder.Zed(() => { var exec = FindExecutable("zeditor"); - if (string.IsNullOrEmpty(exec)) - { - exec = FindExecutable("zed"); - if (string.IsNullOrEmpty(exec)) - exec = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", "zed"); - } - - return exec; + return string.IsNullOrEmpty(exec) ? FindExecutable("zed") : exec; }); return finder.Tools; } @@ -141,7 +134,8 @@ private string FindExecutable(string filename) return test; } - return string.Empty; + var local = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "bin", filename); + return File.Exists(local) ? local : string.Empty; } private string FindJetBrainsFleet(string localAppDataDir) From adf48be160de8755fdf61a57eeaf1a3316bbbf6c Mon Sep 17 00:00:00 2001 From: AquariusStar <48148723+AquariusStar@users.noreply.github.com> Date: Thu, 23 Oct 2025 05:30:56 +0300 Subject: [PATCH 54/72] localization: update Russian translation (#1870) --- src/Resources/Locales/ru_RU.axaml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index 56db54577..3373ec63b 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -56,6 +56,7 @@ Пропустить Раздвоение. Сделать текущую ревизию хорошей или плохой и переключиться на другой. Расследование + Расследование на предыдущей редакции РАССЛЕДОВАНИЕ В ЭТОМ ФАЙЛЕ НЕ ПОДДЕРЖИВАЕТСЯ!!! Переключиться на ${0}$... Сравнить с ${0}$ @@ -188,19 +189,19 @@ Введите тему ревизии Настройка репозитория ШАБЛОН РЕВИЗИИ - Параметры сборки: + Встроенные параметры: ${branch_name} Имя текущей локальной ветки ${files_num} Количество изменённых файлов ${files} Пути изменённых файлов - ${files:N} Максимальное количество N путей изменённых файлов - ${pure_files} Похожие ${files}, но только чистые имена файлов - ${pure_files:N} Похожие ${files:N}, но без каталогов + ${files:N} Пути изменённых файлов, не более N + ${pure_files} То же, что и ${files}, но только имена файлов + ${pure_files:N} То же, что и ${files:N}, но только имена файлов Cодержание: Название: ПОЛЬЗОВАТЕЛЬСКОЕ ДЕЙСТВИЕ Аргументы: - Параметры сборки: + Встроенные параметры: ${REPO} Путь репозитория ${REMOTE} Выбранная удаённая ветка @@ -225,6 +226,7 @@ GIT Автозагрузка изменений Минут(а/ы) + Общепринятые типы ревизии Внешний репозиторий по умолчанию Предпочтительный режим слияния ОТСЛЕЖИВАНИЕ ПРОБЛЕМ @@ -258,6 +260,7 @@ Метка: Опции: Используйте разделитель «|» для опций + Встроенные переменные ${REPO}, ${REMOTE}, ${BRANCH}, ${BRANCH_FRIENDLY_NAME}, ${SHA}, и ${TAG} останутся здесь доступными Тип: Рабочие пространства Цвет @@ -266,7 +269,7 @@ ПРОДОЛЖИТЬ Обнаружена пустая ревизия! Вы хотите продолжить (--allow-empty)? Сформировать всё и зафиксировать ревизию - Обнаружена пустая ревизия! Вы хотите продолжить (--allow-empty) или отложить всё, затем зафиксировать ревизию? + Обнаружена пустая ревизия! Вы хотите продолжить (--allow-empty) или отложить всё, а затем зафиксировать ревизию? Требуется перезапуск Вы должны перезапустить приложение после применения изменений. Общепринятый помощник по ревизии @@ -767,7 +770,7 @@ Загрузка Пропустить эту версию Обновление ПО - В настоящее время обновления недоступны. + Сейчас нет обновлений. Установить ветку подмодуля Подмодуль: Текущий: From 1a5c63da22e7a4ce485e0b89aadc95e4cd59f63b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 23 Oct 2025 02:31:16 +0000 Subject: [PATCH 55/72] doc: Update translation status and sort locale files --- TRANSLATION.md | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/TRANSLATION.md b/TRANSLATION.md index c02202014..ff6dba740 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -814,16 +814,7 @@ This document shows the translation status of each locale file in the repository
-### ![ru__RU](https://img.shields.io/badge/ru__RU-99.67%25-yellow) - -
-Missing keys in ru_RU.axaml - -- Text.Blame.BlameOnPreviousRevision -- Text.Configure.Git.ConventionalTypesOverride -- Text.ConfigureCustomActionControls.StringValue.Tip - -
+### ![ru__RU](https://img.shields.io/badge/ru__RU-%E2%88%9A-brightgreen) ### ![ta__IN](https://img.shields.io/badge/ta__IN-77.26%25-yellow) From 99389dba899656f3635945c64308967c4548500c Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 23 Oct 2025 10:59:23 +0800 Subject: [PATCH 56/72] refactor: always load commit message from `.git/MERGE_MSG` or `.git/rebase-merge/message` when there is some operation in progress (#1795) Signed-off-by: leo --- src/ViewModels/WorkingCopy.cs | 65 ++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index 1acda60c1..586a13dd0 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -769,43 +769,54 @@ private void UpdateDetail() private void UpdateInProgressState() { - if (string.IsNullOrEmpty(_commitMessage)) - { - var mergeMsgFile = Path.Combine(_repo.GitDir, "MERGE_MSG"); - if (File.Exists(mergeMsgFile)) - CommitMessage = File.ReadAllText(mergeMsgFile); - } + var oldType = _inProgressContext != null ? _inProgressContext.GetType() : null; if (File.Exists(Path.Combine(_repo.GitDir, "CHERRY_PICK_HEAD"))) - { InProgressContext = new CherryPickInProgress(_repo); - } else if (Directory.Exists(Path.Combine(_repo.GitDir, "rebase-merge")) || Directory.Exists(Path.Combine(_repo.GitDir, "rebase-apply"))) - { - var rebasing = new RebaseInProgress(_repo); - InProgressContext = rebasing; - - if (string.IsNullOrEmpty(_commitMessage)) - { - var rebaseMsgFile = Path.Combine(_repo.GitDir, "rebase-merge", "message"); - if (File.Exists(rebaseMsgFile)) - CommitMessage = File.ReadAllText(rebaseMsgFile); - else if (rebasing.StoppedAt != null) - CommitMessage = new Commands.QueryCommitFullMessage(_repo.FullPath, rebasing.StoppedAt.SHA).GetResult(); - } - } + InProgressContext = new RebaseInProgress(_repo); else if (File.Exists(Path.Combine(_repo.GitDir, "REVERT_HEAD"))) - { InProgressContext = new RevertInProgress(_repo); - } else if (File.Exists(Path.Combine(_repo.GitDir, "MERGE_HEAD"))) - { InProgressContext = new MergeInProgress(_repo); - } else - { InProgressContext = null; - } + + if (_inProgressContext == null) + return; + + if (_inProgressContext.GetType() == oldType && !string.IsNullOrEmpty(_commitMessage)) + return; + + do + { + if (LoadCommitMessageFromFile(Path.Combine(_repo.GitDir, "MERGE_MSG"))) + break; + + if (LoadCommitMessageFromFile(Path.Combine(_repo.GitDir, "rebase-merge", "message"))) + break; + + var rebaseProcessingFile = Path.Combine(_repo.GitDir, "rebase-apply", "final-commit"); + if (File.Exists(rebaseProcessingFile)) + { + var sha = File.ReadAllText(rebaseProcessingFile); + if (!string.IsNullOrEmpty(sha)) + CommitMessage = new Commands.QueryCommitFullMessage(_repo.FullPath, sha).GetResult(); + } + } while (false); + } + + private bool LoadCommitMessageFromFile(string file) + { + if (!File.Exists(file)) + return false; + + var msg = File.ReadAllText(file).Trim(); + if (string.IsNullOrEmpty(msg)) + return false; + + CommitMessage = msg; + return true; } private void SetDetail(Models.Change change, bool isUnstaged) From 89bc8a7e06db5f54766a46466151cdee846f78ee Mon Sep 17 00:00:00 2001 From: leo Date: Thu, 23 Oct 2025 14:49:48 +0800 Subject: [PATCH 57/72] feature: add original pick-order for commits in `Interactive Rebase` window (#1676) Signed-off-by: leo --- src/ViewModels/InteractiveRebase.cs | 10 ++++++++-- src/Views/InteractiveRebase.axaml | 20 ++++++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/ViewModels/InteractiveRebase.cs b/src/ViewModels/InteractiveRebase.cs index 719b8354b..5e88ce519 100644 --- a/src/ViewModels/InteractiveRebase.cs +++ b/src/ViewModels/InteractiveRebase.cs @@ -15,6 +15,11 @@ public record InteractiveRebasePrefill(string SHA, Models.InteractiveRebaseActio public class InteractiveRebaseItem : ObservableObject { + public int OriginalOrder + { + get; + } + public Models.Commit Commit { get; @@ -59,8 +64,9 @@ public string FullMessage } } - public InteractiveRebaseItem(Models.Commit c, string message, bool canSquashOrFixup) + public InteractiveRebaseItem(int order, Models.Commit c, string message, bool canSquashOrFixup) { + OriginalOrder = order; Commit = c; FullMessage = message; CanSquashOrFixup = canSquashOrFixup; @@ -142,7 +148,7 @@ public InteractiveRebase(Repository repo, Models.Commit on, InteractiveRebasePre for (var i = 0; i < commits.Count; i++) { var c = commits[i]; - list.Add(new InteractiveRebaseItem(c.Commit, c.Message, i < commits.Count - 1)); + list.Add(new InteractiveRebaseItem(commits.Count - i, c.Commit, c.Message, i < commits.Count - 1)); } var selected = list.Count > 0 ? list[0] : null; diff --git a/src/Views/InteractiveRebase.axaml b/src/Views/InteractiveRebase.axaml index 1c9ed42e5..86561d81c 100644 --- a/src/Views/InteractiveRebase.axaml +++ b/src/Views/InteractiveRebase.axaml @@ -84,6 +84,7 @@ + @@ -111,8 +112,15 @@ VerticalAlignment="Center"/> + + + - - + - +