19 KiB
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
# 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):
[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 copybookmarks- Bookmarks pointing to this commit (List)tags- Tags pointing to this commit (List)git_refs- Git refs pointing to this commit (List)git_head- Git HEAD if pointing to this commit (RefName)divergent- Boolean, true if change has divergent commitshidden- Boolean, true if commit is hiddenimmutable- Boolean, true if commit is immutableconflict- Boolean, true if commit has conflictsempty- Boolean, true if commit has no changesroot- Boolean, true if this is the root commit
Relationship Properties
parents- Parent commits (List)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 operationdescription- 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
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
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
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
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:
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
# 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
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.
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.
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 ANDx || y- Logical OR!x- Logical NOT
jj log -T 'if(conflict && !empty, "⚠️", "")'
jj log -T 'if(immutable || hidden, "SKIP", commit_id.short())'
Comparison Operators
x == y- Equalx != y- Not equalx < y- Less thanx > y- Greater thanx <= y- Less than or equalx >= y- Greater than or equal
jj log -T 'if(parents.len() > 1, "MERGE", "NORMAL")'
jj log -T 'if(description.len() < 10, "SHORT", description.first_line())'
Arithmetic Operators
x + y- Additionx - y- Subtractionx * y- Multiplicationx / y- Division (integer)x % y- Modulo
jj log -T 'if(author.timestamp().format("%H").parse_int() > 12, "PM", "AM")'
Method Calls
x.method()- Call method on objectx.method(arg1, arg2)- Call with arguments
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.
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.
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.
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 stylingchange_id- Change ID stylingauthor- Author name stylingtimestamp- Timestamp stylingbookmark- Bookmark stylingtag- Tag stylingerror- Error stylingwarning- Warning styling
fill(width, content)
Wrap text to specified width.
jj log -T 'fill(80, description)'
jj log -T 'commit_id.short() ++ "\n" ++ fill(72, description)'
indent(prefix, content)
Indent lines with prefix.
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.
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.
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.
jj log -T 'truncate_start(50, description.first_line())'
truncate_end(width, content)
Truncate from end if too long.
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.
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:
# 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.
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
jj log -T '
commit_id.short()
++ " "
++ author.username()
++ " "
++ author.timestamp().ago()
++ " "
++ description.first_line()
'
Detailed Format
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
jj log -T '
if(current_working_copy, "→", " ")
++ if(conflict, "⚠", " ")
++ if(empty, "∅", " ")
++ if(divergent, "◆", " ")
++ " "
++ commit_id.short()
++ " "
++ description.first_line()
'
Branch and Tag Display
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
jj log -T '
commit_id.short()
++ " (parents: "
++ parents.map(|c| c.commit_id().short()).join(", ")
++ ") "
++ description.first_line()
'
Merge Commit Detection
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
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
jj log -T '
author.name()
++ " ("
++ author.email()
++ ") - "
++ author.timestamp().format("%Y-%m-%d")
++ " - "
++ commit_id.short()
'
Time-Based Formatting
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:
[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:
jj log -T 'status_icons ++ compact_commit'
jj log -T 'short_commit ++ " by " ++ full_author'
Advanced Patterns
Conditional Styling
jj log -T '
label(
if(conflict, "error",
if(empty, "warning", "commit_id")),
commit_id.short())
++ " "
++ description.first_line()
'
Multi-Line Descriptions
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
jj log -T '
if(current_working_copy,
label("working_copy", ">> " ++ commit_id.short() ++ " <<"),
" " ++ commit_id.short() ++ " ")
++ " "
++ description.first_line()
'
List Filtering and Mapping
# 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
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
# 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
# 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
# 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:
[template-aliases]
my_log = '''
if(current_working_copy, "→ ", " ")
++ commit_id.short()
++ " "
++ author.username()
++ " "
++ author.timestamp().ago()
++ " "
++ description.first_line()
'''
jj log -T my_log
3. Test Templates on Small Sets
# 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
# 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
# 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
# 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
# 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
# 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
jj log -T 'template'
jj log -r 'revset' -T 'template'
jj log --no-graph -T 'template' # Without graph
Show Command
jj show -T 'template'
Operation Log
jj op log -T 'template'
Scriptable Output
# 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.