mirror of
https://fastgit.cc/github.com/sourcegit-scm/sourcegit
synced 2026-04-21 05:10:25 +08:00
354 lines
13 KiB
C#
354 lines
13 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Threading.Tasks;
|
|
using Avalonia.Threading;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
|
|
namespace SourceGit.ViewModels
|
|
{
|
|
public class DiffContext : ObservableObject
|
|
{
|
|
public string Title
|
|
{
|
|
get;
|
|
}
|
|
|
|
public bool IgnoreWhitespace
|
|
{
|
|
get => Preferences.Instance.IgnoreWhitespaceChangesInDiff;
|
|
set
|
|
{
|
|
if (value != Preferences.Instance.IgnoreWhitespaceChangesInDiff)
|
|
{
|
|
Preferences.Instance.IgnoreWhitespaceChangesInDiff = value;
|
|
OnPropertyChanged();
|
|
LoadContent();
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool ShowEntireFile
|
|
{
|
|
get => Preferences.Instance.UseFullTextDiff;
|
|
set
|
|
{
|
|
if (value != Preferences.Instance.UseFullTextDiff)
|
|
{
|
|
Preferences.Instance.UseFullTextDiff = value;
|
|
OnPropertyChanged();
|
|
|
|
if (Content is TextDiffContext ctx)
|
|
LoadContent();
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool UseSideBySide
|
|
{
|
|
get => Preferences.Instance.UseSideBySideDiff;
|
|
set
|
|
{
|
|
if (value != Preferences.Instance.UseSideBySideDiff)
|
|
{
|
|
Preferences.Instance.UseSideBySideDiff = value;
|
|
OnPropertyChanged();
|
|
|
|
if (Content is TextDiffContext ctx && ctx.IsSideBySide() != value)
|
|
Content = ctx.SwitchMode();
|
|
}
|
|
}
|
|
}
|
|
|
|
public string FileModeChange
|
|
{
|
|
get => _fileModeChange;
|
|
private set => SetProperty(ref _fileModeChange, value);
|
|
}
|
|
|
|
public bool IsTextDiff
|
|
{
|
|
get => _isTextDiff;
|
|
private set => SetProperty(ref _isTextDiff, value);
|
|
}
|
|
|
|
public object Content
|
|
{
|
|
get => _content;
|
|
private set => SetProperty(ref _content, value);
|
|
}
|
|
|
|
public int UnifiedLines
|
|
{
|
|
get => _unifiedLines;
|
|
private set => SetProperty(ref _unifiedLines, value);
|
|
}
|
|
|
|
public DiffContext(string repo, Models.DiffOption option, DiffContext previous = null)
|
|
{
|
|
_repo = repo;
|
|
_option = option;
|
|
|
|
if (previous != null)
|
|
{
|
|
_isTextDiff = previous._isTextDiff;
|
|
_content = previous._content;
|
|
_fileModeChange = previous._fileModeChange;
|
|
_unifiedLines = previous._unifiedLines;
|
|
_info = previous._info;
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(_option.OrgPath) || _option.OrgPath == "/dev/null")
|
|
Title = _option.Path;
|
|
else
|
|
Title = $"{_option.OrgPath} → {_option.Path}";
|
|
|
|
LoadContent();
|
|
}
|
|
|
|
public void IncrUnified()
|
|
{
|
|
UnifiedLines = _unifiedLines + 1;
|
|
LoadContent();
|
|
}
|
|
|
|
public void DecrUnified()
|
|
{
|
|
UnifiedLines = Math.Max(4, _unifiedLines - 1);
|
|
LoadContent();
|
|
}
|
|
|
|
public void OpenExternalMergeTool()
|
|
{
|
|
new Commands.DiffTool(_repo, _option).Open();
|
|
}
|
|
|
|
public void CheckSettings()
|
|
{
|
|
if (Content is TextDiffContext ctx)
|
|
{
|
|
if ((ShowEntireFile && _info.UnifiedLines != _entireFileLine) ||
|
|
(!ShowEntireFile && _info.UnifiedLines == _entireFileLine) ||
|
|
(IgnoreWhitespace != _info.IgnoreWhitespace))
|
|
{
|
|
LoadContent();
|
|
return;
|
|
}
|
|
|
|
if (ctx.IsSideBySide() != UseSideBySide)
|
|
Content = ctx.SwitchMode();
|
|
}
|
|
}
|
|
|
|
private void LoadContent()
|
|
{
|
|
if (_option.Path.EndsWith('/'))
|
|
{
|
|
Content = null;
|
|
IsTextDiff = false;
|
|
return;
|
|
}
|
|
|
|
Task.Run(async () =>
|
|
{
|
|
var numLines = Preferences.Instance.UseFullTextDiff ? _entireFileLine : _unifiedLines;
|
|
var ignoreWhitespace = Preferences.Instance.IgnoreWhitespaceChangesInDiff;
|
|
|
|
var latest = await new Commands.Diff(_repo, _option, numLines, ignoreWhitespace)
|
|
.ReadAsync()
|
|
.ConfigureAwait(false);
|
|
|
|
var info = new Info(_option, numLines, ignoreWhitespace, latest);
|
|
if (_info != null && info.IsSame(_info))
|
|
return;
|
|
|
|
_info = info;
|
|
|
|
object rs = null;
|
|
if (latest.TextDiff != null)
|
|
{
|
|
var count = latest.TextDiff.Lines.Count;
|
|
var isSubmodule = false;
|
|
if (count <= 3)
|
|
{
|
|
var submoduleDiff = new Models.SubmoduleDiff();
|
|
var submoduleRoot = $"{_repo}/{_option.Path}".Replace('\\', '/').TrimEnd('/');
|
|
isSubmodule = true;
|
|
for (int i = 1; i < count; i++)
|
|
{
|
|
var line = latest.TextDiff.Lines[i];
|
|
if (!line.Content.StartsWith("Subproject commit ", StringComparison.Ordinal))
|
|
{
|
|
isSubmodule = false;
|
|
break;
|
|
}
|
|
|
|
var sha = line.Content.Substring(18);
|
|
if (line.Type == Models.TextDiffLineType.Added)
|
|
submoduleDiff.New = await QuerySubmoduleRevisionAsync(submoduleRoot, sha).ConfigureAwait(false);
|
|
else if (line.Type == Models.TextDiffLineType.Deleted)
|
|
submoduleDiff.Old = await QuerySubmoduleRevisionAsync(submoduleRoot, sha).ConfigureAwait(false);
|
|
}
|
|
|
|
if (isSubmodule)
|
|
{
|
|
submoduleDiff.FullPath = submoduleRoot;
|
|
rs = submoduleDiff;
|
|
}
|
|
}
|
|
|
|
if (!isSubmodule)
|
|
rs = latest.TextDiff;
|
|
}
|
|
else if (latest.IsBinary)
|
|
{
|
|
var oldPath = string.IsNullOrEmpty(_option.OrgPath) ? _option.Path : _option.OrgPath;
|
|
var imgDecoder = ImageSource.GetDecoder(_option.Path);
|
|
|
|
if (imgDecoder != Models.ImageDecoder.None)
|
|
{
|
|
var imgDiff = new Models.ImageDiff();
|
|
|
|
if (_option.Revisions.Count == 2)
|
|
{
|
|
var oldImage = await ImageSource.FromRevisionAsync(_repo, _option.Revisions[0], oldPath, imgDecoder).ConfigureAwait(false);
|
|
var newImage = await ImageSource.FromRevisionAsync(_repo, _option.Revisions[1], _option.Path, imgDecoder).ConfigureAwait(false);
|
|
imgDiff.Old = oldImage.Bitmap;
|
|
imgDiff.OldFileSize = oldImage.Size;
|
|
imgDiff.New = newImage.Bitmap;
|
|
imgDiff.NewFileSize = newImage.Size;
|
|
}
|
|
else
|
|
{
|
|
if (!oldPath.Equals("/dev/null", StringComparison.Ordinal))
|
|
{
|
|
var oldImage = await ImageSource.FromRevisionAsync(_repo, "HEAD", oldPath, imgDecoder).ConfigureAwait(false);
|
|
imgDiff.Old = oldImage.Bitmap;
|
|
imgDiff.OldFileSize = oldImage.Size;
|
|
}
|
|
|
|
var fullPath = Path.Combine(_repo, _option.Path);
|
|
if (File.Exists(fullPath))
|
|
{
|
|
var newImage = await ImageSource.FromFileAsync(fullPath, imgDecoder).ConfigureAwait(false);
|
|
imgDiff.New = newImage.Bitmap;
|
|
imgDiff.NewFileSize = newImage.Size;
|
|
}
|
|
}
|
|
|
|
rs = imgDiff;
|
|
}
|
|
else
|
|
{
|
|
var binaryDiff = new Models.BinaryDiff();
|
|
if (_option.Revisions.Count == 2)
|
|
{
|
|
binaryDiff.OldSize = await new Commands.QueryFileSize(_repo, oldPath, _option.Revisions[0]).GetResultAsync().ConfigureAwait(false);
|
|
binaryDiff.NewSize = await new Commands.QueryFileSize(_repo, _option.Path, _option.Revisions[1]).GetResultAsync().ConfigureAwait(false);
|
|
}
|
|
else
|
|
{
|
|
var fullPath = Path.Combine(_repo, _option.Path);
|
|
binaryDiff.OldSize = await new Commands.QueryFileSize(_repo, oldPath, "HEAD").GetResultAsync().ConfigureAwait(false);
|
|
binaryDiff.NewSize = File.Exists(fullPath) ? new FileInfo(fullPath).Length : 0;
|
|
}
|
|
rs = binaryDiff;
|
|
}
|
|
}
|
|
else if (latest.IsLFS)
|
|
{
|
|
var imgDecoder = ImageSource.GetDecoder(_option.Path);
|
|
if (imgDecoder != Models.ImageDecoder.None)
|
|
rs = new LFSImageDiff(_repo, latest.LFSDiff, imgDecoder);
|
|
else
|
|
rs = latest.LFSDiff;
|
|
}
|
|
else
|
|
{
|
|
rs = new Models.NoOrEOLChange();
|
|
}
|
|
|
|
Dispatcher.UIThread.Post(() =>
|
|
{
|
|
FileModeChange = latest.FileModeChange;
|
|
|
|
if (rs is Models.TextDiff cur)
|
|
{
|
|
IsTextDiff = true;
|
|
|
|
if (Preferences.Instance.UseSideBySideDiff)
|
|
Content = new TwoSideTextDiff(_option, cur, _content as TextDiffContext);
|
|
else
|
|
Content = new CombinedTextDiff(_option, cur, _content as TextDiffContext);
|
|
}
|
|
else
|
|
{
|
|
IsTextDiff = false;
|
|
Content = rs;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
private async Task<Models.RevisionSubmodule> QuerySubmoduleRevisionAsync(string repo, string sha)
|
|
{
|
|
if (!File.Exists(Path.Combine(repo, ".git")))
|
|
return new Models.RevisionSubmodule() { Commit = new Models.Commit() { SHA = sha } };
|
|
|
|
var uncommittedChangesCount = 0;
|
|
if (sha.EndsWith("-dirty", StringComparison.Ordinal))
|
|
{
|
|
sha = sha.Substring(0, sha.Length - 6);
|
|
uncommittedChangesCount = await new Commands.CountLocalChanges(repo, true).GetResultAsync().ConfigureAwait(false);
|
|
}
|
|
|
|
var commit = await new Commands.QuerySingleCommit(repo, sha).GetResultAsync().ConfigureAwait(false);
|
|
if (commit == null)
|
|
return new Models.RevisionSubmodule() { Commit = new Models.Commit() { SHA = sha } };
|
|
|
|
var body = await new Commands.QueryCommitFullMessage(repo, sha).GetResultAsync().ConfigureAwait(false);
|
|
return new Models.RevisionSubmodule()
|
|
{
|
|
Commit = commit,
|
|
FullMessage = new Models.CommitFullMessage { Message = body },
|
|
UncommittedChanges = uncommittedChangesCount
|
|
};
|
|
}
|
|
|
|
private class Info
|
|
{
|
|
public string Argument { get; }
|
|
public int UnifiedLines { get; }
|
|
public bool IgnoreWhitespace { get; }
|
|
public string OldHash { get; }
|
|
public string NewHash { get; }
|
|
|
|
public Info(Models.DiffOption option, int unifiedLines, bool ignoreWhitespace, Models.DiffResult result)
|
|
{
|
|
Argument = option.ToString();
|
|
UnifiedLines = unifiedLines;
|
|
IgnoreWhitespace = ignoreWhitespace;
|
|
OldHash = result.OldHash;
|
|
NewHash = result.NewHash;
|
|
}
|
|
|
|
public bool IsSame(Info other)
|
|
{
|
|
return Argument.Equals(other.Argument, StringComparison.Ordinal) &&
|
|
UnifiedLines == other.UnifiedLines &&
|
|
IgnoreWhitespace == other.IgnoreWhitespace &&
|
|
OldHash.Equals(other.OldHash, StringComparison.Ordinal) &&
|
|
NewHash.Equals(other.NewHash, StringComparison.Ordinal);
|
|
}
|
|
}
|
|
|
|
private readonly int _entireFileLine = 999999999;
|
|
private readonly string _repo;
|
|
private readonly Models.DiffOption _option = null;
|
|
private string _fileModeChange = string.Empty;
|
|
private int _unifiedLines = 4;
|
|
private bool _isTextDiff = false;
|
|
private object _content = null;
|
|
private Info _info = null;
|
|
}
|
|
}
|