Skip to content

Commit a79b8b8

Browse files
authored
Merge pull request #1881 from microsoft/milestones/m266
Release M266
2 parents 12bf5b3 + f231900 commit a79b8b8

File tree

14 files changed

+361
-110
lines changed

14 files changed

+361
-110
lines changed

.github/workflows/build.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
- name: Look for prior successful runs
3030
id: check
3131
if: github.event.inputs.git_version == ''
32-
uses: actions/github-script@v7
32+
uses: actions/github-script@v8
3333
with:
3434
github-token: ${{secrets.GITHUB_TOKEN}}
3535
result-encoding: string
@@ -130,7 +130,7 @@ jobs:
130130
- name: Skip this job if there is a previous successful run
131131
if: needs.validate.outputs.skip != ''
132132
id: skip
133-
uses: actions/github-script@v7
133+
uses: actions/github-script@v8
134134
with:
135135
script: |
136136
core.info(`Skipping: There already is a successful run: ${{ needs.validate.outputs.skip }}`)
@@ -212,7 +212,7 @@ jobs:
212212
- name: Skip this job if there is a previous successful run
213213
if: needs.validate.outputs.skip != ''
214214
id: skip
215-
uses: actions/github-script@v7
215+
uses: actions/github-script@v8
216216
with:
217217
script: |
218218
core.info(`Skipping: There already is a successful run: ${{ needs.validate.outputs.skip }}`)

GVFS/GVFS.Common/Git/GitRepo.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,21 @@ public virtual bool TryGetBlobLength(string blobSha, out long size)
113113
return this.GetLooseBlobState(blobSha, null, out size) == LooseBlobState.Exists;
114114
}
115115

116+
/// <summary>
117+
/// Try to find the SHAs of subtrees missing from the given tree.
118+
/// </summary>
119+
/// <param name="treeSha">Tree to look up</param>
120+
/// <param name="subtrees">SHAs of subtrees of this tree which are not downloaded yet.</param>
121+
/// <returns></returns>
122+
public virtual bool TryGetMissingSubTrees(string treeSha, out string[] subtrees)
123+
{
124+
string[] missingSubtrees = null;
125+
var succeeded = this.libgit2RepoInvoker.TryInvoke(repo =>
126+
repo.GetMissingSubTrees(treeSha), out missingSubtrees);
127+
subtrees = missingSubtrees;
128+
return succeeded;
129+
}
130+
116131
public void Dispose()
117132
{
118133
if (this.libgit2RepoInvoker != null)

GVFS/GVFS.Common/Git/HashingStream.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public HashingStream(Stream stream)
1717
{
1818
this.stream = stream;
1919

20-
this.hash = SHA1.Create();
20+
this.hash = SHA1.Create(); // CodeQL [SM02196] SHA-1 is acceptable here because this is Git's hashing algorithm, not used for cryptographic purposes
2121
this.hashResult = null;
2222
this.hash.Initialize();
2323
this.closed = false;

GVFS/GVFS.Common/Git/LibGit2Repo.cs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using GVFS.Common.Tracing;
22
using System;
3+
using System.Collections.Generic;
34
using System.IO;
45
using System.Runtime.InteropServices;
56

@@ -147,6 +148,82 @@ public virtual bool TryCopyBlob(string sha, Action<Stream, long> writeAction)
147148
return true;
148149
}
149150

151+
/// <summary>
152+
/// Get the list of missing subtrees for the given treeSha.
153+
/// </summary>
154+
/// <param name="treeSha">Tree to look up</param>
155+
/// <param name="missingSubtrees">SHAs of subtrees of this tree which are not downloaded yet.</param>
156+
public virtual string[] GetMissingSubTrees(string treeSha)
157+
{
158+
List<string> missingSubtreesList = new List<string>();
159+
IntPtr treeHandle;
160+
if (Native.RevParseSingle(out treeHandle, this.RepoHandle, treeSha) != Native.SuccessCode
161+
|| treeHandle == IntPtr.Zero)
162+
{
163+
return Array.Empty<string>();
164+
}
165+
166+
try
167+
{
168+
if (Native.Object.GetType(treeHandle) != Native.ObjectTypes.Tree)
169+
{
170+
return Array.Empty<string>();
171+
}
172+
173+
uint entryCount = Native.Tree.GetEntryCount(treeHandle);
174+
for (uint i = 0; i < entryCount; i++)
175+
{
176+
if (this.IsMissingSubtree(treeHandle, i, out string entrySha))
177+
{
178+
missingSubtreesList.Add(entrySha);
179+
}
180+
}
181+
}
182+
finally
183+
{
184+
Native.Object.Free(treeHandle);
185+
}
186+
187+
return missingSubtreesList.ToArray();
188+
}
189+
190+
/// <summary>
191+
/// Determine if the given index of a tree is a subtree and if it is missing.
192+
/// If it is a missing subtree, return the SHA of the subtree.
193+
/// </summary>
194+
private bool IsMissingSubtree(IntPtr treeHandle, uint i, out string entrySha)
195+
{
196+
entrySha = null;
197+
IntPtr entryHandle = Native.Tree.GetEntryByIndex(treeHandle, i);
198+
if (entryHandle == IntPtr.Zero)
199+
{
200+
return false;
201+
}
202+
203+
var entryMode = Native.Tree.GetEntryFileMode(entryHandle);
204+
if (entryMode != Native.Tree.TreeEntryFileModeDirectory)
205+
{
206+
return false;
207+
}
208+
209+
var entryId = Native.Tree.GetEntryId(entryHandle);
210+
if (entryId == IntPtr.Zero)
211+
{
212+
return false;
213+
}
214+
215+
var rawEntrySha = Native.IntPtrToGitOid(entryId);
216+
entrySha = rawEntrySha.ToString();
217+
218+
if (this.ObjectExists(entrySha))
219+
{
220+
return false;
221+
}
222+
return true;
223+
/* Both the entryHandle and the entryId handle are owned by the treeHandle, so we shouldn't free them or it will lead to corruption of the later entries */
224+
}
225+
226+
150227
public void Dispose()
151228
{
152229
this.Dispose(true);
@@ -247,6 +324,26 @@ public static class Blob
247324
[DllImport(Git2NativeLibName, EntryPoint = "git_blob_rawcontent")]
248325
public static unsafe extern byte* GetRawContent(IntPtr objectHandle);
249326
}
327+
328+
public static class Tree
329+
{
330+
[DllImport(Git2NativeLibName, EntryPoint = "git_tree_entrycount")]
331+
public static extern uint GetEntryCount(IntPtr treeHandle);
332+
333+
[DllImport(Git2NativeLibName, EntryPoint = "git_tree_entry_byindex")]
334+
public static extern IntPtr GetEntryByIndex(IntPtr treeHandle, uint index);
335+
336+
[DllImport(Git2NativeLibName, EntryPoint = "git_tree_entry_id")]
337+
public static extern IntPtr GetEntryId(IntPtr entryHandle);
338+
339+
/* git_tree_entry_type requires the object to exist, so we can't use it to check if
340+
* a missing entry is a tree. Instead, we can use the file mode to determine if it is a tree. */
341+
[DllImport(Git2NativeLibName, EntryPoint = "git_tree_entry_filemode")]
342+
public static extern uint GetEntryFileMode(IntPtr entryHandle);
343+
344+
public const uint TreeEntryFileModeDirectory = 0x4000;
345+
346+
}
250347
}
251348
}
252349
}

GVFS/GVFS.Common/Http/CacheServerResolver.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,12 @@ public bool TryResolveUrlFromRemote(
5454
if (cacheServerName.Equals(CacheServerInfo.ReservedNames.Default, StringComparison.OrdinalIgnoreCase))
5555
{
5656
cacheServer =
57-
serverGVFSConfig.CacheServers.FirstOrDefault(cache => cache.GlobalDefault)
57+
serverGVFSConfig?.CacheServers.FirstOrDefault(cache => cache.GlobalDefault)
5858
?? this.CreateNone();
5959
}
6060
else
6161
{
62-
cacheServer = serverGVFSConfig.CacheServers.FirstOrDefault(cache =>
62+
cacheServer = serverGVFSConfig?.CacheServers.FirstOrDefault(cache =>
6363
cache.Name.Equals(cacheServerName, StringComparison.OrdinalIgnoreCase));
6464

6565
if (cacheServer == null)
@@ -87,7 +87,7 @@ public CacheServerInfo ResolveNameFromRemote(
8787
}
8888

8989
return
90-
serverGVFSConfig.CacheServers.FirstOrDefault(cache => cache.Url.Equals(cacheServerUrl, StringComparison.OrdinalIgnoreCase))
90+
serverGVFSConfig?.CacheServers.FirstOrDefault(cache => cache.Url.Equals(cacheServerUrl, StringComparison.OrdinalIgnoreCase))
9191
?? new CacheServerInfo(cacheServerUrl, CacheServerInfo.ReservedNames.UserDefined);
9292
}
9393

GVFS/GVFS.Common/SHA1Util.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public static byte[] SHA1ForUTF8String(string s)
2121
{
2222
byte[] bytes = Encoding.UTF8.GetBytes(s);
2323

24-
using (SHA1 sha1 = SHA1.Create())
24+
using (SHA1 sha1 = SHA1.Create()) // CodeQL [SM02196] SHA-1 is acceptable here because this is Git's hashing algorithm, not used for cryptographic purposes
2525
{
2626
return sha1.ComputeHash(bytes);
2727
}

GVFS/GVFS.Mount/InProcessMount.cs

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ public class InProcessMount
2727
private const int MaxPipeNameLength = 250;
2828
private const int MutexMaxWaitTimeMS = 500;
2929

30+
// This is value chosen based on tested scenarios to limit the required download time for
31+
// all the trees. This is approximately the amount of trees that can be downloaded in 1 second.
32+
// Downloading an entire commit pack also takes around 1 second, so this should limit downloading
33+
// all the trees in a commit to ~2-3 seconds.
34+
private const int MissingTreeThresholdForDownloadingCommitPack = 200;
35+
3036
private readonly bool showDebugWindow;
3137

3238
private FileSystemCallbacks fileSystemCallbacks;
@@ -47,7 +53,6 @@ public class InProcessMount
4753
private ManualResetEvent unmountEvent;
4854

4955
private readonly Dictionary<string, string> treesWithDownloadedCommits = new Dictionary<string,string>();
50-
private DateTime lastCommitPackDownloadTime = DateTime.MinValue;
5156

5257
// True if InProcessMount is calling git reset as part of processing
5358
// a folder dehydrate request
@@ -518,13 +523,14 @@ private void HandleDownloadObjectRequest(NamedPipeMessages.Message message, Name
518523
if (this.ShouldDownloadCommitPack(objectSha, out string commitSha)
519524
&& this.gitObjects.TryDownloadCommit(commitSha))
520525
{
521-
this.DownloadedCommitPack(objectSha: objectSha, commitSha: commitSha);
526+
this.DownloadedCommitPack(commitSha);
522527
response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.SuccessResult);
523528
// FUTURE: Should the stats be updated to reflect all the trees in the pack?
524529
// FUTURE: Should we try to clean up duplicate trees or increase depth of the commit download?
525530
}
526531
else if (this.gitObjects.TryDownloadAndSaveObject(objectSha, GVFSGitObjects.RequestSource.NamedPipeMessage) == GitObjects.DownloadAndSaveObjectResult.Success)
527532
{
533+
this.UpdateTreesForDownloadedCommits(objectSha);
528534
response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.SuccessResult);
529535
}
530536
else
@@ -548,7 +554,7 @@ private void HandleDownloadObjectRequest(NamedPipeMessages.Message message, Name
548554
* Otherwise, the trees for the commit may be needed soon depending on the context.
549555
* e.g. git log (without a pathspec) doesn't need trees, but git checkout does.
550556
*
551-
* Save the tree/commit so if the tree is requested soon we can download all the trees for the commit in a batch.
557+
* Save the tree/commit so if more trees are requested we can download all the trees for the commit in a batch.
552558
*/
553559
this.treesWithDownloadedCommits[treeSha] = objectSha;
554560
}
@@ -561,28 +567,67 @@ private void HandleDownloadObjectRequest(NamedPipeMessages.Message message, Name
561567
private bool PrefetchHasBeenDone()
562568
{
563569
var prefetchPacks = this.gitObjects.ReadPackFileNames(this.enlistment.GitPackRoot, GVFSConstants.PrefetchPackPrefix);
564-
return prefetchPacks.Length > 0;
570+
var result = prefetchPacks.Length > 0;
571+
if (result)
572+
{
573+
this.treesWithDownloadedCommits.Clear();
574+
}
575+
return result;
565576
}
566577

567578
private bool ShouldDownloadCommitPack(string objectSha, out string commitSha)
568579
{
569-
570580
if (!this.treesWithDownloadedCommits.TryGetValue(objectSha, out commitSha)
571581
|| this.PrefetchHasBeenDone())
572582
{
573583
return false;
574584
}
575585

576-
/* This is a heuristic to prevent downloading multiple packs related to git history commands,
577-
* since commits downloaded close together likely have similar trees. */
578-
var timePassed = DateTime.UtcNow - this.lastCommitPackDownloadTime;
579-
return (timePassed > TimeSpan.FromMinutes(5));
586+
/* This is a heuristic to prevent downloading multiple packs related to git history commands.
587+
* Closely related commits are likely to have similar trees, so we'll find fewer missing trees in them.
588+
* Conversely, if we know (from previously downloaded missing trees) that a commit has a lot of missing
589+
* trees left, we'll probably need to download many more trees for the commit so we should download the pack.
590+
*/
591+
var commitShaLocal = commitSha; // can't use out parameter in lambda
592+
int missingTreeCount = this.treesWithDownloadedCommits.Where(x => x.Value == commitShaLocal).Count();
593+
return missingTreeCount > MissingTreeThresholdForDownloadingCommitPack;
594+
}
595+
596+
private void UpdateTreesForDownloadedCommits(string objectSha)
597+
{
598+
/* If we are downloading missing trees, we probably are missing more trees for the commit.
599+
* Update our list of trees associated with the commit so we can use the # of missing trees
600+
* as a heuristic to decide whether to batch download all the trees for the commit the
601+
* next time a missing one is requested.
602+
*/
603+
if (!this.treesWithDownloadedCommits.TryGetValue(objectSha, out var commitSha)
604+
|| this.PrefetchHasBeenDone())
605+
{
606+
return;
607+
}
608+
609+
if (!this.context.Repository.TryGetObjectType(objectSha, out var objectType)
610+
|| objectType != Native.ObjectTypes.Tree)
611+
{
612+
return;
613+
}
614+
615+
if (this.context.Repository.TryGetMissingSubTrees(objectSha, out var missingSubTrees))
616+
{
617+
foreach (var missingSubTree in missingSubTrees)
618+
{
619+
this.treesWithDownloadedCommits[missingSubTree] = commitSha;
620+
}
621+
}
580622
}
581623

582-
private void DownloadedCommitPack(string objectSha, string commitSha)
624+
private void DownloadedCommitPack(string commitSha)
583625
{
584-
this.lastCommitPackDownloadTime = DateTime.UtcNow;
585-
this.treesWithDownloadedCommits.Remove(objectSha);
626+
var toRemove = this.treesWithDownloadedCommits.Where(x => x.Value == commitSha).ToList();
627+
foreach (var tree in toRemove)
628+
{
629+
this.treesWithDownloadedCommits.Remove(tree.Key);
630+
}
586631
}
587632

588633
private void HandlePostFetchJobRequest(NamedPipeMessages.Message message, NamedPipeServer.Connection connection)

GVFS/GVFS.ReadObjectHook/main.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,13 @@ int main(int, char *argv[])
8484
DisableCRLFTranslationOnStdPipes();
8585

8686
packet_txt_read(packet_buffer, sizeof(packet_buffer));
87-
if (strcmp(packet_buffer, "git-read-object-client"))
87+
if (strcmp(packet_buffer, "git-read-object-client")) // CodeQL [SM01932] `packet_txt_read()` either NUL-terminates or `die()`s
8888
{
8989
die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad welcome message\n");
9090
}
9191

9292
packet_txt_read(packet_buffer, sizeof(packet_buffer));
93-
if (strcmp(packet_buffer, "version=1"))
93+
if (strcmp(packet_buffer, "version=1")) // CodeQL [SM01932] `packet_txt_read()` either NUL-terminates or `die()`s
9494
{
9595
die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad version\n");
9696
}
@@ -105,7 +105,7 @@ int main(int, char *argv[])
105105
packet_flush();
106106

107107
packet_txt_read(packet_buffer, sizeof(packet_buffer));
108-
if (strcmp(packet_buffer, "capability=get"))
108+
if (strcmp(packet_buffer, "capability=get")) // CodeQL [SM01932] `packet_txt_read()` either NUL-terminates or `die()`s
109109
{
110110
die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad capability\n");
111111
}
@@ -125,13 +125,13 @@ int main(int, char *argv[])
125125
while (1)
126126
{
127127
packet_txt_read(packet_buffer, sizeof(packet_buffer));
128-
if (strcmp(packet_buffer, "command=get"))
128+
if (strcmp(packet_buffer, "command=get")) // CodeQL [SM01932] `packet_txt_read()` either NUL-terminates or `die()`s
129129
{
130130
die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad command\n");
131131
}
132132

133133
len = packet_txt_read(packet_buffer, sizeof(packet_buffer));
134-
if ((len != SHA1_LENGTH + 5) || strncmp(packet_buffer, "sha1=", 5))
134+
if ((len != SHA1_LENGTH + 5) || strncmp(packet_buffer, "sha1=", 5)) // CodeQL [SM01932] `packet_txt_read()` either NUL-terminates or `die()`s
135135
{
136136
die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad sha1 in get command\n");
137137
}

GVFS/GVFS.Virtualization/Projection/GitIndexProjection.FolderData.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public void Include()
5454

5555
public string HashedChildrenNamesSha()
5656
{
57-
using (HashAlgorithm hash = SHA1.Create())
57+
using (HashAlgorithm hash = SHA1.Create()) // CodeQL [SM02196] SHA-1 is acceptable here because this is Git's hashing algorithm, not used for cryptographic purposes
5858
{
5959
for (int i = 0; i < this.ChildEntries.Count; i++)
6060
{

0 commit comments

Comments
 (0)