Skip to content

Commit 8f26970

Browse files
authored
Merge pull request #1386: GVFS Health Feature
Initial cut of GVFS Health Feature for the M155 milestone
2 parents cff0743 + 2259396 commit 8f26970

13 files changed

Lines changed: 1100 additions & 6 deletions

File tree

GVFS/GVFS.Common/Git/GitProcess.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -582,14 +582,22 @@ public Result CatFileGetType(string objectId)
582582
return this.InvokeGitAgainstDotGitFolder("cat-file -t " + objectId);
583583
}
584584

585-
public Result LsTree(string treeish, Action<string> parseStdOutLine, bool recursive, bool showAllTrees = false)
585+
public Result LsTree(string treeish, Action<string> parseStdOutLine, bool recursive, bool showAllTrees = false, bool showDirectories = false)
586586
{
587587
return this.InvokeGitAgainstDotGitFolder(
588-
"ls-tree " + (recursive ? "-r " : string.Empty) + (showAllTrees ? "-t " : string.Empty) + treeish,
588+
"ls-tree " + (recursive ? "-r " : string.Empty) + (showAllTrees ? "-t " : string.Empty) + (showDirectories ? "-d " : string.Empty) + treeish,
589589
null,
590590
parseStdOutLine);
591591
}
592592

593+
public Result LsFiles(Action<string> parseStdOutLine)
594+
{
595+
return this.InvokeGitInWorkingDirectoryRoot(
596+
"ls-files -v",
597+
useReadObjectHook: false,
598+
parseStdOutLine: parseStdOutLine);
599+
}
600+
593601
public Result SetUpstream(string branchName, string upstream)
594602
{
595603
return this.InvokeGitAgainstDotGitFolder("branch --set-upstream-to=" + upstream + " " + branchName);
@@ -859,15 +867,16 @@ private Result InvokeGitOutsideEnlistment(
859867
private Result InvokeGitInWorkingDirectoryRoot(
860868
string command,
861869
bool useReadObjectHook,
862-
Action<StreamWriter> writeStdIn = null)
870+
Action<StreamWriter> writeStdIn = null,
871+
Action<string> parseStdOutLine = null)
863872
{
864873
return this.InvokeGitImpl(
865874
command,
866875
workingDirectory: this.workingDirectoryRoot,
867876
dotGitDirectory: null,
868877
useReadObjectHook: useReadObjectHook,
869878
writeStdIn: writeStdIn,
870-
parseStdOutLine: null,
879+
parseStdOutLine: parseStdOutLine,
871880
timeoutMs: -1);
872881
}
873882

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
namespace GVFS.Common
6+
{
7+
/// <summary>
8+
/// Class responsible for the business logic involved in calculating the health statistics
9+
/// of a gvfs enlistment. Constructed with the lists of paths for the enlistment, and then
10+
/// internally stores the calculated information. Compute or recompute via CalculateStatistics
11+
/// with an optional parameter to only look for paths which are under the specified directory
12+
/// </summary>
13+
public class EnlistmentHealthCalculator
14+
{
15+
// In the context of this class, hydrated files are placeholders or modified paths
16+
// The total number of hydrated files is this.PlaceholderCount + this.ModifiedPathsCount
17+
private readonly EnlistmentPathData enlistmentPathData;
18+
19+
public EnlistmentHealthCalculator(EnlistmentPathData pathData)
20+
{
21+
this.enlistmentPathData = pathData;
22+
}
23+
24+
public EnlistmentHealthData CalculateStatistics(string parentDirectory)
25+
{
26+
int gitTrackedItemsCount = 0;
27+
int placeholderCount = 0;
28+
int modifiedPathsCount = 0;
29+
Dictionary<string, int> gitTrackedItemsDirectoryTally = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
30+
Dictionary<string, int> hydratedFilesDirectoryTally = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
31+
32+
// Parent directory is a path relative to the root of the repository which is already in git format
33+
if (!parentDirectory.EndsWith(GVFSConstants.GitPathSeparatorString) && parentDirectory.Length > 0)
34+
{
35+
parentDirectory += GVFSConstants.GitPathSeparator;
36+
}
37+
38+
if (parentDirectory.StartsWith(GVFSConstants.GitPathSeparatorString))
39+
{
40+
parentDirectory = parentDirectory.TrimStart(GVFSConstants.GitPathSeparator);
41+
}
42+
43+
gitTrackedItemsCount += this.CategorizePaths(this.enlistmentPathData.GitFolderPaths, gitTrackedItemsDirectoryTally, parentDirectory);
44+
gitTrackedItemsCount += this.CategorizePaths(this.enlistmentPathData.GitFilePaths, gitTrackedItemsDirectoryTally, parentDirectory);
45+
placeholderCount += this.CategorizePaths(this.enlistmentPathData.PlaceholderFolderPaths, hydratedFilesDirectoryTally, parentDirectory);
46+
placeholderCount += this.CategorizePaths(this.enlistmentPathData.PlaceholderFilePaths, hydratedFilesDirectoryTally, parentDirectory);
47+
modifiedPathsCount += this.CategorizePaths(this.enlistmentPathData.ModifiedFolderPaths, hydratedFilesDirectoryTally, parentDirectory);
48+
modifiedPathsCount += this.CategorizePaths(this.enlistmentPathData.ModifiedFilePaths, hydratedFilesDirectoryTally, parentDirectory);
49+
50+
Dictionary<string, int> mostHydratedDirectories = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
51+
52+
// Map directory names to the corresponding health data from gitTrackedItemsDirectoryTally and hydratedFilesDirectoryTally
53+
foreach (KeyValuePair<string, int> pair in gitTrackedItemsDirectoryTally)
54+
{
55+
if (hydratedFilesDirectoryTally.TryGetValue(pair.Key, out int hydratedFiles))
56+
{
57+
// In-lining this for now until a better "health" calculation is created
58+
// Another possibility is the ability to pass a function to use for health (might not be applicable)
59+
mostHydratedDirectories.Add(pair.Key, hydratedFiles);
60+
}
61+
else
62+
{
63+
mostHydratedDirectories.Add(pair.Key, 0);
64+
}
65+
}
66+
67+
return new EnlistmentHealthData(
68+
parentDirectory,
69+
gitTrackedItemsCount,
70+
placeholderCount,
71+
modifiedPathsCount,
72+
this.CalculateHealthMetric(placeholderCount + modifiedPathsCount, gitTrackedItemsCount),
73+
mostHydratedDirectories.OrderByDescending(kp => kp.Value).ToList());
74+
}
75+
76+
/// <summary>
77+
/// Take a file path and get the top level directory from it, or GVFSConstants.GitPathSeparator if it is not in a directory
78+
/// </summary>
79+
/// <param name="path">The path to a file to parse for the top level directory containing it</param>
80+
/// <returns>A string containing the top level directory from the provided path, or GVFSConstants.GitPathSeparator if the path is for an item in the root</returns>
81+
private string ParseTopDirectory(string path)
82+
{
83+
int whackLocation = path.IndexOf(GVFSConstants.GitPathSeparator);
84+
if (whackLocation == -1)
85+
{
86+
return GVFSConstants.GitPathSeparatorString;
87+
}
88+
89+
return path.Substring(0, whackLocation);
90+
}
91+
92+
/// <summary>
93+
/// Categorizes a list of paths given as strings by mapping them to the top level directory in their path
94+
/// Modifies the directoryTracking dictionary to have an accurate count of the files underneath a top level directory
95+
/// </summary>
96+
/// <remarks>
97+
/// The distinction between files and directories is important --
98+
/// If the path to a file doesn't contain a GVFSConstants.GitPathSeparator, then that means it falls within the root
99+
/// However if a directory's path doesn't contain a GVFSConstants.GitPathSeparator, it doesn't count towards its own hydration
100+
/// </remarks>
101+
/// <param name="paths">An enumerable containing paths as strings</param>
102+
/// <param name="directoryTracking">A dictionary used to track the number of files per top level directory</param>
103+
/// <param name="parentDirectory">Paths will only be categorized if they are descendants of the parentDirectory</param>
104+
private int CategorizePaths(IEnumerable<string> paths, Dictionary<string, int> directoryTracking, string parentDirectory)
105+
{
106+
int count = 0;
107+
foreach (string path in paths)
108+
{
109+
// Only categorize if descendent of the parentDirectory
110+
if (path.StartsWith(parentDirectory, StringComparison.OrdinalIgnoreCase))
111+
{
112+
count++;
113+
114+
// If the path is to the parentDirectory, ignore it to avoid adding string.Empty to the data structures
115+
if (!parentDirectory.Equals(path, StringComparison.OrdinalIgnoreCase))
116+
{
117+
// Trim the path to parent directory
118+
string topDir = this.ParseTopDirectory(this.TrimDirectoryFromPath(path, parentDirectory));
119+
if (!topDir.Equals(GVFSConstants.GitPathSeparatorString))
120+
{
121+
this.IncreaseDictionaryCounterByKey(directoryTracking, topDir);
122+
}
123+
}
124+
}
125+
}
126+
127+
return count;
128+
}
129+
130+
/// <summary>
131+
/// Trim the relative path to a directory from the front of a specified path which is its child
132+
/// </summary>
133+
/// <remarks>Precondition: 'directoryTarget' must be an ancestor of 'path'</remarks>
134+
/// <param name="path">The path being trimmed</param>
135+
/// <param name="directoryTarget">The directory target whose path to trim from the path</param>
136+
/// <returns>The newly formatted path with the directory trimmed</returns>
137+
private string TrimDirectoryFromPath(string path, string directoryTarget)
138+
{
139+
return path.Substring(directoryTarget.Length);
140+
}
141+
142+
private void IncreaseDictionaryCounterByKey(Dictionary<string, int> countingDictionary, string key)
143+
{
144+
if (!countingDictionary.TryGetValue(key, out int count))
145+
{
146+
count = 0;
147+
}
148+
149+
countingDictionary[key] = ++count;
150+
}
151+
152+
private decimal CalculateHealthMetric(int hydratedFileCount, int totalFileCount)
153+
{
154+
if (totalFileCount == 0)
155+
{
156+
return 0;
157+
}
158+
159+
return (decimal)hydratedFileCount / (decimal)totalFileCount;
160+
}
161+
}
162+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
using System.Collections.Generic;
2+
3+
namespace GVFS.Common
4+
{
5+
public class EnlistmentHealthData
6+
{
7+
public EnlistmentHealthData(
8+
string targetDirectory,
9+
int gitItemsCount,
10+
int placeholderCount,
11+
int modifiedPathsCount,
12+
decimal healthMetric,
13+
List<KeyValuePair<string, int>> directoryHydrationLevels)
14+
{
15+
this.TargetDirectory = targetDirectory;
16+
this.GitTrackedItemsCount = gitItemsCount;
17+
this.PlaceholderCount = placeholderCount;
18+
this.ModifiedPathsCount = modifiedPathsCount;
19+
this.HealthMetric = healthMetric;
20+
this.DirectoryHydrationLevels = directoryHydrationLevels;
21+
}
22+
23+
public string TargetDirectory { get; private set; }
24+
public int GitTrackedItemsCount { get; private set; }
25+
public int PlaceholderCount { get; private set; }
26+
public int ModifiedPathsCount { get; private set; }
27+
public List<KeyValuePair<string, int>> DirectoryHydrationLevels { get; private set; }
28+
public decimal HealthMetric { get; private set; }
29+
public decimal PlaceholderPercentage
30+
{
31+
get
32+
{
33+
if (this.GitTrackedItemsCount == 0)
34+
{
35+
return 0;
36+
}
37+
38+
return (decimal)this.PlaceholderCount / this.GitTrackedItemsCount;
39+
}
40+
}
41+
42+
public decimal ModifiedPathsPercentage
43+
{
44+
get
45+
{
46+
if (this.GitTrackedItemsCount == 0)
47+
{
48+
return 0;
49+
}
50+
51+
return (decimal)this.ModifiedPathsCount / this.GitTrackedItemsCount;
52+
}
53+
}
54+
}
55+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System.Collections;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
namespace GVFS.Common
6+
{
7+
public class EnlistmentPathData
8+
{
9+
public List<string> GitFolderPaths;
10+
public List<string> GitFilePaths;
11+
public List<string> PlaceholderFolderPaths;
12+
public List<string> PlaceholderFilePaths;
13+
public List<string> ModifiedFolderPaths;
14+
public List<string> ModifiedFilePaths;
15+
public List<string> GitTrackingPaths;
16+
17+
public EnlistmentPathData()
18+
{
19+
this.GitFolderPaths = new List<string>();
20+
this.GitFilePaths = new List<string>();
21+
this.PlaceholderFolderPaths = new List<string>();
22+
this.PlaceholderFilePaths = new List<string>();
23+
this.ModifiedFolderPaths = new List<string>();
24+
this.ModifiedFilePaths = new List<string>();
25+
this.GitTrackingPaths = new List<string>();
26+
}
27+
28+
public void NormalizeAllPaths()
29+
{
30+
this.NormalizePaths(this.GitFolderPaths);
31+
this.NormalizePaths(this.GitFilePaths);
32+
this.NormalizePaths(this.PlaceholderFolderPaths);
33+
this.NormalizePaths(this.PlaceholderFilePaths);
34+
this.NormalizePaths(this.ModifiedFolderPaths);
35+
this.NormalizePaths(this.ModifiedFilePaths);
36+
this.NormalizePaths(this.GitTrackingPaths);
37+
38+
this.ModifiedFilePaths = this.ModifiedFilePaths.Union(this.GitTrackingPaths).ToList();
39+
}
40+
41+
private void NormalizePaths(List<string> paths)
42+
{
43+
for (int i = 0; i < paths.Count; i++)
44+
{
45+
paths[i] = paths[i].Replace(GVFSPlatform.GVFSPlatformConstants.PathSeparator, GVFSConstants.GitPathSeparator);
46+
paths[i] = paths[i].Trim(GVFSConstants.GitPathSeparator);
47+
}
48+
}
49+
}
50+
}

GVFS/GVFS.Common/NamedPipes/NamedPipeMessages.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ public static class ModifiedPaths
7171
public const string ListRequest = "MPL";
7272
public const string InvalidVersion = "InvalidVersion";
7373
public const string SuccessResult = "S";
74+
public const string CurrentVersion = "1";
7475

7576
public class Request
7677
{

0 commit comments

Comments
 (0)