diff --git a/src/Commands/Checkout.cs b/src/Commands/Checkout.cs index d65e9e73..a1a151aa 100644 --- a/src/Commands/Checkout.cs +++ b/src/Commands/Checkout.cs @@ -62,6 +62,14 @@ namespace SourceGit.Commands return Exec(); } + public bool Commit(string commitId, Action onProgress) + { + Args = $"checkout --detach --progress {commitId}"; + TraitErrorAsOutput = true; + _outputHandler = onProgress; + return Exec(); + } + public bool Files(List files) { StringBuilder builder = new StringBuilder(); diff --git a/src/Commands/QueryBranches.cs b/src/Commands/QueryBranches.cs index cc726fd9..04241933 100644 --- a/src/Commands/QueryBranches.cs +++ b/src/Commands/QueryBranches.cs @@ -51,7 +51,12 @@ namespace SourceGit.Commands var refName = parts[0]; if (refName.EndsWith("/HEAD", StringComparison.Ordinal)) return; - + + if (refName.StartsWith("(HEAD detached at")) + { + branch.isHead = true; + } + if (refName.StartsWith(PREFIX_LOCAL, StringComparison.Ordinal)) { branch.Name = refName.Substring(PREFIX_LOCAL.Length); diff --git a/src/Commands/QueryCommits.cs b/src/Commands/QueryCommits.cs index 7d6ad169..618ff014 100644 --- a/src/Commands/QueryCommits.cs +++ b/src/Commands/QueryCommits.cs @@ -146,6 +146,15 @@ namespace SourceGit.Commands Name = d.Substring(19).Trim(), }); } + else if (d.Equals("HEAD")) + { + isHeadOfCurrent = true; + decorators.Add(new Models.Decorator() + { + Type = Models.DecoratorType.CurrentCommitHead, + Name = d.Trim(), + }); + } else if (d.StartsWith("refs/heads/", StringComparison.Ordinal)) { decorators.Add(new Models.Decorator() diff --git a/src/Converters/DecoratorTypeConverters.cs b/src/Converters/DecoratorTypeConverters.cs index eb016360..9f3d9447 100644 --- a/src/Converters/DecoratorTypeConverters.cs +++ b/src/Converters/DecoratorTypeConverters.cs @@ -38,6 +38,9 @@ namespace SourceGit.Converters }); public static readonly FuncValueConverter ToFontWeight = - new FuncValueConverter(v => v == Models.DecoratorType.CurrentBranchHead ? FontWeight.Bold : FontWeight.Regular); + new FuncValueConverter(v => + v is Models.DecoratorType.CurrentBranchHead or Models.DecoratorType.CurrentCommitHead + ? FontWeight.Bold : FontWeight.Regular + ); } } diff --git a/src/Models/Branch.cs b/src/Models/Branch.cs index c23718d4..0b346712 100644 --- a/src/Models/Branch.cs +++ b/src/Models/Branch.cs @@ -10,5 +10,6 @@ public string Upstream { get; set; } public string UpstreamTrackStatus { get; set; } public string Remote { get; set; } + public bool isHead { get; set; } } } diff --git a/src/Models/Commit.cs b/src/Models/Commit.cs index 4b3f0ed3..d8355e81 100644 --- a/src/Models/Commit.cs +++ b/src/Models/Commit.cs @@ -32,7 +32,7 @@ namespace SourceGit.Models public bool IsCurrentHead { - get => Decorators.Find(x => x.Type == DecoratorType.CurrentBranchHead) != null; + get => Decorators.Find(x => x.Type is DecoratorType.CurrentBranchHead or DecoratorType.CurrentCommitHead) != null; } public string FullMessage diff --git a/src/Models/Decorator.cs b/src/Models/Decorator.cs index 10967b45..60ffc1ee 100644 --- a/src/Models/Decorator.cs +++ b/src/Models/Decorator.cs @@ -7,6 +7,7 @@ namespace SourceGit.Models None, CurrentBranchHead, LocalBranchHead, + CurrentCommitHead, RemoteBranchHead, Tag, } diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index a9971637..ab1f2296 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -54,7 +54,11 @@ Show as List Show as Tree Checkout Branch + Checkout Commit + Warning: By doing a commit checkout, your Head will be detached Branch : + Commit SHA : + Commit Short SHA : Local Changes : Stash & Reapply Discard @@ -74,6 +78,7 @@ CLOSE Cherry-Pick This Commit Copy SHA + Checkout commit${0} Rebase${0}$to Here Reset${0}$to Here Revert Commit diff --git a/src/SourceGit.csproj b/src/SourceGit.csproj index 657b49f3..0cc2f21e 100644 --- a/src/SourceGit.csproj +++ b/src/SourceGit.csproj @@ -44,4 +44,11 @@ + + + + CheckoutCommit.axaml + Code + + diff --git a/src/ViewModels/BranchTreeNode.cs b/src/ViewModels/BranchTreeNode.cs index 194b1a26..79097895 100644 --- a/src/ViewModels/BranchTreeNode.cs +++ b/src/ViewModels/BranchTreeNode.cs @@ -11,6 +11,7 @@ namespace SourceGit.ViewModels { public enum BranchTreeNodeType { + DetachedHead, Remote, Folder, Branch, @@ -51,6 +52,11 @@ namespace SourceGit.ViewModels { get => Type == BranchTreeNodeType.Branch; } + + public bool IsDetachedHead + { + get => Type == BranchTreeNodeType.DetachedHead; + } public bool IsCurrent { @@ -213,11 +219,11 @@ namespace SourceGit.ViewModels start = sepIdx + 1; sepIdx = branch.Name.IndexOf('/', start); } - + lastFolder.Children.Add(new BranchTreeNode() { Name = Path.GetFileName(branch.Name), - Type = BranchTreeNodeType.Branch, + Type = branch.isHead ? BranchTreeNodeType.DetachedHead : BranchTreeNodeType.Branch, Backend = branch, IsExpanded = false, IsFiltered = isFiltered, @@ -228,14 +234,16 @@ namespace SourceGit.ViewModels { nodes.Sort((l, r) => { + if (l.Type == BranchTreeNodeType.DetachedHead) + { + return -1; + } if (l.Type == r.Type) { return l.Name.CompareTo(r.Name); } - else - { - return (int)l.Type - (int)r.Type; - } + + return (int)l.Type - (int)r.Type; }); foreach (var node in nodes) diff --git a/src/ViewModels/CheckoutCommit.cs b/src/ViewModels/CheckoutCommit.cs new file mode 100644 index 00000000..e27537ac --- /dev/null +++ b/src/ViewModels/CheckoutCommit.cs @@ -0,0 +1,87 @@ +using System.Threading.Tasks; + +namespace SourceGit.ViewModels +{ + public class CheckoutCommit: Popup + { + public string Commit + { + get; + private set; + } + + public bool HasLocalChanges + { + get => _repo.WorkingCopyChangesCount > 0; + } + + public bool AutoStash + { + get => _autoStash; + set => SetProperty(ref _autoStash, value); + } + + public CheckoutCommit(Repository repo, string commit) + { + _repo = repo; + Commit = commit; + View = new Views.CheckoutCommit() { DataContext = this }; + } + + public override Task Sure() + { + _repo.SetWatcherEnabled(false); + ProgressDescription = $"Checkout Commit '{Commit}' ..."; + + return Task.Run(() => + { + var needPopStash = false; + if (HasLocalChanges) + { + if (AutoStash) + { + SetProgressDescription("Adding untracked changes ..."); + var succ = new Commands.Add(_repo.FullPath).Exec(); + if (succ) + { + SetProgressDescription("Stash local changes ..."); + succ = new Commands.Stash(_repo.FullPath).Push("CHECKOUT_AUTO_STASH"); + } + + if (!succ) + { + CallUIThread(() => _repo.SetWatcherEnabled(true)); + return false; + } + + needPopStash = true; + } + else + { + SetProgressDescription("Discard local changes ..."); + Commands.Discard.All(_repo.FullPath); + } + } + + SetProgressDescription("Checkout commit ..."); + var rs = new Commands.Checkout(_repo.FullPath).Commit(Commit, SetProgressDescription); + + if (needPopStash) + { + SetProgressDescription("Re-apply local changes..."); + rs = new Commands.Stash(_repo.FullPath).Apply("stash@{0}"); + if (rs) + { + rs = new Commands.Stash(_repo.FullPath).Drop("stash@{0}"); + } + } + + CallUIThread(() => _repo.SetWatcherEnabled(true)); + return rs; + }); + } + + private readonly Repository _repo = null; + private bool _autoStash = true; + } +} diff --git a/src/ViewModels/Histories.cs b/src/ViewModels/Histories.cs index 8cab66c1..3065bcb0 100644 --- a/src/ViewModels/Histories.cs +++ b/src/ViewModels/Histories.cs @@ -1,9 +1,11 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.Threading.Tasks; using Avalonia.Controls; +using Avalonia.Data.Converters; using Avalonia.Platform.Storage; using Avalonia.Threading; @@ -234,6 +236,20 @@ namespace SourceGit.ViewModels e.Handled = true; }; menu.Items.Add(reset); + + var checkoutCommit = new MenuItem(); + + var shortSha = Converters.StringConverters.ToShortSHA + .Convert(commit.SHA, typeof(string), null, CultureInfo.CurrentCulture); + + checkoutCommit.Header = new Views.NameHighlightedTextBlock("CommitCM.Checkout", shortSha); + checkoutCommit.Icon = App.CreateMenuIcon("Icons.Check"); + checkoutCommit.Click += (o, e) => + { + _repo.CheckoutCommit(commit.SHA); + e.Handled = true; + }; + menu.Items.Add(checkoutCommit); } else { diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index b06ed029..d8e5c766 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -722,6 +722,14 @@ namespace SourceGit.ViewModels PopupHost.ShowAndStartPopup(new Checkout(this, branch)); } + public void CheckoutCommit(string commit) + { + if (!PopupHost.CanCreatePopup()) + return; + + PopupHost.ShowPopup(new CheckoutCommit(this, commit)); + } + public void DeleteMultipleBranches(List branches, bool isLocal) { if (PopupHost.CanCreatePopup()) diff --git a/src/Views/CheckoutCommit.axaml b/src/Views/CheckoutCommit.axaml new file mode 100644 index 00000000..70bce62f --- /dev/null +++ b/src/Views/CheckoutCommit.axaml @@ -0,0 +1,45 @@ + + + + + + + + + + () + + + + + + + + + + + + + + diff --git a/src/Views/CheckoutCommit.axaml.cs b/src/Views/CheckoutCommit.axaml.cs new file mode 100644 index 00000000..f44fd6c7 --- /dev/null +++ b/src/Views/CheckoutCommit.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public partial class CheckoutCommit : UserControl + { + public bool HasLocalChanges; + public CheckoutCommit() + { + InitializeComponent(); + } + } +}