Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Keep this file as always-loaded guidance. Load focused docs only when the task t
- [Testing Guide](agents/testing.md) - Rust/Swift/GPUI test placement, fixtures, UI test rules.
- [Architecture Guide](agents/architecture.md) - MVVM boundaries, file layout, review state, presentation surfaces.
- [Design Guide](agents/design.md) - JayJay product context, visual direction, interaction principles.
- [Pull Request Workflow](agents/pull-requests.md) - bookmark-based GitHub and Codeberg PRs, review updates, landing.

When a change spans multiple areas, load each relevant doc before editing.

Expand Down Expand Up @@ -89,12 +90,15 @@ jj describe -m "message"
jj commit -m "message"
jj squash
jj split --paths FILE -m "msg"
jj edit <rev>
jj bookmark set <name> -r <rev>
jj git fetch
jj git push
jj git push --bookmark <name>
jj fix
```

For PR work, load [Pull Request Workflow](agents/pull-requests.md). Use a pushed bookmark and JayJay's **Pull Request on GitHub** or **Pull Request on Codeberg** action.

Do not use `git commit`, `git add`, `git push`, `git stash`, `git branch`, or `git rebase -i`; use the jj equivalents.

## UI And Design
Expand Down
35 changes: 26 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ This project uses [Jujutsu](https://github.com/jj-vcs/jj) for version control, n

## Requirements

| Dependency | Version |
|------------|---------|
| macOS | 15+ (Sequoia) |
| Rust | 1.85+ |
| Xcode | 16+ |
| jj | latest |
| just | latest |
| xcodegen | latest |
| xcbeautify | latest |
| Dependency | Version | Notes |
|------------|---------|-------|
| macOS | 15+ (Sequoia) | |
| Rust | 1.85+ | |
| Xcode | 16+ | |
| jj | latest | |
| just | latest | |
| xcodegen | latest | |
| xcbeautify | latest | |

## Development loop

Expand All @@ -27,6 +27,23 @@ just build # Build the macOS app
just run # Build and run macOS app
```

## Pull request workflow

Use [agents/pull-requests.md](agents/pull-requests.md) for GitHub and Codeberg PR workflows.

Optional tools:

| Tool | Needed for |
|------|------------|
| gh CLI | GitHub PR status, checks, and optional PR creation |
| GitHub account | GitHub PR creation from pushed bookmarks |
| Codeberg account | Codeberg PR creation from pushed bookmarks |

Summary:

- GitHub: push a jj bookmark, then use JayJay's **Pull Request on GitHub** action or `gh pr create`.
- Codeberg: push a jj bookmark, then use JayJay's **Pull Request on Codeberg** action to open Codeberg's PR compose page.

## Testing

**Every new feature ships with both unit and UI test coverage.** Bug fixes add the regression test that would have caught them.
Expand Down
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[workspace]
members = ["crates/jj-diff", "crates/jj-test", "crates/jayjay-core", "crates/jayjay-uniffi", "crates/jayjay-cli", "shell/gpui"]
members = ["crates/jj-diff", "crates/jj-test", "crates/jayjay-network", "crates/jayjay-core", "crates/jayjay-uniffi", "crates/jayjay-cli", "shell/gpui"]
resolver = "2"

[workspace.package]
Expand All @@ -11,6 +11,9 @@ repository = "https://github.com/hewigovens/jayjay"
[workspace.dependencies]
jj-lib = "0.42.0"
gix = { version = "0.84.0", default-features = false }
gix-url = "0.36.1"
percent-encoding = "2.3.2"
ureq = "3"
uniffi = "0.31"
clap = { version = "4", features = ["derive"] }
thiserror = "2"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Browse your DAG, review side-by-side diffs, resolve conflicts, and run every jj
- Absorb hunks into ancestors, back out (revert) changes
- Git push/fetch with auto-track
- Bookmark Manager (⌘⇧B) with stats, filter, clean up stale branches, resolve conflicts
- Pull Request on GitHub from bookmark right-click (DAG row + Bookmark Manager) — opens the existing PR if one exists, else GitHub's compose URL
- Pull Request on GitHub/Codeberg from bookmark right-click (DAG row + Bookmark Manager) — opens an existing PR when one exists, else a GitHub or Codeberg PR compose URL; public Codeberg PR status is shown via the Forgejo API
- Divergent commit detection and resolution
- Undo via operation log
- Command palette (⌘⇧P) with ~35 commands; type `jj <args>` (or `! <args>`) for inline raw jj CLI output
Expand Down
8 changes: 4 additions & 4 deletions UserGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,14 @@ This guide covers JayJay's user-facing features. The released macOS app uses the
- Use Undo to inspect the jj operation log and roll back recent operations.
- JayJay shows lightweight toasts for completed actions and keeps the rest of the window usable when possible.

## Bookmarks, Git, and GitHub
## Bookmarks, Git, and Pull Requests

- Use the Bookmark Manager with `Cmd+Shift+B` to inspect bookmark stats, filter bookmarks, reveal their changes, copy names, diff them, resolve conflicts, and clean up stale entries.
- Use bookmark actions to create, rename, track, move forward, delete, and push bookmarks.
- Push and fetch Git remotes from JayJay; push can auto-track a bookmark when needed.
- Right-click a bookmark in the DAG or Bookmark Manager to open a GitHub pull request.
- If a PR already exists, JayJay opens it. Otherwise it opens GitHub's PR compose page for that bookmark.
- The status bar can show the selected bookmark's PR link and check status via `gh`.
- Right-click a bookmark in the DAG or Bookmark Manager to open a GitHub or Codeberg pull request.
- If a GitHub PR or public Codeberg PR already exists, JayJay opens it. Otherwise it opens a GitHub or Codeberg PR compose page for that bookmark.
- The status bar can show the selected bookmark's PR link and check status via `gh` for GitHub or Codeberg's public Forgejo API.
- Remote repository URLs can be opened in the browser, including `git@...` URLs converted to HTTPS.

## Conflict Resolution
Expand Down
111 changes: 111 additions & 0 deletions agents/pull-requests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Pull Request Workflow

Load this file before creating, updating, landing, or documenting pull request workflows.

JayJay uses jj bookmarks for pull requests on both GitHub and Codeberg. This keeps the pushed commit identical to the signed jj change, avoids generated PR commits, and matches jj's normal edit-and-rewrite model.

GitHub PR status and checks use `gh`. Public Codeberg PR status and commit statuses use the unauthenticated Forgejo API; private Codeberg repositories are not integrated yet.

## Default Flow

Start from current trunk:

```bash
jj git fetch
jj new main@origin
```

Keep edits in the working-copy change `@` until they are ready to publish:

```bash
jj st
jj diff
jj describe -m "scope: concise change summary"
```

Split by responsibility when the working copy contains more than one logical change:

```bash
jj split --paths <paths-for-one-change> -m "scope: one logical change"
```

Repeat `jj split` until each PR-sized change has one clear purpose. Do not split just to mirror file boundaries; split by behavior, bug fix, or user-visible feature.

Before publishing, format and run the checks that match the change:

```bash
jj fix # run configured formatters (rustfmt, SwiftFormat) over the change
just test
just test-app
just lint
```

Publish the selected change by moving a bookmark to it and pushing that bookmark:

```bash
jj bookmark set <topic> -r @
jj git push --bookmark <topic>
```

Then open the bookmark context menu in JayJay and choose **Pull Request on GitHub** or **Pull Request on Codeberg**. For GitHub, `gh pr create --draft --base main --head <topic>` is also fine when the browser flow is inconvenient.

Use `master@origin` or `trunk@origin` instead of `main@origin` when that is the repository's trunk bookmark.

## Review Updates

Handle review feedback by editing the same change and pushing the same bookmark again:

```bash
jj git fetch
jj edit <topic>

# edit files
jj st
jj diff
jj describe -m "scope: updated summary"
jj fix
just test
just lint

jj bookmark set <topic> -r @
jj git push --bookmark <topic>
```

The remote branch moves as part of normal jj history editing. `jj git push` applies jj's bookmark safety checks, so fetch first if the push reports that the remote bookmark changed.

## Multiple Changes

For independent PRs, use one bookmark per ready change:

```bash
jj bookmark set <topic-a> -r <rev-a>
jj bookmark set <topic-b> -r <rev-b>
jj git push --bookmark <topic-a>
jj git push --bookmark <topic-b>
```

For stacked work, prefer separate bookmarks only when the stack is actually useful for review. Push the base change first, then the dependent change, and set the dependent PR's base branch to the base bookmark in the hosting UI.

## Landing Cleanup

After the PR lands:

```bash
jj git fetch
jj new main@origin
```

If the hosting service deleted the remote branch, forget the local bookmark and its stale remote tracking state:

```bash
jj bookmark forget <topic> --include-remotes
```

If the hosting service did not delete the remote branch, delete it before forgetting it:

```bash
jj bookmark delete <topic>
jj git push --bookmark <topic>
```

Keep the local reviewed change only when it is still useful for follow-up work; otherwise start new work from `main@origin`.
3 changes: 3 additions & 0 deletions crates/jayjay-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ repository.workspace = true
[dependencies]
jj-lib = { workspace = true }
gix = { workspace = true }
gix-url = { workspace = true }
percent-encoding = { workspace = true }
jj-diff = { version = "0.1.0", path = "../jj-diff" }
jayjay-network = { version = "0.1.0", path = "../jayjay-network" }
thiserror = { workspace = true }
pollster = { workspace = true }
chrono = { workspace = true }
Expand Down
62 changes: 61 additions & 1 deletion crates/jayjay-core/src/repo/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ pub use super::commit_ai::{COMMIT_MESSAGE_PROMPT, detect_ai_provider};
use std::io::Write;
use std::process::{Command, Stdio};

use gix_url::Scheme;

use super::commit_ai::generate_commit_message_cli;
use super::{JJ_CONFIG_USER_EMAIL, JJ_CONFIG_USER_NAME, Repo};
use crate::types::*;
Expand Down Expand Up @@ -290,6 +292,12 @@ impl Repo {
Ok(url)
}

/// The origin remote as an https web URL, for "open in browser". None if there's
/// no origin or it can't be parsed.
pub fn remote_web_url(&self) -> Option<String> {
git_remote_to_web_url(&self.git_remote_url().ok()?)
}

/// Fetch all remotes, auto-track, rebase, and clean up merged bookmarks.
pub fn git_fetch(&self, remote: &str) -> CoreResult<FetchResult> {
let tracking_before = self.tracking_bookmark_names();
Expand Down Expand Up @@ -456,9 +464,61 @@ fn parse_git_status_submodule_statuses(stdout: &str) -> Vec<GitSubmoduleStatus>
.collect()
}

/// Normalize a git remote — scp (`git@host:owner/repo`), `ssh://`, `git://`, or
/// `http(s)://` — to its https web URL. None if it can't be parsed.
fn git_remote_to_web_url(raw: &str) -> Option<String> {
let url = gix_url::parse(raw.trim().as_bytes().into()).ok()?;
if !matches!(
&url.scheme,
Scheme::Http | Scheme::Https | Scheme::Ssh | Scheme::Git
) {
return None;
}
let host = url.host()?;
let path = std::str::from_utf8(url.path.as_ref())
.ok()?
.trim_matches('/');
let path = path.strip_suffix(".git").unwrap_or(path);
if path.is_empty() {
return None;
}
Some(format!("https://{host}/{path}"))
}

#[cfg(test)]
mod tests {
use super::{parse_git_check_attr_lfs_paths, parse_git_status_submodule_statuses};
use super::{
git_remote_to_web_url, parse_git_check_attr_lfs_paths, parse_git_status_submodule_statuses,
};

#[test]
fn git_remote_to_web_url_normalizes_every_form() {
let cases = [
(
"ssh://git@codeberg.org/hewig/jj-test.git",
"https://codeberg.org/hewig/jj-test",
),
(
"git@github.com:owner/repo.git",
"https://github.com/owner/repo",
),
(
"git://example.com/owner/repo",
"https://example.com/owner/repo",
),
("https://codeberg.org/o/r.git", "https://codeberg.org/o/r"),
("ssh://git@host:2222/o/r.git", "https://host/o/r"),
];
for (raw, expected) in cases {
assert_eq!(
git_remote_to_web_url(raw).as_deref(),
Some(expected),
"{raw}"
);
}
assert_eq!(git_remote_to_web_url("not-a-remote"), None);
assert_eq!(git_remote_to_web_url(""), None);
}

#[test]
fn parses_git_check_attr_lfs_paths() {
Expand Down
Loading
Loading