Skip to content

Interest in internal developer documentation? #1399

@alexg-g

Description

@alexg-g

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:

  • Purpose and design decisions
  • Key functions and data flow
  • Non-obvious behavior

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.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.


Thanks,
Alex

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions