jj-skill/templating.md
2025-11-03 14:13:41 -05:00

879 lines
19 KiB
Markdown

# Jujutsu Templates - Complete Reference
Templates are a functional language in jujutsu for customizing command output. This document provides comprehensive coverage of template syntax, keywords, functions, operators, and practical usage patterns.
## Overview
The template language enables customization of jujutsu command output via the `-T`/`--template` option. Templates can format text, apply styling, perform calculations, and implement conditional logic.
## Basic Usage
### Command Line Templates
```bash
# Simple template
jj log -T 'commit_id.short()'
# Multiple components
jj log -T 'commit_id.short() ++ " " ++ description.first_line()'
# With formatting
jj log -T 'commit_id.short() ++ "\n" ++ indent(" ", description)'
```
### Configuration Templates
Set default templates in config (`jj config edit --user`):
```toml
[templates]
log = '''
commit_id.short() ++ " "
++ if(conflict(), "⚠️ ", "")
++ description.first_line()
'''
op_log = 'id.short() ++ " " ++ description'
```
## Keywords
Keywords represent objects and their properties. The available keywords depend on the command context.
### Commit Keywords
Available in commit templates (e.g., `jj log`, `jj show`):
#### Basic Properties
- `commit_id` - Commit ID (CommitId type)
- `change_id` - Change ID (ChangeId type)
- `description` - Commit description (String)
- `author` - Commit author (Signature type)
- `committer` - Committer (Signature type)
- `working_copies` - Workspaces where this is the working copy (String)
- `current_working_copy` - Boolean, true if this is current working copy
- `bookmarks` - Bookmarks pointing to this commit (List<RefName>)
- `tags` - Tags pointing to this commit (List<RefName>)
- `git_refs` - Git refs pointing to this commit (List<RefName>)
- `git_head` - Git HEAD if pointing to this commit (RefName)
- `divergent` - Boolean, true if change has divergent commits
- `hidden` - Boolean, true if commit is hidden
- `immutable` - Boolean, true if commit is immutable
- `conflict` - Boolean, true if commit has conflicts
- `empty` - Boolean, true if commit has no changes
- `root` - Boolean, true if this is the root commit
#### Relationship Properties
- `parents` - Parent commits (List<Commit>)
- `contained_in` - Bookmarks/tags containing this commit (String)
#### Tree Properties
- `tree` - Commit tree object (Tree type)
### Operation Keywords
Available in operation templates (e.g., `jj op log`):
- `current_operation` - Boolean, true for current operation
- `description` - Operation description (String)
- `id` - Operation ID (OperationId type)
- `tags` - Tags on this operation (String)
- `time` - Operation timestamp (Timestamp type)
- `user` - User who performed operation (String)
- `root` - Boolean, true if root operation
## Types and Methods
### CommitId / ChangeId
Hexadecimal identifiers.
**Methods:**
- `.short([len])` - Abbreviated form (default 12 chars)
- `.shortest([min_len])` - Shortest unique prefix (min 4 chars)
- `.hex()` - Full hexadecimal string
```bash
jj log -T 'commit_id.short()' # 12 chars
jj log -T 'commit_id.short(8)' # 8 chars
jj log -T 'commit_id.shortest()' # Unique prefix
jj log -T 'change_id.hex()' # Full ID
```
### String
Text values.
**Methods:**
- `.contains(needle)` - Check if contains substring
- `.starts_with(prefix)` - Check if starts with prefix
- `.ends_with(suffix)` - Check if ends with suffix
- `.remove_prefix(prefix)` - Remove prefix if present
- `.remove_suffix(suffix)` - Remove suffix if present
- `.substr(start, end)` - Extract substring
- `.first_line()` - First line only
- `.lines()` - Split into list of lines
- `.upper()` - Convert to uppercase
- `.lower()` - Convert to lowercase
- `.len()` - String length
```bash
jj log -T 'description.first_line()'
jj log -T 'description.upper()'
jj log -T 'if(description.contains("bug"), "🐛", "")'
jj log -T 'description.substr(0, 50)'
```
### Signature
Author/committer information.
**Methods:**
- `.name()` - Person's name
- `.email()` - Email address
- `.username()` - Username (part before @ in email)
- `.timestamp()` - Signature timestamp
```bash
jj log -T 'author.name()'
jj log -T 'author.email()'
jj log -T 'author.username() ++ " " ++ author.timestamp()'
```
### Timestamp
Date and time values.
**Methods:**
- `.ago()` - Relative time ("2 days ago")
- `.format(format_string)` - Custom format (strftime)
- `.utc()` - Format in UTC
```bash
jj log -T 'author.timestamp().ago()'
jj log -T 'author.timestamp().format("%Y-%m-%d")'
jj log -T 'author.timestamp().format("%Y-%m-%d %H:%M:%S")'
jj log -T 'author.timestamp().utc()'
```
Format specifiers (strftime):
- `%Y` - Year (4 digits)
- `%m` - Month (01-12)
- `%d` - Day (01-31)
- `%H` - Hour (00-23)
- `%M` - Minute (00-59)
- `%S` - Second (00-59)
- `%a` - Weekday abbreviation
- `%b` - Month abbreviation
### Boolean
True/false values.
**Usage in conditionals:**
```bash
jj log -T 'if(conflict, "⚠️ CONFLICT", "")'
jj log -T 'if(empty, "[empty]", description.first_line())'
jj log -T 'if(current_working_copy, "→ ", " ")'
```
### List
Collections of values.
**Methods:**
- `.len()` - Number of items
- `.join(separator)` - Join into string
- `.map(|item| template)` - Transform each item
- `.filter(|item| condition)` - Select items matching condition
- `.any(|item| condition)` - True if any item matches
- `.all(|item| condition)` - True if all items match
```bash
# Join parent IDs
jj log -T 'parents.map(|c| c.commit_id().short()).join(", ")'
# Show bookmarks
jj log -T 'bookmarks.map(|b| b.name()).join(" ")'
# Filter and show
jj log -T 'tags.filter(|t| t.name().starts_with("v")).map(|t| t.name()).join(", ")'
```
### RefName
Reference names (bookmarks, tags, git refs).
**Methods:**
- `.name()` - Reference name
- `.remote()` - Remote name (for remote refs)
- `.present()` - Boolean, true if ref exists
```bash
jj log -T 'bookmarks.map(|b| b.name()).join(", ")'
jj log -T 'git_refs.map(|r| r.name()).join(", ")'
```
### Commit
Full commit objects (from parents, etc.).
**Methods:**
All commit keywords available as methods:
- `.commit_id()`
- `.change_id()`
- `.description()`
- `.author()`
- `.parents()`
- etc.
```bash
jj log -T 'parents.map(|c| c.commit_id().short()).join(", ")'
jj log -T 'parents.map(|c| c.author().name()).join(" & ")'
```
## Operators
### Template Concatenation (`++`)
Combine templates.
```bash
jj log -T 'commit_id.short() ++ " " ++ description.first_line()'
jj log -T 'author.name() ++ " <" ++ author.email() ++ ">"'
jj log -T '"[" ++ change_id.short() ++ "]"'
```
### Logical Operators
- `x && y` - Logical AND
- `x || y` - Logical OR
- `!x` - Logical NOT
```bash
jj log -T 'if(conflict && !empty, "⚠️", "")'
jj log -T 'if(immutable || hidden, "SKIP", commit_id.short())'
```
### Comparison Operators
- `x == y` - Equal
- `x != y` - Not equal
- `x < y` - Less than
- `x > y` - Greater than
- `x <= y` - Less than or equal
- `x >= y` - Greater than or equal
```bash
jj log -T 'if(parents.len() > 1, "MERGE", "NORMAL")'
jj log -T 'if(description.len() < 10, "SHORT", description.first_line())'
```
### Arithmetic Operators
- `x + y` - Addition
- `x - y` - Subtraction
- `x * y` - Multiplication
- `x / y` - Division (integer)
- `x % y` - Modulo
```bash
jj log -T 'if(author.timestamp().format("%H").parse_int() > 12, "PM", "AM")'
```
### Method Calls
- `x.method()` - Call method on object
- `x.method(arg1, arg2)` - Call with arguments
```bash
jj log -T 'commit_id.short(8)'
jj log -T 'description.substr(0, 50)'
jj log -T 'author.timestamp().format("%Y-%m-%d")'
```
## Functions
Built-in template functions.
### Conditional Functions
#### `if(condition, then, else)`
Conditional output.
```bash
jj log -T 'if(conflict, "⚠️ ", "") ++ description.first_line()'
jj log -T 'if(empty, "[empty]", commit_id.short())'
jj log -T 'if(current_working_copy, "→ ", " ") ++ description.first_line()'
# Nested conditionals
jj log -T '
if(conflict, "⚠️",
if(empty, "∅",
if(divergent, "◆", "●")))
++ " " ++ description.first_line()
'
```
#### `coalesce(option1, option2, ...)`
Return first non-empty value.
```bash
jj log -T 'coalesce(description, "[no description]")'
jj log -T 'coalesce(bookmarks.map(|b| b.name()).join(", "), "[no bookmarks]")'
```
### Formatting Functions
#### `label(label, content)`
Apply styling/color labels.
```bash
jj log -T 'label("commit_id", commit_id.short())'
jj log -T 'label("error", "CONFLICT") ++ " " ++ description'
jj log -T 'if(conflict, label("error", "⚠"), "") ++ description.first_line()'
```
Common labels:
- `commit_id` - Commit ID styling
- `change_id` - Change ID styling
- `author` - Author name styling
- `timestamp` - Timestamp styling
- `bookmark` - Bookmark styling
- `tag` - Tag styling
- `error` - Error styling
- `warning` - Warning styling
#### `fill(width, content)`
Wrap text to specified width.
```bash
jj log -T 'fill(80, description)'
jj log -T 'commit_id.short() ++ "\n" ++ fill(72, description)'
```
#### `indent(prefix, content)`
Indent lines with prefix.
```bash
jj log -T 'commit_id.short() ++ "\n" ++ indent(" ", description)'
jj log -T 'description.first_line() ++ "\n" ++ indent(" ", author.name())'
```
#### `pad_start(width, content[, fill_char])`
Pad start to width.
```bash
jj log -T 'pad_start(10, commit_id.short())'
jj log -T 'pad_start(20, author.name(), ".")'
```
#### `pad_end(width, content[, fill_char])`
Pad end to width.
```bash
jj log -T 'pad_end(50, description.first_line())'
jj log -T 'pad_end(30, author.name(), " ") ++ commit_id.short()'
```
#### `truncate_start(width, content)`
Truncate from start if too long.
```bash
jj log -T 'truncate_start(50, description.first_line())'
```
#### `truncate_end(width, content)`
Truncate from end if too long.
```bash
jj log -T 'truncate_end(50, description.first_line())'
jj log -T 'pad_end(60, truncate_end(60, description.first_line()))'
```
### List Functions
#### `separate(separator, ...values)`
Join values with separator, skipping empty ones.
```bash
jj log -T 'separate(" | ", commit_id.short(), author.name(), description.first_line())'
jj log -T 'separate("\n", description, bookmarks.map(|b| b.name()).join(", "))'
```
Unlike `++`, `separate()` skips empty values:
```bash
# If bookmarks is empty, no trailing separator
jj log -T 'separate(" ", commit_id.short(), bookmarks.map(|b| b.name()).join(","))'
```
### Serialization Functions
#### `json(value)`
Serialize value to JSON.
```bash
jj log -T 'json(commit_id.short())'
jj log -T 'json(description.first_line())'
# For structured output
jj log -T '
"{" ++
"\"commit\": " ++ json(commit_id.short()) ++ "," ++
"\"description\": " ++ json(description.first_line()) ++
"}"
'
```
## Practical Examples
### Compact Log Format
```bash
jj log -T '
commit_id.short()
++ " "
++ author.username()
++ " "
++ author.timestamp().ago()
++ " "
++ description.first_line()
'
```
### Detailed Format
```bash
jj log -T '
"commit " ++ commit_id.short() ++ "\n"
++ "Author: " ++ author.name() ++ " <" ++ author.email() ++ ">\n"
++ "Date: " ++ author.timestamp().format("%Y-%m-%d %H:%M:%S") ++ "\n"
++ "\n"
++ indent(" ", description)
'
```
### Status Indicators
```bash
jj log -T '
if(current_working_copy, "→", " ")
++ if(conflict, "⚠", " ")
++ if(empty, "∅", " ")
++ if(divergent, "◆", " ")
++ " "
++ commit_id.short()
++ " "
++ description.first_line()
'
```
### Branch and Tag Display
```bash
jj log -T '
commit_id.short()
++ " "
++ separate(" ",
bookmarks.map(|b| label("bookmark", b.name())).join(" "),
tags.map(|t| label("tag", t.name())).join(" "))
++ if(bookmarks.len() > 0 || tags.len() > 0, "\n ", " ")
++ description.first_line()
'
```
### Parent Commit IDs
```bash
jj log -T '
commit_id.short()
++ " (parents: "
++ parents.map(|c| c.commit_id().short()).join(", ")
++ ") "
++ description.first_line()
'
```
### Merge Commit Detection
```bash
jj log -T '
if(parents.len() > 1,
"MERGE " ++ commit_id.short() ++ "\n"
++ " parents: " ++ parents.map(|c| c.commit_id().short()).join(", ") ++ "\n"
++ " " ++ description.first_line(),
commit_id.short() ++ " " ++ description.first_line())
'
```
### JSON Output
```bash
jj log -T '
"{\"commit_id\": " ++ json(commit_id.short())
++ ", \"change_id\": " ++ json(change_id.short())
++ ", \"author\": " ++ json(author.name())
++ ", \"email\": " ++ json(author.email())
++ ", \"date\": " ++ json(author.timestamp().format("%Y-%m-%d %H:%M:%S"))
++ ", \"description\": " ++ json(description.first_line())
++ "}"
'
```
### Author Statistics
```bash
jj log -T '
author.name()
++ " ("
++ author.email()
++ ") - "
++ author.timestamp().format("%Y-%m-%d")
++ " - "
++ commit_id.short()
'
```
### Time-Based Formatting
```bash
jj log -T '
"["
++ author.timestamp().format("%Y-%m-%d %H:%M")
++ "] "
++ author.username()
++ ": "
++ truncate_end(60, description.first_line())
'
```
## Template Aliases
Define reusable template fragments in config:
```toml
[template-aliases]
# Simple substitutions
short_commit = 'commit_id.short()'
full_author = 'author.name() ++ " <" ++ author.email() ++ ">"'
# Complex templates
format_timestamp = 'author.timestamp().format("%Y-%m-%d %H:%M")'
status_icons = '''
if(current_working_copy, "→", " ")
++ if(conflict, "⚠", " ")
++ if(empty, "∅", " ")
'''
compact_commit = '''
commit_id.short()
++ " "
++ author.username()
++ " "
++ description.first_line()
'''
```
Use in templates:
```bash
jj log -T 'status_icons ++ compact_commit'
jj log -T 'short_commit ++ " by " ++ full_author'
```
## Advanced Patterns
### Conditional Styling
```bash
jj log -T '
label(
if(conflict, "error",
if(empty, "warning", "commit_id")),
commit_id.short())
++ " "
++ description.first_line()
'
```
### Multi-Line Descriptions
```bash
jj log -T '
commit_id.short() ++ "\n"
++ separate("\n",
if(bookmarks.len() > 0, "Bookmarks: " ++ bookmarks.map(|b| b.name()).join(", "), ""),
if(tags.len() > 0, "Tags: " ++ tags.map(|t| t.name()).join(", "), ""))
++ "\n"
++ indent(" ", description)
++ "\n"
'
```
### Working Copy Highlighting
```bash
jj log -T '
if(current_working_copy,
label("working_copy", ">> " ++ commit_id.short() ++ " <<"),
" " ++ commit_id.short() ++ " ")
++ " "
++ description.first_line()
'
```
### List Filtering and Mapping
```bash
# Show only remote bookmarks
jj log -T '
commit_id.short()
++ " "
++ bookmarks
.filter(|b| b.remote() != "")
.map(|b| b.remote() ++ "/" ++ b.name())
.join(", ")
'
# Show version tags only
jj log -T '
commit_id.short()
++ " "
++ tags
.filter(|t| t.name().starts_with("v"))
.map(|t| t.name())
.join(", ")
'
```
### Complex Conditional Logic
```bash
jj log -T '
if(conflict,
label("error", "⚠ CONFLICT") ++ " " ++ commit_id.short(),
if(empty,
label("warning", "∅ EMPTY") ++ " " ++ commit_id.short(),
if(divergent,
label("warning", "◆ DIVERGENT") ++ " " ++ commit_id.short(),
commit_id.short())))
++ "\n"
++ if(current_working_copy, "→ ", " ")
++ description.first_line()
'
```
## Performance Considerations
### Avoid Expensive Operations in Large Histories
```bash
# Expensive - computes for every commit
jj log -T 'commit_id.short() ++ " " ++ contained_in'
# Better - only show what's needed
jj log -r 'latest(all(), 50)' -T 'commit_id.short() ++ " " ++ description.first_line()'
```
### Limit Template Complexity
```bash
# Complex template - may be slow
jj log -T '
commit_id.short()
++ " "
++ parents.map(|c|
c.commit_id().short()
++ " by "
++ c.author().name()
).join(", ")
++ " "
++ description
'
# Simpler alternative
jj log -T '
commit_id.short()
++ " (parents: " ++ parents.map(|c| c.commit_id().short()).join(", ") ++ ")"
++ " "
++ description.first_line()
'
```
## Best Practices
### 1. Build Templates Incrementally
```bash
# Start simple
jj log -T 'commit_id.short()'
# Add author
jj log -T 'commit_id.short() ++ " " ++ author.name()'
# Add description
jj log -T 'commit_id.short() ++ " " ++ author.name() ++ " " ++ description.first_line()'
# Add formatting
jj log -T '
commit_id.short()
++ " "
++ truncate_end(20, author.name())
++ " "
++ description.first_line()
'
```
### 2. Use Template Aliases
Define commonly-used patterns once:
```toml
[template-aliases]
my_log = '''
if(current_working_copy, "→ ", " ")
++ commit_id.short()
++ " "
++ author.username()
++ " "
++ author.timestamp().ago()
++ " "
++ description.first_line()
'''
```
```bash
jj log -T my_log
```
### 3. Test Templates on Small Sets
```bash
# Test on single commit
jj log -r @ -T 'your template here'
# Test on small range
jj log -r 'latest(all(), 5)' -T 'your template here'
```
### 4. Use Proper Quoting
```bash
# Single quotes for templates (avoid shell expansion)
jj log -T 'commit_id.short() ++ " " ++ description'
# Escape quotes inside template strings
jj log -T '"[" ++ commit_id.short() ++ "]"'
```
### 5. Leverage `separate()` for Clean Output
```bash
# Rather than manual checking for empty values
jj log -T 'commit_id.short() ++ if(bookmarks.len() > 0, " " ++ bookmarks.map(|b| b.name()).join(","), "")'
# Use separate
jj log -T 'separate(" ", commit_id.short(), bookmarks.map(|b| b.name()).join(","))'
```
## Troubleshooting
### Template Syntax Errors
```bash
# Error: missing closing quote
jj log -T 'commit_id.short()
# Fix: close the quote
jj log -T 'commit_id.short()'
# Error: mismatched parentheses
jj log -T 'if(conflict, "X"'
# Fix: close all parentheses
jj log -T 'if(conflict, "X", "")'
```
### Type Errors
```bash
# Error: trying to call string method on commit
jj log -T 'parents.short()'
# Fix: map over list and call method on each
jj log -T 'parents.map(|c| c.commit_id().short()).join(", ")'
# Error: treating string as boolean
jj log -T 'if(description, "yes", "no")'
# Fix: check for empty string
jj log -T 'if(description != "", "yes", "no")'
```
### Empty Output
```bash
# Check if keywords exist
jj log -r @ -T 'description'
# Verify list length
jj log -r @ -T 'bookmarks.len()'
# Use coalesce for defaults
jj log -T 'coalesce(bookmarks.map(|b| b.name()).join(","), "[no bookmarks]")'
```
## Integration with Commands
### Log Output
```bash
jj log -T 'template'
jj log -r 'revset' -T 'template'
jj log --no-graph -T 'template' # Without graph
```
### Show Command
```bash
jj show -T 'template'
```
### Operation Log
```bash
jj op log -T 'template'
```
### Scriptable Output
```bash
# Get commit IDs for scripting
jj log -r 'mine()' -T 'commit_id.hex()' --no-graph
# JSON output
jj log -r @ -T '{"id": ' ++ json(commit_id.short()) ++ '}' --no-graph
```
## Summary
Templates provide powerful output customization in jujutsu:
- **Keywords**: `commit_id`, `change_id`, `description`, `author`, `committer`, etc.
- **Types**: CommitId, String, Signature, Timestamp, Boolean, List, RefName, Commit
- **Operators**: `++` (concat), `&&/||/!` (logical), `==/!=/</>` (comparison), `+/-/*` (arithmetic)
- **Functions**: `if()`, `coalesce()`, `label()`, `fill()`, `indent()`, `separate()`, etc.
- **Methods**: `.short()`, `.first_line()`, `.contains()`, `.map()`, `.filter()`, etc.
- **Aliases**: Define reusable templates in config
- **Best practices**: Build incrementally, use aliases, test on small sets
For commit selection, see revsets.md. For file filtering, see filesets.md.
Master templates to create custom, informative output that fits your workflow perfectly.