Skip to content

Commit c56ffbc

Browse files
authored
Fixes bugs with change files microsoft#1465 microsoft#2777 (microsoft#2799)
1 parent 53ee84d commit c56ffbc

28 files changed

+289
-117
lines changed

docs/changelog.md

+4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ What's changed since pre-release v3.0.0-B0416:
3636
- Bug fix:
3737
- Fixed custom emitter loaded too late by @BernieWhite.
3838
[#2775](https://github.com/microsoft/PSRule/issues/2775)
39+
- Fixed Changed files includes excluded paths by @BernieWhite.
40+
[#1465](https://github.com/microsoft/PSRule/issues/1465)
41+
- Fixed branch ref for working with changes files only examples by @BernieWhite.
42+
[#2777](https://github.com/microsoft/PSRule/issues/2777)
3943

4044
## v3.0.0-B0416 (pre-release)
4145

docs/creating-your-pipeline.md

+19-4
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,23 @@ This option does not work with a shallow or detached checkout, full git history
228228
Assert-PSRule -Options $options -InputPath '.' -Module $modules -Format File -ErrorAction Stop;
229229
```
230230

231-
!!! Tip
232-
In some cases it may be necessary to set `Repository.BaseRef` to the default branch of your repository.
233-
By default, PSRule will detect the default branch of the repository from the build system environment variables.
234-
235231
[8]: concepts/PSRule/en-US/about_PSRule_Options.md#inputignoreunchangedpath
232+
233+
### Setting the base branch
234+
235+
In some cases it may be necessary to set `Repository.BaseRef` to the default branch of your repository for comparison.
236+
This option can also be set by using the `PSRULE_REPOSITORY_BASEREF` environment variable.
237+
By default, PSRule will attempt to detect the default branch of the repository.
238+
239+
```yaml title="ps-rule.yaml"
240+
repository:
241+
baseRef: main
242+
```
243+
244+
For CI pipelines, the default branch is usually `main` or `master` or an integration branch.
245+
However, because these branches are not checked out locally by default, you should prepend the remote to the branch name.
246+
247+
```yaml title="ps-rule.yaml"
248+
repository:
249+
baseRef: origin/main
250+
```

mkdocs.yml

+3-1
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,9 @@ markdown_extensions:
130130
- includes/en/abbreviations.md
131131
- pymdownx.highlight:
132132
auto_title: false
133-
anchor_linenums: false
133+
anchor_linenums: true
134+
line_spans: __span
135+
pygments_lang_class: true
134136
- pymdownx.superfences
135137
- pymdownx.pathconverter
136138
- pymdownx.tabbed:

src/PSRule.CommandLine/Commands/RunCommand.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public static async Task<int> RunAsync(RunOptions operationOptions, ClientContex
6363

6464
// Build command.
6565
var builder = CommandLineBuilder.Assert(operationOptions.Module ?? [], clientContext.Option, clientContext.Host, file, clientContext.ResolvedModuleVersions);
66-
builder.Baseline(Configuration.BaselineOption.FromString(operationOptions.Baseline));
66+
builder.Baseline(BaselineOption.FromString(operationOptions.Baseline));
6767
builder.InputPath(inputPath);
6868
builder.UnblockPublisher(PUBLISHER);
6969
builder.Formats(operationOptions.Formats);

src/PSRule.Types/Data/InputFileInfo.cs

+9-4
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ internal InputFileInfo(string basePath, string path)
2727
BasePath = basePath;
2828
Name = System.IO.Path.GetFileName(path);
2929
Extension = System.IO.Path.GetExtension(path);
30-
DirectoryName = System.IO.Path.GetDirectoryName(path);
31-
DisplayName = Helpers.NormalizePath(basePath, FullName);
32-
Path = Helpers.NormalizePath(basePath, FullName);
30+
DirectoryName = System.IO.Path.GetDirectoryName(Environment.GetRootedPath(path, normalize: false, basePath: basePath));
31+
DisplayName = PathHelpers.NormalizePath(basePath, FullName);
32+
Path = PathHelpers.NormalizePath(basePath, FullName);
3333
_TargetType = string.IsNullOrEmpty(Extension) ? System.IO.Path.GetFileNameWithoutExtension(path) : Extension;
3434
}
3535

@@ -39,10 +39,15 @@ internal InputFileInfo(string basePath, string path)
3939
public string? FullName { get; }
4040

4141
/// <summary>
42-
/// The path to the parent directory containing the file.
42+
/// The base path containing the file.
4343
/// </summary>
4444
public string? BasePath { get; }
4545

46+
/// <summary>
47+
/// The parent path to the file.
48+
/// </summary>
49+
public string? ParentPath { get; }
50+
4651
/// <summary>
4752
/// The file name.
4853
/// </summary>

src/PSRule.Types/Data/TargetSourceInfo.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,6 @@ public string ToString(string? defaultType, bool useRelativePath)
132132

133133
internal string? GetPath(bool useRelativePath)
134134
{
135-
return useRelativePath ? Helpers.NormalizePath(Environment.GetWorkingPath(), File) : File;
135+
return useRelativePath ? PathHelpers.NormalizePath(Environment.GetWorkingPath(), File) : File;
136136
}
137137
}

src/PSRule.Types/Expressions/Helpers.cs src/PSRule.Types/Expressions/PathHelpers.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
namespace PSRule.Expressions;
55

6-
internal static class Helpers
6+
internal static class PathHelpers
77
{
88
private const char Backslash = '\\';
99
private const char Slash = '/';

src/PSRule.Types/Runtime/LoggerExtensions.cs

+36
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using PSRule.Options;
5+
46
namespace PSRule.Runtime;
57

68
/// <summary>
@@ -76,11 +78,45 @@ public static void Log(this ILogger logger, LogLevel logLevel, EventId eventId,
7678
logger.Log(logLevel, eventId, new FormattedLogValues(message, args), exception, _messageFormatter);
7779
}
7880

81+
/// <summary>
82+
/// Log a diagnostic message with an <see cref="ExecutionActionPreference"/>.
83+
/// The log level is determined by the action preference.
84+
/// </summary>
85+
/// <param name="logger">A valid <see cref="ILogger"/> instance.</param>
86+
/// <param name="actionPreference">The action preference of the diagnostic message.</param>
87+
/// <param name="eventId">An event identifier for the diagnostic message.</param>
88+
/// <param name="exception">An optional exception which the diagnostic message is related to.</param>
89+
/// <param name="message">The format message text.</param>
90+
/// <param name="args">Additional arguments to use within the format message.</param>
91+
public static void Log(this ILogger logger, ExecutionActionPreference actionPreference, EventId eventId, Exception? exception, string? message, params object?[] args)
92+
{
93+
var logLevel = GetLogLevel(actionPreference);
94+
if (logger == null || !logger.IsEnabled(logLevel))
95+
return;
96+
97+
logger.Log(logLevel, eventId, new FormattedLogValues(message, args), exception, _messageFormatter);
98+
}
99+
79100
/// <summary>
80101
/// Format log messages with values.
81102
/// </summary>
82103
private static string MessageFormatter(FormattedLogValues state, Exception? error)
83104
{
84105
return state.ToString();
85106
}
107+
108+
/// <summary>
109+
/// Convert an <see cref="ExecutionActionPreference"/> to a <see cref="LogLevel"/>.
110+
/// </summary>
111+
private static LogLevel GetLogLevel(ExecutionActionPreference actionPreference)
112+
{
113+
return actionPreference switch
114+
{
115+
ExecutionActionPreference.Ignore => LogLevel.None,
116+
ExecutionActionPreference.Error => LogLevel.Error,
117+
ExecutionActionPreference.Warn => LogLevel.Warning,
118+
ExecutionActionPreference.Debug => LogLevel.Debug,
119+
_ => LogLevel.None,
120+
};
121+
}
86122
}

src/PSRule/Data/InputFileInfoCollection.cs

+8-2
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
// Licensed under the MIT License.
33

44
using System.Collections;
5+
using PSRule.Expressions;
56

67
namespace PSRule.Data;
78

89
/// <summary>
9-
/// A collection of <see cref="InputFileInfo"/>.
10+
/// A collection of <see cref="InputFileInfo"/> that can be filtered.
1011
/// </summary>
1112
internal sealed class InputFileInfoCollection : IInputFileInfoCollection, IEnumerable<InputFileInfo>
1213
{
@@ -19,14 +20,19 @@ public InputFileInfoCollection(IEnumerable<InputFileInfo> items)
1920

2021
public InputFileInfoCollection(string basePath, string[] items)
2122
{
22-
_Items = items != null && items.Length > 0 ? items.Select(i => new InputFileInfo(basePath, i)).ToArray() : Array.Empty<InputFileInfo>();
23+
_Items = items != null && items.Length > 0 ? [.. items.Select(i => new InputFileInfo(basePath, i))] : [];
2324
}
2425

2526
public IInputFileInfoCollection WithExtension(string extension)
2627
{
2728
return new InputFileInfoCollection(_Items.Where(i => i.Extension == extension));
2829
}
2930

31+
public IInputFileInfoCollection WithinPath(string path, bool caseSensitive = false)
32+
{
33+
return new InputFileInfoCollection(_Items.Where(i => PathHelpers.WithinPath(i.Path, path, caseSensitive)));
34+
}
35+
3036
#region IEnumerable<InputFileInfo>
3137

3238
public IEnumerator<InputFileInfo> GetEnumerator()

src/PSRule/Data/RepositoryInfo.cs

-41
This file was deleted.

src/PSRule/Emitters/EmitterContext.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ namespace PSRule.Emitters;
1616
internal sealed class EmitterContext : BaseEmitterContext
1717
{
1818
private readonly ConcurrentQueue<ITargetObject> _Queue;
19-
private readonly PathFilter? _InputFilter;
19+
private readonly IPathFilter? _InputFilter;
2020

2121
/// <summary>
2222
/// Create an instance containing context for an <see cref="IEmitter"/>.
2323
/// </summary>
24-
internal EmitterContext(ConcurrentQueue<ITargetObject> queue, PathFilter? inputFilter, PSRuleOption? option)
24+
internal EmitterContext(ConcurrentQueue<ITargetObject> queue, IPathFilter? inputFilter, PSRuleOption? option)
2525
: base(option?.Input?.StringFormat, option?.Input?.ObjectPath, option?.Input?.FileObjects ?? false)
2626
{
2727
_Queue = queue;

src/PSRule/Host/HostHelper.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ public static void InvokeRuleBlock(RunspaceContext context, RuleBlock ruleBlock,
371371
{
372372
RunspaceContext.CurrentThread = context;
373373
var condition = ruleBlock.Condition;
374-
context.VerboseObjectStart();
374+
context.LogObjectStart();
375375

376376
try
377377
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace PSRule.Pipeline;
5+
6+
#nullable enable
7+
8+
/// <summary>
9+
/// Wrap a path filter with a filter that only allows files that have changed.
10+
/// </summary>
11+
internal sealed class ChangedFilesPathFilter : IPathFilter
12+
{
13+
private readonly IPathFilter _Inner;
14+
private readonly string _BasePath;
15+
private readonly HashSet<string>? _ChangedFiles;
16+
private readonly StringComparison _FilePathComparer = StringComparison.Ordinal;
17+
18+
public ChangedFilesPathFilter(IPathFilter inner, string basePath, string[] changedFiles)
19+
{
20+
if (string.IsNullOrEmpty(basePath)) throw new ArgumentNullException(nameof(basePath));
21+
if (changedFiles == null) throw new ArgumentNullException(nameof(changedFiles));
22+
23+
_Inner = inner ?? throw new ArgumentNullException(nameof(inner));
24+
_BasePath = Environment.GetRootedBasePath(basePath, normalize: true);
25+
_ChangedFiles = changedFiles.Length == 0 ? null : new HashSet<string>(changedFiles, _FilePathComparer == StringComparison.Ordinal ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase);
26+
}
27+
28+
/// <inheritdoc/>
29+
public bool Match(string path)
30+
{
31+
if (_ChangedFiles == null || string.IsNullOrEmpty(path))
32+
return false;
33+
34+
// Check if the path is rooted and starts with the base path.
35+
var rootedFilePath = Environment.GetRootedPath(path, normalize: true, basePath: _BasePath);
36+
if (!rootedFilePath.StartsWith(_BasePath, _FilePathComparer))
37+
return false;
38+
39+
var relativePath = rootedFilePath.Substring(_BasePath.Length);
40+
return _ChangedFiles.Contains(relativePath) && _Inner.Match(path);
41+
}
42+
}
43+
44+
#nullable restore

src/PSRule/Pipeline/GetTargetPipelineBuilder.cs

+7-5
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,16 @@ public void InputPath(string[]? path)
4343
if (path == null || path.Length == 0)
4444
return;
4545

46-
PathFilter? required = null;
47-
if (TryChangedFiles(out var files))
46+
var basePath = Environment.GetWorkingPath();
47+
var filter = GetInputFilter();
48+
49+
// Wrap with a filter that only allows files that have changed.
50+
if (TryChangedFiles(out var files) && files != null)
4851
{
49-
required = PathFilter.Create(Environment.GetWorkingPath(), path);
50-
path = files;
52+
filter = new ChangedFilesPathFilter(filter, basePath, files);
5153
}
5254

53-
var builder = new InputPathBuilder(GetOutput(), Environment.GetWorkingPath(), "*", GetInputFilter(), required);
55+
var builder = new InputPathBuilder(PrepareWriter(), basePath, "*", filter, null);
5456
builder.Add(path);
5557
_InputPath = builder;
5658
}

src/PSRule/Pipeline/IPathFilter.cs

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace PSRule.Pipeline;
5+
6+
/// <summary>
7+
/// An interface for implementing a path filter.
8+
/// </summary>
9+
internal interface IPathFilter
10+
{
11+
/// <summary>
12+
/// Determine if the specified path matches the filter meaning it met the conditions to be included.
13+
/// </summary>
14+
/// <param name="path">The specified path.</param>
15+
/// <returns>rReturns <c>true</c> if the path should be included.</returns>
16+
bool Match(string path);
17+
}

src/PSRule/Pipeline/InputPathBuilder.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace PSRule.Pipeline;
66
/// <summary>
77
/// A builder for input paths.
88
/// </summary>
9-
internal sealed class InputPathBuilder(IPipelineWriter logger, string basePath, string searchPattern, PathFilter filter, PathFilter required)
9+
internal sealed class InputPathBuilder(IPipelineWriter logger, string basePath, string searchPattern, IPathFilter filter, IPathFilter required)
1010
: PathBuilder(logger, basePath, searchPattern, filter, required)
1111
{
1212
}

src/PSRule/Pipeline/InvokePipelineBuilderBase.cs

+7-5
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,16 @@ public void InputPath(string[]? path)
3131
if (path == null || path.Length == 0)
3232
return;
3333

34-
PathFilter? required = null;
35-
if (TryChangedFiles(out var files))
34+
var basePath = Environment.GetWorkingPath();
35+
var filter = GetInputFilter();
36+
37+
// Wrap with a filter that only allows files that have changed.
38+
if (TryChangedFiles(out var files) && files != null)
3639
{
37-
required = PathFilter.Create(Environment.GetWorkingPath(), path, matchResult: true);
38-
path = files;
40+
filter = new ChangedFilesPathFilter(filter, basePath, files);
3941
}
4042

41-
var builder = new InputPathBuilder(PrepareWriter(), Environment.GetWorkingPath(), "*", GetInputFilter(), required);
43+
var builder = new InputPathBuilder(PrepareWriter(), basePath, "*", filter, null);
4244
builder.Add(path);
4345
_InputPath = builder;
4446
}

0 commit comments

Comments
 (0)