|
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,129 +47,169 @@ 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 = "", |
| 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 |
| 125 | + checkoutFolder.Create(); |
| 126 | + checkoutFolder.Refresh(); |
| 127 | + } |
| 128 | + var isGitInitialized = GitInit(repository, checkoutFolder); |
| 129 | + string? head = null; |
| 130 | + if (isGitInitialized) |
124 | 131 | {
|
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"); |
| 132 | + try |
| 133 | + { |
| 134 | + head = Capture(checkoutFolder, "git", "rev-parse", "HEAD"); |
| 135 | + } |
| 136 | + catch (Exception e) |
| 137 | + { |
| 138 | + _logger.LogError(e, "{RepositoryName}: Failed to acquire current commit, falling back to recreating from scratch", repository.Name); |
| 139 | + checkoutFolder.Delete(true); |
| 140 | + checkoutFolder.Refresh(); |
| 141 | + return CloneRef(repository, gitRef, pull, attempt + 1); |
| 142 | + } |
128 | 143 | }
|
129 |
| - catch (Exception e) |
| 144 | + // Repository already checked out the same commit |
| 145 | + if (head != null && head == gitRef) |
| 146 | + // nothing to do, already at the right commit |
| 147 | + _logger.LogInformation("{RepositoryName}: HEAD already at {GitRef}", repository.Name, gitRef); |
| 148 | + else |
130 | 149 | {
|
131 |
| - _logger.LogError(e, "Failed to update {Name} from {RelativePath}, falling back to recreating from scratch", name, relativePath); |
132 |
| - if (checkoutFolder.Exists) |
| 150 | + FetchAndCheckout(repository, gitRef, checkoutFolder); |
| 151 | + if (!pull) |
| 152 | + { |
| 153 | + return new Checkout |
| 154 | + { |
| 155 | + Directory = checkoutFolder, |
| 156 | + HeadReference = gitRef, |
| 157 | + Repository = repository, |
| 158 | + }; |
| 159 | + } |
| 160 | + try |
| 161 | + { |
| 162 | + ExecIn(checkoutFolder, "git", "pull", "--depth", "1", "--allow-unrelated-histories", "--no-ff", "origin", gitRef); |
| 163 | + } |
| 164 | + catch (Exception e) |
133 | 165 | {
|
| 166 | + _logger.LogError(e, "{RepositoryName}: Failed to update {GitRef} from {RelativePath}, falling back to recreating from scratch", |
| 167 | + repository.Name, gitRef, checkoutFolder.FullName); |
134 | 168 | checkoutFolder.Delete(true);
|
135 | 169 | checkoutFolder.Refresh();
|
| 170 | + return CloneRef(repository, gitRef, pull, attempt + 1); |
136 | 171 | }
|
137 |
| - return false; |
138 | 172 | }
|
139 | 173 |
|
140 |
| - head = Capture(checkoutFolder, "git", "rev-parse", "HEAD"); |
| 174 | + return new Checkout |
| 175 | + { |
| 176 | + Directory = checkoutFolder, |
| 177 | + HeadReference = gitRef, |
| 178 | + Repository = repository, |
| 179 | + }; |
| 180 | + } |
141 | 181 |
|
142 |
| - return true; |
| 182 | + /// <summary> |
| 183 | + /// Initializes the git repository if it is not already initialized. |
| 184 | + /// Returns true if the repository was already initialized. |
| 185 | + /// </summary> |
| 186 | + private bool GitInit(Repository repository, IDirectoryInfo checkoutFolder) |
| 187 | + { |
| 188 | + var isGitAlreadyInitialized = Directory.Exists(Path.Combine(checkoutFolder.FullName, ".git")); |
| 189 | + if (isGitAlreadyInitialized) |
| 190 | + return true; |
| 191 | + ExecIn(checkoutFolder, "git", "init"); |
| 192 | + ExecIn(checkoutFolder, "git", "remote", "add", "origin", repository.Origin); |
| 193 | + return false; |
143 | 194 | }
|
144 | 195 |
|
145 |
| - private string CheckoutFromScratch(Repository repository, string name, string branch, string relativePath, IDirectoryInfo checkoutFolder) |
| 196 | + private void FetchAndCheckout(Repository repository, string gitRef, IDirectoryInfo checkoutFolder) |
146 | 197 | {
|
147 |
| - _logger.LogInformation("Checkout: {Name}\t{Branch}\t{RelativePath}", name, branch, relativePath); |
| 198 | + ExecIn(checkoutFolder, "git", "fetch", "--no-tags", "--prune", "--no-recurse-submodules", "--depth", "1", "origin", gitRef); |
148 | 199 | switch (repository.CheckoutStrategy)
|
149 | 200 | {
|
150 |
| - case "full": |
151 |
| - Exec("git", "clone", repository.Origin, checkoutFolder.FullName, |
152 |
| - "--depth", "1", "--single-branch", |
153 |
| - "--branch", branch |
154 |
| - ); |
| 201 | + case CheckoutStrategy.Full: |
| 202 | + ExecIn(checkoutFolder, "git", "sparse-checkout", "disable"); |
155 | 203 | 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); |
| 204 | + case CheckoutStrategy.Partial: |
163 | 205 | ExecIn(checkoutFolder, "git", "sparse-checkout", "set", "docs");
|
164 | 206 | break;
|
| 207 | + default: |
| 208 | + throw new ArgumentOutOfRangeException(nameof(repository), repository.CheckoutStrategy, null); |
165 | 209 | }
|
166 |
| - |
167 |
| - return Capture(checkoutFolder, "git", "rev-parse", "HEAD"); |
| 210 | + ExecIn(checkoutFolder, "git", "checkout", "--force", gitRef); |
168 | 211 | }
|
169 | 212 |
|
170 |
| - private void Exec(string binary, params string[] args) => ExecIn(null, binary, args); |
171 |
| - |
172 | 213 | private void ExecIn(IDirectoryInfo? workingDirectory, string binary, params string[] args)
|
173 | 214 | {
|
174 | 215 | var arguments = new ExecArguments(binary, args)
|
@@ -221,7 +262,6 @@ string CaptureOutput()
|
221 | 262 | return line;
|
222 | 263 | }
|
223 | 264 | }
|
224 |
| - |
225 | 265 | }
|
226 | 266 |
|
227 | 267 | public class NoopConsoleWriter : IConsoleOutWriter
|
|
0 commit comments