feature(Submodule): supports git submodule

This commit is contained in:
leo 2020-07-22 20:14:39 +08:00
parent 789231fdc4
commit 0132fc496b
8 changed files with 359 additions and 13 deletions

View file

@ -23,6 +23,7 @@ namespace SourceGit.Git {
[XmlIgnore] public Action OnStashChanged = null;
[XmlIgnore] public Action OnBranchChanged = null;
[XmlIgnore] public Action OnCommitsChanged = null;
[XmlIgnore] public Action OnSubmoduleChanged = null;
#endregion
#region PROPERTIES_SAVED
@ -57,6 +58,8 @@ namespace SourceGit.Git {
#endregion
#region PROPERTIES_RUNTIME
[XmlIgnore] public Repository Parent = null;
private List<Remote> cachedRemotes = new List<Remote>();
private List<Branch> cachedBranches = new List<Branch>();
private List<Tag> cachedTags = new List<Tag>();
@ -306,6 +309,7 @@ namespace SourceGit.Git {
OnStashChanged = null;
OnWorkingCopyChanged = null;
OnNavigateCommit = null;
OnSubmoduleChanged = null;
cachedBranches.Clear();
cachedRemotes.Clear();
@ -718,7 +722,7 @@ namespace SourceGit.Git {
/// <returns>Changes.</returns>
public List<Change> LocalChanges() {
List<Change> changes = new List<Change>();
RunCommand("status -uall --porcelain", line => {
RunCommand("status -uall --ignore-submodules=dirty --porcelain", line => {
if (!string.IsNullOrEmpty(line)) {
var change = Change.Parse(line);
if (change != null) changes.Add(change);
@ -833,6 +837,46 @@ namespace SourceGit.Git {
return stashes;
}
/// <summary>
/// Get all submodules
/// </summary>
/// <returns></returns>
public List<string> Submodules() {
var test = new Regex(@"^[\-\+ ][0-9a-f]+\s(.*)\(.*\)$");
var modules = new List<string>();
var errs = RunCommand("submodule status", line => {
var match = test.Match(line);
if (!match.Success) return;
modules.Add(match.Groups[1].Value);
});
return modules;
}
/// <summary>
/// Add submodule
/// </summary>
/// <param name="url"></param>
/// <param name="localPath"></param>
/// <param name="recursive"></param>
/// <param name="onProgress"></param>
public void AddSubmodule(string url, string localPath, bool recursive, Action<string> onProgress) {
isWatcherDisabled = true;
var errs = RunCommand($"submodule add {url} {localPath}", onProgress, true);
if (errs == null) {
if (recursive) RunCommand($"submodule update --init --recursive -- {localPath}", onProgress, true);
OnWorkingCopyChanged?.Invoke();
OnSubmoduleChanged?.Invoke();
} else {
App.RaiseError(errs);
}
isWatcherDisabled = false;
}
/// <summary>
/// Blame file.
/// </summary>

View file

@ -121,4 +121,16 @@ namespace SourceGit.Helpers {
return !succ ? new ValidationResult(false, "Invalid path for patch file") : ValidationResult.ValidResult;
}
}
/// <summary>
/// Required for submodule path.
/// </summary>
public class SubmodulePathRequiredRule : ValidationRule {
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
var regex = new Regex(@"^[\w\-\._/]+$");
var path = value as string;
var succ = !string.IsNullOrEmpty(path) && regex.IsMatch(path.Trim());
return !succ ? new ValidationResult(false, "Invalid path for submodules") : ValidationResult.ValidResult;
}
}
}

View file

@ -1,6 +1,7 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Geometry x:Key="Icon.Git">M1004.824 466.4L557.72 19.328c-25.728-25.76-67.488-25.76-93.28 0L360.568 123.2l78.176 78.176c12.544-5.984 26.56-9.376 41.376-9.376 53.024 0 96 42.976 96 96 0 14.816-3.36 28.864-9.376 41.376l127.968 127.968c12.544-5.984 26.56-9.376 41.376-9.376 53.024 0 96 42.976 96 96s-42.976 96-96 96-96-42.976-96-96c0-14.816 3.36-28.864 9.376-41.376L521.496 374.624a88.837 88.837 0 0 1-9.376 3.872v266.976c37.28 13.184 64 48.704 64 90.528 0 53.024-42.976 96-96 96s-96-42.976-96-96c0-41.792 26.72-77.344 64-90.528V378.496c-37.28-13.184-64-48.704-64-90.528 0-14.816 3.36-28.864 9.376-41.376l-78.176-78.176L19.416 464.288c-25.76 25.792-25.76 67.52 0 93.28l447.136 447.072c25.728 25.76 67.488 25.76 93.28 0l444.992-444.992c25.76-25.76 25.76-67.552 0-93.28z</Geometry>
<Geometry x:Key="Icon.Submodule">M557.696 545.347L789.873 402.66c23.998-14.999 31.297-46.496 16.398-70.493-14.798-23.798-45.995-31.197-69.993-16.699L506.501 456.555 277.123 315.37c-24.098-14.798-55.595-7.3-70.493 16.799-14.799 24.097-7.3 55.594 16.798 70.493l231.778 142.586V819.12c0 28.297 22.897 51.195 51.195 51.195 28.297 0 51.195-22.898 51.195-51.195V545.347h0.1zM506.5 0l443.356 255.975v511.95L506.501 1023.9 63.144 767.925v-511.95L506.5 0z</Geometry>
<Geometry x:Key="Icon.Minimize">F1M0,6L0,9 9,9 9,6 0,6z</Geometry>
<Geometry x:Key="Icon.Maximize">F1M0,0L0,9 9,9 9,0 0,0 0,3 8,3 8,8 1,8 1,3z</Geometry>

View file

@ -0,0 +1,72 @@
<UserControl x:Class="SourceGit.UI.AddSubmodule"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:SourceGit.Helpers"
mc:Ignorable="d"
d:DesignHeight="192" d:DesignWidth="500" Height="192" Width="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="32"/>
<RowDefinition Height="16"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.ColumnSpan="2" FontWeight="DemiBold" FontSize="18" Content="Add Submodule"/>
<Label Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Content="URL :"/>
<TextBox x:Name="txtRepoUrl" Grid.Row="2" Grid.Column="1"
Height="24"
helpers:TextBoxHelper.Placeholder="Git Repository URL">
<TextBox.Text>
<Binding Path="RepoURL" ElementName="me" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
<Binding.ValidationRules>
<helpers:RemoteUriRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Label Grid.Row="3" Grid.Column="0" HorizontalAlignment="Right" Content="Parent Folder :"/>
<TextBox Grid.Row="3" Grid.Column="1"
x:Name="txtPath"
Height="24"
helpers:TextBoxHelper.Placeholder="Relative foler to store this module. Optional.">
<TextBox.Text>
<Binding Path="LocalPath" ElementName="me" UpdateSourceTrigger="PropertyChanged" Mode="TwoWay">
<Binding.ValidationRules>
<helpers:SubmodulePathRequiredRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<CheckBox Grid.Row="4" Grid.Column="1"
x:Name="chkRecursive"
IsChecked="True"
Content="Fetch nested submodules"/>
<Grid Grid.Row="6" Grid.ColumnSpan="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="8"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" Click="Sure" Content="SURE" Style="{StaticResource Style.Button.AccentBordered}"/>
<Button Grid.Column="3" Click="Cancel" Content="CANCEL" Style="{StaticResource Style.Button.Bordered}"/>
</Grid>
</Grid>
</UserControl>

View file

@ -0,0 +1,71 @@
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace SourceGit.UI {
/// <summary>
/// Dialog to add new submodule.
/// </summary>
public partial class AddSubmodule : UserControl {
private Git.Repository repo = null;
/// <summary>
/// Submodule's repository URL.
/// </summary>
public string RepoURL { get; set; }
/// <summary>
/// Submodule's relative path.
/// </summary>
public string LocalPath { get; set; }
/// <summary>
/// Constructor.
/// </summary>
/// <param name="opened"></param>
public AddSubmodule(Git.Repository opened) {
repo = opened;
InitializeComponent();
}
/// <summary>
/// Show this dialog.
/// </summary>
/// <param name="repo"></param>
public static void Show(Git.Repository repo) {
PopupManager.Show(new AddSubmodule(repo));
}
#region EVENTS
private void SelectFolder(object sender, RoutedEventArgs e) {
var dialog = new System.Windows.Forms.FolderBrowserDialog();
dialog.Description = "Select Folder To Clone Repository";
dialog.SelectedPath = repo.Path;
dialog.ShowNewFolderButton = true;
if (dialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) {
txtPath.Text = dialog.SelectedPath;
}
}
private async void Sure(object sender, RoutedEventArgs e) {
txtRepoUrl.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(txtRepoUrl)) return;
txtPath.GetBindingExpression(TextBox.TextProperty).UpdateSource();
if (Validation.GetHasError(txtPath)) return;
var recursive = chkRecursive.IsChecked == true;
PopupManager.Lock();
await Task.Run(() => repo.AddSubmodule(RepoURL, LocalPath, recursive, PopupManager.UpdateStatus));
PopupManager.Close(true);
}
private void Cancel(object sender, RoutedEventArgs e) {
PopupManager.Close();
}
#endregion
}
}

View file

@ -51,9 +51,18 @@
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Manager}"/>
<Label Content="Repositories"/>
<Path Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Navigator}"/>
</StackPanel>
</Button>
<Button x:Name="btnParent" Click="GotoParent" ToolTip="Open Parent Repository" Padding="0" Margin="6,0,0,0" Visibility="Collapsed">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Git}"/>
<Label x:Name="txtParent"/>
<Path Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Navigator}"/>
</StackPanel>
</Button>
<Path Width="12" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Navigator}"/>
<Path Margin="6,0,0,0" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Git}"/>
<Label x:Name="repoName"/>
</StackPanel>
@ -133,13 +142,16 @@
<!-- Left panel -->
<Grid Grid.Column="0" x:Name="main" Background="{StaticResource Brush.BG4}">
<Grid.RowDefinitions>
<RowDefinition Height="24"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="24"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="24"/>
<RowDefinition Height="*"/>
<RowDefinition Height="24"/>
<RowDefinition Height="1"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="24"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
@ -194,7 +206,7 @@
</ListView>
<!-- LOCAL BRANCHES -->
<Grid Grid.Row="2" Margin="4,0,0,0">
<Grid Grid.Row="2" Margin="4,0,2,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
@ -275,7 +287,7 @@
</TreeView>
<!-- REMOTES -->
<Grid Grid.Row="4" Margin="4,0,0,0">
<Grid Grid.Row="4" Margin="4,0,2,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
@ -362,7 +374,7 @@
Grid.Row="6"
Style="{StaticResource Style.ToggleButton.Expender}"
IsChecked="{Binding Source={x:Static source:App.Preference}, Path=UIShowTags, Mode=TwoWay}">
<Grid Margin="4,0,0,0">
<Grid Margin="4,0,2,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
@ -374,8 +386,9 @@
</Button>
</Grid>
</ToggleButton>
<Rectangle Grid.Row="7" Height="1" Fill="{StaticResource Brush.BG3}"/>
<DataGrid
Grid.Row="7"
Grid.Row="8"
x:Name="tagList"
Visibility="{Binding ElementName=tagListToggle, Path=IsChecked, Converter={StaticResource Bool2Collapsed}}"
Background="{StaticResource Brush.BG3}"
@ -383,12 +396,12 @@
Height="200"
LostFocus="TagLostFocus"
SelectionChanged="TagSelectionChanged"
ContextMenuOpening="TagContextMenuOpening"
ContextMenuOpening="TagContextMenuOpening"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
SelectionMode="Single"
SelectionUnit="FullRow">
<DataGrid.Resources>
<DataGrid.Resources>
<Style x:Key="Style.DataGridText.TagName" TargetType="{x:Type TextBlock}">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Foreground" Value="{StaticResource Brush.FG}"/>
@ -421,6 +434,63 @@
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<!-- SUBMODULES -->
<ToggleButton
x:Name="submoduleListToggle"
Grid.Row="9"
Style="{StaticResource Style.ToggleButton.Expender}"
IsChecked="False">
<Grid Margin="4,0,2,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="16"/>
</Grid.ColumnDefinitions>
<Label Grid.Column="0" x:Name="submoduleCount" Content="SUBMODULES" Style="{StaticResource Style.Label.GroupHeader}"/>
<Button Grid.Column="1" Click="OpenAddSubmodule" ToolTip="ADD SUBMODULE">
<Path Width="14" Height="14" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Submodule}"/>
</Button>
</Grid>
</ToggleButton>
<DataGrid
Grid.Row="11"
x:Name="submoduleList"
Visibility="{Binding ElementName=submoduleListToggle, Path=IsChecked, Converter={StaticResource Bool2Collapsed}}"
Background="{StaticResource Brush.BG3}"
RowHeight="24"
Height="200"
LostFocus="SubmoduleLostFocus"
ContextMenuOpening="SubmoduleContextMenuOpening"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Auto"
SelectionMode="Single"
SelectionUnit="FullRow">
<DataGrid.Resources>
<Style x:Key="Style.DataGridText.SubmodulePath" TargetType="{x:Type TextBlock}">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="Foreground" Value="{StaticResource Brush.FG}"/>
</Style>
</DataGrid.Resources>
<DataGrid.RowStyle>
<Style TargetType="{x:Type DataGridRow}" BasedOn="{StaticResource Style.DataGridRow}">
<EventSetter Event="MouseDoubleClick" Handler="SubmoduleMouseDoubleClick"/>
</Style>
</DataGrid.RowStyle>
<DataGrid.Columns>
<DataGridTemplateColumn Width="26">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Path Width="10" Style="{StaticResource Style.Icon}" Data="{StaticResource Icon.Submodule}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Width="*" IsReadOnly="True" Binding="{Binding}" ElementStyle="{StaticResource Style.DataGridText.SubmodulePath}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
<!-- Splitter -->

View file

@ -55,7 +55,8 @@ namespace SourceGit.UI {
opened.OnTagChanged = UpdateTags;
opened.OnStashChanged = UpdateStashes;
opened.OnBranchChanged = () => UpdateBranches(false);
opened.OnCommitsChanged = UpdateHistories;
opened.OnCommitsChanged = UpdateHistories;
opened.OnSubmoduleChanged = UpdateSubmodules;
opened.OnNavigateCommit = commit => {
Dispatcher.Invoke(() => {
workspace.SelectedItem = historiesSwitch;
@ -70,11 +71,19 @@ namespace SourceGit.UI {
histories.Repo = opened;
commits.Repo = opened;
if (repo.Parent != null) {
btnParent.Visibility = Visibility.Visible;
txtParent.Content = repo.Parent.Name;
} else {
btnParent.Visibility = Visibility.Collapsed;
}
UpdateBranches();
UpdateHistories();
UpdateLocalChanges();
UpdateStashes();
UpdateTags();
UpdateSubmodules();
}
#region DATA_UPDATE
@ -270,6 +279,16 @@ namespace SourceGit.UI {
});
}
private void UpdateSubmodules() {
Task.Run(() => {
var submodules = repo.Submodules();
Dispatcher.Invoke(() => {
submoduleCount.Content = $"SUBMODULES ({submodules.Count})";
submoduleList.ItemsSource = submodules;
});
});
}
private void Cleanup(object sender, RoutedEventArgs e) {
localBranchTree.ItemsSource = null;
remoteBranchTree.ItemsSource = null;
@ -290,6 +309,12 @@ namespace SourceGit.UI {
repo.Close();
}
private void GotoParent(object sender, RoutedEventArgs e) {
if (repo.Parent == null) return;
repo.Parent.Open();
e.Handled = true;
}
private void OpenFetch(object sender, RoutedEventArgs e) {
Fetch.Show(repo);
}
@ -897,6 +922,58 @@ namespace SourceGit.UI {
}
#endregion
#region SUBMODULES
private void OpenAddSubmodule(object sender, RoutedEventArgs e) {
AddSubmodule.Show(repo);
}
private void SubmoduleLostFocus(object sender, RoutedEventArgs e) {
(sender as DataGrid).UnselectAll();
}
private void SubmoduleContextMenuOpening(object sender, ContextMenuEventArgs e) {
var path = (sender as DataGrid).SelectedItem as string;
if (path == null) return;
var open = new MenuItem();
open.Header = "Open Submodule Repository";
open.Click += (o, ev) => {
var sub = new Git.Repository();
sub.Path = Path.Combine(repo.Path, path);
sub.Name = Path.GetFileName(path);
sub.Parent = repo;
sub.Open();
ev.Handled = true;
};
var copy = new MenuItem();
copy.Header = "Copy Relative Path";
copy.Click += (o, ev) => {
Clipboard.SetText(path);
ev.Handled = true;
};
var menu = new ContextMenu();
menu.Items.Add(open);
menu.Items.Add(copy);
menu.IsOpen = true;
e.Handled = true;
}
private void SubmoduleMouseDoubleClick(object sender, MouseButtonEventArgs e) {
var path = (sender as DataGridRow).DataContext as string;
if (path == null) return;
var sub = new Git.Repository();
sub.Path = Path.Combine(repo.Path, path);
sub.Name = Path.GetFileName(path);
sub.Parent = repo;
sub.Open();
}
#endregion
#region TREES
private TreeViewItem FindTreeViewItem(ItemsControl item, BranchNode node) {
if (item == null) return null;

View file

@ -53,7 +53,6 @@ namespace SourceGit.UI {
/// <param name="repo"></param>
private void ShowDashboard(Git.Repository repo) {
Dispatcher.Invoke(() => {
if (body.Content is Dashboard) return;
body.Content = new Dashboard(repo);
});
}