Skip to content

Commit 65d1f6b

Browse files
committed
Added Git action
1 parent edeba89 commit 65d1f6b

File tree

14 files changed

+434
-6
lines changed

14 files changed

+434
-6
lines changed

Diff for: src/Files.App/Actions/Git/GitCloneAction.cs

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) Files Community
2+
// Licensed under the MIT License.
3+
4+
namespace Files.App.Actions
5+
{
6+
internal sealed partial class GitCloneAction : ObservableObject, IAction
7+
{
8+
private readonly IContentPageContext pageContext = Ioc.Default.GetRequiredService<IContentPageContext>();
9+
private readonly IDialogService dialogService = Ioc.Default.GetRequiredService<IDialogService>();
10+
11+
public string Label { get; } = Strings.GitClone.GetLocalizedResource();
12+
13+
public string Description { get; } = Strings.GitCloneDescription.GetLocalizedResource();
14+
15+
public bool IsExecutable
16+
=> pageContext.CanCreateItem;
17+
18+
public GitCloneAction()
19+
{
20+
pageContext.PropertyChanged += Context_PropertyChanged;
21+
}
22+
23+
public Task ExecuteAsync(object? parameter = null)
24+
{
25+
if (pageContext.ShellPage is null)
26+
return Task.CompletedTask;
27+
28+
var repoUrl = parameter?.ToString() ?? string.Empty;
29+
var viewModel = new CloneRepoDialogViewModel(repoUrl, pageContext.ShellPage.ShellViewModel.WorkingDirectory);
30+
return dialogService.ShowDialogAsync(viewModel);
31+
}
32+
33+
private void Context_PropertyChanged(object? sender, PropertyChangedEventArgs e)
34+
{
35+
if (e.PropertyName is nameof(IContentPageContext.CanCreateItem))
36+
OnPropertyChanged(nameof(IsExecutable));
37+
}
38+
}
39+
}

Diff for: src/Files.App/Data/Commands/Manager/CommandCodes.cs

+1
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ public enum CommandCodes
230230
PlayAll,
231231

232232
// Git
233+
GitClone,
233234
GitFetch,
234235
GitInit,
235236
GitPull,

Diff for: src/Files.App/Data/Commands/Manager/CommandManager.cs

+2
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ public IRichCommand this[HotKey hotKey]
217217
public IRichCommand ArrangePanesHorizontally => commands[CommandCodes.ArrangePanesHorizontally];
218218
public IRichCommand OpenFileLocation => commands[CommandCodes.OpenFileLocation];
219219
public IRichCommand PlayAll => commands[CommandCodes.PlayAll];
220+
public IRichCommand GitClone => commands[CommandCodes.GitClone];
220221
public IRichCommand GitFetch => commands[CommandCodes.GitFetch];
221222
public IRichCommand GitInit => commands[CommandCodes.GitInit];
222223
public IRichCommand GitPull => commands[CommandCodes.GitPull];
@@ -421,6 +422,7 @@ public IEnumerator<IRichCommand> GetEnumerator() =>
421422
[CommandCodes.ArrangePanesHorizontally] = new ArrangePanesHorizontallyAction(),
422423
[CommandCodes.OpenFileLocation] = new OpenFileLocationAction(),
423424
[CommandCodes.PlayAll] = new PlayAllAction(),
425+
[CommandCodes.GitClone] = new GitCloneAction(),
424426
[CommandCodes.GitFetch] = new GitFetchAction(),
425427
[CommandCodes.GitInit] = new GitInitAction(),
426428
[CommandCodes.GitPull] = new GitPullAction(),

Diff for: src/Files.App/Data/Commands/Manager/ICommandManager.cs

+1
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ public interface ICommandManager : IEnumerable<IRichCommand>
210210

211211
IRichCommand PlayAll { get; }
212212

213+
IRichCommand GitClone { get; }
213214
IRichCommand GitFetch { get; }
214215
IRichCommand GitInit { get; }
215216
IRichCommand GitPull { get; }

Diff for: src/Files.App/Data/Enums/FileOperationType.cs

+5
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,10 @@ public enum FileOperationType : byte
6262
/// An item has been added to an archive
6363
/// </summary>
6464
Compressed = 11,
65+
66+
/// <summary>
67+
/// A git repo has been cloned
68+
/// </summary>
69+
GitClone = 12,
6570
}
6671
}

Diff for: src/Files.App/Dialogs/CloneRepoDialog.xaml

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!-- Copyright (c) Files Community. Licensed under the MIT License. -->
2+
<ContentDialog
3+
x:Class="Files.App.Dialogs.CloneRepoDialog"
4+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
5+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
6+
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
7+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
8+
xmlns:helpers="using:Files.App.Helpers"
9+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
10+
Title="Clone repo"
11+
HighContrastAdjustment="None"
12+
IsPrimaryButtonEnabled="{x:Bind ViewModel.CanCloneRepo, Mode=OneWay}"
13+
PrimaryButtonCommand="{x:Bind ViewModel.CloneRepoCommand, Mode=OneWay}"
14+
PrimaryButtonStyle="{ThemeResource AccentButtonStyle}"
15+
PrimaryButtonText="Clone"
16+
RequestedTheme="{x:Bind RootAppElement.RequestedTheme, Mode=OneWay}"
17+
SecondaryButtonText="Cancel"
18+
Style="{StaticResource DefaultContentDialogStyle}"
19+
mc:Ignorable="d">
20+
21+
<Grid Width="340">
22+
<StackPanel Spacing="8">
23+
<TextBox
24+
Header="Repository URL"
25+
PlaceholderText="https://github.com/files-community/Files"
26+
Text="{x:Bind ViewModel.RepoUrl, Mode=TwoWay}" />
27+
</StackPanel>
28+
</Grid>
29+
30+
</ContentDialog>

Diff for: src/Files.App/Dialogs/CloneRepoDialog.xaml.cs

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) Files Community
2+
// Licensed under the MIT License.
3+
4+
using Microsoft.UI.Xaml;
5+
using Microsoft.UI.Xaml.Controls;
6+
using Windows.ApplicationModel.DataTransfer;
7+
8+
namespace Files.App.Dialogs
9+
{
10+
public sealed partial class CloneRepoDialog : ContentDialog, IDialog<CloneRepoDialogViewModel>
11+
{
12+
private FrameworkElement RootAppElement
13+
=> (FrameworkElement)MainWindow.Instance.Content;
14+
15+
public CloneRepoDialogViewModel ViewModel
16+
{
17+
get => (CloneRepoDialogViewModel)DataContext;
18+
set => DataContext = value;
19+
}
20+
21+
public CloneRepoDialog()
22+
{
23+
InitializeComponent();
24+
}
25+
26+
public new async Task<DialogResult> ShowAsync()
27+
{
28+
return (DialogResult)await base.ShowAsync();
29+
}
30+
}
31+
}

Diff for: src/Files.App/Services/App/AppDialogService.cs

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public DialogService()
3636
{ typeof(FileTooLargeDialogViewModel), () => new FileTooLargeDialog() },
3737
{ typeof(ReleaseNotesDialogViewModel), () => new ReleaseNotesDialog() },
3838
{ typeof(BulkRenameDialogViewModel), () => new BulkRenameDialog() },
39+
{ typeof(CloneRepoDialogViewModel), () => new CloneRepoDialog() },
3940
}.ToFrozenDictionary();
4041
}
4142

Diff for: src/Files.App/Strings/en-US/Resources.resw

+41
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,9 @@
536536
<data name="CopyToFolderCaptionText" xml:space="preserve">
537537
<value>Copy to {0}</value>
538538
</data>
539+
<data name="CloneToFolderCaptionText" xml:space="preserve">
540+
<value>Clone to {0}</value>
541+
</data>
539542
<data name="CreateShortcut" xml:space="preserve">
540543
<value>Create shortcut</value>
541544
</data>
@@ -3129,6 +3132,9 @@
31293132
<data name="ImproveTranslation" xml:space="preserve">
31303133
<value>Translate on Crowdin</value>
31313134
</data>
3135+
<data name="GitClone" xml:space="preserve">
3136+
<value>Clone</value>
3137+
</data>
31323138
<data name="GitFetch" xml:space="preserve">
31333139
<value>Fetch</value>
31343140
</data>
@@ -3138,6 +3144,9 @@
31383144
<data name="GitFetchDescription" xml:space="preserve">
31393145
<value>Run git fetch</value>
31403146
</data>
3147+
<data name="GitCloneDescription" xml:space="preserve">
3148+
<value>Clone a git repo</value>
3149+
</data>
31413150
<data name="GitPullDescription" xml:space="preserve">
31423151
<value>Run git pull</value>
31433152
</data>
@@ -3464,6 +3473,38 @@
34643473
<data name="StatusCenter_CompressInProgress_SubHeader" xml:space="preserve">
34653474
<value>Compressing {0} item(s) from "{1}" to "{2}"</value>
34663475
<comment>Shown in a StatusCenter card.</comment>
3476+
</data>
3477+
<data name="StatusCenter_GitCloneCanceled_Header" xml:space="preserve">
3478+
<value>Canceled cloning {0} to "{1}"</value>
3479+
<comment>Shown in a StatusCenter card.</comment>
3480+
</data>
3481+
<data name="StatusCenter_GitCloneCanceled_SubHeader" xml:space="preserve">
3482+
<value>Canceled cloning {0} from "{1}" to "{2}"</value>
3483+
<comment>Shown in a StatusCenter card.</comment>
3484+
</data>
3485+
<data name="StatusCenter_GitCloneComplete_Header" xml:space="preserve">
3486+
<value>Cloned "{0}" to "{1}"</value>
3487+
<comment>Shown in a StatusCenter card.</comment>
3488+
</data>
3489+
<data name="StatusCenter_GitCloneComplete_SubHeader" xml:space="preserve">
3490+
<value>Cloned {0} from "{1}" to "{2}"</value>
3491+
<comment>Shown in a StatusCenter card.</comment>
3492+
</data>
3493+
<data name="StatusCenter_GitCloneFailed_Header" xml:space="preserve">
3494+
<value>Error cloning "{0}" to "{1}"</value>
3495+
<comment>Shown in a StatusCenter card.</comment>
3496+
</data>
3497+
<data name="StatusCenter_GitCloneFailed_SubHeader" xml:space="preserve">
3498+
<value>Failed to clone {0} from "{1}" to "{2}"</value>
3499+
<comment>Shown in a StatusCenter card.</comment>
3500+
</data>
3501+
<data name="StatusCenter_GitCloneInProgress_Header" xml:space="preserve">
3502+
<value>Cloning "{0}" to "{1}"</value>
3503+
<comment>Shown in a StatusCenter card.</comment>
3504+
</data>
3505+
<data name="StatusCenter_GitCloneInProgress_SubHeader" xml:space="preserve">
3506+
<value>Cloning {0} from "{0}" to "{1}"</value>
3507+
<comment>Shown in a StatusCenter card.</comment>
34673508
</data>
34683509
<data name="StatusCenter_CopyCanceled_Header" xml:space="preserve">
34693510
<value>Canceled copying {0} item(s) to "{1}"</value>

Diff for: src/Files.App/Utils/Git/GitHelpers.cs

+82-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using Files.App.Dialogs;
55
using LibGit2Sharp;
66
using Microsoft.Extensions.Logging;
7-
using Sentry;
87
using System.Net.Http;
98
using System.Net.Http.Json;
109
using System.Text;
@@ -13,6 +12,8 @@ namespace Files.App.Utils.Git
1312
{
1413
internal static class GitHelpers
1514
{
15+
private static readonly StatusCenterViewModel StatusCenterViewModel = Ioc.Default.GetRequiredService<StatusCenterViewModel>();
16+
1617
private const string GIT_RESOURCE_NAME = "Files:https://github.com";
1718

1819
private const string GIT_RESOURCE_USERNAME = "Personal Access Token";
@@ -845,5 +846,85 @@ private static bool IsAuthorizationException(Exception ex)
845846
GitOperationSemaphore.Release();
846847
}
847848
}
849+
850+
/// <summary>
851+
/// Gets repository information from a GitHub URL.
852+
/// </summary>
853+
/// <param name="url"></param>
854+
/// <returns></returns>
855+
public static (string RepoUrl, string RepoName) GetRepoInfo(string url)
856+
{
857+
// Remove protocol and normalize slashes
858+
var normalizedUrl = url.ToLower().Replace("https://", "").Replace("http://", "").Replace("//", "");
859+
860+
string[] parts = normalizedUrl.Split('/');
861+
862+
// Check if the URL includes an organization or user name + repo (github.com/username/repo)
863+
if (parts.Length >= 3 && parts[0] == "github.com")
864+
{
865+
// Construct the repo URL from the first three parts
866+
string repoUrl = $"https://{parts[0]}/{parts[1]}/{parts[2]}";
867+
return (repoUrl, parts[2]);
868+
}
869+
870+
return (string.Empty, string.Empty);
871+
}
872+
873+
/// <summary>
874+
/// Checks if the provided URL is a valid GitHub URL.
875+
/// </summary>
876+
/// <param name="url">The URL to validate.</param>
877+
/// <returns>True if the URL is a valid GitHub URL; otherwise, false.</returns>
878+
public static bool IsValidRepoUrl(string url)
879+
{
880+
return !string.IsNullOrWhiteSpace(url) && url.Contains("github.com");
881+
}
882+
883+
public static async Task CloneRepoAsync(string repoUrl, string repoName, string targetDirectory)
884+
{
885+
var banner = StatusCenterHelper.AddCard_GitClone(repoName.CreateEnumerable(), targetDirectory.CreateEnumerable(), ReturnResult.InProgress);
886+
var fsProgress = new StatusCenterItemProgressModel(banner.ProgressEventSource, enumerationCompleted: true, FileSystemStatusCode.InProgress);
887+
888+
bool isSuccess = await Task.Run(() =>
889+
{
890+
try
891+
{
892+
var cloneOptions = new CloneOptions
893+
{
894+
FetchOptions =
895+
{
896+
OnTransferProgress = progress =>
897+
{
898+
banner.CancellationToken.ThrowIfCancellationRequested();
899+
fsProgress.ItemsCount = progress.TotalObjects;
900+
fsProgress.SetProcessedSize(progress.ReceivedBytes);
901+
fsProgress.AddProcessedItemsCount(1);
902+
fsProgress.Report((int)((progress.ReceivedObjects / (double)progress.TotalObjects) * 100));
903+
return true;
904+
},
905+
OnProgress = _ => !banner.CancellationToken.IsCancellationRequested
906+
},
907+
OnCheckoutProgress = (path, completed, total) =>
908+
banner.CancellationToken.ThrowIfCancellationRequested()
909+
};
910+
911+
Repository.Clone(repoUrl, targetDirectory, cloneOptions);
912+
return true;
913+
}
914+
catch
915+
{
916+
return false;
917+
}
918+
}, banner.CancellationToken);
919+
920+
StatusCenterViewModel.RemoveItem(banner);
921+
922+
StatusCenterHelper.AddCard_GitClone(
923+
repoName.CreateEnumerable(),
924+
targetDirectory.CreateEnumerable(),
925+
isSuccess ? ReturnResult.Success :
926+
banner.CancellationToken.IsCancellationRequested ? ReturnResult.Cancelled :
927+
ReturnResult.Failed);
928+
}
848929
}
849930
}

0 commit comments

Comments
 (0)