Initial commit
This commit is contained in:
425
skills/jj-conflicts/SKILL.md
Normal file
425
skills/jj-conflicts/SKILL.md
Normal file
@@ -0,0 +1,425 @@
|
||||
---
|
||||
name: jj-conflicts
|
||||
description: Help users identify, understand, and resolve conflicts in jj repositories. Use when user mentions 'conflict', 'resolve conflicts', 'merge conflict', 'conflicted commits', '2-sided conflict', or encounters conflict-related errors.
|
||||
allowed-tools:
|
||||
- Bash(jj status:*)
|
||||
- Bash(jj log -r 'conflicts()':*)
|
||||
- Bash(jj resolve:*)
|
||||
- Bash(jj restore:*)
|
||||
- Bash(jj diff:*)
|
||||
- Bash(jj edit:*)
|
||||
---
|
||||
|
||||
# Jj Conflict Resolution
|
||||
|
||||
This skill helps you identify and resolve conflicts in jj repositories, with special emphasis on safely using `jj restore` with path specifications.
|
||||
|
||||
## Understanding Jj Conflicts
|
||||
|
||||
### How Jj Differs from Git
|
||||
|
||||
- **Git**: Conflicts block operations; you must resolve before continuing
|
||||
- **Jj**: Conflicts are stored in commits; you can continue working and resolve later
|
||||
|
||||
When jj encounters a conflict, it:
|
||||
|
||||
1. Marks the commit with a conflict indicator (× in `jj log`)
|
||||
2. Stores conflict markers in the affected files
|
||||
3. Allows you to continue working on descendants
|
||||
|
||||
### Types of Conflicts
|
||||
|
||||
**"2-sided conflict"**: Two versions of the same content that can't be automatically merged
|
||||
|
||||
**"2-sided conflict including 1 deletion"**: One side deleted a file/content, the other modified it
|
||||
|
||||
- Common when adding files to `.gitignore` after they were already tracked
|
||||
- One of the most frequent conflict scenarios
|
||||
|
||||
## Identifying Conflicts
|
||||
|
||||
### Find All Conflicted Commits
|
||||
|
||||
```bash
|
||||
jj log -r 'conflicts()'
|
||||
```
|
||||
|
||||
This shows all commits with unresolved conflicts. Look for the × marker.
|
||||
|
||||
### Check Current Status
|
||||
|
||||
```bash
|
||||
jj status
|
||||
```
|
||||
|
||||
If you're in a conflicted commit, this will show:
|
||||
|
||||
```
|
||||
Warning: There are unresolved conflicts at these paths:
|
||||
.obsidian/workspace.json 2-sided conflict including 1 deletion
|
||||
```
|
||||
|
||||
### Inspect Specific Conflict
|
||||
|
||||
```bash
|
||||
jj edit <conflicted-commit-id>
|
||||
jj diff
|
||||
```
|
||||
|
||||
## Resolution Strategies
|
||||
|
||||
### Strategy A: Using jj resolve (Recommended for Most Cases)
|
||||
|
||||
The `jj resolve` command is purpose-built for conflict resolution:
|
||||
|
||||
```bash
|
||||
# Navigate to conflicted commit
|
||||
jj edit <commit-id>
|
||||
|
||||
# List all conflicts
|
||||
jj resolve --list
|
||||
|
||||
# Accept parent's version (side 1) - "ours"
|
||||
jj resolve --tool :ours <path>
|
||||
|
||||
# Accept child's version (side 2) - "theirs"
|
||||
jj resolve --tool :theirs <path>
|
||||
|
||||
# Use interactive merge tool (if configured)
|
||||
jj resolve <path>
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Most conflict scenarios
|
||||
- When you want semantic clarity (`:ours` vs `:theirs`)
|
||||
- When working with merge tools
|
||||
|
||||
### Strategy B: Using jj restore (Safe When Paths Specified)
|
||||
|
||||
The `jj restore` command can restore files from any commit:
|
||||
|
||||
```bash
|
||||
# Navigate to conflicted commit
|
||||
jj edit <commit-id>
|
||||
|
||||
# Restore SPECIFIC path from parent
|
||||
jj restore --from @- <path>
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
|
||||
- Accepting parent's version for specific files
|
||||
- When you want more control over the source (`--from` can be any revision)
|
||||
- For deletion conflicts (equivalent to `:ours` in resolve)
|
||||
|
||||
### ⚠️ CRITICAL: The Path Argument
|
||||
|
||||
This is the **most important safety rule** when using `jj restore`:
|
||||
|
||||
```bash
|
||||
# ❌ DANGEROUS - Restores ALL files from parent
|
||||
# This will LOSE ALL CHANGES in the current commit!
|
||||
jj restore --from @-
|
||||
|
||||
# ✅ SAFE - Restores ONLY the specified path
|
||||
# All other changes in the commit are preserved
|
||||
jj restore --from @- .obsidian/
|
||||
jj restore --from @- src/config.rs
|
||||
```
|
||||
|
||||
**Why this matters:**
|
||||
|
||||
- Without a path argument, `jj restore` operates on ALL files
|
||||
- With a path argument, it operates ONLY on that specific path
|
||||
- The difference between preserving your work and losing it entirely
|
||||
|
||||
### Strategy C: Manual Editing
|
||||
|
||||
For complex conflicts, you can edit the conflict markers directly:
|
||||
|
||||
```bash
|
||||
jj edit <commit-id>
|
||||
# Edit files with conflict markers
|
||||
# Remove markers and keep desired content
|
||||
jj diff # Verify your resolution
|
||||
```
|
||||
|
||||
Conflict markers look like:
|
||||
|
||||
```
|
||||
<<<<<<<
|
||||
Content from side 1 (parent)
|
||||
%%%%%%%
|
||||
Common ancestor content
|
||||
+++++++
|
||||
Content from side 2 (child)
|
||||
>>>>>>>
|
||||
```
|
||||
|
||||
### Strategy D: New Commit Then Squash (Jj's Recommended Pattern)
|
||||
|
||||
For complex resolutions, jj recommends creating a resolution commit:
|
||||
|
||||
```bash
|
||||
# Create new commit on top of conflicted one
|
||||
jj new <conflicted-commit-id>
|
||||
|
||||
# Resolve using any method above
|
||||
jj resolve --tool :ours <path>
|
||||
|
||||
# Review the resolution
|
||||
jj diff
|
||||
|
||||
# Squash resolution back into parent
|
||||
jj squash
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- Separates resolution from original changes
|
||||
- Easy to review resolution before committing
|
||||
- Can undo resolution easily
|
||||
|
||||
## Common Conflict Scenarios
|
||||
|
||||
### Scenario 1: Parent Deleted, Child Modified
|
||||
|
||||
**Situation:** Parent commit deleted files (e.g., added to `.gitignore`), but child commits still have changes to those files.
|
||||
|
||||
**Example:** You added `.obsidian/` to `.gitignore` and untracked it in commit `oo`, but 13 descendant commits still had `.obsidian/` changes.
|
||||
|
||||
**Conflict message:**
|
||||
|
||||
```
|
||||
.obsidian/workspace.json 2-sided conflict including 1 deletion
|
||||
```
|
||||
|
||||
**Resolution:** Accept the deletion by restoring from parent
|
||||
|
||||
```bash
|
||||
# Method 1: Using jj resolve (more semantic)
|
||||
jj edit <conflicted-commit>
|
||||
jj resolve --tool :ours .obsidian/
|
||||
|
||||
# Method 2: Using jj restore (equally correct)
|
||||
jj edit <conflicted-commit>
|
||||
jj restore --from @- .obsidian/
|
||||
```
|
||||
|
||||
**For multiple commits:**
|
||||
|
||||
```bash
|
||||
# Process each conflicted commit
|
||||
for commit in $(jj log -r 'conflicts()' --no-graph -T 'change_id.short(4)'); do
|
||||
jj edit "$commit"
|
||||
jj restore --from @- .obsidian/
|
||||
done
|
||||
```
|
||||
|
||||
### Scenario 2: Both Sides Modified Same Content
|
||||
|
||||
**Situation:** Both parent and child modified the same lines in a file.
|
||||
|
||||
**Resolution options:**
|
||||
|
||||
1. Accept one side: `jj resolve --tool :ours` or `:theirs`
|
||||
2. Merge manually: Edit conflict markers
|
||||
3. Use merge tool: `jj resolve <path>` (if configured)
|
||||
|
||||
### Scenario 3: Rename Conflicts
|
||||
|
||||
**Situation:** One side renamed a file, the other modified it.
|
||||
|
||||
**Resolution:** Choose which version to keep, potentially applying changes from other side manually.
|
||||
|
||||
## Key Differences: jj restore vs jj resolve
|
||||
|
||||
| Aspect | jj resolve | jj restore --from @- <path> |
|
||||
| -------------------- | --------------------------------- | ------------------------------------------- |
|
||||
| **Purpose** | Conflict resolution | Generic file restoration |
|
||||
| **Semantic clarity** | `:ours`/`:theirs` explicit | Less explicit (must know parent/child) |
|
||||
| **Merge tools** | Supported | Not supported |
|
||||
| **Flexibility** | Limited to conflict resolution | Can restore from any revision |
|
||||
| **Safety** | Only operates on conflicted files | **MUST specify paths** or affects all files |
|
||||
|
||||
**Both are correct for accepting deletions, but resolve is more semantically clear.**
|
||||
|
||||
## Safety Checklist
|
||||
|
||||
Before resolving conflicts:
|
||||
|
||||
- ✅ **Always specify path arguments** when using `jj restore --from`
|
||||
- ✅ **Use `jj diff` to verify changes** before and after resolution
|
||||
- ✅ **Test resolution** with one commit before batch processing
|
||||
- ✅ **Check `jj status`** to confirm conflict is resolved
|
||||
- ❌ **Never use `jj restore --from @-` without paths** unless you intend to reset entire commit
|
||||
|
||||
## Real-World Example: Untracking Previously-Committed Files
|
||||
|
||||
This documents a real scenario that illustrates the critical importance of path specification:
|
||||
|
||||
### The Situation
|
||||
|
||||
1. You added `.obsidian/` to `.gitignore` in commit `oo`
|
||||
2. You untracked `.obsidian/` files in that commit: `jj file untrack .obsidian/`
|
||||
3. 13 descendant commits still contained `.obsidian/` changes
|
||||
4. After rebasing: `jj rebase -r 'oo..@' -d oo`
|
||||
5. Result: All 13 descendant commits now have conflicts
|
||||
|
||||
### The Conflicts
|
||||
|
||||
Each conflict shows:
|
||||
|
||||
```
|
||||
.obsidian/workspace.json 2-sided conflict including 1 deletion
|
||||
```
|
||||
|
||||
This means:
|
||||
|
||||
- Parent (commit `oo`): Deleted `.obsidian/` files
|
||||
- Child commits: Still have changes to `.obsidian/` files
|
||||
|
||||
### The Wrong Approach (What NOT to Do)
|
||||
|
||||
```bash
|
||||
# ❌ WRONG - This was tried first
|
||||
jj edit <commit-id>
|
||||
jj restore --from @- # No path specified!
|
||||
|
||||
# Result: ALL files restored from parent
|
||||
# - All task files: DELETED
|
||||
- All document changes: LOST
|
||||
# - Only .obsidian/ should have been affected, but EVERYTHING was reset
|
||||
```
|
||||
|
||||
**Why this failed:** Without a path argument, `jj restore --from @-` restores **every file** from the parent, effectively undoing all changes in the commit.
|
||||
|
||||
### The Correct Solution
|
||||
|
||||
```bash
|
||||
# ✅ CORRECT - Specify the path
|
||||
jj edit <commit-id>
|
||||
jj restore --from @- .obsidian/ # Path specified!
|
||||
|
||||
# Result: Only .obsidian/ restored from parent
|
||||
# - Task files: PRESERVED ✓
|
||||
# - Document changes: PRESERVED ✓
|
||||
# - .obsidian/ conflicts: RESOLVED ✓
|
||||
```
|
||||
|
||||
**Or using jj resolve (more semantic):**
|
||||
|
||||
```bash
|
||||
jj edit <commit-id>
|
||||
jj resolve --tool :ours .obsidian/
|
||||
```
|
||||
|
||||
### Processing All Conflicts
|
||||
|
||||
```bash
|
||||
# Get list of conflicted commits
|
||||
jj log -r 'conflicts()'
|
||||
|
||||
# Process each one with PATHS SPECIFIED
|
||||
for commit in oymp zzyv knzl xlxr lutt xznz uvnk zosw vzxv utmq xtsk qvot pqnr; do
|
||||
echo "Resolving $commit"
|
||||
jj edit "$commit"
|
||||
jj restore --from @- .obsidian/ # ← The critical path argument
|
||||
done
|
||||
|
||||
# Verify all conflicts resolved
|
||||
jj log -r 'conflicts()' # Should return empty
|
||||
```
|
||||
|
||||
### Key Takeaway
|
||||
|
||||
The difference between these two commands is **losing all your work** vs **safely resolving conflicts**:
|
||||
|
||||
```bash
|
||||
jj restore --from @- # ← Danger: ALL files
|
||||
jj restore --from @- .obsidian/ # ← Safe: ONLY specified path
|
||||
```
|
||||
|
||||
**Always specify the path when resolving conflicts with `jj restore`.**
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Find conflicts
|
||||
|
||||
```bash
|
||||
jj log -r 'conflicts()'
|
||||
jj status
|
||||
```
|
||||
|
||||
### Resolve with jj resolve
|
||||
|
||||
```bash
|
||||
jj edit <commit-id>
|
||||
jj resolve --tool :ours <path> # Accept parent's version
|
||||
jj resolve --tool :theirs <path> # Accept child's version
|
||||
```
|
||||
|
||||
### Resolve with jj restore (MUST SPECIFY PATH)
|
||||
|
||||
```bash
|
||||
jj edit <commit-id>
|
||||
jj restore --from @- <path> # Accept parent's version for PATH ONLY
|
||||
```
|
||||
|
||||
### Verify resolution
|
||||
|
||||
```bash
|
||||
jj diff
|
||||
jj status
|
||||
jj log -r 'conflicts()' # Should not include current commit
|
||||
```
|
||||
|
||||
## Integration with Other Workflows
|
||||
|
||||
### After Rebase
|
||||
|
||||
Rebasing often creates conflicts:
|
||||
|
||||
```bash
|
||||
jj rebase -r <commits> -d <destination>
|
||||
# Check for new conflicts
|
||||
jj log -r 'conflicts()'
|
||||
# Resolve as needed
|
||||
```
|
||||
|
||||
### Before Push
|
||||
|
||||
Always resolve conflicts before pushing:
|
||||
|
||||
```bash
|
||||
# Check for unresolved conflicts
|
||||
jj log -r 'conflicts() & mine()'
|
||||
|
||||
# If any found, resolve them first
|
||||
# Then proceed with push
|
||||
```
|
||||
|
||||
### With jj-spr
|
||||
|
||||
Conflicts can appear when updating stacked PRs:
|
||||
|
||||
```bash
|
||||
jj rebase -d main
|
||||
# Resolve any conflicts
|
||||
jj restore --from @- <conflicted-path>
|
||||
# Update PRs
|
||||
jj spr update
|
||||
```
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Invoke this skill when you encounter:
|
||||
|
||||
- "There are unresolved conflicts at these paths"
|
||||
- × markers in `jj log` output
|
||||
- "2-sided conflict" messages
|
||||
- Questions about using `jj restore` safely
|
||||
- Need to accept parent's or child's version in conflicts
|
||||
- Rebase operations that create conflicts
|
||||
- Files that were added to `.gitignore` after being tracked
|
||||
Reference in New Issue
Block a user