Skip to content

Commit

Permalink
Merge pull request #135 from AshleighAdams/shadow
Browse files Browse the repository at this point in the history
Added shadow repo functionality for shallow clones
  • Loading branch information
AshleighAdams authored Jun 23, 2023
2 parents fcc94d8 + 08e2e11 commit 1616781
Show file tree
Hide file tree
Showing 10 changed files with 363 additions and 168 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ See [docs/VersionCalculation.md](docs/VersionCalculation.md) for further reading
| A command to test whether a tag should be ignored via exit code. | -f, --filter-tags, VerliteFilterTags | |
| The remote endpoint to use when fetching tags and commits. | -r, --remote, VerliteRemote | origin |
| Generate version strings and embed them via a source generator. | VerliteEmbedVersion | true |
| Use a shadow repo (partial, only commits) to read commit history. | --enable-shadow-repo, EnableShadowRepo | false |

## Comparison with GitVersion

Expand Down Expand Up @@ -262,6 +263,12 @@ public static class Version
}
```

<h3 id="shadow-repo">What is a shadow repo?</h3>

Using a shadow repo is an experimental method of allowing shallow depth=1 clones without causing
any issues with Verlite. A special repo within your repositories `.git` directory will be created
and updated as needed, where only commits are fetched (via `--filter=tree:0`).

[verlite-msbuild-badge]: https://img.shields.io/nuget/v/Verlite.MsBuild?label=Verlite.MsBuild
[verlite-msbuild-link]: https://www.nuget.org/packages/Verlite.MsBuild
[verlite-cli-badge]: https://img.shields.io/nuget/v/Verlite.MsBuild?label=Verlite.CLI
Expand Down
6 changes: 6 additions & 0 deletions src/Verlite.CLI/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,10 @@ public static async Task<int> Main(string[] args)
aliases: new[] { "--enable-lightweight-tags" },
getDefaultValue: () => false,
description: "Create a lightweight tag instead of fetching the remote's."),
new Option<bool>(
aliases: new[] { "--enable-shadow-repo" },
getDefaultValue: () => false,
description: "Use a shadow repro for shallow clones using filter branches to fetch only commits."),
new Option<AutoIncrement>(
aliases: new[] { "--auto-increment", "-a" },
isDefault: true,
Expand Down Expand Up @@ -118,6 +122,7 @@ private async static Task RootCommandAsync(
Show show,
bool autoFetch,
bool enableLightweightTags,
bool enableShadowRepo,
AutoIncrement autoIncrement,
string filterTags,
string remote,
Expand Down Expand Up @@ -155,6 +160,7 @@ private async static Task RootCommandAsync(
using var repo = await GitRepoInspector.FromPath(sourceDirectory, opts.Remote, log, commandRunner);
repo.CanDeepen = autoFetch;
repo.EnableLightweightTags = enableLightweightTags;
repo.EnableShadowRepo = enableShadowRepo;

ITagFilter? tagFilter = null;
if (!string.IsNullOrWhiteSpace(filterTags))
Expand Down
10 changes: 8 additions & 2 deletions src/Verlite.Core/Command.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,14 @@ public static class Command
proc.Start();
proc.StandardInput.Close();

string stdout = await proc.StandardOutput.ReadToEndAsync();
string stderr = await proc.StandardError.ReadToEndAsync();
Task<string> stdoutTask = proc.StandardOutput.ReadToEndAsync();
Task<string> stderrTask = proc.StandardError.ReadToEndAsync();

await Task.WhenAll(stdoutTask, stderrTask, exitPromise.Task);

string stdout = await stdoutTask;
string stderr = await stderrTask;
int exitCode = await exitPromise.Task;

if (await exitPromise.Task != 0)
throw new CommandException(proc.ExitCode, stdout, stderr);
Expand Down
196 changes: 196 additions & 0 deletions src/Verlite.Core/GitCatFileProcess.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Verlite
{
internal sealed class GitCatFileProcess : IDisposable
{
internal Process? CatFileProcess { get; set; }
internal Process? ShadowCatFileProcess { get; set; }
private UTF8Encoding Encoding { get; } = new(
encoderShouldEmitUTF8Identifier: false,
throwOnInvalidBytes: false);

private char[] WhitespaceChars { get; } = new[]
{
'\t',
'\v',
'\f',
'\r',
'\n',
' ',
'\u0085', // NEXT LINE
'\u00A0', // NBSP
'\u1680', // OGHAM SPACE MARK
'\u2000', // EN QUAD
'\u2001', // EM QUAD
'\u2002', // EN SPACE
'\u2003', // EM SPACE
'\u2004', // THREE-PER-EM SPACE
'\u2005', // FOUR-PER-EM SPACE
'\u2006', // SIX-PER-EM SPACE
'\u2007', // FIGURE SPACE
'\u2008', // PUNCTUATION SPACE
'\u2009', // THIN SPACE
'\u200A', // HAIR SPACE
'\u2028', // LINE SEPARATOR
'\u2029', // PARAGRAPH SEPARATOR
'\u202F', // NARROW NBSP
'\u205F', // MEDIUM MATHEMATICAL SPACE
'\u3000', // IDEOGRAPHIC SPACE
'\u180E', // MONGOLIAN VOWEL SEPARATOR
'\u200B', // ZERO WIDTH SPACE
'\u200C', // ZERO WIDTH NON-JOINER
'\u200D', // ZERO WIDTH JOINER
'\u2060', // WORD JOINER
'\uFEFF', // ZERO WIDTH NON-BREAKING SPACE
};

private ILogger? Log { get; }
private string Root { get; }
private string Name { get; }
public GitCatFileProcess(ILogger? log, string root, string name)
{
Root = root;
Name = name;
}

private readonly SemaphoreSlim catFileSemaphore = new(initialCount: 1, maxCount: 1);
public async Task<string?> ReadObject(string type, string id)
{
await catFileSemaphore.WaitAsync();
try
{

bool isFirst = false;
if (CatFileProcess is null)
{
isFirst = true;

Log?.Verbatim($"{Root} $ git cat-file --batch");
ProcessStartInfo info = new()
{
FileName = "git",
Arguments = "cat-file --batch",
WorkingDirectory = Root,
RedirectStandardError = false,
RedirectStandardOutput = true,
StandardOutputEncoding = Encoding,
RedirectStandardInput = true,
UseShellExecute = false,
};
CatFileProcess = Process.Start(info);
}

var (cin, cout) = (CatFileProcess.StandardInput, CatFileProcess.StandardOutput.BaseStream);

// if this git call is forwarded onto another shell script,
// then it's possible to query git before it's ready, but once
// it does respond, it's ready to be used.
if (isFirst)
{
Log?.Verbatim($"First run: awaiting cat-file startup ({Name})");
await cin.WriteLineAsync(" ");

using var cts = new CancellationTokenSource();

var timeout = Task.Delay(5000, cts.Token);
var gotBack = ReadLineAsync(cout);

var completedTask = await Task.WhenAny(timeout, gotBack);

if (completedTask != timeout)
cts.Cancel();
else
throw new UnknownGitException($"The git cat-file process timed out ({Name})");

var result = await gotBack;
if (result.Trim(WhitespaceChars) != "missing")
throw new UnknownGitException($"The git cat-file process returned unexpected output: {result} ({Name})");
}

using var cts2 = new CancellationTokenSource();
var timeout2 = Task.Delay(30_000, cts2.Token);

Log?.Verbatim($"git cat-file < {id} ({Name})");
if (await Task.WhenAny(cin.WriteLineAsync(id), timeout2) == timeout2)
throw new UnknownGitException($"The git cat-file process write timed out ({Name})");

var readLineTask = ReadLineAsync(cout);
if (await Task.WhenAny(readLineTask, timeout2) == timeout2)
throw new UnknownGitException($"The git cat-file process read timed out ({Name})");

string line = await readLineTask;
Log?.Verbatim($"git cat-file > {line} ({Name})");
string[] response = line.Trim(WhitespaceChars).Split(' ');


if (response[0] != id)
throw new UnknownGitException($"The returned blob hash did not match ({Name})");
else if (response[1] == "missing")
return null;
else if (response[1] != type)
throw new UnknownGitException($"Blob for {id} expected {type} but was {response[1]} ({Name})");

var length = int.Parse(response[2], CultureInfo.InvariantCulture);
var buffer = new byte[length];

if (await Task.WhenAny(cout.ReadAsync(buffer, 0, length), timeout2) == timeout2)
throw new UnknownGitException($"The git cat-file process read block timed out ({Name})");
if (await Task.WhenAny(ReadLineAsync(cout), timeout2) == timeout2)
throw new UnknownGitException($"The git cat-file process read block line timed out ({Name})");

cts2.Cancel();
return Encoding.GetString(buffer);
}
catch (Exception ex)
{
throw new UnknownGitException($"Failed to communicate with the git cat-file process: {ex.Message} ({Name})");
}
finally
{
catFileSemaphore.Release();
}
}

private async Task<string> ReadLineAsync(Stream stream, int maxLength = 128)
{
var buffer = new byte[maxLength];

int length = 0;
for (; length < maxLength; length++)
{
if (await stream.ReadAsync(buffer, length, 1) == -1)
break;
else if (buffer[length] == '\n')
break;
}

return Encoding.GetString(buffer, 0, length);
}

public void Dispose()
{
try
{
CatFileProcess?.StandardInput.Close();
CatFileProcess?.Kill();
CatFileProcess?.Close();
catFileSemaphore.Dispose();
}
catch (IOException) { } // process may already be terminated
catch (System.ComponentModel.Win32Exception) { }
finally { }
}

internal Process? GetProcess()
{
return CatFileProcess;
}
}
}
Loading

0 comments on commit 1616781

Please sign in to comment.