a73x

specs/010-email-patch-import/tasks.md

Ref:   Size: 10.7 KiB

# Tasks: Email/Format-Patch Import

**Input**: Design documents from `/specs/010-email-patch-import/`
**Prerequisites**: plan.md (required), spec.md (required for user stories)

**Tests**: Included per TDD preference (user memory). Tests written first, must fail before implementation.

**Organization**: Tasks grouped by user story. Primary changes in `src/patch.rs`, `src/cli.rs`, `src/main.rs`, `src/error.rs`.

## Format: `[ID] [P?] [Story] Description`

- **[P]**: Can run in parallel (different files, no dependencies)
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
- Include exact file paths in descriptions

## Phase 1: Setup (Shared Infrastructure)

**Purpose**: Add CLI wiring and error variants for the import subcommand

- [ ] T001 [US1] Add `Import` variant to `PatchCmd` enum in `src/cli.rs` with `file: PathBuf`, `--base` (default "main"), and `--series` flag
- [ ] T002 [P] [US1] Add import-specific error variants to `src/error.rs`: `PatchFileNotFound(PathBuf)`, `InvalidPatchFile(String)`, `PatchApplyConflict(String)`, `BaseBranchNotFound(String)`
- [ ] T003 [US1] Add match arm for `PatchCmd::Import` in `src/main.rs` that delegates to `patch::import()` (stub initially)

**Checkpoint**: Project compiles with `git collab patch import <file>` recognized by clap. Stub returns an error.

---

## Phase 2: User Story 1 - Import a .patch File and Create Patch DAG Entry (Priority: P1)

**Goal**: Parse a `git format-patch` output file, apply it to a temp branch, and create a patch DAG entry via existing `patch::create()` infrastructure.

**Independent Test**: Generate a `.patch` file with `git format-patch`, run `git collab patch import <file>`, verify the patch appears in `git collab patch list` with correct title and head commit OID.

### Tests for User Story 1

> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**

- [ ] T004 [P] [US1] Write integration test `test_import_single_patch` in `tests/patch_import.rs`: create a temp repo, make a commit, generate a `.patch` file with `git format-patch`, import it, verify DAG entry exists with correct title and commit OID
- [ ] T005 [P] [US1] Write integration test `test_import_malformed_file` in `tests/patch_import.rs`: try importing a plain text file, verify error contains "not a valid patch file"
- [ ] T006 [P] [US1] Write integration test `test_import_missing_file` in `tests/patch_import.rs`: try importing a nonexistent path, verify error contains "file not found"
- [ ] T007 [P] [US1] Write integration test `test_import_conflict` in `tests/patch_import.rs`: create a patch against a diverged base, verify error indicates conflict and no DAG entry is created
- [ ] T008 [P] [US1] Write integration test `test_import_custom_base` in `tests/patch_import.rs`: import with `base = "develop"`, verify `base_ref` in DAG entry matches "develop"
- [ ] T009 [P] [US1] Write integration test `test_import_missing_base_branch` in `tests/patch_import.rs`: import with a nonexistent base branch, verify error contains "base branch"
- [ ] T010 [P] [US1] Write integration test `test_import_empty_file` in `tests/patch_import.rs`: import an empty (0-byte) file, verify error contains "not a valid patch file"

### Implementation for User Story 1

- [ ] T011 [US1] Add `patch::parse_patch_file()` helper in `src/patch.rs`: read file from disk, validate `From ` line prefix and `Subject:` header, parse subject (strip `[PATCH]` prefix), parse body (between headers and `---`), extract raw diff content (from `diff --git` onward). Return a struct with `title`, `body`, `diff_bytes`, `author_name`, `author_email`, `author_date`.
- [ ] T012 [US1] Add `patch::apply_patch_to_tree()` helper in `src/patch.rs`: resolve base branch OID via `repo.revparse_single()`, get base commit tree, call `git2::Diff::from_buffer(diff_bytes)`, call `repo.apply_to_tree(&base_tree, &diff, None)` to get new index, write tree via `index.write_tree_to(repo)`, create commit with original author signature and maintainer as committer, parent = base commit. Return the new commit OID.
- [ ] T013 [US1] Add `patch::import()` public function in `src/patch.rs`: validate file exists (return `PatchFileNotFound` if not), validate base branch exists (return `BaseBranchNotFound` if not), call `parse_patch_file()`, call `apply_patch_to_tree()`, create temp branch ref `refs/heads/collab/imported/<short-oid>`, call existing `patch::create(repo, &title, &body, &base, &commit_oid_str)`, print patch ID to stdout. Return `Ok(patch_id)`.
- [ ] T014 [US1] Update match arm in `src/main.rs` to call `patch::import()` with the parsed arguments (replacing the stub from T003)

**Checkpoint**: Single `.patch` file import works end-to-end. All US1 tests pass. `cargo clippy` clean.

---

## Phase 3: User Story 2 - Review Imported Patches Using Existing Review Flow (Priority: P2)

**Goal**: Verify that imported patches are fully compatible with existing review, comment, diff, and merge commands. No new code expected -- this phase is primarily verification.

**Independent Test**: Import a patch, then run `patch::show()`, `patch::review()`, `patch::comment()`, and `patch::merge()` on it.

### Tests for User Story 2

- [ ] T015 [P] [US2] Write integration test `test_imported_patch_show` in `tests/patch_import.rs`: import a patch, call `patch::show()` on it, verify it displays title, author, base, head commit
- [ ] T016 [P] [US2] Write integration test `test_imported_patch_review` in `tests/patch_import.rs`: import a patch, call `patch::review()` with approve verdict, verify review is appended to DAG
- [ ] T017 [P] [US2] Write integration test `test_imported_patch_comment` in `tests/patch_import.rs`: import a patch, call `patch::comment()`, verify comment is appended to DAG
- [ ] T018 [P] [US2] Write integration test `test_imported_patch_merge` in `tests/patch_import.rs`: import a patch, call `patch::merge()`, verify base branch is updated to the imported commit OID

**Checkpoint**: All review flow operations verified on imported patches. No new code needed if tests pass.

---

## Phase 4: User Story 3 - Multi-Patch Series Import with Rollback (Priority: P3)

**Goal**: Import multiple `.patch` files as a sequential series, creating a single patch DAG entry. Roll back on failure.

**Independent Test**: Generate a 3-commit series with `git format-patch -3`, import all three, verify a single patch DAG entry with head commit pointing to the last applied commit.

### Tests for User Story 3

> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**

- [ ] T019 [P] [US3] Write integration test `test_import_series` in `tests/patch_import.rs`: create 3 sequential commits, generate patch files, import with `--series`, verify single DAG entry with head commit = final applied commit
- [ ] T020 [P] [US3] Write integration test `test_import_series_rollback` in `tests/patch_import.rs`: create a series where patch 2 conflicts, verify error, no DAG entry created, no orphan temp branches remain
- [ ] T021 [P] [US3] Write integration test `test_import_series_cover_letter` in `tests/patch_import.rs`: create a series with cover letter, import, verify title comes from cover letter subject and body from cover letter body

### Implementation for User Story 3

- [ ] T022 [US3] Update `Import` variant in `src/cli.rs` to accept multiple files: change `file: PathBuf` to `files: Vec<PathBuf>` (positional args, `num_args = 1..`)
- [ ] T023 [US3] Update match arm in `src/main.rs` to dispatch to `patch::import()` for single file or `patch::import_series()` for `--series` with multiple files
- [ ] T024 [US3] Add `patch::import_series()` function in `src/patch.rs`: sort files by name, detect cover letter (`0000-cover-letter.patch`), extract title/body from cover letter or first patch, apply patches sequentially (each patch's parent = previous commit), on failure delete temp branch and return error with which patch failed, on success create single DAG entry with head_commit = final commit OID

**Checkpoint**: Multi-patch series import works. Rollback on failure verified. All US3 tests pass.

---

## Phase 5: Polish and Edge Cases

**Purpose**: Error message quality, edge cases, final cleanup

- [ ] T025 [P] Ensure `patch::import()` does not modify working tree or current branch (verify FR-007) -- add assertion test in `tests/patch_import.rs`
- [ ] T026 [P] Handle binary diffs gracefully in `src/patch.rs`: if `git2::Diff::from_buffer()` or `apply_to_tree()` fails on binary content, return a clear error message mentioning binary diffs
- [ ] T027 Verify duplicate import creates separate DAG entries (edge case EC-004) -- add test in `tests/patch_import.rs`
- [ ] T028 Run `cargo clippy` and fix any warnings introduced by the new code
- [ ] T029 Run `cargo test` and verify all existing and new tests pass

---

## Dependencies & Execution Order

### Phase Dependencies

- **Setup (Phase 1)**: No dependencies -- can start immediately
- **User Story 1 (Phase 2)**: Depends on Phase 1 -- BLOCKS US2 and US3
- **User Story 2 (Phase 3)**: Depends on Phase 2 (needs working import)
- **User Story 3 (Phase 4)**: Depends on Phase 2 (extends import to series)
- **Polish (Phase 5)**: Depends on all user stories

### Parallel Opportunities

- **Phase 1**: T001 and T002 can run in parallel (different files: `src/cli.rs` vs `src/error.rs`). T003 depends on T001.
- **Phase 2 tests**: T004-T010 can all run in parallel (same test file but independent tests)
- **Phase 2 implementation**: T011-T013 are sequential (each builds on previous). T014 depends on T013.
- **Phase 3 tests**: T015-T018 can all run in parallel
- **Phase 4 tests**: T019-T021 can all run in parallel
- **Phase 4 implementation**: T022-T024 are sequential
- **Phase 5**: T025-T027 can run in parallel

### Within Each User Story

- Tests MUST be written and FAIL before implementation
- Implementation tasks are sequential within a story

---

## Implementation Strategy

### MVP First (User Story 1 Only)

1. Complete Phase 1: Setup (T001-T003)
2. Complete Phase 2: User Story 1 tests + implementation (T004-T014)
3. **STOP and VALIDATE**: Test single-file import end-to-end
4. Deploy/demo if ready

### Incremental Delivery

1. Setup + US1 -> Single patch import works -> Validate
2. Add US2 -> Review flow verified -> Validate
3. Add US3 -> Series import with rollback -> Validate
4. Polish -> Edge cases, cleanup -> Final validation

---

## Notes

- 29 total tasks: 3 setup + 7 US1 tests + 4 US1 impl + 4 US2 tests + 3 US3 tests + 3 US3 impl + 5 polish
- Primary files: `src/patch.rs` (core logic), `src/cli.rs` (CLI wiring), `src/main.rs` (dispatch), `src/error.rs` (error variants), `tests/patch_import.rs` (all tests)
- No new crates required -- `git2::Diff::from_buffer()` and `repo.apply_to_tree()` are available in git2 0.19
- No new `Action` variant needed -- uses existing `Action::PatchCreate`
- TDD approach per user preference: tests first, then implement