From 0c21bcd06aa9efd134c9183dce1f1f14604c6ac9 Mon Sep 17 00:00:00 2001 From: leo Date: Wed, 26 Jun 2024 11:49:56 +0800 Subject: [PATCH] enhance: Git LFS supports (#209) * add a new context menu to push local LFS object to selected remote * supports to choose remote for fetch/pull/push/lock/unlock actions * auto select remote if there's only one remote --- src/Commands/LFS.cs | 29 ++++--- src/Resources/Locales/en_US.axaml | 4 + src/Resources/Locales/zh_CN.axaml | 4 + src/Resources/Locales/zh_TW.axaml | 4 + src/ViewModels/LFSFetch.cs | 14 ++- src/ViewModels/LFSLocks.cs | 8 +- src/ViewModels/LFSPull.cs | 14 ++- src/ViewModels/LFSPush.cs | 37 ++++++++ src/ViewModels/Repository.cs | 48 +++++++++-- src/ViewModels/WorkingCopy.cs | 137 ++++++++++++++++++++++++------ src/Views/LFSFetch.axaml | 33 +++++-- src/Views/LFSPull.axaml | 33 +++++-- src/Views/LFSPush.axaml | 41 +++++++++ src/Views/LFSPush.axaml.cs | 12 +++ 14 files changed, 359 insertions(+), 59 deletions(-) create mode 100644 src/ViewModels/LFSPush.cs create mode 100644 src/Views/LFSPush.axaml create mode 100644 src/Views/LFSPush.axaml.cs diff --git a/src/Commands/LFS.cs b/src/Commands/LFS.cs index 068e8ac5..b52b1df8 100644 --- a/src/Commands/LFS.cs +++ b/src/Commands/LFS.cs @@ -55,14 +55,19 @@ namespace SourceGit.Commands return new SubCmd(_repo, $"lfs track {opt} \"{pattern}\"", null).Exec(); } - public void Fetch(Action outputHandler) + public void Fetch(string remote, Action outputHandler) { - new SubCmd(_repo, $"lfs fetch", outputHandler).Exec(); + new SubCmd(_repo, $"lfs fetch {remote}", outputHandler).Exec(); } - public void Pull(Action outputHandler) + public void Pull(string remote, Action outputHandler) { - new SubCmd(_repo, $"lfs pull", outputHandler).Exec(); + new SubCmd(_repo, $"lfs pull {remote}", outputHandler).Exec(); + } + + public void Push(string remote, Action outputHandler) + { + new SubCmd(_repo, $"lfs push {remote}", outputHandler).Exec(); } public void Prune(Action outputHandler) @@ -70,10 +75,10 @@ namespace SourceGit.Commands new SubCmd(_repo, "lfs prune", outputHandler).Exec(); } - public List Locks() + public List Locks(string remote) { var locks = new List(); - var cmd = new SubCmd(_repo, "lfs locks", null); + var cmd = new SubCmd(_repo, $"lfs locks --remote={remote}", null); var rs = cmd.ReadToEnd(); if (rs.IsSuccess) { @@ -96,21 +101,21 @@ namespace SourceGit.Commands return locks; } - public bool Lock(string file) + public bool Lock(string remote, string file) { - return new SubCmd(_repo, $"lfs lock \"{file}\"", null).Exec(); + return new SubCmd(_repo, $"lfs lock --remote={remote} \"{file}\"", null).Exec(); } - public bool Unlock(string file, bool force) + public bool Unlock(string remote, string file, bool force) { var opt = force ? "-f" : ""; - return new SubCmd(_repo, $"lfs unlock {opt} \"{file}\"", null).Exec(); + return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} \"{file}\"", null).Exec(); } - public bool Unlock(long id, bool force) + public bool Unlock(string remote, long id, bool force) { var opt = force ? "-f" : ""; - return new SubCmd(_repo, $"lfs unlock {opt} --id={id}", null).Exec(); + return new SubCmd(_repo, $"lfs unlock --remote={remote} {opt} --id={id}", null).Exec(); } private readonly string _repo; diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index 1f22e3be..0d050afe 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -254,6 +254,10 @@ Pull Pull LFS Objects Run `git lfs pull` to download all Git LFS files for current ref & checkout + Push + Push LFS Objects + Push queued large files to the Git LFS endpoint + Remote: Track files named '{0}' Track all *{0} files Histories diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index c425e971..543eb8ea 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -257,6 +257,10 @@ 拉回LFS对象 (pull) 拉回LFS对象 运行`git lfs pull`命令,下载远程LFS对象并更新工作副本。 + 推送 + 推送LFS对象 + 将排队的大文件推送到Git LFS远程服务 + 远程 : 跟踪名为'{0}'的文件 跟踪所有 *{0} 文件 历史记录 diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml index 9c6dadad..c97d994d 100644 --- a/src/Resources/Locales/zh_TW.axaml +++ b/src/Resources/Locales/zh_TW.axaml @@ -257,6 +257,10 @@ 拉回LFS物件 (pull) 拉回LFS物件 執行`git lfs pull`命令,下載遠端LFS物件并更新工作副本。 + 推送 + 推送LFS物件 + 將排隊的大檔推送到Git LFS遠端服務 + 遠端倉庫 : 跟蹤名為'{0}'的檔案 跟蹤所有 *{0} 檔案 歷史記錄 diff --git a/src/ViewModels/LFSFetch.cs b/src/ViewModels/LFSFetch.cs index 2591c7d9..a3fb1f08 100644 --- a/src/ViewModels/LFSFetch.cs +++ b/src/ViewModels/LFSFetch.cs @@ -1,12 +1,22 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading.Tasks; namespace SourceGit.ViewModels { public class LFSFetch : Popup { + public List Remotes => _repo.Remotes; + + public Models.Remote SelectedRemote + { + get; + set; + } + public LFSFetch(Repository repo) { _repo = repo; + SelectedRemote = _repo.Remotes[0]; View = new Views.LFSFetch() { DataContext = this }; } @@ -16,7 +26,7 @@ namespace SourceGit.ViewModels ProgressDescription = $"Fetching LFS objects from remote ..."; return Task.Run(() => { - new Commands.LFS(_repo.FullPath).Fetch(SetProgressDescription); + new Commands.LFS(_repo.FullPath).Fetch(SelectedRemote.Name, SetProgressDescription); CallUIThread(() => _repo.SetWatcherEnabled(true)); return true; }); diff --git a/src/ViewModels/LFSLocks.cs b/src/ViewModels/LFSLocks.cs index 92a01d10..ec4ff822 100644 --- a/src/ViewModels/LFSLocks.cs +++ b/src/ViewModels/LFSLocks.cs @@ -27,14 +27,15 @@ namespace SourceGit.ViewModels private set; } - public LFSLocks(string repo) + public LFSLocks(string repo, string remote) { _repo = repo; + _remote = remote; Locks = new AvaloniaList(); Task.Run(() => { - var collect = new Commands.LFS(_repo).Locks(); + var collect = new Commands.LFS(_repo).Locks(_remote); Dispatcher.UIThread.Invoke(() => { if (collect.Count > 0) @@ -54,7 +55,7 @@ namespace SourceGit.ViewModels IsLoading = true; Task.Run(() => { - var succ = new Commands.LFS(_repo).Unlock(lfsLock.ID, force); + var succ = new Commands.LFS(_repo).Unlock(_remote, lfsLock.ID, force); Dispatcher.UIThread.Invoke(() => { if (succ) @@ -67,6 +68,7 @@ namespace SourceGit.ViewModels } private string _repo = string.Empty; + private string _remote = string.Empty; private bool _isLoading = true; private bool _isEmpty = false; } diff --git a/src/ViewModels/LFSPull.cs b/src/ViewModels/LFSPull.cs index f1f55448..b949ddc2 100644 --- a/src/ViewModels/LFSPull.cs +++ b/src/ViewModels/LFSPull.cs @@ -1,12 +1,22 @@ -using System.Threading.Tasks; +using System.Collections.Generic; +using System.Threading.Tasks; namespace SourceGit.ViewModels { public class LFSPull : Popup { + public List Remotes => _repo.Remotes; + + public Models.Remote SelectedRemote + { + get; + set; + } + public LFSPull(Repository repo) { _repo = repo; + SelectedRemote = _repo.Remotes[0]; View = new Views.LFSPull() { DataContext = this }; } @@ -16,7 +26,7 @@ namespace SourceGit.ViewModels ProgressDescription = $"Pull LFS objects from remote ..."; return Task.Run(() => { - new Commands.LFS(_repo.FullPath).Pull(SetProgressDescription); + new Commands.LFS(_repo.FullPath).Pull(SelectedRemote.Name, SetProgressDescription); CallUIThread(() => _repo.SetWatcherEnabled(true)); return true; }); diff --git a/src/ViewModels/LFSPush.cs b/src/ViewModels/LFSPush.cs new file mode 100644 index 00000000..5ea06757 --- /dev/null +++ b/src/ViewModels/LFSPush.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace SourceGit.ViewModels +{ + public class LFSPush : Popup + { + public List Remotes => _repo.Remotes; + + public Models.Remote SelectedRemote + { + get; + set; + } + + public LFSPush(Repository repo) + { + _repo = repo; + SelectedRemote = _repo.Remotes[0]; + View = new Views.LFSPush() { DataContext = this }; + } + + public override Task Sure() + { + _repo.SetWatcherEnabled(false); + ProgressDescription = $"Push LFS objects to remote ..."; + return Task.Run(() => + { + new Commands.LFS(_repo.FullPath).Push(SelectedRemote.Name, SetProgressDescription); + CallUIThread(() => _repo.SetWatcherEnabled(true)); + return true; + }); + } + + private readonly Repository _repo = null; + } +} diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 5fb1b858..b362e8b5 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -911,6 +911,24 @@ namespace SourceGit.ViewModels }; menu.Items.Add(pull); + var push = new MenuItem(); + push.Header = App.Text("GitLFS.Push"); + push.Icon = App.CreateMenuIcon("Icons.Push"); + push.IsEnabled = Remotes.Count > 0; + push.Click += (o, e) => + { + if (PopupHost.CanCreatePopup()) + { + if (Remotes.Count == 1) + PopupHost.ShowAndStartPopup(new LFSPush(this)); + else + PopupHost.ShowPopup(new LFSPush(this)); + } + + e.Handled = true; + }; + menu.Items.Add(push); + var prune = new MenuItem(); prune.Header = App.Text("GitLFS.Prune"); prune.Icon = App.CreateMenuIcon("Icons.Clean"); @@ -927,13 +945,33 @@ namespace SourceGit.ViewModels var locks = new MenuItem(); locks.Header = App.Text("GitLFS.Locks"); locks.Icon = App.CreateMenuIcon("Icons.Lock"); - locks.Click += (o, e) => + locks.IsEnabled = Remotes.Count > 0; + if (Remotes.Count == 1) { - var dialog = new Views.LFSLocks() { DataContext = new LFSLocks(_fullpath) }; - dialog.Show(App.GetTopLevel() as Window); + locks.Click += (o, e) => + { + var dialog = new Views.LFSLocks() { DataContext = new LFSLocks(_fullpath, Remotes[0].Name) }; + dialog.Show(App.GetTopLevel() as Window); + e.Handled = true; + }; + } + else + { + foreach (var remote in Remotes) + { + var remoteName = remote.Name; + var lockRemote = new MenuItem(); + lockRemote.Header = remoteName; + lockRemote.Click += (o, e) => + { + var dialog = new Views.LFSLocks() { DataContext = new LFSLocks(_fullpath, remoteName) }; + dialog.Show(App.GetTopLevel() as Window); + e.Handled = true; + }; + locks.Items.Add(lockRemote); + } + } - e.Handled = true; - }; menu.Items.Add(new MenuItem() { Header = "-" }); menu.Items.Add(locks); } diff --git a/src/ViewModels/WorkingCopy.cs b/src/ViewModels/WorkingCopy.cs index fd3dc569..5d99da24 100644 --- a/src/ViewModels/WorkingCopy.cs +++ b/src/ViewModels/WorkingCopy.cs @@ -643,27 +643,71 @@ namespace SourceGit.ViewModels var lfsLock = new MenuItem(); lfsLock.Header = App.Text("GitLFS.Locks.Lock"); lfsLock.Icon = App.CreateMenuIcon("Icons.Lock"); - lfsLock.Click += async (_, e) => + lfsLock.IsEnabled = _repo.Remotes.Count > 0; + if (_repo.Remotes.Count == 1) { - var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Lock(change.Path)); - if (succ) - App.SendNotification(_repo.FullPath, $"Lock file \"{change.Path}\" successfully!"); + lfsLock.Click += async (o, e) => + { + var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Lock(_repo.Remotes[0].Name, change.Path)); + if (succ) + App.SendNotification(_repo.FullPath, $"Lock file \"{change.Path}\" successfully!"); - e.Handled = true; - }; + e.Handled = true; + }; + } + else + { + foreach (var remote in _repo.Remotes) + { + var remoteName = remote.Name; + var lockRemote = new MenuItem(); + lockRemote.Header = remoteName; + lockRemote.Click += async (o, e) => + { + var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Lock(remoteName, change.Path)); + if (succ) + App.SendNotification(_repo.FullPath, $"Lock file \"{change.Path}\" successfully!"); + + e.Handled = true; + }; + lfsLock.Items.Add(lockRemote); + } + } lfs.Items.Add(lfsLock); var lfsUnlock = new MenuItem(); lfsUnlock.Header = App.Text("GitLFS.Locks.Unlock"); lfsUnlock.Icon = App.CreateMenuIcon("Icons.Unlock"); - lfsUnlock.Click += async (_, e) => + lfsUnlock.IsEnabled = _repo.Remotes.Count > 0; + if (_repo.Remotes.Count == 1) { - var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Unlock(change.Path, false)); - if (succ) - App.SendNotification(_repo.FullPath, $"Unlock file \"{change.Path}\" successfully!"); + lfsUnlock.Click += async (o, e) => + { + var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Unlock(_repo.Remotes[0].Name, change.Path, false)); + if (succ) + App.SendNotification(_repo.FullPath, $"Unlock file \"{change.Path}\" successfully!"); - e.Handled = true; - }; + e.Handled = true; + }; + } + else + { + foreach (var remote in _repo.Remotes) + { + var remoteName = remote.Name; + var unlockRemote = new MenuItem(); + unlockRemote.Header = remoteName; + unlockRemote.Click += async (o, e) => + { + var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Unlock(remoteName, change.Path, false)); + if (succ) + App.SendNotification(_repo.FullPath, $"Unlock file \"{change.Path}\" successfully!"); + + e.Handled = true; + }; + lfsUnlock.Items.Add(unlockRemote); + } + } lfs.Items.Add(lfsUnlock); menu.Items.Add(lfs); @@ -926,28 +970,71 @@ namespace SourceGit.ViewModels var lfsLock = new MenuItem(); lfsLock.Header = App.Text("GitLFS.Locks.Lock"); lfsLock.Icon = App.CreateMenuIcon("Icons.Lock"); - lfsLock.Click += async (_, e) => + lfsLock.IsEnabled = _repo.Remotes.Count > 0; + if (_repo.Remotes.Count == 1) { - var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Lock(change.Path)); - if (succ) - App.SendNotification(_repo.FullPath, $"Lock file \"{change.Path}\" successfully!"); + lfsLock.Click += async (o, e) => + { + var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Lock(_repo.Remotes[0].Name, change.Path)); + if (succ) + App.SendNotification(_repo.FullPath, $"Lock file \"{change.Path}\" successfully!"); - e.Handled = true; - }; - lfs.Items.Add(new MenuItem() { Header = "-" }); + e.Handled = true; + }; + } + else + { + foreach (var remote in _repo.Remotes) + { + var remoteName = remote.Name; + var lockRemote = new MenuItem(); + lockRemote.Header = remoteName; + lockRemote.Click += async (o, e) => + { + var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Lock(remoteName, change.Path)); + if (succ) + App.SendNotification(_repo.FullPath, $"Lock file \"{change.Path}\" successfully!"); + + e.Handled = true; + }; + lfsLock.Items.Add(lockRemote); + } + } lfs.Items.Add(lfsLock); var lfsUnlock = new MenuItem(); lfsUnlock.Header = App.Text("GitLFS.Locks.Unlock"); lfsUnlock.Icon = App.CreateMenuIcon("Icons.Unlock"); - lfsUnlock.Click += async (_, e) => + lfsUnlock.IsEnabled = _repo.Remotes.Count > 0; + if (_repo.Remotes.Count == 1) { - var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Unlock(change.Path, false)); - if (succ) - App.SendNotification(_repo.FullPath, $"Unlock file \"{change.Path}\" successfully!"); + lfsUnlock.Click += async (o, e) => + { + var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Unlock(_repo.Remotes[0].Name, change.Path, false)); + if (succ) + App.SendNotification(_repo.FullPath, $"Unlock file \"{change.Path}\" successfully!"); - e.Handled = true; - }; + e.Handled = true; + }; + } + else + { + foreach (var remote in _repo.Remotes) + { + var remoteName = remote.Name; + var unlockRemote = new MenuItem(); + unlockRemote.Header = remoteName; + unlockRemote.Click += async (o, e) => + { + var succ = await Task.Run(() => new Commands.LFS(_repo.FullPath).Unlock(remoteName, change.Path, false)); + if (succ) + App.SendNotification(_repo.FullPath, $"Unlock file \"{change.Path}\" successfully!"); + + e.Handled = true; + }; + lfsUnlock.Items.Add(unlockRemote); + } + } lfs.Items.Add(lfsUnlock); menu.Items.Add(lfs); diff --git a/src/Views/LFSFetch.axaml b/src/Views/LFSFetch.axaml index 4fd8e5cb..4004f028 100644 --- a/src/Views/LFSFetch.axaml +++ b/src/Views/LFSFetch.axaml @@ -5,14 +5,37 @@ xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="SourceGit.Views.LFSFetch"> + x:Class="SourceGit.Views.LFSFetch" + x:DataType="vm:LFSFetch"> - + + + + + + + + + + + + + + + + diff --git a/src/Views/LFSPull.axaml b/src/Views/LFSPull.axaml index f472f567..a56bd45b 100644 --- a/src/Views/LFSPull.axaml +++ b/src/Views/LFSPull.axaml @@ -5,14 +5,37 @@ xmlns:m="using:SourceGit.Models" xmlns:vm="using:SourceGit.ViewModels" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="SourceGit.Views.LFSPull"> + x:Class="SourceGit.Views.LFSPull" + x:DataType="vm:LFSPull"> - + + + + + + + + + + + + + + + + diff --git a/src/Views/LFSPush.axaml b/src/Views/LFSPush.axaml new file mode 100644 index 00000000..080e6926 --- /dev/null +++ b/src/Views/LFSPush.axaml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/LFSPush.axaml.cs b/src/Views/LFSPush.axaml.cs new file mode 100644 index 00000000..5641dfef --- /dev/null +++ b/src/Views/LFSPush.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public partial class LFSPush : UserControl + { + public LFSPush() + { + InitializeComponent(); + } + } +}