feature: Allow show uncommitted changes in commits history like TortoiseHG Workspace

This commit is contained in:
Giuseppe Lippolis 2024-07-26 11:28:40 +02:00
parent 3c5a661fa0
commit ba8c6382e7
15 changed files with 166 additions and 65 deletions

View file

@ -35,6 +35,7 @@ namespace SourceGit.Models
public bool IsCommitterVisible => !Author.Equals(Committer) || AuthorTime != CommitterTime;
public bool IsCurrentHead => Decorators.Find(x => x.Type is DecoratorType.CurrentBranchHead or DecoratorType.CurrentCommitHead) != null;
public bool IsWorkCopy => string.IsNullOrWhiteSpace(SHA);
public double Opacity => IsMerged ? 1 : OpacityForNotMerged;
public FontWeight FontWeight => IsCurrentHead ? FontWeight.Bold : FontWeight.Regular;

View file

@ -101,6 +101,7 @@ namespace SourceGit.Models
{
public Point Center;
public int Color;
public bool IsWorkCopy;
}
public List<Path> Paths { get; set; } = new List<Path>();
@ -156,42 +157,45 @@ namespace SourceGit.Models
// Find first curves that links to this commit and marks others that links to this commit ended.
double offsetX = -HALF_WIDTH;
foreach (var l in unsolved)
if (!string.IsNullOrEmpty(commit.SHA))
{
if (l.Next == commit.SHA)
foreach (var l in unsolved)
{
if (major == null)
if (l.Next == commit.SHA)
{
offsetX += UNIT_WIDTH;
major = l;
if (commit.Parents.Count > 0)
if (major == null)
{
major.Next = commit.Parents[0];
if (!mapUnsolved.ContainsKey(major.Next))
mapUnsolved.Add(major.Next, major);
offsetX += UNIT_WIDTH;
major = l;
if (commit.Parents.Count > 0)
{
major.Next = commit.Parents[0];
if (!mapUnsolved.ContainsKey(major.Next))
mapUnsolved.Add(major.Next, major);
}
else
{
major.Next = "ENDED";
ended.Add(l);
}
major.Add(offsetX, offsetY, HALF_HEIGHT);
}
else
{
major.Next = "ENDED";
ended.Add(l);
}
major.Add(offsetX, offsetY, HALF_HEIGHT);
isMerged = isMerged || l.IsMerged;
}
else
{
ended.Add(l);
if (!mapUnsolved.ContainsKey(l.Next))
mapUnsolved.Add(l.Next, l);
offsetX += UNIT_WIDTH;
l.Add(offsetX, offsetY, HALF_HEIGHT);
}
isMerged = isMerged || l.IsMerged;
}
else
{
if (!mapUnsolved.ContainsKey(l.Next))
mapUnsolved.Add(l.Next, l);
offsetX += UNIT_WIDTH;
l.Add(offsetX, offsetY, HALF_HEIGHT);
}
}
@ -211,11 +215,11 @@ namespace SourceGit.Models
{
major.IsMerged = isMerged;
position = new Point(major.LastX, offsetY);
temp.Dots.Add(new Dot() { Center = position, Color = major.Path.Color });
temp.Dots.Add(new Dot() { Center = position, Color = major.Path.Color, IsWorkCopy = commit.IsWorkCopy });
}
else
{
temp.Dots.Add(new Dot() { Center = position, Color = 0 });
temp.Dots.Add(new Dot() { Center = position, Color = 0, IsWorkCopy = commit.IsWorkCopy });
}
// Deal with parents

View file

@ -286,6 +286,7 @@
<x:String x:Key="Text.Histories.Search" xml:space="preserve">SEARCH SHA/SUBJECT/AUTHOR. PRESS ENTER TO SEARCH, ESC TO QUIT</x:String>
<x:String x:Key="Text.Histories.SearchClear" xml:space="preserve">CLEAR</x:String>
<x:String x:Key="Text.Histories.Selected" xml:space="preserve">SELECTED {0} COMMITS</x:String>
<x:String x:Key="Text.Histories.UncommittedChanges.Subject" xml:space="preserve">★Uncommitted local changes★</x:String>
<x:String x:Key="Text.Hotkeys" xml:space="preserve">Keyboard Shortcuts Reference</x:String>
<x:String x:Key="Text.Hotkeys.Global" xml:space="preserve">GLOBAL</x:String>
<x:String x:Key="Text.Hotkeys.Global.CancelPopup" xml:space="preserve">Cancel current popup</x:String>
@ -380,6 +381,7 @@
<x:String x:Key="Text.Preference.Git.DefaultCloneDir" xml:space="preserve">Default Clone Dir</x:String>
<x:String x:Key="Text.Preference.Git.Email" xml:space="preserve">User Email</x:String>
<x:String x:Key="Text.Preference.Git.Email.Placeholder" xml:space="preserve">Global git user email</x:String>
<x:String x:Key="Text.Preference.Git.ShowUncommittedChangesInHistory" xml:space="preserve">Show uncommitted changes in history</x:String>
<x:String x:Key="Text.Preference.Git.Path" xml:space="preserve">Install Path</x:String>
<x:String x:Key="Text.Preference.Git.Shell" xml:space="preserve">Shell</x:String>
<x:String x:Key="Text.Preference.Git.User" xml:space="preserve">User Name</x:String>

View file

@ -289,6 +289,7 @@
<x:String x:Key="Text.Histories.Search" xml:space="preserve">查询提交指纹、信息、作者。回车键开始ESC键取消</x:String>
<x:String x:Key="Text.Histories.SearchClear" xml:space="preserve">清空</x:String>
<x:String x:Key="Text.Histories.Selected" xml:space="preserve">已选中 {0} 项提交</x:String>
<x:String x:Key="Text.Histories.UncommittedChanges.Subject" xml:space="preserve">★未提交的本地更改★</x:String>
<x:String x:Key="Text.Hotkeys" xml:space="preserve">快捷键参考</x:String>
<x:String x:Key="Text.Hotkeys.Global" xml:space="preserve">全局快捷键</x:String>
<x:String x:Key="Text.Hotkeys.Global.CancelPopup" xml:space="preserve">取消弹出面板</x:String>
@ -383,6 +384,7 @@
<x:String x:Key="Text.Preference.Git.DefaultCloneDir" xml:space="preserve">默认克隆路径</x:String>
<x:String x:Key="Text.Preference.Git.Email" xml:space="preserve">邮箱</x:String>
<x:String x:Key="Text.Preference.Git.Email.Placeholder" xml:space="preserve">默认GIT用户邮箱</x:String>
<x:String x:Key="Text.Preference.Git.ShowUncommittedChangesInHistory" xml:space="preserve">顯示歷史中未提交的更改</x:String>
<x:String x:Key="Text.Preference.Git.Path" xml:space="preserve">安装路径</x:String>
<x:String x:Key="Text.Preference.Git.Shell" xml:space="preserve">终端Shell</x:String>
<x:String x:Key="Text.Preference.Git.User" xml:space="preserve">用户名</x:String>

View file

@ -289,6 +289,7 @@
<x:String x:Key="Text.Histories.Search" xml:space="preserve">查詢提交指紋、資訊、作者。回車鍵開始ESC鍵取消</x:String>
<x:String x:Key="Text.Histories.SearchClear" xml:space="preserve">清空</x:String>
<x:String x:Key="Text.Histories.Selected" xml:space="preserve">已選中 {0} 項提交</x:String>
<x:String x:Key="Text.Histories.UncommittedChanges.Subject" xml:space="preserve">★未提交的本地更改★</x:String>
<x:String x:Key="Text.Hotkeys" xml:space="preserve">快捷鍵參考</x:String>
<x:String x:Key="Text.Hotkeys.Global" xml:space="preserve">全域性快捷鍵</x:String>
<x:String x:Key="Text.Hotkeys.Global.CancelPopup" xml:space="preserve">取消彈出面板</x:String>
@ -383,6 +384,7 @@
<x:String x:Key="Text.Preference.Git.DefaultCloneDir" xml:space="preserve">預設克隆路徑</x:String>
<x:String x:Key="Text.Preference.Git.Email" xml:space="preserve">郵箱</x:String>
<x:String x:Key="Text.Preference.Git.Email.Placeholder" xml:space="preserve">預設GIT使用者郵箱</x:String>
<x:String x:Key="Text.Preference.Git.ShowUncommittedChangesInHistory" xml:space="preserve">显示历史记录中未提交的更改</x:String>
<x:String x:Key="Text.Preference.Git.Path" xml:space="preserve">安裝路徑</x:String>
<x:String x:Key="Text.Preference.Git.Shell" xml:space="preserve">終端Shell</x:String>
<x:String x:Key="Text.Preference.Git.User" xml:space="preserve">使用者名稱</x:String>

View file

@ -116,7 +116,14 @@ namespace SourceGit.ViewModels
AutoSelectedCommit = commit;
NavigationId = _navigationId + 1;
if (_detailContext is CommitDetail detail)
if (commit.IsWorkCopy)
{
var wc = _repo.WorkingCopy ??= new WorkingCopy(_repo);
DetailContext = wc;
wc.RefreshWorkingCopyChangesAsync();
}
else if (_detailContext is CommitDetail detail)
{
detail.Commit = commit;
}

View file

@ -147,6 +147,12 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _maxHistoryCommits, value);
}
public bool ShowUncommittedChangesInHistory
{
get => _showUncommittedChangesInHistory;
set => SetProperty(ref _showUncommittedChangesInHistory, value);
}
public int SubjectGuideLength
{
get => _subjectGuideLength;
@ -515,6 +521,7 @@ namespace SourceGit.ViewModels
private LayoutInfo _layout = new LayoutInfo();
private int _maxHistoryCommits = 20000;
private bool _showUncommittedChangesInHistory = false;
private int _subjectGuideLength = 50;
private bool _restoreTabs = false;
private bool _useFixedTabWidth = true;

View file

@ -716,6 +716,7 @@ namespace SourceGit.ViewModels
Dispatcher.UIThread.Invoke(() => _histories.IsLoading = true);
var limits = $"-{Preference.Instance.MaxHistoryCommits} ";
var showUncommitedChangedInHistory = Preference.Instance.ShowUncommittedChangesInHistory;
var validFilters = new List<string>();
foreach (var filter in _settings.Filters)
{
@ -762,6 +763,36 @@ namespace SourceGit.ViewModels
}
var commits = new Commands.QueryCommits(_fullpath, limits).Result();
if (showUncommitedChangedInHistory)
{
var changes = new Commands.QueryLocalChanges(_fullpath, _includeUntracked).Result();
if(changes.Count > 0)
{
var config = new Commands.Config(_fullpath).ListAll();
var currentUser = new Models.User();
if (config.TryGetValue("user.name", out var name))
currentUser.Name = name;
if (config.TryGetValue("user.email", out var email))
currentUser.Email = email;
var date = (ulong)DateTime.Now.ToUniversalTime().Subtract(DateTime.UnixEpoch).TotalSeconds;
commits.Insert(0,new Models.Commit()
{
Subject = App.Text("Histories.UncommittedChanges.Subject"),
CanPullFromUpstream = false,
Author = currentUser,
Committer = currentUser,
Parents = [currentBranch.Head],
AuthorTime = date,
CommitterTime = date,
Decorators = [
new Models.Decorator()
{
Type = Models.DecoratorType.CurrentBranchHead,
Name = currentBranch.FriendlyName,
}]
});
}
}
var graph = Models.CommitGraph.Parse(commits, canPushCommits, canPullCommits);
Dispatcher.UIThread.Invoke(() =>
@ -781,41 +812,15 @@ namespace SourceGit.ViewModels
Dispatcher.UIThread.Invoke(() => Submodules = submodules);
}
public void RefreshWorkingCopyChanges()
public async void RefreshWorkingCopyChanges()
{
var changes = new Commands.QueryLocalChanges(_fullpath, _includeUntracked).Result();
if (_workingCopy == null)
return;
var hasUnsolvedConflict = _workingCopy.SetData(changes);
var inProgress = null as InProgressContext;
var result = await _workingCopy.RefreshWorkingCopyChangesAsync();
var rebaseMergeFolder = Path.Combine(_gitDir, "rebase-merge");
var rebaseApplyFolder = Path.Combine(_gitDir, "rebase-apply");
if (File.Exists(Path.Combine(_gitDir, "CHERRY_PICK_HEAD")))
{
inProgress = new CherryPickInProgress(_fullpath);
}
else if (File.Exists(Path.Combine(_gitDir, "REBASE_HEAD")) && Directory.Exists(rebaseMergeFolder))
{
inProgress = new RebaseInProgress(this);
}
else if (File.Exists(Path.Combine(_gitDir, "REVERT_HEAD")))
{
inProgress = new RevertInProgress(_fullpath);
}
else if (File.Exists(Path.Combine(_gitDir, "MERGE_HEAD")))
{
inProgress = new MergeInProgress(_fullpath);
}
else
{
if (Directory.Exists(rebaseMergeFolder))
Directory.Delete(rebaseMergeFolder, true);
if (Directory.Exists(rebaseApplyFolder))
Directory.Delete(rebaseApplyFolder, true);
}
var hasUnsolvedConflict = result.HasUnsolvedConflict;
var inProgress = result.InProgressContext;
Dispatcher.UIThread.Invoke(() =>
{
@ -1957,6 +1962,12 @@ namespace SourceGit.ViewModels
}
}
public WorkingCopy WorkingCopy
{
get => _workingCopy;
set => _workingCopy = value;
}
private string _fullpath = string.Empty;
private string _gitDir = string.Empty;
private Models.RepositorySettings _settings = null;

View file

@ -1280,6 +1280,47 @@ namespace SourceGit.ViewModels
});
}
public async Task<(InProgressContext InProgressContext, bool HasUnsolvedConflict)> RefreshWorkingCopyChangesAsync()
{
return await Task.Factory.StartNew(() =>
{
var gitDir = _repo.GitDir;
var fullpath = _repo.FullPath;
var changes = new Commands.QueryLocalChanges(fullpath, _repo.IncludeUntracked).Result();
var hasUnsolvedConflict = SetData(changes);
var inProgress = null as InProgressContext;
var rebaseMergeFolder = Path.Combine(gitDir, "rebase-merge");
var rebaseApplyFolder = Path.Combine(gitDir, "rebase-apply");
if (File.Exists(Path.Combine(gitDir, "CHERRY_PICK_HEAD")))
{
inProgress = new CherryPickInProgress(fullpath);
}
else if (File.Exists(Path.Combine(gitDir, "REBASE_HEAD")) && Directory.Exists(rebaseMergeFolder))
{
inProgress = new RebaseInProgress(_repo);
}
else if (File.Exists(Path.Combine(gitDir, "REVERT_HEAD")))
{
inProgress = new RevertInProgress(fullpath);
}
else if (File.Exists(Path.Combine(gitDir, "MERGE_HEAD")))
{
inProgress = new MergeInProgress(fullpath);
}
else
{
if (Directory.Exists(rebaseMergeFolder))
Directory.Delete(rebaseMergeFolder, true);
if (Directory.Exists(rebaseApplyFolder))
Directory.Delete(rebaseApplyFolder, true);
}
return (inProgress, hasUnsolvedConflict);
});
}
private Repository _repo = null;
private bool _isLoadingData = false;
private bool _isStaging = false;

View file

@ -15,6 +15,7 @@ namespace SourceGit.Views
public Geometry Icon { get; set; } = null;
public FormattedText Label { get; set; } = null;
public bool IsTag { get; set; } = false;
public bool IsWorkCopy { get; internal set; } = false;
}
public static readonly StyledProperty<FontFamily> FontFamilyProperty =
@ -117,6 +118,14 @@ namespace SourceGit.Views
using (context.PushTransform(Matrix.CreateTranslation(x + 4, 4)))
context.DrawGeometry(iconFG, null, item.Icon);
if (item.IsWorkCopy)
{
var workCopyBorderRect = new RoundedRect(new Rect(x, 0, 16 + item.Label.Width + 8, 16)
, new CornerRadius(2, 0, 0, 2));
var workCopyBorderPen = new Pen(item.IsTag ? tagBG : branchBG, 2, new DashStyle() { Dashes = [1, 1], Offset = 1 });
context.DrawRectangle(null, workCopyBorderPen, workCopyBorderRect);
}
x += item.Label.Width + 16 + 8 + 4;
}
}
@ -156,6 +165,7 @@ namespace SourceGit.Views
{
Label = label,
IsTag = decorator.Type == Models.DecoratorType.Tag,
IsWorkCopy = commit.IsWorkCopy,
};
var geo = null as StreamGeometry;

View file

@ -189,6 +189,11 @@
</ContentControl.Content>
<ContentControl.DataTemplates>
<DataTemplate DataType="vm:WorkingCopy">
<v:WorkingCopy/>
</DataTemplate>
<DataTemplate DataType="vm:CommitDetail">
<v:CommitDetail/>
</DataTemplate>

View file

@ -263,8 +263,12 @@ namespace SourceGit.Views
continue;
if (dot.Center.Y > bottom)
break;
context.DrawEllipse(dotFill, Models.CommitGraph.Pens[dot.Color], dot.Center, 3, 3);
var pen = Models.CommitGraph.Pens[dot.Color];
if (dot.IsWorkCopy)
{
pen = new Pen(pen.Brush, pen.Thickness, s_UncommittedCahngesLineDashStyle);
}
context.DrawEllipse(dotFill, pen, dot.Center, 5, 5);
}
}
@ -282,7 +286,6 @@ namespace SourceGit.Views
var geo = new StreamGeometry();
var pen = Models.CommitGraph.Pens[line.Color];
using (var ctx = geo.Open())
{
var started = false;
@ -351,10 +354,12 @@ namespace SourceGit.Views
ctx.BeginFigure(link.Start, false);
ctx.QuadraticBezierTo(link.Control, link.End);
}
context.DrawGeometry(null, Models.CommitGraph.Pens[link.Color], geo);
var pen = Models.CommitGraph.Pens[link.Color];
context.DrawGeometry(null, pen, geo);
}
}
private readonly static IDashStyle s_UncommittedCahngesLineDashStyle = new DashStyle() { Dashes = [1, 1], Offset = 1 };
}
public partial class Histories : UserControl
@ -403,7 +408,7 @@ namespace SourceGit.Views
private void OnCommitDataGridContextRequested(object sender, ContextRequestedEventArgs e)
{
if (DataContext is ViewModels.Histories histories && sender is DataGrid datagrid)
if (DataContext is ViewModels.Histories histories && sender is DataGrid datagrid && datagrid.SelectedItem is Models.Commit { IsWorkCopy: false})
{
var menu = histories.MakeContextMenu(datagrid);
datagrid.OpenContextMenu(menu);

View file

@ -244,7 +244,7 @@
<TextBlock Classes="tab_header" Text="{DynamicResource Text.Preference.Git}"/>
</TabItem.Header>
<Grid Margin="8" RowDefinitions="32,32,Auto,32,32,32,32,32,Auto" ColumnDefinitions="Auto,*">
<Grid Margin="8" RowDefinitions="32,32,Auto,32,32,32,32,32,Auto,32" ColumnDefinitions="Auto,*">
<TextBlock Grid.Row="0" Grid.Column="0"
Text="{DynamicResource Text.Preference.Git.Path}"
HorizontalAlignment="Right"
@ -395,6 +395,10 @@
Margin="5,0,0,0"
Text="{DynamicResource Text.Preference.Git.AutoFetchIntervalSuffix}" />
</Grid>
<CheckBox Grid.Row="9" Grid.Column="1"
Content="{DynamicResource Text.Preference.Git.ShowUncommittedChangesInHistory}"
IsChecked="{Binding ShowUncommittedChangesInHistory, Mode=TwoWay}"
/>
</Grid>
</TabItem>

View file

@ -87,7 +87,7 @@
</Grid>
</ListBoxItem>
<ListBoxItem>
<ListBoxItem IsVisible="{Binding Source={x:Static vm:Preference.Instance}, Path=!ShowUncommittedChangesInHistory , Mode=OneWay}">
<Grid Classes="view_mode" ColumnDefinitions="32,*,Auto">
<Path Grid.Column="0" Width="12" Height="12" Data="{StaticResource Icons.Send}"/>
<TextBlock Grid.Column="1" Classes="primary" Text="{DynamicResource Text.WorkingCopy}"/>

View file

@ -118,7 +118,7 @@
<!-- Right -->
<Grid Grid.Column="2" Margin="0,4,4,4">
<Grid.RowDefinitions>
<RowDefinition Height="*" MinHeight="400"/>
<RowDefinition Height="*"/>
<RowDefinition Height="4"/>
<RowDefinition Height="128" MinHeight="100"/>
<RowDefinition Height="36"/>
@ -153,7 +153,7 @@
<TextBlock Margin="0,16,0,8" FontSize="20" FontWeight="Bold" Text="{DynamicResource Text.WorkingCopy.Conflicts.Resolved}" Foreground="{DynamicResource Brush.FG2}" HorizontalAlignment="Center"/>
<TextBlock Text="{DynamicResource Text.WorkingCopy.CanStageTip}" Foreground="{DynamicResource Brush.FG2}" HorizontalAlignment="Center"/>
</StackPanel>
</Grid>
</Grid>
</Border>
</DataTemplate>