mirror of
https://github.com/sourcegit-scm/sourcegit.git
synced 2024-12-23 20:47:25 -08:00
feature<TextDiffView>: supports line staging/unstaging in working copy diff view
This commit is contained in:
parent
91ef4e44a4
commit
671e46f8b3
10 changed files with 443 additions and 46 deletions
|
@ -1,11 +1,12 @@
|
||||||
namespace SourceGit.Commands {
|
namespace SourceGit.Commands {
|
||||||
public class Apply : Command {
|
public class Apply : Command {
|
||||||
public Apply(string repo, string file, bool ignoreWhitespace, string whitespaceMode) {
|
public Apply(string repo, string file, bool ignoreWhitespace, string whitespaceMode, string extra) {
|
||||||
WorkingDirectory = repo;
|
WorkingDirectory = repo;
|
||||||
Context = repo;
|
Context = repo;
|
||||||
Args = "apply ";
|
Args = "apply ";
|
||||||
if (ignoreWhitespace) Args += "--ignore-whitespace ";
|
if (ignoreWhitespace) Args += "--ignore-whitespace ";
|
||||||
else Args += $"--whitespace={whitespaceMode} ";
|
else Args += $"--whitespace={whitespaceMode} ";
|
||||||
|
if (!string.IsNullOrEmpty(extra)) Args += $"{extra} ";
|
||||||
Args += $"\"{file}\"";
|
Args += $"\"{file}\"";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,11 +65,11 @@ namespace SourceGit.Commands {
|
||||||
|
|
||||||
_oldLine = int.Parse(match.Groups[1].Value);
|
_oldLine = int.Parse(match.Groups[1].Value);
|
||||||
_newLine = int.Parse(match.Groups[2].Value);
|
_newLine = int.Parse(match.Groups[2].Value);
|
||||||
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, "", ""));
|
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0));
|
||||||
} else {
|
} else {
|
||||||
if (line.Length == 0) {
|
if (line.Length == 0) {
|
||||||
ProcessInlineHighlights();
|
ProcessInlineHighlights();
|
||||||
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", $"{_oldLine}", $"{_newLine}"));
|
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, "", _oldLine, _newLine));
|
||||||
_oldLine++;
|
_oldLine++;
|
||||||
_newLine++;
|
_newLine++;
|
||||||
return;
|
return;
|
||||||
|
@ -77,10 +77,10 @@ namespace SourceGit.Commands {
|
||||||
|
|
||||||
var ch = line[0];
|
var ch = line[0];
|
||||||
if (ch == '-') {
|
if (ch == '-') {
|
||||||
_deleted.Add(new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), $"{_oldLine}", ""));
|
_deleted.Add(new Models.TextDiffLine(Models.TextDiffLineType.Deleted, line.Substring(1), _oldLine, 0));
|
||||||
_oldLine++;
|
_oldLine++;
|
||||||
} else if (ch == '+') {
|
} else if (ch == '+') {
|
||||||
_added.Add(new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), "", $"{_newLine}"));
|
_added.Add(new Models.TextDiffLine(Models.TextDiffLineType.Added, line.Substring(1), 0, _newLine));
|
||||||
_newLine++;
|
_newLine++;
|
||||||
} else if (ch != '\\') {
|
} else if (ch != '\\') {
|
||||||
ProcessInlineHighlights();
|
ProcessInlineHighlights();
|
||||||
|
@ -88,7 +88,7 @@ namespace SourceGit.Commands {
|
||||||
if (match.Success) {
|
if (match.Success) {
|
||||||
_oldLine = int.Parse(match.Groups[1].Value);
|
_oldLine = int.Parse(match.Groups[1].Value);
|
||||||
_newLine = int.Parse(match.Groups[2].Value);
|
_newLine = int.Parse(match.Groups[2].Value);
|
||||||
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, "", ""));
|
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Indicator, line, 0, 0));
|
||||||
} else {
|
} else {
|
||||||
if (line.StartsWith(PREFIX_LFS)) {
|
if (line.StartsWith(PREFIX_LFS)) {
|
||||||
_result.IsLFS = true;
|
_result.IsLFS = true;
|
||||||
|
@ -96,7 +96,7 @@ namespace SourceGit.Commands {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, line.Substring(1), $"{_oldLine}", $"{_newLine}"));
|
_result.TextDiff.Lines.Add(new Models.TextDiffLine(Models.TextDiffLineType.Normal, line.Substring(1), _oldLine, _newLine));
|
||||||
_oldLine++;
|
_oldLine++;
|
||||||
_newLine++;
|
_newLine++;
|
||||||
}
|
}
|
||||||
|
|
23
src/Commands/QueryStagedFileBlobGuid.cs
Normal file
23
src/Commands/QueryStagedFileBlobGuid.cs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace SourceGit.Commands {
|
||||||
|
public class QueryStagedFileBlobGuid : Command {
|
||||||
|
private static readonly Regex REG_FORMAT = new Regex(@"^\d+\s+([0-9a-f]+)\s+.*$");
|
||||||
|
|
||||||
|
public QueryStagedFileBlobGuid(string repo, string file) {
|
||||||
|
WorkingDirectory = repo;
|
||||||
|
Context = repo;
|
||||||
|
Args = $"ls-files -s -- \"{file}\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Result() {
|
||||||
|
var rs = ReadToEnd();
|
||||||
|
var match = REG_FORMAT.Match(rs.StdOut.Trim());
|
||||||
|
if (match.Success) {
|
||||||
|
return match.Groups[1].Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,8 @@ using System.Text;
|
||||||
|
|
||||||
namespace SourceGit.Models {
|
namespace SourceGit.Models {
|
||||||
public class DiffOption {
|
public class DiffOption {
|
||||||
|
public Change WorkingCopyChange => _workingCopyChange;
|
||||||
|
public bool IsUnstaged => _isUnstaged;
|
||||||
public List<string> Revisions => _revisions;
|
public List<string> Revisions => _revisions;
|
||||||
public string Path => _path;
|
public string Path => _path;
|
||||||
public string OrgPath => _orgPath;
|
public string OrgPath => _orgPath;
|
||||||
|
@ -13,6 +15,9 @@ namespace SourceGit.Models {
|
||||||
/// <param name="change"></param>
|
/// <param name="change"></param>
|
||||||
/// <param name="isUnstaged"></param>
|
/// <param name="isUnstaged"></param>
|
||||||
public DiffOption(Change change, bool isUnstaged) {
|
public DiffOption(Change change, bool isUnstaged) {
|
||||||
|
_workingCopyChange = change;
|
||||||
|
_isUnstaged = isUnstaged;
|
||||||
|
|
||||||
if (isUnstaged) {
|
if (isUnstaged) {
|
||||||
switch (change.WorkTree) {
|
switch (change.WorkTree) {
|
||||||
case ChangeState.Added:
|
case ChangeState.Added:
|
||||||
|
@ -47,7 +52,7 @@ namespace SourceGit.Models {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Diff with filepath.
|
/// Diff with filepath. Used by FileHistories
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="commit"></param>
|
/// <param name="commit"></param>
|
||||||
/// <param name="file"></param>
|
/// <param name="file"></param>
|
||||||
|
@ -87,6 +92,8 @@ namespace SourceGit.Models {
|
||||||
return builder.ToString();
|
return builder.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Change _workingCopyChange = null;
|
||||||
|
private bool _isUnstaged = false;
|
||||||
private string _orgPath = string.Empty;
|
private string _orgPath = string.Empty;
|
||||||
private string _path = string.Empty;
|
private string _path = string.Empty;
|
||||||
private string _extra = string.Empty;
|
private string _extra = string.Empty;
|
||||||
|
|
|
@ -18,16 +18,19 @@ namespace SourceGit.Models {
|
||||||
public class TextDiffLine {
|
public class TextDiffLine {
|
||||||
public TextDiffLineType Type { get; set; } = TextDiffLineType.None;
|
public TextDiffLineType Type { get; set; } = TextDiffLineType.None;
|
||||||
public string Content { get; set; } = "";
|
public string Content { get; set; } = "";
|
||||||
public string OldLine { get; set; } = "";
|
public int OldLineNumber { get; set; } = 0;
|
||||||
public string NewLine { get; set; } = "";
|
public int NewLineNumber { get; set; } = 0;
|
||||||
public List<TextInlineRange> Highlights { get; set; } = new List<TextInlineRange>();
|
public List<TextInlineRange> Highlights { get; set; } = new List<TextInlineRange>();
|
||||||
|
|
||||||
|
public string OldLine => OldLineNumber == 0 ? string.Empty : OldLineNumber.ToString();
|
||||||
|
public string NewLine => NewLineNumber == 0 ? string.Empty : NewLineNumber.ToString();
|
||||||
|
|
||||||
public TextDiffLine() { }
|
public TextDiffLine() { }
|
||||||
public TextDiffLine(TextDiffLineType type, string content, string oldLine, string newLine) {
|
public TextDiffLine(TextDiffLineType type, string content, int oldLine, int newLine) {
|
||||||
Type = type;
|
Type = type;
|
||||||
Content = content;
|
Content = content;
|
||||||
OldLine = oldLine;
|
OldLineNumber = oldLine;
|
||||||
NewLine = newLine;
|
NewLineNumber = newLine;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -228,6 +228,9 @@
|
||||||
<sys:String x:Key="Text.FileCM.StashMulti">Stash {0} files...</sys:String>
|
<sys:String x:Key="Text.FileCM.StashMulti">Stash {0} files...</sys:String>
|
||||||
<sys:String x:Key="Text.FileCM.SaveAsPatch">Save As Patch...</sys:String>
|
<sys:String x:Key="Text.FileCM.SaveAsPatch">Save As Patch...</sys:String>
|
||||||
<sys:String x:Key="Text.FileCM.AssumeUnchanged">Assume unchaged</sys:String>
|
<sys:String x:Key="Text.FileCM.AssumeUnchanged">Assume unchaged</sys:String>
|
||||||
|
<sys:String x:Key="Text.FileCM.StageSelectedLines">Stage Changes in Selected Line(s)</sys:String>
|
||||||
|
<sys:String x:Key="Text.FileCM.DiscardSelectedLines">Discard Changes in Selected Line(s)</sys:String>
|
||||||
|
<sys:String x:Key="Text.FileCM.UnstageSelectedLines">Unstage Changes in Selected Line(s)</sys:String>
|
||||||
|
|
||||||
<sys:String x:Key="Text.DeleteBranch">Delete Branch</sys:String>
|
<sys:String x:Key="Text.DeleteBranch">Delete Branch</sys:String>
|
||||||
<sys:String x:Key="Text.DeleteBranch.Branch">Branch :</sys:String>
|
<sys:String x:Key="Text.DeleteBranch.Branch">Branch :</sys:String>
|
||||||
|
|
|
@ -227,6 +227,9 @@
|
||||||
<sys:String x:Key="Text.FileCM.StashMulti">贮藏选中的 {0} 个文件...</sys:String>
|
<sys:String x:Key="Text.FileCM.StashMulti">贮藏选中的 {0} 个文件...</sys:String>
|
||||||
<sys:String x:Key="Text.FileCM.SaveAsPatch">另存为补丁...</sys:String>
|
<sys:String x:Key="Text.FileCM.SaveAsPatch">另存为补丁...</sys:String>
|
||||||
<sys:String x:Key="Text.FileCM.AssumeUnchanged">不跟踪此文件的更改</sys:String>
|
<sys:String x:Key="Text.FileCM.AssumeUnchanged">不跟踪此文件的更改</sys:String>
|
||||||
|
<sys:String x:Key="Text.FileCM.StageSelectedLines">暂存选中的更改</sys:String>
|
||||||
|
<sys:String x:Key="Text.FileCM.DiscardSelectedLines">放弃选中的更改</sys:String>
|
||||||
|
<sys:String x:Key="Text.FileCM.UnstageSelectedLines">从暂存中移除选中的更改</sys:String>
|
||||||
|
|
||||||
<sys:String x:Key="Text.DeleteBranch">确定要删除此分支吗?</sys:String>
|
<sys:String x:Key="Text.DeleteBranch">确定要删除此分支吗?</sys:String>
|
||||||
<sys:String x:Key="Text.DeleteBranch.Branch">分支名 :</sys:String>
|
<sys:String x:Key="Text.DeleteBranch.Branch">分支名 :</sys:String>
|
||||||
|
|
|
@ -54,7 +54,7 @@ namespace SourceGit.ViewModels {
|
||||||
ProgressDescription = "Apply patch...";
|
ProgressDescription = "Apply patch...";
|
||||||
|
|
||||||
return Task.Run(() => {
|
return Task.Run(() => {
|
||||||
var succ = new Commands.Apply(_repo.FullPath, _patchFile, _ignoreWhiteSpace, SelectedWhiteSpaceMode.Arg).Exec();
|
var succ = new Commands.Apply(_repo.FullPath, _patchFile, _ignoreWhiteSpace, SelectedWhiteSpaceMode.Arg, null).Exec();
|
||||||
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
CallUIThread(() => _repo.SetWatcherEnabled(true));
|
||||||
return succ;
|
return succ;
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,6 +5,18 @@ using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace SourceGit.ViewModels {
|
namespace SourceGit.ViewModels {
|
||||||
public class DiffContext : ObservableObject {
|
public class DiffContext : ObservableObject {
|
||||||
|
public string RepositoryPath {
|
||||||
|
get => _repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Models.Change WorkingCopyChange {
|
||||||
|
get => _option.WorkingCopyChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsUnstaged {
|
||||||
|
get => _option.IsUnstaged;
|
||||||
|
}
|
||||||
|
|
||||||
public string FilePath {
|
public string FilePath {
|
||||||
get => _option.Path;
|
get => _option.Path;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ using Avalonia.Controls.Primitives;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
|
using Avalonia.VisualTree;
|
||||||
using AvaloniaEdit;
|
using AvaloniaEdit;
|
||||||
using AvaloniaEdit.Document;
|
using AvaloniaEdit.Document;
|
||||||
using AvaloniaEdit.Editing;
|
using AvaloniaEdit.Editing;
|
||||||
|
@ -11,12 +12,27 @@ using AvaloniaEdit.Rendering;
|
||||||
using AvaloniaEdit.TextMate;
|
using AvaloniaEdit.TextMate;
|
||||||
using AvaloniaEdit.Utils;
|
using AvaloniaEdit.Utils;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using TextMateSharp.Grammars;
|
using TextMateSharp.Grammars;
|
||||||
|
|
||||||
namespace SourceGit.Views {
|
namespace SourceGit.Views {
|
||||||
|
public class TextDiffUnifiedSelection {
|
||||||
|
public int StartLine { get; set; } = 0;
|
||||||
|
public int EndLine { get; set; } = 0;
|
||||||
|
public bool HasChanges { get; set; } = false;
|
||||||
|
public bool HasLeftChanges { get; set; } = false;
|
||||||
|
public int IgnoredAdds { get; set; } = 0;
|
||||||
|
public int IgnoredDeletes { get; set; } = 0;
|
||||||
|
|
||||||
|
public bool IsInRange(int idx) {
|
||||||
|
return idx >= StartLine - 1 && idx < EndLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class CombinedTextDiffPresenter : TextEditor {
|
public class CombinedTextDiffPresenter : TextEditor {
|
||||||
public class LineNumberMargin : AbstractMargin {
|
public class LineNumberMargin : AbstractMargin {
|
||||||
public LineNumberMargin(CombinedTextDiffPresenter editor, bool isOldLine) {
|
public LineNumberMargin(CombinedTextDiffPresenter editor, bool isOldLine) {
|
||||||
|
@ -231,24 +247,23 @@ namespace SourceGit.Views {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e) {
|
private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e) {
|
||||||
var selected = SelectedText;
|
var selection = TextArea.Selection;
|
||||||
if (string.IsNullOrEmpty(selected)) return;
|
if (selection.IsEmpty) return;
|
||||||
|
|
||||||
var icon = new Avalonia.Controls.Shapes.Path();
|
var menu = new ContextMenu();
|
||||||
icon.Width = 10;
|
var parentView = this.FindAncestorOfType<TextDiffView>();
|
||||||
icon.Height = 10;
|
if (parentView != null) {
|
||||||
icon.Stretch = Stretch.Uniform;
|
parentView.FillContextMenuForWorkingCopyChange(menu, selection.StartPosition.Line, selection.EndPosition.Line, false);
|
||||||
icon.Data = App.Current?.FindResource("Icons.Copy") as StreamGeometry;
|
}
|
||||||
|
|
||||||
var copy = new MenuItem();
|
var copy = new MenuItem();
|
||||||
copy.Header = App.Text("Copy");
|
copy.Header = App.Text("Copy");
|
||||||
copy.Icon = icon;
|
copy.Icon = App.CreateMenuIcon("Icons.Copy");
|
||||||
copy.Click += (o, ev) => {
|
copy.Click += (o, ev) => {
|
||||||
App.CopyText(selected);
|
App.CopyText(SelectedText);
|
||||||
ev.Handled = true;
|
ev.Handled = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
var menu = new ContextMenu();
|
|
||||||
menu.Items.Add(copy);
|
menu.Items.Add(copy);
|
||||||
menu.Open(TextArea.TextView);
|
menu.Open(TextArea.TextView);
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
|
@ -265,7 +280,7 @@ namespace SourceGit.Views {
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateGrammar();
|
UpdateGrammar();
|
||||||
Text = builder.ToString();
|
Text = builder.ToString();
|
||||||
} else {
|
} else {
|
||||||
Text = string.Empty;
|
Text = string.Empty;
|
||||||
}
|
}
|
||||||
|
@ -484,7 +499,7 @@ namespace SourceGit.Views {
|
||||||
public SingleSideTextDiffPresenter() : base(new TextArea(), new TextDocument()) {
|
public SingleSideTextDiffPresenter() : base(new TextArea(), new TextDocument()) {
|
||||||
IsReadOnly = true;
|
IsReadOnly = true;
|
||||||
ShowLineNumbers = false;
|
ShowLineNumbers = false;
|
||||||
WordWrap = false;
|
WordWrap = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnLoaded(RoutedEventArgs e) {
|
protected override void OnLoaded(RoutedEventArgs e) {
|
||||||
|
@ -527,24 +542,23 @@ namespace SourceGit.Views {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e) {
|
private void OnTextViewContextRequested(object sender, ContextRequestedEventArgs e) {
|
||||||
var selected = SelectedText;
|
var selection = TextArea.Selection;
|
||||||
if (string.IsNullOrEmpty(selected)) return;
|
if (selection.IsEmpty) return;
|
||||||
|
|
||||||
var icon = new Avalonia.Controls.Shapes.Path();
|
var menu = new ContextMenu();
|
||||||
icon.Width = 10;
|
var parentView = this.FindAncestorOfType<TextDiffView>();
|
||||||
icon.Height = 10;
|
if (parentView != null) {
|
||||||
icon.Stretch = Stretch.Uniform;
|
parentView.FillContextMenuForWorkingCopyChange(menu, selection.StartPosition.Line, selection.EndPosition.Line, IsOld);
|
||||||
icon.Data = App.Current?.FindResource("Icons.Copy") as StreamGeometry;
|
}
|
||||||
|
|
||||||
var copy = new MenuItem();
|
var copy = new MenuItem();
|
||||||
copy.Header = App.Text("Copy");
|
copy.Header = App.Text("Copy");
|
||||||
copy.Icon = icon;
|
copy.Icon = App.CreateMenuIcon("Icons.Copy");
|
||||||
copy.Click += (o, ev) => {
|
copy.Click += (o, ev) => {
|
||||||
App.CopyText(selected);
|
App.CopyText(SelectedText);
|
||||||
ev.Handled = true;
|
ev.Handled = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
var menu = new ContextMenu();
|
|
||||||
menu.Items.Add(copy);
|
menu.Items.Add(copy);
|
||||||
menu.Open(TextArea.TextView);
|
menu.Open(TextArea.TextView);
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
|
@ -617,18 +631,150 @@ namespace SourceGit.Views {
|
||||||
set => SetValue(UseCombinedProperty, value);
|
set => SetValue(UseCombinedProperty, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readonly StyledProperty<Vector> SyncScrollOffsetProperty =
|
|
||||||
AvaloniaProperty.Register<TextDiffView, Vector>(nameof(SyncScrollOffset), Vector.Zero);
|
|
||||||
|
|
||||||
public Vector SyncScrollOffset {
|
|
||||||
get => GetValue(SyncScrollOffsetProperty);
|
|
||||||
set => SetValue(SyncScrollOffsetProperty, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public TextDiffView() {
|
public TextDiffView() {
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void FillContextMenuForWorkingCopyChange(ContextMenu menu, int startLine, int endLine, bool isOldSide) {
|
||||||
|
var parentView = this.FindAncestorOfType<DiffView>();
|
||||||
|
if (parentView == null) return;
|
||||||
|
|
||||||
|
var ctx = parentView.DataContext as ViewModels.DiffContext;
|
||||||
|
if (ctx == null) return;
|
||||||
|
|
||||||
|
var change = ctx.WorkingCopyChange;
|
||||||
|
if (change == null) return;
|
||||||
|
|
||||||
|
if (startLine > endLine) {
|
||||||
|
var tmp = startLine;
|
||||||
|
startLine = endLine;
|
||||||
|
endLine = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
var selection = GetUnifiedSelection(startLine, endLine, isOldSide);
|
||||||
|
if (!selection.HasChanges) return;
|
||||||
|
|
||||||
|
// If all changes has been selected the use method provided by ViewModels.WorkingCopy.
|
||||||
|
// Otherwise, use `git apply`
|
||||||
|
if (!selection.HasLeftChanges) {
|
||||||
|
var workcopyView = this.FindAncestorOfType<WorkingCopy>();
|
||||||
|
if (workcopyView == null) return;
|
||||||
|
|
||||||
|
if (ctx.IsUnstaged) {
|
||||||
|
var stage = new MenuItem();
|
||||||
|
stage.Header = App.Text("FileCM.StageSelectedLines");
|
||||||
|
stage.Icon = App.CreateMenuIcon("Icons.File.Add");
|
||||||
|
stage.Click += (_, e) => {
|
||||||
|
var workcopy = workcopyView.DataContext as ViewModels.WorkingCopy;
|
||||||
|
workcopy.StageChanges(new List<Models.Change> { change });
|
||||||
|
e.Handled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
var discard = new MenuItem();
|
||||||
|
discard.Header = App.Text("FileCM.DiscardSelectedLines");
|
||||||
|
discard.Icon = App.CreateMenuIcon("Icons.Undo");
|
||||||
|
discard.Click += (_, e) => {
|
||||||
|
var workcopy = workcopyView.DataContext as ViewModels.WorkingCopy;
|
||||||
|
workcopy.Discard(new List<Models.Change> { change });
|
||||||
|
e.Handled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
menu.Items.Add(stage);
|
||||||
|
menu.Items.Add(discard);
|
||||||
|
} else {
|
||||||
|
var unstage = new MenuItem();
|
||||||
|
unstage.Header = App.Text("FileCM.UnstageSelectedLines");
|
||||||
|
unstage.Icon = App.CreateMenuIcon("Icons.File.Remove");
|
||||||
|
unstage.Click += (_, e) => {
|
||||||
|
var workcopy = workcopyView.DataContext as ViewModels.WorkingCopy;
|
||||||
|
workcopy.UnstageChanges(new List<Models.Change> { change });
|
||||||
|
e.Handled = true;
|
||||||
|
};
|
||||||
|
menu.Items.Add(unstage);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var repoView = this.FindAncestorOfType<Repository>();
|
||||||
|
if (repoView == null) return;
|
||||||
|
|
||||||
|
if (ctx.IsUnstaged) {
|
||||||
|
var stage = new MenuItem();
|
||||||
|
stage.Header = App.Text("FileCM.StageSelectedLines");
|
||||||
|
stage.Icon = App.CreateMenuIcon("Icons.File.Add");
|
||||||
|
stage.Click += (_, e) => {
|
||||||
|
var repo = repoView.DataContext as ViewModels.Repository;
|
||||||
|
repo.SetWatcherEnabled(false);
|
||||||
|
|
||||||
|
var tmpFile = Path.GetTempFileName();
|
||||||
|
if (change.WorkTree == Models.ChangeState.Untracked) {
|
||||||
|
GenerateNewPatchFromSelection(change, null, selection, false, tmpFile);
|
||||||
|
} else if (UseCombined) {
|
||||||
|
var treeGuid = new Commands.QueryStagedFileBlobGuid(ctx.RepositoryPath, change.Path).Result();
|
||||||
|
GenerateCombinedPatchFromSelection(change, treeGuid, selection, false, tmpFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
new Commands.Apply(ctx.RepositoryPath, tmpFile, true, "nowarn", "--cache --index").Exec();
|
||||||
|
File.Delete(tmpFile);
|
||||||
|
|
||||||
|
repo.RefreshWorkingCopyChanges();
|
||||||
|
repo.SetWatcherEnabled(true);
|
||||||
|
e.Handled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
var discard = new MenuItem();
|
||||||
|
discard.Header = App.Text("FileCM.DiscardSelectedLines");
|
||||||
|
discard.Icon = App.CreateMenuIcon("Icons.Undo");
|
||||||
|
discard.Click += (_, e) => {
|
||||||
|
var repo = repoView.DataContext as ViewModels.Repository;
|
||||||
|
repo.SetWatcherEnabled(false);
|
||||||
|
|
||||||
|
var tmpFile = Path.GetTempFileName();
|
||||||
|
if (change.WorkTree == Models.ChangeState.Untracked) {
|
||||||
|
GenerateNewPatchFromSelection(change, null, selection, true, tmpFile);
|
||||||
|
} else if (UseCombined) {
|
||||||
|
var treeGuid = new Commands.QueryStagedFileBlobGuid(ctx.RepositoryPath, change.Path).Result();
|
||||||
|
GenerateCombinedPatchFromSelection(change, treeGuid, selection, true, tmpFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
new Commands.Apply(ctx.RepositoryPath, tmpFile, true, "nowarn", "--reverse").Exec();
|
||||||
|
File.Delete(tmpFile);
|
||||||
|
|
||||||
|
repo.RefreshWorkingCopyChanges();
|
||||||
|
repo.SetWatcherEnabled(true);
|
||||||
|
e.Handled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
menu.Items.Add(stage);
|
||||||
|
menu.Items.Add(discard);
|
||||||
|
} else {
|
||||||
|
var unstage = new MenuItem();
|
||||||
|
unstage.Header = App.Text("FileCM.UnstageSelectedLines");
|
||||||
|
unstage.Icon = App.CreateMenuIcon("Icons.File.Remove");
|
||||||
|
unstage.Click += (_, e) => {
|
||||||
|
var repo = repoView.DataContext as ViewModels.Repository;
|
||||||
|
repo.SetWatcherEnabled(false);
|
||||||
|
|
||||||
|
var treeGuid = new Commands.QueryStagedFileBlobGuid(ctx.RepositoryPath, change.Path).Result();
|
||||||
|
var tmpFile = Path.GetTempFileName();
|
||||||
|
if (change.Index == Models.ChangeState.Added) {
|
||||||
|
GenerateNewPatchFromSelection(change, treeGuid, selection, true, tmpFile);
|
||||||
|
} else if (UseCombined) {
|
||||||
|
GenerateCombinedPatchFromSelection(change, treeGuid, selection, true, tmpFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
new Commands.Apply(ctx.RepositoryPath, tmpFile, true, "nowarn", "--cache --index --reverse").Exec();
|
||||||
|
File.Delete(tmpFile);
|
||||||
|
|
||||||
|
repo.RefreshWorkingCopyChanges();
|
||||||
|
repo.SetWatcherEnabled(true);
|
||||||
|
e.Handled = true;
|
||||||
|
};
|
||||||
|
menu.Items.Add(unstage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.Items.Add(new MenuItem() { Header = "-" });
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) {
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) {
|
||||||
base.OnPropertyChanged(change);
|
base.OnPropertyChanged(change);
|
||||||
|
|
||||||
|
@ -636,11 +782,210 @@ namespace SourceGit.Views {
|
||||||
if (TextDiff == null) {
|
if (TextDiff == null) {
|
||||||
Content = null;
|
Content = null;
|
||||||
} else if (UseCombined) {
|
} else if (UseCombined) {
|
||||||
Content = new ViewModels.TwoSideTextDiff(TextDiff);
|
|
||||||
} else {
|
|
||||||
Content = TextDiff;
|
Content = TextDiff;
|
||||||
|
} else {
|
||||||
|
Content = new ViewModels.TwoSideTextDiff(TextDiff);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TextDiffUnifiedSelection GetUnifiedSelection(int startLine, int endLine, bool isOldSide) {
|
||||||
|
var rs = new TextDiffUnifiedSelection();
|
||||||
|
if (Content is Models.TextDiff combined) {
|
||||||
|
rs.StartLine = startLine;
|
||||||
|
rs.EndLine = endLine;
|
||||||
|
|
||||||
|
for (int i = 0; i < startLine - 1; i++) {
|
||||||
|
var line = combined.Lines[i];
|
||||||
|
if (line.Type == Models.TextDiffLineType.Added) {
|
||||||
|
rs.HasLeftChanges = true;
|
||||||
|
rs.IgnoredAdds++;
|
||||||
|
} else if (line.Type == Models.TextDiffLineType.Deleted) {
|
||||||
|
rs.HasLeftChanges = true;
|
||||||
|
rs.IgnoredDeletes++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = startLine - 1; i < endLine; i++) {
|
||||||
|
var line = combined.Lines[i];
|
||||||
|
if (line.Type == Models.TextDiffLineType.Added || line.Type == Models.TextDiffLineType.Deleted) {
|
||||||
|
rs.HasChanges = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rs.HasLeftChanges) {
|
||||||
|
for (int i = endLine; i < combined.Lines.Count; i++) {
|
||||||
|
var line = combined.Lines[i];
|
||||||
|
if (line.Type == Models.TextDiffLineType.Added || line.Type == Models.TextDiffLineType.Deleted) {
|
||||||
|
rs.HasLeftChanges = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (Content is ViewModels.TwoSideTextDiff twoSides) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return rs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GenerateNewPatchFromSelection(Models.Change change, string fileBlobGuid, TextDiffUnifiedSelection selection, bool revert, string output) {
|
||||||
|
var isTracked = !string.IsNullOrEmpty(fileBlobGuid);
|
||||||
|
var fileGuid = isTracked ? fileBlobGuid.Substring(0, 8) : "00000000";
|
||||||
|
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.Append("diff --git a/").Append(change.Path).Append(" b/").Append(change.Path).Append('\n');
|
||||||
|
if (!revert && !isTracked) builder.Append("new file mode 100644\n");
|
||||||
|
builder.Append("index 00000000...").Append(fileGuid).Append('\n');
|
||||||
|
builder.Append("--- ").Append((revert || isTracked) ? $"a/{change.Path}\n" : "/dev/null\n");
|
||||||
|
builder.Append("+++ b/").Append(change.Path).Append('\n');
|
||||||
|
|
||||||
|
var additions = selection.EndLine - selection.StartLine;
|
||||||
|
if (selection.StartLine != 1) additions++;
|
||||||
|
|
||||||
|
if (revert) {
|
||||||
|
var totalLines = TextDiff.Lines.Count - 1;
|
||||||
|
builder.Append($"@@ -0,").Append(totalLines - additions).Append(" +0,").Append(totalLines).Append(" @@");
|
||||||
|
for (int i = 1; i <= totalLines; i++) {
|
||||||
|
var line = TextDiff.Lines[i];
|
||||||
|
if (line.Type != Models.TextDiffLineType.Added) continue;
|
||||||
|
builder.Append(selection.IsInRange(i) ? "\n+" : "\n ").Append(line.Content);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
builder.Append("@@ -0,0 +0,").Append(additions).Append(" @@");
|
||||||
|
for (int i = selection.StartLine - 1; i < selection.EndLine; i++) {
|
||||||
|
var line = TextDiff.Lines[i];
|
||||||
|
if (line.Type != Models.TextDiffLineType.Added) continue;
|
||||||
|
builder.Append("\n+").Append(line.Content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append("\n\\ No newline at end of file\n");
|
||||||
|
File.WriteAllText(output, builder.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GenerateCombinedPatchFromSelection(Models.Change change, string fileTreeGuid, TextDiffUnifiedSelection selection, bool revert, string output) {
|
||||||
|
var orgFile = !string.IsNullOrEmpty(change.OriginalPath) ? change.OriginalPath : change.Path;
|
||||||
|
var indicatorRegex = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@");
|
||||||
|
var diff = TextDiff;
|
||||||
|
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.Append("diff --git a/").Append(change.Path).Append(" b/").Append(change.Path).Append('\n');
|
||||||
|
builder.Append("index 00000000...").Append(fileTreeGuid).Append(" 100644\n");
|
||||||
|
builder.Append("--- a/").Append(orgFile).Append('\n');
|
||||||
|
builder.Append("+++ b/").Append(change.Path).Append('\n');
|
||||||
|
|
||||||
|
// If last line of selection is a change. Find one more line.
|
||||||
|
var tail = null as string;
|
||||||
|
if (selection.EndLine < diff.Lines.Count) {
|
||||||
|
var lastLine = diff.Lines[selection.EndLine - 1];
|
||||||
|
if (lastLine.Type == Models.TextDiffLineType.Added || lastLine.Type == Models.TextDiffLineType.Deleted) {
|
||||||
|
for (int i = selection.EndLine; i < diff.Lines.Count; i++) {
|
||||||
|
var line = diff.Lines[i];
|
||||||
|
if (line.Type == Models.TextDiffLineType.Indicator) break;
|
||||||
|
if (line.Type == Models.TextDiffLineType.Normal || line.Type == Models.TextDiffLineType.Deleted) {
|
||||||
|
tail = line.Content;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the first line is not indicator.
|
||||||
|
if (diff.Lines[selection.StartLine - 1].Type != Models.TextDiffLineType.Indicator) {
|
||||||
|
var indicator = selection.StartLine - 1;
|
||||||
|
for (int i = selection.StartLine - 2; i >= 0; i--) {
|
||||||
|
var line = diff.Lines[i];
|
||||||
|
if (line.Type == Models.TextDiffLineType.Indicator) {
|
||||||
|
indicator = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ignoreAdds = 0;
|
||||||
|
var ignoreRemoves = 0;
|
||||||
|
for (int i = 0; i < indicator; i++) {
|
||||||
|
var line = diff.Lines[i];
|
||||||
|
if (line.Type == Models.TextDiffLineType.Added) {
|
||||||
|
ignoreAdds++;
|
||||||
|
} else if (line.Type == Models.TextDiffLineType.Deleted) {
|
||||||
|
ignoreRemoves++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = indicator; i < selection.StartLine - 1; i++) {
|
||||||
|
var line = diff.Lines[i];
|
||||||
|
if (line.Type == Models.TextDiffLineType.Indicator) {
|
||||||
|
ProcessIndicatorForPatch(builder, line, i, selection.StartLine, selection.EndLine, ignoreRemoves, ignoreAdds, tail != null);
|
||||||
|
} else if (line.Type == Models.TextDiffLineType.Added) {
|
||||||
|
// Ignores
|
||||||
|
} else if (line.Type == Models.TextDiffLineType.Deleted || line.Type == Models.TextDiffLineType.Normal) {
|
||||||
|
// Traits ignored deleted as normal.
|
||||||
|
builder.Append("\n ").Append(line.Content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outputs the selected lines.
|
||||||
|
for (int i = selection.StartLine - 1; i < selection.EndLine; i++) {
|
||||||
|
var line = diff.Lines[i];
|
||||||
|
if (line.Type == Models.TextDiffLineType.Indicator) {
|
||||||
|
if (!ProcessIndicatorForPatch(builder, line, i, selection.StartLine, selection.EndLine, selection.IgnoredDeletes, selection.IgnoredAdds, tail != null)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (line.Type == Models.TextDiffLineType.Normal) {
|
||||||
|
builder.Append("\n ").Append(line.Content);
|
||||||
|
} else if (line.Type == Models.TextDiffLineType.Added) {
|
||||||
|
builder.Append("\n+").Append(line.Content);
|
||||||
|
} else if (line.Type == Models.TextDiffLineType.Deleted) {
|
||||||
|
builder.Append("\n-").Append(line.Content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append("\n ").Append(tail);
|
||||||
|
builder.Append("\n");
|
||||||
|
File.WriteAllText(output, builder.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool ProcessIndicatorForPatch(StringBuilder builder, Models.TextDiffLine indicator, int idx, int start, int end, int ignoreRemoves, int ignoreAdds, bool tailed) {
|
||||||
|
var indicatorRegex = new Regex(@"^@@ \-(\d+),?\d* \+(\d+),?\d* @@");
|
||||||
|
var diff = TextDiff;
|
||||||
|
|
||||||
|
var match = indicatorRegex.Match(indicator.Content);
|
||||||
|
var oldStart = int.Parse(match.Groups[1].Value);
|
||||||
|
var newStart = int.Parse(match.Groups[2].Value) + ignoreRemoves - ignoreAdds;
|
||||||
|
var oldCount = 0;
|
||||||
|
var newCount = 0;
|
||||||
|
for (int i = idx + 1; i < end; i++) {
|
||||||
|
var test = diff.Lines[i];
|
||||||
|
if (test.Type == Models.TextDiffLineType.Indicator) break;
|
||||||
|
|
||||||
|
if (test.Type == Models.TextDiffLineType.Normal) {
|
||||||
|
oldCount++;
|
||||||
|
newCount++;
|
||||||
|
} else if (test.Type == Models.TextDiffLineType.Added) {
|
||||||
|
if (i >= start - 1) newCount++;
|
||||||
|
|
||||||
|
if (i == end - 1 && tailed) {
|
||||||
|
newCount++;
|
||||||
|
oldCount++;
|
||||||
|
}
|
||||||
|
} else if (test.Type == Models.TextDiffLineType.Deleted) {
|
||||||
|
if (i < start - 1) newCount++;
|
||||||
|
oldCount++;
|
||||||
|
|
||||||
|
if (i == end - 1 && tailed) {
|
||||||
|
newCount++;
|
||||||
|
oldCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldCount == 0 && newCount == 0) return false;
|
||||||
|
|
||||||
|
builder.Append($"@@ -{oldStart},{oldCount} +{newStart},{newCount} @@");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue