Skip to content

Commit 0a11f3e

Browse files
committed
Added worktree detection.
1 parent 226e2c1 commit 0a11f3e

File tree

8 files changed

+143
-73
lines changed

8 files changed

+143
-73
lines changed

GitReader.Core/Internal/RepositoryAccessor.cs

Lines changed: 89 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,65 @@ public static async Task<CandidateRepositoryPaths> DetectLocalRepositoryPathAsyn
173173
}
174174
}
175175

176+
internal readonly struct CandidateFilePath
177+
{
178+
public readonly string GitPath;
179+
public readonly string BasePath;
180+
public readonly string Path;
181+
182+
public CandidateFilePath(string gitPath, string basePath, string path)
183+
{
184+
this.GitPath = gitPath;
185+
this.BasePath = basePath;
186+
this.Path = path;
187+
}
188+
}
189+
190+
#if NET45_OR_GREATER || NETSTANDARD || NETCOREAPP
191+
public static async ValueTask<CandidateFilePath?> GetCandidateFilePathAsync(
192+
#else
193+
public static async Task<CandidateFilePath?> GetCandidateFilePathAsync(
194+
#endif
195+
Repository repository,
196+
string relativePathFromGitPath,
197+
CancellationToken ct)
198+
{
199+
foreach (var gitPath in repository.TryingPathList)
200+
{
201+
var candidatePath = repository.fileSystem.Combine(gitPath, relativePathFromGitPath);
202+
if (await repository.fileSystem.IsFileExistsAsync(candidatePath, ct))
203+
{
204+
return new(gitPath, repository.fileSystem.GetDirectoryPath(candidatePath), candidatePath);
205+
}
206+
}
207+
return null;
208+
}
209+
210+
#if NET45_OR_GREATER || NETSTANDARD || NETCOREAPP
211+
public static async ValueTask<CandidateFilePath[]> GetCandidateFilePathsAsync(
212+
#else
213+
public static async Task<CandidateFilePath[]> GetCandidateFilePathsAsync(
214+
#endif
215+
Repository repository,
216+
string relativePathFromGitPath,
217+
string match,
218+
CancellationToken ct) =>
219+
(await Utilities.WhenAll(repository.TryingPathList.
220+
#if NET45_OR_GREATER || NETSTANDARD || NETCOREAPP
221+
Select((Func<string, ValueTask<CandidateFilePath[]>>)(async gitPath =>
222+
#else
223+
Select((async gitPath =>
224+
#endif
225+
{
226+
var basePath = repository.fileSystem.Combine(gitPath, relativePathFromGitPath);
227+
var candidatePaths = await repository.fileSystem.GetFilesAsync(basePath, match, ct);
228+
return candidatePaths.
229+
Select(candidatePath => new CandidateFilePath(gitPath, basePath, candidatePath)).
230+
ToArray();
231+
})))).
232+
SelectMany(paths => paths).
233+
ToArray();
234+
176235
public static string GetReferenceTypeName(ReferenceTypes type) =>
177236
type switch
178237
{
@@ -216,13 +275,12 @@ public static async Task<ReadOnlyDictionary<string, string>> ReadRemoteReference
216275
Repository repository,
217276
CancellationToken ct)
218277
{
219-
var path = repository.fileSystem.Combine(repository.GitPath, "config");
220-
if (!await repository.fileSystem.IsFileExistsAsync(path, ct))
278+
if (await GetCandidateFilePathAsync(repository, "config", ct) is not { } cp)
221279
{
222280
return new(new());
223281
}
224282

225-
using var fs = await repository.fileSystem.OpenAsync(path, false, ct);
283+
using var fs = await repository.fileSystem.OpenAsync(cp.Path, false, ct);
226284
var tr = new AsyncTextReader(fs);
227285

228286
var remotes = new Dictionary<string, string>();
@@ -264,13 +322,12 @@ public static async Task<ReferenceCache> ReadFetchHeadsAsync(
264322
var remoteNameByUrl = repository.remoteUrls.
265323
ToDictionary(entry => entry.Value, entry => entry.Key);
266324

267-
var path = repository.fileSystem.Combine(repository.GitPath, "FETCH_HEAD");
268-
if (!await repository.fileSystem.IsFileExistsAsync(path, ct))
325+
if (await GetCandidateFilePathAsync(repository, "FETCH_HEAD", ct) is not { } cp)
269326
{
270327
return new(new(new()), new(new()), new(new()));
271328
}
272329

273-
using var fs = await repository.fileSystem.OpenAsync(path, false, ct);
330+
using var fs = await repository.fileSystem.OpenAsync(cp.Path, false, ct);
274331
var tr = new AsyncTextReader(fs);
275332

276333
var remoteBranches = new Dictionary<string, Hash>();
@@ -356,13 +413,12 @@ public static async Task<ReferenceCache> ReadPackedRefsAsync(
356413
Repository repository,
357414
CancellationToken ct)
358415
{
359-
var path = repository.fileSystem.Combine(repository.GitPath, "packed-refs");
360-
if (!await repository.fileSystem.IsFileExistsAsync(path, ct))
416+
if (await GetCandidateFilePathAsync(repository, "packed-refs", ct) is not { } cp)
361417
{
362418
return new(new(new()), new(new()), new(new()));
363419
}
364420

365-
using var fs = await repository.fileSystem.OpenAsync(path, false, ct);
421+
using var fs = await repository.fileSystem.OpenAsync(cp.Path, false, ct);
366422
var tr = new AsyncTextReader(fs);
367423

368424
var branches = new Dictionary<string, Hash>();
@@ -469,10 +525,8 @@ public static async Task<ReferenceCache> ReadPackedRefsAsync(
469525
Replace("refs/tags/", string.Empty);
470526
names.Add(name);
471527

472-
var path = repository.fileSystem.Combine(
473-
repository.GitPath,
474-
currentLocation.Replace('/', Path.DirectorySeparatorChar));
475-
if (!await repository.fileSystem.IsFileExistsAsync(path, ct))
528+
if (await GetCandidateFilePathAsync(
529+
repository, currentLocation.Replace('/', Path.DirectorySeparatorChar), ct) is not { } cp)
476530
{
477531
if (currentLocation.StartsWith("refs/heads/"))
478532
{
@@ -498,7 +552,7 @@ public static async Task<ReferenceCache> ReadPackedRefsAsync(
498552
return null;
499553
}
500554

501-
using var fs = await repository.fileSystem.OpenAsync(path, false, ct);
555+
using var fs = await repository.fileSystem.OpenAsync(cp.Path, false, ct);
502556
var tr = new AsyncTextReader(fs);
503557

504558
var line = await tr.ReadLineAsync(ct);
@@ -528,14 +582,13 @@ public static Task<PrimitiveReflogEntry[]> ReadReflogEntriesAsync(
528582
public static async Task<PrimitiveReflogEntry[]> ReadReflogEntriesAsync(
529583
Repository repository, string refRelativePath, CancellationToken ct)
530584
{
531-
var path = repository.fileSystem.Combine(
532-
repository.GitPath, "logs", refRelativePath);
533-
if (!await repository.fileSystem.IsFileExistsAsync(path, ct))
585+
if (await GetCandidateFilePathAsync(
586+
repository, repository.fileSystem.Combine("logs", refRelativePath), ct) is not { } cp)
534587
{
535588
return new PrimitiveReflogEntry[]{};
536589
}
537590

538-
using var fs = await repository.fileSystem.OpenAsync(path, false, ct);
591+
using var fs = await repository.fileSystem.OpenAsync(cp.Path, false, ct);
539592
var tr = new AsyncTextReader(fs);
540593

541594
var entries = new List<PrimitiveReflogEntry>();
@@ -561,37 +614,32 @@ public static async Task<PrimitiveReference[]> ReadReferencesAsync(
561614
ReferenceTypes type,
562615
CancellationToken ct)
563616
{
564-
var headsPath = repository.fileSystem.Combine(
565-
repository.GitPath, "refs", GetReferenceTypeName(type));
566-
var files = await repository.fileSystem.GetFilesAsync(
567-
headsPath, "*", ct);
617+
var candidatePaths = await GetCandidateFilePathsAsync(
618+
repository, repository.fileSystem.Combine("refs", GetReferenceTypeName(type)), "*", ct);
568619
var references = (await Utilities.WhenAll(
569-
files.
620+
candidatePaths.
570621
#if NET45_OR_GREATER || NETSTANDARD || NETCOREAPP
571-
Select((Func<string, ValueTask<PrimitiveReference?>>)(async path =>
622+
Select((Func<CandidateFilePath, ValueTask<PrimitiveReference?>>)(async cp =>
572623
#else
573-
Select(async path =>
624+
Select((async cp =>
574625
#endif
575626
{
576627
if (await ReadHashAsync(
577628
repository,
578-
path.Substring(repository.GitPath.Length + 1),
629+
cp.Path.Substring(cp.GitPath.Length + 1),
579630
ct) is not { } results)
580631
{
581632
return default(PrimitiveReference?);
582633
}
583634
else
584635
{
585636
return new(
586-
path.Substring(headsPath.Length + 1).Replace(Path.DirectorySeparatorChar, '/'),
587-
path.Substring(repository.GitPath.Length + 1).Replace(Path.DirectorySeparatorChar, '/'),
637+
cp.Path.Substring(cp.BasePath.Length + 1).Replace(Path.DirectorySeparatorChar, '/'),
638+
cp.Path.Substring(cp.GitPath.Length + 1).Replace(Path.DirectorySeparatorChar, '/'),
588639
results.Hash);
589640
}
590641
}
591-
#if NET45_OR_GREATER || NETSTANDARD || NETCOREAPP
592-
)
593-
#endif
594-
))).
642+
)))).
595643
CollectValue(reference => reference).
596644
ToDictionary(reference => reference.Name);
597645

@@ -632,38 +680,33 @@ public static async Task<PrimitiveTagReference[]> ReadTagReferencesAsync(
632680
Repository repository,
633681
CancellationToken ct)
634682
{
635-
var headsPath = repository.fileSystem.Combine(
636-
repository.GitPath, "refs", "tags");
637-
var files = await repository.fileSystem.GetFilesAsync(
638-
headsPath, "*", ct);
683+
var candidatePaths = await GetCandidateFilePathsAsync(
684+
repository, repository.fileSystem.Combine("refs", "tags"), "*", ct);
639685
var references = (await Utilities.WhenAll(
640-
files.
686+
candidatePaths.
641687
#if NET45_OR_GREATER || NETSTANDARD || NETCOREAPP
642-
Select((Func<string, ValueTask<PrimitiveTagReference?>>)(async path =>
688+
Select((Func<CandidateFilePath, ValueTask<PrimitiveTagReference?>>)(async cp =>
643689
#else
644-
Select(async path =>
690+
Select((async cp =>
645691
#endif
646692
{
647693
if (await ReadHashAsync(
648694
repository,
649-
path.Substring(repository.GitPath.Length + 1),
695+
cp.Path.Substring(cp.GitPath.Length + 1),
650696
ct) is not { } results)
651697
{
652698
return default(PrimitiveTagReference?);
653699
}
654700
else
655701
{
656702
return new(
657-
path.Substring(headsPath.Length + 1).Replace(Path.DirectorySeparatorChar, '/'),
658-
path.Substring(repository.GitPath.Length + 1).Replace(Path.DirectorySeparatorChar, '/'),
703+
cp.Path.Substring(cp.BasePath.Length + 1).Replace(Path.DirectorySeparatorChar, '/'),
704+
cp.Path.Substring(cp.GitPath.Length + 1).Replace(Path.DirectorySeparatorChar, '/'),
659705
results.Hash,
660706
null);
661707
}
662708
}
663-
#if NET45_OR_GREATER || NETSTANDARD || NETCOREAPP
664-
)
665-
#endif
666-
))).
709+
)))).
667710
CollectValue(reference => reference).
668711
ToDictionary(reference => reference.Name);
669712

@@ -765,7 +808,6 @@ private static ObjectAccessor GetObjectAccessor(
765808
}
766809
}
767810

768-
769811
public static async Task<PrimitiveCommit?> ReadCommitAsync(
770812
Repository repository,
771813
Hash hash, CancellationToken ct)

GitReader.Core/Primitive/PrimitiveRepositoryFacade.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,17 +130,16 @@ public static async Task<PrimitiveRepository> OpenSubModuleAsync(
130130
{
131131
throw new ArgumentException($"Could not use non-submodule entry: {treePath[treePath.Length - 1]}");
132132
}
133-
134-
var repositoryPath = repository.fileSystem.Combine(
135-
repository.GitPath, "modules",
136-
repository.fileSystem.Combine(treePath.Select(tree => tree.Name).ToArray()));
137-
138-
if (!await repository.fileSystem.IsFileExistsAsync(
139-
repository.fileSystem.Combine(repositoryPath, "config"), ct))
133+
134+
if (await RepositoryAccessor.GetCandidateFilePathAsync(
135+
repository, repository.fileSystem.Combine(
136+
"modules",
137+
repository.fileSystem.Combine(treePath.Select(tree => tree.Name).ToArray()),
138+
"config"), ct) is not { } cp)
140139
{
141140
throw new ArgumentException("Submodule repository does not exist.");
142141
}
143142

144-
return await InternalOpenPrimitiveAsync(repositoryPath, [], repository.fileSystem, ct);
143+
return await InternalOpenPrimitiveAsync(cp.BasePath, [], repository.fileSystem, ct);
145144
}
146145
}

GitReader.Core/Repository.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111
using GitReader.Internal;
1212
using GitReader.IO;
1313
using System;
14+
using System.Collections.Generic;
15+
using System.ComponentModel;
16+
using System.Linq;
1417
using System.Threading;
18+
using System.Threading.Tasks;
1519

1620
namespace GitReader;
1721

@@ -30,7 +34,7 @@ private protected Repository(
3034
IFileSystem fileSystem)
3135
{
3236
this.GitPath = gitPath;
33-
this.GitPaths = [..alternativeGitPaths, gitPath];
37+
this.TryingPathList = [..alternativeGitPaths, gitPath];
3438
this.fileSystem = fileSystem;
3539
this.fileStreamCache = new(this.fileSystem);
3640
this.objectAccessor = new(this.pool, this.fileSystem, this.fileStreamCache, gitPath);
@@ -49,8 +53,9 @@ public void Dispose()
4953
}
5054

5155
public string GitPath { get; }
52-
53-
internal string[] GitPaths { get; }
56+
57+
[EditorBrowsable(EditorBrowsableState.Advanced)]
58+
public string[] TryingPathList { get; }
5459

5560
public ReadOnlyDictionary<string, string> RemoteUrls =>
5661
this.remoteUrls;

GitReader.Core/Structures/StructuredRepositoryFacade.cs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -424,22 +424,20 @@ public static async Task<StructuredRepository> OpenSubModuleAsync(
424424
{
425425
var (repository, _) = GetRelatedRepository(subModule);
426426

427-
var repositoryPath = repository.fileSystem.Combine(
428-
repository.GitPath,
429-
"modules",
430-
repository.fileSystem.Combine(subModule.
431-
Traverse<TreeEntry>(tree => tree.Parent as TreeEntry).
432-
Select(tree => tree.Name).
433-
Reverse().
434-
ToArray()));
435-
436-
if (!await repository.fileSystem.IsFileExistsAsync(
437-
repository.fileSystem.Combine(repositoryPath, "config"), ct))
427+
if (await RepositoryAccessor.GetCandidateFilePathAsync(
428+
repository, repository.fileSystem.Combine(
429+
"modules",
430+
repository.fileSystem.Combine(subModule.
431+
Traverse<TreeEntry>(tree => tree.Parent as TreeEntry).
432+
Select(tree => tree.Name).
433+
Reverse().
434+
ToArray()),
435+
"config"), ct) is not { } cp)
438436
{
439437
throw new ArgumentException("Submodule repository does not exist.");
440438
}
441439

442-
return await InternalOpenStructuredAsync(repositoryPath, [], repository.fileSystem, ct);
440+
return await InternalOpenStructuredAsync(cp.BasePath, [], repository.fileSystem, ct);
443441
}
444442

445443
public static Task<Stream> OpenBlobAsync(

GitReader.Tests/Internal/RepositoryAccessorTests.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ public async Task DetectLocalRepositoryPath(int depth)
4040
new StandardFileSystem(65536),
4141
default);
4242

43-
Assert.AreEqual(Path.Combine(basePath, ".git"), actual);
43+
Assert.AreEqual(Path.Combine(basePath, ".git"), actual.GitPath);
44+
Assert.AreEqual(0, actual.AlternativePaths.Length);
4445
}
4546

4647
[Test]
@@ -61,6 +62,29 @@ public async Task DetectLocalRepositoryPathFromDotGitFile()
6162
new StandardFileSystem(65536),
6263
default);
6364

64-
Assert.AreEqual(Path.Combine(basePath, ".git", "modules", "GitReader"), actual);
65+
Assert.AreEqual(Path.Combine(basePath, ".git", "modules", "GitReader"), actual.GitPath);
66+
Assert.AreEqual(0, actual.AlternativePaths.Length);
67+
}
68+
69+
[Test]
70+
public async Task DetectLocalRepositoryPathFromWorkTree()
71+
{
72+
var basePath = Path.GetFullPath(
73+
RepositoryTestsSetUp.GetBasePath(
74+
$"DetectLocalRepositoryPathFromWorkTree"));
75+
76+
ZipFile.ExtractToDirectory(
77+
Path.Combine("artifacts", "test6.zip"),
78+
basePath);
79+
80+
var innerPath = Path.Combine(basePath, "worktree");
81+
82+
var actual = await RepositoryAccessor.DetectLocalRepositoryPathAsync(
83+
innerPath,
84+
new StandardFileSystem(65536),
85+
default);
86+
87+
Assert.AreEqual(Path.Combine(basePath, "root", ".git"), actual.GitPath);
88+
Assert.AreEqual(new[] { Path.Combine(basePath, "root", ".git", "worktrees", "worktree") }, actual.AlternativePaths);
6589
}
6690
}

GitReader.Tests/artifacts/test6.zip

4.98 MB
Binary file not shown.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,8 @@ Apache-v2
638638

639639
## History
640640

641+
* 1.10.0:
642+
* Added Git worktree detection. (#15)
641643
* 1.9.0:
642644
* Supported multiple same named branches.
643645
* Added .NET 9.0 tfm.

0 commit comments

Comments
 (0)