diff --git a/.github/actions/install_unix_deps/action.yml b/.github/actions/install_unix_deps/action.yml index b0a1831abf2..bd44fe9f52b 100644 --- a/.github/actions/install_unix_deps/action.yml +++ b/.github/actions/install_unix_deps/action.yml @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 diff --git a/.github/copy-pr-bot.yaml b/.github/copy-pr-bot.yaml index e2fc4a5f4a2..1c328793a11 100644 --- a/.github/copy-pr-bot.yaml +++ b/.github/copy-pr-bot.yaml @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # Configuration file for `copy-pr-bot` GitHub App diff --git a/.github/workflows/ci-nightly.yml b/.github/workflows/ci-nightly.yml index 0abfdd626cb..75425ef0627 100644 --- a/.github/workflows/ci-nightly.yml +++ b/.github/workflows/ci-nightly.yml @@ -65,7 +65,7 @@ jobs: - name: Run CI tools unit tests run: | - python -m pip install pytest pyyaml==6.0.3 + python -m pip install pytest pyyaml==6.0.3 pathspec==0.12.1 # Standalone CI tool tests; skip repo-root conftest.py (imports cuda.pathfinder). python -m pytest -v --noconftest ci/tools/tests diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 40c4636eb2a..b8efab0f610 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -58,10 +58,17 @@ jobs: # PR workflows run from the merge revision; check out that same revision # so their helper scripts and configuration stay in sync. + # check_spdx.py runs `git log -1 -- ` to find the year each file last + # changed (copyright-year enforcement), so it needs the full commit history. + # In a shallow clone every file looks like it was introduced by the tip + # commit, so each would be judged against the tip commit's year. + # blob:none keeps all commits and trees but skips historical file contents, + # which a path-limited `git log` does not read. - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - fetch-depth: 1 + fetch-depth: 0 + filter: blob:none persist-credentials: false - name: Set up Python diff --git a/ci/tools/guess_latest.sh b/ci/tools/guess_latest.sh index 8a0a13034ca..f9d383f2094 100644 --- a/ci/tools/guess_latest.sh +++ b/ci/tools/guess_latest.sh @@ -1,5 +1,5 @@ #!/bin/bash -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 diff --git a/ci/tools/tests/test_check_spdx.py b/ci/tools/tests/test_check_spdx.py new file mode 100644 index 00000000000..b5e7afd6fa8 --- /dev/null +++ b/ci/tools/tests/test_check_spdx.py @@ -0,0 +1,195 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Tests for the copyright-year rule in toolshed/check_spdx.py. + +The rule must give the same answer however the hook is invoked (``git commit``, +``pre-commit run --all-files``, CI), so each test builds a throwaway git repository +with controlled commit dates and runs the script against it. Years are derived from +the current year so the tests do not expire. + + python -m pytest ci/tools/tests/test_check_spdx.py -v --noconftest +""" + +from __future__ import annotations + +import datetime +import os +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +pytest.importorskip("pathspec", reason="check_spdx.py requires pathspec") + +REPO = Path(__file__).resolve().parents[3] +CHECKER = REPO / "toolshed" / "check_spdx.py" +LICENSE = REPO / "LICENSE" + +CURRENT_YEAR = datetime.datetime.now(tz=datetime.UTC).year +SAMPLE = "toolshed/sample.py" + +CONTENT = ( + "# SPDX-FileCopyrightText: Copyright (c) {years} NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n" + "# SPDX-License-Identifier: Apache-2.0\n" + "\n" + "pass\n" +) + + +def git(repo, *args, year=None): + env = dict(os.environ) + env.update( + GIT_AUTHOR_NAME="Test", + GIT_AUTHOR_EMAIL="test@example.com", + GIT_COMMITTER_NAME="Test", + GIT_COMMITTER_EMAIL="test@example.com", + ) + if year is not None: + date = f"{year}-06-15T12:00:00+0000" + env.update(GIT_AUTHOR_DATE=date, GIT_COMMITTER_DATE=date) + subprocess.run( # noqa: S603 + ["git", "-c", "commit.gpgsign=false", *args], # noqa: S607 + cwd=repo, + env=env, + check=True, + capture_output=True, + ) + + +def commit(repo, relpath, years, *, year): + path = repo / relpath + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(CONTENT.format(years=years), encoding="utf-8") + git(repo, "add", relpath) + git(repo, "commit", "-m", f"add {relpath}", year=year) + return path + + +def run_checker(repo, *args): + # The script reads LICENSE and runs git relative to the working directory, + # as pre-commit does from the repository root. + return subprocess.run( # noqa: S603 + [sys.executable, str(CHECKER), *args, SAMPLE], + cwd=repo, + capture_output=True, + text=True, + check=False, + ) + + +@pytest.fixture +def repo(tmp_path): + git(tmp_path, "init") + shutil.copy(LICENSE, tmp_path / "LICENSE") + git(tmp_path, "add", "LICENSE") + git(tmp_path, "commit", "-m", "init", year=CURRENT_YEAR - 5) + return tmp_path + + +@pytest.mark.agent_authored(model="claude-sonnet-5.5") +@pytest.mark.parametrize( + ("commit_age", "copyright_age", "passes"), + [ + # Not modified this year, so last year's copyright is still correct. + pytest.param(1, 1, True, id="untouched-since-last-year"), + # The bug in #2273: committed this year, nothing staged, stale copyright. + pytest.param(0, 1, False, id="committed-this-year-stale-copyright"), + ], +) +def test_committed_file_is_checked_against_its_last_commit_year(repo, commit_age, copyright_age, passes): + commit(repo, SAMPLE, str(CURRENT_YEAR - copyright_age), year=CURRENT_YEAR - commit_age) + + result = run_checker(repo) + + assert (result.returncode == 0) is passes, result.stdout + result.stderr + assert ("OUTDATED" in result.stdout) is (not passes) + + +@pytest.mark.agent_authored(model="claude-sonnet-5.5") +def test_staged_edit_requires_current_year_and_fix_writes_it(repo): + # The `git commit` path: the edited file is staged when the hook runs. + path = commit(repo, SAMPLE, str(CURRENT_YEAR - 3), year=CURRENT_YEAR - 2) + path.write_text(path.read_text(encoding="utf-8") + "# edited\n", encoding="utf-8") + git(repo, "add", SAMPLE) + + check = run_checker(repo) + assert check.returncode == 1, check.stdout + check.stderr + assert f"(expected '{CURRENT_YEAR}')" in check.stdout + + # --fix reports that it changed something, as the hook always has. + fix = run_checker(repo, "--fix") + assert fix.returncode == 1, fix.stdout + fix.stderr + assert f"Copyright (c) {CURRENT_YEAR - 3}-{CURRENT_YEAR} " in path.read_text(encoding="utf-8") + + # The rewrite is stable: a second run accepts the file. + assert run_checker(repo).returncode == 0 + + +@pytest.mark.agent_authored(model="claude-sonnet-5.5") +def test_fix_of_clean_stale_file_survives_being_committed(repo): + # Stale relative to an old commit, with nothing staged (for example a manual + # `pre-commit run --all-files`). + path = commit(repo, SAMPLE, str(CURRENT_YEAR - 3), year=CURRENT_YEAR - 2) + + fix = run_checker(repo, "--fix") + assert fix.returncode == 1, fix.stdout + fix.stderr + # Not the older last-commit year: committing the fix is a change made this year. + assert f"Copyright (c) {CURRENT_YEAR - 3}-{CURRENT_YEAR} " in path.read_text(encoding="utf-8") + + git(repo, "add", SAMPLE) + git(repo, "commit", "-m", "fix copyright", year=CURRENT_YEAR) + + # The fix must not demand another fix once it is part of this year's history. + assert run_checker(repo).returncode == 0 + + +@pytest.mark.parametrize( + ("date", "passes"), + [ + pytest.param(f"{CURRENT_YEAR - 1}-12-31T23:30:00-0800", False, id="utc-year-after-recorded-year"), + pytest.param(f"{CURRENT_YEAR}-01-01T00:30:00+1400", True, id="utc-year-before-recorded-year"), + ], +) +@pytest.mark.agent_authored(model="gpt-6") +def test_committed_file_uses_utc_year_across_new_year(repo, monkeypatch, date, passes): + monkeypatch.setenv("GIT_AUTHOR_DATE", date) + monkeypatch.setenv("GIT_COMMITTER_DATE", date) + commit(repo, SAMPLE, str(CURRENT_YEAR - 1), year=None) + + result = run_checker(repo) + + assert (result.returncode == 0) is passes, result.stdout + result.stderr + assert ("OUTDATED" in result.stdout) is (not passes) + + +@pytest.mark.agent_authored(model="gpt-6") +def test_signed_last_commit_with_show_signature_enabled(repo): + ssh_keygen = shutil.which("ssh-keygen") + if ssh_keygen is None: + pytest.skip("ssh-keygen is required to create a signed commit") + + signing_key = repo / "signing_key" + subprocess.run( # noqa: S603 + [ssh_keygen, "-q", "-t", "ed25519", "-N", "", "-f", str(signing_key)], + check=True, + capture_output=True, + ) + allowed_signers = repo / "allowed_signers" + allowed_signers.write_text( + "test@example.com " + signing_key.with_suffix(".pub").read_text(encoding="utf-8"), encoding="utf-8" + ) + git(repo, "config", "gpg.format", "ssh") + git(repo, "config", "user.signingkey", str(signing_key)) + git(repo, "config", "gpg.ssh.allowedSignersFile", str(allowed_signers)) + git(repo, "config", "log.showSignature", "true") + path = commit(repo, SAMPLE, str(CURRENT_YEAR), year=CURRENT_YEAR) + path.write_text(path.read_text(encoding="utf-8") + "# signed edit\n", encoding="utf-8") + git(repo, "add", SAMPLE) + git(repo, "-c", "commit.gpgsign=true", "commit", "-m", "signed edit", year=CURRENT_YEAR) + + result = run_checker(repo) + + assert result.returncode == 0, result.stdout + result.stderr diff --git a/cuda_core/DESCRIPTION.rst b/cuda_core/DESCRIPTION.rst index 0d725a2520a..9759aed8731 100644 --- a/cuda_core/DESCRIPTION.rst +++ b/cuda_core/DESCRIPTION.rst @@ -1,4 +1,4 @@ -.. SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. SPDX-License-Identifier: Apache-2.0 ******************************************************* diff --git a/cuda_core/cuda/core/_memory/_graph_memory_resource.pxd b/cuda_core/cuda/core/_memory/_graph_memory_resource.pxd index 492aa23cd3d..d28c638c04f 100644 --- a/cuda_core/cuda/core/_memory/_graph_memory_resource.pxd +++ b/cuda_core/cuda/core/_memory/_graph_memory_resource.pxd @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 diff --git a/cuda_core/cuda/core/_utils/__init__.py b/cuda_core/cuda/core/_utils/__init__.py index 79599c77db0..644dbb92e75 100644 --- a/cuda_core/cuda/core/_utils/__init__.py +++ b/cuda_core/cuda/core/_utils/__init__.py @@ -1,3 +1,3 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 diff --git a/cuda_core/docs/source/_templates/autosummary/cyclass.rst b/cuda_core/docs/source/_templates/autosummary/cyclass.rst index 81d045a6b0a..99b74ca0658 100644 --- a/cuda_core/docs/source/_templates/autosummary/cyclass.rst +++ b/cuda_core/docs/source/_templates/autosummary/cyclass.rst @@ -1,4 +1,4 @@ -.. SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. SPDX-License-Identifier: Apache-2.0 {{ fullname | escape | underline}} diff --git a/cuda_core/docs/source/release.rst b/cuda_core/docs/source/release.rst index f7c9f19c776..b8726b4d661 100644 --- a/cuda_core/docs/source/release.rst +++ b/cuda_core/docs/source/release.rst @@ -1,4 +1,4 @@ -.. SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. SPDX-License-Identifier: Apache-2.0 Release Notes diff --git a/cuda_core/docs/source/release/0.3.0-notes.rst b/cuda_core/docs/source/release/0.3.0-notes.rst index d59f912743d..057664a56ff 100644 --- a/cuda_core/docs/source/release/0.3.0-notes.rst +++ b/cuda_core/docs/source/release/0.3.0-notes.rst @@ -1,4 +1,4 @@ -.. SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. SPDX-License-Identifier: Apache-2.0 .. currentmodule:: cuda.core.experimental diff --git a/cuda_core/docs/source/release/0.4.0-notes.rst b/cuda_core/docs/source/release/0.4.0-notes.rst index cbe3d432400..f7d69dc5d58 100644 --- a/cuda_core/docs/source/release/0.4.0-notes.rst +++ b/cuda_core/docs/source/release/0.4.0-notes.rst @@ -1,4 +1,4 @@ -.. SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. SPDX-License-Identifier: Apache-2.0 .. currentmodule:: cuda.core.experimental diff --git a/cuda_core/docs/source/release/0.5.1-notes.rst b/cuda_core/docs/source/release/0.5.1-notes.rst index a52b88d1380..538b838a3ca 100644 --- a/cuda_core/docs/source/release/0.5.1-notes.rst +++ b/cuda_core/docs/source/release/0.5.1-notes.rst @@ -1,4 +1,4 @@ -.. SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. .. SPDX-License-Identifier: Apache-2.0 .. currentmodule:: cuda.core diff --git a/cuda_core/tests/cython/build_tests.sh b/cuda_core/tests/cython/build_tests.sh index 7ee65c50d68..72403baf571 100755 --- a/cuda_core/tests/cython/build_tests.sh +++ b/cuda_core/tests/cython/build_tests.sh @@ -1,7 +1,7 @@ #!/bin/bash set -eo pipefail -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 UNAME=$(uname) diff --git a/cuda_core/tests/helpers/layout.py b/cuda_core/tests/helpers/layout.py index e4102f4d45a..3e2aa6d2241 100644 --- a/cuda_core/tests/helpers/layout.py +++ b/cuda_core/tests/helpers/layout.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # # SPDX-License-Identifier: Apache-2.0 diff --git a/cuda_core/tests/test_context.py b/cuda_core/tests/test_context.py index 66f7582ea7e..53182520e24 100644 --- a/cuda_core/tests/test_context.py +++ b/cuda_core/tests/test_context.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import pytest diff --git a/cuda_core/tests/test_memory.py b/cuda_core/tests/test_memory.py index a2afff99469..7eb92608d1c 100644 --- a/cuda_core/tests/test_memory.py +++ b/cuda_core/tests/test_memory.py @@ -2946,6 +2946,7 @@ def test_mempool_watermark_reset(memory_resource_factory, kind): mr.close() +@pytest.mark.thread_unsafe(reason="CUDA legacy-default-stream mempool teardown/sync race (gh-3350)") @pytest.mark.agent_authored(model="claude-opus-5-5") def test_mempool_trim(memory_resource_factory): device = Device() diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py index 12198ac9f7f..b8c3267d541 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/env_vars.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 """Centralized CUDA environment variable handling. diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/find_sub_dirs.py b/cuda_pathfinder/cuda/pathfinder/_utils/find_sub_dirs.py index ebb7b13f488..55705ba6393 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/find_sub_dirs.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/find_sub_dirs.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import functools diff --git a/cuda_pathfinder/cuda/pathfinder/_utils/platform_aware.py b/cuda_pathfinder/cuda/pathfinder/_utils/platform_aware.py index af0610a6cdb..be55b6ff9c3 100644 --- a/cuda_pathfinder/cuda/pathfinder/_utils/platform_aware.py +++ b/cuda_pathfinder/cuda/pathfinder/_utils/platform_aware.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import sys diff --git a/cuda_pathfinder/tests/conftest.py b/cuda_pathfinder/tests/conftest.py index e8a5e11b391..d88e22b1959 100644 --- a/cuda_pathfinder/tests/conftest.py +++ b/cuda_pathfinder/tests/conftest.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 diff --git a/cuda_pathfinder/tests/test_utils_env_vars.py b/cuda_pathfinder/tests/test_utils_env_vars.py index 99a55324fc0..e675f243b4a 100644 --- a/cuda_pathfinder/tests/test_utils_env_vars.py +++ b/cuda_pathfinder/tests/test_utils_env_vars.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 import os diff --git a/cuda_pathfinder/tests/test_utils_find_sub_dirs.py b/cuda_pathfinder/tests/test_utils_find_sub_dirs.py index 56dab23dc42..7714e898649 100644 --- a/cuda_pathfinder/tests/test_utils_find_sub_dirs.py +++ b/cuda_pathfinder/tests/test_utils_find_sub_dirs.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 from pathlib import Path diff --git a/toolshed/check_spdx.py b/toolshed/check_spdx.py index 1be8a565db7..03733ba533f 100644 --- a/toolshed/check_spdx.py +++ b/toolshed/check_spdx.py @@ -71,14 +71,24 @@ def load_spdx_ignore(): CURRENT_YEAR = str(datetime.datetime.now(tz=datetime.UTC).year) -def is_staged(filepath): - # If the file is staged, we need to update it to the current year - process = subprocess.run( # noqa: S603 - ["git", "diff", "--staged", "--", filepath], # noqa: S607 +def last_modified_year(filepath): + # Staged or unstaged working-tree changes mean the file is being modified now. + status = subprocess.run( # noqa: S603 + ["git", "status", "--porcelain", "--", filepath], # noqa: S607 capture_output=True, text=True, ) - return process.stdout.strip() != "" + if status.stdout.strip(): + return int(CURRENT_YEAR) + # Use the last commit's UTC year, matching the year for working-tree changes. + log = subprocess.run( # noqa: S603 + ["git", "log", "-1", "--no-show-signature", "--format=%ct", "--", filepath], # noqa: S607 + capture_output=True, + text=True, + ) + timestamp = log.stdout.strip() + # Fall back to current year for brand-new untracked files with no git history. + return datetime.datetime.fromtimestamp(int(timestamp), tz=datetime.UTC).year if timestamp else int(CURRENT_YEAR) def normalize_repo_path(filepath): @@ -165,13 +175,15 @@ def validate_or_fix_copyright(filepath, blob, fix): else: start_year = end_year = years - if not is_staged(filepath) or int(end_year) >= int(CURRENT_YEAR): + if int(end_year) >= last_modified_year(filepath): return True, blob print(f"OUTDATED copyright {years!r} (expected {CURRENT_YEAR!r}) in {filepath!r}") if not fix: return False, blob + # Always write the current year: committing the fix is itself a change made + # this year, so an older year would be stale again immediately. new_years = f"{start_year}-{CURRENT_YEAR}" return ( False,