Skip to main content

Move or rename Markdown files and update the relative links that point to and from them, plus an audit command for broken internal links.

Project description

docmv

CI PyPI Python versions License: MIT

Move or rename Markdown files and update the relative links that point to and from them.

docmv is a command-line tool for moving and renaming Markdown files without breaking relative links. When you move a file or a folder, it updates links in two places: links elsewhere that point at the moved file, and links inside the moved file that point back out. It also has an audit command that reports broken internal links.

Every command can emit JSON (--json) for use in scripts. apply refuses to run when a move is ambiguous or would break a link, rather than guessing, and the explain command shows how a given link resolves without changing anything.

Why

Editors like VS Code, JetBrains, and Obsidian update Markdown links on move, but only when they perform the move. The moment a file is moved by git mv, a script, or an automated agent, those updates never fire and links rot. The usual fallback is a manual git grep for the old path, which misses the links inside the moved file entirely. docmv closes that gap for command-line and automated workflows.

Install

# from PyPI (recommended)
uv tool install docmv      # or: pipx install docmv

# from a local clone
uv tool install .          # or: pipx install .

# run without installing
uv run docmv --help

Pure standard library, no runtime dependencies. Requires Python 3.10+.

Usage

docmv audit [paths...]          # read-only: every internal link must resolve
docmv plan  SRC DST             # dry-run: show the move + link edits, write nothing
docmv apply SRC DST             # perform the move + edits (refuses on ambiguity)
docmv explain FILE[:LINE]       # show how each link in FILE resolves

SRC/DST may be files or directories. Add --json to any command for stable, machine-readable output.

apply moves files on disk and rewrites links, then leaves staging and committing to you (git detects the rename in the resulting diff). Pass --stage to also stage the move with git mv, or --no-stage to force a plain move.

To make staging the default without typing the flag each time, set it in git config (a repo setting overrides the global one), or via an env var for one-offs:

git config --global docmv.stage true   # stage in every repo
git config docmv.stage true            # stage in this repo only
DOCMV_STAGE=1 docmv apply SRC DST      # stage for a single invocation

Precedence is --stage/--no-stage > DOCMV_STAGE > git config docmv.stage > off. docmv apply --help documents this too.

Examples

# What would break / change if I moved this doc into a new folder?
docmv plan docs/architecture/loot-tables.md docs/architecture/loot/loot-tables.md

# Do it (only if the working tree is clean and nothing would break)
docmv apply docs/architecture/loot-tables.md docs/architecture/loot/loot-tables.md

# Move an entire folder
docmv apply docs/meta docs/meta/docs-and-rules

# CI link check
docmv audit            # exits non-zero if any internal link is broken

How it stays correct

  • Matches by resolved absolute target, not by raw string. ../x.md means different files from different folders, so resolving first is what makes cross-folder and folder moves correct.
  • One recompute pass handles inbound, outbound, intra-folder, and cross-depth links the same way: for every link, recompute the relative path from its post-move container to its post-move target.
  • Code-aware, CommonMark-faithful parsing. Links inside fenced blocks, inline code spans, HTML comments, raw HTML tags, and autolinks are treated as opaque, so the example links that fill docs-about-docs are never rewritten or falsely flagged. The destination, title, and label scanners are ported from markdown-it-py and tested against the full CommonMark 0.30 spec corpus with zero false positives (see Limitations).
  • Display-path detection. When the visible link text mirrors the path (e.g. [../meta/x.md](../meta/x.md)), the text is rewritten too, but only on an exact match. Fuzzy matches are surfaced for review rather than auto-applied.
  • Fragments and queries preserved. ./b.md#section keeps its #section.
  • Post-move verification. After planning, a simulation confirms no link is left dangling, and separates breakage the move would introduce (a bug, shown loudly) from pre-existing rot.

Agent safety

  • apply refuses and changes nothing if there are blockers (destination exists, dirty working tree), if the move would introduce any broken link, or if low-confidence review items exist (override with --allow-low-confidence).
  • All output is available as --json with a flat, stable schema.
  • Exit codes: 0 ok, 1 audit found broken links, 2 apply was blocked, 3 usage error.
  • explain is read-only and shows the base dir, normalized path, and existence for each link, so an agent or human can verify a flagged link instead of trusting a bare verdict.

Scope and safety

docmv only writes Markdown files. It never edits source code.

  • The only files whose contents change are .md and .markdown. Code files (.py, .ts, and so on) are touched only when you name one as the SRC or DST of a move, and then they're relocated, not content-edited.
  • One consequence: doc references embedded inside code, like a path in a Python docstring or comment, are not auto-updated. That's the safe side of the trade; docmv would rather miss a code-embedded reference than risk corrupting code.
  • Scanning code isn't reliable here anyway: a doc-path string in code is just data, and there's no way to know whether it points at this repo's own docs or somewhere else. Any future code scanning would be opt-in and review-only.

Limitations

docmv's scanner is a small, line-oriented stdlib tokenizer, not a full block-level CommonMark parser. It's validated against the official CommonMark 0.30 spec corpus using markdown-it-py as a reference oracle. The trade-off is deliberately asymmetric:

  • Zero false positives (enforced). Every destination docmv extracts is one the reference parser also recognises. It won't mistake code, an escaped bracket, an autolink, or raw HTML for a rewritable link, so it can't corrupt a file by rewriting a non-link.
  • A few safe false negatives (tolerated). A handful of valid-but-rare constructs aren't rewritten. A missed link is surfaced by audit rather than corrupted. These are:
    • multi-line reference definitions, and reference definitions nested inside blockquotes or lists;
    • inline links whose destination or title wraps across lines;
    • nested links and images: a clickable badge like [![logo](logo.png)](/url) updates the outer /url but not the inner logo.png;
    • email autolinks (<me@example.com>), which are external and never rewritten.
  • Heading-anchor (#fragment) checks use an approximate GitHub slug algorithm and are reported as best-effort warnings, not hard failures.
  • Setext headings (===/---) are not collected for anchor checks.

Development

uv run --extra dev pytest

If you change the parser or link-resolution logic, please add adversarial test cases, not just the happy path.

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

docmv-0.1.0.tar.gz (63.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

docmv-0.1.0-py3-none-any.whl (28.8 kB view details)

Uploaded Python 3

File details

Details for the file docmv-0.1.0.tar.gz.

File metadata

  • Download URL: docmv-0.1.0.tar.gz
  • Upload date:
  • Size: 63.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.8

File hashes

Hashes for docmv-0.1.0.tar.gz
Algorithm Hash digest
SHA256 502d09b1fdd9b51c4fe9b47e46f9053c32a45c96fc1d2726ce510aea32af53a1
MD5 a23f044f9cf19432b5240efa4dee524a
BLAKE2b-256 c357ead8bf2c666a5818d0a1fc8e6bf2dde34472b23493a05e83b3baacaba878

See more details on using hashes here.

File details

Details for the file docmv-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: docmv-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 28.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.8

File hashes

Hashes for docmv-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 470b1e7a814e87a92b6e3b879cfe133d38a9a6060612b757d23c1cb1c5c965f0
MD5 470c684cd0308301d878e954c652264a
BLAKE2b-256 fb116465ec73931e86268015e1dc06d19b4c29d158d1284c62fdb3f1f144a10b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page