diff --git a/src/Files.App/Helpers/DynamicDialogFactory.cs b/src/Files.App/Helpers/DynamicDialogFactory.cs index 24ec25803493..0376ff5972e9 100644 --- a/src/Files.App/Helpers/DynamicDialogFactory.cs +++ b/src/Files.App/Helpers/DynamicDialogFactory.cs @@ -1,19 +1,11 @@ // Copyright (c) 2023 Files Community // Licensed under the MIT License. See the LICENSE. -using CommunityToolkit.WinUI; using Files.App.Dialogs; -using Files.App.Extensions; -using Files.App.Filesystem; using Files.App.ViewModels.Dialogs; -using Files.Shared.Enums; -using Files.Shared.Extensions; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Data; -using System; -using System.Collections.Generic; -using System.Linq; using Windows.System; namespace Files.App.Helpers @@ -224,5 +216,51 @@ public static DynamicDialog GetFor_CredentialEntryDialog(string path) return dialog; } + + public static DynamicDialog GetFor_GitCheckoutConflicts(string checkoutBranchName, string headBranchName) + { + DynamicDialog dialog = null!; + + var optionsListView = new ListView() + { + ItemsSource = new string[] + { + string.Format("BringChanges".GetLocalizedResource(), checkoutBranchName), + string.Format("StashChanges".GetLocalizedResource(), headBranchName), + "DiscardChanges".GetLocalizedResource() + }, + SelectionMode = ListViewSelectionMode.Single + }; + optionsListView.SelectedIndex = 0; + + optionsListView.SelectionChanged += (listView, args) => + { + dialog.ViewModel.AdditionalData = (GitCheckoutOptions)optionsListView.SelectedIndex; + }; + + dialog = new DynamicDialog(new DynamicDialogViewModel() + { + TitleText = "SwitchBranch".GetLocalizedResource(), + PrimaryButtonText = "Switch".GetLocalizedResource(), + CloseButtonText = "Cancel".GetLocalizedResource(), + SubtitleText = "UncommittedChanges".GetLocalizedResource(), + DisplayControl = new Grid() + { + MinWidth = 250d, + Children = + { + optionsListView + } + }, + AdditionalData = GitCheckoutOptions.BringChanges, + CloseButtonAction = (vm, e) => + { + dialog.ViewModel.AdditionalData = GitCheckoutOptions.None; + vm.HideDialog(); + } + }); + + return dialog; + } } } diff --git a/src/Files.App/Helpers/GitHelpers.cs b/src/Files.App/Helpers/GitHelpers.cs index e97886564729..0a5e0268380c 100644 --- a/src/Files.App/Helpers/GitHelpers.cs +++ b/src/Files.App/Helpers/GitHelpers.cs @@ -1,8 +1,9 @@ // Copyright (c) 2023 Files Community // Licensed under the MIT License. See the LICENSE. -using LibGit2Sharp; using Files.App.Filesystem.StorageItems; +using LibGit2Sharp; +using Microsoft.AppCenter.Analytics; namespace Files.App.Helpers { @@ -32,5 +33,67 @@ public static class GitHelpers return null; } } + + public static string[] GetLocalBranchesNames(string? path) + { + if (string.IsNullOrWhiteSpace(path) || !Repository.IsValid(path)) + return Array.Empty(); + + using var repository = new Repository(path); + return repository.Branches + .Where(b => !b.IsRemote) + .OrderByDescending(b => b.IsCurrentRepositoryHead) + .ThenByDescending(b => b.Tip.Committer.When) + .Select(b => b.FriendlyName) + .ToArray(); + } + + public static async Task Checkout(string? repositoryPath, string? branch) + { + if (string.IsNullOrWhiteSpace(repositoryPath) || !Repository.IsValid(repositoryPath)) + return false; + + using var repository = new Repository(repositoryPath); + var checkoutBranch = repository.Branches[branch]; + if (checkoutBranch is null) + return false; + + var options = new CheckoutOptions(); + var isBringingChanges = false; + + Analytics.TrackEvent($"Triggered git checkout"); + + if (repository.RetrieveStatus().IsDirty) + { + var dialog = DynamicDialogFactory.GetFor_GitCheckoutConflicts(checkoutBranch.FriendlyName, repository.Head.FriendlyName); + await dialog.ShowAsync(); + + var resolveConflictOption = (GitCheckoutOptions)dialog.ViewModel.AdditionalData; + + switch (resolveConflictOption) + { + case GitCheckoutOptions.None: + return false; + case GitCheckoutOptions.DiscardChanges: + options.CheckoutModifiers = CheckoutModifiers.Force; + break; + case GitCheckoutOptions.BringChanges: + case GitCheckoutOptions.StashChanges: + repository.Stashes.Add(repository.Config.BuildSignature(DateTimeOffset.Now)); + + isBringingChanges = resolveConflictOption is GitCheckoutOptions.BringChanges; + break; + } + } + + LibGit2Sharp.Commands.Checkout(repository, checkoutBranch, options); + + if (isBringingChanges) + { + var lastStashIndex = repository.Stashes.Count() - 1; + repository.Stashes.Pop(lastStashIndex, new StashApplyOptions()); + } + return true; + } } } diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw index f7d31f11612d..7b1e785ebea2 100644 --- a/src/Files.App/Strings/en-US/Resources.resw +++ b/src/Files.App/Strings/en-US/Resources.resw @@ -3262,4 +3262,28 @@ Unable to display permissions. + + Leave my changes on '{0}' + + + Discard my changes + + + Bring my changes to '{0}' + + + You have uncommitted changes on this branch. What would you like to do with them? + + + Switch Branch + + + Branches + + + Switch + + + New branch + \ No newline at end of file diff --git a/src/Files.App/UserControls/StatusBarControl.xaml b/src/Files.App/UserControls/StatusBarControl.xaml index 2576610767ae..e71d3a25bf54 100644 --- a/src/Files.App/UserControls/StatusBarControl.xaml +++ b/src/Files.App/UserControls/StatusBarControl.xaml @@ -5,6 +5,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:converters="using:Files.App.Converters" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:helpers="using:Files.App.Helpers" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" d:DesignHeight="32" d:DesignWidth="400" @@ -53,10 +54,63 @@ VerticalAlignment="Center" Orientation="Horizontal" Spacing="8"> - + Background="Transparent" + BorderThickness="0" + Content="{x:Bind DirectoryPropertiesViewModel.GitBranchDisplayName, Mode=OneWay}"> + + + + + + + + + + + + + + + diff --git a/src/Files.App/UserControls/StatusBarControl.xaml.cs b/src/Files.App/UserControls/StatusBarControl.xaml.cs index 2bbce48edc35..6281957952a0 100644 --- a/src/Files.App/UserControls/StatusBarControl.xaml.cs +++ b/src/Files.App/UserControls/StatusBarControl.xaml.cs @@ -1,7 +1,6 @@ // Copyright (c) 2023 Files Community // Licensed under the MIT License. See the LICENSE. -using Files.App.ViewModels; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -42,5 +41,15 @@ public StatusBarControl() { InitializeComponent(); } + + private void BranchesFlyout_Opening(object sender, object e) + { + DirectoryPropertiesViewModel.SelectedBranchIndex = DirectoryPropertiesViewModel.ActiveBranchIndex; + } + + private void BranchesList_ItemClick(object sender, ItemClickEventArgs e) + { + BranchesFlyout.Hide(); + } } } diff --git a/src/Files.App/ViewModels/DirectoryPropertiesViewModel.cs b/src/Files.App/ViewModels/DirectoryPropertiesViewModel.cs index 31602dee97db..e7eec3ed1ec8 100644 --- a/src/Files.App/ViewModels/DirectoryPropertiesViewModel.cs +++ b/src/Files.App/ViewModels/DirectoryPropertiesViewModel.cs @@ -5,18 +5,52 @@ namespace Files.App.ViewModels { public class DirectoryPropertiesViewModel : ObservableObject { - private string directoryItemCount; + public int ActiveBranchIndex { get; private set; } + + private string _DirectoryItemCount; public string DirectoryItemCount { - get => directoryItemCount; - set => SetProperty(ref directoryItemCount, value); + get => _DirectoryItemCount; + set => SetProperty(ref _DirectoryItemCount, value); } - private string? gitBranchDisplayName; + private string? _GitBranchDisplayName; public string? GitBranchDisplayName { - get => gitBranchDisplayName; - set => SetProperty(ref gitBranchDisplayName, value); + get => _GitBranchDisplayName; + private set => SetProperty(ref _GitBranchDisplayName, value); + } + + private int _SelectedBranchIndex; + public int SelectedBranchIndex + { + get => _SelectedBranchIndex; + set + { + if (SetProperty(ref _SelectedBranchIndex, value) && value != -1 && value != ActiveBranchIndex) + CheckoutRequested?.Invoke(this, BranchesNames[value]); + } + } + + public ObservableCollection BranchesNames { get; } = new(); + + public EventHandler? CheckoutRequested; + + public void UpdateGitInfo(bool isGitRepository, string activeBranch, string[] branches) + { + GitBranchDisplayName = isGitRepository + ? string.Format("Branch".GetLocalizedResource(), activeBranch) + : null; + + if (isGitRepository) + { + BranchesNames.Clear(); + foreach (var name in branches) + BranchesNames.Add(name); + + ActiveBranchIndex = BranchesNames.IndexOf(activeBranch); + SelectedBranchIndex = ActiveBranchIndex; + } } } } diff --git a/src/Files.App/Views/Shells/BaseShellPage.cs b/src/Files.App/Views/Shells/BaseShellPage.cs index 472dad20bb13..a7cb81bac741 100644 --- a/src/Files.App/Views/Shells/BaseShellPage.cs +++ b/src/Files.App/Views/Shells/BaseShellPage.cs @@ -75,10 +75,15 @@ public BaseLayout ContentPage { if (value != _ContentPage) { + if (_ContentPage is not null) + _ContentPage.DirectoryPropertiesViewModel.CheckoutRequested -= GitCheckout_Required; + _ContentPage = value; NotifyPropertyChanged(nameof(ContentPage)); NotifyPropertyChanged(nameof(SlimContentPage)); + if (value is not null) + _ContentPage.DirectoryPropertiesViewModel.CheckoutRequested += GitCheckout_Required; } } } @@ -219,9 +224,10 @@ protected void FilesystemViewModel_DirectoryInfoUpdated(object sender, EventArgs InstanceViewModel.GitRepositoryPath = FilesystemViewModel.GitDirectory; - ContentPage.DirectoryPropertiesViewModel.GitBranchDisplayName = InstanceViewModel.IsGitRepository - ? string.Format("Branch".GetLocalizedResource(), InstanceViewModel.GitBranchName) - : null; + ContentPage.DirectoryPropertiesViewModel.UpdateGitInfo( + InstanceViewModel.IsGitRepository, + InstanceViewModel.GitBranchName, + GitHelpers.GetLocalBranchesNames(InstanceViewModel.GitRepositoryPath)); ContentPage.DirectoryPropertiesViewModel.DirectoryItemCount = $"{FilesystemViewModel.FilesAndFolders.Count} {directoryItemCountLocalization}"; ContentPage.UpdateSelectionSize(); @@ -230,9 +236,16 @@ protected void FilesystemViewModel_DirectoryInfoUpdated(object sender, EventArgs protected void FilesystemViewModel_GitDirectoryUpdated(object sender, EventArgs e) { InstanceViewModel.UpdateCurrentBranchName(); - ContentPage.DirectoryPropertiesViewModel.GitBranchDisplayName = InstanceViewModel.IsGitRepository - ? string.Format("Branch".GetLocalizedResource(), InstanceViewModel.GitBranchName) - : null; + ContentPage.DirectoryPropertiesViewModel.UpdateGitInfo( + InstanceViewModel.IsGitRepository, + InstanceViewModel.GitBranchName, + GitHelpers.GetLocalBranchesNames(InstanceViewModel.GitRepositoryPath)); + } + + protected async void GitCheckout_Required(object? sender, string branchName) + { + if (!await GitHelpers.Checkout(FilesystemViewModel.GitDirectory, branchName)) + _ContentPage.DirectoryPropertiesViewModel.SelectedBranchIndex = _ContentPage.DirectoryPropertiesViewModel.ActiveBranchIndex; } protected virtual void Page_Loaded(object sender, RoutedEventArgs e) diff --git a/src/Files.Backend/Enums/GitCheckoutOptions.cs b/src/Files.Backend/Enums/GitCheckoutOptions.cs new file mode 100644 index 000000000000..681f313abc40 --- /dev/null +++ b/src/Files.Backend/Enums/GitCheckoutOptions.cs @@ -0,0 +1,10 @@ +namespace Files.Backend.Enums +{ + public enum GitCheckoutOptions + { + BringChanges, + StashChanges, + DiscardChanges, + None + } +}