Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/Models/Commit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ public class Commit : ObservableObject

public bool IsMerged { get; set; } = false;
public int Color { get; set; } = 0;
public double LeftMargin { get; set; } = 0;
public double LeftMargin
{
get => _leftMargin;
set => SetProperty(ref _leftMargin, value);
}

private double _leftMargin;

public bool IsHighlightedInGraph
{
Expand Down
54 changes: 53 additions & 1 deletion src/Models/CommitGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public enum DotType
Default,
Head,
Merge,
WorkingDirectory,
}

public class Dot
Expand All @@ -71,7 +72,7 @@ public class Dot
public List<Link> Links { get; } = [];
public List<Dot> Dots { get; } = [];

public static CommitGraph Generate(List<Commit> commits, bool firstParentOnlyEnabled, CommitGraphHighlighting highlighting, HashSet<string> highlightExtraCommits)
public static CommitGraph Generate(List<Commit> commits, bool firstParentOnlyEnabled, CommitGraphHighlighting highlighting, HashSet<string> highlightExtraCommits, bool hasWorkingDirectory = false, bool reserveHeadLane = true)
{
const double unitWidth = 12;
const double halfWidth = 6;
Expand All @@ -85,6 +86,17 @@ public static CommitGraph Generate(List<Commit> commits, bool firstParentOnlyEna
var colorPicker = new ColorPicker();
var defHighlighting = highlighting == CommitGraphHighlighting.All;

// Keep HEAD's first-parent ancestry in the first lane, even when other
// branch tips appear above it. The reservation is not a Git commit or
// a visible edge; drawing starts when the actual HEAD is encountered.
PathHelper headLane = null;
var headCommit = commits.Find(x => x.IsCurrentHead);
if (reserveHeadLane && headCommit != null)
{
headLane = new PathHelper(headCommit.SHA, defHighlighting, colorPicker.Next(), new Point(10, -halfHeight));
unsolved.Add(headLane);
}

foreach (var commit in commits)
{
PathHelper major = null;
Expand Down Expand Up @@ -176,6 +188,12 @@ public static CommitGraph Generate(List<Commit> commits, bool firstParentOnlyEna
}
commit.IsHighlightedInGraph = isHighlighted;

if (headLane != null && commit.IsCurrentHead && major == headLane)
{
headLane.Path.Points.Clear();
headLane.Path.Points.Add(new Point(10, offsetY));
}

// If no path found, create new curve for branch head
// Otherwise, create new curve for new merged commit
if (major == null)
Expand All @@ -195,6 +213,9 @@ public static CommitGraph Generate(List<Commit> commits, bool firstParentOnlyEna
temp.Paths.Add(major.Path);
}

if (headLane != null && commit.IsCurrentHead && major == headLane && commit.Parents.Count > 0 && !temp.Paths.Contains(major.Path))
temp.Paths.Add(major.Path);

// Calculate link position of this commit.
var position = new Point(major?.LastX ?? offsetX, offsetY);
var dotColor = major?.Path.Color ?? 0;
Expand Down Expand Up @@ -265,6 +286,37 @@ public static CommitGraph Generate(List<Commit> commits, bool firstParentOnlyEna
}
unsolved.Clear();

if (hasWorkingDirectory)
{
// Add a presentation row without moving any of the existing graph lanes.
var offset = new Vector(0, 1);
foreach (var path in temp.Paths)
for (var i = 0; i < path.Points.Count; i++)
path.Points[i] += offset;
foreach (var link in temp.Links)
{
link.Start += offset;
link.Control += offset;
link.End += offset;
}
foreach (var dot in temp.Dots)
dot.Center += offset;

var working = new Dot { Type = DotType.WorkingDirectory, Center = new Point(10, 0.5), IsHighlighted = true };
var headIndex = commits.FindIndex(x => x.IsCurrentHead);
if (headIndex >= 0)
{
var head = temp.Dots[headIndex];
working.Center = new Point(head.Center.X, 0.5);
var edge = new Path(head.Color, true);
edge.Points.Add(working.Center);
edge.Points.Add(head.Center);
temp.Paths.Insert(0, edge);
working.Color = head.Color;
}
temp.Dots.Insert(0, working);
}

return temp;
}

Expand Down
1 change: 1 addition & 0 deletions src/Resources/Icons.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
<StreamGeometry x:Key="Icons.Password">M640 96c-158 0-288 130-288 288 0 17 3 31 5 46L105 681 96 691V928h224v-96h96v-96h96v-95c38 18 82 31 128 31 158 0 288-130 288-288s-130-288-288-288zm0 64c123 0 224 101 224 224s-101 224-224 224a235 235 0 01-109-28l-8-4H448v96h-96v96H256v96H160v-146l253-254 12-11-3-17C419 417 416 400 416 384c0-123 101-224 224-224zm64 96a64 64 0 100 128 64 64 0 100-128z</StreamGeometry>
<StreamGeometry x:Key="Icons.Paste">M544 85c49 0 90 37 95 85h75a96 96 0 0196 89L811 267a32 32 0 01-28 32L779 299a32 32 0 01-32-28L747 267a32 32 0 00-28-32L715 235h-91a96 96 0 01-80 42H395c-33 0-62-17-80-42L224 235a32 32 0 00-32 28L192 267v576c0 16 12 30 28 32l4 0h128a32 32 0 0132 28l0 4a32 32 0 01-32 32h-128a96 96 0 01-96-89L128 843V267a96 96 0 0189-96L224 171h75a96 96 0 0195-85h150zm256 256a96 96 0 0196 89l0 7v405a96 96 0 01-89 96L800 939h-277a96 96 0 01-96-89L427 843v-405a96 96 0 0189-96L523 341h277zm-256-192H395a32 32 0 000 64h150a32 32 0 100-64z</StreamGeometry>
<StreamGeometry x:Key="Icons.Pattern">M470 722q-23 3-43 3T384 722v-150l-106 106q-34-26-60-59L324 512H174q-3-23-3-43t3-42h150L218 320q16-20 28-32t32-27L384 367V217q23-4 43-4t43 4v150l106-106q34 26 60 59l-106 107h150q3 22 3 42T680 512h-150l106 107q-16 20-28 32t-32 27l-106-106v150zM0 811q0-36 25-61t61-25 61 25 25 61-25 61-61 25-61-25T0 811z</StreamGeometry>
<StreamGeometry x:Key="Icons.Pencil">M652 157a113 113 0 11156 161L731 395 572 236l80-80 1 1zM334 792v0H175v-159l358-358 159 159-358 358z</StreamGeometry>
<StreamGeometry x:Key="Icons.Plus">M576 64H448v384H64v128h384v384h128V576h384V448H576z</StreamGeometry>
<StreamGeometry x:Key="Icons.Pull">M432 0h160c27 0 48 21 48 48v336h175c36 0 53 43 28 68L539 757c-15 15-40 15-55 0L180 452c-25-25-7-68 28-68H384V48c0-27 21-48 48-48zm592 752v224c0 27-21 48-48 48H48c-27 0-48-21-48-48V752c0-27 21-48 48-48h293l98 98c40 40 105 40 145 0l98-98H976c27 0 48 21 48 48zm-248 176c0-22-18-40-40-40s-40 18-40 40s18 40 40 40s40-18 40-40zm128 0c0-22-18-40-40-40s-40 18-40 40s18 40 40 40s40-18 40-40z</StreamGeometry>
<StreamGeometry x:Key="Icons.Push">M592 768h-160c-27 0-48-21-48-48V384h-175c-36 0-53-43-28-68L485 11c15-15 40-15 55 0l304 304c25 25 7 68-28 68H640v336c0 27-21 48-48 48zm432-16v224c0 27-21 48-48 48H48c-27 0-48-21-48-48V752c0-27 21-48 48-48h272v16c0 62 50 112 112 112h160c62 0 112-50 112-112v-16h272c27 0 48 21 48 48zm-248 176c0-22-18-40-40-40s-40 18-40 40s18 40 40 40s40-18 40-40zm128 0c0-22-18-40-40-40s-40 18-40 40s18 40 40 40s40-18 40-40z</StreamGeometry>
Expand Down
9 changes: 9 additions & 0 deletions src/Resources/Locales/en_US.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,10 @@
<x:String x:Key="Text.Preferences.Appearance.UseAutoHideScrollBars" xml:space="preserve">Use auto-hide scrollbars</x:String>
<x:String x:Key="Text.Preferences.Appearance.UseFixedTabWidth" xml:space="preserve">Use fixed tab width in titlebar</x:String>
<x:String x:Key="Text.Preferences.Appearance.UseNativeWindowFrame" xml:space="preserve">Use native window frame</x:String>
<x:String x:Key="Text.Preferences.Appearance.WorkingDirectoryHead" xml:space="preserve">Working Directory Head</x:String>
<x:String x:Key="Text.Preferences.Appearance.WorkingDirectoryHead.Hide" xml:space="preserve">Hide</x:String>
<x:String x:Key="Text.Preferences.Appearance.WorkingDirectoryHead.WhenChanged" xml:space="preserve">Show when there are local changes</x:String>
<x:String x:Key="Text.Preferences.Appearance.WorkingDirectoryHead.Always" xml:space="preserve">Show always</x:String>
<x:String x:Key="Text.Preferences.DiffMerge" xml:space="preserve">DIFF/MERGE TOOL</x:String>
<x:String x:Key="Text.Preferences.DiffMerge.DiffArgs" xml:space="preserve">Diff Arguments</x:String>
<x:String x:Key="Text.Preferences.DiffMerge.DiffArgs.Tip" xml:space="preserve">Available variables: $LOCAL, $REMOTE</x:String>
Expand Down Expand Up @@ -1089,4 +1093,9 @@
<x:String x:Key="Text.Worktree.Remove" xml:space="preserve">Remove</x:String>
<x:String x:Key="Text.Worktree.Unlock" xml:space="preserve">Unlock</x:String>
<x:String x:Key="Text.Yes" xml:space="preserve">YES</x:String>
<x:String x:Key="Text.Histories.WorkingDirectory">* Working Directory *</x:String>
<x:String x:Key="Text.Histories.WorkingDirectory.Modified">Modified files (including renames, type changes and conflicts)</x:String>
<x:String x:Key="Text.Histories.WorkingDirectory.Added">Added and untracked files</x:String>
<x:String x:Key="Text.Histories.WorkingDirectory.Deleted">Deleted files</x:String>
<x:String x:Key="Text.Histories.LocalChanges">LOCAL CHANGES</x:String>
</ResourceDictionary>
138 changes: 135 additions & 3 deletions src/ViewModels/Histories.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Threading.Tasks;

Expand All @@ -10,7 +11,7 @@

namespace SourceGit.ViewModels
{
public class Histories : ObservableObject
public class Histories : ObservableObject, IDisposable
{
public bool IsLoading
{
Expand Down Expand Up @@ -109,10 +110,99 @@ public List<Models.Commit> Commits
{
GenerateGraph(value);
if (SetProperty(ref _commits, value))
PostCommitsChanged();
{
IsUpdatingEntries = true;
Entries.Clear();
if (IsWorkingDirectoryVisible)
Entries.Add(WorkingDirectory);
Entries.AddRange(value);
IsUpdatingEntries = false;
if (_selectHeadAfterRefresh)
{
_selectHeadAfterRefresh = false;
var head = value.Find(x => x.IsCurrentHead);
SelectedCommits = head == null ? [] : [head];
if (head == null)
DetailContext = Models.Null.Instance;
}
else
PostCommitsChanged();
if (IsWorkingDirectorySelected)
SelectedCommits = [];
}
}
}

public bool IsUpdatingEntries { get; private set; }

public AvaloniaList<object> Entries { get; } = [];
public WorkingDirectoryEntry WorkingDirectory { get; } = new();
public WorkingCopy WorkingCopy => _repo.WorkingCopy;

public bool IsWorkingDirectoryVisible => !_repo.IsBare && Preferences.Instance.WorkingDirectoryHeadMode switch
{
1 => WorkingDirectory.HasChanges,
2 => true,
_ => false,
};

public bool IsWorkingDirectorySelected
{
get => _isWorkingDirectorySelected;
set
{
if ((value && !IsWorkingDirectoryVisible) || !SetProperty(ref _isWorkingDirectorySelected, value))
return;

if (value)
{
_selectHeadAfterRefresh = false;
SelectedCommits = [];
_searchCommitContext.Selected = null;
DetailContext = WorkingCopy;
}
else if (_selectedCommits.Count == 0)
DetailContext = Models.Null.Instance;
}
}

public void UpdateWorkingDirectory(List<Models.Change> changes)
{
WorkingDirectory.SetChanges(changes);
RefreshWorkingDirectoryVisibility(true);
}

private void RefreshWorkingDirectoryVisibility(bool followHeadAfterRefresh)
{
if (Entries.Contains(WorkingDirectory) == IsWorkingDirectoryVisible)
return;

OnPropertyChanged(nameof(IsWorkingDirectoryVisible));

if (IsWorkingDirectoryVisible)
{
_selectHeadAfterRefresh = false;
Entries.Insert(0, WorkingDirectory);
}
else
{
var wasSelected = IsWorkingDirectorySelected;
IsWorkingDirectorySelected = false;
Entries.Remove(WorkingDirectory);
if (wasSelected)
{
var head = _commits.Find(x => x.IsCurrentHead);
SelectedCommits = head == null ? [] : [head];
if (head == null)
DetailContext = Models.Null.Instance;
// Status may finish before the history query after committing. Follow
// the newly checked-out HEAD when that query arrives, unless the user navigates.
_selectHeadAfterRefresh = followHeadAfterRefresh;
}
}
GenerateGraph(_commits);
}

public Models.CommitGraph Graph
{
get => _graph;
Expand All @@ -137,6 +227,11 @@ public List<Models.Commit> SelectedCommits
get => _selectedCommits;
set
{
if (value.Count > 0)
{
_selectHeadAfterRefresh = false;
IsWorkingDirectorySelected = false;
}
var oldCount = _selectedCommits.Count;
if (SetProperty(ref _selectedCommits, value) && oldCount + value.Count > 0)
PostSelectedCommitsChanged();
Expand Down Expand Up @@ -233,6 +328,30 @@ public Histories(Repository repo)
_repo = repo;
_commitDetailSharedData = new CommitDetailSharedData();
_searchCommitContext = new SearchCommitContext(repo.FullPath, this);
Preferences.Instance.PropertyChanged += OnPreferencesChanged;
}

public void Dispose()
{
Preferences.Instance.PropertyChanged -= OnPreferencesChanged;
}

private void OnPreferencesChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(Preferences.WorkingDirectoryHeadMode))
return;

_selectHeadAfterRefresh = false;
var graph = Graph;
RefreshWorkingDirectoryVisibility(false);
// Hide also restores the original graph lanes when the repository is clean.
if (ReferenceEquals(graph, Graph))
GenerateGraph(_commits);
}

public void CancelPendingHeadSelection()
{
_selectHeadAfterRefresh = false;
}

public bool HasShowFlag(Models.HistoryShowFlags flag)
Expand Down Expand Up @@ -296,6 +415,7 @@ public Models.BisectState UpdateBisectInfo()

public void NavigateTo(string commitSHA)
{
_selectHeadAfterRefresh = false;
var commit = _commits.Find(x => x.SHA.StartsWith(commitSHA, StringComparison.Ordinal));
if (commit != null)
{
Expand All @@ -312,6 +432,7 @@ public void NavigateTo(string commitSHA)
Dispatcher.UIThread.Post(() =>
{
_ignoreSelectionChange = true;
IsWorkingDirectorySelected = false;
SelectedCommits = [];

if (_detailContext is CommitDetail detail)
Expand Down Expand Up @@ -524,6 +645,12 @@ private void PostSelectedCommitsChanged()
if (_ignoreSelectionChange)
return;

if (IsWorkingDirectorySelected)
{
DetailContext = WorkingCopy;
return;
}

if (_selectedCommits.Count == 0)
{
_searchCommitContext.Selected = null;
Expand Down Expand Up @@ -574,9 +701,14 @@ private void GenerateGraph(List<Models.Commit> commits)
extraHeads.Add(c.SHA);
}

Graph = Models.CommitGraph.Generate(commits, firstParentOnly, highlighting, extraHeads);
var graph = Models.CommitGraph.Generate(commits, firstParentOnly, highlighting, extraHeads, IsWorkingDirectoryVisible, Preferences.Instance.WorkingDirectoryHeadMode != 0);
if (IsWorkingDirectoryVisible)
WorkingDirectory.LeftMargin = graph.Dots[0].Center.X + 14;
Graph = graph;
}

private bool _selectHeadAfterRefresh;
private bool _isWorkingDirectorySelected;
private Repository _repo = null;
private Models.Branch _currentBranch = null;
private bool _hasSingleRemote = false;
Expand Down
11 changes: 11 additions & 0 deletions src/ViewModels/Preferences.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,16 @@ public bool Use24Hours
}
}

public int WorkingDirectoryHeadMode
{
get => _workingDirectoryHeadMode;
set
{
if (value >= 0 && value <= 2)
SetProperty(ref _workingDirectoryHeadMode, value);
}
}

public bool UseFixedTabWidth
{
get => _useFixedTabWidth;
Expand Down Expand Up @@ -820,6 +830,7 @@ private bool RemoveInvalidRepositoriesRecursive(List<RepositoryNode> collection)

private int _maxHistoryCommits = 20000;
private int _subjectGuideLength = 50;
private int _workingDirectoryHeadMode = 0;
private bool _useFixedTabWidth = true;
private bool _useAutoHideScrollBars = true;
private bool _useGitHubStyleAvatar = true;
Expand Down
2 changes: 2 additions & 0 deletions src/ViewModels/Repository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ public void Close()
if (_cancellationRefreshStashes is { IsCancellationRequested: false })
_cancellationRefreshStashes.Cancel();

_histories.Dispose();
_watcher?.Dispose();
_autoFetchTimer.Dispose();
}
Expand Down Expand Up @@ -1348,6 +1349,7 @@ public void RefreshWorkingCopyChanges()

_workingCopy.SetData(changes);
LocalChangesCount = changes.Count;
_histories.UpdateWorkingDirectory(changes);
OnPropertyChanged(nameof(InProgressContext));
GetOwnerPage()?.ChangeDirtyState(Models.DirtyState.HasLocalChanges, changes.Count == 0);
});
Expand Down
Loading