diff --git a/TRANSLATION.md b/TRANSLATION.md index 501f81b2d..5da949857 100644 --- a/TRANSLATION.md +++ b/TRANSLATION.md @@ -15,7 +15,7 @@ This document shows the translation status of each locale file in the repository -### ![de__DE](https://img.shields.io/badge/de__DE-93.76%25-yellow) +### ![de__DE](https://img.shields.io/badge/de__DE-93.82%25-yellow)
Missing keys in de_DE.axaml @@ -90,7 +90,7 @@ This document shows the translation status of each locale file in the repository
-### ![el__GR](https://img.shields.io/badge/el__GR-93.30%25-yellow) +### ![el__GR](https://img.shields.io/badge/el__GR-93.36%25-yellow)
Missing keys in el_GR.axaml @@ -170,7 +170,7 @@ This document shows the translation status of each locale file in the repository
-### ![es__ES](https://img.shields.io/badge/es__ES-99.81%25-yellow) +### ![es__ES](https://img.shields.io/badge/es__ES-99.82%25-yellow)
Missing keys in es_ES.axaml @@ -180,7 +180,7 @@ This document shows the translation status of each locale file in the repository
-### ![fr__FR](https://img.shields.io/badge/fr__FR-89.57%25-yellow) +### ![fr__FR](https://img.shields.io/badge/fr__FR-89.67%25-yellow)
Missing keys in fr_FR.axaml @@ -300,7 +300,7 @@ This document shows the translation status of each locale file in the repository
-### ![he__IL](https://img.shields.io/badge/he__IL-89.57%25-yellow) +### ![he__IL](https://img.shields.io/badge/he__IL-89.67%25-yellow)
Missing keys in he_IL.axaml @@ -420,7 +420,7 @@ This document shows the translation status of each locale file in the repository
-### ![id__ID](https://img.shields.io/badge/id__ID-93.76%25-yellow) +### ![id__ID](https://img.shields.io/badge/id__ID-93.82%25-yellow)
Missing keys in id_ID.axaml @@ -495,7 +495,7 @@ This document shows the translation status of each locale file in the repository
-### ![it__IT](https://img.shields.io/badge/it__IT-83.61%25-yellow) +### ![it__IT](https://img.shields.io/badge/it__IT-83.76%25-yellow)
Missing keys in it_IT.axaml @@ -679,7 +679,7 @@ This document shows the translation status of each locale file in the repository
-### ![ja__JP](https://img.shields.io/badge/ja__JP-97.77%25-yellow) +### ![ja__JP](https://img.shields.io/badge/ja__JP-97.79%25-yellow)
Missing keys in ja_JP.axaml @@ -711,7 +711,7 @@ This document shows the translation status of each locale file in the repository
-### ![ko__KR](https://img.shields.io/badge/ko__KR-90.97%25-yellow) +### ![ko__KR](https://img.shields.io/badge/ko__KR-91.05%25-yellow)
Missing keys in ko_KR.axaml @@ -816,7 +816,7 @@ This document shows the translation status of each locale file in the repository
-### ![pt__BR](https://img.shields.io/badge/pt__BR-58.94%25-red) +### ![pt__BR](https://img.shields.io/badge/pt__BR-59.32%25-red)
Missing keys in pt_BR.axaml @@ -1265,7 +1265,7 @@ This document shows the translation status of each locale file in the repository
-### ![ru__RU](https://img.shields.io/badge/ru__RU-99.81%25-yellow) +### ![ru__RU](https://img.shields.io/badge/ru__RU-99.82%25-yellow)
Missing keys in ru_RU.axaml @@ -1275,7 +1275,7 @@ This document shows the translation status of each locale file in the repository
-### ![ta__IN](https://img.shields.io/badge/ta__IN-60.52%25-red) +### ![ta__IN](https://img.shields.io/badge/ta__IN-60.89%25-red)
Missing keys in ta_IN.axaml @@ -1707,7 +1707,7 @@ This document shows the translation status of each locale file in the repository
-### ![uk__UA](https://img.shields.io/badge/uk__UA-61.27%25-red) +### ![uk__UA](https://img.shields.io/badge/uk__UA-61.62%25-red)
Missing keys in uk_UA.axaml diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index de299ed6b..ef446ac8c 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -26,7 +26,13 @@ public class Commit : ObservableObject public bool IsMerged { get; set; } = false; public int Color { get; set; } = 0; - public double LeftMargin { get; set; } = 0; + public double LeftMargin + { + get => _leftMargin; + set => SetProperty(ref _leftMargin, value); + } + + private double _leftMargin; public bool IsHighlightedInGraph { diff --git a/src/Models/CommitGraph.cs b/src/Models/CommitGraph.cs index 916a79b1b..984ef6daf 100644 --- a/src/Models/CommitGraph.cs +++ b/src/Models/CommitGraph.cs @@ -41,6 +41,7 @@ public class Path(int color, bool isHighlighted) public List Points { get; } = []; public int Color { get; } = color; public bool IsHighlighted { get; } = isHighlighted; + public bool IsDotted { get; init; } } public class Link @@ -57,6 +58,7 @@ public enum DotType Default, Head, Merge, + WorkingDirectory, } public class Dot @@ -71,7 +73,7 @@ public class Dot public List Links { get; } = []; public List Dots { get; } = []; - public static CommitGraph Generate(List commits, bool firstParentOnlyEnabled, CommitGraphHighlighting highlighting, HashSet highlightExtraCommits) + public static CommitGraph Generate(List commits, bool firstParentOnlyEnabled, CommitGraphHighlighting highlighting, HashSet highlightExtraCommits, bool hasWorkingDirectory = false, bool reserveHeadLane = true) { const double unitWidth = 12; const double halfWidth = 6; @@ -85,6 +87,17 @@ public static CommitGraph Generate(List commits, bool firstParentOnlyEna var colorPicker = new ColorPicker(); var defHighlighting = highlighting == CommitGraphHighlighting.All; + // Keep HEAD's first-parent ancestry in the first lane, even when other + // branch tips appear above it. The reservation is not a Git commit or + // a visible edge; drawing starts when the actual HEAD is encountered. + PathHelper headLane = null; + var headCommit = commits.Find(x => x.IsCurrentHead); + if (reserveHeadLane && headCommit != null) + { + headLane = new PathHelper(headCommit.SHA, defHighlighting, colorPicker.Next(), new Point(10, -halfHeight)); + unsolved.Add(headLane); + } + foreach (var commit in commits) { PathHelper major = null; @@ -176,6 +189,12 @@ public static CommitGraph Generate(List commits, bool firstParentOnlyEna } commit.IsHighlightedInGraph = isHighlighted; + if (headLane != null && commit.IsCurrentHead && major == headLane) + { + headLane.Path.Points.Clear(); + headLane.Path.Points.Add(new Point(10, offsetY)); + } + // If no path found, create new curve for branch head // Otherwise, create new curve for new merged commit if (major == null) @@ -195,6 +214,9 @@ public static CommitGraph Generate(List commits, bool firstParentOnlyEna temp.Paths.Add(major.Path); } + if (headLane != null && commit.IsCurrentHead && major == headLane && commit.Parents.Count > 0 && !temp.Paths.Contains(major.Path)) + temp.Paths.Add(major.Path); + // Calculate link position of this commit. var position = new Point(major?.LastX ?? offsetX, offsetY); var dotColor = major?.Path.Color ?? 0; @@ -265,6 +287,52 @@ public static CommitGraph Generate(List commits, bool firstParentOnlyEna } unsolved.Clear(); + if (hasWorkingDirectory) + { + // Add a presentation row without moving any of the existing graph lanes. + var offset = new Vector(0, 1); + foreach (var path in temp.Paths) + for (var i = 0; i < path.Points.Count; i++) + path.Points[i] += offset; + foreach (var link in temp.Links) + { + link.Start += offset; + link.Control += offset; + link.End += offset; + } + foreach (var dot in temp.Dots) + dot.Center += offset; + + var working = new Dot { Type = DotType.WorkingDirectory, Center = new Point(10, 0.5), IsHighlighted = true }; + var headIndex = commits.FindIndex(x => x.IsCurrentHead); + if (headIndex >= 0) + { + var head = temp.Dots[headIndex]; + working.Center = new Point(head.Center.X, 0.5); + var lastJunction = working.Center; + foreach (var link in temp.Links) + { + if (link.End.X == head.Center.X && link.End.Y > lastJunction.Y && link.End.Y < head.Center.Y) + lastJunction = link.End; + } + + var connection = lastJunction == working.Center ? head.Center : lastJunction; + var edge = new Path(head.Color, true) { IsDotted = true }; + edge.Points.Add(working.Center); + edge.Points.Add(connection); + temp.Paths.Insert(0, edge); + if (connection != head.Center) + { + var continuation = new Path(head.Color, true); + continuation.Points.Add(connection); + continuation.Points.Add(head.Center); + temp.Paths.Insert(1, continuation); + } + working.Color = head.Color; + } + temp.Dots.Insert(0, working); + } + return temp; } diff --git a/src/Resources/Icons.axaml b/src/Resources/Icons.axaml index a410aef45..4b0c83c3c 100644 --- a/src/Resources/Icons.axaml +++ b/src/Resources/Icons.axaml @@ -109,6 +109,7 @@ M640 96c-158 0-288 130-288 288 0 17 3 31 5 46L105 681 96 691V928h224v-96h96v-96h96v-95c38 18 82 31 128 31 158 0 288-130 288-288s-130-288-288-288zm0 64c123 0 224 101 224 224s-101 224-224 224a235 235 0 01-109-28l-8-4H448v96h-96v96H256v96H160v-146l253-254 12-11-3-17C419 417 416 400 416 384c0-123 101-224 224-224zm64 96a64 64 0 100 128 64 64 0 100-128z M544 85c49 0 90 37 95 85h75a96 96 0 0196 89L811 267a32 32 0 01-28 32L779 299a32 32 0 01-32-28L747 267a32 32 0 00-28-32L715 235h-91a96 96 0 01-80 42H395c-33 0-62-17-80-42L224 235a32 32 0 00-32 28L192 267v576c0 16 12 30 28 32l4 0h128a32 32 0 0132 28l0 4a32 32 0 01-32 32h-128a96 96 0 01-96-89L128 843V267a96 96 0 0189-96L224 171h75a96 96 0 0195-85h150zm256 256a96 96 0 0196 89l0 7v405a96 96 0 01-89 96L800 939h-277a96 96 0 01-96-89L427 843v-405a96 96 0 0189-96L523 341h277zm-256-192H395a32 32 0 000 64h150a32 32 0 100-64z M470 722q-23 3-43 3T384 722v-150l-106 106q-34-26-60-59L324 512H174q-3-23-3-43t3-42h150L218 320q16-20 28-32t32-27L384 367V217q23-4 43-4t43 4v150l106-106q34 26 60 59l-106 107h150q3 22 3 42T680 512h-150l106 107q-16 20-28 32t-32 27l-106-106v150zM0 811q0-36 25-61t61-25 61 25 25 61-25 61-61 25-61-25T0 811z + M652 157a113 113 0 11156 161L731 395 572 236l80-80 1 1zM334 792v0H175v-159l358-358 159 159-358 358z M576 64H448v384H64v128h384v384h128V576h384V448H576z M432 0h160c27 0 48 21 48 48v336h175c36 0 53 43 28 68L539 757c-15 15-40 15-55 0L180 452c-25-25-7-68 28-68H384V48c0-27 21-48 48-48zm592 752v224c0 27-21 48-48 48H48c-27 0-48-21-48-48V752c0-27 21-48 48-48h293l98 98c40 40 105 40 145 0l98-98H976c27 0 48 21 48 48zm-248 176c0-22-18-40-40-40s-40 18-40 40s18 40 40 40s40-18 40-40zm128 0c0-22-18-40-40-40s-40 18-40 40s18 40 40 40s40-18 40-40z M592 768h-160c-27 0-48-21-48-48V384h-175c-36 0-53-43-28-68L485 11c15-15 40-15 55 0l304 304c25 25 7 68-28 68H640v336c0 27-21 48-48 48zm432-16v224c0 27-21 48-48 48H48c-27 0-48-21-48-48V752c0-27 21-48 48-48h272v16c0 62 50 112 112 112h160c62 0 112-50 112-112v-16h272c27 0 48 21 48 48zm-248 176c0-22-18-40-40-40s-40 18-40 40s18 40 40 40s40-18 40-40zm128 0c0-22-18-40-40-40s-40 18-40 40s18 40 40 40s40-18 40-40z diff --git a/src/Resources/Locales/ca_ES.axaml b/src/Resources/Locales/ca_ES.axaml index 8d2222280..0f372e076 100644 --- a/src/Resources/Locales/ca_ES.axaml +++ b/src/Resources/Locales/ca_ES.axaml @@ -1092,4 +1092,14 @@ Suprimeix Desbloqueja SÍ + Capçalera del directori de treball + Amaga + Mostra quan hi ha canvis locals + Mostra sempre + * Directori de treball * + Fitxers modificats (inclou canvis de nom, de tipus i conflictes) + Fitxers afegits i sense seguiment + Fitxers suprimits + CANVIS LOCALS + Obre a la pàgina de canvis locals diff --git a/src/Resources/Locales/de_DE.axaml b/src/Resources/Locales/de_DE.axaml index 3ba62d7aa..f4c756c4d 100644 --- a/src/Resources/Locales/de_DE.axaml +++ b/src/Resources/Locales/de_DE.axaml @@ -1026,4 +1026,14 @@ $1, $2, … Werte der Eingabe-Steuerelemente Entfernen Entsperren JA + Arbeitsverzeichnis im Graph + Ausblenden + Bei lokalen Änderungen anzeigen + Immer anzeigen + * Arbeitsverzeichnis * + Geänderte Dateien (einschließlich Umbenennungen, Typänderungen und Konflikten) + Hinzugefügte und unversionierte Dateien + Gelöschte Dateien + LOKALE ÄNDERUNGEN + Auf der Seite für lokale Änderungen öffnen diff --git a/src/Resources/Locales/el_GR.axaml b/src/Resources/Locales/el_GR.axaml index 8b2bc2010..249e3a9b4 100644 --- a/src/Resources/Locales/el_GR.axaml +++ b/src/Resources/Locales/el_GR.axaml @@ -1021,4 +1021,14 @@ Αφαίρεση Ξεκλείδωμα ΝΑΙ + Κεφαλή καταλόγου εργασίας + Απόκρυψη + Εμφάνιση όταν υπάρχουν τοπικές αλλαγές + Πάντα εμφάνιση + * Κατάλογος εργασίας * + Τροποποιημένα αρχεία (μαζί με μετονομασίες, αλλαγές τύπου και διενέξεις) + Προστιθέμενα και μη παρακολουθούμενα αρχεία + Διαγραμμένα αρχεία + ΤΟΠΙΚΕΣ ΑΛΛΑΓΕΣ + Άνοιγμα στη σελίδα τοπικών αλλαγών diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 9abb3bdcc..a574a96f5 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -686,6 +686,10 @@ Use auto-hide scrollbars Use fixed tab width in titlebar Use native window frame + Working Directory Head + Hide + Show when there are local changes + Show always DIFF/MERGE TOOL Diff Arguments Available variables: $LOCAL, $REMOTE @@ -1089,4 +1093,10 @@ Remove Unlock YES + * Working Directory * + Modified files (including renames, type changes and conflicts) + Added and untracked files + Deleted files + LOCAL CHANGES + Open in Local Changes page diff --git a/src/Resources/Locales/es_ES.axaml b/src/Resources/Locales/es_ES.axaml index 96ea1ec58..bf3c90dbc 100644 --- a/src/Resources/Locales/es_ES.axaml +++ b/src/Resources/Locales/es_ES.axaml @@ -1091,4 +1091,14 @@ Eliminar Desbloquear SÍ + Cabecera del directorio de trabajo + Ocultar + Mostrar cuando haya cambios locales + Mostrar siempre + * Directorio de trabajo * + Archivos modificados (incluidos cambios de nombre, de tipo y conflictos) + Archivos añadidos y sin seguimiento + Archivos eliminados + CAMBIOS LOCALES + Abrir en la página de cambios locales diff --git a/src/Resources/Locales/fr_FR.axaml b/src/Resources/Locales/fr_FR.axaml index 05b98ea1e..4c315d423 100644 --- a/src/Resources/Locales/fr_FR.axaml +++ b/src/Resources/Locales/fr_FR.axaml @@ -981,4 +981,14 @@ Supprimer Déverrouiller OUI + Tête du répertoire de travail + Masquer + Afficher en présence de modifications locales + Toujours afficher + * Répertoire de travail * + Fichiers modifiés (y compris renommages, changements de type et conflits) + Fichiers ajoutés et non suivis + Fichiers supprimés + MODIFICATIONS LOCALES + Ouvrir la page des modifications locales diff --git a/src/Resources/Locales/he_IL.axaml b/src/Resources/Locales/he_IL.axaml index 0abcb3b0c..0cc266b7f 100644 --- a/src/Resources/Locales/he_IL.axaml +++ b/src/Resources/Locales/he_IL.axaml @@ -981,4 +981,14 @@ הסר שחרר נעילה כן + ראש תיקיית העבודה + הסתרה + הצגה כשיש שינויים מקומיים + הצגה תמיד + * תיקיית עבודה * + קבצים ששונו (כולל שינויי שם, שינויי סוג והתנגשויות) + קבצים שנוספו וקבצים שאינם במעקב + קבצים שנמחקו + שינויים מקומיים + פתיחה בדף השינויים המקומיים diff --git a/src/Resources/Locales/id_ID.axaml b/src/Resources/Locales/id_ID.axaml index 5e7e5e57f..611826da3 100644 --- a/src/Resources/Locales/id_ID.axaml +++ b/src/Resources/Locales/id_ID.axaml @@ -1026,4 +1026,14 @@ Hapus Unlock YA + Head direktori kerja + Sembunyikan + Tampilkan saat ada perubahan lokal + Selalu tampilkan + * Direktori Kerja * + Berkas yang diubah (termasuk penggantian nama, perubahan jenis, dan konflik) + Berkas yang ditambahkan dan tidak dilacak + Berkas yang dihapus + PERUBAHAN LOKAL + Buka halaman Perubahan Lokal diff --git a/src/Resources/Locales/it_IT.axaml b/src/Resources/Locales/it_IT.axaml index 630746c5d..96b66dfeb 100644 --- a/src/Resources/Locales/it_IT.axaml +++ b/src/Resources/Locales/it_IT.axaml @@ -917,4 +917,14 @@ ${pure_files:N} Come ${files:N}, ma senza cartelle Rimuovi Sblocca SÌ + HEAD della directory di lavoro + Nascondi + Mostra quando ci sono modifiche locali + Mostra sempre + * Directory di lavoro * + File modificati (incluse rinominazioni, modifiche del tipo e conflitti) + File aggiunti e non tracciati + File eliminati + MODIFICHE LOCALI + Apri nella pagina delle modifiche locali diff --git a/src/Resources/Locales/ja_JP.axaml b/src/Resources/Locales/ja_JP.axaml index 023fc65df..f2a608f79 100644 --- a/src/Resources/Locales/ja_JP.axaml +++ b/src/Resources/Locales/ja_JP.axaml @@ -1069,4 +1069,14 @@ 削除 ロックを解除 はい + 作業ディレクトリの HEAD + 非表示 + ローカルの変更がある場合に表示 + 常に表示 + * 作業ディレクトリ * + 変更されたファイル(名前変更、種類の変更、競合を含む) + 追加されたファイルと未追跡のファイル + 削除されたファイル + ローカルの変更 + ローカルの変更ページを開く diff --git a/src/Resources/Locales/ko_KR.axaml b/src/Resources/Locales/ko_KR.axaml index 219cddf68..029b9ecb1 100644 --- a/src/Resources/Locales/ko_KR.axaml +++ b/src/Resources/Locales/ko_KR.axaml @@ -989,4 +989,14 @@ 제거 잠금 해제 예 + 작업 디렉터리 HEAD + 숨기기 + 로컬 변경 사항이 있을 때 표시 + 항상 표시 + * 작업 디렉터리 * + 수정된 파일 (이름 변경, 유형 변경 및 충돌 포함) + 추가된 파일 및 추적되지 않은 파일 + 삭제된 파일 + 로컬 변경 사항 + 로컬 변경 사항 페이지에서 열기 diff --git a/src/Resources/Locales/pt_BR.axaml b/src/Resources/Locales/pt_BR.axaml index 5084f0180..1b046d8f9 100644 --- a/src/Resources/Locales/pt_BR.axaml +++ b/src/Resources/Locales/pt_BR.axaml @@ -636,4 +636,14 @@ Bloquear Remover Desbloquear + HEAD do diretório de trabalho + Ocultar + Mostrar quando houver alterações locais + Sempre mostrar + * Diretório de trabalho * + Arquivos modificados (incluindo renomeações, alterações de tipo e conflitos) + Arquivos adicionados e não rastreados + Arquivos excluídos + ALTERAÇÕES LOCAIS + Abrir na página de alterações locais diff --git a/src/Resources/Locales/ru_RU.axaml b/src/Resources/Locales/ru_RU.axaml index 5aef219db..c3cccc9d9 100644 --- a/src/Resources/Locales/ru_RU.axaml +++ b/src/Resources/Locales/ru_RU.axaml @@ -1091,4 +1091,14 @@ Удалить Разблокировать Да + HEAD рабочего каталога + Скрывать + Показывать при наличии локальных изменений + Показывать всегда + * Рабочий каталог * + Изменённые файлы (включая переименования, изменения типа и конфликты) + Добавленные и неотслеживаемые файлы + Удалённые файлы + ЛОКАЛЬНЫЕ ИЗМЕНЕНИЯ + Открыть страницу локальных изменений diff --git a/src/Resources/Locales/ta_IN.axaml b/src/Resources/Locales/ta_IN.axaml index 3473ff1ca..c6aef913f 100644 --- a/src/Resources/Locales/ta_IN.axaml +++ b/src/Resources/Locales/ta_IN.axaml @@ -653,4 +653,14 @@ பூட்டு நீக்கு திற + பணிக் கோப்பகத் தலை + மறை + உள்ளூர் மாற்றங்கள் இருக்கும்போது காட்டு + எப்போதும் காட்டு + * பணிக் கோப்பகம் * + மாற்றப்பட்ட கோப்புகள் (மறுபெயரிடுதல், வகை மாற்றங்கள் மற்றும் முரண்பாடுகள் உட்பட) + சேர்க்கப்பட்ட மற்றும் கண்காணிக்கப்படாத கோப்புகள் + நீக்கப்பட்ட கோப்புகள் + உள்ளூர் மாற்றங்கள் + உள்ளூர் மாற்றங்கள் பக்கத்தில் திற diff --git a/src/Resources/Locales/uk_UA.axaml b/src/Resources/Locales/uk_UA.axaml index 5f44193e3..55b490abb 100644 --- a/src/Resources/Locales/uk_UA.axaml +++ b/src/Resources/Locales/uk_UA.axaml @@ -661,4 +661,14 @@ Заблокувати Видалити Розблокувати + HEAD робочого каталогу + Приховувати + Показувати за наявності локальних змін + Показувати завжди + * Робочий каталог * + Змінені файли (зокрема перейменування, зміни типу та конфлікти) + Додані та невідстежувані файли + Видалені файли + ЛОКАЛЬНІ ЗМІНИ + Відкрити сторінку локальних змін diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 97e636774..ea593ca0d 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -1093,4 +1093,14 @@ 移除工作树 解除工作树锁定 好的 + 工作目录 HEAD + 隐藏 + 有本地更改时显示 + 始终显示 + * 工作目录 * + 已修改的文件(包括重命名、类型更改和冲突) + 新增和未跟踪的文件 + 已删除的文件 + 本地更改 + 在本地更改页面中打开 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index ef2e3d7ca..f34281df7 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -1092,4 +1092,14 @@ 移除工作樹 解除鎖定工作樹 是 + 工作目錄 HEAD + 隱藏 + 有本機變更時顯示 + 一律顯示 + * 工作目錄 * + 已修改的檔案(包含重新命名、類型變更和衝突) + 新增和未追蹤的檔案 + 已刪除的檔案 + 本機變更 + 在本機變更頁面中開啟 diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index 128b53ff9..398bbaaa4 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Threading.Tasks; @@ -10,7 +11,7 @@ namespace SourceGit.ViewModels { - public class Histories : ObservableObject + public class Histories : ObservableObject, IDisposable { public bool IsLoading { @@ -109,10 +110,107 @@ public List Commits { GenerateGraph(value); if (SetProperty(ref _commits, value)) - PostCommitsChanged(); + { + IsUpdatingEntries = true; + Entries.Clear(); + if (IsWorkingDirectoryVisible) + Entries.Add(WorkingDirectory); + Entries.AddRange(value); + IsUpdatingEntries = false; + if (_selectHeadAfterRefresh) + { + _selectHeadAfterRefresh = false; + var head = value.Find(x => x.IsCurrentHead); + SelectedCommits = head == null ? [] : [head]; + if (head == null) + DetailContext = Models.Null.Instance; + } + else + PostCommitsChanged(); + } } } + public bool IsUpdatingEntries { get; private set; } + + public AvaloniaList Entries { get; } = []; + public WorkingDirectoryEntry WorkingDirectory { get; } = new(); + public WorkingCopy WorkingCopy => _repo.WorkingCopy; + + public bool IsWorkingDirectoryVisible => !_repo.IsBare && Preferences.Instance.WorkingDirectoryHeadMode switch + { + 1 => WorkingDirectory.HasChanges, + 2 => true, + _ => false, + }; + + public bool IsWorkingDirectorySelected + { + get => _isWorkingDirectorySelected; + set + { + if ((value && !IsWorkingDirectoryVisible) || !SetProperty(ref _isWorkingDirectorySelected, value)) + return; + + if (value) + { + _selectHeadAfterRefresh = false; + SelectedCommits = []; + _searchCommitContext.Selected = null; + DetailContext = WorkingCopy; + } + else if (_selectedCommits.Count == 0) + DetailContext = Models.Null.Instance; + } + } + + public void UpdateWorkingDirectory(List changes) + { + WorkingDirectory.SetChanges(changes); + RefreshWorkingDirectoryVisibility(true); + } + + public void SelectHistoryEntries(List commits, bool includeWorkingDirectory) + { + _updatingSelection = true; + SetProperty(ref _isWorkingDirectorySelected, includeWorkingDirectory && IsWorkingDirectoryVisible && commits.Count <= 1, nameof(IsWorkingDirectorySelected)); + SelectedCommits = commits; + _updatingSelection = false; + PostSelectedCommitsChanged(); + } + + private void RefreshWorkingDirectoryVisibility(bool followHeadAfterRefresh) + { + if (Entries.Contains(WorkingDirectory) == IsWorkingDirectoryVisible) + return; + + OnPropertyChanged(nameof(IsWorkingDirectoryVisible)); + + if (IsWorkingDirectoryVisible) + { + _selectHeadAfterRefresh = false; + Entries.Insert(0, WorkingDirectory); + } + else + { + var wasSelected = IsWorkingDirectorySelected; + var comparedCommit = _selectedCommits.Count == 1 ? _selectedCommits[0] : null; + IsWorkingDirectorySelected = false; + Entries.Remove(WorkingDirectory); + if (wasSelected) + { + var head = _commits.Find(x => x.IsCurrentHead); + SelectedCommits = comparedCommit != null ? [comparedCommit] : head == null ? [] : [head]; + if (head == null && comparedCommit == null) + DetailContext = Models.Null.Instance; + // Status may finish before the history query after committing. Follow + // the newly checked-out HEAD when that query arrives, unless the user navigates. + _selectHeadAfterRefresh = followHeadAfterRefresh && comparedCommit == null; + } + } + GenerateGraph(_commits); + } + public Models.CommitGraph Graph { get => _graph; @@ -137,6 +235,11 @@ public List SelectedCommits get => _selectedCommits; set { + if (!_updatingSelection && value.Count > 0) + { + _selectHeadAfterRefresh = false; + IsWorkingDirectorySelected = false; + } var oldCount = _selectedCommits.Count; if (SetProperty(ref _selectedCommits, value) && oldCount + value.Count > 0) PostSelectedCommitsChanged(); @@ -233,6 +336,30 @@ public Histories(Repository repo) _repo = repo; _commitDetailSharedData = new CommitDetailSharedData(); _searchCommitContext = new SearchCommitContext(repo.FullPath, this); + Preferences.Instance.PropertyChanged += OnPreferencesChanged; + } + + public void Dispose() + { + Preferences.Instance.PropertyChanged -= OnPreferencesChanged; + } + + private void OnPreferencesChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName != nameof(Preferences.WorkingDirectoryHeadMode)) + return; + + _selectHeadAfterRefresh = false; + var graph = Graph; + RefreshWorkingDirectoryVisibility(false); + // Hide also restores the original graph lanes when the repository is clean. + if (ReferenceEquals(graph, Graph)) + GenerateGraph(_commits); + } + + public void CancelPendingHeadSelection() + { + _selectHeadAfterRefresh = false; } public bool HasShowFlag(Models.HistoryShowFlags flag) @@ -296,6 +423,7 @@ public Models.BisectState UpdateBisectInfo() public void NavigateTo(string commitSHA) { + _selectHeadAfterRefresh = false; var commit = _commits.Find(x => x.SHA.StartsWith(commitSHA, StringComparison.Ordinal)); if (commit != null) { @@ -312,6 +440,7 @@ public void NavigateTo(string commitSHA) Dispatcher.UIThread.Post(() => { _ignoreSelectionChange = true; + IsWorkingDirectorySelected = false; SelectedCommits = []; if (_detailContext is CommitDetail detail) @@ -492,7 +621,11 @@ public void CompareWithWorktree(Models.Commit commit) private void PostCommitsChanged() { if (_selectedCommits.Count == 0) + { + if (IsWorkingDirectorySelected) + SelectHistoryEntries([], true); return; + } if (_commits.Count == 0 || _selectedCommits.Count > 20) { @@ -516,15 +649,23 @@ private void PostCommitsChanged() } } - SelectedCommits = selected; + SelectHistoryEntries(selected, IsWorkingDirectorySelected); } private void PostSelectedCommitsChanged() { - if (_ignoreSelectionChange) + if (_ignoreSelectionChange || _updatingSelection) return; - if (_selectedCommits.Count == 0) + if (IsWorkingDirectorySelected) + { + _searchCommitContext.Selected = null; + if (_selectedCommits.Count == 1) + CompareWithWorktree(_selectedCommits[0]); + else + DetailContext = WorkingCopy; + } + else if (_selectedCommits.Count == 0) { _searchCommitContext.Selected = null; DetailContext = Models.Null.Instance; @@ -574,9 +715,15 @@ private void GenerateGraph(List commits) extraHeads.Add(c.SHA); } - Graph = Models.CommitGraph.Generate(commits, firstParentOnly, highlighting, extraHeads); + var graph = Models.CommitGraph.Generate(commits, firstParentOnly, highlighting, extraHeads, IsWorkingDirectoryVisible, Preferences.Instance.WorkingDirectoryHeadMode != 0); + if (IsWorkingDirectoryVisible) + WorkingDirectory.LeftMargin = graph.Dots[0].Center.X + 14; + Graph = graph; } + private bool _selectHeadAfterRefresh; + private bool _isWorkingDirectorySelected; + private bool _updatingSelection; private Repository _repo = null; private Models.Branch _currentBranch = null; private bool _hasSingleRemote = false; diff --git a/src/ViewModels/Preferences.cs b/src/ViewModels/Preferences.cs index 4803ec429..68fe274ad 100644 --- a/src/ViewModels/Preferences.cs +++ b/src/ViewModels/Preferences.cs @@ -169,6 +169,16 @@ public bool Use24Hours } } + public int WorkingDirectoryHeadMode + { + get => _workingDirectoryHeadMode; + set + { + if (value >= 0 && value <= 2) + SetProperty(ref _workingDirectoryHeadMode, value); + } + } + public bool UseFixedTabWidth { get => _useFixedTabWidth; @@ -820,6 +830,7 @@ private bool RemoveInvalidRepositoriesRecursive(List collection) private int _maxHistoryCommits = 20000; private int _subjectGuideLength = 50; + private int _workingDirectoryHeadMode = 0; private bool _useFixedTabWidth = true; private bool _useAutoHideScrollBars = true; private bool _useGitHubStyleAvatar = true; diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 1ea5951a8..5887ee82b 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -479,6 +479,7 @@ public void Close() if (_cancellationRefreshStashes is { IsCancellationRequested: false }) _cancellationRefreshStashes.Cancel(); + _histories.Dispose(); _watcher?.Dispose(); _autoFetchTimer.Dispose(); } @@ -1348,6 +1349,7 @@ public void RefreshWorkingCopyChanges() _workingCopy.SetData(changes); LocalChangesCount = changes.Count; + _histories.UpdateWorkingDirectory(changes); OnPropertyChanged(nameof(InProgressContext)); GetOwnerPage()?.ChangeDirtyState(Models.DirtyState.HasLocalChanges, changes.Count == 0); }); diff --git a/src/ViewModels/WorkingDirectoryEntry.cs b/src/ViewModels/WorkingDirectoryEntry.cs new file mode 100644 index 000000000..1d9d01732 --- /dev/null +++ b/src/ViewModels/WorkingDirectoryEntry.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using CommunityToolkit.Mvvm.ComponentModel; + +namespace SourceGit.ViewModels +{ + // Presentation-only history entry. It has no revision identity and never enters commit queries. + public class WorkingDirectoryEntry : ObservableObject + { + public string Summary => App.Text("Histories.WorkingDirectory"); + public double LeftMargin { get => _leftMargin; set => SetProperty(ref _leftMargin, value); } + public int Modified { get => _modified; private set => SetProperty(ref _modified, value); } + public int Added { get => _added; private set => SetProperty(ref _added, value); } + public int Deleted { get => _deleted; private set => SetProperty(ref _deleted, value); } + public bool HasChanges => Modified + Added + Deleted > 0; + + public void SetChanges(List changes) + { + var modified = 0; + var added = 0; + var deleted = 0; + var paths = new HashSet(StringComparer.Ordinal); + foreach (var c in changes) + { + if (!paths.Add(c.Path)) + continue; + + // Structural changes and conflicts take precedence over addition/deletion. + // AM/AD remain additions: the index introduces a path absent from HEAD. + // MD is deleted; RD stays modified (the rename still exists in the index). + if (IsStructural(c.Index) || IsStructural(c.WorkTree)) + modified++; + else if (c.Index == Models.ChangeState.Added || c.WorkTree is Models.ChangeState.Added or Models.ChangeState.Untracked) + added++; + else if (c.Index == Models.ChangeState.Deleted || c.WorkTree == Models.ChangeState.Deleted) + deleted++; + else + modified++; + } + Modified = modified; + Added = added; + Deleted = deleted; + } + + private static bool IsStructural(Models.ChangeState state) => state is + Models.ChangeState.Renamed or Models.ChangeState.Copied or Models.ChangeState.TypeChanged or Models.ChangeState.Conflicted; + + private double _leftMargin = 24; + private int _modified; + private int _added; + private int _deleted; + } +} diff --git a/src/Views/CommitGraph.cs b/src/Views/CommitGraph.cs index 88a8217c6..20f0d9012 100644 --- a/src/Views/CommitGraph.cs +++ b/src/Views/CommitGraph.cs @@ -104,10 +104,14 @@ private void DrawCurves(DrawingContext context, Models.CommitGraph graph, double if (endY < top) continue; if (last.Y > bottom) - break; + // The working-directory continuation can precede paths for + // newer commits, so later paths may still be visible. + continue; var geo = new StreamGeometry(); var pen = line.IsHighlighted ? Models.CommitGraph.Pens[line.Color] : grayedPen; + if (line.IsDotted) + pen = new Pen(pen.Brush, pen.Thickness, new DashStyle([0, 2], 0), PenLineCap.Round); using (var ctx = geo.Open()) { @@ -183,6 +187,9 @@ private void DrawAnchors(DrawingContext context, Models.CommitGraph graph, doubl var pen = dot.IsHighlighted ? Models.CommitGraph.Pens[dot.Color] : grayedPen; switch (dot.Type) { + case Models.CommitGraph.DotType.WorkingDirectory: + context.DrawEllipse(dotFill, new Pen(pen.Brush, pen.Thickness, new DashStyle([2, 2], 0)), center, 6, 6); + break; case Models.CommitGraph.DotType.Head: context.DrawEllipse(dotFill, pen, center, 6, 6); context.DrawEllipse(pen.Brush, null, center, 3, 3); diff --git a/src/Views/Histories.axaml b/src/Views/Histories.axaml index a3d24529f..485ff924e 100644 --- a/src/Views/Histories.axaml +++ b/src/Views/Histories.axaml @@ -28,7 +28,7 @@ Classes="static_scrollbar" ScrollViewer.AllowAutoHide="False" Background="Transparent" - ItemsSource="{Binding Commits, Mode=OneWay}" + ItemsSource="{Binding Entries, Mode=OneWay}" TotalCommits="{Binding Commits.Count, Mode=OneWay}" SelectedCommits="{Binding SelectedCommits, Mode=TwoWay}" ColumnHeaderHeight="24" @@ -89,7 +89,7 @@