Compare commits

...

21 commits

Author SHA1 Message Date
goran-w
904ec16486 Added Preference and ToggleButton for diff navigation style 2024-12-06 20:27:41 +01:00
goran-w
830d4b20c9 Added new Icon and String (en-US) for Highlighted Diff Navigation 2024-12-06 20:27:41 +01:00
goran-w
ad1263b0a8 Added indicator of current/total change-blocks in DiffView toolbar 2024-12-06 20:27:41 +01:00
goran-w
eddc1a91ce Added safeguards for edge cases 2024-12-06 20:27:41 +01:00
goran-w
ad9cf996f5 Implemented change-block navigation
* Modified behavior of the Prev/Next Change buttons in DiffView toolbar.
* Well-defined change-blocks are pre-calculated and can be navigated between.
* Current change-block is highlighted in the Diff panel(s).
* Prev/next at start/end of range (re-)scrolls to first/last change-block
(I.e when unset, or already at first/last change-block, or at the only one.)
* Current change-block is unset in RefreshContent().
2024-12-06 20:27:41 +01:00
goran-w
6c5e31f481 Corrected misspelled local variable nextHigh(t)light 2024-12-06 20:27:41 +01:00
GadflyFang
c062f27081
fix: Dispose _autoFetchTimer before _setting set to null (#792)
Some checks failed
Continuous Integration / Build (push) Has been cancelled
Continuous Integration / Prepare version string (push) Has been cancelled
Continuous Integration / Package (push) Has been cancelled
Signed-off-by: Gadfly <gadfly@gadfly.vip>
2024-12-06 15:09:14 +08:00
Bernat Borràs Civil
c2252266ce
Update Spanish translation (#791)
Some checks failed
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Has been cancelled
* Update spanish translation

* doc: Update translation status and missing keys

* Add missing key

(cherry picked from commit 2bf0641323325bf97d1fac9ed225228e5015a3ba)

* doc: Update translation status and missing keys

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-12-06 09:13:45 +08:00
leo
e18d6d65e8
ux: style of MenuItem 2024-12-05 20:43:31 +08:00
leo
75e9f1e9a4
feature: show track status in Delete Multiple Branches panel (#785)
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
2024-12-04 19:14:48 +08:00
leo
1ddd348a40
feature: show tracking status in Delete Branch panel if possible (#785) 2024-12-04 18:04:57 +08:00
leo
d616d0897b
refactor: relative time display mode (#777) 2024-12-04 11:25:25 +08:00
stone-w4tch3r
43928936bc
Added repositories for deb/rpm packages on buildkite (#780)
* added and used new github action for publishing packages to packagecloud

* publish-packages.yml WIP

* publish-packages.yml now uses curl push

* fix naming from packagecloud to buildkite

* Add debug logs to publish-packages.yml

* fix package path

* change repo name to sourcegit

* fixed unused code

* added --fail to curl pushes

* Remove leftowers from release.yml
2024-12-04 09:57:30 +08:00
stone-w4tch3r
abacccab00
feat: Add libicu dependency to RPM spec file (#781)
(cherry picked from commit f312895ef8c77098612645a54420539fef70a849)
(cherry picked from commit 662149045f)
2024-12-04 09:48:30 +08:00
leo
ca29a232e4
enhance: call gc after viewing commit changed
Some checks are pending
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
2024-12-03 10:44:27 +08:00
leo
ea0bec16da
refactor: use control instead of DataContext to get input string 2024-12-03 09:35:32 +08:00
leo
0160600c75
revert: changes about SystemAccentColor (#776)
This reverts commit db8ee3410b.
2024-12-03 09:27:16 +08:00
leo
d1a1b4b2b9
enhance: do NOT show search suggestion if input string is empty (#775)
Some checks failed
Continuous Integration / Build (push) Waiting to run
Continuous Integration / Prepare version string (push) Waiting to run
Continuous Integration / Package (push) Blocked by required conditions
Localization Check / localization-check (push) Has been cancelled
2024-12-02 21:51:05 +08:00
github-actions[bot]
a52977baf3 doc: Update translation status and missing keys 2024-12-02 13:44:50 +00:00
leo
894f3e9b03
feature: supports searching revision files (#775) 2024-12-02 21:44:15 +08:00
leo
536f225867
feature: allow using Amend while rebasing (#773) 2024-12-02 10:38:40 +08:00
34 changed files with 853 additions and 114 deletions

39
.github/workflows/publish-packages.yml vendored Normal file
View file

@ -0,0 +1,39 @@
name: Publish to Buildkite
on:
workflow_call:
secrets:
BUILDKITE_TOKEN:
required: true
jobs:
publish:
name: Publish to Buildkite
runs-on: ubuntu-latest
strategy:
matrix:
runtime: [linux-x64, linux-arm64]
steps:
- name: Download package artifacts
uses: actions/download-artifact@v4
with:
name: package.${{ matrix.runtime }}
path: packages
- name: Publish DEB package
env:
BUILDKITE_TOKEN: ${{ secrets.BUILDKITE_TOKEN }}
run: |
FILE=$(echo packages/*.deb)
curl -X POST https://api.buildkite.com/v2/packages/organizations/sourcegit/registries/sourcegit-deb/packages \
-H "Authorization: Bearer $BUILDKITE_TOKEN" \
-F "file=@$FILE" \
--fail
- name: Publish RPM package
env:
BUILDKITE_TOKEN: ${{ secrets.BUILDKITE_TOKEN }}
run: |
FILE=$(echo packages/*.rpm)
curl -X POST https://api.buildkite.com/v2/packages/organizations/sourcegit/registries/sourcegit-rpm/packages \
-H "Authorization: Bearer $BUILDKITE_TOKEN" \
-F "file=@$FILE" \
--fail

View file

@ -24,6 +24,11 @@ jobs:
uses: ./.github/workflows/package.yml
with:
version: ${{ needs.version.outputs.version }}
publish-packages:
name: Publish Packages
uses: ./.github/workflows/publish-packages.yml
secrets:
BUILDKITE_TOKEN: ${{ secrets.BUILDKITE_TOKEN }}
release:
needs: [package, version]
name: Release

View file

@ -47,7 +47,7 @@
## Translation Status
[![en_US](https://img.shields.io/badge/en__US-100%25-brightgreen)](TRANSLATION.md) [![de__DE](https://img.shields.io/badge/de__DE-99.86%25-yellow)](TRANSLATION.md) [![es__ES](https://img.shields.io/badge/es__ES-97.87%25-yellow)](TRANSLATION.md) [![fr__FR](https://img.shields.io/badge/fr__FR-97.30%25-yellow)](TRANSLATION.md) [![it__IT](https://img.shields.io/badge/it__IT-97.73%25-yellow)](TRANSLATION.md) [![pt__BR](https://img.shields.io/badge/pt__BR-99.15%25-yellow)](TRANSLATION.md) [![ru__RU](https://img.shields.io/badge/ru__RU-100.00%25-brightgreen)](TRANSLATION.md) [![zh__CN](https://img.shields.io/badge/zh__CN-100.00%25-brightgreen)](TRANSLATION.md) [![zh__TW](https://img.shields.io/badge/zh__TW-100.00%25-brightgreen)](TRANSLATION.md)
[![en_US](https://img.shields.io/badge/en__US-100%25-brightgreen)](TRANSLATION.md) [![de__DE](https://img.shields.io/badge/de__DE-99.72%25-yellow)](TRANSLATION.md) [![es__ES](https://img.shields.io/badge/es__ES-100.00%25-brightgreen)](TRANSLATION.md) [![fr__FR](https://img.shields.io/badge/fr__FR-97.17%25-yellow)](TRANSLATION.md) [![it__IT](https://img.shields.io/badge/it__IT-97.59%25-yellow)](TRANSLATION.md) [![pt__BR](https://img.shields.io/badge/pt__BR-99.01%25-yellow)](TRANSLATION.md) [![ru__RU](https://img.shields.io/badge/ru__RU-99.86%25-yellow)](TRANSLATION.md) [![zh__CN](https://img.shields.io/badge/zh__CN-100.00%25-brightgreen)](TRANSLATION.md) [![zh__TW](https://img.shields.io/badge/zh__TW-100.00%25-brightgreen)](TRANSLATION.md)
## How to Use

View file

@ -18,6 +18,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
.github\workflows\package.yml = .github\workflows\package.yml
.github\workflows\release.yml = .github\workflows\release.yml
.github\workflows\localization-check.yml = .github\workflows\localization-check.yml
.github\workflows\publish-packages.yml = .github\workflows\publish-packages.yml
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{49A7C2D6-558C-4FAA-8F5D-EEE81497AED7}"

View file

@ -1,38 +1,25 @@
### de_DE.axaml: 99.86%
### de_DE.axaml: 99.72%
<details>
<summary>Missing Keys</summary>
- Text.CommitDetail.Files.Search
- Text.WorkingCopy.CommitToEdit
</details>
### es_ES.axaml: 97.87%
### es_ES.axaml: 100.00%
<details>
<summary>Missing Keys</summary>
- Text.CommitDetail.Info.Children
- Text.Fetch.Force
- Text.Preference.Appearance.FontSize
- Text.Preference.Appearance.FontSize.Default
- Text.Preference.Appearance.FontSize.Editor
- Text.Preference.General.ShowChildren
- Text.Repository.FilterCommits
- Text.Repository.FilterCommits.Default
- Text.Repository.FilterCommits.Exclude
- Text.Repository.FilterCommits.Include
- Text.Repository.HistoriesOrder
- Text.Repository.HistoriesOrder.ByDate
- Text.Repository.HistoriesOrder.Topo
- Text.SHALinkCM.NavigateTo
- Text.WorkingCopy.CommitToEdit
</details>
### fr_FR.axaml: 97.30%
### fr_FR.axaml: 97.17%
<details>
@ -41,6 +28,7 @@
- Text.CherryPick.AppendSourceToMessage
- Text.CherryPick.Mainline.Tips
- Text.CommitCM.CherryPickMultiple
- Text.CommitDetail.Files.Search
- Text.Fetch.Force
- Text.Preference.Appearance.FontSize
- Text.Preference.Appearance.FontSize.Default
@ -60,12 +48,13 @@
</details>
### it_IT.axaml: 97.73%
### it_IT.axaml: 97.59%
<details>
<summary>Missing Keys</summary>
- Text.CommitDetail.Files.Search
- Text.CommitDetail.Info.Children
- Text.Configure.IssueTracker.AddSampleGitLabMergeRequest
- Text.Configure.OpenAI.Preferred
@ -85,12 +74,13 @@
</details>
### pt_BR.axaml: 99.15%
### pt_BR.axaml: 99.01%
<details>
<summary>Missing Keys</summary>
- Text.CommitDetail.Files.Search
- Text.CommitDetail.Info.Children
- Text.Fetch.Force
- Text.Preference.General.ShowChildren
@ -100,13 +90,13 @@
</details>
### ru_RU.axaml: 100.00%
### ru_RU.axaml: 99.86%
<details>
<summary>Missing Keys</summary>
- Text.CommitDetail.Files.Search
</details>

View file

@ -7,6 +7,7 @@ URL: https://sourcegit-scm.github.io/
Source: https://github.com/sourcegit-scm/sourcegit/archive/refs/tags/v%_version.tar.gz
Requires: libX11.so.6()(%{__isa_bits}bit)
Requires: libSM.so.6()(%{__isa_bits}bit)
Requires: libicu
%define _build_id_links none

View file

@ -164,7 +164,12 @@ namespace SourceGit
var resDic = new ResourceDictionary();
var overrides = JsonSerializer.Deserialize(File.ReadAllText(themeOverridesFile), JsonCodeGen.Default.ThemeOverrides);
foreach (var kv in overrides.BasicColors)
{
if (kv.Key.Equals("SystemAccentColor", StringComparison.Ordinal))
resDic["SystemAccentColor"] = kv.Value;
else
resDic[$"Color.{kv.Key}"] = kv.Value;
}
if (overrides.GraphColors.Count > 0)
Models.CommitGraph.SetPens(overrides.GraphColors, overrides.GraphPenThickness);

View file

@ -51,6 +51,9 @@ namespace SourceGit.Commands
_result.TextDiff.MaxLineNumber = Math.Max(_newLine, _oldLine);
}
if (_result.TextDiff != null)
_result.TextDiff.ProcessChangeBlocks();
return _result;
}

View file

@ -1,19 +1,19 @@
namespace SourceGit.Commands
{
public class QueryCurrentRevisionFiles : Command
public class QueryRevisionFileNames : Command
{
public QueryCurrentRevisionFiles(string repo)
public QueryRevisionFileNames(string repo, string revision)
{
WorkingDirectory = repo;
Context = repo;
Args = "ls-tree -r --name-only HEAD";
Args = $"ls-tree -r -z --name-only {revision}";
}
public string[] Result()
{
var rs = ReadToEnd();
if (rs.IsSuccess)
return rs.StdOut.Split('\n', System.StringSplitOptions.RemoveEmptyEntries);
return rs.StdOut.Split('\0', System.StringSplitOptions.RemoveEmptyEntries);
return [];
}

View file

@ -12,7 +12,7 @@ namespace SourceGit.Commands
{
WorkingDirectory = repo;
Context = repo;
Args = $"ls-tree {sha}";
Args = $"ls-tree -z {sha}";
if (!string.IsNullOrEmpty(parentFolder))
Args += $" -- \"{parentFolder}\"";
@ -20,11 +20,27 @@ namespace SourceGit.Commands
public List<Models.Object> Result()
{
Exec();
var rs = ReadToEnd();
if (rs.IsSuccess)
{
var start = 0;
var end = rs.StdOut.IndexOf('\0', start);
while (end > 0)
{
var line = rs.StdOut.Substring(start, end - start);
Parse(line);
start = end + 1;
end = rs.StdOut.IndexOf('\0', start);
}
if (start < rs.StdOut.Length)
Parse(rs.StdOut.Substring(start));
}
return _objects;
}
protected override void OnReadline(string line)
private void Parse(string line)
{
var match = REG_FORMAT().Match(line);
if (!match.Success)

View file

@ -7,6 +7,8 @@ namespace SourceGit.Models
public List<string> Ahead { get; set; } = new List<string>();
public List<string> Behind { get; set; } = new List<string>();
public bool IsVisible => Ahead.Count > 0 || Behind.Count > 0;
public override string ToString()
{
if (Ahead.Count == 0 && Behind.Count == 0)

View file

@ -2,6 +2,8 @@
using System.Text;
using System.Text.RegularExpressions;
using CommunityToolkit.Mvvm.ComponentModel;
using Avalonia;
using Avalonia.Media.Imaging;
@ -59,16 +61,70 @@ namespace SourceGit.Models
}
}
public partial class TextDiff
public class TextDiffChangeBlock
{
public TextDiffChangeBlock(int startLine, int endLine)
{
StartLine = startLine;
EndLine = endLine;
}
public int StartLine { get; set; } = 0;
public int EndLine { get; set; } = 0;
public bool IsInRange(int line)
{
return line >= StartLine && line <= EndLine;
}
}
public partial class TextDiff : ObservableObject
{
public string File { get; set; } = string.Empty;
public List<TextDiffLine> Lines { get; set; } = new List<TextDiffLine>();
public Vector ScrollOffset { get; set; } = Vector.Zero;
public int MaxLineNumber = 0;
public int CurrentChangeBlockIdx
{
get => _currentChangeBlockIdx;
set => SetProperty(ref _currentChangeBlockIdx, value);
}
public string Repo { get; set; } = null;
public DiffOption Option { get; set; } = null;
public List<TextDiffChangeBlock> ChangeBlocks { get; set; } = [];
public void ProcessChangeBlocks()
{
ChangeBlocks.Clear();
int lineIdx = 0, blockStartIdx = 0;
bool isNewBlock = true;
foreach (var line in Lines)
{
lineIdx++;
if (line.Type == Models.TextDiffLineType.Added ||
line.Type == Models.TextDiffLineType.Deleted ||
line.Type == Models.TextDiffLineType.None) // Empty
{
if (isNewBlock)
{
isNewBlock = false;
blockStartIdx = lineIdx;
}
}
else
{
if (!isNewBlock)
{
ChangeBlocks.Add(new TextDiffChangeBlock(blockStartIdx, lineIdx - 1));
isNewBlock = true;
}
}
}
}
public TextDiffSelection MakeSelection(int startLine, int endLine, bool isCombined, bool isOldSide)
{
var rs = new TextDiffSelection();
@ -626,6 +682,8 @@ namespace SourceGit.Models
return true;
}
private int _currentChangeBlockIdx = -1; // NOTE: Use -1 as "not set".
[GeneratedRegex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@")]
private static partial Regex REG_INDICATOR();
}

View file

@ -54,6 +54,7 @@
<StreamGeometry x:Key="Icons.Grid">M30 271l241 0 0-241-241 0 0 241zM392 271l241 0 0-241-241 0 0 241zM753 30l0 241 241 0 0-241-241 0zM30 632l241 0 0-241-241 0 0 241zM392 632l241 0 0-241-241 0 0 241zM753 632l241 0 0-241-241 0 0 241zM30 994l241 0 0-241-241 0 0 241zM392 994l241 0 0-241-241 0 0 241zM753 994l241 0 0-241-241 0 0 241z</StreamGeometry>
<StreamGeometry x:Key="Icons.Head">M0 512M1024 512M512 0M512 1024M955 323q0 23-16 39l-414 414-78 78q-16 16-39 16t-39-16l-78-78-207-207q-16-16-16-39t16-39l78-78q16-16 39-16t39 16l168 169 375-375q16-16 39-16t39 16l78 78q16 16 16 39z</StreamGeometry>
<StreamGeometry x:Key="Icons.HiddenSymbol">M416 64H768v64h-64v704h64v64H448v-64h64V512H416a224 224 0 1 1 0-448zM576 832h64V128H576v704zM416 128H512v320H416a160 160 0 0 1 0-320z</StreamGeometry>
<StreamGeometry x:Key="Icons.Highlight">M20.2585648,2.00438474 C20.6382605,2.00472706 20.9518016,2.28716326 21.0011348,2.65328337 L21.0078899,2.75506004 L21.0038407,7.25276883 C21.0009137,8.40908568 20.1270954,9.36072944 19.0029371,9.48671858 L19.0024932,11.7464847 C19.0024932,12.9373487 18.0773316,13.9121296 16.906542,13.9912939 L16.7524932,13.9964847 L16.501,13.9963847 L16.5017549,16.7881212 C16.5017549,17.6030744 16.0616895,18.349347 15.3600767,18.7462439 L15.2057929,18.8258433 L8.57108142,21.9321389 C8.10484975,22.1504232 7.57411944,21.8450614 7.50959937,21.3535767 L7.50306874,21.2528982 L7.503,13.9963847 L7.25,13.9964847 C6.05913601,13.9964847 5.08435508,13.0713231 5.00519081,11.9005335 L5,11.7464847 L5.00043957,9.4871861 C3.92882124,9.36893736 3.08392302,8.49812196 3.0058865,7.41488149 L3,7.25086975 L3,2.75438506 C3,2.3401715 3.33578644,2.00438474 3.75,2.00438474 C4.12969577,2.00438474 4.44349096,2.28653894 4.49315338,2.6526145 L4.5,2.75438506 L4.5,7.25086975 C4.5,7.63056552 4.78215388,7.94436071 5.14822944,7.99402313 L5.25,8.00086975 L18.7512697,8.00087075 C19.1315998,8.00025031 19.4461483,7.71759877 19.4967392,7.3518545 L19.5038434,7.25019537 L19.5078902,2.75371008 C19.508263,2.33949668 19.8443515,2.00401258 20.2585648,2.00438474 Z M15.001,13.9963847 L9.003,13.9963847 L9.00306874,20.0736262 L14.5697676,17.4673619 C14.8004131,17.3593763 14.9581692,17.1431606 14.9940044,16.89581 L15.0017549,16.7881212 L15.001,13.9963847 Z M17.502,9.50038474 L6.5,9.50038474 L6.5,11.7464847 C6.5,12.1261805 6.78215388,12.4399757 7.14822944,12.4896381 L7.25,12.4964847 L16.7524932,12.4964847 C17.1321889,12.4964847 17.4459841,12.2143308 17.4956465,11.8482552 L17.5024932,11.7464847 L17.502,9.50038474 Z</StreamGeometry>
<StreamGeometry x:Key="Icons.Histories">M24 512A488 488 0 01512 24A488 488 0 011000 512A488 488 0 01512 1000A488 488 0 0124 512zm447-325v327L243 619l51 111 300-138V187H471z</StreamGeometry>
<StreamGeometry x:Key="Icons.Home">M832 64h128v278l-128-146V64zm64 448L512 73 128 512H0L448 0h128l448 512h-128zm0 83V1024H640V704c0-35-29-64-64-64h-128a64 64 0 00-64 64v320H128V595l384-424 384 424z</StreamGeometry>
<StreamGeometry x:Key="Icons.Hotkeys">M512 0C229 0 0 229 0 512c0 283 229 512 512 512s512-229 512-512c0-283-229-512-512-512zm0 958C266 958 66 758 66 512S266 66 512 66 958 266 958 512 758 958 512 958zM192 416h96a32 32 0 0032-32v-32a32 32 0 00-32-32H192a32 32 0 00-32 32v32a32 32 0 0032 32zM384 416h96a32 32 0 0032-32v-32a32 32 0 00-32-32h-96a32 32 0 00-32 32v32a32 32 0 0032 32zM576 416h96a32 32 0 0032-32v-32a32 32 0 00-32-32h-96a32 32 0 00-32 32v32a32 32 0 0032 32zM832 320h-64a32 32 0 00-32 32v128h-160a32 32 0 00-32 32v32a32 32 0 0032 32h256a32 32 0 0032-32v-192a32 32 0 00-32-32zM320 544v-32a32 32 0 00-32-32H192a32 32 0 00-32 32v32a32 32 0 0032 32h96a32 32 0 0032-32zM384 576h96a32 32 0 0032-32v-32a32 32 0 00-32-32h-96a32 32 0 00-32 32v32a32 32 0 0032 32zM800 640H256a32 32 0 00-32 32v32a32 32 0 0032 32h544a32 32 0 0032-32v-32a32 32 0 00-32-32z</StreamGeometry>

View file

@ -121,6 +121,7 @@
<x:String x:Key="Text.CommitDetail.Changes.Search" xml:space="preserve">Search Changes...</x:String>
<x:String x:Key="Text.CommitDetail.Files" xml:space="preserve">FILES</x:String>
<x:String x:Key="Text.CommitDetail.Files.LFS" xml:space="preserve">LFS File</x:String>
<x:String x:Key="Text.CommitDetail.Files.Search" xml:space="preserve">Search Files...</x:String>
<x:String x:Key="Text.CommitDetail.Files.Submodule" xml:space="preserve">Submodule</x:String>
<x:String x:Key="Text.CommitDetail.Info" xml:space="preserve">INFORMATION</x:String>
<x:String x:Key="Text.CommitDetail.Info.Author" xml:space="preserve">AUTHOR</x:String>
@ -232,6 +233,7 @@
<x:String x:Key="Text.Diff.Binary.New" xml:space="preserve">NEW</x:String>
<x:String x:Key="Text.Diff.Binary.Old" xml:space="preserve">OLD</x:String>
<x:String x:Key="Text.Diff.Copy" xml:space="preserve">Copy</x:String>
<x:String x:Key="Text.Diff.HighlightedDiffNavigation" xml:space="preserve">Highlighted Diff Navigation</x:String>
<x:String x:Key="Text.Diff.FileModeChanged" xml:space="preserve">File Mode Changed</x:String>
<x:String x:Key="Text.Diff.IgnoreWhitespace" xml:space="preserve">Ignore Whitespace Change</x:String>
<x:String x:Key="Text.Diff.LFS" xml:space="preserve">LFS OBJECT CHANGE</x:String>

View file

@ -126,10 +126,12 @@
<x:String x:Key="Text.CommitDetail.Changes.Search" xml:space="preserve">Buscar Cambios...</x:String>
<x:String x:Key="Text.CommitDetail.Files" xml:space="preserve">ARCHIVOS</x:String>
<x:String x:Key="Text.CommitDetail.Files.LFS" xml:space="preserve">Archivo LFS</x:String>
<x:String x:Key="Text.CommitDetail.Files.Search" xml:space="preserve">Buscar Archivos...</x:String>
<x:String x:Key="Text.CommitDetail.Files.Submodule" xml:space="preserve">Submódulo</x:String>
<x:String x:Key="Text.CommitDetail.Info" xml:space="preserve">INFORMACIÓN</x:String>
<x:String x:Key="Text.CommitDetail.Info.Author" xml:space="preserve">AUTOR</x:String>
<x:String x:Key="Text.CommitDetail.Info.Changed" xml:space="preserve">CAMBIADO</x:String>
<x:String x:Key="Text.CommitDetail.Info.Children" xml:space="preserve">HIJOS</x:String>
<x:String x:Key="Text.CommitDetail.Info.Committer" xml:space="preserve">COMMITTER</x:String>
<x:String x:Key="Text.CommitDetail.Info.ContainsIn" xml:space="preserve">Ver refs que contienen este commit</x:String>
<x:String x:Key="Text.CommitDetail.Info.ContainsIn.Title" xml:space="preserve">COMMIT ESTÁ CONTENIDO EN</x:String>
@ -272,6 +274,7 @@
<x:String x:Key="Text.FastForwardWithoutCheck" xml:space="preserve">Fast-Forward (sin checkout)</x:String>
<x:String x:Key="Text.Fetch" xml:space="preserve">Fetch</x:String>
<x:String x:Key="Text.Fetch.AllRemotes" xml:space="preserve">Fetch todos los remotos</x:String>
<x:String x:Key="Text.Fetch.Force" xml:space="preserve">Utilizar opción '--force'</x:String>
<x:String x:Key="Text.Fetch.NoTags" xml:space="preserve">Fetch sin etiquetas</x:String>
<x:String x:Key="Text.Fetch.Remote" xml:space="preserve">Remoto:</x:String>
<x:String x:Key="Text.Fetch.Title" xml:space="preserve">Fetch Cambios Remotos</x:String>
@ -437,6 +440,9 @@
<x:String x:Key="Text.Preference.AI.Server" xml:space="preserve">Servidor</x:String>
<x:String x:Key="Text.Preference.Appearance" xml:space="preserve">APARIENCIA</x:String>
<x:String x:Key="Text.Preference.Appearance.DefaultFont" xml:space="preserve">Fuente por defecto</x:String>
<x:String x:Key="Text.Preference.Appearance.FontSize" xml:space="preserve">Tamaño de fuente</x:String>
<x:String x:Key="Text.Preference.Appearance.FontSize.Default" xml:space="preserve">Por defecto</x:String>
<x:String x:Key="Text.Preference.Appearance.FontSize.Editor" xml:space="preserve">Editor</x:String>
<x:String x:Key="Text.Preference.Appearance.MonospaceFont" xml:space="preserve">Fuente Monospace</x:String>
<x:String x:Key="Text.Preference.Appearance.OnlyUseMonoFontInEditor" xml:space="preserve">Usar solo fuente monospace en el editor de texto</x:String>
<x:String x:Key="Text.Preference.Appearance.Theme" xml:space="preserve">Tema</x:String>
@ -452,6 +458,7 @@
<x:String x:Key="Text.Preference.General.Locale" xml:space="preserve">Idioma</x:String>
<x:String x:Key="Text.Preference.General.MaxHistoryCommits" xml:space="preserve">Commits en el historial</x:String>
<x:String x:Key="Text.Preference.General.ShowAuthorTime" xml:space="preserve">Mostrar hora del autor en lugar de la hora del commit en el gráfico</x:String>
<x:String x:Key="Text.Preference.General.ShowChildren" xml:space="preserve">Mostrar hijos en los detalles de commit</x:String>
<x:String x:Key="Text.Preference.General.SubjectGuideLength" xml:space="preserve">Longitud de la guía del asunto</x:String>
<x:String x:Key="Text.Preference.Git" xml:space="preserve">GIT</x:String>
<x:String x:Key="Text.Preference.Git.CRLF" xml:space="preserve">Habilitar Auto CRLF</x:String>
@ -541,6 +548,13 @@
<x:String x:Key="Text.Repository.EnableReflog" xml:space="preserve">Habilitar Opción '--reflog'</x:String>
<x:String x:Key="Text.Repository.Explore" xml:space="preserve">Abrir en el Explorador</x:String>
<x:String x:Key="Text.Repository.Filter" xml:space="preserve">Buscar Ramas/Etiquetas/Submódulos</x:String>
<x:String x:Key="Text.Repository.FilterCommits" xml:space="preserve">Visibilidad en el Gráfico</x:String>
<x:String x:Key="Text.Repository.FilterCommits.Default" xml:space="preserve">Desestablecer</x:String>
<x:String x:Key="Text.Repository.FilterCommits.Exclude" xml:space="preserve">Ocultar en el Gráfico de Commits</x:String>
<x:String x:Key="Text.Repository.FilterCommits.Include" xml:space="preserve">Filtrar en el Gráfico de Commits</x:String>
<x:String x:Key="Text.Repository.HistoriesOrder" xml:space="preserve">Cambiar Modo de Ordenación</x:String>
<x:String x:Key="Text.Repository.HistoriesOrder.ByDate" xml:space="preserve">Fecha de Commit (--date-order)</x:String>
<x:String x:Key="Text.Repository.HistoriesOrder.Topo" xml:space="preserve">Topológicamente (--topo-order)</x:String>
<x:String x:Key="Text.Repository.LocalBranches" xml:space="preserve">RAMAS LOCALES</x:String>
<x:String x:Key="Text.Repository.NavigateToCurrentHead" xml:space="preserve">Navegar a HEAD</x:String>
<x:String x:Key="Text.Repository.FirstParentFilterToggle" xml:space="preserve">Habilitar Opción '--first-parent'</x:String>
@ -593,6 +607,7 @@
<x:String x:Key="Text.SelfUpdate.Title" xml:space="preserve">Actualización de Software</x:String>
<x:String x:Key="Text.SelfUpdate.UpToDate" xml:space="preserve">Actualmente no hay actualizaciones disponibles.</x:String>
<x:String x:Key="Text.SHALinkCM.CopySHA" xml:space="preserve">Copiar SHA</x:String>
<x:String x:Key="Text.SHALinkCM.NavigateTo" xml:space="preserve">Ir a</x:String>
<x:String x:Key="Text.Squash" xml:space="preserve">Squash Commits</x:String>
<x:String x:Key="Text.Squash.Into" xml:space="preserve">En:</x:String>
<x:String x:Key="Text.SSHKey" xml:space="preserve">Clave Privada SSH:</x:String>
@ -670,6 +685,7 @@
<x:String x:Key="Text.WorkingCopy.CommitAndPush" xml:space="preserve">COMMIT &amp; PUSH</x:String>
<x:String x:Key="Text.WorkingCopy.CommitMessageHelper" xml:space="preserve">Plantilla/Historias</x:String>
<x:String x:Key="Text.WorkingCopy.CommitTip" xml:space="preserve">Activar evento de clic</x:String>
<x:String x:Key="Text.WorkingCopy.CommitToEdit" xml:space="preserve">Commit (Editar)</x:String>
<x:String x:Key="Text.WorkingCopy.CommitWithAutoStage" xml:space="preserve">Stagear todos los cambios y commit</x:String>
<x:String x:Key="Text.WorkingCopy.ConfirmCommitWithoutFiles" xml:space="preserve">¡Commit vacío detectado! ¿Quieres continuar (--allow-empty)?</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts" xml:space="preserve">CONFLICTOS DETECTADOS</x:String>

View file

@ -124,6 +124,7 @@
<x:String x:Key="Text.CommitDetail.Changes.Search" xml:space="preserve">查找变更...</x:String>
<x:String x:Key="Text.CommitDetail.Files" xml:space="preserve">文件列表</x:String>
<x:String x:Key="Text.CommitDetail.Files.LFS" xml:space="preserve">LFS文件</x:String>
<x:String x:Key="Text.CommitDetail.Files.Search" xml:space="preserve">查找文件...</x:String>
<x:String x:Key="Text.CommitDetail.Files.Submodule" xml:space="preserve">子模块</x:String>
<x:String x:Key="Text.CommitDetail.Info" xml:space="preserve">基本信息</x:String>
<x:String x:Key="Text.CommitDetail.Info.Author" xml:space="preserve">修改者</x:String>

View file

@ -124,6 +124,7 @@
<x:String x:Key="Text.CommitDetail.Changes.Search" xml:space="preserve">搜尋變更...</x:String>
<x:String x:Key="Text.CommitDetail.Files" xml:space="preserve">檔案列表</x:String>
<x:String x:Key="Text.CommitDetail.Files.LFS" xml:space="preserve">LFS 檔案</x:String>
<x:String x:Key="Text.CommitDetail.Files.Search" xml:space="preserve">搜尋檔案...</x:String>
<x:String x:Key="Text.CommitDetail.Files.Submodule" xml:space="preserve">子模組</x:String>
<x:String x:Key="Text.CommitDetail.Info" xml:space="preserve">基本資訊</x:String>
<x:String x:Key="Text.CommitDetail.Info.Author" xml:space="preserve">作者</x:String>

View file

@ -856,12 +856,7 @@
</Border.Effect>
</Border>
<Border BorderThickness="0"
Margin="8,4"
MaxWidth="{DynamicResource FlyoutThemeMaxWidth}"
MinHeight="{DynamicResource MenuFlyoutThemeMinHeight}"
HorizontalAlignment="Stretch"
CornerRadius="{DynamicResource OverlayCornerRadius}">
<Border BorderThickness="0" Margin="8,4" MaxHeight="400">
<ScrollViewer Theme="{StaticResource FluentMenuScrollViewer}">
<ItemsPresenter Name="PART_ItemsPresenter"
Margin="{DynamicResource MenuFlyoutScrollerMargin}"

View file

@ -82,7 +82,7 @@ namespace SourceGit.ViewModels
{
get;
private set;
} = new AvaloniaList<string>();
} = [];
public string SearchChangeFilter
{
@ -106,13 +106,70 @@ namespace SourceGit.ViewModels
{
get;
private set;
} = new AvaloniaList<Models.CommitLink>();
} = [];
public AvaloniaList<Models.IssueTrackerRule> IssueTrackerRules
{
get => _repo.Settings?.IssueTrackerRules;
}
public string RevisionFileSearchFilter
{
get => _revisionFileSearchFilter;
set
{
if (SetProperty(ref _revisionFileSearchFilter, value))
{
RevisionFileSearchSuggestion.Clear();
if (!string.IsNullOrEmpty(value))
{
if (_revisionFiles.Count == 0)
{
var sha = Commit.SHA;
Task.Run(() =>
{
var files = new Commands.QueryRevisionFileNames(_repo.FullPath, sha).Result();
Dispatcher.UIThread.Invoke(() => {
if (sha == Commit.SHA)
{
_revisionFiles.Clear();
_revisionFiles.AddRange(files);
if (!string.IsNullOrEmpty(_revisionFileSearchFilter))
UpdateRevisionFileSearchSuggestion();
}
});
});
}
else
{
UpdateRevisionFileSearchSuggestion();
}
}
else
{
IsRevisionFileSearchSuggestionOpen = false;
GC.Collect();
}
}
}
}
public AvaloniaList<string> RevisionFileSearchSuggestion
{
get;
private set;
} = [];
public bool IsRevisionFileSearchSuggestionOpen
{
get => _isRevisionFileSearchSuggestionOpen;
set => SetProperty(ref _isRevisionFileSearchSuggestionOpen, value);
}
public CommitDetail(Repository repo)
{
_repo = repo;
@ -147,17 +204,23 @@ namespace SourceGit.ViewModels
{
_repo = null;
_commit = null;
if (_changes != null)
_changes.Clear();
if (_visibleChanges != null)
_visibleChanges.Clear();
if (_selectedChanges != null)
_selectedChanges.Clear();
_signInfo = null;
_searchChangeFilter = null;
_diffContext = null;
_viewRevisionFileContent = null;
_cancelToken = null;
WebLinks.Clear();
_revisionFiles.Clear();
RevisionFileSearchSuggestion.Clear();
}
public void NavigateTo(string commitSHA)
@ -175,6 +238,11 @@ namespace SourceGit.ViewModels
SearchChangeFilter = string.Empty;
}
public void ClearRevisionFileSearchFilter()
{
RevisionFileSearchFilter = string.Empty;
}
public Models.Commit GetParent(string sha)
{
return new Commands.QuerySingleCommit(_repo.FullPath, sha).Result();
@ -543,6 +611,8 @@ namespace SourceGit.ViewModels
private void Refresh()
{
_changes = null;
_revisionFiles.Clear();
FullMessage = string.Empty;
SignInfo = null;
Changes = [];
@ -550,6 +620,10 @@ namespace SourceGit.ViewModels
SelectedChanges = null;
ViewRevisionFileContent = null;
Children.Clear();
RevisionFileSearchFilter = string.Empty;
IsRevisionFileSearchSuggestionOpen = false;
GC.Collect();
if (_commit == null)
return;
@ -716,6 +790,24 @@ namespace SourceGit.ViewModels
menu.Items.Add(new MenuItem() { Header = "-" });
}
private void UpdateRevisionFileSearchSuggestion()
{
var suggestion = new List<string>();
foreach (var file in _revisionFiles)
{
if (file.Contains(_revisionFileSearchFilter, StringComparison.OrdinalIgnoreCase) &&
file.Length != _revisionFileSearchFilter.Length)
suggestion.Add(file);
if (suggestion.Count >= 100)
break;
}
RevisionFileSearchSuggestion.Clear();
RevisionFileSearchSuggestion.AddRange(suggestion);
IsRevisionFileSearchSuggestionOpen = suggestion.Count > 0;
}
[GeneratedRegex(@"^version https://git-lfs.github.com/spec/v\d+\r?\noid sha256:([0-9a-f]+)\r?\nsize (\d+)[\r\n]*$")]
private static partial Regex REG_LFS_FORMAT();
@ -736,5 +828,8 @@ namespace SourceGit.ViewModels
private DiffContext _diffContext = null;
private object _viewRevisionFileContent = null;
private Commands.Command.CancelToken _cancelToken = null;
private List<string> _revisionFiles = [];
private string _revisionFileSearchFilter = string.Empty;
private bool _isRevisionFileSearchSuggestionOpen = false;
}
}

View file

@ -51,6 +51,12 @@ namespace SourceGit.ViewModels
private set => SetProperty(ref _unifiedLines, value);
}
public string ChangeBlockIndicator
{
get => _changeBlockIndicator;
private set => SetProperty(ref _changeBlockIndicator, value);
}
public DiffContext(string repo, Models.DiffOption option, DiffContext previous = null)
{
_repo = repo;
@ -73,12 +79,68 @@ namespace SourceGit.ViewModels
LoadDiffContent();
}
public void PrevChange()
{
if (_content is Models.TextDiff textDiff)
{
if (textDiff.CurrentChangeBlockIdx > 0)
{
textDiff.CurrentChangeBlockIdx--;
}
else if (textDiff.ChangeBlocks.Count > 0)
{
// Force property value change and (re-)jump to first change block
textDiff.CurrentChangeBlockIdx = -1;
textDiff.CurrentChangeBlockIdx = 0;
}
}
RefreshChangeBlockIndicator();
}
public void NextChange()
{
if (_content is Models.TextDiff textDiff)
{
if (textDiff.CurrentChangeBlockIdx < textDiff.ChangeBlocks.Count - 1)
{
textDiff.CurrentChangeBlockIdx++;
}
else if (textDiff.ChangeBlocks.Count > 0)
{
// Force property value change and (re-)jump to last change block
textDiff.CurrentChangeBlockIdx = -1;
textDiff.CurrentChangeBlockIdx = textDiff.ChangeBlocks.Count - 1;
}
RefreshChangeBlockIndicator();
}
}
public void RefreshChangeBlockIndicator()
{
string curr = "-", tot = "-";
if (_content is Models.TextDiff textDiff)
{
if (textDiff.CurrentChangeBlockIdx >= 0)
curr = (textDiff.CurrentChangeBlockIdx + 1).ToString();
tot = (textDiff.ChangeBlocks.Count).ToString();
}
ChangeBlockIndicator = curr + "/" + tot;
}
public void ToggleFullTextDiff()
{
Preference.Instance.UseFullTextDiff = !Preference.Instance.UseFullTextDiff;
LoadDiffContent();
}
public void ToggleHighlightedDiffNavigation()
{
Preference.Instance.EnableChangeBlocks = !Preference.Instance.EnableChangeBlocks;
if (_content is Models.TextDiff textDiff)
textDiff.CurrentChangeBlockIdx = -1;
RefreshChangeBlockIndicator();
}
public void IncrUnified()
{
UnifiedLines = _unifiedLines + 1;
@ -91,6 +153,12 @@ namespace SourceGit.ViewModels
LoadDiffContent();
}
public void ToggleTwoSideDiff()
{
Preference.Instance.UseSideBySideDiff = !Preference.Instance.UseSideBySideDiff;
RefreshChangeBlockIndicator();
}
public void OpenExternalMergeTool()
{
var toolType = Preference.Instance.ExternalMergeToolType;
@ -217,6 +285,8 @@ namespace SourceGit.ViewModels
FileModeChange = latest.FileModeChange;
Content = rs;
IsTextDiff = rs is Models.TextDiff;
RefreshChangeBlockIndicator();
});
});
}
@ -281,6 +351,7 @@ namespace SourceGit.ViewModels
private string _title;
private string _fileModeChange = string.Empty;
private int _unifiedLines = 4;
private string _changeBlockIndicator = "-/-";
private bool _isTextDiff = false;
private bool _ignoreWhitespace = false;
private object _content = null;

View file

@ -206,6 +206,12 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _useFullTextDiff, value);
}
public bool EnableChangeBlocks
{
get => _enableChangeBlocks;
set => SetProperty(ref _enableChangeBlocks, value);
}
public Models.ChangeViewMode UnstagedChangeViewMode
{
get => _unstagedChangeViewMode;
@ -614,6 +620,7 @@ namespace SourceGit.ViewModels
private bool _enableDiffViewWordWrap = false;
private bool _showHiddenSymbolsInDiffView = false;
private bool _useFullTextDiff = false;
private bool _enableChangeBlocks = false;
private Models.ChangeViewMode _unstagedChangeViewMode = Models.ChangeViewMode.List;
private Models.ChangeViewMode _stagedChangeViewMode = Models.ChangeViewMode.List;

View file

@ -427,12 +427,12 @@ namespace SourceGit.ViewModels
{
// Ignore
}
_settings = null;
_historiesFilterMode = Models.FilterMode.None;
_autoFetchTimer.Dispose();
_autoFetchTimer = null;
_settings = null;
_historiesFilterMode = Models.FilterMode.None;
_watcher?.Dispose();
_histories.Cleanup();
_workingCopy.Cleanup();
@ -2147,7 +2147,7 @@ namespace SourceGit.ViewModels
{
Task.Run(() =>
{
var files = new Commands.QueryCurrentRevisionFiles(_fullpath).Result();
var files = new Commands.QueryRevisionFileNames(_fullpath, "HEAD").Result();
Dispatcher.UIThread.Invoke(() =>
{
if (_searchCommitFilterType != 3)

View file

@ -45,10 +45,43 @@ namespace SourceGit.ViewModels
FillEmptyLines();
ProcessChangeBlocks();
if (previous != null && previous.File == File)
_syncScrollOffset = previous._syncScrollOffset;
}
public List<Models.TextDiffChangeBlock> ChangeBlocks { get; set; } = [];
public void ProcessChangeBlocks()
{
ChangeBlocks.Clear();
int lineIdx = 0, blockStartIdx = 0;
bool isNewBlock = true;
foreach (var line in Old) // NOTE: Same block size in both Old and New lines.
{
lineIdx++;
if (line.Type == Models.TextDiffLineType.Added ||
line.Type == Models.TextDiffLineType.Deleted ||
line.Type == Models.TextDiffLineType.None) // Empty
{
if (isNewBlock)
{
isNewBlock = false;
blockStartIdx = lineIdx;
}
}
else
{
if (!isNewBlock)
{
ChangeBlocks.Add(new Models.TextDiffChangeBlock(blockStartIdx, lineIdx - 1));
isNewBlock = true;
}
}
}
}
public void ConvertsToCombinedRange(Models.TextDiff combined, ref int startLine, ref int endLine, bool isOldSide)
{
endLine = Math.Min(endLine, combined.Lines.Count - 1);

View file

@ -16,6 +16,18 @@
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal">
<Path Width="14" Height="14" Margin="8,0" Data="{StaticResource Icons.Branch}"/>
<TextBlock Text="{Binding Target.FriendlyName}"/>
<Border Height="18"
Margin="8,0,0,0"
Padding="8,0"
VerticalAlignment="Center"
CornerRadius="9"
Background="{DynamicResource Brush.Badge}"
IsVisible="{Binding Target.TrackStatus.IsVisible}">
<TextBlock Foreground="{DynamicResource Brush.BadgeFG}"
FontFamily="{DynamicResource Fonts.Monospace}"
FontSize="10"
Text="{Binding Target.TrackStatus}"/>
</Border>
</StackPanel>
<Border Grid.Row="1" Grid.Column="1" Height="32" IsVisible="{Binding !Target.IsLocal}">

View file

@ -42,9 +42,22 @@
<ListBox.ItemTemplate>
<DataTemplate DataType="m:Branch">
<Grid Height="26" ColumnDefinitions="22,*">
<Grid Height="26" ColumnDefinitions="22,*,Auto">
<Path Grid.Column="0" Width="10" Height="10" Margin="4,0,8,0" Data="{StaticResource Icons.Branch}" />
<TextBlock Grid.Column="1" Text="{Binding FriendlyName}" Classes="primary" />
<Border Grid.Column="2"
Height="18"
Margin="8,0"
Padding="8,0"
VerticalAlignment="Center"
CornerRadius="9"
Background="{DynamicResource Brush.Badge}"
IsVisible="{Binding TrackStatus.IsVisible}">
<TextBlock Foreground="{DynamicResource Brush.BadgeFG}"
FontFamily="{DynamicResource Fonts.Monospace}"
FontSize="10"
Text="{Binding TrackStatus}"/>
</Border>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>

View file

@ -34,6 +34,15 @@
<!-- Toolbar Buttons -->
<StackPanel Grid.Column="3" Margin="8,0,0,0" Orientation="Horizontal" VerticalAlignment="Center">
<ToggleButton Classes="line_path"
Width="28"
Command="{Binding ToggleHighlightedDiffNavigation}"
IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=EnableChangeBlocks, Mode=OneWay}"
IsVisible="{Binding IsTextDiff}"
ToolTip.Tip="{DynamicResource Text.Diff.HighlightedDiffNavigation}">
<Path Width="13" Height="13" Data="{StaticResource Icons.Highlight}" Margin="0,3,0,0"/>
</ToggleButton>
<Button Classes="icon_button"
Width="28"
Click="OnGotoPrevChange"
@ -42,6 +51,18 @@
<Path Width="12" Height="12" Stretch="Uniform" Margin="0,6,0,0" Data="{StaticResource Icons.Up}"/>
</Button>
<TextBlock Classes="primary"
Margin="0,0,0,0"
Text="{Binding ChangeBlockIndicator}"
FontSize="11">
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="IsTextDiff"/>
<Binding Source="{x:Static vm:Preference.Instance}" Path="EnableChangeBlocks" Mode="OneWay"/>
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
<Button Classes="icon_button"
Width="28"
Click="OnGotoNextChange"
@ -124,7 +145,8 @@
<ToggleButton Classes="line_path"
Width="28" Height="18"
IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSideBySideDiff, Mode=TwoWay}"
Command="{Binding ToggleTwoSideDiff}"
IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSideBySideDiff, Mode=OneWay}"
IsVisible="{Binding IsTextDiff}"
ToolTip.Tip="{DynamicResource Text.Diff.SideBySide}">
<Path Width="12" Height="12" Data="{StaticResource Icons.LayoutHorizontal}" Margin="0,2,0,0"/>
@ -241,7 +263,8 @@
<DataTemplate DataType="m:TextDiff">
<v:TextDiffView
UseSideBySideDiff="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSideBySideDiff, Mode=OneWay}"
UseFullTextDiff="{Binding Source={x:Static vm:Preference.Instance}, Path=UseFullTextDiff, Mode=OneWay}"/>
UseFullTextDiff="{Binding Source={x:Static vm:Preference.Instance}, Path=UseFullTextDiff, Mode=OneWay}"
CurrentChangeBlockIdx="{Binding CurrentChangeBlockIdx, Mode=OneWay}"/>
</DataTemplate>
<!-- Empty or only EOL changes -->

View file

@ -12,6 +12,13 @@ namespace SourceGit.Views
}
private void OnGotoPrevChange(object _, RoutedEventArgs e)
{
if (ViewModels.Preference.Instance.EnableChangeBlocks)
{
if (DataContext is ViewModels.DiffContext diffCtx)
diffCtx.PrevChange();
}
else
{
var textDiff = this.FindDescendantOfType<ThemedTextDiffPresenter>();
if (textDiff == null)
@ -23,8 +30,16 @@ namespace SourceGit.Views
e.Handled = true;
}
}
private void OnGotoNextChange(object _, RoutedEventArgs e)
{
if (ViewModels.Preference.Instance.EnableChangeBlocks)
{
if (DataContext is ViewModels.DiffContext diffCtx)
diffCtx.NextChange();
}
else
{
var textDiff = this.FindDescendantOfType<ThemedTextDiffPresenter>();
if (textDiff == null)
@ -38,3 +53,4 @@ namespace SourceGit.Views
}
}
}
}

View file

@ -430,31 +430,43 @@ namespace SourceGit.Views
if (ShowAsDateTime)
return DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime().ToString("yyyy/MM/dd HH:mm:ss");
var today = DateTime.Today;
var localTime = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime();
if (localTime >= today)
{
var now = DateTime.Now;
var timespan = now - localTime;
if (timespan.TotalHours > 1)
return App.Text("Period.HoursAgo", (int)timespan.TotalHours);
var localTime = DateTime.UnixEpoch.AddSeconds(timestamp).ToLocalTime();
var span = now - localTime;
if (span.TotalMinutes < 1)
return App.Text("Period.JustNow");
return timespan.TotalMinutes < 1 ? App.Text("Period.JustNow") : App.Text("Period.MinutesAgo", (int)timespan.TotalMinutes);
}
if (span.TotalHours < 1)
return App.Text("Period.MinutesAgo", (int)span.TotalMinutes);
var diffYear = today.Year - localTime.Year;
if (diffYear == 0)
if (span.TotalDays < 1)
return App.Text("Period.HoursAgo", (int)span.TotalHours);
var lastDay = now.AddDays(-1).Date;
if (localTime >= lastDay)
return App.Text("Period.Yesterday");
if ((localTime.Year == now.Year && localTime.Month == now.Month) || span.TotalDays < 28)
{
var diffMonth = today.Month - localTime.Month;
if (diffMonth > 0)
return diffMonth == 1 ? App.Text("Period.LastMonth") : App.Text("Period.MonthsAgo", diffMonth);
var diffDay = today.Day - localTime.Day;
return diffDay == 1 ? App.Text("Period.Yesterday") : App.Text("Period.DaysAgo", diffDay);
var diffDay = now.Date - localTime.Date;
return App.Text("Period.DaysAgo", (int)diffDay.TotalDays);
}
return diffYear == 1 ? App.Text("Period.LastYear") : App.Text("Period.YearsAgo", diffYear);
var lastMonth = now.AddMonths(-1).Date;
if (localTime.Year == lastMonth.Year && localTime.Month == lastMonth.Month)
return App.Text("Period.LastMonth");
if (localTime.Year == now.Year || localTime > now.AddMonths(-11))
{
var diffMonth = (12 + now.Month - localTime.Month) % 12;
return App.Text("Period.MonthsAgo", diffMonth);
}
var diffYear = now.Year - localTime.Year;
if (diffYear == 1)
return App.Text("Period.LastYear");
return App.Text("Period.YearsAgo", diffYear);
}
private IDisposable _refreshTimer = null;

View file

@ -144,6 +144,68 @@ namespace SourceGit.Views
InitializeComponent();
}
public void SetSearchResult(string file)
{
_rows.Clear();
_searchResult.Clear();
var rows = new List<ViewModels.RevisionFileTreeNode>();
if (string.IsNullOrEmpty(file))
{
MakeRows(rows, _tree, 0);
}
else
{
var vm = DataContext as ViewModels.CommitDetail;
if (vm == null || vm.Commit == null)
return;
var objects = vm.GetRevisionFilesUnderFolder(file);
if (objects == null || objects.Count != 1)
return;
var routes = file.Split('/', StringSplitOptions.None);
if (routes.Length == 1)
{
_searchResult.Add(new ViewModels.RevisionFileTreeNode
{
Backend = objects[0]
});
}
else
{
var last = _searchResult;
var prefix = string.Empty;
for (var i = 0; i < routes.Length - 1; i++)
{
var folder = new ViewModels.RevisionFileTreeNode
{
Backend = new Models.Object
{
Type = Models.ObjectType.Tree,
Path = prefix + routes[i],
},
IsExpanded = true,
};
last.Add(folder);
last = folder.Children;
prefix = folder.Backend + "/";
}
last.Add(new ViewModels.RevisionFileTreeNode
{
Backend = objects[0]
});
}
MakeRows(rows, _searchResult, 0);
}
_rows.AddRange(rows);
GC.Collect();
}
public void ToggleNodeIsExpanded(ViewModels.RevisionFileTreeNode node)
{
_disableSelectionChangingEvent = true;
@ -189,6 +251,7 @@ namespace SourceGit.Views
{
_tree.Clear();
_rows.Clear();
_searchResult.Clear();
var vm = DataContext as ViewModels.CommitDetail;
if (vm == null || vm.Commit == null)
@ -308,5 +371,6 @@ namespace SourceGit.Views
private List<ViewModels.RevisionFileTreeNode> _tree = [];
private AvaloniaList<ViewModels.RevisionFileTreeNode> _rows = [];
private bool _disableSelectionChangingEvent = false;
private List<ViewModels.RevisionFileTreeNode> _searchResult = [];
}
}

View file

@ -4,6 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:SourceGit.ViewModels"
xmlns:v="using:SourceGit.Views"
xmlns:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.RevisionFiles"
x:DataType="vm:CommitDetail">
@ -14,17 +15,96 @@
<ColumnDefinition Width="*" MinWidth="100"/>
</Grid.ColumnDefinitions>
<!-- File Tree -->
<Border Grid.Column="0" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="1" Background="{DynamicResource Brush.Contents}">
<v:RevisionFileTreeView Revision="{Binding Commit.SHA}"/>
<!-- Left -->
<Grid Grid.Column="0" RowDefinitions="26,*">
<Grid Grid.Row="0" Height="26">
<TextBox Grid.Row="0"
x:Name="TxtSearchRevisionFiles"
Height="26"
BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}"
Background="Transparent"
CornerRadius="4"
Watermark="{DynamicResource Text.CommitDetail.Files.Search}"
Text="{Binding RevisionFileSearchFilter, Mode=TwoWay}"
KeyDown="OnSearchBoxKeyDown"
TextChanged="OnSearchBoxTextChanged">
<TextBox.InnerLeftContent>
<Path Width="14" Height="14" Margin="4,0,0,0" Fill="{DynamicResource Brush.FG2}" Data="{StaticResource Icons.Search}"/>
</TextBox.InnerLeftContent>
<TextBox.InnerRightContent>
<Button Classes="icon_button"
IsVisible="{Binding RevisionFileSearchFilter, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
Command="{Binding ClearRevisionFileSearchFilter}">
<Path Width="14" Height="14" Fill="{DynamicResource Brush.FG2}" Data="{StaticResource Icons.Clear}"/>
</Button>
</TextBox.InnerRightContent>
</TextBox>
<Popup PlacementTarget="{Binding #TxtSearchRevisionFiles}"
Placement="BottomEdgeAlignedLeft"
HorizontalOffset="-8" VerticalAlignment="-8"
IsOpen="{Binding IsRevisionFileSearchSuggestionOpen}">
<Border Margin="8" VerticalAlignment="Top" Effect="drop-shadow(0 0 8 #80000000)">
<Border Background="{DynamicResource Brush.Popup}" CornerRadius="4" Padding="4" BorderThickness="0.65" BorderBrush="{DynamicResource Brush.Accent}">
<ListBox x:Name="SearchSuggestionBox"
Background="Transparent"
SelectionMode="Single"
ItemsSource="{Binding RevisionFileSearchSuggestion}"
MaxHeight="400"
Focusable="True"
KeyDown="OnSearchSuggestionBoxKeyDown">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Padding" Value="0"/>
<Setter Property="MinHeight" Value="26"/>
<Setter Property="CornerRadius" Value="4"/>
</Style>
<Style Selector="ListBox">
<Setter Property="FocusAdorner">
<FocusAdornerTemplate>
<Grid/>
</FocusAdornerTemplate>
</Setter>
</Style>
</ListBox.Styles>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="x:String">
<StackPanel Background="Transparent" Orientation="Vertical" Margin="8,4" DoubleTapped="OnSearchSuggestionDoubleTapped">
<StackPanel Orientation="Horizontal">
<Path Width="12" Height="12" Data="{StaticResource Icons.File}"/>
<TextBlock Classes="primary" Margin="6,0,0,0" Text="{Binding Converter={x:Static c:PathConverters.PureFileName}}"/>
</StackPanel>
<TextBlock Classes="primary" FontSize="12" Margin="18,2,0,0" Foreground="{DynamicResource Brush.FG2}" Text="{Binding Converter={x:Static c:PathConverters.PureDirectoryName}}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Border>
</Popup>
</Grid>
<!-- File Tree -->
<Border Grid.Row="1" Margin="0,4,0,0" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="1" Background="{DynamicResource Brush.Contents}">
<v:RevisionFileTreeView x:Name="FileTree" Revision="{Binding Commit.SHA}"/>
</Border>
</Grid>
<GridSplitter Grid.Column="1"
MinWidth="1"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Background="Transparent"/>
<!-- File Content Viewer -->
<!-- Right: File Content Viewer -->
<Grid Grid.Column="2">
<Border BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}">
<v:RevisionFileContentViewer Content="{Binding ViewRevisionFileContent}"/>

View file

@ -3,6 +3,7 @@ using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
@ -118,5 +119,81 @@ namespace SourceGit.Views
{
InitializeComponent();
}
private void OnSearchBoxKeyDown(object _, KeyEventArgs e)
{
var vm = DataContext as ViewModels.CommitDetail;
if (vm == null)
return;
if (e.Key == Key.Enter)
{
FileTree.SetSearchResult(vm.RevisionFileSearchFilter);
e.Handled = true;
}
else if (e.Key == Key.Down || e.Key == Key.Up)
{
if (vm.IsRevisionFileSearchSuggestionOpen)
{
SearchSuggestionBox.Focus(NavigationMethod.Tab);
SearchSuggestionBox.SelectedIndex = 0;
}
e.Handled = true;
}
else if (e.Key == Key.Escape)
{
if (vm.IsRevisionFileSearchSuggestionOpen)
{
vm.RevisionFileSearchSuggestion.Clear();
vm.IsRevisionFileSearchSuggestionOpen = false;
}
e.Handled = true;
}
}
private void OnSearchBoxTextChanged(object _, TextChangedEventArgs e)
{
if (string.IsNullOrEmpty(TxtSearchRevisionFiles.Text))
FileTree.SetSearchResult(null);
}
private void OnSearchSuggestionBoxKeyDown(object _, KeyEventArgs e)
{
var vm = DataContext as ViewModels.CommitDetail;
if (vm == null)
return;
if (e.Key == Key.Escape)
{
vm.RevisionFileSearchSuggestion.Clear();
e.Handled = true;
}
else if (e.Key == Key.Enter && SearchSuggestionBox.SelectedItem is string content)
{
vm.RevisionFileSearchFilter = content;
TxtSearchRevisionFiles.CaretIndex = content.Length;
FileTree.SetSearchResult(vm.RevisionFileSearchFilter);
e.Handled = true;
}
}
private void OnSearchSuggestionDoubleTapped(object sender, TappedEventArgs e)
{
var vm = DataContext as ViewModels.CommitDetail;
if (vm == null)
return;
var content = (sender as StackPanel)?.DataContext as string;
if (!string.IsNullOrEmpty(content))
{
vm.RevisionFileSearchFilter = content;
TxtSearchRevisionFiles.CaretIndex = content.Length;
FileTree.SetSearchResult(vm.RevisionFileSearchFilter);
}
e.Handled = true;
}
}
}

View file

@ -30,6 +30,7 @@
UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}"
WordWrap="{Binding Source={x:Static vm:Preference.Instance}, Path=EnableDiffViewWordWrap}"
ShowHiddenSymbols="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView}"
CurrentChangeBlockIdx="{Binding #ThisControl.CurrentChangeBlockIdx}"
EnableChunkSelection="{Binding #ThisControl.EnableChunkSelection}"
SelectedChunk="{Binding #ThisControl.SelectedChunk, Mode=TwoWay}"/>
@ -61,6 +62,7 @@
UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}"
WordWrap="False"
ShowHiddenSymbols="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView}"
CurrentChangeBlockIdx="{Binding #ThisControl.CurrentChangeBlockIdx}"
EnableChunkSelection="{Binding #ThisControl.EnableChunkSelection}"
SelectedChunk="{Binding #ThisControl.SelectedChunk, Mode=TwoWay}"/>
@ -82,6 +84,7 @@
UseSyntaxHighlighting="{Binding Source={x:Static vm:Preference.Instance}, Path=UseSyntaxHighlighting}"
WordWrap="False"
ShowHiddenSymbols="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowHiddenSymbolsInDiffView}"
CurrentChangeBlockIdx="{Binding #ThisControl.CurrentChangeBlockIdx}"
EnableChunkSelection="{Binding #ThisControl.EnableChunkSelection}"
SelectedChunk="{Binding #ThisControl.SelectedChunk, Mode=TwoWay}"/>

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using Avalonia;
@ -254,6 +255,10 @@ namespace SourceGit.Views
if (_presenter.Document == null || !textView.VisualLinesValid)
return;
var changeBlock = _presenter.GetCurrentChangeBlock();
Brush changeBlockBG = new SolidColorBrush(Colors.Gray, 0.25);
Pen changeBlockFG = new Pen(Brushes.Gray, 1);
var lines = _presenter.GetLines();
var width = textView.Bounds.Width;
foreach (var line in textView.VisualLines)
@ -266,12 +271,14 @@ namespace SourceGit.Views
break;
var info = lines[index - 1];
var bg = GetBrushByLineType(info.Type);
if (bg == null)
continue;
var startY = line.GetTextLineVisualYPosition(line.TextLines[0], VisualYPosition.LineTop) - textView.VerticalOffset;
var endY = line.GetTextLineVisualYPosition(line.TextLines[^1], VisualYPosition.LineBottom) - textView.VerticalOffset;
var bg = GetBrushByLineType(info.Type);
if (bg != null)
{
if (bg != null)
drawingContext.DrawRectangle(bg, null, new Rect(0, startY, width, endY - startY));
if (info.Highlights.Count > 0)
@ -279,7 +286,7 @@ namespace SourceGit.Views
var highlightBG = info.Type == Models.TextDiffLineType.Added ? _presenter.AddedHighlightBrush : _presenter.DeletedHighlightBrush;
var processingIdxStart = 0;
var processingIdxEnd = 0;
var nextHightlight = 0;
var nextHighlight = 0;
foreach (var tl in line.TextLines)
{
@ -288,9 +295,9 @@ namespace SourceGit.Views
var y = line.GetTextLineVisualYPosition(tl, VisualYPosition.LineTop) - textView.VerticalOffset;
var h = line.GetTextLineVisualYPosition(tl, VisualYPosition.LineBottom) - textView.VerticalOffset - y;
while (nextHightlight < info.Highlights.Count)
while (nextHighlight < info.Highlights.Count)
{
var highlight = info.Highlights[nextHightlight];
var highlight = info.Highlights[nextHighlight];
if (highlight.Start >= processingIdxEnd)
break;
@ -305,13 +312,23 @@ namespace SourceGit.Views
if (highlight.End >= processingIdxEnd)
break;
nextHightlight++;
nextHighlight++;
}
processingIdxStart = processingIdxEnd;
}
}
}
if (changeBlock != null && changeBlock.IsInRange(index))
{
drawingContext.DrawRectangle(changeBlockBG, null, new Rect(0, startY, width, endY - startY));
if (index == changeBlock.StartLine)
drawingContext.DrawLine(changeBlockFG, new Point(0, startY), new Point(width, startY));
if (index == changeBlock.EndLine)
drawingContext.DrawLine(changeBlockFG, new Point(0, endY), new Point(width, endY));
}
}
}
private IBrush GetBrushByLineType(Models.TextDiffLineType type)
@ -486,6 +503,15 @@ namespace SourceGit.Views
set => SetValue(DisplayRangeProperty, value);
}
public static readonly StyledProperty<int> CurrentChangeBlockIdxProperty =
AvaloniaProperty.Register<ThemedTextDiffPresenter, int>(nameof(CurrentChangeBlockIdx));
public int CurrentChangeBlockIdx
{
get => GetValue(CurrentChangeBlockIdxProperty);
set => SetValue(CurrentChangeBlockIdxProperty, value);
}
protected override Type StyleKeyOverride => typeof(TextEditor);
public ThemedTextDiffPresenter(TextArea area, TextDocument doc) : base(area, doc)
@ -590,6 +616,28 @@ namespace SourceGit.Views
}
}
public Models.TextDiffChangeBlock GetCurrentChangeBlock()
{
return GetChangeBlock(CurrentChangeBlockIdx);
}
public virtual Models.TextDiffChangeBlock GetChangeBlock(int changeBlockIdx)
{
return null;
}
public void JumpToChangeBlock(int changeBlockIdx)
{
var changeBlock = GetChangeBlock(changeBlockIdx);
if (changeBlock != null)
{
TextArea.Caret.Line = changeBlock.StartLine;
//TextArea.Caret.BringCaretToView(); // NOTE: Brings caret line (barely) into view.
ScrollToLine(changeBlock.StartLine); // NOTE: Brings specified line into center of view.
}
TextArea.TextView.Redraw();
}
public override void Render(DrawingContext context)
{
base.Render(context);
@ -665,6 +713,10 @@ namespace SourceGit.Views
{
InvalidateVisual();
}
else if (change.Property == CurrentChangeBlockIdxProperty)
{
InvalidateVisual();
}
}
private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e)
@ -1018,6 +1070,16 @@ namespace SourceGit.Views
}
}
public override Models.TextDiffChangeBlock GetChangeBlock(int changeBlockIdx)
{
if (DataContext is Models.TextDiff diff)
{
if (changeBlockIdx >= 0 && changeBlockIdx < diff.ChangeBlocks.Count)
return diff.ChangeBlocks[changeBlockIdx];
}
return null;
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
@ -1089,6 +1151,8 @@ namespace SourceGit.Views
public void ForceSyncScrollOffset()
{
if (_scrollViewer == null)
return;
if (DataContext is ViewModels.TwoSideTextDiff diff)
diff.SyncScrollOffset = _scrollViewer?.Offset ?? Vector.Zero;
}
@ -1234,6 +1298,16 @@ namespace SourceGit.Views
}
}
public override Models.TextDiffChangeBlock GetChangeBlock(int changeBlockIdx)
{
if (DataContext is ViewModels.TwoSideTextDiff diff)
{
if (changeBlockIdx >= 0 && changeBlockIdx < diff.ChangeBlocks.Count)
return diff.ChangeBlocks[changeBlockIdx];
}
return null;
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
@ -1480,6 +1554,15 @@ namespace SourceGit.Views
set => SetValue(EnableChunkSelectionProperty, value);
}
public static readonly StyledProperty<int> CurrentChangeBlockIdxProperty =
AvaloniaProperty.Register<TextDiffView, int>(nameof(CurrentChangeBlockIdx));
public int CurrentChangeBlockIdx
{
get => GetValue(CurrentChangeBlockIdxProperty);
set => SetValue(CurrentChangeBlockIdxProperty, value);
}
static TextDiffView()
{
UseSideBySideDiffProperty.Changed.AddClassHandler<TextDiffView>((v, _) =>
@ -1506,6 +1589,19 @@ namespace SourceGit.Views
v.Popup.Margin = new Thickness(0, top, right, 0);
v.Popup.IsVisible = true;
});
CurrentChangeBlockIdxProperty.Changed.AddClassHandler<TextDiffView>((v, e) =>
{
if (v.Editor.Presenter != null)
{
foreach (var p in v.Editor.Presenter.GetVisualDescendants().OfType<ThemedTextDiffPresenter>())
{
p.JumpToChangeBlock((int)e.NewValue);
if (p is SingleSideTextDiffPresenter ssp)
ssp.ForceSyncScrollOffset();
}
}
});
}
public TextDiffView()
@ -1553,6 +1649,8 @@ namespace SourceGit.Views
IsUnstagedChange = diff.Option.IsUnstaged;
EnableChunkSelection = diff.Option.WorkingCopyChange != null;
diff.CurrentChangeBlockIdx = -1; // Unset current change block.
}
private void OnStageChunk(object _1, RoutedEventArgs _2)

View file

@ -239,7 +239,6 @@
Margin="8,0,0,0"
HorizontalAlignment="Left"
IsChecked="{Binding UseAmend, Mode=TwoWay}"
IsVisible="{Binding InProgressContext, Converter={x:Static ObjectConverters.IsNull}}"
Content="{DynamicResource Text.WorkingCopy.Amend}"/>
<v:LoadingIcon Grid.Column="5" Width="18" Height="18" IsVisible="{Binding IsCommitting}"/>