feat: change @Last modified by only when the file's content was edited - #128
Merged
Merged
Conversation
@last modified by now records whoever last edited the file's content (everything outside the header), not whoever last ran fix-headers. The content counts as edited when the file's body, with the header taken out, differs from its body at git HEAD, or when HEAD does not have the file (new, untracked or ignored). Header rewrites fix-headers makes on its own (date format, epoch repair, fixCreatedDate, time zone conversion, frame, spacing, margin, @Project/@Filename/@copyright) still restamp @last modified time but keep the recorded editor. Outside a git work tree content edits cannot be detected and the recorded editor is kept. A content edit by the editor already recorded is restamped once when the recorded time is older than the file's last commit, so repeat edits move the time without making re-runs churn. @Author/@Email stay the original author unless forceAuthorUpdate is set. forceLastModifiedAuthorUpdate keeps its previous meaning (write the run identity on every file) and is documented as not needed for normal use. Closes #127
Shinrai
approved these changes
Oct 4, 2026
cldmv-bot Bot
added a commit
that referenced
this pull request
Oct 4, 2026
# @cldmv/fix-headers v2.2.0 Changelog **Release Date**: October 2026 **Release Type**: Minor **Branch**: `release/2.2.0` --- ## Overview Version 2.2.0 changes what `@Last modified by` means. It now names whoever last edited the file's content, the part of the file outside the header, instead of whoever last ran fix-headers. A run that only rewrites a header (a date format conversion, a corrected `@Project`, new spacing or margin) keeps the recorded editor, so running the tool never claims other people's files. `@Author` was already the original author and stays that way unless `forceAuthorUpdate` is set. No option was removed, the `fixHeaders` API and the header format are unchanged, and `@Last modified time` is still restamped whenever a header is rewritten. See the upgrade notes for the one behaviour change that can show up in an existing setup. --- ## ✨ Features ### `@Last modified by` follows content edits, not header rewrites ([#128](#128), closes [#127](#127)) Before, any run that changed a header could write the identity detected for that run into `@Last modified by`, and with `forceLastModifiedAuthorUpdate` it did so on every file. With a shared config that forces the author fields, whoever ran the tool became the last editor of every file it touched, including files they never opened. fix-headers now decides whether a file's content was edited by comparing it with the file at git `HEAD`. The header is taken out of both versions, along with the blank lines after it, and what is left is compared: | File | Content edited? | `@Author` | `@Last modified by` | `@Last modified time` | | ------------------------------------------------------------ | -------------------------- | -------------------------- | ------------------- | --------------------- | | Tracked, body the same as at `HEAD`, header rewritten | no | kept | kept | now | | Tracked, body differs from `HEAD` | yes | kept | run identity | now | | Not in `HEAD` (new, untracked or ignored, or no commits yet) | yes | kept (filled when missing) | run identity | now | | Outside a git work tree, header rewritten | cannot tell, treated as no | kept | kept | now | | Header already current, content not edited | no | unchanged | unchanged | unchanged | - **Header-only rewrites keep the recorded editor.** The date format (`normalizeDateFormat`), an epoch repair, `@Date` (`fixCreatedDate`), a time zone conversion, the frame, `spacing` and `margin`, and the `@Project`, `@Filename` and `@Copyright` values all rewrite the header without changing who last edited the file. `@Last modified time` still moves, because the header did. - **A content edit makes the run's identity the last editor.** That is the identity detected for the run (`authorName` / `authorEmail`, or git as described under `useGpgSignerAuthor`). A missing field is filled with it in every case, and a file with no header gets it in both fields. Adding a header to a committed file is not a content edit, so a later header-only run by someone else keeps whoever was recorded then. - **A repeat edit by the editor already recorded is stamped once.** When the body differs from `HEAD` and the recorded time is older than the file's last commit, the edit has not been stamped yet, so `@Last modified time` is restamped even though the editor is unchanged. Once stamped, the time is newer than the last commit and running fix-headers again changes nothing until the next commit. - **`forceLastModifiedAuthorUpdate` keeps its meaning** (write the run identity as `@Last modified by` on every file, edited or not), and `forceAuthorUpdate` still replaces an existing `@Author` / `@Email`. With content-edit detection, `forceLastModifiedAuthorUpdate` is no longer needed for normal use, and the README says so. The `--help` text for both force flags is reworded to match what they do. - **The `issues` result follows the same rule.** The `lastModifiedByName` / `lastModifiedByEmail` entries appear only when the value that would be written really differs from the existing header, which now depends on whether the content was edited. The check compares the working tree with `HEAD`, so run fix-headers before committing (a pre-commit hook, or `npm run fix:headers` before `git commit`). Content committed without a run is not detected later, because by then the body matches `HEAD`. The README gains an **Author and last modified** section with the same table and rules. ## 🧪 Tests - New suite `tests/last-modified-content-edit.test.vitest.mjs` covers header-only rewrites that keep the editor, body edits that take the run identity, `@Author` preserved for a different runner, `forceAuthorUpdate` still replacing it, untracked and new files, files outside a git work tree, and the one-time restamp of a repeat edit. `tests/core-edge.test.vitest.mjs` and `tests/sample-output.test.vitest.mjs` are updated for the new `issues` output. Coverage stays at 100%. ## 📚 Documentation - **NEW:** [docs/changelog/v2/v2.2.0.md](./v2.2.0.md): this changelog. - README: a new [Author and last modified](https://github.com/CLDMV/fix-headers/blob/master/README.md#-author-and-last-modified) section, a matching Key Features entry, and reworded `--force-author-update` / `--force-last-modified-author-update` CLI and API entries ([#128](#128)). ## 🔧 Dependencies _No dependency updates._ `ignore`, the only runtime dependency, is unchanged. --- ## Upgrade notes - No breaking changes: no option was removed and the API is unchanged. - **`@Last modified by` can differ from before after a run.** A header-only rewrite no longer writes the run's identity into it, and a content edit now does. A config that sets `forceLastModifiedAuthorUpdate` keeps the old behaviour (the run identity on every file) and can drop that option once its files carry the right editor. - **Run the tool before you commit.** The edit check reads the working tree against `HEAD`, so a hook or script that runs fix-headers after the commit will see an unchanged body and keep the recorded editor. - **Outside a git work tree nothing is detected as an edit**, so recorded editors are kept and only `@Last modified time` moves when a header is rewritten. - Preview the effect on your project with `--dry-run --diff --verbose`. <details> <summary>👥 Contributors</summary> - @Shinrai </details> --- <!-- coverage-start -->  | Metric | Coverage | |--------|----------| | Statements | 100.0% | | Branches | 100.0% | | Functions | 100.0% | | Lines | 100.0% | *Avg: **100.0%** · `e643b86` · Node lts/** <!-- coverage-end --> <!-- co-authors --> Co-authored-by: Shinrai <Shinrai@users.noreply.github.com>
cldmv-bot Bot
added a commit
to CLDMV/configs
that referenced
this pull request
Oct 4, 2026
# @cldmv/configs v1.2.4 Changelog **Release Date**: October 2026 **Release Type**: Patch **Branch**: `release/1.2.4` --- ## Overview The shared `fix-headers.json` stops forcing `@Last modified by`. Until now every fix-headers run in an extending repository stamped whoever ran it as the last modifier of every file it touched, even when the only change was a header rewrite such as a new date format. With `forceLastModifiedAuthorUpdate` set to `false` and fix-headers 2.2.0, `@Last modified by` now records the last person who actually edited a file's content, while `@Last modified time` still moves whenever fix-headers rewrites a header. Together with `forceAuthorUpdate: false`, which shipped in v1.2.3, the shared config no longer rewrites anyone's authorship: `@Author` keeps the file's creator, and `@Last modified by` follows real edits. Extending repositories should upgrade to `@cldmv/fix-headers` 2.2.0 or later alongside this release. --- ## 🐛 Bug Fixes ### `@Last modified by` follows real edits ([#20](#20)) `forceLastModifiedAuthorUpdate` was `true`, so every run rewrote `@Last modified by` to the identity running fix-headers (the name on their signing key, or `git config user.name` without one) on every file it changed. A contributor who ran `npm run fix:headers` therefore became the last modifier of every restamped file, even files they never opened ([#15](#15)). The option is now `false`. From fix-headers 2.2.0 ([CLDMV/fix-headers#128](CLDMV/fix-headers#128)) the field changes only when a file's content, everything outside the header, differs from the last commit, or when the file is new: | What changed in a file | `@Last modified time` | `@Last modified by` | `@Author` | | ------------------------------------------------------------------- | --------------------- | ------------------------------ | --------- | | Nothing | unchanged | unchanged | unchanged | | Only the header (date format, epoch, `@Date`, spacing, filename, …) | updated | unchanged | unchanged | | Content edited, or a new file | updated | the person running fix-headers | unchanged | On fix-headers releases before 2.2.0 the same setting keeps the existing `@Last modified by` on every run, real edits included, so the upgrade matters. ## 🔧 Dependencies - `@cldmv/fix-headers` `^2.1.4` → `^2.2.0` ([#20](#20)), dev-only. The repository's own header pass ran under 2.2.0 and changed no files. - `vitest` and `@vitest/coverage-v8` `^5.0.2` → `^5.0.3`, and `globals` `^17.12.0` → `^17.13.0` ([#21](#21)), dev-only. The coverage and globals bumps first merged as [#18](#18) and [#19](#19), two minutes after the v1.2.3 release, and were lost when `next` was reset after that release ([CLDMV/.github#360](CLDMV/.github#360)). #21 re-applies them, together with the vitest bump from [#17](#17). ## 📚 Documentation - **NEW:** [docs/changelog/v1/v1.2.4.md](./v1.2.4.md) — this changelog. - README: the `fix-headers.json` table and notes explain both author settings, and the requirements now name fix-headers 2.2.0 as the version the author rules need. - [v1.2.3](./v1.2.3.md) changelog corrected: that release also shipped `forceAuthorUpdate: false`, which its notes left out. --- ## Upgrade notes - **Upgrade `@cldmv/fix-headers` to 2.2.0 or later** in every repository that extends this config. On older fix-headers, `@Last modified by` would no longer update even when a file is edited. - The first fix-headers run after upgrading leaves existing `@Last modified by` values alone and only changes them on files whose content was edited since the last commit. No repository-wide restamp of authors. <details> <summary>👥 Contributors</summary> - @Shinrai </details> --- <!-- coverage-start -->  | Metric | Coverage | |--------|----------| | Statements | 100.0% | | Branches | 100.0% | | Functions | 100.0% | | Lines | 100.0% | *Avg: **100.0%** · `117287f` · Node lts/** <!-- coverage-end --> <!-- co-authors --> Co-authored-by: Shinrai <Shinrai@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🚀 What's Changed
💥 Breaking Changes
No breaking changes
✨ Features
🐛 Bug Fixes
No bug fixes
📦 Dependencies
No dependency updates
🔧 Other Changes
No other changes
👥 Contributors