Procházet zdrojové kódy

Make git remote first

Tim Jones před 3 týdny
rodič
revize
923e393898

+ 32 - 19
RackPeek.Domain/Git/LibGit2GitRepository.cs

@@ -6,6 +6,7 @@ namespace RackPeek.Domain.Git;
 public interface IGitCredentialsProvider {
     CredentialsHandler GetHandler();
 }
+
 public sealed class GitHubTokenCredentialsProvider(string username, string token) : IGitCredentialsProvider {
     private readonly string _username = username ?? throw new ArgumentNullException(nameof(username));
     private readonly string _token = token ?? throw new ArgumentNullException(nameof(token));
@@ -29,6 +30,15 @@ public sealed class LibGit2GitRepository(
 
     public void Init() {
         Repository.Init(configDirectory);
+
+        using Repository repo = OpenRepo();
+
+        // create main branch if it doesn't exist
+        if (repo.Branches["main"] == null) {
+            Branch? branch = repo.CreateBranch("main");
+            Commands.Checkout(repo, branch);
+        }
+
         _isAvailable = true;
     }
 
@@ -41,7 +51,8 @@ public sealed class LibGit2GitRepository(
         return new Signature(name, email, DateTimeOffset.Now);
     }
 
-    private static Remote GetRemote(Repository repo) => repo.Network.Remotes["origin"] ?? repo.Network.Remotes.First();
+    private static Remote GetRemote(Repository repo)
+        => repo.Network.Remotes["origin"] ?? repo.Network.Remotes.First();
 
     public GitRepoStatus GetStatus() {
         if (!_isAvailable)
@@ -146,14 +157,14 @@ public sealed class LibGit2GitRepository(
             new FetchOptions { CredentialsProvider = _credentials },
             null);
 
-        Branch? tracking = repo.Head.TrackedBranch;
+        Branch? remoteBranch = repo.Branches[$"{remote.Name}/{repo.Head.FriendlyName}"];
 
-        if (tracking is null)
+        if (remoteBranch == null)
             return new(repo.Commits.Count(), 0, true);
 
-        HistoryDivergence divergence = repo.ObjectDatabase.CalculateHistoryDivergence(
+        HistoryDivergence? divergence = repo.ObjectDatabase.CalculateHistoryDivergence(
             repo.Head.Tip,
-            tracking.Tip);
+            remoteBranch.Tip);
 
         return new(
             divergence.AheadBy ?? 0,
@@ -161,34 +172,29 @@ public sealed class LibGit2GitRepository(
             true);
     }
 
-    public void Push()
-    {
+    public void Push() {
         using Repository repo = OpenRepo();
-    
+
         Remote remote = GetRemote(repo);
         var branch = repo.Head.FriendlyName;
         var refSpec = $"refs/heads/{branch}:refs/heads/{branch}";
-    
-        try
-        {
+
+        try {
             repo.Network.Push(
                 remote,
                 refSpec,
                 new PushOptions { CredentialsProvider = _credentials });
         }
-        catch (LibGit2Sharp.NonFastForwardException)
-        {
-            // remote has commits we don't have
-            Pull();
-    
+        catch (NonFastForwardException) {
+            PullInternal(repo);
+
             repo.Network.Push(
                 remote,
                 refSpec,
                 new PushOptions { CredentialsProvider = _credentials });
         }
-    
-        if (repo.Head.TrackedBranch is null)
-        {
+
+        if (repo.Head.TrackedBranch is null) {
             repo.Branches.Update(repo.Head,
                 b => b.TrackedBranch = $"refs/remotes/{remote.Name}/{branch}");
         }
@@ -196,7 +202,10 @@ public sealed class LibGit2GitRepository(
 
     public void Pull() {
         using Repository repo = OpenRepo();
+        PullInternal(repo);
+    }
 
+    private void PullInternal(Repository repo) {
         Commands.Pull(
             repo,
             GetSignature(repo),
@@ -212,6 +221,10 @@ public sealed class LibGit2GitRepository(
 
     public void AddRemote(string name, string url) {
         using Repository repo = OpenRepo();
+
+        if (repo.Network.Remotes[name] != null)
+            return;
+
         repo.Network.Remotes.Add(name, url);
     }
 

+ 137 - 293
Shared.Rcl/Layout/GitStatusIndicator.razor

@@ -35,6 +35,7 @@
                 Enable Git
             </button>
         }
+
         @if (_errorMessage is not null)
         {
             <span class="text-red-400 text-xs" data-testid="git-error">@_errorMessage</span>
@@ -45,8 +46,7 @@ else
 {
     <div class="relative flex items-center gap-2 text-sm" data-testid="git-status-indicator">
 
-        @* Branch name — clickable to open history *@
-        @if (!string.IsNullOrEmpty(_branch))
+        @if (!string.IsNullOrEmpty(_branch) && _hasRemote)
         {
             <button class="text-zinc-400 text-xs hover:text-emerald-400 transition"
                     data-testid="git-branch"
@@ -55,33 +55,32 @@ else
             </button>
         }
 
-        @if (_status == GitRepoStatus.Clean)
+        @if (_status == GitRepoStatus.Clean && _hasRemote)
         {
             <span class="inline-block w-2 h-2 rounded-full bg-emerald-400"
                   data-testid="git-status-dot-clean"
-                  title="All changes committed">
-            </span>
+                  title="All changes committed"></span>
+
             <span class="text-zinc-500 text-xs" data-testid="git-status-text">
                 Saved
             </span>
         }
-        else if (_status == GitRepoStatus.Dirty)
+        else if (_status == GitRepoStatus.Dirty && _hasRemote)
         {
             <span class="inline-block w-2 h-2 rounded-full bg-amber-400 animate-pulse"
                   data-testid="git-status-dot-dirty"
-                  title="Uncommitted changes">
-            </span>
+                  title="Uncommitted changes"></span>
 
-            @* Save button with dropdown toggle *@
             <div class="relative flex" data-testid="git-save-group">
                 <button class="px-2 py-1 text-xs rounded-l bg-emerald-600 hover:bg-emerald-500 text-white transition disabled:opacity-50"
-                        disabled="@_isBusy"
+                        disabled="@(_isBusy || !_hasRemote)"
                         data-testid="git-save-button"
                         @onclick="CommitAsync">
                     @(_isCommitting ? "Saving..." : "Save")
                 </button>
+
                 <button class="px-1.5 py-1 text-xs rounded-r bg-emerald-700 hover:bg-emerald-600 text-white transition border-l border-emerald-800 disabled:opacity-50"
-                        disabled="@_isBusy"
+                        disabled="@(_isBusy || !_hasRemote)"
                         data-testid="git-save-dropdown"
                         @onclick="ToggleDropdown">
                     &#9662;
@@ -91,11 +90,13 @@ else
                 {
                     <div class="absolute top-full right-0 mt-1 bg-zinc-800 border border-zinc-700 rounded shadow-lg z-50 min-w-[120px]"
                          data-testid="git-dropdown-menu">
+
                         <button class="w-full text-left px-3 py-2 text-xs text-zinc-300 hover:bg-zinc-700 hover:text-white transition"
                                 data-testid="git-diff-button"
                                 @onclick="OpenDiffAsync">
                             Diff
                         </button>
+
                         @if (!_confirmDiscard)
                         {
                             <button class="w-full text-left px-3 py-2 text-xs text-zinc-400 hover:bg-zinc-700 hover:text-red-400 transition"
@@ -108,11 +109,13 @@ else
                         {
                             <div class="px-3 py-2 flex items-center gap-2">
                                 <span class="text-red-400 text-xs">Sure?</span>
+
                                 <button class="px-2 py-0.5 text-xs rounded bg-red-600 hover:bg-red-500 text-white transition"
                                         data-testid="git-discard-confirm"
                                         @onclick="DiscardAsync">
                                     Yes
                                 </button>
+
                                 <button class="px-2 py-0.5 text-xs rounded text-zinc-400 hover:text-white transition"
                                         data-testid="git-discard-cancel"
                                         @onclick="() => _confirmDiscard = false">
@@ -125,10 +128,10 @@ else
             </div>
         }
 
-        @* Sync / Remote section *@
         @if (!_hasRemote)
         {
             <div class="relative flex items-center gap-1" data-testid="git-remote-group">
+
                 @if (_showAddRemote)
                 {
                     <input type="text"
@@ -137,12 +140,14 @@ else
                            @bind="_remoteUrl"
                            @bind:event="oninput"
                            data-testid="git-remote-url" />
+
                     <button class="px-2 py-0.5 text-xs rounded bg-emerald-600 hover:bg-emerald-500 text-white transition disabled:opacity-50"
                             disabled="@(string.IsNullOrWhiteSpace(_remoteUrl))"
                             data-testid="git-remote-save"
                             @onclick="AddRemoteAsync">
                         Add
                     </button>
+
                     <button class="px-2 py-0.5 text-xs rounded text-zinc-400 hover:text-white transition"
                             data-testid="git-remote-cancel"
                             @onclick="CancelAddRemote">
@@ -157,15 +162,21 @@ else
                         Add Remote
                     </button>
                 }
+
+                <span class="text-xs text-zinc-500">
+                    Connect a remote repository to enable saving and sync.
+                </span>
             </div>
         }
-        else if (_hasRemote)
+        else
         {
             <div class="relative flex" data-testid="git-sync-group">
+
                 <button class="px-2 py-1 text-xs rounded text-zinc-400 hover:text-white hover:bg-zinc-700 transition disabled:opacity-50"
                         disabled="@_isSyncing"
                         data-testid="git-sync-button"
                         @onclick="ToggleSyncAsync">
+
                     @if (_isFetching)
                     {
                         <span>Checking...</span>
@@ -188,32 +199,14 @@ else
                 {
                     <div class="absolute top-full right-0 mt-1 bg-zinc-800 border border-zinc-700 rounded shadow-lg z-50 min-w-[140px]"
                          data-testid="git-sync-dropdown">
-                        @if (_syncStatus.Error is not null)
-                        {
-                            <div class="px-3 py-2 text-xs text-red-400 border-b border-zinc-700">
-                                Fetch failed
-                            </div>
-                        }
-                        else if (_syncStatus.Ahead > 0 || _syncStatus.Behind > 0)
-                        {
-                            <div class="px-3 py-2 text-xs text-zinc-500 border-b border-zinc-700">
-                                @if (_syncStatus.Ahead > 0) { <span class="text-emerald-400">↑@_syncStatus.Ahead ahead</span> }
-                                @if (_syncStatus.Ahead > 0 && _syncStatus.Behind > 0) { <span> · </span> }
-                                @if (_syncStatus.Behind > 0) { <span class="text-blue-400">↓@_syncStatus.Behind behind</span> }
-                            </div>
-                        }
-                        else
-                        {
-                            <div class="px-3 py-2 text-xs text-zinc-500 border-b border-zinc-700">
-                                Up to date
-                            </div>
-                        }
+
                         <button class="w-full text-left px-3 py-2 text-xs text-zinc-300 hover:bg-zinc-700 hover:text-white transition disabled:opacity-50"
-                                disabled="@(_isSyncing)"
+                                disabled="@_isSyncing"
                                 data-testid="git-push-button"
                                 @onclick="PushAsync">
                             @(_isPushing ? "Pushing..." : "Push")
                         </button>
+
                         <button class="w-full text-left px-3 py-2 text-xs text-zinc-300 hover:bg-zinc-700 hover:text-white transition disabled:opacity-50"
                                 disabled="@(_isSyncing || _syncStatus.Error is not null)"
                                 data-testid="git-pull-button"
@@ -227,147 +220,39 @@ else
 
         @if (_errorMessage is not null)
         {
-            <span class="text-red-400 text-xs" data-testid="git-error">
-                @_errorMessage
-            </span>
+            <span class="text-red-400 text-xs">@_errorMessage</span>
         }
     </div>
-
-    @* Dropdown backdrop — closes all dropdowns on outside click *@
-    @if (_showDropdown || _showSyncDropdown)
-    {
-        <div class="fixed inset-0 z-40" @onclick="CloseAllDropdowns"></div>
-    }
-
-    @* Diff Modal *@
-    @if (_showDiff)
-    {
-        <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
-             data-testid="git-diff-overlay"
-             @onclick="CloseDiff">
-            <div class="bg-zinc-900 border border-zinc-700 rounded-lg shadow-xl w-[90vw] max-w-4xl max-h-[80vh] flex flex-col"
-                 @onclick:stopPropagation="true">
-
-                <div class="flex items-center justify-between px-4 py-3 border-b border-zinc-700">
-                    <div class="flex items-center gap-3">
-                        <span class="text-sm font-semibold text-zinc-200">Changes</span>
-                        <span class="text-xs text-zinc-500">@_changedFiles.Length file(s)</span>
-                    </div>
-                    <button class="text-zinc-400 hover:text-white transition text-lg"
-                            data-testid="git-diff-close"
-                            @onclick="CloseDiff">
-                        &times;
-                    </button>
-                </div>
-
-                @if (_changedFiles.Length > 0)
-                {
-                    <div class="px-4 py-2 border-b border-zinc-800 flex flex-wrap gap-2">
-                        @foreach (var file in _changedFiles)
-                        {
-                            var status = file.Length >= 2 ? file[..2].Trim() : "?";
-                            var name = file.Length >= 3 ? file[3..] : file;
-                            var color = status switch
-                            {
-                                "M" => "text-amber-400",
-                                "A" or "?" => "text-emerald-400",
-                                "D" => "text-red-400",
-                                _ => "text-zinc-400"
-                            };
-                            <span class="text-xs">
-                                <span class="@color font-bold">@status</span>
-                                <span class="text-zinc-300">@name</span>
-                            </span>
-                        }
-                    </div>
-                }
-
-                <div class="overflow-auto flex-1 p-4">
-                    <pre class="text-xs leading-relaxed whitespace-pre-wrap">@((MarkupString)FormatDiff(_diffContent))</pre>
-                </div>
-            </div>
-        </div>
-    }
-
-    @* History Modal *@
-    @if (_showHistory)
-    {
-        <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60"
-             data-testid="git-history-overlay"
-             @onclick="CloseHistory">
-            <div class="bg-zinc-900 border border-zinc-700 rounded-lg shadow-xl w-[90vw] max-w-3xl max-h-[80vh] flex flex-col"
-                 @onclick:stopPropagation="true">
-
-                <div class="flex items-center justify-between px-4 py-3 border-b border-zinc-700">
-                    <div class="flex items-center gap-3">
-                        <span class="text-sm font-semibold text-zinc-200">History</span>
-                        <span class="text-xs text-zinc-500">@_branch</span>
-                        <span class="text-xs text-zinc-600">@_logEntries.Length commits</span>
-                    </div>
-                    <button class="text-zinc-400 hover:text-white transition text-lg"
-                            data-testid="git-history-close"
-                            @onclick="CloseHistory">
-                        &times;
-                    </button>
-                </div>
-
-                <div class="overflow-auto flex-1">
-                    @if (_logEntries.Length == 0)
-                    {
-                        <div class="p-4 text-zinc-500 text-sm">No commits yet.</div>
-                    }
-                    else
-                    {
-                        <div class="divide-y divide-zinc-800">
-                            @foreach (var entry in _logEntries)
-                            {
-                                <div class="px-4 py-3 hover:bg-zinc-800/50 transition" data-testid="git-log-entry">
-                                    <div class="flex items-center gap-3">
-                                        <span class="text-xs text-emerald-400 font-mono shrink-0">@entry.Hash</span>
-                                        <span class="text-sm text-zinc-200 truncate">@entry.Message</span>
-                                    </div>
-                                    <div class="flex items-center gap-3 mt-1">
-                                        <span class="text-xs text-zinc-500">@entry.Author</span>
-                                        <span class="text-xs text-zinc-600">@entry.Date</span>
-                                    </div>
-                                </div>
-                            }
-                        </div>
-                    }
-                </div>
-            </div>
-        </div>
-    }
 }
 
 @code {
+
     private GitRepoStatus _status = GitRepoStatus.NotAvailable;
-    private string _branch = string.Empty;
+    private string _branch = "";
     private bool _isCommitting;
     private bool _isRestoring;
     private bool _confirmDiscard;
     private bool _showDropdown;
-    private string? _errorMessage;
-    private PeriodicTimer? _timer;
-    private CancellationTokenSource? _cts;
-
-    private bool _showDiff;
-    private string _diffContent = string.Empty;
-    private string[] _changedFiles = [];
-
+    private bool _showAddRemote;
+    private bool _showSyncDropdown;
     private bool _showHistory;
-    private GitLogEntry[] _logEntries = [];
-
     private bool _hasRemote;
-    private GitSyncStatus _syncStatus = new(0, 0, false);
-    private bool _showSyncDropdown;
     private bool _isFetching;
     private bool _isPushing;
     private bool _isPulling;
     private bool _isInitializing;
     private bool _confirmInit;
-    private bool _showAddRemote;
-    private string _remoteUrl = string.Empty;
+
+    private string? _errorMessage;
+    private string _remoteUrl = "";
+    private string _diffContent = "";
+    private string[] _changedFiles = [];
+    private GitLogEntry[] _logEntries = [];
+
+    private PeriodicTimer? _timer;
+    private CancellationTokenSource? _cts;
+
+    private GitSyncStatus _syncStatus = new(0,0,false);
 
     private bool _isBusy => _isCommitting || _isRestoring || _isSyncing;
     private bool _isSyncing => _isPushing || _isPulling || _isFetching;
@@ -382,11 +267,9 @@ else
         _branch = GitRepo.GetCurrentBranch();
         _hasRemote = GitRepo.HasRemote();
 
-        _cts?.Cancel();
-        _timer?.Dispose();
-        
         _cts = new CancellationTokenSource();
-        _timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
+        _timer = new PeriodicTimer(TimeSpan.FromSeconds(15));
+
         _ = PollStatusAsync(_cts.Token);
 
         await Task.CompletedTask;
@@ -406,16 +289,11 @@ else
                 if (newStatus != _status)
                 {
                     _status = newStatus;
-                    _confirmDiscard = false;
-                    _showDropdown = false;
-
                     await InvokeAsync(StateHasChanged);
                 }
             }
         }
-        catch (OperationCanceledException)
-        {
-        }
+        catch (OperationCanceledException) {}
     }
 
     private async Task InitRepoAsync()
@@ -426,8 +304,7 @@ else
         try
         {
             var error = await InitRepo.ExecuteAsync();
-
-            if (error is not null)
+            if (error != null)
             {
                 _errorMessage = error;
                 return;
@@ -436,14 +313,6 @@ else
             _status = GitRepo.GetStatus();
             _branch = GitRepo.GetCurrentBranch();
             _hasRemote = GitRepo.HasRemote();
-
-            _cts?.Cancel();
-            _timer?.Dispose();
-
-            _cts = new CancellationTokenSource();
-            _timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
-
-            _ = PollStatusAsync(_cts.Token);
         }
         catch (Exception ex)
         {
@@ -458,7 +327,7 @@ else
     private void CancelAddRemote()
     {
         _showAddRemote = false;
-        _remoteUrl = string.Empty;
+        _remoteUrl = "";
     }
 
     private async Task AddRemoteAsync()
@@ -468,7 +337,8 @@ else
         try
         {
             var error = await AddRemoteUseCase.ExecuteAsync(_remoteUrl);
-            if (error is not null)
+
+            if (error != null)
             {
                 _errorMessage = error;
                 return;
@@ -476,7 +346,12 @@ else
 
             _hasRemote = true;
             _showAddRemote = false;
-            _remoteUrl = string.Empty;
+            _remoteUrl = "";
+
+            _syncStatus = GitRepo.FetchAndGetSyncStatus();
+
+            if (_syncStatus.Behind > 0)
+                await PullAsync();
         }
         catch (Exception ex)
         {
@@ -484,26 +359,30 @@ else
         }
     }
 
-        private async Task CommitAsync()
+    private async Task CommitAsync()
     {
+        if (!_hasRemote)
+        {
+            _errorMessage = "Add a remote repository before saving.";
+            return;
+        }
+
         _errorMessage = null;
         _isCommitting = true;
-        _confirmDiscard = false;
-        _showDropdown = false;
 
         try
         {
             var error = await CommitAll.ExecuteAsync(
                 $"rackpeek: save config {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC");
 
-            if (error is not null)
+            if (error != null)
                 _errorMessage = error;
 
             _status = GitRepo.GetStatus();
         }
         catch (Exception ex)
         {
-            _errorMessage = $"Unexpected error: {ex.Message}";
+            _errorMessage = $"Commit error: {ex.Message}";
         }
         finally
         {
@@ -515,182 +394,147 @@ else
     {
         _showDropdown = !_showDropdown;
         _showSyncDropdown = false;
-        _confirmDiscard = false;
     }
 
     private void ToggleSyncAsync()
     {
+        _showSyncDropdown = !_showSyncDropdown;
+        _showDropdown = false;
+
         if (_showSyncDropdown)
         {
-            _showSyncDropdown = false;
+            _isFetching = true;
+
+            try
+            {
+                _syncStatus = GitRepo.FetchAndGetSyncStatus();
+            }
+            finally
+            {
+                _isFetching = false;
+            }
+        }
+    }
+
+    private async Task PushAsync()
+    {
+        if (!_hasRemote)
+        {
+            _errorMessage = "Add a remote first.";
             return;
         }
 
-        _showDropdown = false;
         _errorMessage = null;
-        _isFetching = true;
-        _showSyncDropdown = true;
+        _isPushing = true;
 
         try
         {
+            var error = await PushUseCase.ExecuteAsync();
+
+            if (error != null)
+                _errorMessage = error;
+
             _syncStatus = GitRepo.FetchAndGetSyncStatus();
-            if (_syncStatus.Error is not null)
-                _errorMessage = _syncStatus.Error;
         }
         catch (Exception ex)
         {
-            _errorMessage = $"Fetch error: {ex.Message}";
+            _errorMessage = $"Push error: {ex.Message}";
         }
         finally
         {
-            _isFetching = false;
-        }
-    }
-
-    private void CloseAllDropdowns()
-    {
-        _showDropdown = false;
-        _showSyncDropdown = false;
-        _confirmDiscard = false;
-    }
-
-    private void OpenDiffAsync()
-    {
-        _showDropdown = false;
-        _changedFiles = GitRepo.GetChangedFiles();
-        _diffContent = GitRepo.GetDiff();
-        _showDiff = true;
-    }
-
-    private void CloseDiff()
-    {
-        _showDiff = false;
-        _diffContent = string.Empty;
-        _changedFiles = [];
-    }
-
-    private void ToggleHistoryAsync()
-    {
-        if (_showHistory)
-        {
-            CloseHistory();
-            return;
+            _isPushing = false;
         }
-
-        _showDropdown = false;
-        _showSyncDropdown = false;
-        _logEntries = GitRepo.GetLog(20);
-        _showHistory = true;
     }
 
-    private void CloseHistory()
-    {
-        _showHistory = false;
-        _logEntries = [];
-    }
-
-    private async Task DiscardAsync()
+    private async Task PullAsync()
     {
         _errorMessage = null;
-        _isRestoring = true;
-        _confirmDiscard = false;
-        _showDropdown = false;
+        _isPulling = true;
 
         try
         {
-            var error = await RestoreAll.ExecuteAsync();
-            if (error is not null)
+            var error = await PullUseCase.ExecuteAsync();
+
+            if (error != null)
                 _errorMessage = error;
 
+            _syncStatus = GitRepo.FetchAndGetSyncStatus();
             _status = GitRepo.GetStatus();
         }
         catch (Exception ex)
         {
-            _errorMessage = $"Unexpected error: {ex.Message}";
+            _errorMessage = $"Pull error: {ex.Message}";
         }
         finally
         {
-            _isRestoring = false;
+            _isPulling = false;
         }
     }
 
-    private async Task PushAsync()
+    public void Dispose()
     {
-        _errorMessage = null;
-        _isPushing = true;
+        _cts?.Cancel();
+        _cts?.Dispose();
+        _timer?.Dispose();
+    }
+    
+    private void OpenDiffAsync()
+    {
+        _showDropdown = false;
 
         try
         {
-            var error = await PushUseCase.ExecuteAsync();
-            if (error is not null)
-                _errorMessage = error;
-
-            _syncStatus = GitRepo.FetchAndGetSyncStatus();
+            _changedFiles = GitRepo.GetChangedFiles();
+            _diffContent = GitRepo.GetDiff();
         }
         catch (Exception ex)
         {
-            _errorMessage = $"Push error: {ex.Message}";
-        }
-        finally
-        {
-            _isPushing = false;
+            _errorMessage = $"Diff error: {ex.Message}";
         }
     }
 
-    private async Task PullAsync()
+    private async Task DiscardAsync()
     {
         _errorMessage = null;
-        _isPulling = true;
+        _isRestoring = true;
+        _confirmDiscard = false;
+        _showDropdown = false;
 
         try
         {
-            var error = await PullUseCase.ExecuteAsync();
-            if (error is not null)
+            var error = await RestoreAll.ExecuteAsync();
+
+            if (error != null)
                 _errorMessage = error;
 
-            _syncStatus = GitRepo.FetchAndGetSyncStatus();
             _status = GitRepo.GetStatus();
         }
         catch (Exception ex)
         {
-            _errorMessage = $"Pull error: {ex.Message}";
+            _errorMessage = $"Discard error: {ex.Message}";
         }
         finally
         {
-            _isPulling = false;
+            _isRestoring = false;
         }
     }
 
-    private static string FormatDiff(string diff)
+    private void ToggleHistoryAsync()
     {
-        if (string.IsNullOrWhiteSpace(diff))
-            return "<span class=\"text-zinc-500\">No diff available</span>";
-
-        var lines = diff.Split('\n');
-        var sb = new System.Text.StringBuilder();
-
-        foreach (var line in lines)
+        if (_showHistory)
         {
-            var escaped = System.Net.WebUtility.HtmlEncode(line);
-            if (line.StartsWith('+') && !line.StartsWith("+++"))
-                sb.AppendLine($"<span class=\"text-emerald-400\">{escaped}</span>");
-            else if (line.StartsWith('-') && !line.StartsWith("---"))
-                sb.AppendLine($"<span class=\"text-red-400\">{escaped}</span>");
-            else if (line.StartsWith("@@"))
-                sb.AppendLine($"<span class=\"text-blue-400\">{escaped}</span>");
-            else if (line.StartsWith("diff "))
-                sb.AppendLine($"<span class=\"text-amber-300 font-bold\">{escaped}</span>");
-            else
-                sb.AppendLine($"<span class=\"text-zinc-400\">{escaped}</span>");
+            _showHistory = false;
+            return;
         }
 
-        return sb.ToString();
-    }
-
-    public void Dispose()
-    {
-        _cts?.Cancel();
-        _cts?.Dispose();
-        _timer?.Dispose();
+        try
+        {
+            _logEntries = GitRepo.GetLog(20);
+            _showHistory = true;
+        }
+        catch (Exception ex)
+        {
+            _errorMessage = $"History error: {ex.Message}";
+        }
     }
-}
+}