Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
root = true

# ======================================================================================================
# Every file
#
# Line endings are LF everywhere, on every OS. .gitattributes stores LF and checks out LF - that is the
# guarantee, and it holds whatever core.autocrlf a contributor has. end_of_line below is what makes the
# editors and formatters write LF too, so the working tree never drifts.
# ======================================================================================================

[*]
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
tab_width = 4
indent_size = 4
end_of_line = crlf

# Shell scripts require LF (see .gitattributes).
[*.sh]
end_of_line = lf

# YAML conventionally uses 2-space indentation.
Expand Down
14 changes: 14 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Commits that rewrote lines across the repository without changing what the code does.
# `git blame` skips them, so it points at whoever wrote the logic rather than at the tool.
#
# GitHub reads this file automatically. Locally you have to opt in, once per clone:
#
# git config blame.ignoreRevsFile .git-blame-ignore-revs
#
# The SHAs below are load-bearing, and git does NOT warn about one it cannot resolve: it skips the
# entry. "Rebase and merge" and "Squash and merge" rewrite every SHA on a branch, so merge a branch
# that adds an entry here with a MERGE COMMIT. CI's lint job fails if a revision stops resolving.

# style: renormalize line endings to LF
# Line endings only.
3c9a8deec2a886ad7c8e5a269c5f8b60b11cc78b
45 changes: 15 additions & 30 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,31 +1,16 @@
# Auto-detect text files, normalize to LF in the repo
* text=auto
# Every text file is LF, in the repository and in the working tree, on every OS.
# Git auto-detects text vs. binary; the rules below are the exceptions.
#
# The explicit eol= overrides core.autocrlf and core.eol, so a contributor needs no git configuration
# and a wrong one cannot change what lands in the repository. CI fails if a file is ever stored
# otherwise.
* text=auto eol=lf

# Force specific line endings where it matters
*.cs text eol=crlf
*.csproj text eol=crlf
*.slnf text eol=crlf
*.slnx text eol=crlf
*.props text eol=crlf
*.targets text eol=crlf
*.xaml text eol=crlf
*.axaml text eol=crlf
*.resx text eol=crlf
*.manifest text eol=crlf
*.json text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
*.md text eol=lf

# Windows batch files require CRLF; shell scripts require LF.
*.cmd text eol=crlf
*.bat text eol=crlf
*.sh text eol=lf

# Binary files - never touch
*.png binary
*.jpg binary
*.ico binary
*.dll binary
*.exe binary
*.snk binary
# Binary files - never convert. text=auto already detects most of these; the list makes it certain.
*.png binary
*.jpg binary
*.gif binary
*.ico binary
*.snk binary
*.dll binary
*.exe binary
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@
- [ ] Public API changes are declared in the affected `PublicAPI.Unshipped.txt`.
- [ ] XML docs and `README.md` updated for public API changes.
- [ ] `CHANGELOG.md` updated under *Unreleased*.
- [ ] Code formatted (`scripts/formatCode.cmd` / `scripts/formatCode.sh`).
- [ ] Code formatted (`pwsh -File scripts/tidy-code.ps1`).
103 changes: 76 additions & 27 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,31 +34,88 @@ env:
NUGET_CONFIG: NuGet.config
PACKAGE_OUTPUT: artifacts/packages
RELEASE_NOTES_FILE: release-notes.md
RELEASE_NOTES_SCRIPT: scripts/extract-release-notes.sh
RELEASE_NOTES_SCRIPT: scripts/extract-release-notes.ps1
SOLUTION: ResXLocalization.slnx
SOLUTION_FILTER_NON_WINDOWS: ResXLocalization.NonWindows.slnf
SAMPLES_DIRECTORY: samples
SOURCE_DIRECTORY: src
TEST_RESULTS: artifacts/test-results
WPF_CONSUMER_PROJECT: tests/package-consumption/WpfConsumer/WpfConsumer.csproj
WPF_TEST_PROJECT: tests/ResXLocalization.WPF.Sample.Tests/ResXLocalization.WPF.Sample.Tests.csproj

jobs:
# Enforce the formatting the repo ships tooling for (dotnet format + XamlStyler): a style rule that
# is not enforced in CI drifts with the first external pull request.
# Windows runner: XamlStyler always writes the host OS's newline and has no line-ending setting,
# and .gitattributes checks the XAML files out as CRLF everywhere — so the passive check can only
# pass on Windows.
# Enforce the formatting the repo ships tooling for: a rule that is not enforced in CI drifts with
# the first external pull request.
#
# This runs scripts/tidy-code.ps1, the same entry point a developer uses. -Check formats the checkout
# for real and then asks whether anything changed: XamlStyler has no usable check mode here, because
# it compares against its own CRLF output and so fails on LF files on Windows. Writing to the
# checkout is harmless in CI; it is thrown away at the end of the job.
#
# Ubuntu is a cost and speed choice, not a correctness one: -Check gives the same answer on every OS,
# and on Linux the non-Windows solution filter keeps the WPF projects out of the restore.
lint:
name: Lint
runs-on: windows-latest
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
# Full history, for the .git-blame-ignore-revs check below; at depth 1 no revision it names
# would resolve.
fetch-depth: 0

# .gitattributes only applies when a file is ADDED. A file committed past it, or a rule added
# after the file, stays wrong forever and nothing complains - and commits created server-side,
# such as a bot's, bypass the filter entirely. Renormalizing and asking whether anything changed
# is the exact question, and it needs no toolchain.
- name: Verify line endings are normalized
run: |
git add --renormalize .
if ! git diff --cached --quiet; then
git --no-pager diff --cached --name-only
echo "::error title=Line endings::The files above are not stored as .gitattributes requires. Run 'git add --renormalize .' and commit the result."
exit 1
fi

# .git-blame-ignore-revs names commits by raw SHA, and git does NOT complain about one it cannot
# resolve - it silently skips the entry. A rebase-merge or squash-merge rewrites every SHA on a
# branch, which is exactly how that happens. This step is what makes it visible.
- name: Verify .git-blame-ignore-revs
run: |
if [ ! -f .git-blame-ignore-revs ]; then
echo "No .git-blame-ignore-revs - nothing to check."
exit 0
fi

status=0
count=0
while read -r revision _ || [ -n "${revision}" ]; do
revision="${revision%$'\r'}"
case "${revision}" in ''|\#*) continue ;; esac
count=$((count + 1))

if ! git rev-parse --verify --quiet "${revision}^{commit}" > /dev/null; then
echo "::error file=.git-blame-ignore-revs::${revision} is not a commit in this repository. If the branch that introduced it was rebased or squash-merged, replace it with the SHA it became."
status=1
elif ! git merge-base --is-ancestor "${revision}" HEAD; then
echo "::error file=.git-blame-ignore-revs::${revision} is a commit, but not an ancestor of HEAD, so blame can never reach it."
status=1
fi
done < .git-blame-ignore-revs

if [ "${count}" -eq 0 ]; then
echo "::error file=.git-blame-ignore-revs::The file lists no revisions. Delete it if it is no longer wanted."
exit 1
fi

if [ "${status}" -ne 0 ]; then
echo "Checked ${count} revision(s); see the errors above."
exit 1
fi

echo "All ${count} revision(s) resolve and are reachable."

- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
Expand All @@ -82,23 +139,13 @@ jobs:
- name: Restore
run: dotnet restore ${{ env.SOLUTION_FILTER_NON_WINDOWS }} --configfile ${{ env.NUGET_CONFIG }}

# Whitespace only: the semantic style and analyzer rules are already enforced by the build
# itself (EnforceCodeStyleInBuild + TreatWarningsAsErrors) in the verify jobs.
- name: Verify C# formatting (dotnet format)
run: dotnet format whitespace ${{ env.SOLUTION_FILTER_NON_WINDOWS }} --no-restore --verify-no-changes
- name: Verify formatting
run: pwsh -NoProfile -NonInteractive -File scripts/tidy-code.ps1 -Check

- name: Verify XAML formatting (XamlStyler)
shell: bash
# xstyler's passive mode exits non-zero when files fail the check; "|| true" keeps the
# step alive under "bash -e" so the output and the actionable error below still print.
# The grep on the output is what decides pass/fail.
run: |
output="$(dotnet xstyler --passive --recursive --directory "${SOURCE_DIRECTORY}" || true; dotnet xstyler --passive --recursive --directory "${SAMPLES_DIRECTORY}" || true)"
echo "${output}"
if grep -q "FAIL" <<< "${output}"; then
echo "::error title=XAML formatting::Run scripts/formatCode (dotnet xstyler --recursive --directory src && dotnet xstyler --recursive --directory samples) and commit the result."
exit 1
fi
# The check above only says that something is unformatted. This says what.
- name: Show the changes that would fix it
if: failure()
run: git --no-pager diff --stat && git --no-pager diff

# Cross-platform: build and test the shared Core, the source generator, and the Avalonia stack. These
# projects target net8.0/net10.0 and build/test identically on Linux and Windows. The WPF projects are
Expand Down Expand Up @@ -465,7 +512,8 @@ jobs:
path: ${{ env.PACKAGE_OUTPUT }}

- name: Validate release changelog before publication
run: ${{ env.RELEASE_NOTES_SCRIPT }} "${GITHUB_REF_NAME#v}"
shell: pwsh
run: ./${{ env.RELEASE_NOTES_SCRIPT }} -Version "$($env:GITHUB_REF_NAME -replace '^v', '')"

- name: Verify the tag matches the packed version
shell: bash
Expand Down Expand Up @@ -536,7 +584,8 @@ jobs:
path: ${{ env.PACKAGE_OUTPUT }}

- name: Extract the changelog section for this version
run: ${{ env.RELEASE_NOTES_SCRIPT }} "${GITHUB_REF_NAME#v}"
shell: pwsh
run: ./${{ env.RELEASE_NOTES_SCRIPT }} -Version "$($env:GITHUB_REF_NAME -replace '^v', '')"

- name: Create the release
env:
Expand Down
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ dotnet test tests/ResXLocalization.Avalonia.Sample.Tests/ResXLocalization.Avalon
dotnet test tests/ResXLocalization.WPF.Sample.Tests/ResXLocalization.WPF.Sample.Tests.csproj -c Release # Windows only

# Format before committing (CI fails otherwise).
scripts\formatCode.cmd # Windows
scripts/formatCode.sh # Linux/macOS
pwsh -File scripts/tidy-code.ps1
```

## Conventions the build enforces (get these right or CI fails)
Expand All @@ -30,6 +29,9 @@ scripts/formatCode.sh # Linux/macOS
to allow it.
- **Zero warnings.** `TreatWarningsAsErrors` + `AnalysisMode=All` + StyleCop + Roslynator +
ErrorProne.NET. A warning is a build failure. Match the surrounding style; `.editorconfig` decides.
- **Line endings are LF everywhere.** `.gitattributes` and `.editorconfig` enforce this and CI verifies
it. Never hand-convert line endings, and never compare a multi-line source literal against
`Environment.NewLine` - the literal carries the file's bytes, `Environment.NewLine` carries the host's.
- **Avalonia/WPF symmetry.** `src/ResXLocalization.Avalonia` and `src/ResXLocalization.WPF` mirror
each other (markup extensions, converters, behaviors). A change to one almost always needs the
mirrored change in the other, plus mirrored tests.
Expand Down
32 changes: 30 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,36 @@ dotnet test tests/ResXLocalization.WPF.Sample.Tests/ResXLocalization.WPF.Sample.
Format the code before committing - CI enforces it:

```shell
scripts\formatCode.cmd # Windows
scripts/formatCode.sh # Linux/macOS
pwsh -File scripts/tidy-code.ps1
```

## Line endings

Every text file is LF, in the repository and in the working tree, on every OS. `.gitattributes`
enforces this whatever your `core.autocrlf` is set to, so there is nothing to configure, and CI fails
if a wrongly stored file lands anyway.

`.editorconfig` also asks editors and formatters to write LF. XamlStyler cannot: it always writes the
host OS newline, so `scripts/tidy-code.ps1` rewrites the XAML back to LF after running it. If some
other tool writes CRLF, git still stores LF, but `git status` lists the file as modified while
`git diff` shows nothing. Run `pwsh -File scripts/tidy-code.ps1` to fix it, or `git checkout -- <path>`.

If you have set `git config core.safecrlf true`, git refuses to add such a file with "CRLF would be
replaced by LF". Run the tidy script first, or use `core.safecrlf warn`.

One more one-time step after cloning, so `git blame` skips whole-repository mechanical commits (GitHub
does this automatically):

```shell
git config blame.ignoreRevsFile .git-blame-ignore-revs
```

To refresh a clone made before this policy (commit or stash your changes first - the second command
discards uncommitted work):

```shell
git rm -r --cached . -q
git reset --hard
```

## Pull request checklist
Expand Down
74 changes: 37 additions & 37 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,37 +1,37 @@
<Project>
<!-- Central Package Management: every PackageReference version in the repo lives here.
https://learn.microsoft.com/nuget/consume-packages/central-package-management -->
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<!-- Runtime dependencies. -->
<PackageVersion Include="Avalonia" Version="12.1.0" />
<PackageVersion Include="Avalonia.Desktop" Version="12.1.0" />
<PackageVersion Include="Avalonia.Fonts.Inter" Version="12.1.0" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.1.0" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />
<!-- Source generator toolchain. -->
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" />
<!-- Analyzers (referenced centrally from Directory.Build.props; PublicApiAnalyzers only by
the shipped libraries). -->
<PackageVersion Include="ErrorProne.NET.CoreAnalyzers" Version="0.9.0-beta.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="5.6.0" />
<PackageVersion Include="Roslynator.Analyzers" Version="4.15.0" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<!-- Test stack (coverlet + the logger are referenced centrally for every test project). -->
<PackageVersion Include="Avalonia.Headless.XUnit" Version="12.1.0" />
<PackageVersion Include="AwesomeAssertions" Version="9.5.0" />
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
<!-- 2.x is the VSTest logger; 3.x targets Microsoft.Testing.Platform, which xunit.v3 under
Microsoft.NET.Test.Sdk does not use here. -->
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
</ItemGroup>
</Project>
<Project>
<!-- Central Package Management: every PackageReference version in the repo lives here.
https://learn.microsoft.com/nuget/consume-packages/central-package-management -->
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>

<ItemGroup>
<!-- Runtime dependencies. -->
<PackageVersion Include="Avalonia" Version="12.1.0" />
<PackageVersion Include="Avalonia.Desktop" Version="12.1.0" />
<PackageVersion Include="Avalonia.Fonts.Inter" Version="12.1.0" />
<PackageVersion Include="Avalonia.Themes.Fluent" Version="12.1.0" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.2" />

<!-- Source generator toolchain. -->
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" />

<!-- Analyzers (referenced centrally from Directory.Build.props; PublicApiAnalyzers only by
the shipped libraries). -->
<PackageVersion Include="ErrorProne.NET.CoreAnalyzers" Version="0.9.0-beta.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="5.6.0" />
<PackageVersion Include="Roslynator.Analyzers" Version="4.15.0" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />

<!-- Test stack (coverlet + the logger are referenced centrally for every test project). -->
<PackageVersion Include="Avalonia.Headless.XUnit" Version="12.1.0" />
<PackageVersion Include="AwesomeAssertions" Version="9.5.0" />
<PackageVersion Include="coverlet.collector" Version="10.0.1" />
<!-- 2.x is the VSTest logger; 3.x targets Microsoft.Testing.Platform, which xunit.v3 under
Microsoft.NET.Test.Sdk does not use here. -->
<PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageVersion Include="xunit.v3" Version="3.2.2" />
</ItemGroup>
</Project>
Loading
Loading