feature: warn users when checking out new branch with commits not connected to any branches (#1463)

Signed-off-by: leo <longshuang@msn.cn>
This commit is contained in:
leo 2025-06-26 12:27:09 +08:00
parent 67efe76b97
commit 0e22503b22
No known key found for this signature in database
14 changed files with 130 additions and 53 deletions

View file

@ -89,6 +89,7 @@
<x:String x:Key="Text.Checkout.LocalChanges.StashAndReply" xml:space="preserve">Stash &amp; Reapply</x:String>
<x:String x:Key="Text.Checkout.RecurseSubmodules" xml:space="preserve">Update all submodules</x:String>
<x:String x:Key="Text.Checkout.Target" xml:space="preserve">Branch:</x:String>
<x:String x:Key="Text.Checkout.WarnLostCommits" xml:space="preserve">Your current HEAD contains commit(s) not connected to any branches/tags! Do you want to continue?</x:String>
<x:String x:Key="Text.Checkout.WithFastForward" xml:space="preserve">Checkout &amp; Fast-Forward</x:String>
<x:String x:Key="Text.Checkout.WithFastForward.Upstream" xml:space="preserve">Fast-Forward to:</x:String>
<x:String x:Key="Text.CherryPick" xml:space="preserve">Cherry Pick</x:String>

View file

@ -93,6 +93,7 @@
<x:String x:Key="Text.Checkout.LocalChanges.StashAndReply" xml:space="preserve">贮藏并自动恢复</x:String>
<x:String x:Key="Text.Checkout.RecurseSubmodules" xml:space="preserve">同时更新所有子模块</x:String>
<x:String x:Key="Text.Checkout.Target" xml:space="preserve">目标分支 </x:String>
<x:String x:Key="Text.Checkout.WarnLostCommits" xml:space="preserve">您当前游离的HEAD包含未被任何分支及标签引用的提交是否继续</x:String>
<x:String x:Key="Text.Checkout.WithFastForward" xml:space="preserve">检出分支并快进</x:String>
<x:String x:Key="Text.Checkout.WithFastForward.Upstream" xml:space="preserve">上游分支 </x:String>
<x:String x:Key="Text.CherryPick" xml:space="preserve">挑选提交</x:String>

View file

@ -93,6 +93,7 @@
<x:String x:Key="Text.Checkout.LocalChanges.StashAndReply" xml:space="preserve">擱置變更並自動復原</x:String>
<x:String x:Key="Text.Checkout.RecurseSubmodules" xml:space="preserve">同時更新所有子模組</x:String>
<x:String x:Key="Text.Checkout.Target" xml:space="preserve">目標分支:</x:String>
<x:String x:Key="Text.Checkout.WarnLostCommits" xml:space="preserve">您目前的分離的 HEAD 包含與任何分支/標籤無關的提交!您要繼續嗎?</x:String>
<x:String x:Key="Text.Checkout.WithFastForward" xml:space="preserve">簽出分支並快轉</x:String>
<x:String x:Key="Text.Checkout.WithFastForward.Upstream" xml:space="preserve">上游分支 </x:String>
<x:String x:Key="Text.CherryPick" xml:space="preserve">揀選提交</x:String>

View file

@ -47,6 +47,12 @@ namespace SourceGit.ViewModels
var succ = false;
var needPopStash = false;
if (!_repo.ConfirmCheckoutBranch())
{
CallUIThread(() => _repo.SetWatcherEnabled(true));
return true;
}
if (DiscardLocalChanges)
{
succ = new Commands.Checkout(_repo.FullPath).Use(log).Branch(Branch, true);

View file

@ -52,6 +52,12 @@ namespace SourceGit.ViewModels
var succ = false;
var needPopStash = false;
if (!_repo.ConfirmCheckoutBranch())
{
CallUIThread(() => _repo.SetWatcherEnabled(true));
return true;
}
if (DiscardLocalChanges)
{
succ = new Commands.Checkout(_repo.FullPath).Use(log).Branch(LocalBranch.Name, RemoteBranch.Head, true, true);

View file

@ -47,6 +47,12 @@ namespace SourceGit.ViewModels
var succ = false;
var needPop = false;
if (!_repo.ConfirmCheckoutBranch())
{
CallUIThread(() => _repo.SetWatcherEnabled(true));
return true;
}
if (DiscardLocalChanges)
{
succ = new Commands.Checkout(_repo.FullPath).Use(log).Commit(Commit.SHA, true);

31
src/ViewModels/Confirm.cs Normal file
View file

@ -0,0 +1,31 @@
using System;
namespace SourceGit.ViewModels
{
public class Confirm
{
public string Message
{
get;
private set;
}
public Confirm(string message, Action onSure, Action onCancel = null)
{
Message = message;
_onSure = onSure;
_onCancel = onCancel;
}
public void Done(bool isSure)
{
if (isSure)
_onSure?.Invoke();
else
_onCancel?.Invoke();
}
private Action _onSure;
private Action _onCancel;
}
}

View file

@ -1,26 +0,0 @@
using System;
namespace SourceGit.ViewModels
{
public class ConfirmCommit
{
public string Message
{
get;
private set;
}
public ConfirmCommit(string message, Action onSure)
{
Message = message;
_onSure = onSure;
}
public void Continue()
{
_onSure?.Invoke();
}
private Action _onSure;
}
}

View file

@ -130,6 +130,13 @@ namespace SourceGit.ViewModels
return Task.Run(() =>
{
bool succ = false;
if (CheckoutAfterCreated && !_repo.ConfirmCheckoutBranch())
{
CallUIThread(() => _repo.SetWatcherEnabled(true));
return true;
}
if (CheckoutAfterCreated && !_repo.IsBare)
{
var needPopStash = false;

View file

@ -1330,6 +1330,41 @@ namespace SourceGit.ViewModels
ShowPopup(new CreateBranch(this, _currentBranch));
}
public bool ConfirmCheckoutBranch()
{
if (Dispatcher.UIThread.CheckAccess())
return true;
if (_currentBranch is not { IsDetachedHead: true })
return true;
var refs = new Commands.QueryRefsContainsCommit(_fullpath, _currentBranch.Head).Result();
if (refs.Count == 0)
{
var confirmCheckout = false;
var resetEvent = new AutoResetEvent(false);
Dispatcher.UIThread.Invoke(() =>
{
var msg = App.Text("Checkout.WarnLostCommits");
App.ShowWindow(new Confirm(msg, () =>
{
confirmCheckout = true;
resetEvent.Set();
}, () =>
{
confirmCheckout = false;
resetEvent.Set();
}), true);
});
resetEvent.WaitOne();
return confirmCheckout;
}
return true;
}
public void CheckoutBranch(Models.Branch branch)
{
if (branch.IsLocal)

View file

@ -1795,14 +1795,14 @@ namespace SourceGit.ViewModels
if (_repo.CurrentBranch is { IsDetachedHead: true } && checkPassed < CommitCheckPassed.DetachedHead)
{
var msg = App.Text("WorkingCopy.ConfirmCommitWithDetachedHead");
App.ShowWindow(new ConfirmCommit(msg, () => DoCommit(autoStage, autoPush, CommitCheckPassed.DetachedHead)), true);
App.ShowWindow(new Confirm(msg, () => DoCommit(autoStage, autoPush, CommitCheckPassed.DetachedHead)), true);
return;
}
if (!string.IsNullOrEmpty(_filter) && _staged.Count > _visibleStaged.Count && checkPassed < CommitCheckPassed.Filter)
{
var msg = App.Text("WorkingCopy.ConfirmCommitWithFilter", _staged.Count, _visibleStaged.Count, _staged.Count - _visibleStaged.Count);
App.ShowWindow(new ConfirmCommit(msg, () => DoCommit(autoStage, autoPush, CommitCheckPassed.Filter)), true);
App.ShowWindow(new Confirm(msg, () => DoCommit(autoStage, autoPush, CommitCheckPassed.Filter)), true);
return;
}

View file

@ -5,8 +5,8 @@
xmlns:v="using:SourceGit.Views"
xmlns:vm="using:SourceGit.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.ConfirmCommit"
x:DataType="vm:ConfirmCommit"
x:Class="SourceGit.Views.Confirm"
x:DataType="vm:Confirm"
x:Name="ThisControl"
Icon="/App.ico"
Title="{DynamicResource Text.Warn}"

View file

@ -0,0 +1,32 @@
using System;
using Avalonia.Interactivity;
namespace SourceGit.Views
{
public partial class Confirm : ChromelessWindow
{
public Confirm()
{
InitializeComponent();
}
protected override void OnClosed(EventArgs e)
{
(DataContext as ViewModels.Confirm)?.Done(_isOkPressed);
base.OnClosed(e);
}
private void Sure(object _1, RoutedEventArgs _2)
{
_isOkPressed = true;
Close();
}
private void CloseWindow(object _1, RoutedEventArgs _2)
{
Close();
}
private bool _isOkPressed = false;
}
}

View file

@ -1,23 +0,0 @@
using Avalonia.Interactivity;
namespace SourceGit.Views
{
public partial class ConfirmCommit : ChromelessWindow
{
public ConfirmCommit()
{
InitializeComponent();
}
private void Sure(object _1, RoutedEventArgs _2)
{
(DataContext as ViewModels.ConfirmCommit)?.Continue();
Close();
}
private void CloseWindow(object _1, RoutedEventArgs _2)
{
Close();
}
}
}