Skip to content

Commit

Permalink
Fix gitextensions#3280: Optimize unstage/reset performance by git res…
Browse files Browse the repository at this point in the history
…et/unstage batch files
  • Loading branch information
hieuxlu committed Oct 18, 2019
1 parent 04f193e commit 5fe04ff
Show file tree
Hide file tree
Showing 12 changed files with 308 additions and 126 deletions.
24 changes: 17 additions & 7 deletions GitCommands/ArgumentBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -315,16 +315,24 @@ public static void Add(this ArgumentBuilder builder, [CanBeNull, ItemCanBeNull]
/// <param name="baseLength">Base executable file and command line length.</param>
/// <param name="maxLength">Command line max length. Default is 32767 - 1 on Windows.</param>
/// <returns>Array of batch arguments split by max length.</returns>
public static ArgumentString[] BuildBatchArguments(this ArgumentBuilder builder, IEnumerable<string> arguments, int baseLength, int maxLength = short.MaxValue)
public static List<BatchArgumentItem> BuildBatchArguments(this ArgumentBuilder builder, IEnumerable<string> arguments, int? baseLength = null, int maxLength = short.MaxValue)
{
// 3: double quotes + ' '
// '"git.exe" ' is always included in final command line arguments
if (!baseLength.HasValue)
{
baseLength = AppSettings.GitCommand.Length + 3;
}

var baseArgument = builder.ToString();
if (baseLength + baseArgument.Length >= maxLength)
{
throw new ArgumentException($"Git base command \"{baseArgument}\" always reached max length of {maxLength} characters.", nameof(baseArgument));
}

// Clone command as argument builder
var batches = new List<ArgumentString>();
var batches = new List<BatchArgumentItem>();
var currentBatchItemCount = 0;
var currentArgumentLength = baseArgument.Length;
var lastBatchBuilder = arguments.Aggregate(builder, (currentBatchBuilder, argument) =>
{
Expand All @@ -333,21 +341,23 @@ public static ArgumentString[] BuildBatchArguments(this ArgumentBuilder builder,
if (baseLength + currentArgumentLength + 1 + argument.Length >= maxLength)
{
// Handle abnormal case when base command and a single argument exceed max length
if (currentArgumentLength == baseArgument.Length)
if (currentBatchItemCount == 0)
{
throw new ArgumentException($"Git command \"{currentBatchBuilder}\" always exceeded max length of {maxLength} characters.", nameof(arguments));
}

// Finish current command line
batches.Add(currentBatchBuilder);
batches.Add(new BatchArgumentItem(currentBatchBuilder, currentBatchItemCount));

// Return new argument builder
currentBatchItemCount = 1;
currentArgumentLength = baseArgument.Length + 1 + argument.Length;
return new ArgumentBuilder() { baseArgument, argument };
}

currentBatchBuilder.Add(argument);
currentArgumentLength += argument.Length + 1;
currentBatchItemCount++;
return currentBatchBuilder;
});

Expand All @@ -360,10 +370,10 @@ public static ArgumentString[] BuildBatchArguments(this ArgumentBuilder builder,
// Add last commandline batch
if (!lastBatchBuilder.IsEmpty)
{
batches.Add(lastBatchBuilder.ToString());
batches.Add(new BatchArgumentItem(lastBatchBuilder, currentBatchItemCount));
}

return batches.ToArray();
return batches;
}
}
}
}
24 changes: 24 additions & 0 deletions GitCommands/BatchArgumentItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace GitCommands
{
/// <summary>
/// Result model for batch processing arguments and count of items for batch progress
/// </summary>
public sealed class BatchArgumentItem
{
public BatchArgumentItem(ArgumentString argument, int count)
{
Argument = argument;
BatchItemsCount = count;
}

/// <summary>
/// Batch command line argument
/// </summary>
public ArgumentString Argument { get; }

/// <summary>
/// Count of items in batch, used for batch progress update
/// </summary>
public int BatchItemsCount { get; }
}
}
26 changes: 26 additions & 0 deletions GitCommands/Git/BatchProgressEventArgs.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;

namespace GitCommands
{
/// <summary>
/// Event arguments for batch progress updating
/// </summary>
public sealed class BatchProgressEventArgs : EventArgs
{
public BatchProgressEventArgs(int batchItemsProcessed, bool executionResult)
{
ProcessedCount = batchItemsProcessed;
ExecutionResult = executionResult;
}

/// <summary>
/// Number of items processed in this batch event
/// </summary>
public int ProcessedCount { get; }

/// <summary>
/// Batch execution result
/// </summary>
public bool ExecutionResult { get; }
}
}
52 changes: 48 additions & 4 deletions GitCommands/Git/ExecutableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,38 @@ public static string GetOutput(
() => executable.GetOutputAsync(arguments, input, outputEncoding, cache, stripAnsiEscapeCodes));
}

/// <summary>
/// Launches a process for the executable per batch item and returns its output.
/// </summary>
/// <remarks>
/// This method uses <see cref="GetOutput"/> to get concatenated outputs of multiple commands in batch.
/// </remarks>
/// <param name="executable">The executable from which to launch processes.</param>
/// <param name="batchArguments">The array of batch arguments to pass to the executable</param>
/// <param name="input">Bytes to be written to each process's standard input stream, or <c>null</c> if no input is required.</param>
/// <param name="outputEncoding">The text encoding to use when decoding bytes read from each process's standard output and standard error streams, or <c>null</c> if the default encoding is to be used.</param>
/// <param name="cache">A <see cref="CommandCache"/> to use if command results may be cached, otherwise <c>null</c>.</param>
/// <param name="stripAnsiEscapeCodes">A flag indicating whether ANSI escape codes should be removed from output strings.</param>
/// <returns>The concatenation of standard output and standard error. To receive these outputs separately, use <see cref="Execute"/> instead.</returns>
[NotNull]
[MustUseReturnValue("If output text is not required, use " + nameof(RunCommand) + " instead")]
public static string GetBatchOutput(
this IExecutable executable,
ICollection<BatchArgumentItem> batchArguments = default,
byte[] input = null,
Encoding outputEncoding = null,
CommandCache cache = null,
bool stripAnsiEscapeCodes = true)
{
var sb = new StringBuilder();
foreach (var batch in batchArguments)
{
sb.Append(executable.GetOutput(batch.Argument, input, outputEncoding, cache, stripAnsiEscapeCodes));
}

return sb.ToString();
}

/// <summary>
/// Launches a process for the executable and returns its output.
/// </summary>
Expand Down Expand Up @@ -147,7 +179,7 @@ public static bool RunCommand(
/// </summary>
/// <remarks>
/// This method uses <see cref="RunCommand"/> to execute multiple commands in batch, used in accordance with
/// <see cref="ArgumentBuilderExtensions.BuildBatchArguments(ArgumentBuilder, IEnumerable{string}, int, int)"/>
/// <see cref="ArgumentBuilderExtensions.BuildBatchArguments(ArgumentBuilder, IEnumerable{string}, int?, int)"/>
/// to work around Windows command line length 32767 character limitation
/// <see href="https://docs.microsoft.com/en-us/windows/desktop/api/processthreadsapi/nf-processthreadsapi-createprocessa"/>.
/// </remarks>
Expand All @@ -159,11 +191,23 @@ public static bool RunCommand(
[MustUseReturnValue("Callers should verify that " + nameof(RunBatchCommand) + " returned true")]
public static bool RunBatchCommand(
this IExecutable executable,
ArgumentString[] batchArguments,
ICollection<BatchArgumentItem> batchArguments,
Action<BatchProgressEventArgs> action = null,
byte[] input = null,
bool createWindow = false)
{
return batchArguments.Aggregate(true, (result, arguments) => executable.RunCommand(arguments, input, createWindow) && result);
int total = batchArguments.Sum(item => item.BatchItemsCount);
var result = true;

foreach (var item in batchArguments)
{
result &= executable.RunCommand(item.Argument, input, createWindow);

// Invoke batch progress callback
action?.Invoke(new BatchProgressEventArgs(item.BatchItemsCount, result));
}

return result;
}

/// <summary>
Expand Down Expand Up @@ -358,4 +402,4 @@ private static string CleanString(bool stripAnsiEscapeCodes, [NotNull] string s)
: s;
}
}
}
}
64 changes: 63 additions & 1 deletion GitCommands/Git/GitModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1355,6 +1355,22 @@ public string ResetFile(string file)
});
}

public string ResetFiles(IReadOnlyList<string> files)
{
if (files == null || files.Count == 0)
{
return string.Empty;
}

return _gitExecutable.GetBatchOutput(new GitArgumentBuilder("checkout-index")
{
"--index",
"--force",
"--"
}
.BuildBatchArguments(files.Select(item => item.ToPosixPath().Quote())));
}

/// <summary>
/// Delete index.lock in the current working folder.
/// </summary>
Expand Down Expand Up @@ -1402,7 +1418,7 @@ public void CheckoutFiles(IReadOnlyList<string> files, ObjectId revision, bool f
revision,
"--"
}
.BuildBatchArguments(files.Select(f => f.ToPosixPath().Quote()), AppSettings.GitCommand.Length + 3));
.BuildBatchArguments(files.Select(f => f.ToPosixPath().Quote())));
}

public string RemoveFiles(IReadOnlyList<string> files, bool force)
Expand Down Expand Up @@ -1813,6 +1829,52 @@ public string UnstageFiles(IReadOnlyList<GitItemStatus> files)
return output.ToString();
}

/// <summary>
/// Batch unstage files using <see cref="ExecutableExtensions.RunBatchCommand(IExecutable, ICollection{BatchArgumentItem}, Action{BatchProgressEventArgs}, byte[], bool)"/>
/// </summary>
/// <param name="selectedItems">Selected file items</param>
/// <param name="action">Progress update callback</param>
/// <returns><see langword="true" /> if changes should be rescanned; otherwise <see langword="false" /></returns>.
public bool BatchUnstageFiles(IEnumerable<GitItemStatus> selectedItems, Action<BatchProgressEventArgs> action = null)
{
var files = new List<GitItemStatus>();
var filesToRemove = new List<string>();
var shouldRescanChanges = false;
foreach (var item in selectedItems)
{
if (!item.IsNew)
{
filesToRemove.Add(item.Name);

if (item.IsRenamed)
{
filesToRemove.Add(item.OldName);
}

if (item.IsDeleted)
{
shouldRescanChanges = true;
}
}
else
{
files.Add(item);
}
}

if (filesToRemove.Count > 0)
{
var args = GitCommandHelpers.ResetCmd(ResetMode.ResetIndex, "HEAD");
_gitExecutable.RunBatchCommand(new ArgumentBuilder() { args }
.BuildBatchArguments(filesToRemove),
action);
}

UnstageFiles(files);

return shouldRescanChanges;
}

public async Task<bool> AddInteractiveAsync(GitItemStatus file)
{
var args = new GitArgumentBuilder("add")
Expand Down
2 changes: 2 additions & 0 deletions GitCommands/GitCommands.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
<Compile Include="AppTitleGenerator.cs" />
<Compile Include="ArgumentBuilderExtensions.cs" />
<Compile Include="AsyncLoader.cs" />
<Compile Include="BatchArgumentItem.cs" />
<Compile Include="CommandStatus.cs" />
<Compile Include="CommitData.cs" />
<Compile Include="CommitDataManager.cs" />
Expand All @@ -90,6 +91,7 @@
<Compile Include="GitRevisionInfoProvider.cs" />
<Compile Include="Git\AheadBehindData.cs" />
<Compile Include="Git\AheadBehindDataProvider.cs" />
<Compile Include="Git\BatchProgressEventArgs.cs" />
<Compile Include="Git\ConflictData.cs" />
<Compile Include="Git\ConflictedFileData.cs" />
<Compile Include="Git\ExecutableExtensions.cs" />
Expand Down
Loading

0 comments on commit 5fe04ff

Please sign in to comment.