|
8 | 8 | using System.IO.Abstractions; |
9 | 9 | using Elastic.Documentation.Configuration.Assembler; |
10 | 10 | using Elastic.Documentation.Diagnostics; |
| 11 | +using Elastic.Documentation.LinkIndex; |
11 | 12 | using Elastic.Markdown.IO; |
12 | 13 | using Microsoft.Extensions.Logging; |
13 | 14 | using ProcNet; |
@@ -46,128 +47,158 @@ public IReadOnlyCollection<Checkout> GetAll() |
46 | 47 | return checkouts; |
47 | 48 | } |
48 | 49 |
|
49 | | - public async Task<IReadOnlyCollection<Checkout>> AcquireAllLatest(Cancel ctx = default) |
| 50 | + public async Task<IReadOnlyCollection<Checkout>> CloneAll(bool fetchLatest, Cancel ctx = default) |
50 | 51 | { |
51 | | - _logger.LogInformation( |
52 | | - "Cloning all repositories for environment {EnvironmentName} using '{ContentSourceStrategy}' content sourcing strategy", |
| 52 | + _logger.LogInformation("Cloning all repositories for environment {EnvironmentName} using '{ContentSourceStrategy}' content sourcing strategy", |
53 | 53 | PublishEnvironment.Name, |
54 | 54 | PublishEnvironment.ContentSource.ToStringFast(true) |
55 | 55 | ); |
| 56 | + var checkouts = new ConcurrentBag<Checkout>(); |
| 57 | + |
| 58 | + ILinkIndexReader linkIndexReader = Aws3LinkIndexReader.CreateAnonymous(); |
| 59 | + var linkRegistry = await linkIndexReader.GetRegistry(ctx); |
56 | 60 |
|
57 | 61 | var repositories = new Dictionary<string, Repository>(Configuration.ReferenceRepositories) |
58 | 62 | { |
59 | 63 | { NarrativeRepository.RepositoryName, Configuration.Narrative } |
60 | 64 | }; |
61 | | - return await RepositorySourcer.AcquireAllLatest(repositories, PublishEnvironment.ContentSource, ctx); |
62 | | - } |
63 | | -} |
64 | | - |
65 | | -public class RepositorySourcer(ILoggerFactory logger, IDirectoryInfo checkoutDirectory, IFileSystem readFileSystem, DiagnosticsCollector collector) |
66 | | -{ |
67 | | - private readonly ILogger<RepositorySourcer> _logger = logger.CreateLogger<RepositorySourcer>(); |
68 | 65 |
|
69 | | - public async Task<IReadOnlyCollection<Checkout>> AcquireAllLatest(Dictionary<string, Repository> repositories, ContentSource source, Cancel ctx = default) |
70 | | - { |
71 | | - var dict = new ConcurrentDictionary<string, Stopwatch>(); |
72 | | - var checkouts = new ConcurrentBag<Checkout>(); |
73 | 66 | await Parallel.ForEachAsync(repositories, |
74 | 67 | new ParallelOptions |
75 | 68 | { |
76 | 69 | CancellationToken = ctx, |
77 | 70 | MaxDegreeOfParallelism = Environment.ProcessorCount |
78 | | - }, async (kv, c) => |
| 71 | + }, async (repo, c) => |
79 | 72 | { |
80 | 73 | await Task.Run(() => |
81 | 74 | { |
82 | | - var name = kv.Key.Trim(); |
83 | | - var repo = kv.Value; |
84 | | - var clone = CloneOrUpdateRepository(kv.Value, name, repo.GetBranch(source), dict); |
85 | | - checkouts.Add(clone); |
| 75 | + if (!linkRegistry.Repositories.TryGetValue(repo.Key, out var entry)) |
| 76 | + { |
| 77 | + context.Collector.EmitError("", $"'{repo.Key}' does not exist in link index"); |
| 78 | + return; |
| 79 | + } |
| 80 | + var branch = repo.Value.GetBranch(PublishEnvironment.ContentSource); |
| 81 | + var gitRef = branch; |
| 82 | + if (!fetchLatest) |
| 83 | + { |
| 84 | + if (!entry.TryGetValue(branch, out var entryInfo)) |
| 85 | + { |
| 86 | + context.Collector.EmitError("", $"'{repo.Key}' does not have a '{branch}' entry in link index"); |
| 87 | + return; |
| 88 | + } |
| 89 | + gitRef = entryInfo.GitReference; |
| 90 | + } |
| 91 | + checkouts.Add(RepositorySourcer.CloneRef(repo.Value, gitRef, fetchLatest)); |
86 | 92 | }, c); |
87 | 93 | }).ConfigureAwait(false); |
88 | | - |
89 | | - return checkouts.ToList().AsReadOnly(); |
| 94 | + return checkouts; |
90 | 95 | } |
| 96 | +} |
91 | 97 |
|
92 | | - public Checkout CloneOrUpdateRepository(Repository repository, string name, string branch, ConcurrentDictionary<string, Stopwatch> dict) |
93 | | - { |
94 | | - var fs = readFileSystem; |
95 | | - var checkoutFolder = fs.DirectoryInfo.New(Path.Combine(checkoutDirectory.FullName, name)); |
96 | | - var relativePath = Path.GetRelativePath(Paths.WorkingDirectoryRoot.FullName, checkoutFolder.FullName); |
97 | | - var sw = Stopwatch.StartNew(); |
98 | 98 |
|
99 | | - _ = dict.AddOrUpdate($"{name} ({branch})", sw, (_, _) => sw); |
| 99 | +public class RepositorySourcer(ILoggerFactory logger, IDirectoryInfo checkoutDirectory, IFileSystem readFileSystem, DiagnosticsCollector collector) |
| 100 | +{ |
| 101 | + private readonly ILogger<RepositorySourcer> _logger = logger.CreateLogger<RepositorySourcer>(); |
100 | 102 |
|
101 | | - string? head; |
102 | | - if (checkoutFolder.Exists) |
| 103 | + // <summary> |
| 104 | + // Clones the repository to the checkout directory and checks out the specified git reference. |
| 105 | + // </summary> |
| 106 | + // <param name="repository">The repository to clone.</param> |
| 107 | + // <param name="gitRef">The git reference to check out. Branch, commit or tag</param> |
| 108 | + public Checkout CloneRef(Repository repository, string gitRef, bool pull = false, int attempt = 1) |
| 109 | + { |
| 110 | + var checkoutFolder = readFileSystem.DirectoryInfo.New(Path.Combine(checkoutDirectory.FullName, repository.Name)); |
| 111 | + if (attempt > 3) |
103 | 112 | { |
104 | | - if (!TryUpdateSource(name, branch, relativePath, checkoutFolder, out head)) |
105 | | - head = CheckoutFromScratch(repository, name, branch, relativePath, checkoutFolder); |
| 113 | + collector.EmitError("", $"Failed to clone repository {repository.Name}@{gitRef} after 3 attempts"); |
| 114 | + return new Checkout |
| 115 | + { |
| 116 | + Directory = checkoutFolder, |
| 117 | + HeadReference = gitRef, |
| 118 | + Repository = repository, |
| 119 | + }; |
106 | 120 | } |
107 | | - else |
108 | | - head = CheckoutFromScratch(repository, name, branch, relativePath, checkoutFolder); |
109 | | - |
110 | | - sw.Stop(); |
111 | | - |
112 | | - return new Checkout |
| 121 | + _logger.LogInformation("{RepositoryName}: Cloning repository {RepositoryName}@{Commit} to {CheckoutFolder}", repository.Name, repository.Name, gitRef, |
| 122 | + checkoutFolder.FullName); |
| 123 | + if (!checkoutFolder.Exists) |
113 | 124 | { |
114 | | - Repository = repository, |
115 | | - Directory = checkoutFolder, |
116 | | - HeadReference = head |
117 | | - }; |
118 | | - } |
119 | | - |
120 | | - private bool TryUpdateSource(string name, string branch, string relativePath, IDirectoryInfo checkoutFolder, [NotNullWhen(true)] out string? head) |
121 | | - { |
122 | | - head = null; |
123 | | - try |
124 | | - { |
125 | | - _logger.LogInformation("Pull: {Name}\t{Branch}\t{RelativePath}", name, branch, relativePath); |
126 | | - // --allow-unrelated-histories due to shallow clones not finding a common ancestor |
127 | | - ExecIn(checkoutFolder, "git", "pull", "--depth", "1", "--allow-unrelated-histories", "--no-ff"); |
| 125 | + checkoutFolder.Create(); |
| 126 | + checkoutFolder.Refresh(); |
128 | 127 | } |
129 | | - catch (Exception e) |
| 128 | + var isGitInitialized = GitInit(repository, checkoutFolder); |
| 129 | + string? head = null; |
| 130 | + if (isGitInitialized) |
130 | 131 | { |
131 | | - _logger.LogError(e, "Failed to update {Name} from {RelativePath}, falling back to recreating from scratch", name, relativePath); |
132 | | - if (checkoutFolder.Exists) |
| 132 | + try |
| 133 | + { |
| 134 | + head = Capture(checkoutFolder, "git", "rev-parse", "HEAD"); |
| 135 | + } |
| 136 | + catch (Exception e) |
133 | 137 | { |
| 138 | + _logger.LogError(e, "{RepositoryName}: Failed to acquire current commit, falling back to recreating from scratch", repository.Name); |
134 | 139 | checkoutFolder.Delete(true); |
135 | 140 | checkoutFolder.Refresh(); |
| 141 | + return CloneRef(repository, gitRef, pull, attempt + 1); |
136 | 142 | } |
137 | | - return false; |
138 | 143 | } |
139 | 144 |
|
140 | | - head = Capture(checkoutFolder, "git", "rev-parse", "HEAD"); |
| 145 | + if (head != null && head == gitRef) |
| 146 | + _logger.LogInformation("{RepositoryName}: HEAD already at {GitRef}", repository.Name, gitRef); |
| 147 | + else |
| 148 | + { |
| 149 | + FetchAndCheckout(repository, gitRef, checkoutFolder); |
| 150 | + if (!pull) |
| 151 | + { |
| 152 | + return new Checkout |
| 153 | + { |
| 154 | + Directory = checkoutFolder, |
| 155 | + HeadReference = gitRef, |
| 156 | + Repository = repository, |
| 157 | + }; |
| 158 | + } |
| 159 | + try |
| 160 | + { |
| 161 | + ExecIn(checkoutFolder, "git", "pull", "--depth", "1", "--allow-unrelated-histories", "--no-ff", "origin", gitRef); |
| 162 | + } |
| 163 | + catch (Exception e) |
| 164 | + { |
| 165 | + _logger.LogError(e, "{RepositoryName}: Failed to update {GitRef} from {RelativePath}, falling back to recreating from scratch", |
| 166 | + repository.Name, gitRef, checkoutFolder.FullName); |
| 167 | + checkoutFolder.Delete(true); |
| 168 | + checkoutFolder.Refresh(); |
| 169 | + return CloneRef(repository, gitRef, pull, attempt + 1); |
| 170 | + } |
| 171 | + } |
141 | 172 |
|
142 | | - return true; |
| 173 | + return new Checkout |
| 174 | + { |
| 175 | + Directory = checkoutFolder, |
| 176 | + HeadReference = gitRef, |
| 177 | + Repository = repository, |
| 178 | + }; |
143 | 179 | } |
144 | 180 |
|
145 | | - private string CheckoutFromScratch(Repository repository, string name, string branch, string relativePath, IDirectoryInfo checkoutFolder) |
| 181 | + /// <summary> |
| 182 | + /// Initializes the git repository if it is not already initialized. |
| 183 | + /// Returns true if the repository was already initialized. |
| 184 | + /// </summary> |
| 185 | + private bool GitInit(Repository repository, IDirectoryInfo checkoutFolder) |
146 | 186 | { |
147 | | - _logger.LogInformation("Checkout: {Name}\t{Branch}\t{RelativePath}", name, branch, relativePath); |
148 | | - switch (repository.CheckoutStrategy) |
149 | | - { |
150 | | - case "full": |
151 | | - Exec("git", "clone", repository.Origin, checkoutFolder.FullName, |
152 | | - "--depth", "1", "--single-branch", |
153 | | - "--branch", branch |
154 | | - ); |
155 | | - break; |
156 | | - case "partial": |
157 | | - Exec( |
158 | | - "git", "clone", "--filter=blob:none", "--no-checkout", repository.Origin, checkoutFolder.FullName |
159 | | - ); |
160 | | - |
161 | | - ExecIn(checkoutFolder, "git", "sparse-checkout", "set", "--cone"); |
162 | | - ExecIn(checkoutFolder, "git", "checkout", branch); |
163 | | - ExecIn(checkoutFolder, "git", "sparse-checkout", "set", "docs"); |
164 | | - break; |
165 | | - } |
166 | | - |
167 | | - return Capture(checkoutFolder, "git", "rev-parse", "HEAD"); |
| 187 | + var isGitAlreadyInitialized = Directory.Exists(Path.Combine(checkoutFolder.FullName, ".git")); |
| 188 | + if (isGitAlreadyInitialized) |
| 189 | + return true; |
| 190 | + ExecIn(checkoutFolder, "git", "init"); |
| 191 | + ExecIn(checkoutFolder, "git", "remote", "add", "origin", repository.Origin); |
| 192 | + return false; |
168 | 193 | } |
169 | 194 |
|
170 | | - private void Exec(string binary, params string[] args) => ExecIn(null, binary, args); |
| 195 | + private void FetchAndCheckout(Repository repository, string gitRef, IDirectoryInfo checkoutFolder) |
| 196 | + { |
| 197 | + ExecIn(checkoutFolder, "git", "fetch", "--no-tags", "--prune", "--no-recurse-submodules", "--depth", "1", "origin", gitRef); |
| 198 | + if (repository.CheckoutStrategy == CheckoutStrategy.Partial) |
| 199 | + ExecIn(checkoutFolder, "git", "sparse-checkout", "set", "docs"); |
| 200 | + ExecIn(checkoutFolder, "git", "checkout", "--force", gitRef); |
| 201 | + } |
171 | 202 |
|
172 | 203 | private void ExecIn(IDirectoryInfo? workingDirectory, string binary, params string[] args) |
173 | 204 | { |
@@ -221,7 +252,6 @@ string CaptureOutput() |
221 | 252 | return line; |
222 | 253 | } |
223 | 254 | } |
224 | | - |
225 | 255 | } |
226 | 256 |
|
227 | 257 | public class NoopConsoleWriter : IConsoleOutWriter |
|
0 commit comments