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
6 changes: 6 additions & 0 deletions GVFS/GVFS.FunctionalTests.LockHolder/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ public class Program
{
public static void Main(string[] args)
{
if (args.Length > 0 && args[0] == "--echo-arguments")
{
Console.Write(string.Join("\0", args, 1, args.Length - 1));
return;
}

if (args.Length > 0 && args[0] == "--wait-for-event")
{
string eventName = Environment.GetEnvironmentVariable("GVFS_TEST_HOOK_WAIT_EVENT_NAME");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,21 @@ public void GitCommandWaitsWhileAnotherIsRunning(string command)
[TestCase("count-objects -vH")]
[TestCase("ls-remote --get-url origin")]
[TestCase("range-diff HEAD~1..HEAD HEAD~1..HEAD")]
[TestCase("reflog")]
[TestCase("reflog show --all")]
[TestCase("reflog list")]
[TestCase("reflog exists HEAD")]
[TestCase("describe --always HEAD")]
[TestCase("grep -n -e . HEAD -- Readme.md")]
[TestCase("grep --cached -e . -- Readme.md")]
[TestCase("refs list --count=1")]
[TestCase("merge-file -p .git/HEAD .git/HEAD .git/HEAD")]
[TestCase("merge-file --object-id HEAD:Readme.md HEAD:Readme.md HEAD:Readme.md")]
[TestCase("merge-tree --write-tree HEAD HEAD")]
[TestCase("notes list")]
[TestCase("replace -l")]
[TestCase("format-patch --stdout -1 HEAD")]
[TestCase("var GIT_AUTHOR_IDENT")]
[TestCase("shortlog -sn HEAD")]
[TestCase("show-branch --list")]
public void ReadOnlyCommandDoesNotWaitForLock(string command)
Expand All @@ -128,6 +143,188 @@ public void ReadOnlyCommandDoesNotWaitForLock(string command)
});
}

[TestCase("describe --dirty --always")]
[TestCase("describe --broken --always")]
[TestCase("describe --di --always")]
[TestCase("grep -nOcat -e . -- Readme.md")]
[TestCase("grep --open=cat -e . -- Readme.md")]
[TestCase("refs migrate -h")]
public void UnsafeCommandModeStillWaitsForLock(string command)
{
File.ReadAllText(this.Enlistment.GetVirtualPathTo("Readme.md"));
ProcessResult result = this.RunWithLockHolder(command);
result.ExitCode.ShouldEqual(0, result.Output + result.Errors);
result.Errors.ShouldContain(ExpectedStatusWaitingText);
}

[TestCase(false)]
[TestCase(true)]
public void MergeFileOnlyBypassesLockWhenOutputModeRemainsEnabled(bool disableStdout)
{
string inputPath = Path.Combine(this.Enlistment.EnlistmentRoot, "merge-file-" + Guid.NewGuid().ToString("N"));
File.WriteAllText(inputPath, "merge input\n");
try
{
string command = $"merge-file --stdout {(disableStdout ? "--no-stdout" : string.Empty)} \"{inputPath}\" \"{inputPath}\" \"{inputPath}\"";
ProcessResult result = this.RunWithLockHolder(command, shouldWait: disableStdout);
result.ExitCode.ShouldEqual(0, result.Output + result.Errors);
result.Errors.Contains(ExpectedStatusWaitingText).ShouldEqual(disableStdout);
File.ReadAllText(inputPath).ShouldEqual("merge input\n");
}
finally
{
File.Delete(inputPath);
}
}

[TestCase]
public void ReflogWritesPreserveIndexAndTrackedContentUnderLockContention()
{
string refName = "refs/heads/gvfs-reflog-test-" + Guid.NewGuid().ToString("N");
string filePath = this.Enlistment.GetVirtualPathTo("Readme.md");
string indexPath = this.Enlistment.GetDotGitPath("index");
string originalContent = File.ReadAllText(filePath);
string oldObject = GitProcess.Invoke(this.Enlistment.RepoRoot, "rev-parse HEAD~1").Trim();
string newObject = GitProcess.Invoke(this.Enlistment.RepoRoot, "rev-parse HEAD").Trim();
try
{
File.WriteAllText(filePath, originalContent + "\nstaged reflog test\n");
GitHelpers.CheckGitCommandAgainstGVFSRepo(this.Enlistment.RepoRoot, "add -- Readme.md");
string dirtyContent = originalContent + "\nunstaged reflog test\n";
File.WriteAllText(filePath, dirtyContent);
GitHelpers.CheckGitCommandAgainstGVFSRepo(
this.Enlistment.RepoRoot,
$"update-ref --create-reflog -m first {refName} {oldObject}");
GitHelpers.CheckGitCommandAgainstGVFSRepo(
this.Enlistment.RepoRoot,
$"update-ref -m second {refName} {newObject}");

GitHelpers.RunWithGVFSLock(
this.Enlistment,
() =>
{
byte[] index = File.ReadAllBytes(indexPath);
DateTime indexWriteTime = File.GetLastWriteTimeUtc(indexPath);
(string Command, string ExpectedRef, int ReflogEntries)[] commands =
{
($"reflog write {refName} {newObject} {newObject} appended", newObject, 3),
($"reflog delete --rewrite --updateref {refName}@{{0}}", newObject, 2),
($"reflog delete --rewrite --updateref {refName}@{{0}}", oldObject, 1),
($"reflog expire --stale-fix --rewrite --updateref --expire=never --expire-unreachable=never {refName}", oldObject, 1),
($"reflog expire --stale-fix --rewrite --updateref --expire=all {refName}", oldObject, 0),
($"reflog drop {refName}", oldObject, -1),
};

foreach ((string command, string expectedRef, int reflogEntries) in commands)
{
ProcessResult result = GitProcess.InvokeProcess(
this.Enlistment.RepoRoot,
command,
timeoutMs: AcquireGVFSLockTimeout);
result.ExitCode.ShouldEqual(0, result.Output + result.Errors);
result.Errors.ShouldNotContain(false, ExpectedStatusWaitingText);
GitProcess.Invoke(this.Enlistment.RepoRoot, "rev-parse " + refName).Trim().ShouldEqual(expectedRef);
if (reflogEntries < 0)
{
GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "reflog exists " + refName).ExitCode.ShouldEqual(1);
}
else
{
ProcessResult reflog = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "reflog show --format=%H " + refName);
reflog.ExitCode.ShouldEqual(0, reflog.Errors);
reflog.Output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Length.ShouldEqual(reflogEntries);
}

File.ReadAllBytes(indexPath).ShouldMatchInOrder(index);
File.GetLastWriteTimeUtc(indexPath).ShouldEqual(indexWriteTime);
File.ReadAllText(filePath).ShouldEqual(dirtyContent);
}
});
}
finally
{
GitHelpers.CheckGitCommandAgainstGVFSRepo(this.Enlistment.RepoRoot, "reset --hard");
GitHelpers.CheckGitCommandAgainstGVFSRepo(this.Enlistment.RepoRoot, "update-ref -d " + refName);
}
}

[TestCase("")]
[TestCase("with space")]
[TestCase("with\ttab")]
[TestCase("with\"quote")]
[TestCase("trailing space slash\\")]
[TestCase("before\\\\\"quote")]
public void HookLoaderPreservesArgumentBoundaries(string argument)
{
string loaderPath = this.Enlistment.GetDotGitPath("hooks", "pre-command.exe");
string hookListPath = this.Enlistment.GetDotGitPath("hooks", "pre-command.hooks");
string originalHookList = File.ReadAllText(hookListPath);
try
{
File.WriteAllText(hookListPath, $"\"{GitHelpers.LockHolderCommandPath}\" --echo-arguments");
ProcessStartInfo loader = new ProcessStartInfo(loaderPath)
{
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
};
loader.ArgumentList.Add("first");
loader.ArgumentList.Add(argument);
loader.ArgumentList.Add("last");
loader.Environment.Remove("GITHOOKSLOADER_PERFTRACE");
ProcessResult result = ProcessHelper.Run(loader, timeoutMs: AcquireGVFSLockTimeout);
result.ExitCode.ShouldEqual(0, result.Errors);
result.Output.Split('\0').ShouldMatchInOrder("pre-command", "first", argument, "last");
}
finally
{
File.WriteAllText(hookListPath, originalHookList);
}
}

[TestCase("rm -f")]
[TestCase("foo --")]
public void GrepOptionValuesCannotHidePagerDispatch(string pattern)
{
File.ReadAllText(this.Enlistment.GetVirtualPathTo("Readme.md"));
string[] arguments = { "grep", "-e", pattern, "-Ocat", "-e", ".", "--", "Readme.md" };
ProcessResult result = this.RunWithLockHolder(string.Empty, argumentList: arguments);
// Git grep returns 1 when no line matches.
Assert.That(result.ExitCode, Is.InRange(0, 1), result.Output + result.Errors);
result.Errors.ShouldContain(ExpectedStatusWaitingText);
}

[TestCase("v1* --")]
[TestCase("v\"")]
public void DescribeOptionValuesCannotHideIndexRefresh(string pattern)
{
string[] arguments = { "describe", "--match", pattern, "--dirty", "--always" };
ProcessResult result = this.RunWithLockHolder(string.Empty, argumentList: arguments);
result.ExitCode.ShouldEqual(0, result.Output + result.Errors);
result.Errors.ShouldContain(ExpectedStatusWaitingText);
}

[TestCase("ours -p")]
[TestCase("ours\t-p")]
[TestCase("ours\"")]
public void MergeFileLabelsCannotEnableAnOutputMode(string label)
{
string inputPath = Path.Combine(this.Enlistment.EnlistmentRoot, "merge-label-" + Guid.NewGuid().ToString("N"));
File.WriteAllText(inputPath, "merge input\n");
try
{
string[] arguments = { "merge-file", "-L", label, inputPath, inputPath, inputPath };
ProcessResult result = this.RunWithLockHolder(string.Empty, argumentList: arguments);
result.ExitCode.ShouldEqual(0, result.Output + result.Errors);
result.Errors.ShouldContain(ExpectedStatusWaitingText);
File.ReadAllText(inputPath).ShouldEqual("merge input\n");
}
finally
{
File.Delete(inputPath);
}
}

[TestCase, Order(5)]
public void GitAliasNamedAfterKnownCommandAcquiresLock()
{
Expand Down Expand Up @@ -567,7 +764,11 @@ private ProcessStartInfo CreateGitStartInfo(string command, Dictionary<string, s
return startInfo;
}

private ProcessResult RunWithLockHolder(string command, Dictionary<string, string> environment = null, bool shouldWait = true)
private ProcessResult RunWithLockHolder(
string command,
Dictionary<string, string> environment = null,
bool shouldWait = true,
string[] argumentList = null)
{
using (Process holder = new Process())
using (Process git = new Process())
Expand All @@ -593,6 +794,14 @@ private ProcessResult RunWithLockHolder(string command, Dictionary<string, strin
this.Enlistment.WaitForLock("GVFS.FunctionalTests.LockHolder (PID:" + holder.Id + ")")
.ShouldBeTrue("The expected lock-holder process did not acquire the lock.");
git.StartInfo = this.CreateGitStartInfo(command, environment);
if (argumentList != null)
{
foreach (string argument in argumentList)
{
git.StartInfo.ArgumentList.Add(argument);
}
}

git.OutputDataReceived += (sender, args) =>
{
if (args.Data != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public void AllowsPlaceholderCreationForDefaultAnnotate()
[TestCase("hash-object --stdin")]
[TestCase("index-pack --stdin")]
[TestCase("name-rev --stdin")]
[TestCase("notes copy --stdin")]
[TestCase("rev-list --stdin --quiet --all")]
[TestCase("update-ref --stdin")]
public void AllowsPlaceholderCreationWhileGitCommandIsRunning(string commandToRun)
Expand All @@ -50,7 +51,6 @@ public void AllowsPlaceholderCreationWhileGitCommandIsRunning(string commandToRu

[TestCase("checkout-index --stdin")]
[TestCase("fetch-pack --stdin URL")]
[TestCase("notes copy --stdin")]
[TestCase("reset --stdin")]
[TestCase("send-pack --stdin URL")]
[TestCase("update-index --stdin")]
Expand All @@ -62,11 +62,16 @@ public void BlocksPlaceholderCreationWhileGitCommandIsRunning(string commandToRu
[TestCase("worktree list")]
[TestCase("worktree list --porcelain")]
[TestCase("worktree list --porcelain -z")]
public void AllowsPlaceholderCreationWhileWorktreeListIsRunning(string command)
[TestCase("reflog")]
[TestCase("describe --always HEAD")]
[TestCase("grep -q -e . HEAD -- Readme.md")]
[TestCase("refs list --count=1")]
[TestCase("merge-file --object-id HEAD:Readme.md HEAD:Readme.md HEAD:Readme.md")]
public void AllowsPlaceholderCreationWhileExemptCommandIsRunning(string command)
{
string hookListPath = this.Enlistment.GetVirtualPathTo(".git", "hooks", "post-command.hooks");
string originalHookList = File.ReadAllText(hookListPath);
string eventName = "GVFS.WorktreeList." + Guid.NewGuid().ToString("N");
string eventName = "GVFS.ExemptCommand." + Guid.NewGuid().ToString("N");
string expectedContent = File.ReadAllText(Path.Combine(this.ControlGitRepo.RootPath, FileToRead));

using (EventWaitHandle ready = new EventWaitHandle(false, EventResetMode.ManualReset, eventName + ".Ready"))
Expand Down
Loading
Loading