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
51 changes: 39 additions & 12 deletions README.md

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions docs/changelog/v2/v2.2.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# @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](https://github.com/CLDMV/fix-headers/pull/128), closes [#127](https://github.com/CLDMV/fix-headers/issues/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](https://github.com/CLDMV/fix-headers/pull/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`.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@cldmv/fix-headers",
"version": "2.1.4",
"version": "2.2.0",
"description": "Multi-language project header normalizer with auto-detection and override support.",
"type": "module",
"main": "./dist/index.cjs",
Expand Down
4 changes: 2 additions & 2 deletions src/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* @Email: <Shinrai@users.noreply.github.com>
* -----
* @Last modified by: Nate Corcoran <CLDMV> (Shinrai@users.noreply.github.com)
* @Last modified time: 2026-10-02T12:28:11-07:00 (1790969291)
* @Last modified time: 2026-10-03T22:50:30-07:00 (1791093030)
* -----
* @Copyright: Copyright (c) 2013-2026 Catalyzed Motivation Inc. All rights reserved.
*
Expand All @@ -24,7 +24,7 @@ import fixHeaders from "./fix-header.mjs";
* @module fix-headers/cli
*/

const HELP_TEXT = `fix-headers CLI\n\nUsage:\n fix-headers [options]\n\nOptions:\n -h, --help Show help\n --dry-run Compute changes without writing files\n --check Validate header dates without writing; exit 1 on date drift\n --fix-created-date Move an existing @Date back to the oldest of git first commit / file creation\n --strict-created-date With --check, fail when @Date is later than git first commit / file creation\n --normalize-date-format Write every header date in the ISO 8601 T-form (git %aI)\n --timezone <name> Write new dates in an IANA time zone (e.g. America/Los_Angeles, UTC); the instant never changes\n --convert-timezone With --timezone, also rewrite existing @Date/@Last modified time values into that zone\n --json Print JSON output\n --verbose Print updated file paths in summary mode; with --sample-output or --diff, also print each file's field differences\n --sample-output Show previous/new header sample for changed files\n --diff Print a unified diff of each changed file's header (implies sample output)\n --force-author-update Always update @Author/@Email to detected/current values\n --force-last-modified-author-update Always update @Last modified by to detected/current values\n --use-gpg-signer-author Use signed-commit UID (%GS) for detected @Author\n --cwd <path> Working directory for project detection\n --input <path> File or folder to process instead of the whole project (repeatable)\n --include-folder <path> Include folder (repeatable)\n --include-folder-non-recursive <path> Include only a folder's own files, not its subfolders (repeatable)\n --exclude-folder <path> Exclude folder name/path (repeatable)\n --include-extension <ext> Include extension (repeatable)\n --enable-detector <id> Enable only specific detector (repeatable)\n --disable-detector <id> Disable detector by id (repeatable)\n --force-detector <id> Use a force-only detector, e.g. markdown (HTML-comment headers in .md files) (repeatable)\n --project-name <name> Override project name\n --language <id> Override language id\n --project-root <path> Override project root\n --marker <name|null> Override marker filename\n --author-name <name> Override author name\n --author-email <email> Override author email\n --company <name> Append company suffix to @Author (Name <Company>)\n --company-name <name> @Copyright holder (default: the project manifest's author; omitted when none)\n --copyright-start-year <year> Set the copyright start year (default: each file's @Date year)\n --spacing <n> Empty comment lines just inside the header's opening and closing (default: 1)\n --margin <n> Blank lines between the header and the file's next content (default: 2)\n --config <path> Load JSON options file; its 'extends' can pull in shared configs (URL, package or path)\n\nExamples:\n fix-headers --dry-run --include-folder src\n fix-headers --dry-run --diff --verbose\n fix-headers --check --verbose\n fix-headers --timezone America/Los_Angeles --convert-timezone\n fix-headers --project-name @scope/pkg --company-name "Catalyzed Motivation Inc."\n`;
const HELP_TEXT = `fix-headers CLI\n\nUsage:\n fix-headers [options]\n\nOptions:\n -h, --help Show help\n --dry-run Compute changes without writing files\n --check Validate header dates without writing; exit 1 on date drift\n --fix-created-date Move an existing @Date back to the oldest of git first commit / file creation\n --strict-created-date With --check, fail when @Date is later than git first commit / file creation\n --normalize-date-format Write every header date in the ISO 8601 T-form (git %aI)\n --timezone <name> Write new dates in an IANA time zone (e.g. America/Los_Angeles, UTC); the instant never changes\n --convert-timezone With --timezone, also rewrite existing @Date/@Last modified time values into that zone\n --json Print JSON output\n --verbose Print updated file paths in summary mode; with --sample-output or --diff, also print each file's field differences\n --sample-output Show previous/new header sample for changed files\n --diff Print a unified diff of each changed file's header (implies sample output)\n --force-author-update Replace an existing @Author/@Email with the detected/current values\n --force-last-modified-author-update Write the detected identity as @Last modified by on every file, edited or not\n --use-gpg-signer-author Use signed-commit UID (%GS) for detected @Author\n --cwd <path> Working directory for project detection\n --input <path> File or folder to process instead of the whole project (repeatable)\n --include-folder <path> Include folder (repeatable)\n --include-folder-non-recursive <path> Include only a folder's own files, not its subfolders (repeatable)\n --exclude-folder <path> Exclude folder name/path (repeatable)\n --include-extension <ext> Include extension (repeatable)\n --enable-detector <id> Enable only specific detector (repeatable)\n --disable-detector <id> Disable detector by id (repeatable)\n --force-detector <id> Use a force-only detector, e.g. markdown (HTML-comment headers in .md files) (repeatable)\n --project-name <name> Override project name\n --language <id> Override language id\n --project-root <path> Override project root\n --marker <name|null> Override marker filename\n --author-name <name> Override author name\n --author-email <email> Override author email\n --company <name> Append company suffix to @Author (Name <Company>)\n --company-name <name> @Copyright holder (default: the project manifest's author; omitted when none)\n --copyright-start-year <year> Set the copyright start year (default: each file's @Date year)\n --spacing <n> Empty comment lines just inside the header's opening and closing (default: 1)\n --margin <n> Blank lines between the header and the file's next content (default: 2)\n --config <path> Load JSON options file; its 'extends' can pull in shared configs (URL, package or path)\n\nExamples:\n fix-headers --dry-run --include-folder src\n fix-headers --dry-run --diff --verbose\n fix-headers --check --verbose\n fix-headers --timezone America/Los_Angeles --convert-timezone\n fix-headers --project-name @scope/pkg --company-name "Catalyzed Motivation Inc."\n`;

/**
* Converts CLI flag token to camelCase key.
Expand Down
Loading
Loading