Skip to content

Commit

Permalink
Feature: Git Integration Phase 3 (#12344)
Browse files Browse the repository at this point in the history
  • Loading branch information
ferrariofilippo authored May 21, 2023
1 parent 656977e commit cf9cdca
Show file tree
Hide file tree
Showing 12 changed files with 499 additions and 43 deletions.
7 changes: 7 additions & 0 deletions src/Files.App/Data/Items/BranchItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright (c) 2023 Files Community
// Licensed under the MIT License. See the LICENSE.

namespace Files.App.Data.Items
{
public record BranchItem(string Name, bool IsRemote);
}
74 changes: 61 additions & 13 deletions src/Files.App/Data/Models/DirectoryPropertiesViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
// Copyright (c) 2023 Files Community
// Licensed under the MIT License. See the LICENSE.

using System.Windows.Input;

namespace Files.App.Data.Models
{
public class DirectoryPropertiesViewModel : ObservableObject
{
public int ActiveBranchIndex { get; private set; }
// The first branch will always be the active one.
public const int ACTIVE_BRANCH_INDEX = 0;

private string? _gitRepositoryPath;

private readonly ObservableCollection<string> _localBranches = new();

private string _DirectoryItemCount;
public string DirectoryItemCount
private readonly ObservableCollection<string> _remoteBranches = new();

private string? _DirectoryItemCount;
public string? DirectoryItemCount
{
get => _DirectoryItemCount;
set => SetProperty(ref _DirectoryItemCount, value);
Expand All @@ -27,29 +36,68 @@ public int SelectedBranchIndex
get => _SelectedBranchIndex;
set
{
if (SetProperty(ref _SelectedBranchIndex, value) && value != -1 && value != ActiveBranchIndex)
if (SetProperty(ref _SelectedBranchIndex, value) &&
value != -1 &&
(value != ACTIVE_BRANCH_INDEX || !_ShowLocals))
{
CheckoutRequested?.Invoke(this, BranchesNames[value]);
}
}
}

public ObservableCollection<string> BranchesNames { get; } = new();
private bool _ShowLocals = true;
public bool ShowLocals
{
get => _ShowLocals;
set
{
if (SetProperty(ref _ShowLocals, value))
{
OnPropertyChanged(nameof(BranchesNames));

if (value)
SelectedBranchIndex = ACTIVE_BRANCH_INDEX;
}
}
}

public ObservableCollection<string> BranchesNames => _ShowLocals
? _localBranches
: _remoteBranches;

public EventHandler<string>? CheckoutRequested;

public void UpdateGitInfo(bool isGitRepository, string activeBranch, string[] branches)
public ICommand NewBranchCommand { get; }

public DirectoryPropertiesViewModel()
{
NewBranchCommand = new AsyncRelayCommand(()
=> GitHelpers.CreateNewBranch(_gitRepositoryPath!, _localBranches[ACTIVE_BRANCH_INDEX]));
}

public void UpdateGitInfo(bool isGitRepository, string? repositoryPath, BranchItem[] branches)
{
GitBranchDisplayName = isGitRepository
? string.Format("Branch".GetLocalizedResource(), activeBranch)
GitBranchDisplayName = isGitRepository && branches.Any()
? string.Format("Branch".GetLocalizedResource(), branches[ACTIVE_BRANCH_INDEX].Name)
: null;

_gitRepositoryPath = repositoryPath;
ShowLocals = true;

if (isGitRepository)
{
BranchesNames.Clear();
foreach (var name in branches)
BranchesNames.Add(name);
_localBranches.Clear();
_remoteBranches.Clear();

foreach (var branch in branches)
{
if (branch.IsRemote)
_remoteBranches.Add(branch.Name);
else
_localBranches.Add(branch.Name);
}

ActiveBranchIndex = BranchesNames.IndexOf(activeBranch);
SelectedBranchIndex = ActiveBranchIndex;
SelectedBranchIndex = ACTIVE_BRANCH_INDEX;
}
}
}
Expand Down
102 changes: 102 additions & 0 deletions src/Files.App/Dialogs/AddBranchDialog.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<!-- Copyright (c) 2023 Files Community. Licensed under the MIT License. See the LICENSE. -->
<ContentDialog
x:Class="Files.App.Dialogs.AddBranchDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="using:Files.App.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
x:Name="AddBranch"
Title="{helpers:ResourceString Name=CreateNewBranch}"
Closing="ContentDialog_Closing"
CornerRadius="{StaticResource OverlayCornerRadius}"
DefaultButton="Primary"
IsPrimaryButtonEnabled="{x:Bind ViewModel.IsBranchValid, Mode=OneWay}"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}"
PrimaryButtonText="{helpers:ResourceString Name=Create}"
RequestedTheme="{x:Bind helpers:ThemeHelper.RootTheme}"
SecondaryButtonText="{helpers:ResourceString Name=Cancel}"
Style="{StaticResource DefaultContentDialogStyle}"
mc:Ignorable="d">

<StackPanel Width="440" Spacing="4">
<!-- Branch Name -->
<Grid
Padding="12"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
ColumnSpacing="8"
CornerRadius="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
VerticalAlignment="Center"
Text="{helpers:ResourceString Name=Name}" />
<TextBox
x:Name="BranchNameBox"
Grid.Column="1"
Width="260"
PlaceholderText="{helpers:ResourceString Name=EnterName}"
Text="{x:Bind ViewModel.NewBranchName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<TextBox.Resources>
<TeachingTip
x:Name="InvalidNameWarning"
Title="{helpers:ResourceString Name=InvalidBranchName}"
IsOpen="{x:Bind ViewModel.ShowWarningTip, Mode=OneWay}"
PreferredPlacement="Bottom"
Target="{x:Bind BranchNameBox}" />
</TextBox.Resources>
</TextBox>
</Grid>

<!-- Branch Options -->
<Grid
Padding="12"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
ColumnSpacing="8"
CornerRadius="4"
RowSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>

<!-- Based On -->
<TextBlock
Grid.Row="0"
Grid.Column="0"
VerticalAlignment="Center"
Text="{helpers:ResourceString Name=BasedOn}" />
<ComboBox
x:Name="BranchBox"
Grid.Row="0"
Grid.Column="1"
Width="160"
ItemsSource="{x:Bind ViewModel.Branches}"
SelectedItem="{x:Bind ViewModel.BasedOn, Mode=TwoWay}" />

<!-- Switch To Branch -->
<TextBlock
Grid.Row="1"
Grid.Column="0"
VerticalAlignment="Center"
Text="{helpers:ResourceString Name=SwitchToNewBranch}" />
<ToggleSwitch
Grid.Row="1"
Grid.Column="1"
MinWidth="0"
HorizontalAlignment="Right"
IsOn="{x:Bind ViewModel.Checkout, Mode=TwoWay}" />
</Grid>
</StackPanel>
</ContentDialog>
33 changes: 33 additions & 0 deletions src/Files.App/Dialogs/AddBranchDialog.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) 2023 Files Community
// Licensed under the MIT License. See the LICENSE.

using Files.App.ViewModels.Dialogs;
using Files.Backend.ViewModels.Dialogs;
using Microsoft.UI.Xaml.Controls;

// The Content Dialog item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238

namespace Files.App.Dialogs
{
public sealed partial class AddBranchDialog : ContentDialog, IDialog<AddBranchDialogViewModel>
{
public AddBranchDialogViewModel ViewModel
{
get => (AddBranchDialogViewModel)DataContext;
set => DataContext = value;
}

public AddBranchDialog()
{
InitializeComponent();
}

public new async Task<DialogResult> ShowAsync() => (DialogResult)await base.ShowAsync();

private void ContentDialog_Closing(ContentDialog _, ContentDialogClosingEventArgs e)
{
InvalidNameWarning.IsOpen = false;
Closing -= ContentDialog_Closing;
}
}
}
96 changes: 85 additions & 11 deletions src/Files.App/Helpers/GitHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@
// Licensed under the MIT License. See the LICENSE.

using Files.App.Filesystem.StorageItems;
using Files.App.ViewModels.Dialogs;
using Files.Backend.Services;
using LibGit2Sharp;
using Microsoft.AppCenter.Analytics;
using System.Text.RegularExpressions;

namespace Files.App.Helpers
{
public static class GitHelpers
internal static class GitHelpers
{
private const string BRANCH_NAME_PATTERN = @"^(?!/)(?!.*//)[^\000-\037\177 ~^:?*[]+(?!.*\.\.)(?!.*@\{)(?!.*\\)(?<!/\.)(?<!\.)(?<!/)(?<!\.lock)$";

private const int END_OF_ORIGIN_PREFIX = 7;

public static string? GetGitRepositoryPath(string? path, string root)
{
if (root.EndsWith('\\'))
Expand All @@ -34,17 +41,18 @@ public static class GitHelpers
}
}

public static string[] GetLocalBranchesNames(string? path)
public static BranchItem[] GetBranchesNames(string? path)
{
if (string.IsNullOrWhiteSpace(path) || !Repository.IsValid(path))
return Array.Empty<string>();
return Array.Empty<BranchItem>();

using var repository = new Repository(path);
return repository.Branches
.Where(b => !b.IsRemote)
.Where(b => !b.IsRemote || b.RemoteName == "origin")
.OrderByDescending(b => b.IsCurrentRepositoryHead)
.ThenBy(b => b.IsRemote)
.ThenByDescending(b => b.Tip.Committer.When)
.Select(b => b.FriendlyName)
.Select(b => new BranchItem(b.FriendlyName, b.IsRemote))
.ToArray();
}

Expand Down Expand Up @@ -79,21 +87,87 @@ public static async Task<bool> Checkout(string? repositoryPath, string? branch)
break;
case GitCheckoutOptions.BringChanges:
case GitCheckoutOptions.StashChanges:
repository.Stashes.Add(repository.Config.BuildSignature(DateTimeOffset.Now));
var signature = repository.Config.BuildSignature(DateTimeOffset.Now);
if (signature is null)
return false;

repository.Stashes.Add(signature);

isBringingChanges = resolveConflictOption is GitCheckoutOptions.BringChanges;
break;
}
}

LibGit2Sharp.Commands.Checkout(repository, checkoutBranch, options);
try
{
if (checkoutBranch.IsRemote)
CheckoutRemoteBranch(repository, checkoutBranch);
else
LibGit2Sharp.Commands.Checkout(repository, checkoutBranch, options);

if (isBringingChanges)
if (isBringingChanges)
{
var lastStashIndex = repository.Stashes.Count() - 1;
repository.Stashes.Pop(lastStashIndex, new StashApplyOptions());
}
return true;
}
catch (Exception)
{
var lastStashIndex = repository.Stashes.Count() - 1;
repository.Stashes.Pop(lastStashIndex, new StashApplyOptions());
return false;
}
return true;
}

public static async Task CreateNewBranch(string repositoryPath, string activeBranch)
{
var viewModel = new AddBranchDialogViewModel(repositoryPath, activeBranch);
var dialog = Ioc.Default.GetRequiredService<IDialogService>().GetDialog(viewModel);

var result = await dialog.TryShowAsync();

if (result != DialogResult.Primary)
return;

using var repository = new Repository(repositoryPath);

if (repository.Head.FriendlyName.Equals(viewModel.NewBranchName) ||
await Checkout(repositoryPath, viewModel.BasedOn))
{
Analytics.TrackEvent($"Triggered git branch");

repository.CreateBranch(viewModel.NewBranchName);

if (viewModel.Checkout)
await Checkout(repositoryPath, viewModel.NewBranchName);
}
}

public static bool ValidateBranchNameForRepository(string branchName, string repositoryPath)
{
if (string.IsNullOrEmpty(branchName) || !Repository.IsValid(repositoryPath))
return false;

var nameValidator = new Regex(BRANCH_NAME_PATTERN);
if (!nameValidator.IsMatch(branchName))
return false;

using var repository = new Repository(repositoryPath);
return !repository.Branches.Any(branch =>
branch.FriendlyName.Equals(branchName, StringComparison.OrdinalIgnoreCase));
}

private static void CheckoutRemoteBranch(Repository repository, Branch branch)
{
var uniqueName = branch.FriendlyName.Substring(END_OF_ORIGIN_PREFIX);

var discriminator = 0;
while (repository.Branches.Any(b => !b.IsRemote && b.FriendlyName == uniqueName))
uniqueName = $"{branch.FriendlyName}_{++discriminator}";

var newBranch = repository.CreateBranch(uniqueName, branch.Tip);
repository.Branches.Update(newBranch, b => b.TrackedBranch = branch.CanonicalName);

LibGit2Sharp.Commands.Checkout(repository, newBranch);
}
}
}
3 changes: 2 additions & 1 deletion src/Files.App/ServicesImplementation/DialogService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ public DialogService()
{ typeof(DecompressArchiveDialogViewModel), () => new DecompressArchiveDialog() },
{ typeof(SettingsDialogViewModel), () => new SettingsDialog() },
{ typeof(CreateShortcutDialogViewModel), () => new CreateShortcutDialog() },
{ typeof(ReorderSidebarItemsDialogViewModel), () => new ReorderSidebarItemsDialog() }
{ typeof(ReorderSidebarItemsDialogViewModel), () => new ReorderSidebarItemsDialog() },
{ typeof(AddBranchDialogViewModel), () => new AddBranchDialog() }
};
}

Expand Down
Loading

0 comments on commit cf9cdca

Please sign in to comment.