jj-vine 0.4.0

Stacked pull requests for jj (jujutsu). Supports GitLab and bookmark-based flow.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
# `jj-vine`

A tool for submitting stacked Pull/Merge Requests from Jujutsu bookmarks.

Supports the following code forges:

- GitLab ([gitlab.com]https://gitlab.com or self-hosted)
- [GitHub]https://github.com or GitHub Enterprise
- [Forgejo]https://forgejo.org / [Codeberg]https://codeberg.org / Gitea
- [Azure DevOps]https://azure.microsoft.com/en-us/products/devops

*The canonical location for `jj-vine` is [codeberg.org/abrenneke/jj-vine](https://codeberg.org/abrenneke/jj-vine). GitHub is used as a mirror and CI only.*

## Table of Contents

- [Overview]#overview
- [Main Features]#main-features
- [Planned Features]#planned-features
- [Installation]#installation

  - [Cargo Binstall]#cargo-binstall
  - [Mise]#mise
  - [Pre-built Binaries]#pre-built-binaries
  - [Attestations]#attestations
  - [Alias Setup]#alias-setup
- [Quick Start]#quick-start
- [Commands]#commands

  - [submit]#submit
  - [init]#init
  - [status]#status
- [Configuration]#configuration

  - [Forge-Specific Settings]#forge-specific-settings

    - [Forge]#forge
    - [GitLab]#gitlab
    - [GitHub]#github
    - [Forgejo/Codeberg/Gitea]#forgejo-codeberg-gitea
    - [Azure DevOps]#azure-devops
  - [Common Settings]#common-settings
- [Description Generation / Stack Visualization]#description-generation-stack-visualization

  - [Configuration]#description-configuration
  - [Linear Format]#linear-format
  - [Tree Format]#tree-format
  - [Description Generation]#description-generation
- [Title Generation]#title-generation

  - [Configuration]#title-configuration
  - [Custom Title Templates]#custom-title-templates
  - [Credits]#credits
- [FAQs]#faqs

  - [Is this vibe-coded slop?]#is-this-vibe-coded-slop
  - [Ok, but really?]#ok-but-really
  - [Why a new project?]#why-a-new-project
- [Contributing]#contributing
- [License]#license

## Overview{#overview}

As `jj` is so flexible, it can sometimes be tedious to manage pull/merge requests for a `jj` repository. Additionally, many people like the "stacked pull/merge request" workflow, where a tool can manage your stack of pull/merge requests for you, including modifying the base branch, description, and other settings. `jj-vine` aims to smooth out the process of managing pull/merge requests for a `jj` repository.

There are several tools these days that aim to solve this problem, most notably [`jj-spr`](https://github.com/LucioFranco/jj-spr) and [`jj-stack`](https://github.com/keanemind/jj-stack). `jj-vine` has its own design choices, and may not be a direct replacement for these tools. Take a look at the differences & features below to see if `jj-vine` is a good fit for you.

Major differences:

- `jj-vine` is bookmark-based, rather than change-based. It expects you to create your bookmarks before submitting them, and (usually) expects you to push them (e.g. `jj git push -c @`) as well. This means that you can have multiple commits in each pull/merge request.
- `jj-spr` aims to more be a full workflow rather than a lightweight tool. `jj-vine` is less opinionated.
- `jj-vine` is primarily based around the `submit --tracked` command. This submits *all* (your) tracked bookmarks at once. Other tools often require you to submit each bookmark individually. The idea is to simply "sync your current state to the code forge".
- Bookmarks are not automatically forwarded. You'll need to use `jj bookmark set` to update the target of a bookmark.

## Main Features{#main-features}

- **Stacked pull/merge request creation**

    Automatically creates pull/merge requests with correct base branches based on bookmark dependencies. Works great for single branches as well.
- **[Stack visualization]#description-generation--stack-visualization**

    Adds a navigable stack diagram to pull/merge request descriptions with links to related pull/merge requests. Can be customized and disabled.
- **Unopinionated**

    `jj-vine` is not opinionated about how you should structure your commits, bookmarks, and pull/merge requests. It can work with what you have. It also works great with auto-generated bookmarks (`jj git push -c <rev>`).
- **[Complex branching]#complex-graph-of-bookmarks**

    Not only does `jj-vine` support trees of bookmarks, but it fully supports complex (DAG) graphs of changes just like `jj` itself does. While actual forge support for PRs/MRs with multiple parents may be limited, `jj-vine` can still visualize and manage them.
- **[Status]#status**

    Can easily report the status of your bookmarks and their pull/merge requests.
- **Automatic syncing**

    Updates pull/merge request base branches & all related descriptions when stack structure changes.

## Planned Features{#planned-features}

- **Automatic rebasing**

    Once a pull/merge request is merged, rebases the stack on top of the trunk
- **Landing**

    Merge a pull/merge request and automatically rebase dependent pull/merge requests on top of the trunk

## Installation{#installation}

### Cargo Binstall{#cargo-binstall}

The preferred way to install `jj-vine` is to use [`cargo-binstall`](https://github.com/cargo-bins/cargo-binstall):

```bash
# Will put the binary in $HOME/.cargo/bin
cargo binstall jj-vine
```

### Mise{#mise}

If you use [mise](https://mise.jdx.dev/), you can install with:

```bash
mise use -g cargo:jj-vine
```

### Pre-built Binaries{#pre-built-binaries}

Pre-built binaries are available for Linux, macOS, and Windows (ARM64 and x86_64 for all). You can download directly from the [releases page](https://codeberg.org/abrenneke/jj-vine/releases).

### Attestations{#attestations}

Binaries are built with GitHub attestations. You may [verify the provenance of a binary](https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/use-artifact-attestations#verifying-artifact-attestations-with-the-github-cli) by running:

```bash
gh attestation verify <binary> -R abrenneke/jj-vine
```

### Alias Setup{#alias-setup}

You can set up a `jj` alias to make it easier to use `jj-vine`. Aliases that work great are `jj pr`, `jj mr`, or `jj vine`. You can run the following command to install an alias:

```bash
# Take your pick:
jj config set --user aliases.pr '["util", "exec", "--", "jj-vine"]'
jj config set --user aliases.mr '["util", "exec", "--", "jj-vine"]'
jj config set --user aliases.vine '["util", "exec", "--", "jj-vine"]'
```

## Quick Start{#quick-start}

1. Run `jj-vine init` to set up your code forge configuration for your repository. This is stored in `.jj/repo/config.toml`. You may also move any configuration settings to the global config file `~/.config/jj/config.toml`.

    ```bash
    jj-vine init
    ```

2. Push up some bookmarks (auto-generated bookmarks work great!)

    ```bash
    jj new main

    jj commit -m "Add feature A"
    # Make some changes
    jj git push -c @-

    jj commit -m "Add feature B"
    # Make some changes
    jj git push -c @-
    ```

3. Submit all tracked bookmarks at once:

    ```bash
    jj-vine submit --tracked
    ```

    This creates two pull/merge requests:

    - `feature-a` targeting `main`
    - `feature-b` targeting `feature-a`

## Commands{#commands}

### `submit`{#submit}

Submit a bookmark and its dependencies as pull/merge requests.

```bash
# Submit a single bookmark or revset (and its dependencies!)
jj-vine submit <revset/bookmark>
jj-vine submit -r <revset/bookmark>

# Submit all tracked bookmarks. Roughly equivalent to `(mine() & tracked_remote_bookmarks()) ~ trunk()`, but has additional stipulations. See `jj-vine submit --help` for more details.
# This is the recommended command to use!
jj-vine submit --tracked

# Preview without making changes
jj-vine submit <options> --dry-run
```

See all options and additional help with `jj-vine submit --help`.

### `init`{#init}

Interactive setup wizard to configure jj-vine for your repository.

```bash
jj-vine init
```

### `status`{#status}

Show the status of tracked bookmarks and their pull/merge requests.

```bash
# Show the status of all my bookmarks
jj-vine status

# Show the status of all tracked bookmarks
jj-vine status --tracked

# Show the status of a specific revset that includes bookmarks
jj-vine status -r <revset>
```

The output will look roughly like this (but with colors):

```bash
!100 "This is the title of the first merge request"
     push-xxxxxxxa • ✓ Checks OK • Needs approval (0/1) • 2 open discussions • 24d 21h old • https://forge-url.example/100

!101 "This is the title of the second merge request"
     push-xxxxxxxb • [READY] • ✓ Checks OK • Approved (1/1) • 16d 18h old • https://forge-url.example/101

!102 "Third title"
     push-xxxxxxxc • ✓ Checks OK • Needs approval (0/1) • 5d 6h old • https://forge-url.example/102

!103 "Fourth merge request title"
     push-xxxxxxxd • ✓ Checks OK • Needs approval (0/1) • 19h old • https://forge-url.example/103

!104 "Fifth title"
     push-xxxxxxxe • ✗ Checks failing • Needs approval (0/1) • 1 open discussion • 19h old • https://forge-url.example/104

wip No merge request

wip-2 No merge request
```

See all options and additional help with `jj-vine status --help`.

## Configuration{#configuration}

Configuration is stored in [jj's configuration system](https://docs.jj-vcs.dev/latest/config/) under the `jj-vine` section. You can use `jj config edit --repo` to edit the configuration for a specific repository or `jj config edit --user` to edit the global configuration. You can also use `jj config set --repo <key> <value>` to set a configuration value for a specific repository:

```bash
jj config set --repo jj-vine.deleteSourceBranch true
jj config set --repo jj-vine.defaultReviewers '["alice", "bob"]'
```

### Forge-Specific Settings{#forge-specific-settings}

All listed settings are under the `jj-vine` section so should be prefixed with `jj-vine.`.

#### Forge{#forge}

Ad a minimum, the `jj-vine.forge` configuration setting must be set to the type of forge you are using.

| Setting | Description | Type | Required | Default |
|---------|-------------|------|----------|---------|
| `forge` | The type of forge you are using | "gitlab" \| "github" \| "forgejo" \| "azure" | Yes | - |

#### GitLab{#gitlab}

Required when `jj-vine.forge` is set to `gitlab`.

| Setting | Description | Type | Required | Default |
|---------|-------------|------|----------|---------|
| `gitlab.host` | GitLab instance URL (e.g., `https://gitlab.example.com`) | String | Yes | - |
| `gitlab.project` | Project ID where branches are pushed (`group/project` or numeric ID like `12345`) | String | Yes | - |
| `gitlab.token` | Personal Access Token with `api` scope | String | Yes | - |
| `gitlab.targetProject` | Target project ID for merge requests (e.g., `upstream/project`). Use if you are using a fork. | String | No | (same as `gitlab.project`) |
| `gitlab.createMergeRequestDependencies` | Whether to create dependencies between merge requests, requiring that all parent merge requests are merged before the child merge request can be merged. | Boolean | No | true |

#### GitHub{#github}

Required when `jj-vine.forge` is set to `github`.

| Setting | Description | Type | Required | Default |
|---------|-------------|------|----------|---------|
| `github.host` | GitHub API URL (defaults to `https://api.github.com` for GitHub.com, or `https://github.example.com/api/v3` for Enterprise) | String | Yes | - |
| `github.project` | Repository where branches are pushed in `owner/repo` format | String | Yes | - |
| `github.token` | Personal Access Token with `repo` scope | String | Yes | - |
| `github.targetProject` | Target repository for pull requests (e.g., `upstream-owner/repo`). Use if you are using a fork. | String | No | (same as `github.project`) |

#### Forgejo/Codeberg/Gitea{#forgejo-codeberg-gitea}

Required when `jj-vine.forge` is set to `forgejo`.

| Setting | Description | Type | Required | Default |
|---------|-------------|------|----------|---------|
| `forgejo.host` | Forgejo/Codeberg/Gitea instance URL (e.g., `https://codeberg.org`) | String | Yes | - |
| `forgejo.project` | Repository where branches are pushed in `owner/repo` format | String | Yes | - |
| `forgejo.token` | API access token with `repo` scope | String | Yes | - |
| `forgejo.targetProject` | Target repository for pull requests (e.g., `upstream-owner/repo`). Use if you are using a fork. | String | No | (same as `forgejo.project`) |
| `forgejo.wipPrefix` | Prefix for WIP/draft pull requests. What counts as a draft pull request is configurable per-repository on Forgejo. | String | No | "WIP: " |

#### Azure DevOps{#azure-devops}

Required when `jj-vine.forge` is set to `azure`.

| Setting | Description | Type | Required | Default |
|---------|-------------|------|----------|---------|
| `azure.host` | Azure DevOps instance URL (e.g., `https://dev.azure.com`) | String | Yes | - |
| `azure.vsspsHost` | Azure DevOps Security (VSSP) host (e.g., `https://vssps.dev.azure.com`). Used to look up other users for automatic review requests. | String | No | - |
| `azure.project` | Organization and project where branches are pushed, formatted as `organization/project` | String | Yes | - |
| `azure.sourceRepositoryName` | Name of the repository in the project where branches are pushed | String | Required if `azure.sourceRepositoryId` is not set | - |
| `azure.sourceRepositoryId` | ID of the repository in the project where branches are pushed | String | Required if `azure.sourceRepositoryName` is not set | - |
| `azure.token` | Personal Access Token | String | Yes | - |
| `azure.targetProject` | Target organization & project for pull requests (e.g., `upstream-organization/project`). Use if you are using a fork. | String | No | (same as `azure.project`) |
| `azure.targetRepositoryName` | Name of the repository in the target project for pull requests | String | Required if `azure.targetRepositoryId` is not set and `azure.targetProject` is different from `azure.project` | - |
| `azure.targetRepositoryId` | ID of the repository in the target project for pull requests | String | Required if `azure.targetRepositoryName` is not set and `azure.targetProject` is different from `azure.project` | - |

### Common Settings{#common-settings}

These settings apply to all forges:

| Setting | Description | Type | Required | Default |
|---------|-------------|------|----------|---------|
| `remoteName` | The remote name to use for pushing and pulling branches | String | No | "origin" |
| `deleteSourceBranch` | Configures the pull/merge request to delete the source branch when merged. Currently has no effect for GitHub and Forgejo (is a repository-level setting and on-merge flag only) | Boolean | No | true |
| `squashCommits` | Configures the pull/merge request to squash commits when merging. Currently has no effect for GitHub and Forgejo (is a repository-level setting and on-merge flag only) | Boolean | No | false |
| `assignToSelf` | Automatically assign created pull/merge requests to yourself. Has no effect for AzureDevOps | Boolean | No | false |
| `defaultReviewers` | List of usernames to automatically add as reviewers when creating pull/merge requests. For Azure DevOps, this should be a list of "user descriptors" and `azure.vsspsHost` must be set | Array<String> | No | [] |
| `caBundle` | Path to CA certificate bundle for custom TLS | String \| null | No | null |
| `tlsAcceptNonCompliantCerts` | Accept non-compliant TLS certificates (for certificates that don't meet strict X.509 standards). This is almost always unnecessary unless you have a unique situation. | Boolean | No | false |
| `defaultBaseBranch` | Default target branch for pull/merge requests into `trunk()` | String | No | (detected automatically using the `trunk()` revset) |
| `openAsDraft` | Open newly created pull/merge requests as drafts | Boolean | No | false |
| `description` | Configuration for pull/merge request description generation | Object (see below) | No | (see below) |

## Description Generation / Stack Visualization{#description-generation-stack-visualization}

If enabled, `jj-vine` can generate both a description/body for a pull/merge request, and a stack diagram to include in the description. The stack diagram
will always be kept in sync with your PR/MR stack upon submitting your bookmark(s). The description generation will only sync if `description.sync` is enabled, otherwise the description generation will only happen when a pull/merge request is first created.

If you would like description generation, but not a stack diagram, you can set each value of `description.diagram` to `none`. Alternatively, if you would
like to only generate a stack diagram, you can set `description.singleRevision` and `description.multipleRevisions` to `none`.

### Configuration{#description-configuration}

| Setting | Description | Type | Required | Default |
|---------|-------------|------|----------|---------|
| `description.enabled` | Whether to enable or disable description generation entirely. If false, pull/merge request descriptions will not be touched | Boolean | No | true |
| `description.sync` | Whether to sync the description of a pull/merge request every time the bookmark is submitted. If this is enabled, any changes you make to the description will be overwritten by the generated description on the next submission. Defaults to false | Boolean | No | false |
| `description.diagram` | How to render the stack diagram for different types of merge request stacks | Object | No | (see next rows) |
| `description.singleRevision` | How to generate the non-stack part of the description for a pull/merge request when there is only one revision in the pull/merge request  | `none` \| `notFirstLine` \| `fullMessage` \| `commitListFirstLine` \| `commitListFull` \| `file(path_to_file)` (see below) | No | `notFirstLine` |
| `description.multipleRevisions` | How to generate the non-stack part of the description for a pull/merge request when there are multiple revisions in the pull/merge request | `none` \| `notFirstLine` \| `fullMessage` \| `commitListFirstLine` \| `commitListFull` \| `file(path_to_file)` (see below) | No | `commitListFull` |
| `description.diagram.single` | How to render a **single** pull/merge request, without any parents or children besides the trunk | `none` \| `linear` \| `tree` | No | `none` |
| `description.diagram.linear` | How to render a **linear** stack of bookmarks. This means that no tracked bookmark has multiple parents or multiple children | `none` \| `linear` \| `tree` | No | `linear` |
| `description.diagram.tree` | How to render a **tree** of bookmarks, where two bookmarks merge into a common parent, but no bookmark has multiple parents | `none` \| `linear` \| `tree` | No | `tree` |
| `description.diagram.complex` | How to render a **complex** (DAG) graph of bookmarks, where any bookmark has multiple parents. Because forges only support a pull/merge request merging into a single parent, in this situation you may see commits of one pull/merge request included in other pull/merge requests | `none` \| `linear` \| `tree` | No | `complex` |

The following sections show examples of the different stack formats.

### Linear Format{#linear-format}

#### Linear/Single Bookmark Stack

(`description.diagram.single = "linear"` and `description.diagram.linear = "linear"`)

This PR is part of a stack containing 5 PRs:

1. `main`
2. [#1]# "Feature A"
3. **"Feature B" (this PR)**
4. [#3]# "Feature C"
5. [#4]# "Feature D"
6. [#5]# "Feature E"

#### Tree Bookmarks

(`description.diagram.tree = "linear"`)

This PR is part of a tree containing 8 PRs:

1. `main`
2. [#1]# "Feature A" → `main`
3. [#4]# "Feature D" → [#1]#
4. [#2]# "Feature B" → [#1]#
5. **"Feature E" (this PR) → [#2]#**
6. [#3]# "Feature C" → [#2]#
7. [#7]# "Feature G" → [#3]#
8. [#8]# "Feature H" → [#7]#
9. [#6]# "Feature F" → [#3]#

#### Complex Graph of Bookmarks

(`description.diagram.complex = "linear"`)

This PR is part of a complex set of PRs containing 10 PRs:

1. `main`
2. [#9]# "Feature I" → `main`
3. [#10]# "Feature J" → [#9]#
4. [#1]# "Feature A" → `main`
5. [#2]# "Feature B" → [#1]#
6. **"Feature E" (this PR) → [#2]#, [#10]#**
7. [#4]# "Feature D" → [#1]#, [#2]#
8. [#3]# "Feature C" → [#2]#
9. [#7]# "Feature G" → [#3]#, [#5]#, [#10]#
10. [#8]# "Feature H" → [#7]#
11. [#6]# "Feature F" → [#3]#, [#9]#

### Tree Format{#tree-format}

#### Linear/Single Bookmark Stack

(`description.diagram.single = "tree"` and `description.diagram.linear = "tree"`)

This PR is part of a stack containing 5 PRs:

- `main`

    - [#1]# "Feature A"

        - **"Feature B" (this PR)**

            - [#3]# "Feature C"

                - [#4]# "Feature D"

                    - [#5]# "Feature E"

#### Tree of Bookmarks

(`description.diagram.tree = "tree"`)

This PR is part of a tree containing 8 PRs:

- `main`

    - [#1]# "Feature A"

        1. [#2](#) "Feature B"

            1. [#3](#) "Feature C"

                1. [#7](#) "Feature G"

                    - [#8]# "Feature H"

                2. [#6](#) "Feature F"

            2. **"Feature E" (this PR)**

        2. [#4](#) "Feature D"

#### Complex Graph of Bookmarks

(`description.diagram.complex = "tree"`)

This PR is part of a complex set of PRs containing 10 PRs:

- `main`

    1. [#9]# "Feature I"

        1. [#10](#) "Feature J"

            1. [#7](#) "Feature G" (→ [#3](#), [#5](#) also)

                - [#8]# "Feature H"

            2. **"Feature E" (this PR) (→ [#2](#) also)**

                - [#7]# "Feature G" (→ [#3]#, [#10]# also)

                    - [#8]# "Feature H"

        2. [#6](#) "Feature F" (→ [#3](#) also)

    2. [#1]# "Feature A"

        1. [#2](#) "Feature B"

            1. **"Feature E" (this PR) (→ [#10](#) also)**

                - [#7]# "Feature G" (→ [#3]#, [#10]# also)

                    - [#8]# "Feature H"

            2. [#3](#) "Feature C"

                1. [#7](#) "Feature G" (→ [#5](#), [#10](#) also)

                    - [#8]# "Feature H"

                2. [#6](#) "Feature F" (→ [#9](#) also)

            3. [#4](#) "Feature D" (→ [#1](#) also)

        2. [#4](#) "Feature D" (→ [#2](#) also)

### Description Generation{#description-generation}

jj-vine can generate a description for a pull/merge request when it is created. Note that this description will not be updated automatically if changes are made later (in case you want to remove it entirely, or change it, etc).

You can configure how the description is generated by setting the `description.singleRevision` and `description.multipleRevisions` settings. `singleRevision` is used when there is only one revision in the pull/merge request, and `multipleRevisions` is used when there are multiple revisions in the pull/merge request.

If `jj-vine.description.enabled` is false, the description will not be generated.

The following options are available:

#### `none`

Do not generate a description in this situation.

#### `notFirstLine`

Take the commit message of the head commit in the branch, trim the first line, and use the rest as the description. This is the default behavior for when there is only one revision in the pull/merge request (because the title of the PR/MR uses the first line of the commit message).

#### `fullMessage`

Use the full commit message of the head commit in the branch as the description.

#### `commitListFirstLine`

Generate a list of all commits in the branch, with their hashes and the first line of each commit message. For example:

- `xxxxxxxa` Head Commit Message
- `xxxxxxxb` Parent 1 Message
- `xxxxxxxc` Parent 2 Message

#### `commitListFull`

Generate a list of all commits in the branch, with their hashes and the full commit messages. This is the default behavior for when there are multiple revisions in the pull/merge request (because the default title of the PR/MR uses the bookmark name). For example:

- `xxxxxxxa` Head Commit Message\
Message line 2\
Message line 3
- `xxxxxxxb` Parent 1 Message\
Message line 2\
\
Message line 3\
Message line 4
- `xxxxxxxc` Parent 2 Message\
Message line 2\
Message line 3

#### `file(path_to_file)`

Include the contents of a file at the given path as the description. This is useful if you use pull request templates. For example, `file(.github/pull_request_template.md)` will include the contents of `.github/pull_request_template.md` as the description. The file path is relative to the root of the repository.

## Title Generation{#title-generation}

`jj-vine` will automatically generate a title for a pull/merge request when it is created. It can also optionally sync the title of a pull/merge request every time the bookmark is submitted (default on).

By default, the title is generated as:

- When there is only one revision in an MR/PR, the first line of the revision description.
- When there are multiple revisions in an MR/PR, the name of the bookmark.

### Configuration{#title-configuration}

The title generation is highly configurable, using the below settings:

| Setting | Description | Type | Required | Default |
|---------|-------------|------|----------|---------|
| `title.singleRevision` | How to generate the title when an MR has only one revision on top of the bookmark's parent(s) | `firstRevisionFirstLine` \| `firstRevisionFullMessage` \| `headRevisionFirstLine` \| `headRevisionFullMessage` \| `bookmarkName` \| (custom template, see below) | No | `firstRevisionFirstLine` |
| `title.syncSingleRevision` | Whether to sync/update the title of a pull/merge request every time the bookmark is submitted, when there is only one revision on top of the bookmark's parent(s). If enabled, this will overwrite any changes you may have manually made to the title. | Boolean | No | true |
| `title.multipleRevisions` | How to generate the title when an MR has multiple revisions on top of the bookmark's parent(s) | `firstRevisionFirstLine` \| `firstRevisionFullMessage` \| `headRevisionFirstLine` \| `headRevisionFullMessage` \| `bookmarkName` \| (custom template, see below) | No | `bookmarkName` |
| `title.syncMultipleRevisions` | Whether to sync/update the title of a pull/merge request every time the bookmark is submitted, when there are multiple revisions on top of the bookmark's parent(s). If enabled, this will overwrite any changes you may have manually made to the title. | Boolean | No | true |

### Custom Title Templates{#custom-title-templates}

You can use custom templates to generate the title of a pull or merge request. While the full power of jj's template language is planned, at the moment you can use any string with the following placeholders replaced with the corresponding values:

| Placeholder | Description |
|-------------|-------------|
| `{first.id}` | The jj commit/revision ID of the first (bottommost) revision in the PR/MR. |
| `{first.change_id}` | The jj change ID of the first (bottommost) revision in the PR/MR. |
| `{first.description}` | The full description of the first (bottommost) revision in the PR/MR. |
| `{first.description_first_line}` | The first line of the description of the first (bottommost) revision in the PR/MR. |
| `{first.description_not_first_line}` | The description of the first (bottommost) revision in the PR/MR, excluding the first line (trimmed). |
| `{head.id}` | The jj commit/revision ID of the head revision in the PR/MR. |
| `{head.change_id}` | The jj change ID of the head revision in the PR/MR. |
| `{head.description}` | The full description of the head revision in the PR/MR. |
| `{head.description_first_line}` | The first line of the description of the head revision in the PR/MR. |
| `{head.description_not_first_line}` | The description of the head revision in the PR/MR, excluding the first line (trimmed). |
| `{stack_index`} | The 1-based index of the revision in the stack. |
| `{stack_count}` | The total number of revisions in the stack. |
| `{bookmark_name}` | The name of the bookmark. |
| `{parent_bookmark_name}` | The name of the parent bookmark (i.e. the target branch). |

Some examples:

- `{bookmark_name}: {head.description_first_line}`

    - feature-a: Add feature A
    - feature-b: Add feature B
- `[{stack_index}/{stack_count}] {first.description_first_line}`

    - [1/2] Add feature A
    - [2/2] Add feature B
- `{stack_index}/N: {head.description_first_line} ({bookmark_name} -> {parent_bookmark_name})`

    - 1/N: Add feature A (feature-a -> main)
    - 2/N: Add feature B (feature-b -> feature-a)
- `{bookmark_name} → {parent_bookmark_name}: {first.description_first_line}`

    - feature-a → main: Add feature A
    - feature-b → feature-a: Add feature B
- `[{head.change_id}] {head.description_first_line}`

    - [kxqpmsyz] Add feature A
    - [rlvkpnkp] Add feature B

## Credits{#credits}

- [`jj-spr`]https://github.com/LucioFranco/jj-spr heavily for inspiration & code approaches
- [`jj-stack`]https://github.com/keanemind/jj-stack heavily for inspiration & code approaches

## FAQs{#faqs}

### Is this vibe-coded slop?{#is-this-vibe-coded-slop}

Don't worry, I berated Claude with profanity until things looked good.

### Ok, but really?{#ok-but-really}

Nah. It may have started out as a test to see how well Claude Code was (conclusion: meh not great), but large swathes of the code has been rewritten by hand at this point. AI-generated code is so verbose and inelegant at times, often 2x the size of the hand-written code that uses Rust best practices. I'm not against AI coding, nor think it will replace developers. Be measured, people.

There are a decent amount of tests, but there could always be more.

### Why a new project?{#why-a-new-project}

Well primarily, existing tools did not support GitLab (though `jj-vine` now supports GitLab, GitHub, Forgejo, and Azure DevOps). `jj-spr` was too heavy-handed - it imposes a strict "one pull request per commit" workflow. `jj-stack` was in TypeScript (nothing against it, but seems sane for a `jj` tool to also be built in Rust). My current `jj` workflow was also just different enough that those existing tools did not fit my needs.

## Contributing{#contributing}

All contributions extremely welcome! Please feel free to open an issue or pull request. See [CONTRIBUTING.djot](./CONTRIBUTING.djot) for more details.

## License{#license}

[MIT License](./LICENSE)