---
phase: 05-file-operation-commands
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - cmd_add.py
  - cmd_commit.py
  - gitsl.py
  - tests/test_cmd_add.py
  - tests/test_cmd_commit.py
autonomous: true

must_haves:
  truths:
    - "git add <files> stages specified files in Sapling repo"
    - "git add -A stages all changes (new, modified, deleted)"
    - "git add --all works the same as -A"
    - "git commit -m 'message' creates commit with that message"
    - "add -> commit workflow results in clean status"
  artifacts:
    - path: "cmd_add.py"
      provides: "Handler for git add command"
      exports: ["handle"]
    - path: "cmd_commit.py"
      provides: "Handler for git commit command"
      exports: ["handle"]
    - path: "gitsl.py"
      provides: "Dispatch for add and commit commands"
      contains: "import cmd_add"
    - path: "tests/test_cmd_add.py"
      provides: "E2E tests for add command"
      min_lines: 40
    - path: "tests/test_cmd_commit.py"
      provides: "E2E tests for commit command"
      min_lines: 30
  key_links:
    - from: "gitsl.py"
      to: "cmd_add.py"
      via: "import and dispatch"
      pattern: "cmd_add\\.handle"
    - from: "gitsl.py"
      to: "cmd_commit.py"
      via: "import and dispatch"
      pattern: "cmd_commit\\.handle"
    - from: "cmd_add.py"
      to: "common.py"
      via: "run_sl() call"
      pattern: "run_sl\\("
---

<objective>
Implement file operation commands: git add and git commit

Purpose: Enable staging and committing files through gitsl, completing the core git workflow (add -> commit -> status)
Output: Two command handlers (cmd_add.py, cmd_commit.py), updated gitsl.py dispatch, E2E tests
</objective>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/05-file-operation-commands/05-RESEARCH.md

# Existing patterns
@gitsl.py
@common.py
@cmd_status.py
@tests/conftest.py
@tests/test_cmd_init.py
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create command handlers for add and commit</name>
  <files>
    - cmd_add.py
    - cmd_commit.py
    - gitsl.py
  </files>
  <action>
Create cmd_add.py with flag translation for -A/--all:

```python
"""Handler for 'git add' command."""

from common import ParsedCommand, run_sl


def handle(parsed: ParsedCommand) -> int:
    """
    Handle 'git add' command.

    Translations:
    - git add <files>  -> sl add <files>
    - git add -A       -> sl addremove
    - git add --all    -> sl addremove
    """
    # Check for -A or --all flag -> translate to addremove
    if "-A" in parsed.args or "--all" in parsed.args:
        # Filter out the -A/--all flag
        filtered_args = [a for a in parsed.args if a not in ("-A", "--all")]
        return run_sl(["addremove"] + filtered_args)

    # Standard add passthrough
    return run_sl(["add"] + parsed.args)
```

Create cmd_commit.py as simple passthrough (follows cmd_status.py pattern):

```python
"""Handler for 'git commit' command."""

from common import ParsedCommand, run_sl


def handle(parsed: ParsedCommand) -> int:
    """
    Handle 'git commit' command.

    Translates to 'sl commit' and passes through all arguments.
    """
    return run_sl(["commit"] + parsed.args)
```

Update gitsl.py:
- Add imports: `import cmd_add` and `import cmd_commit`
- Add dispatch cases before the fallback stub:
  ```python
  if parsed.command == "add":
      return cmd_add.handle(parsed)

  if parsed.command == "commit":
      return cmd_commit.handle(parsed)
  ```
  </action>
  <verify>
Run `python gitsl.py --help` to verify imports work.
Run `GITSL_DEBUG=1 python gitsl.py add test.txt` - should show debug output.
Run `GITSL_DEBUG=1 python gitsl.py add -A` - should show debug output.
Run `GITSL_DEBUG=1 python gitsl.py commit -m "test"` - should show debug output.
  </verify>
  <done>
cmd_add.py exists with -A/--all detection and translation to addremove.
cmd_commit.py exists as passthrough to sl commit.
gitsl.py dispatches add and commit to their handlers.
  </done>
</task>

<task type="auto">
  <name>Task 2: Create E2E tests for add and commit commands</name>
  <files>
    - tests/test_cmd_add.py
    - tests/test_cmd_commit.py
  </files>
  <action>
Create tests/test_cmd_add.py following established patterns (see test_cmd_init.py):

```python
"""
E2E tests for git add command (CMD-02, CMD-08).

Tests:
- git add <files> stages files via sl add
- git add -A stages all changes via sl addremove
- git add --all works same as -A
"""

import shutil
from pathlib import Path

import pytest

from conftest import run_gitsl
from helpers.commands import run_command


sl_available = shutil.which("sl") is not None
pytestmark = pytest.mark.skipif(not sl_available, reason="Sapling (sl) not installed")


class TestAddBasic:
    """CMD-02: git add <files> translates to sl add <files>."""

    def test_add_new_file_succeeds(self, sl_repo: Path):
        """git add stages a new file."""
        new_file = sl_repo / "test.txt"
        new_file.write_text("test content\n")

        result = run_gitsl(["add", "test.txt"], cwd=sl_repo)
        assert result.exit_code == 0

        # Verify with sl status
        status = run_command(["sl", "status"], cwd=sl_repo)
        assert "A test.txt" in status.stdout

    def test_add_multiple_files(self, sl_repo: Path):
        """git add can stage multiple files at once."""
        (sl_repo / "a.txt").write_text("a\n")
        (sl_repo / "b.txt").write_text("b\n")

        result = run_gitsl(["add", "a.txt", "b.txt"], cwd=sl_repo)
        assert result.exit_code == 0

        status = run_command(["sl", "status"], cwd=sl_repo)
        assert "A a.txt" in status.stdout
        assert "A b.txt" in status.stdout


class TestAddAll:
    """CMD-08: git add -A translates to sl addremove."""

    def test_add_all_short_flag(self, sl_repo: Path):
        """git add -A stages new files."""
        (sl_repo / "new.txt").write_text("new\n")

        result = run_gitsl(["add", "-A"], cwd=sl_repo)
        assert result.exit_code == 0

        status = run_command(["sl", "status"], cwd=sl_repo)
        assert "A new.txt" in status.stdout

    def test_add_all_long_flag(self, sl_repo: Path):
        """git add --all works the same as -A."""
        (sl_repo / "new.txt").write_text("new\n")

        result = run_gitsl(["add", "--all"], cwd=sl_repo)
        assert result.exit_code == 0

        status = run_command(["sl", "status"], cwd=sl_repo)
        assert "A new.txt" in status.stdout

    def test_add_all_with_deleted_file(self, sl_repo_with_commit: Path):
        """git add -A marks deleted files for removal."""
        # Delete an existing tracked file
        (sl_repo_with_commit / "README.md").unlink()

        result = run_gitsl(["add", "-A"], cwd=sl_repo_with_commit)
        assert result.exit_code == 0

        status = run_command(["sl", "status"], cwd=sl_repo_with_commit)
        assert "R README.md" in status.stdout or "! README.md" in status.stdout
```

Create tests/test_cmd_commit.py:

```python
"""
E2E tests for git commit command (CMD-03).

Tests:
- git commit -m creates commit with message
- Commit with nothing staged returns non-zero
- Add -> commit workflow results in clean status
"""

import shutil
from pathlib import Path

import pytest

from conftest import run_gitsl
from helpers.commands import run_command


sl_available = shutil.which("sl") is not None
pytestmark = pytest.mark.skipif(not sl_available, reason="Sapling (sl) not installed")


class TestCommitBasic:
    """CMD-03: git commit -m translates to sl commit -m."""

    def test_commit_with_message_succeeds(self, sl_repo: Path):
        """git commit -m creates a commit with the message."""
        # Create and add a file
        (sl_repo / "test.txt").write_text("test\n")
        run_command(["sl", "add", "test.txt"], cwd=sl_repo)

        # Commit via gitsl
        result = run_gitsl(["commit", "-m", "Test commit"], cwd=sl_repo)
        assert result.exit_code == 0

        # Verify commit exists with correct message
        log = run_command(["sl", "log", "--limit", "1"], cwd=sl_repo)
        assert "Test commit" in log.stdout

    def test_commit_nothing_staged_returns_nonzero(self, sl_repo_with_commit: Path):
        """git commit with nothing to commit returns non-zero."""
        result = run_gitsl(["commit", "-m", "Empty"], cwd=sl_repo_with_commit)
        assert result.exit_code != 0


class TestWorkflow:
    """Integration: add -> commit -> status shows clean."""

    def test_add_commit_workflow(self, sl_repo: Path):
        """Full add -> commit workflow results in clean status."""
        # Create file
        (sl_repo / "file.txt").write_text("content\n")

        # Add via gitsl
        add_result = run_gitsl(["add", "file.txt"], cwd=sl_repo)
        assert add_result.exit_code == 0

        # Commit via gitsl
        commit_result = run_gitsl(["commit", "-m", "Add file"], cwd=sl_repo)
        assert commit_result.exit_code == 0

        # Status should be clean
        status = run_command(["sl", "status"], cwd=sl_repo)
        assert status.stdout.strip() == ""
```
  </action>
  <verify>
Run `python -m pytest tests/test_cmd_add.py tests/test_cmd_commit.py -v`
All tests should pass (or skip if sl not installed).
  </verify>
  <done>
tests/test_cmd_add.py exists with tests for CMD-02 (add files) and CMD-08 (add -A).
tests/test_cmd_commit.py exists with tests for CMD-03 (commit -m) and workflow test.
All tests pass when sl is available.
  </done>
</task>

</tasks>

<verification>
Run full test suite:
```bash
python -m pytest tests/ -v
```

Manual verification:
```bash
# Create temp sl repo
cd /tmp && mkdir test-gitsl && cd test-gitsl
sl init
sl config --local ui.username "Test <test@test.com>"

# Test add
echo "test" > file.txt
python /path/to/gitsl.py add file.txt
sl status  # Should show "A file.txt"

# Test commit
python /path/to/gitsl.py commit -m "Add file"
sl log -l1  # Should show "Add file" commit

# Test add -A
echo "new" > new.txt
rm file.txt
python /path/to/gitsl.py add -A
sl status  # Should show new.txt added, file.txt removed
```
</verification>

<success_criteria>
1. `git add <files>` stages files via `sl add` (exit code 0, files appear in status)
2. `git add -A` runs `sl addremove` (stages new, removes deleted)
3. `git add --all` behaves same as `-A`
4. `git commit -m "message"` creates commit with that message
5. Full workflow: add -> commit -> clean status
6. All E2E tests pass
</success_criteria>

<output>
After completion, create `.planning/phases/05-file-operation-commands/05-01-SUMMARY.md`
</output>
