Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 185 additions & 10 deletions src/Commands/QueryFileContent.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,44 @@
using System;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading.Tasks;

#nullable enable

namespace SourceGit.Commands
{
public static class QueryFileContent
{
public static async Task<Stream> RunAsync(string repo, string revision, string file)
public static Task<Stream> RunIndexAsync(string repo, string file)
{
var starter = new ProcessStartInfo();
starter.WorkingDirectory = repo;
starter.FileName = Native.OS.GitExecutable;
starter.Arguments = $"show {revision}:{file.Quoted()}";
starter.UseShellExecute = false;
starter.CreateNoWindow = true;
starter.WindowStyle = ProcessWindowStyle.Hidden;
starter.RedirectStandardOutput = true;
// Read from index (staged content).
return RunObjectSpecAsync(repo, $":{file.Quoted()}");
}

public static Task<Stream> RunAsync(string repo, string revision, string file)
{
// Read from a specific revision.
return RunObjectSpecAsync(repo, $"{revision}:{file.Quoted()}");
}

private static async Task<Stream> RunObjectSpecAsync(string repo, string objectSpec)
{
// Shared git show runner for both index and revision reads.
var starter = new ProcessStartInfo
{
WorkingDirectory = repo,
FileName = Native.OS.GitExecutable,
Arguments = $"show {objectSpec}",
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
RedirectStandardOutput = true,
};

var stream = new MemoryStream();
var sw = Stopwatch.StartNew();
try
{
using var proc = Process.Start(starter)!;
Expand All @@ -34,6 +54,161 @@ public static async Task<Stream> RunAsync(string repo, string revision, string f
return stream;
}

// Batch read file contents using git cat-file --batch.
// maxBytesPerObject: if > 0, read only first N bytes of each object (for performance).
public static async Task<Dictionary<string, byte[]>> RunBatchAsync(string repo, IReadOnlyList<string> objectSpecs, int maxBytesPerObject = 0)
{
var results = new Dictionary<string, byte[]>(StringComparer.Ordinal);
if (objectSpecs == null || objectSpecs.Count == 0)
return results;

var starter = new ProcessStartInfo
{
WorkingDirectory = repo,
FileName = Native.OS.GitExecutable,
Arguments = "cat-file --batch",
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
RedirectStandardInput = true,
RedirectStandardOutput = true,
};

try
{
using var proc = Process.Start(starter)!;

// Write requests in background to avoid deadlock (pipe buffer full)
var writeTask = Task.Run(async () =>
{
await using var input = proc.StandardInput;
foreach (var spec in objectSpecs)
{
await input.WriteLineAsync(spec).ConfigureAwait(false);
}
});

await using var output = proc.StandardOutput.BaseStream;
for (int i = 0; i < objectSpecs.Count; i++)
{
var header = await ReadBatchHeaderLineAsync(output).ConfigureAwait(false);
if (header == null)
break;

if (header.EndsWith(" missing", StringComparison.Ordinal))
continue;

var size = ParseBatchObjectSize(header);
if (size > 0)
{
// If maxBytesPerObject is set, read only that many bytes and skip the rest.
var bytesToRead = (maxBytesPerObject > 0 && size > maxBytesPerObject)
? maxBytesPerObject
: size;
var bytesToSkip = size - bytesToRead;

var data = await ReadExactBytesAsync(output, bytesToRead).ConfigureAwait(false);
if (data != null)
{
results[objectSpecs[i]] = data;
}

// Skip remaining bytes if we limited the read.
if (bytesToSkip > 0)
{
await SkipBytesAsync(output, bytesToSkip).ConfigureAwait(false);
}
}

// Consume trailing newline after object content (even for size 0).
_ = await ReadSingleByteAsync(output).ConfigureAwait(false);
}

// Ensure writing is finished (should be, or implies error)
await writeTask.ConfigureAwait(false);
await proc.WaitForExitAsync().ConfigureAwait(false);
}
catch (Exception e)
{
App.RaiseException(repo, $"Failed to query batch file content: {e}");
}

return results;
}

private static int ParseBatchObjectSize(string header)
{
// Header format: "<sha1> <type> <size>" or "<spec> missing"
var lastSpace = header.LastIndexOf(' ');
if (lastSpace <= 0 || lastSpace == header.Length - 1)
return 0;

if (int.TryParse(header.AsSpan(lastSpace + 1), out var size))
return size;

return 0;
}

private static async Task<string?> ReadBatchHeaderLineAsync(Stream stream)
{
var buffer = new MemoryStream();
while (true)
{
int value = await ReadSingleByteAsync(stream).ConfigureAwait(false);
if (value == -1)
break;

if (value == '\n')
break;

buffer.WriteByte((byte)value);
}

if (buffer.Length == 0)
return null;

var line = Encoding.ASCII.GetString(buffer.ToArray());
return line.EndsWith('\r') ? line[..^1] : line;
}

private static async Task<byte[]?> ReadExactBytesAsync(Stream stream, int length)
{
var buffer = new byte[length];
var totalRead = 0;
while (totalRead < length)
{
var read = await stream.ReadAsync(buffer.AsMemory(totalRead, length - totalRead)).ConfigureAwait(false);
if (read <= 0)
return null;

totalRead += read;
}

return buffer;
}

private static async Task SkipBytesAsync(Stream stream, int length)
{
// Use a small buffer to skip bytes efficiently.
var buffer = new byte[Math.Min(length, 8192)];
var remaining = length;
while (remaining > 0)
{
var toRead = Math.Min(remaining, buffer.Length);
var read = await stream.ReadAsync(buffer.AsMemory(0, toRead)).ConfigureAwait(false);
if (read <= 0)
break;
remaining -= read;
}
}

private static async Task<int> ReadSingleByteAsync(Stream stream)
{
var buffer = new byte[1];
var read = await stream.ReadAsync(buffer.AsMemory(0, 1)).ConfigureAwait(false);
return read == 0 ? -1 : buffer[0];
}

public static async Task<Stream> FromLFSAsync(string repo, string oid, long size)
{
var starter = new ProcessStartInfo();
Expand Down
38 changes: 38 additions & 0 deletions src/Converters/OFPAConverters.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using Avalonia.Data.Converters;

namespace SourceGit.Converters
{
public class PathToDisplayNameConverter : IMultiValueConverter
{
public object Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
{
if (values.Count < 2)
return "";

string path = values[0] as string ?? string.Empty;
var decodedPaths = values[1] as IReadOnlyDictionary<string, string>;

if (decodedPaths != null &&
decodedPaths.TryGetValue(path, out var decoded) &&
!string.IsNullOrEmpty(decoded))
{
return decoded;
}

if (parameter as string == "PureFileName")
return Path.GetFileName(path);

return path;
}
}

public static class OFPAConverters
{
public static readonly PathToDisplayNameConverter PathToDisplayName = new();
}
}
12 changes: 12 additions & 0 deletions src/Models/RepositorySettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,18 @@ public bool IncludeUntrackedInLocalChanges
set;
} = true;

public bool EnableOFPADecoding
{
get;
set;
} = false;

public bool EnableUnrealEngineSupport
{
get;
set;
} = false;

public bool EnableForceOnFetch
{
get;
Expand Down
1 change: 1 addition & 0 deletions src/Resources/Icons.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,5 @@
<StreamGeometry x:Key="Icons.Worktree">M853 267H514c-4 0-6-2-9-4l-38-66c-13-21-38-36-64-36H171c-41 0-75 34-75 75v555c0 41 34 75 75 75h683c41 0 75-34 75-75V341c0-41-34-75-75-75zm-683-43h233c4 0 6 2 9 4l38 66c13 21 38 36 64 36H853c6 0 11 4 11 11v75h-704V235c0-6 4-11 11-11zm683 576H171c-6 0-11-4-11-11V480h704V789c0 6-4 11-11 11z</StreamGeometry>
<StreamGeometry x:Key="Icons.Worktree.Add">M896 96 614 96c-58 0-128-19-179-51C422 38 390 19 358 19L262 19 128 19c-70 0-128 58-128 128l0 736c0 70 58 128 128 128l768 0c70 0 128-58 128-128L1024 224C1024 154 966 96 896 96zM704 685 544 685l0 160c0 19-13 32-32 32s-32-13-32-32l0-160L320 685c-19 0-32-13-32-32 0-19 13-32 32-32l160 0L480 461c0-19 13-32 32-32s32 13 32 32l0 160L704 621c19 0 32 13 32 32C736 666 723 685 704 685zM890 326 102 326 102 250c0-32 32-64 64-64l659 0c38 0 64 32 64 64L890 326z</StreamGeometry>
<StreamGeometry x:Key="Icons.Worktrees">M1182 527a91 91 0 00-88-117H92a91 91 0 00-88 117l137 441A80 80 0 00217 1024h752a80 80 0 0076-56zM133 295a31 31 0 0031 31h858a31 31 0 0031-31A93 93 0 00959 203H226a93 93 0 00-94 92zM359 123h467a31 31 0 0031-31A92 92 0 00765 0H421a92 92 0 00-92 92 31 31 0 0031 31z</StreamGeometry>
<StreamGeometry x:Key="Icons.Unreal">M803.7,995.81c156.5-73.92,205.56-210.43,216.6-263.61c-57.22,58.6-120.53,118-163.11,76.88c0,0-2.33-219.45-2.33-309.43c0-121,114.75-211.18,114.75-211.18c-63.11,11.24-138.89,33.71-219.33,112.65c-7.26,7.2-14.14,14.76-20.62,22.67c-34.47-26.39-79.14-18.48-79.14-18.48c24.14,13.26,48.23,51.88,48.23,83.85v314.26c0,0-52.63,46.3-93.19,46.3c-9.14,0.07-18.17-2.05-26.33-6.18c-8.16-4.13-15.21-10.15-20.56-17.56c-3.21-4.19-5.87-8.78-7.91-13.65V424.07c-11.99,9.89-52.51,18.04-52.51-49.22c0-41.79,30.11-91.6,83.73-122.15c-73.63,11.23-142.59,43.04-198.92,91.76c-42.8,36.98-77.03,82.85-100.31,134.4c-23.28,51.55-35.06,107.55-34.51,164.12c0,0,39.21-122.51,88.32-133.83c7.15-1.88,14.65-2.07,21.89-0.54c7.24,1.53,14.02,4.72,19.81,9.34c5.79,4.61,10.41,10.51,13.51,17.23c3.1,6.72,4.59,14.07,4.34,21.46V844.3c0,29.16-18.8,35.53-36.17,35.22c-11.77-0.83-23.4-3.02-34.66-6.53c35.86,48.53,82.46,88.12,136.15,115.66c53.69,27.54,113.03,42.29,173.37,43.1l106.05-106.6L803.7,995.81z</StreamGeometry>
</ResourceDictionary>
4 changes: 4 additions & 0 deletions src/Resources/Locales/en_US.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -945,4 +945,8 @@
<x:String x:Key="Text.Worktree.Remove" xml:space="preserve">Remove</x:String>
<x:String x:Key="Text.Worktree.Unlock" xml:space="preserve">Unlock</x:String>
<x:String x:Key="Text.Yes" xml:space="preserve">YES</x:String>
<x:String x:Key="Text.WorkingCopy.EnableOFPADecoding" xml:space="preserve">Unreal Engine: show OFPA actor names</x:String>
<x:String x:Key="Text.Configure.UnrealEngine" xml:space="preserve">Unreal Engine</x:String>
<x:String x:Key="Text.Configure.UnrealEngine.Support" xml:space="preserve">Enable Unreal Engine support</x:String>
<x:String x:Key="Text.Configure.UnrealEngine.Support.Tip" xml:space="preserve">Enables Unreal Engine-specific features such as OFPA file name decoding.</x:String>
</ResourceDictionary>
8 changes: 6 additions & 2 deletions src/Resources/Locales/ru_RU.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -924,8 +924,8 @@
<x:String x:Key="Text.WorkingCopy.Conflicts.Resolved" xml:space="preserve">КОНФЛИКТЫ ФАЙЛОВ РАЗРЕШЕНЫ</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.UseMine" xml:space="preserve">ИСПОЛЬЗОВАТЬ МОИ</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.UseTheirs" xml:space="preserve">ИСПОЛЬЗОВАТЬ ИХ</x:String>
<x:String x:Key="Text.WorkingCopy.IncludeUntracked" xml:space="preserve">ВКЛЮЧИТЬ НЕОТСЛЕЖИВАЕМЫЕ ФАЙЛЫ</x:String>
<x:String x:Key="Text.WorkingCopy.NoCommitHistories" xml:space="preserve">НЕТ ПОСЛЕДНИХ ВХОДНЫХ СООБЩЕНИЙ</x:String>
<x:String x:Key="Text.WorkingCopy.IncludeUntracked" xml:space="preserve">Показывать неотслеживаемые файлы</x:String>
<x:String x:Key="Text.WorkingCopy.NoCommitHistories" xml:space="preserve">Нет недавних сообщений коммитов</x:String>
<x:String x:Key="Text.WorkingCopy.NoCommitTemplates" xml:space="preserve">НЕТ ШАБЛОНОВ РЕВИЗИИ</x:String>
<x:String x:Key="Text.WorkingCopy.NoVerify" xml:space="preserve">Не проверять</x:String>
<x:String x:Key="Text.WorkingCopy.ResetAuthor" xml:space="preserve">Сбросить автора</x:String>
Expand All @@ -947,4 +947,8 @@
<x:String x:Key="Text.Worktree.Remove" xml:space="preserve">Удалить</x:String>
<x:String x:Key="Text.Worktree.Unlock" xml:space="preserve">Разблокировать</x:String>
<x:String x:Key="Text.Yes" xml:space="preserve">Да</x:String>
<x:String x:Key="Text.WorkingCopy.EnableOFPADecoding" xml:space="preserve">Unreal Engine: show OFPA actor names</x:String>
<x:String x:Key="Text.Configure.UnrealEngine" xml:space="preserve">Unreal Engine</x:String>
<x:String x:Key="Text.Configure.UnrealEngine.Support" xml:space="preserve">Enable Unreal Engine support</x:String>
<x:String x:Key="Text.Configure.UnrealEngine.Support.Tip" xml:space="preserve">Enables Unreal Engine-specific features such as OFPA file name decoding.</x:String>
</ResourceDictionary>
4 changes: 4 additions & 0 deletions src/Resources/Locales/zh_CN.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -949,4 +949,8 @@
<x:String x:Key="Text.Worktree.Remove" xml:space="preserve">移除工作树</x:String>
<x:String x:Key="Text.Worktree.Unlock" xml:space="preserve">解除工作树锁定</x:String>
<x:String x:Key="Text.Yes" xml:space="preserve">好的</x:String>
<x:String x:Key="Text.WorkingCopy.EnableOFPADecoding" xml:space="preserve">Unreal Engine: show OFPA actor names</x:String>
<x:String x:Key="Text.Configure.UnrealEngine" xml:space="preserve">Unreal Engine</x:String>
<x:String x:Key="Text.Configure.UnrealEngine.Support" xml:space="preserve">Enable Unreal Engine support</x:String>
<x:String x:Key="Text.Configure.UnrealEngine.Support.Tip" xml:space="preserve">Enables Unreal Engine-specific features such as OFPA file name decoding.</x:String>
</ResourceDictionary>
1 change: 1 addition & 0 deletions src/Resources/Locales/zh_TW.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,7 @@
<x:String x:Key="Text.WorkingCopy.Conflicts.UseMine" xml:space="preserve">使用我方版本 (ours)</x:String>
<x:String x:Key="Text.WorkingCopy.Conflicts.UseTheirs" xml:space="preserve">使用對方版本 (theirs)</x:String>
<x:String x:Key="Text.WorkingCopy.IncludeUntracked" xml:space="preserve">顯示未追蹤檔案</x:String>
<x:String x:Key="Text.WorkingCopy.EnableOFPADecoding" xml:space="preserve">解碼 Unreal Engine OFPA 檔案名稱</x:String>
<x:String x:Key="Text.WorkingCopy.NoCommitHistories" xml:space="preserve">沒有提交訊息記錄</x:String>
<x:String x:Key="Text.WorkingCopy.NoCommitTemplates" xml:space="preserve">沒有可套用的提交訊息範本</x:String>
<x:String x:Key="Text.WorkingCopy.NoVerify" xml:space="preserve">繞過 Hooks 檢查</x:String>
Expand Down
4 changes: 4 additions & 0 deletions src/SourceGit.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
<AssemblyMetadata Include="BuildDate" Value="$([System.DateTime]::Now.ToString('o'))"/>
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="SourceGit.Tests" />
</ItemGroup>

<ItemGroup>
<AvaloniaResource Include="App.ico" />
<AvaloniaResource Include="Resources/Fonts/*" />
Expand Down
Loading