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

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 copy
  • bookmarks - 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 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)
  • 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
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 AND
  • x || 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 - 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
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
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
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 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.

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.