/
FilterPath.cs
192 lines (172 loc) · 7.63 KB
/
FilterPath.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using LibGit2Sharp;
using Validation;
namespace Nerdbank.GitVersioning
{
/// <summary>
/// A filter (include or exclude) representing a repo relative path.
/// </summary>
internal class FilterPath
{
private readonly StringComparison stringComparison;
/// <summary>
/// True if this <see cref="FilterPath"/> represents an exclude filter.
/// </summary>
internal bool IsExclude { get; }
/// <summary>
/// Path relative to the repository root that this <see cref="FilterPath"/> represents.
/// Slashes are canonical for this OS.
/// </summary>
internal string RepoRelativePath { get; }
/// <summary>
/// True if this <see cref="FilterPath"/> represents the root of the repository.
/// </summary>
internal bool IsRoot => this.RepoRelativePath == "";
/// <summary>
/// Parses a pathspec-like string into a root-relative path.
/// </summary>
/// <param name="path">
/// See <see cref="FilterPath(string, string, bool)"/> for supported
/// formats of pathspecs.
/// </param>
/// <param name="relativeTo">
/// Path that <paramref name="path"/> is relative to.
/// Can be <c>null</c> - which indicates <paramref name="path"/> is
/// relative to the root of the repository.
/// </param>
/// <returns>
/// Forward slash delimited string representing the root-relative path.
/// </returns>
private static string ParsePath(string path, string relativeTo)
{
// Path is absolute, nothing to do here
if (path[0] == '/' || path[0] == '\\')
{
return path.Substring(1);
}
var combined = relativeTo == null ? path : relativeTo + '/' + path;
return string.Join("/",
combined
.Split(new[] {Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar},
StringSplitOptions.RemoveEmptyEntries)
// Loop through each path segment...
.Aggregate(new Stack<string>(), (parts, segment) =>
{
switch (segment)
{
// If it refers to the current directory, skip it
case ".":
return parts;
// If it refers to the parent directory, pop the most recent directory
case "..":
if (parts.Count == 0)
throw new FormatException($"Too many '..' in path '{combined}' - would escape the root of the repository.");
parts.Pop();
return parts;
// Otherwise it's a directory/file name - add it to the stack
default:
parts.Push(segment);
return parts;
}
})
// Reverse the stack, so it iterates root -> leaf
.Reverse()
);
}
/// <summary>
/// Construct a <see cref="FilterPath"/> from a pathspec-like string and a
/// relative path within the repository.
/// </summary>
/// <param name="pathSpec">
/// A string that supports some pathspec features.
/// This path is relative to <paramref name="relativeTo"/>.
///
/// Examples:
/// - <c>../relative/inclusion.txt</c>
/// - <c>:/absolute/inclusion.txt</c>
/// - <c>:!relative/exclusion.txt</c>
/// - <c>:^relative/exclusion.txt</c>
/// - <c>:^/absolute/exclusion.txt</c>
/// </param>
/// <param name="relativeTo">
/// Path (relative to the root of the repository) that <paramref name="pathSpec"/> is relative to.
/// </param>
/// <param name="ignoreCase">Whether case should be ignored by <see cref="Excludes"/></param>
/// <exception cref="FormatException">Invalid path spec.</exception>
internal FilterPath(string pathSpec, string relativeTo, bool ignoreCase = false)
{
Requires.NotNullOrEmpty(pathSpec, nameof(pathSpec));
if (pathSpec[0] == ':')
{
if (pathSpec.Length > 1 && (pathSpec[1] == '^' || pathSpec[1] == '!'))
{
this.IsExclude = true;
this.stringComparison = ignoreCase
? StringComparison.OrdinalIgnoreCase
: StringComparison.Ordinal;
this.RepoRelativePath = ParsePath(pathSpec.Substring(2), relativeTo);
}
else if (pathSpec.Length > 1 && pathSpec[1] == '/' || pathSpec[1] == '\\')
{
this.RepoRelativePath = pathSpec.Substring(2);
}
else
{
throw new FormatException($"Unrecognized path spec '{pathSpec}'");
}
}
else
{
this.RepoRelativePath = ParsePath(pathSpec, relativeTo);
}
this.RepoRelativePath =
this.RepoRelativePath
.Replace('\\', '/')
.TrimEnd('/');
}
/// <summary>
/// Calculate the <see cref="FilterPath"/>s for a given project within a repository.
/// </summary>
/// <param name="versionOptions">Version options for the project.</param>
/// <param name="relativeRepoProjectDirectory">
/// Path to the project directory, relative to the root of the repository.
/// If <c>null</c>, assumes root of repository.
/// </param>
/// <param name="repository">Git repository containing the project.</param>
/// <returns>
/// <c>null</c> if no path filters are set. Otherwise, returns a list of
/// <see cref="FilterPath"/> instances.
/// </returns>
internal static IReadOnlyList<FilterPath> FromVersionOptions(VersionOptions versionOptions,
string relativeRepoProjectDirectory,
IRepository repository)
{
Requires.NotNull(versionOptions, nameof(versionOptions));
var ignoreCase = repository?.Config.Get<bool>("core.ignorecase")?.Value ?? false;
return versionOptions.PathFilters
?.Select(pathSpec => new FilterPath(pathSpec, relativeRepoProjectDirectory,
ignoreCase))
.ToList();
}
/// <summary>
/// Determines if <paramref name="repoRelativePath"/> should be excluded by this <see cref="FilterPath"/>.
/// </summary>
/// <param name="repoRelativePath">Forward-slash delimited path (repo relative).</param>
/// <returns>
/// True if this <see cref="FilterPath"/> is an excluding filter that matches
/// <paramref name="repoRelativePath"/>, otherwise false.
/// </returns>
internal bool Excludes(string repoRelativePath)
{
if (repoRelativePath is null)
throw new ArgumentNullException(nameof(repoRelativePath));
if (!this.IsExclude) return false;
return this.RepoRelativePath.Equals(repoRelativePath, this.stringComparison) ||
repoRelativePath.StartsWith(this.RepoRelativePath + "/",
this.stringComparison);
}
}
}