Skip to main content

Landing Requests

Landing Requests (LRs) are JJHub’s native replacement for Pull Requests. They’re built from the ground up for jj’s stacked changes model - stable Change IDs, first-class conflict tracking, and automatic rebasing.

Landing Requests vs Pull Requests

Create a Landing Request

Key flags:

Landing a Stack

When you’re working with stacked changes, use --stack to include the entire chain:
The LR will show the stack_size and all change_ids in the stack.

Draft Landing Requests

A draft landing request is a work-in-progress LR that is not yet ready for review. Drafts let you push changes early, share context with teammates, and iterate on a stack without triggering review notifications or enforcing protection rules.

What makes a draft different from an open LR

Draft LRs appear with a draft state badge when viewed with jjhub land view.

Creating a draft landing request

Use the --draft flag when creating:
This creates the LR in draft state. No review notifications are sent and no CI status checks are required while it remains a draft.

Creating a draft via the API

The response includes "state": "draft" to confirm the LR was created as a draft.

Publishing a draft (moving from draft to open)

When the changes are ready for review, publish the draft to move it to open state. There are two ways to do this: Using jjhub land ready:
This is the recommended shorthand. It moves the LR from draft to open, triggering review notifications and enabling protection rule enforcement. Using jjhub land edit:
Via the API:

When to use drafts

  • Early feedback — Push changes and share the LR URL with a teammate for informal feedback before the code is complete.
  • Work-in-progress changes — Start an LR to track your progress without cluttering the review queue.
  • Stacked changes still in progress — When building a stack, create a draft LR for the full stack so you can push intermediate states without triggering CI on incomplete code.
  • AI agent collaboration — Create a draft LR, let an AI agent iterate on the code via suggestions, then publish once the changes are ready for human review.

Converting an open LR back to draft

If you need to pull a published LR back to draft state (for example, if you discover the changes need more work), use:
This stops review notifications and suspends protection rule enforcement until the LR is published again. Existing reviews are preserved but stale review dismissal (if configured via bookmark protection) does not apply to draft LRs.

List Landing Requests

View a Landing Request

Shows the title, body, state, change IDs, stack size, CI check status, and reviews.

Review

Suggested Changes

Suggested changes let reviewers propose specific code edits inline within a review comment. Instead of describing what to change in plain text, a reviewer can include the exact replacement code. The landing request author can then accept the suggestion with a single command, and JJHub automatically creates a new change with the edit applied. This is JJHub’s equivalent of GitHub’s suggested changes feature, adapted for jj’s change model.

How suggested changes differ from regular review comments

Regular review comments are plain text — they describe what should change but leave the author to interpret and implement the fix. Suggested changes contain an actual code diff: the reviewer specifies the exact lines to replace and what they should become. This eliminates ambiguity, speeds up the review cycle, and is particularly powerful for AI agent reviews where the agent can propose precise fixes. Because JJHub is jj-native, accepting a suggestion creates a new jj change (with a stable Change ID) rather than amending an existing commit. This preserves the full history of how the code evolved during review.

Creating a suggestion via CLI

Use the suggestion code fence syntax inside a review comment body. The fenced block specifies the replacement code for the lines being commented on:
The ```suggestion block works the same way as on GitHub: the contents replace the lines the comment is attached to. You can include multiple suggestion blocks in a single review if commenting on different parts of the diff. When creating a review comment via the API, attach the suggestion to a specific file path and line range so JJHub knows which lines the replacement targets.

Viewing suggestions

When viewing a landing request, suggestions are shown alongside regular review comments. Each suggestion has a unique ID and a status (pending, accepted, or rejected):

Accepting and rejecting suggestions

The landing request author (or anyone with write access) can accept or reject individual suggestions:
When a suggestion is accepted:
  1. JJHub applies the code replacement to the target file at the specified lines.
  2. A new jj change is created with the edit. The change description is auto-generated (e.g., “Apply suggestion from @reviewer on src/auth.go”).
  3. The new change is added to the landing request’s change stack.
  4. The suggestion status updates to accepted.
  5. CI checks re-run against the updated code.
When a suggestion is rejected, its status updates to rejected and no code changes are made.

Batch accepting suggestions

For landing requests with many suggestions, use accept-all to apply every pending suggestion in a single operation:
This creates one new change per accepted suggestion (preserving granular history) or, if the --squash flag is used, combines all accepted suggestions into a single change:

Suggestion syntax in comments

The suggestion syntax uses a fenced code block with the suggestion language identifier:
The code inside the suggestion block replaces the lines that the review comment is anchored to. A single comment can contain at most one suggestion block. To suggest changes to multiple locations, create separate review comments for each.

API endpoints for suggestions

Suggestions are managed through dedicated endpoints on the landing request:
Create a suggestion:
Response:
Accept a suggestion:
After acceptance, the response includes the change_id of the newly created change:

How accepted suggestions create new changes

When a suggestion is accepted, JJHub performs the following operations on the repo-host:
  1. The repo-host reads the target file at the specified lines from the change that the suggestion is attached to.
  2. It replaces the specified line range with the suggestion content.
  3. A new jj change is created as a descendant of the current tip of the landing request’s change stack.
  4. The change description is set to: Apply suggestion from @<reviewer> on <path>.
  5. The new change’s stable Change ID is recorded on the suggestion record.
  6. The landing request’s change_ids list and stack_size are updated to include the new change.
This approach preserves full auditability — every accepted suggestion is a distinct change in the jj operation log, traceable back to the reviewer who proposed it and the exact lines that were modified.

Suggestions and AI agents

Suggested changes are particularly powerful when combined with JJHub’s AI agent system. An AI agent reviewing a landing request can propose precise code fixes as suggestions rather than just describing issues in prose. The author can review the agent’s suggestions and accept them with a single command, creating a tight human-in-the-loop workflow:

Check CI Status

Shows the status of all CI checks associated with the landing request.

Conflict Resolution

jj treats conflicts as first-class objects, not file markers. When two changes modify the same file, jj records a conflict object that preserves all three sides: the base (common ancestor), the left (your version), and the right (the other version). This means conflicts are data, not corruption — you can continue working, push, and even review code that contains conflicts. Landing Requests surface jj’s conflict tracking automatically. When a conflict is detected, the LR’s conflict_status field changes from "clean" to "conflicted", and a landing.conflict webhook event fires if webhooks are configured.

How jj conflicts differ from git merge conflicts

In git, a conflict produces inline markers (<<<<<<<, =======, >>>>>>>) that corrupt the file until manually resolved. The file is in a broken state and cannot be compiled or tested. In jj, a conflict is a structured object stored alongside the file. The file itself is not corrupted — jj records what the base, left, and right versions are as separate trees. This means:
  • You can view each side independently
  • Other changes in the stack can continue to land even if one change has conflicts
  • Conflict state is tracked per-change, not per-file globally
  • The programmable landing queue can be configured to allow landing with conflicts (for workflows that resolve them later)

Viewing conflicts

Use jjhub land conflicts to see the conflict details for a landing request:
This shows each conflicted file with the paths, conflict type, and the change IDs involved. For deeper inspection of a specific change’s conflicts, use:
The jjhub change show command displays the full conflict object, including the base, left, and right file contents so you can understand exactly what diverged.

Viewing conflicts via the API

The GET /repos/{owner}/{repo}/landings/{number}/conflicts endpoint returns the list of conflicts for the landing request, including the file paths, conflict type, and the change IDs where conflicts were detected. You can also check conflicts at the individual change level:

The conflict resolution workflow

  1. Detect — The LR shows conflict_status: "conflicted" automatically. You can also see it when running jjhub land view 42.
  2. Inspect — Run jjhub land conflicts 42 to see which files are conflicted and which changes are involved. Then use jjhub change show <change-id> to see the base, left, and right versions.
  3. Resolve locally — In your local jj repository, resolve the conflicts using jj’s built-in resolution tools:
  4. Push — After resolving, push your changes. The LR updates automatically because it tracks stable Change IDs, not commit hashes:
  5. Verify — The LR’s conflict_status updates to "clean" once all conflicts are resolved. CI checks re-run automatically against the updated changes.

Conflicts in stacked changes

When working with stacked changes, a conflict in one change can cascade to descendant changes in the stack. jj handles this naturally — resolving the conflict in the parent change and rebasing descendants will clear the cascading conflicts:
The LR will reflect the updated conflict status for the entire stack.

Review Conversations

When reviewing a landing request, reviewers leave comments — either general comments about the overall LR or inline comments on specific lines of code. These comments form review conversations (also called threads). A conversation starts with a top-level review comment and may include replies from the author or other reviewers. Review conversations can be resolved to indicate that the feedback has been addressed. Resolving a conversation collapses it visually and signals to the team that the discussion point is handled. Resolved conversations are not deleted — they remain accessible and can be re-opened (unresolved) at any time if the issue resurfaces.

Resolving and unresolving conversations

Use jjhub land thread resolve to mark a review comment thread as resolved, and jjhub land thread unresolve to re-open it:
The <comment-id> is the ID of the review comment that anchors the thread. You can find comment IDs by listing the threads on a landing request (see below).

Listing threads

To see review threads on a landing request, use jjhub land threads. By default this shows all threads; use --unresolved to filter to only unresolved ones:
The output includes each thread’s comment ID, file path and line number (for inline comments), the author, the resolution status, and a body preview.

Who can resolve conversations

The following users can resolve or unresolve a review conversation:
  • The LR author — can resolve conversations on their own landing request to indicate feedback has been addressed
  • The comment author — the reviewer who started the conversation can resolve it to confirm the fix is satisfactory
  • Repository admins and owners — can resolve or unresolve any conversation
  • Users with write access — any collaborator with write access to the repository can resolve conversations, matching Gitea’s behavior

API endpoints

Review conversation resolution is managed through the landing request comments API:
Resolve a review thread:
Response:
Unresolve a review thread:
The unresolve response returns the same comment object with resolved set to false and resolved_by and resolved_at cleared to null.

Integration with bookmark protection

Bookmark protection rules can require that all review conversations are resolved before a landing request can be merged. This ensures that every piece of review feedback has been explicitly addressed before code lands. To enable this rule on a protected bookmark:
When this rule is active and unresolved conversations remain, jjhub land land will fail with a clear error:
The jjhub land view command also shows the count of unresolved conversations, so authors can track their progress toward satisfying this protection rule.

Land

Lands the landing request into the target bookmark. The landing queue handles conflict detection and serialization.

Programmable Landing Queue

The landing queue is programmable via TypeScript workflows. By default, it serializes merges (one at a time), but you can configure custom merge strategies:

Bookmark Protection

Bookmark protection lets you define rules that gate landing into important bookmarks. When a landing request targets a protected bookmark, it must satisfy all configured rules before it can be merged. This is the jj-native equivalent of Gitea’s branch protection.

Why Bookmark Protection?

Without protection rules, any collaborator with write access can land changes directly into main or other critical bookmarks. Protection rules enforce code quality gates:
  • Require peer review before landing
  • Require CI status checks to pass
  • Prevent force pushes that rewrite history
  • Automatically dismiss stale reviews when new changes are pushed

Setting Up Protection Rules

Use jjhub bookmark protect to configure protection on a bookmark pattern:
To remove protection from a bookmark pattern:
To list all protection rules for a repository:

Configurable Rules

Pattern Matching

Bookmark protection rules use glob-style pattern matching: When a landing request targets a bookmark, all matching protection rules are evaluated. If multiple patterns match, the most restrictive combined rules apply. For example, if main requires 1 approval and * requires status checks, a landing request targeting main requires both 1 approval and passing status checks.

API Endpoints

Bookmark protection rules are managed through the repository API:
Create a protection rule:
Response:

How Protection Interacts with Landing Requests

When you run jjhub land land on a landing request that targets a protected bookmark, JJHub enforces all matching protection rules:
  1. Review check - If require_review is enabled, the LR must have at least required_approvals approving reviews. Reviews with “request changes” block landing regardless of approval count.
  2. Status check - If require_status_checks is enabled, all CI status checks associated with the LR’s changes must report a success status.
  3. Resolved conversations - If require_resolved_conversations is enabled, all review comment threads on the LR must be resolved before landing. Use jjhub land threads 42 --unresolved to see remaining threads.
  4. Stale review dismissal - If dismiss_stale_reviews is enabled, pushing new changes to the LR automatically dismisses previous approvals, requiring fresh reviews.
  5. Force push prevention - If no_force_push is enabled, any attempt to force-push to the protected bookmark is rejected at the git protocol level.
If any rule is not satisfied, the landing is rejected with a clear error message explaining which rules failed:
The jjhub land view and jjhub land checks commands also display protection rule status, so authors know exactly what is needed before attempting to land.

Lifecycle

  1. Create - Author opens an LR with jjhub land create (optionally as a --draft)
  2. Draft (optional) - If created as a draft, iterate on changes without triggering notifications. Publish with jjhub land ready when ready.
  3. Review - Team reviews with jjhub land review. Reviewers can leave comments, approve, request changes, or propose suggested changes with inline code replacements.
  4. Apply Suggestions - Author reviews any suggested changes and accepts or rejects them with jjhub land suggestion accept / reject. Accepted suggestions automatically create new changes in the stack.
  5. CI Checks - Automated workflows validate the changes
  6. Resolve Conflicts - If conflicts arise, view them with jjhub land conflicts, resolve locally with jj resolve, and push. The LR updates automatically.
  7. Resolve Conversations - Address review feedback and mark threads as resolved with jjhub land thread resolve. If bookmark protection requires resolved conversations, all threads must be resolved before landing.
  8. Protection Rules - All bookmark protection rules for the target bookmark must be satisfied
  9. Land - Author or maintainer lands with jjhub land land
LRs track conflicts automatically via jj’s first-class conflict objects. Unlike git, conflicts do not block work — the LR surfaces them, other changes in the stack can proceed, and you resolve at your own pace. See Conflict Resolution for the full workflow.

Reactions

Reactions let you add emoji responses to issues and comments. JJHub supports the same reaction types as Gitea: +1, -1, laugh, hooray, confused, heart, rocket, and eyes.

Supported Reaction Types

CLI Commands

API Endpoints

Issue Reactions

Comment Reactions

Each reaction response includes the reaction id, content (the reaction type), and the user who reacted.