Two identical walls of twelve toy bricks, before and after: the same red brick has left second place on the top row and landed at the end, with a dotted arrow tracing the move. Nothing added, nothing removed, one brick moved
Article · Moved code

Moved code is not churn – it is not an add/delete pair. It is a move! This matters to your code management.

4 September 2026 · MOV defined in-page · figures measured with CodeDelta 2.0.1 on FFmpeg, OpenCV and Spring Framework

A developer opens a Java file and sorts the import block. One import leaves line four and reappears at line six. Nothing about the program has changed: same classes, same calls, same bytecode. Ask git what happened and the answer is one deletion and one insertion. Ask a churn tool built on the same arithmetic and it says two statements of churn. As a mere record of the edit, the numbers are not inaccurate. As a measure of change they are wrong: nothing was written and nothing was lost, and churn is supposed to measure change. This is the oldest hole in churn measurement.

The oldest hole in the measurement

Take two snapshots of a codebase and classify every changed statement into three piles: added, deleted, or changed in place. Churn* is the sum. Measured on logical statements rather than physical lines it is a good instrument: a reformat scores zero and a one-statement fix scores one. But the three piles have no place for a statement that simply went somewhere else. A moved statement is a deletion where it was and an addition where it is now. It scores two.

* Churn, defined: CRN_LLOC = CHG_LLOC + DEL_LLOC + ADD_LLOC, counted over logical statements (LLOC) rather than physical lines. The two-pass differ and the statement rules are in the churn measurement paper; why statements and not lines is the LLOC paper; the short form is on the code churn page.

Two is not a rounding error, because of what churn is used for. Churn is a risk signal: the files with the most of it get the review time, the merge gate and the auditor’s attention. New code earns that attention. A block that moved from the bottom of a file to the top is the same text it was yesterday. It compiled yesterday. Its tests passed yesterday. Counting it as delete-plus-add hands it the risk profile of a rewrite, and the more thoroughly a developer tidies, the worse the number looks. An instrument that punishes tidying will, in time, get less of it.

It also poisons a ratio that matters. REP_CHURN§ is the share of churn that was replacement rather than repair, and it is the clearest process signature CodeDelta has measured for AI-assisted code: agents delete and regenerate, humans edit in place. Every moved statement counted as a delete and an add lands on the replacement side of that ratio. A reorganising human starts to look like a regenerating machine.

§ REP_CHURN = (ADD + DEL) ÷ CRN; its complement REWORK = CHG ÷ CRN is the share edited in place. The agent-built measurement is paper C; the human baselines across 136 codebases are paper D.

The rule

CodeDelta 2.0 counts a move as a move.

The rule† is narrow on purpose. A statement deleted at one position whose identical text is added at another position in the same file is recorded once, as a move, and not as a delete plus an add. It has to be at least twelve characters once whitespace is stripped, and it has to be unique on both sides. Those two conditions exist because i++; and return; appear everywhere, and a pairing rule that matched them would invent moves out of coincidence. What survives the filter is a statement distinctive enough that its reappearance somewhere else is not chance.

† MOV_LLOC, defined: a statement deleted at one position whose identical text (at least 12 characters, whitespace stripped, unique on both sides) is added at another position in the same file, recorded once. A move is not churn: CRN_LLOC = CHG + DEL + ADD, and MOV_LLOC is reported beside it, never inside it. MOV_SLOC is the distinct physical lines those moved statements occupy: it is derived from the statement pairing, never measured separately, and is likewise kept out of CRN_SLOC. A line holding several statements is one moved line, so MOV_SLOC can be smaller than MOV_LLOC as well as larger. Full definitions in the user guide.

Check it yourself

You can check the arithmetic in a minute. Put four assignments in a C file, then make a second copy with the first one moved to the end:

old/m.c                          new/m.c
int alpha_value_moved = 1000;    int b = 2;
int b = 2;                       int c = 3;
int c = 3;                       int d = 4;
int d = 4;                       int alpha_value_moved = 1000;

git diff --numstat reports one insertion and one deletion. CodeDelta reports MOV_LLOC 1 and CRN_LLOC 0: one statement moved, no churn. Swap two blocks of two statements and the same rule holds: MOV_LLOC 2, churn 0. These are known-answer cases the engine is tested against on every build, with the expected numbers written down before the run.

Git shows a deletion and an insertion. CodeDelta shows a move.

Everything so far has been about statements, and that is deliberate: LLOC is what counts, and the move rule is applied where the counting is done. Physical lines follow. MOV_SLOC is simply the distinct lines the moved statements sat on, and it is usually equal to MOV_LLOC, because most statements are one line long. When it is not equal, the gap is informative in both directions: a long statement moved gives more lines than statements, and a line packed with statements gives more statements than lines. Take a swap lifted from libpng as OpenCV ships it: a one-line debug call and a six-line array initialiser changed places.

old/p.c                                         new/p.c
png_debug(1, "in png_riffle_palette_neon");     uint8x16x4_t w = {{
                                                   vdupq_n_u8(0x00),
uint8x16x4_t w = {{                                vdupq_n_u8(0x00),
   vdupq_n_u8(0x00),                               vdupq_n_u8(0x00),
   vdupq_n_u8(0x00),                               vdupq_n_u8(0xff),
   vdupq_n_u8(0x00),                            }};
   vdupq_n_u8(0xff),
}};                                             png_debug(1, "in png_riffle_palette_neon");

Git takes the cheapest description in lines, credits the short side, and reports two lines deleted and two inserted. CodeDelta reads the swap the same way and reports MOV_LLOC 1, MOV_SLOC 1 and churn 0: one statement moved, one line, nothing written.

Now turn it round. Put twelve statements on one line and move that line past two others:

old/m.c
frame_count = 0; dropped_frames = 0; max_frames = 1000; ... retry_delay_ms = 250;
stride_bytes = 640;
keepalive_s = 60;

new/m.c
stride_bytes = 640;
keepalive_s = 60;
frame_count = 0; dropped_frames = 0; max_frames = 1000; ... retry_delay_ms = 250;

Git gets one thing right here: which line moved. It reports one deletion and one insertion, and as a record of the edit that is not inaccurate. As a measure of change it is blind. It has no idea that twelve statements were inside that line, and it has no category for a move at all. CodeDelta reports MOV_SLOC 1 and MOV_LLOC 12: one line moved, twelve statements moved, churn 0. One physical line, twelve logical lines, and both counts are true at once. That is why the statement figure is the one to read first, and why the line figure is reported beside it rather than instead of it.

At scale

At scale the effect is modest and real. Here it is across three release pairs:

Release pairChanged filesFiles with movesMoved statements (MOV_LLOC)Moved lines (MOV_SLOC)Churn (CRN_LLOC)If moves were delete + add
FFmpeg 6.0 → 7.02,56312874497077,758+1.9%
OpenCV 4.8.0 → 4.10.01,4268736440843,579+1.7%
Spring Framework 6.0.0 → 6.1.03,25716261365935,393+3.5%

A couple of percent of total churn does not sound like much until you look at where it sits, because it is not spread evenly. In FFmpeg, 71 statements moved in one file and 47 in another; counted as delete plus add, those two files alone would have carried 236 statements of churn that never happened. In OpenCV’s bundled zlib, deflate.c had 53 moved statements against a churn of 122; the old arithmetic would have reported 228, nearly doubling the file’s number for code that was rearranged, not written. And the small cases are everywhere: an OpenJPEG header where typedef int OPJ_BOOL; left the top of the file to sit beside the other typedefs. Git shows a deletion and an insertion. CodeDelta shows a move.

The rule has edges, and they are documented. It pairs identical text only: a statement that was moved and edited on the way is counted as churn, because something did change. It pairs within a file only: a statement that left for another file is still a deletion here and an addition there, because a file’s numbers must never depend on what happened in a different file. The diff viewer draws those cross-file moves as connected pairs so you can see them, but the counters stay put. And moving code is not risk-free: order matters, and a block moved past the guard that used to protect it is a real change. The point is not that moves are safe. It is that they are a different thing from new code, and a measurement that cannot tell them apart cannot tell you either.

What git, GitHub and cloc report

Git has known about this for years. Since version 2.15, in October 2017, git diff --color-moved paints a line that left one place and arrived at another in its own colour, so a reviewer at a terminal can see a move for what it is. Ask the same git for numbers and nothing changes: --numstat, --shortstat and --stat report one deletion and one insertion for the four-line file whether or not the colouring is on, and the manual lists moved-line handling only among the colour options. The one thing git does count is a whole file that moved, which -M reports as a rename; a block that moved inside a file has no number anywhere. The display knows and the arithmetic does not, and it is the arithmetic that ends up in the dashboards.

GitHub is where most of those dashboards live, and it inherits the arithmetic without adding to it. A pull request carries additions, deletions and a count of files changed; each file in it carries additions, deletions and their sum. A commit carries the same. The Insights graphs are weekly additions and deletions for the whole repository, and per contributor additions, deletions and commits for the top hundred contributors, on repositories under ten thousand commits. The API behind them exposes nothing finer. There is no changed-in-place category, no statement count, no per-file history of churn without scripting it commit by commit, and no move: requests to bring --color-moved into the pull-request view have been open since December 2021 without an answer. A team that reads its churn off GitHub is reading git’s two numbers, summed by week or by author.

cloc, the free counter most people reach for, does have a comparison mode. cloc --diff takes two trees and reports, per language, the lines that are the same, modified, added and removed. Those are its four piles and there is no fifth. It runs a line diff over the physical lines with blanks stripped out and labels each line with whatever the diff says, so a moved line is a removed line and an added line, exactly as in git. With --git it will pair a renamed file so that a whole file that moved is not counted twice; a block that moved inside a file still is. The small files from this piece, through all three tools:

Changegitcloc --diffCodeDelta
One statement moved (four-line file)1 deleted, 1 inserted1 removed, 1 addedMOV_LLOC 1, churn 0
One-line call swapped with six-line initialiser2 deleted, 2 inserted1 removed, 1 addedMOV_LLOC 1, MOV_SLOC 1, churn 0
One line holding twelve statements, moved1 deleted, 1 inserted1 removed, 1 addedMOV_LLOC 12, MOV_SLOC 1, churn 0
One line re-indented, nothing else1 deleted, 1 inserted1 modifiedunchanged, churn 0

The last row is the LLOC argument again from the other side. A reformat is not a change to the program, and a tool that counts statements says so; a tool that counts lines cannot, and GitHub’s numbers are those lines. cloc’s reading of the swap is smaller than git’s only because it threw the blank line away first. Neither of them has a word for what actually happened.

Run it on your last release pair and read MOV_LLOC beside CRN_LLOC. Where the moves are large and the churn is small, someone reorganised. Where the churn is high and the moves are nil, the code is genuinely new, and that is where the attention belongs.

A move is a move. Count it once, and count it apart.

Q&A

Q: “I already have a diff tool and it gives me what’s changed between releases. I could easily write a script to give me total changes, additions and deletions — why should I buy CodeDelta?”

A: “CodeDelta is a ‘churn’ tool. It measures total changed SLOC‡ (CHG_SLOC) and changed LLOC¶ (CHG_LLOC) across the entire project (and much more) — no diff tool does that — plus CodeDelta shows you the diff as well!”

‡ SLOC = source lines of code: physical lines that are not blank and not comments. The six-line initialiser above is six SLOC; int b = 2; is one.

¶ LLOC = logical lines of code, that is, statements, however many physical lines each one occupies. The six-line initialiser is one LLOC; int b = 2; is one; a line holding a = 1; b = 2; is one SLOC and two LLOC.

See what your own codebase says

The move counters in this piece are CodeDelta’s MOV_LLOC and MOV_SLOC — reported beside statement-level churn on every scan of two snapshots of your own repository, on macOS, Linux and Windows, free to try. Download CodeDelta → or add it to every pull request.

  1. Churn definition, the two-pass (physical and logical) differ: Automated Source Code Churn Measurement. Why logical statements: Why LLOC is what really counts.
  2. REP_CHURN and REWORK: paper C. Human baselines (136 codebases, 393M statements): paper D.
  3. MOV_LLOC and MOV_SLOC definitions, the 12-character and uniqueness conditions, and the cross-file (XMOV) overlay: CodeDelta user guide, metrics reference and diff viewer sections.
  4. Release pairs measured with CodeDelta 2.0.1: FFmpeg 6.0 → 7.0, OpenCV 4.8.0 → 4.10.0, Spring Framework 6.0.0 → 6.1.0. Changed files are files present in both snapshots with any difference; percentages are 2 × moved statements over CRN_LLOC.
  5. MOV_SLOC on the same three pairs, same binary: FFmpeg 970 lines for 744 statements (86 of the 128 files with moves have MOV_SLOC equal to MOV_LLOC), OpenCV 408 for 364 (69 of 87), Spring 659 for 613 (141 of 162). No file in any pair has MOV_SLOC below MOV_LLOC.
  6. The four-line example: git diff --no-index --numstat old/m.c new/m.c prints 1 1; with --color-moved the display changes and the numbers do not. The six-line swap: --numstat prints 2 2 and --color-moved paints the debug call and its blank line; CodeDelta’s audit output pairs the same statement. The twelve-statement line: --numstat prints 1 1; CodeDelta’s audit output lists all twelve statements as moved. All checked on git 2.50.1.
  7. GitHub: pull-request, commit and compare responses carry additions, deletions and changed_files, with per-file additions, deletions, changes, status and patch (checked against git/git pull request 2392 and the v2.15.0 commit). The statistics API lists five endpoints: code_frequency (“a weekly aggregate of the number of additions and deletions pushed to a repository”), contributors (per week: additions, deletions, commits), and three commit-count series. The code frequency graph “displays the content additions and deletions for each week”; the contributors graph shows the top 100 contributors and needs fewer than 10,000 commits. Moved-code requests: discussion 8573 (9 December 2021) and discussion 9632 (4 January 2022, 86 votes), both without a staff answer at the time of writing.
  8. cloc 2.10 (github.com/AlDanial/cloc): --diff is documented as “Compute differences in code and comments between source file(s) of SET1 and SET2”; the implementation is Algorithm::Diff::sdiff over the two files’ non-blank lines, with each line labelled same, modified, added or removed. With --git, “a more sophisticated pairing algorithm which additionally attempts to pair renamed files is used.” Fixture results above are from cloc --diff old new on the three files.
  9. When git learned it: the sb/diff-color-move branch was merged on 27 August 2017 and is an ancestor of the v2.15.0 tag (30 October 2017) and not of v2.14.0. The option is documented under --color-moved in the git-diff manual: “Moved lines of code are colored differently.” Whole-file moves are detected by -M / --find-renames and are reported as renames.