I'm an amateur developer who is using AI to help me learn about software development. I'm working on understanding tig's internals and used Claude Code to help me write some documentation for src/status.c. It covers:
Would you be interested in retaining this kind of internal documentation? If so, I'm happy to submit a PR for your review or follow any additional contribution protocols you require.
status.c — Status View Implementation (click to expand)
Purpose
status.c implements tig's interactive status view — the equivalent of git status but navigable and actionable. It displays staged, unstaged, and untracked files, and lets users stage, unstage, revert, and inspect changes without leaving the TUI.
Why it exists:
The command-line git status is read-only output. To act on it, you run separate commands (git add, git checkout, git diff). The status view collapses this workflow: see status, navigate to a file, press a key to act. It's the hub for pre-commit workflow in tig.
Design choice:
Rather than parsing git status --porcelain, it runs three lower-level git commands directly:
git diff-index -z -C --cached HEAD — staged changes (with -C for copy detection)
git diff-files -z — unstaged changes
git ls-files -z --others --exclude-standard — untracked files
The -z flag produces NUL-separated output for safe filename handling. Using plumbing commands gives stable, parseable output and avoids git status's user-facing formatting.
Note: The staged diff command also includes --diff-filter=ACDMRTXB to exclude unmerged files (handled separately).
Key Components
Data Structures
struct status (defined in tig/status.h)
Represents a single file entry. Contains:
status — single char using git's standard status codes: M (modified), A (added), D (deleted), R (renamed), C (copied), U (unmerged), ? (untracked)
old — previous state (mode, revision, name)
new — current state (mode, revision, name)
status_onbranch (static global)
String holding the header line text: "On branch main", "Rebasing feature-x", "HEAD detached at v1.0", etc.
Core Functions
| Function |
Purpose |
status_open() |
Entry point. Runs git commands, populates view with staged/unstaged/untracked sections. |
status_run() |
Executes a git command, parses NUL-separated output, creates view lines. Called three times by status_open(). |
status_get_diff() |
Parses a single diff metadata line (:100644 100644 <sha> <sha> M) into a struct status. |
User Action Handlers
| Function |
Trigger |
Action |
status_request() |
Any keypress |
Dispatcher — routes to appropriate handler |
status_update() |
u key |
Stage or unstage file(s) |
status_update_file() |
(internal) |
Stage/unstage a single file via git update-index |
status_update_files() |
(internal) |
Batch stage/unstage all files in a section |
status_revert() |
! key |
Discard unstaged changes via git checkout |
status_enter() |
Enter |
Open stage view to show diff |
Display Functions
| Function |
Purpose |
status_get_column_data() |
Provides status char and filename (or section header text) to the column renderer |
status_select() |
Updates status bar hint when cursor moves ("Press u to stage...") |
status_update_onbranch() |
Detects git state (rebasing, merging, etc.) and builds header text |
status_branch_tracking_info() |
Gets ahead/behind count relative to upstream |
View Registration
| Symbol |
Purpose |
status_ops |
Callback table implementing tig's view interface |
DEFINE_VIEW(status) |
Macro that creates the global status_view struct |
Entry Points
Opening the View
open_status_view() (line 39)
Public function called by other parts of tig to open the status view.
Called from:
- Main tig dispatcher when user runs
tig status or presses S
- Main view when showing untracked files inline
The untracked_only flag filters to just untracked files (used when invoked from main view).
View Lifecycle
status_open() (line 380)
Called by tig's view system when:
- View is first opened
- View is refreshed (
REQ_REFRESH)
- After any action that modifies state (stage, unstage, revert)
This is where git commands run and the view gets populated.
User Input
status_request() (line 715)
Called by tig's event loop for every keypress while status view is active.
Key mappings handled:
| Request |
Default Key |
Handler |
REQ_STATUS_UPDATE |
u |
status_update() |
REQ_STATUS_REVERT |
! |
status_revert() |
REQ_STATUS_MERGE |
M |
open_mergetool() |
REQ_ENTER |
Enter |
status_enter() |
REQ_EDIT |
e |
open_editor() |
REQ_VIEW_BLAME |
B |
(returns request to parent) |
REQ_REFRESH |
R |
load_repo_head() + refresh |
Called by Other Views
status_exists() (line 509)
Utility for other views to check if a file exists in a given status section. Used by stage view to navigate back after operations.
Data Flow
Loading: Git → View
status_open()
│
├─► status_update_onbranch()
│ │
│ └─► Checks .git/rebase-merge/, MERGE_HEAD, etc.
│ Runs: git rev-list --left-right (for ahead/behind)
│ Writes: status_onbranch string
│
├─► status_run(staged_argv, LINE_STAT_STAGED)
│ │
│ └─► Runs: git diff-index -z -C --diff-filter=ACDMRTXB --cached HEAD
│ Parses: :100644 100644 <old-sha> <new-sha> M\0filename\0
│ Creates: view lines with struct status data
│
├─► status_run(unstaged_argv, LINE_STAT_UNSTAGED)
│ │
│ └─► Runs: git diff-files -z
│ Same parsing as above
│
└─► status_run(untracked_argv, LINE_STAT_UNTRACKED)
│
└─► Runs: git ls-files -z --others --exclude-standard
Parses: filename\0filename\0 (simpler format)
Creates: view lines with status='?'
View Structure After Load
Line 0: [LINE_HEADER] data=NULL → "On branch main"
Line 1: [LINE_STAT_STAGED] data=NULL → "Changes to be committed:"
Line 2: [LINE_STAT_STAGED] data=status* → M src/foo.c
Line 3: [LINE_STAT_STAGED] data=status* → A src/bar.c
Line 4: [LINE_STAT_UNSTAGED] data=NULL → "Changes not staged:"
Line 5: [LINE_STAT_UNSTAGED] data=status* → M src/baz.c
Line 6: [LINE_STAT_UNTRACKED] data=NULL → "Untracked files:"
Line 7: [LINE_STAT_UNTRACKED] data=status* → ? newfile.txt
data=NULL → section header
data=status* → actual file entry
User Actions: View → Git
Staging (unstaged → staged)
User presses 'u' on unstaged file
│
└─► status_update_file(status, LINE_STAT_UNSTAGED)
│
└─► Runs: git update-index -z --add --remove --stdin
Writes to stdin: filename\0
Unstaging (staged → unstaged)
User presses 'u' on staged file
│
└─► status_update_file(status, LINE_STAT_STAGED)
│
└─► Runs: git update-index -z --index-info
Writes to stdin: 100644 <old-sha>\tfilename\0
Reverting
User presses '!' on unstaged file
│
└─► status_revert(status, LINE_STAT_UNSTAGED)
│
├─► (if unmerged) git update-index --cacheinfo ...
│
└─► Runs: git checkout -- filename
Dependencies
Internal Modules (tig)
| Include |
Used For |
tig/io.h |
Process spawning, pipe I/O (io_run, io_get, io_printf) |
tig/view.h |
View infrastructure (add_line_alloc, refresh_view, view_column_draw) |
tig/repo.h |
Repository state (repo.head, repo.git_dir, repo.upstream) |
tig/refdb.h |
Reference lookup (get_canonical_ref for detached HEAD) |
tig/options.h |
User preferences (opt_status_show_untracked_files) |
tig/prompt.h |
User confirmation (prompt_yesno for revert) |
tig/watch.h |
File watching (watch_register, watch_apply) |
tig/stage.h |
Stage view (open_stage_view) |
tig/git.h |
Git command macros (GIT_DIFF_STAGED_FILES, GIT_DIFF_UNSTAGED_FILES) |
External: Git Commands
Reading state:
| Command |
Purpose |
git diff-index -z -C --diff-filter=ACDMRTXB --cached HEAD |
List staged changes |
git diff-files -z |
List unstaged changes |
git ls-files -z --others --exclude-standard |
List untracked files |
git ls-files -z --cached |
List files in initial commit (no HEAD yet) |
git rev-list --left-right <head>...<upstream> |
Count ahead/behind commits |
Modifying state:
| Command |
Purpose |
git update-index -z --index-info |
Unstage files (restore old index entry) |
git update-index -z --add --remove --stdin |
Stage files |
git update-index --cacheinfo |
Fix index for unmerged files |
git checkout -- <file> |
Revert working tree changes |
git add -- <dir> |
Stage untracked directories |
git mergetool <file> |
Launch merge tool for conflicts |
Git State Files Read
| Path |
Indicates |
.git/rebase-apply/rebasing |
Non-interactive rebase |
.git/rebase-merge/interactive |
Interactive rebase |
.git/MERGE_HEAD |
Merge in progress |
.git/BISECT_LOG |
Bisect in progress |
.git/HEAD |
Current head reference |
Gotchas
1. Line Type Dual Meaning
A line's type (e.g., LINE_STAT_STAGED) doesn't tell you whether it's a header or a file entry. You must check line->data:
if (!line->data) {
// Section header: "Changes to be committed:"
} else {
// Actual file entry
}
2. Unstaging Restores, Not Removes
Pressing u on a staged file doesn't remove it from the index — it restores the previous index entry. This is why renamed files unstage correctly.
3. No Batch Revert
Unlike staging (u on header stages all files), revert (!) only works on single files. Intentional safety measure.
4. Unmerged Files Appear Twice in Git Output
Git reports unmerged files multiple times (once per stage slot). The code collapses these into a single 'U' entry.
5. Initial Commit Uses Different Command
When there's no HEAD, git ls-files --cached is used instead of git diff-index HEAD, with status forced to 'A'.
6. NUL-Separated Output Requires Two Reads
For diff output, metadata and filename are separate NUL-terminated records. Renames require three reads (metadata, old name, new name).
7. Directories Are Special-Cased
Untracked directories use git add instead of update-index --stdin.
8. Order Matters in Branch Detection
status_update_onbranch() checks state files in a specific order — more specific states (rebase variants) must come before generic (HEAD).
9. VIEW_SEND_CHILD_ENTER Side Effect
The flag in status_ops causes Enter to propagate to child views. When you Enter on a file, the stage view receives the Enter too.
Hi Jonas,
I'm an amateur developer who is using AI to help me learn about software development. I'm working on understanding tig's internals and used Claude Code to help me write some documentation for
src/status.c. It covers:Would you be interested in retaining this kind of internal documentation? If so, I'm happy to submit a PR for your review or follow any additional contribution protocols you require.
Here's the full documentation:
status.c — Status View Implementation (click to expand)
Purpose
status.cimplements tig's interactive status view — the equivalent ofgit statusbut navigable and actionable. It displays staged, unstaged, and untracked files, and lets users stage, unstage, revert, and inspect changes without leaving the TUI.Why it exists:
The command-line
git statusis read-only output. To act on it, you run separate commands (git add,git checkout,git diff). The status view collapses this workflow: see status, navigate to a file, press a key to act. It's the hub for pre-commit workflow in tig.Design choice:
Rather than parsing
git status --porcelain, it runs three lower-level git commands directly:git diff-index -z -C --cached HEAD— staged changes (with-Cfor copy detection)git diff-files -z— unstaged changesgit ls-files -z --others --exclude-standard— untracked filesThe
-zflag produces NUL-separated output for safe filename handling. Using plumbing commands gives stable, parseable output and avoidsgit status's user-facing formatting.Note: The staged diff command also includes
--diff-filter=ACDMRTXBto exclude unmerged files (handled separately).Key Components
Data Structures
struct status(defined intig/status.h)Represents a single file entry. Contains:
status— single char using git's standard status codes: M (modified), A (added), D (deleted), R (renamed), C (copied), U (unmerged), ? (untracked)old— previous state (mode, revision, name)new— current state (mode, revision, name)status_onbranch(static global)String holding the header line text: "On branch main", "Rebasing feature-x", "HEAD detached at v1.0", etc.
Core Functions
status_open()status_run()status_open().status_get_diff():100644 100644 <sha> <sha> M) into astruct status.User Action Handlers
status_request()status_update()ukeystatus_update_file()git update-indexstatus_update_files()status_revert()!keygit checkoutstatus_enter()Display Functions
status_get_column_data()status_select()status_update_onbranch()status_branch_tracking_info()View Registration
status_opsDEFINE_VIEW(status)status_viewstructEntry Points
Opening the View
open_status_view()(line 39)Public function called by other parts of tig to open the status view.
Called from:
tig statusor pressesSThe
untracked_onlyflag filters to just untracked files (used when invoked from main view).View Lifecycle
status_open()(line 380)Called by tig's view system when:
REQ_REFRESH)This is where git commands run and the view gets populated.
User Input
status_request()(line 715)Called by tig's event loop for every keypress while status view is active.
Key mappings handled:
REQ_STATUS_UPDATEustatus_update()REQ_STATUS_REVERT!status_revert()REQ_STATUS_MERGEMopen_mergetool()REQ_ENTERstatus_enter()REQ_EDITeopen_editor()REQ_VIEW_BLAMEBREQ_REFRESHRload_repo_head()+ refreshCalled by Other Views
status_exists()(line 509)Utility for other views to check if a file exists in a given status section. Used by stage view to navigate back after operations.
Data Flow
Loading: Git → View
View Structure After Load
data=NULL→ section headerdata=status*→ actual file entryUser Actions: View → Git
Staging (unstaged → staged)
Unstaging (staged → unstaged)
Reverting
Dependencies
Internal Modules (tig)
tig/io.hio_run,io_get,io_printf)tig/view.hadd_line_alloc,refresh_view,view_column_draw)tig/repo.hrepo.head,repo.git_dir,repo.upstream)tig/refdb.hget_canonical_reffor detached HEAD)tig/options.hopt_status_show_untracked_files)tig/prompt.hprompt_yesnofor revert)tig/watch.hwatch_register,watch_apply)tig/stage.hopen_stage_view)tig/git.hGIT_DIFF_STAGED_FILES,GIT_DIFF_UNSTAGED_FILES)External: Git Commands
Reading state:
git diff-index -z -C --diff-filter=ACDMRTXB --cached HEADgit diff-files -zgit ls-files -z --others --exclude-standardgit ls-files -z --cachedgit rev-list --left-right <head>...<upstream>Modifying state:
git update-index -z --index-infogit update-index -z --add --remove --stdingit update-index --cacheinfogit checkout -- <file>git add -- <dir>git mergetool <file>Git State Files Read
.git/rebase-apply/rebasing.git/rebase-merge/interactive.git/MERGE_HEAD.git/BISECT_LOG.git/HEADGotchas
1. Line Type Dual Meaning
A line's
type(e.g.,LINE_STAT_STAGED) doesn't tell you whether it's a header or a file entry. You must checkline->data:2. Unstaging Restores, Not Removes
Pressing
uon a staged file doesn't remove it from the index — it restores the previous index entry. This is why renamed files unstage correctly.3. No Batch Revert
Unlike staging (
uon header stages all files), revert (!) only works on single files. Intentional safety measure.4. Unmerged Files Appear Twice in Git Output
Git reports unmerged files multiple times (once per stage slot). The code collapses these into a single
'U'entry.5. Initial Commit Uses Different Command
When there's no HEAD,
git ls-files --cachedis used instead ofgit diff-index HEAD, with status forced to'A'.6. NUL-Separated Output Requires Two Reads
For diff output, metadata and filename are separate NUL-terminated records. Renames require three reads (metadata, old name, new name).
7. Directories Are Special-Cased
Untracked directories use
git addinstead ofupdate-index --stdin.8. Order Matters in Branch Detection
status_update_onbranch()checks state files in a specific order — more specific states (rebase variants) must come before generic (HEAD).9.
VIEW_SEND_CHILD_ENTERSide EffectThe flag in
status_opscauses Enter to propagate to child views. When you Enter on a file, the stage view receives the Enter too.Thanks,
Alex