magit-status-mode

magit-status: the primary magit workhorse — staged / unstaged / untracked sections, stashes, recent commits, lazy inline diffs (via = for files, <CR> for stashes), a dedicated per-file diff buffer (dd), line-, hunk- and file-level staging (s/u/x), commit (cc/ca), and context-aware visit (<CR>, which opens the commit buffer for a commit entry and, inside a diff, the right version of the file at the right line and column).

On this page

The *magit:status:<repo>* buffer is the primary workhorse — a section- collapsible view of your repository's current state. It shows every changed file organised into sections, lets you stage and unstage a whole file, a single hunk, or just the lines you select, commit, amend, open diffs on demand, and navigate between sections, files, and hunks.

Open it with C-x g or :magit-status from any buffer.

Status: status buffer rendering, all five sections, lazy inline diffs via = (files) and <CR> (stashes), a dedicated per-file diff buffer via dd (opens against the file's own section baseline — --cached for Staged, working-tree-vs-index for Unstaged — useful for diffs too large to read comfortably inline), staging (s/u/x — on the lines you select, else the hunk under the cursor, else the whole file), commit (cc/ca), context-aware visit (<CR>, opens the dedicated commit buffer for a commit entry), manual refresh (gr), and close (q) are shipped. TAB genuinely folds: closing a file's fold hides its inline diff, and each @@ hunk inside an expanded diff is independently foldable and nested inside the file's fold.

Headerline (MG.14). A sticky row above the first line shows the repository name, the checked-out branch with upstream tracking, and the dirty counts — lattice main ↑2 ↓1 3 staged 5 unstaged, or lattice main clean when there is nothing to commit. It refreshes with the buffer (gr and every staging action), and every other magit buffer carries the equivalent row for its own view. (An earlier revision of this page claimed this headerline was already active when it was not; MG.14 made the claim true.)


Quick reference

ChordAction
sStage the hunk or file at cursor
uUnstage the hunk or file at cursor
xDiscard the hunk or file at cursor (asks for confirmation first)
=Toggle inline diff for the file at cursor
ddOpen the file at cursor's diff in a dedicated buffer (against the section's baseline)
ccOpen the commit buffer
caAmend the previous commit
pDisabled — shows an error (see Staging and unstaging)
<CR>Context-aware open/visit at cursor — open the file, toggle a stash's inline patch, open the commit buffer for a commit entry, or (inside a diff) open that version of the file on that line
grManual refresh (re-runs git status)
qClose the buffer (bury, return to previous)
]] / [[Next / previous top-level section
]f / [fNext / previous file or entry within the current section
]c / [cNext / previous hunk (within expanded diffs)
s / u in Visual over file rowsStage / unstage every file the selection covers
s / u / x in Visual inside a diffAct on the selected lines only
A / _Cherry-pick / revert the commit at cursor
Os / Om / OhReset --soft / --mixed / --hard to the commit at cursor
TABToggle section or hunk fold at cursor
S-TABCycle section visibility
dvOpen the file at cursor side-by-side against its baseline
DArguments — this buffer has none, so it says so

Sections

The status buffer is organised into five collapsible sections (top to bottom):

Staged changes

Files with changes in the index — what will go into the next commit. Listed with their status labels (modified, new file, deleted).

If = seems to do nothing, read the headerline. A status buffer is a snapshot: if the file's changes were committed, staged or reverted somewhere else since the scan, the row is still listed but git diff for it now comes back empty — so there is nothing to expand. The headerline says no changes in <path> — press gr to refresh, and gr both fixes the view and clears the notice. Before this the press was silent, which reads as a dead key rather than a stale buffer.

Diffs are not pre-computed — press = on a file entry to load its staged diff inline, or dd to open it in a dedicated buffer (git diff --cached) instead — better for a diff too large to read comfortably inline, since it doesn't inflate the status buffer's line count or its inline-highlight bookkeeping.

Staged changes (3)
  modified    src/main.rs
  new file    src/auth.rs
  deleted     src/old.rs

After pressing = on src/main.rs:

Staged changes (3)
  modified    src/main.rs
  ─────────────────────────
  + fn authenticate() {
  +     // new auth module
  + }
  ─────────────────────────
  new file    src/auth.rs
  deleted     src/old.rs

The diff is inserted as a local edit to the buffer — other sections and files are untouched. The diff content is styled text with virtual rows for deletion blocks; hunk boundaries are foldable. Press = again to collapse the diff.

Unstaged changes

Files with working-tree modifications not yet staged. Same format and behaviour as staged — file list with status labels, diffs loaded on demand via = or dd. dd here opens git diff (working tree vs index) — the Unstaged section's own baseline, distinct from Staged's --cached.

Untracked files

Files not tracked by git. Shown by default; hide this section with :set magit.status.show-untracked=false.

Stashes

The stash list (most recent first). Shown by default; hide with :set magit.status.show-stashes=false. <CR> toggles the stash's patch inline, at the cursor — the same mechanism = uses for files.

To act on one, use the dispatch menu's stash submenu with the cursor on the row: h z p pops it, h z a applies it, h z k drops it (asking first), h z v shows its patch. These read the stash under the cursor exactly as the stash list's own a/p/d do — the rows here are the same format — and fall back to a picker when the cursor is not on one. Full menu: magit-transient.

Unmerged into <upstream>

Commits this branch has that its upstream does not — everything you have not pushed yet. The header names the tracked ref, so it reads Unmerged into origin/main (34). Rows are the same <sha> <subject> shape as Recent commits, and <CR> opens the same commit buffer.

Shown only when there is something unpushed, and Recent commits is hidden while it is — the two are mutually exclusive. When everything is pushed the section disappears and Recent commits comes back. That is magit's behaviour (magit-insert-unpushed-to-upstream-or-recent): while you have unpushed work, "what have I not pushed" answers "what have I done lately" better, and showing both would list the same commits twice under two headings.

A branch with no upstream — or a detached HEAD — has no such section. That is an ordinary state, not an error.

Unlike Recent commits, this list is not capped: how much you have not pushed is a number worth having exactly right, and truncating it would make the header's own count a lie.

Recent commits

Last N commits (default 20) with abbreviated SHAs and subjects. <CR> opens the dedicated commit buffer for the commit at cursor — the same target :magit-log's own <CR> opens, and every other magit view that shows a per-row SHA (log, blame, rebase). This used to toggle the commit's patch inline (the same mechanism = uses for files) — changed for consistency with those other views, which all treat <CR> on a SHA as "go to the commit", not "preview inline".

The commit operations from magit-core-mode act on the row under the cursor here: A cherry-pick, _ revert, Os / Om / Oh reset --soft / --mixed / --hard. On a row that isn't a commit — a file entry, a stash — they ask which commit, opening a picker of recent commits rather than acting on a neighbour.


Staging and unstaging

File-level

When the cursor is on a file header (the modified path line):

ChordAction
sStage the entire file
uUnstage the entire file
xDiscard all working-tree changes to the file — asks for confirmation first (Discard changes to <path>?, y/n) before running git checkout --
x on an untracked entryDeletes the file. Asks Delete untracked file <path>? git has no copy to restore. before running git clean -f -d --

x on an untracked entry is a genuinely different act, and the prompt says so. There is no committed version to go back to, so "discard the changes" would mean nothing — the only thing available is removing the file, and unlike every other x in this buffer, git holds no copy to recover it from. (Until this was fixed, x there ran git checkout -- and failed with pathspec '<path>' did not match any file(s) known to git.)

Hunk-level

Expand a file's diff with =, put the cursor anywhere inside one hunk — a +, -, or context line, or the @@ header itself — and the same three chords act on that hunk alone:

ChordAction
sStage this hunk, leaving the file's other hunks unstaged
uUnstage this hunk (cursor in a Staged entry's diff)
xDiscard this hunk from the working tree — asks first (Discard hunk at <path>:<line>?)

]c / [c jump between hunks, and staging uses the same hunk boundaries they do, so ]c then s always stages the hunk you just landed on.

Each chord acts on the side it belongs to. s and x want an Unstaged hunk, u wants a Staged one; press the wrong one and magit says so ("that hunk isn't staged") rather than running a git command that would fail — or, for x on a staged hunk, one that would succeed and quietly remove the change from your file while leaving it staged for the next commit. Unstage it with u first, then discard.

Hunks inside a commit's or stash's expanded patch can't be staged — they belong to neither the index nor the working tree. s there reports that hunk staging isn't available in this view; move to the file header if you meant the whole file.

If the working tree has moved under the buffer since it was drawn, git refuses the patch outright rather than applying it somewhere plausible-looking. The failure is reported in *messages* and the view refreshes; press gr and try again.

The view keeps your place. After staging a hunk the file's diff is still open and the cursor is on the hunk that took the staged one's place — so staging four of a file's six hunks is four keypresses, not four keypresses and four searches. Staging the last one leaves you on the new last hunk; staging a file's only remaining hunk moves it to another section, and the cursor stays where the refresh put it rather than jumping somewhere arbitrary.

A selection of lines

Select the lines inside a hunk — V for linewise, extended with j / k — then press the same chord:

ChordAction
sStage only the selected lines
uUnstage only the selected lines
xDiscard only the selected lines — asks first, naming the count

This is the finest granularity magit offers, and the usual way to split one edit across two commits. The echo names what moved (magit: staged 3 lines of src/main.rs:42), and Visual mode ends, as it does after any operator on a selection.

Two things worth knowing:

  • One hunk at a time. The selection is intersected with the hunk your cursor is in; lines outside it are ignored. The echo's count is what actually moved, so a selection drawn across two hunks reads as the smaller number.
  • A selection with no +/- line in it does nothing, and says so — selecting only context lines is not a change to move.

Because of how git formats a diff, a modified line appears as a removal and an addition, usually with all the removals grouped above all the additions. Selecting one line's removal without its addition stages the deletion alone, which is valid and occasionally what you want; to move a whole modification, select both rows.

p (interactively stage via git add -p) is disabled outright: it shows magit: interactive git add -p isn't supported yet rather than attempting anything. git add -p is fundamentally interactive — it reads its own prompts from stdin — and there's no terminal-suspend mechanism yet to hand it a real TTY. Stage the whole file with s, or expand the diff with = to review before staging.

Staged + unstaged simultaneously

A file with changes in both the index AND the working tree shows as modified in both sections, as two independent rows. s/u/x on the staged row target the index; on the unstaged row they target the working tree. This matches git's two-staging-area model, at the file level.


Visual mode over several files

Select a run of file entries and press s or u — every file in the selection is staged or unstaged, in one go.

One git command, all-or-nothing. The whole selection goes to a single git add / git reset, not one invocation per file. That matters for more than speed: every git invocation takes .git/index.lock for its lifetime, so staging N files as N commands is N chances for a background refresh — or your own next keystroke — to collide with it and fail. It also makes the operation atomic. Either the selection stages or none of it does; there is no half-applied outcome to untangle, and correspondingly no "3 of 5 staged" report.

The same keys inside an expanded diff still act on the selected lines, which is the finer-grained thing you want there. Which one you get depends on what the selection covers, not on a different key.

An entry and its expanded diff are rows of the same file, so a selection spanning both stages it once. A file that is partly staged appears in two sections, and selecting across them is still one file.

x (discard) is deliberately not ranged yet: it is destructive and goes through a confirmation that names its target, and a confirmation that named several would need to carry the whole list to its execute half. Until it does, x acts on the file at the cursor.


The status buffer has a three-level hierarchy, each with its own chord family:

LevelWhat you navigateChords
SectionsTop-level headers (Staged, Unstaged, Untracked, Stashes, Unmerged into <upstream>, Recent commits)]] / [[
Files / entriesFile headers within the current section, stash entries, commit lines]f / [f
HunksIndividual diff hunks within an expanded file diff]c / [c

Hunk navigation only works for files whose diff is currently expanded (via =). If the cursor is on a file header (diff not expanded), ]c / [c falls back to file-level navigation (same as ]f / [f).


Committing

New commit (cc)

Press cc to open the commit buffer (*magit:commit:<repo>*). The commit buffer shows the staged diff as a read- only preview and provides an editable message region. C-c C-c creates the commit; C-c C-k aborts.

After committing, the buffer refreshes as part of the commit action — the staged section clears and the recent-commits section updates.

Amend (ca)

Press ca to amend the previous commit. The commit buffer opens with the previous message pre-populated. C-c C-c amends; C-c C-k aborts.


Context-aware visit (<CR>)

<CR> is a general "visit / drill-into" action. Its behaviour depends on what's under the cursor:

Cursor on<CR> action
File entry (staged / unstaged)Open the file for editing (working-tree version)
Untracked fileOpen the file for editing
Commit lineOpen the dedicated commit buffer for that commit
Stash entryToggle the stash's patch inline (same mechanism as =)
A diff content line (inside an expanded entry)Open that version of the file, at that line and column — see below

There are no branch entries in the status buffer, so there's no "check out a branch" case here — that's magit-branch's <CR>.

Inside a diff: the right version, the right position

Put the cursor on a line of code inside an expanded diff and <CR> opens the file you are looking at — but which version depends on which section the diff is in, because that is what the diff is against:

Section<CR> opens
Staged changesthe index copy — *magit:file:<repo>:staged:<path>*, what the diff's + side actually is
Unstaged changesthe working-tree file, editable
Recent commits (an expanded commit's patch)the file at that commit*magit:file:<repo>:<sha>:<path>*

It lands on the code under the cursor — the right line and the right place along it, so the caret comes down on the same token you were looking at rather than at the start of the line.

The @@ header declares where the new side starts, and counting the added and context rows above the cursor gives the line; - rows are skipped, since a removed line has no position in the file you are opening. The horizontal half is simpler: every diff row carries a one-character marker ( , +, -) in front of the code, so the caret shifts left by one to account for it. Parking on the marker itself lands you at the start of the line.

Two edges worth knowing. On a - row the caret lands at the same offset on whatever line now occupies that position — the removed text is not in the file, so there is nothing exact to aim at. And if that line is shorter than where you were, the caret stops at its end rather than hanging past it.

The two halves compose the way you would want them to: <CR> on a deleted function inside a commit's patch opens that commit's copy of the file, scrolled to the function. In Emacs magit this is the same <CR> and the same answer.

On a file entry row — the modified src/main.rs line rather than the diff below it — there is no line to compute, so the file opens at the top. That is not a fallback firing; a file entry is not inside a hunk, and magit opens those at the top too.


Section visibility cycling (S-TAB)

S-TAB cycles through four visibility states:

  1. All sections expanded (default on open)
  2. Only changed sections visible (Staged, Unstaged) — untracked / stashes / commits collapsed
  3. Only section headers visible — file-list content hidden
  4. All collapsed (only headers, no body content)

TAB toggles the fold at the cursor (innermost first: hunk → file → section).


Refreshing

Manual refresh (gr)

Press gr to re-run git status, git stash list, and git log and rebuild the buffer content.

The headerline says refreshing while it runs. A gr in a large repository is several git calls; the row shows that it is working rather than looking frozen, and the marker clears when the new content lands. Every staging action shows it too.

Expanded diffs stay expanded. A refresh rebuilds the buffer, and it rebuilds the diffs you had open along with it — re-running git diff for those entries only, so a refresh with nothing open costs nothing extra. An entry that has since left its section simply doesn't come back. Every staging action refreshes too, which is why staging one hunk leaves you looking at the rest of the file's diff rather than at a collapsed entry.

Auto-refresh — not built

Earlier revisions of this page described an automatic refresh driven by a repository watcher, controlled by magit.auto-refresh. None of it exists — there is no such option, no repository watcher, and no filesystem-event path into the status buffer. :set magit.auto-refresh fails with unknown option, as the Options section below says of every magit.* name.

What does refresh the buffer: gr, and every staging action, which refreshes as part of its own completion.


While a bisect is running

The headerline gains BISECTING 3 left, ~2 steps — git's own numbers, from git's own plumbing, so they agree with what git bisect prints in a terminal. It appears on a clean tree too, which is the normal case during a bisect since git checks out each candidate for you.

Start, mark and end a bisect from B on the dispatch transient; the menu offers start when none is running and good / bad / skip / reset when one is.


Edge cases

  • Not a git repository: the buffer shows Not a git repository.
  • Detached HEAD: the headerline shows HEAD as the branch (what git rev-parse --abbrev-ref HEAD reports), with no ahead/behind.
  • Bare repository: write operations (stage, commit, branch) are rejected with a user-visible message.
  • Empty repository (no commits yet): the staged / unstaged sections function normally; the recent-commits section is empty.
  • Binary files: shown in the file list but diffs are not expandable (no text diff to display).
  • Merge conflicts: files in the Unmerged state appear in both staged and unstaged sections with a conflicted label.

See also

  • magit-diff-mode — how the inline = diffs are coloured, and the theme elements that control it.
  • magit-core-mode — the shared navigation, refresh and fold chords.