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