Skip to content

Commit a8df728

Browse files
Feature: Added support for local branch checkout (#12316)
1 parent aae3dfe commit a8df728

File tree

8 files changed

+269
-24
lines changed

8 files changed

+269
-24
lines changed

src/Files.App/Helpers/DynamicDialogFactory.cs

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,11 @@
11
// Copyright (c) 2023 Files Community
22
// Licensed under the MIT License. See the LICENSE.
33

4-
using CommunityToolkit.WinUI;
54
using Files.App.Dialogs;
6-
using Files.App.Extensions;
7-
using Files.App.Filesystem;
85
using Files.App.ViewModels.Dialogs;
9-
using Files.Shared.Enums;
10-
using Files.Shared.Extensions;
116
using Microsoft.UI.Xaml;
127
using Microsoft.UI.Xaml.Controls;
138
using Microsoft.UI.Xaml.Data;
14-
using System;
15-
using System.Collections.Generic;
16-
using System.Linq;
179
using Windows.System;
1810

1911
namespace Files.App.Helpers
@@ -224,5 +216,51 @@ public static DynamicDialog GetFor_CredentialEntryDialog(string path)
224216

225217
return dialog;
226218
}
219+
220+
public static DynamicDialog GetFor_GitCheckoutConflicts(string checkoutBranchName, string headBranchName)
221+
{
222+
DynamicDialog dialog = null!;
223+
224+
var optionsListView = new ListView()
225+
{
226+
ItemsSource = new string[]
227+
{
228+
string.Format("BringChanges".GetLocalizedResource(), checkoutBranchName),
229+
string.Format("StashChanges".GetLocalizedResource(), headBranchName),
230+
"DiscardChanges".GetLocalizedResource()
231+
},
232+
SelectionMode = ListViewSelectionMode.Single
233+
};
234+
optionsListView.SelectedIndex = 0;
235+
236+
optionsListView.SelectionChanged += (listView, args) =>
237+
{
238+
dialog.ViewModel.AdditionalData = (GitCheckoutOptions)optionsListView.SelectedIndex;
239+
};
240+
241+
dialog = new DynamicDialog(new DynamicDialogViewModel()
242+
{
243+
TitleText = "SwitchBranch".GetLocalizedResource(),
244+
PrimaryButtonText = "Switch".GetLocalizedResource(),
245+
CloseButtonText = "Cancel".GetLocalizedResource(),
246+
SubtitleText = "UncommittedChanges".GetLocalizedResource(),
247+
DisplayControl = new Grid()
248+
{
249+
MinWidth = 250d,
250+
Children =
251+
{
252+
optionsListView
253+
}
254+
},
255+
AdditionalData = GitCheckoutOptions.BringChanges,
256+
CloseButtonAction = (vm, e) =>
257+
{
258+
dialog.ViewModel.AdditionalData = GitCheckoutOptions.None;
259+
vm.HideDialog();
260+
}
261+
});
262+
263+
return dialog;
264+
}
227265
}
228266
}

src/Files.App/Helpers/GitHelpers.cs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// Copyright (c) 2023 Files Community
22
// Licensed under the MIT License. See the LICENSE.
33

4-
using LibGit2Sharp;
54
using Files.App.Filesystem.StorageItems;
5+
using LibGit2Sharp;
6+
using Microsoft.AppCenter.Analytics;
67

78
namespace Files.App.Helpers
89
{
@@ -32,5 +33,67 @@ public static class GitHelpers
3233
return null;
3334
}
3435
}
36+
37+
public static string[] GetLocalBranchesNames(string? path)
38+
{
39+
if (string.IsNullOrWhiteSpace(path) || !Repository.IsValid(path))
40+
return Array.Empty<string>();
41+
42+
using var repository = new Repository(path);
43+
return repository.Branches
44+
.Where(b => !b.IsRemote)
45+
.OrderByDescending(b => b.IsCurrentRepositoryHead)
46+
.ThenByDescending(b => b.Tip.Committer.When)
47+
.Select(b => b.FriendlyName)
48+
.ToArray();
49+
}
50+
51+
public static async Task<bool> Checkout(string? repositoryPath, string? branch)
52+
{
53+
if (string.IsNullOrWhiteSpace(repositoryPath) || !Repository.IsValid(repositoryPath))
54+
return false;
55+
56+
using var repository = new Repository(repositoryPath);
57+
var checkoutBranch = repository.Branches[branch];
58+
if (checkoutBranch is null)
59+
return false;
60+
61+
var options = new CheckoutOptions();
62+
var isBringingChanges = false;
63+
64+
Analytics.TrackEvent($"Triggered git checkout");
65+
66+
if (repository.RetrieveStatus().IsDirty)
67+
{
68+
var dialog = DynamicDialogFactory.GetFor_GitCheckoutConflicts(checkoutBranch.FriendlyName, repository.Head.FriendlyName);
69+
await dialog.ShowAsync();
70+
71+
var resolveConflictOption = (GitCheckoutOptions)dialog.ViewModel.AdditionalData;
72+
73+
switch (resolveConflictOption)
74+
{
75+
case GitCheckoutOptions.None:
76+
return false;
77+
case GitCheckoutOptions.DiscardChanges:
78+
options.CheckoutModifiers = CheckoutModifiers.Force;
79+
break;
80+
case GitCheckoutOptions.BringChanges:
81+
case GitCheckoutOptions.StashChanges:
82+
repository.Stashes.Add(repository.Config.BuildSignature(DateTimeOffset.Now));
83+
84+
isBringingChanges = resolveConflictOption is GitCheckoutOptions.BringChanges;
85+
break;
86+
}
87+
}
88+
89+
LibGit2Sharp.Commands.Checkout(repository, checkoutBranch, options);
90+
91+
if (isBringingChanges)
92+
{
93+
var lastStashIndex = repository.Stashes.Count() - 1;
94+
repository.Stashes.Pop(lastStashIndex, new StashApplyOptions());
95+
}
96+
return true;
97+
}
3598
}
3699
}

src/Files.App/Strings/en-US/Resources.resw

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3262,4 +3262,28 @@
32623262
<data name="SecurityUnableToDisplayPermissions" xml:space="preserve">
32633263
<value>Unable to display permissions.</value>
32643264
</data>
3265+
<data name="StashChanges" xml:space="preserve">
3266+
<value>Leave my changes on '{0}'</value>
3267+
</data>
3268+
<data name="DiscardChanges" xml:space="preserve">
3269+
<value>Discard my changes</value>
3270+
</data>
3271+
<data name="BringChanges" xml:space="preserve">
3272+
<value>Bring my changes to '{0}'</value>
3273+
</data>
3274+
<data name="UncommittedChanges" xml:space="preserve">
3275+
<value>You have uncommitted changes on this branch. What would you like to do with them?</value>
3276+
</data>
3277+
<data name="SwitchBranch" xml:space="preserve">
3278+
<value>Switch Branch</value>
3279+
</data>
3280+
<data name="Branches" xml:space="preserve">
3281+
<value>Branches</value>
3282+
</data>
3283+
<data name="Switch" xml:space="preserve">
3284+
<value>Switch</value>
3285+
</data>
3286+
<data name="NewBranch" xml:space="preserve">
3287+
<value>New branch</value>
3288+
</data>
32653289
</root>

src/Files.App/UserControls/StatusBarControl.xaml

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
66
xmlns:converters="using:Files.App.Converters"
77
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
8+
xmlns:helpers="using:Files.App.Helpers"
89
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
910
d:DesignHeight="32"
1011
d:DesignWidth="400"
@@ -53,10 +54,63 @@
5354
VerticalAlignment="Center"
5455
Orientation="Horizontal"
5556
Spacing="8">
56-
<TextBlock
57+
<Button
5758
x:Name="GitBranch"
5859
x:Load="{x:Bind DirectoryPropertiesViewModel.GitBranchDisplayName, Mode=OneWay, Converter={StaticResource NullToFalseConverter}}"
59-
Text="{x:Bind DirectoryPropertiesViewModel.GitBranchDisplayName, Mode=OneWay}" />
60+
Background="Transparent"
61+
BorderThickness="0"
62+
Content="{x:Bind DirectoryPropertiesViewModel.GitBranchDisplayName, Mode=OneWay}">
63+
<Button.Flyout>
64+
<Flyout x:Name="BranchesFlyout" Opening="BranchesFlyout_Opening">
65+
<Grid
66+
Width="300"
67+
Height="340"
68+
Margin="-16">
69+
<Grid.RowDefinitions>
70+
<RowDefinition Height="Auto" />
71+
<RowDefinition Height="*" />
72+
</Grid.RowDefinitions>
73+
74+
<!-- Header -->
75+
<Grid
76+
Grid.Row="0"
77+
Padding="12"
78+
Background="{ThemeResource AcrylicBackgroundFillColorDefaultBrush}"
79+
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
80+
BorderThickness="0,0,0,1">
81+
<!-- Title -->
82+
<TextBlock
83+
VerticalAlignment="Center"
84+
FontSize="14"
85+
Text="{helpers:ResourceString Name=Branches}" />
86+
87+
<!-- New Branch Button -->
88+
<Button
89+
x:Name="NewBranchButton"
90+
Height="24"
91+
Padding="8,0"
92+
HorizontalAlignment="Right"
93+
x:Load="False"
94+
Content="{helpers:ResourceString Name=NewBranch}"
95+
FontSize="12"
96+
ToolTipService.ToolTip="{helpers:ResourceString Name=NewBranch}" />
97+
</Grid>
98+
99+
<!-- Branches List -->
100+
<ListView
101+
x:Name="BranchesList"
102+
Grid.Row="1"
103+
Padding="4"
104+
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
105+
IsItemClickEnabled="True"
106+
ItemClick="BranchesList_ItemClick"
107+
ItemsSource="{x:Bind DirectoryPropertiesViewModel.BranchesNames, Mode=OneWay}"
108+
SelectedIndex="{x:Bind DirectoryPropertiesViewModel.SelectedBranchIndex, Mode=TwoWay}"
109+
SelectionMode="Single" />
110+
</Grid>
111+
</Flyout>
112+
</Button.Flyout>
113+
</Button>
60114
</StackPanel>
61115
</Grid>
62116
</UserControl>

src/Files.App/UserControls/StatusBarControl.xaml.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright (c) 2023 Files Community
22
// Licensed under the MIT License. See the LICENSE.
33

4-
using Files.App.ViewModels;
54
using Microsoft.UI.Xaml;
65
using Microsoft.UI.Xaml.Controls;
76

@@ -42,5 +41,15 @@ public StatusBarControl()
4241
{
4342
InitializeComponent();
4443
}
44+
45+
private void BranchesFlyout_Opening(object sender, object e)
46+
{
47+
DirectoryPropertiesViewModel.SelectedBranchIndex = DirectoryPropertiesViewModel.ActiveBranchIndex;
48+
}
49+
50+
private void BranchesList_ItemClick(object sender, ItemClickEventArgs e)
51+
{
52+
BranchesFlyout.Hide();
53+
}
4554
}
4655
}

src/Files.App/ViewModels/DirectoryPropertiesViewModel.cs

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,52 @@ namespace Files.App.ViewModels
55
{
66
public class DirectoryPropertiesViewModel : ObservableObject
77
{
8-
private string directoryItemCount;
8+
public int ActiveBranchIndex { get; private set; }
9+
10+
private string _DirectoryItemCount;
911
public string DirectoryItemCount
1012
{
11-
get => directoryItemCount;
12-
set => SetProperty(ref directoryItemCount, value);
13+
get => _DirectoryItemCount;
14+
set => SetProperty(ref _DirectoryItemCount, value);
1315
}
1416

15-
private string? gitBranchDisplayName;
17+
private string? _GitBranchDisplayName;
1618
public string? GitBranchDisplayName
1719
{
18-
get => gitBranchDisplayName;
19-
set => SetProperty(ref gitBranchDisplayName, value);
20+
get => _GitBranchDisplayName;
21+
private set => SetProperty(ref _GitBranchDisplayName, value);
22+
}
23+
24+
private int _SelectedBranchIndex;
25+
public int SelectedBranchIndex
26+
{
27+
get => _SelectedBranchIndex;
28+
set
29+
{
30+
if (SetProperty(ref _SelectedBranchIndex, value) && value != -1 && value != ActiveBranchIndex)
31+
CheckoutRequested?.Invoke(this, BranchesNames[value]);
32+
}
33+
}
34+
35+
public ObservableCollection<string> BranchesNames { get; } = new();
36+
37+
public EventHandler<string>? CheckoutRequested;
38+
39+
public void UpdateGitInfo(bool isGitRepository, string activeBranch, string[] branches)
40+
{
41+
GitBranchDisplayName = isGitRepository
42+
? string.Format("Branch".GetLocalizedResource(), activeBranch)
43+
: null;
44+
45+
if (isGitRepository)
46+
{
47+
BranchesNames.Clear();
48+
foreach (var name in branches)
49+
BranchesNames.Add(name);
50+
51+
ActiveBranchIndex = BranchesNames.IndexOf(activeBranch);
52+
SelectedBranchIndex = ActiveBranchIndex;
53+
}
2054
}
2155
}
2256
}

src/Files.App/Views/Shells/BaseShellPage.cs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,15 @@ public BaseLayout ContentPage
7575
{
7676
if (value != _ContentPage)
7777
{
78+
if (_ContentPage is not null)
79+
_ContentPage.DirectoryPropertiesViewModel.CheckoutRequested -= GitCheckout_Required;
80+
7881
_ContentPage = value;
7982

8083
NotifyPropertyChanged(nameof(ContentPage));
8184
NotifyPropertyChanged(nameof(SlimContentPage));
85+
if (value is not null)
86+
_ContentPage.DirectoryPropertiesViewModel.CheckoutRequested += GitCheckout_Required;
8287
}
8388
}
8489
}
@@ -219,9 +224,10 @@ protected void FilesystemViewModel_DirectoryInfoUpdated(object sender, EventArgs
219224

220225
InstanceViewModel.GitRepositoryPath = FilesystemViewModel.GitDirectory;
221226

222-
ContentPage.DirectoryPropertiesViewModel.GitBranchDisplayName = InstanceViewModel.IsGitRepository
223-
? string.Format("Branch".GetLocalizedResource(), InstanceViewModel.GitBranchName)
224-
: null;
227+
ContentPage.DirectoryPropertiesViewModel.UpdateGitInfo(
228+
InstanceViewModel.IsGitRepository,
229+
InstanceViewModel.GitBranchName,
230+
GitHelpers.GetLocalBranchesNames(InstanceViewModel.GitRepositoryPath));
225231

226232
ContentPage.DirectoryPropertiesViewModel.DirectoryItemCount = $"{FilesystemViewModel.FilesAndFolders.Count} {directoryItemCountLocalization}";
227233
ContentPage.UpdateSelectionSize();
@@ -230,9 +236,16 @@ protected void FilesystemViewModel_DirectoryInfoUpdated(object sender, EventArgs
230236
protected void FilesystemViewModel_GitDirectoryUpdated(object sender, EventArgs e)
231237
{
232238
InstanceViewModel.UpdateCurrentBranchName();
233-
ContentPage.DirectoryPropertiesViewModel.GitBranchDisplayName = InstanceViewModel.IsGitRepository
234-
? string.Format("Branch".GetLocalizedResource(), InstanceViewModel.GitBranchName)
235-
: null;
239+
ContentPage.DirectoryPropertiesViewModel.UpdateGitInfo(
240+
InstanceViewModel.IsGitRepository,
241+
InstanceViewModel.GitBranchName,
242+
GitHelpers.GetLocalBranchesNames(InstanceViewModel.GitRepositoryPath));
243+
}
244+
245+
protected async void GitCheckout_Required(object? sender, string branchName)
246+
{
247+
if (!await GitHelpers.Checkout(FilesystemViewModel.GitDirectory, branchName))
248+
_ContentPage.DirectoryPropertiesViewModel.SelectedBranchIndex = _ContentPage.DirectoryPropertiesViewModel.ActiveBranchIndex;
236249
}
237250

238251
protected virtual void Page_Loaded(object sender, RoutedEventArgs e)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace Files.Backend.Enums
2+
{
3+
public enum GitCheckoutOptions
4+
{
5+
BringChanges,
6+
StashChanges,
7+
DiscardChanges,
8+
None
9+
}
10+
}

0 commit comments

Comments
 (0)