{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "cell-23-24-00",
   "metadata": {},
   "source": [
    "# 23-24 · Читаем git diff и группируем изменения\n",
    "\n",
    "Практика к разделу [«Самопроверка изменений и дисциплина коммитов»](../../site/chapters/glava-23/23-28-git-kommit.html)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "setup-23-24",
   "metadata": {},
   "source": [
    "## Reproducible local environment\n",
    "\n",
    "```bash\n",
    "git clone https://github.com/Cartesian-School/safesort.git\n",
    "cd safesort\n",
    "python3.14 -m venv .venv\n",
    "source .venv/bin/activate\n",
    "# Windows PowerShell: .venv\\Scripts\\Activate.ps1\n",
    "python -m pip install -U pip\n",
    "python -m pip install -e \".[dev]\"\n",
    "python -m pip install jupyter ipykernel\n",
    "python -m ipykernel install --user --name safesort-py314 --display-name \"SafeSort Python 3.14\"\n",
    "jupyter lab\n",
    "```\n",
    "\n",
    "Select the **SafeSort Python 3.14** kernel. The diagnostic cell below must\n",
    "point into this `.venv` and the cloned `src/safesort` tree."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "diagnostic-23-24",
   "metadata": {},
   "outputs": [],
   "source": [
    "import sys\n",
    "import safesort\n",
    "\n",
    "print(sys.executable)\n",
    "print(safesort.__file__)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-24-03",
   "metadata": {},
   "source": [
    "## Цель\n",
    "\n",
    "`git diff` не выполняется — его читают. Это упражнение не о запуске кода, а о том, чтобы разобраться, что именно изменилось, и сформулировать по этому изменению короткое, честное commit-сообщение."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-24-04",
   "metadata": {},
   "source": [
    "## Example — результат git diff перед коммитом"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-23-24-05",
   "metadata": {},
   "outputs": [],
   "source": [
    "PRIMER_DIFF = \"\"\"diff --git a/src/safesort/duplicates.py b/src/safesort/duplicates.py\n",
    "index 1a2b3c4..5d6e7f8 100644\n",
    "--- a/src/safesort/duplicates.py\n",
    "+++ b/src/safesort/duplicates.py\n",
    "@@ -40,6 +40,9 @@ def find_duplicates(files, chunk_size=DEFAULT_CHUNK_SIZE):\n",
    "     for size, candidates in by_size.items():\n",
    "         if len(candidates) < 2:\n",
    "             continue\n",
    "+\n",
    "+        if size == 0:\n",
    "+            logger.info(\"Пустые файлы тоже считаются дубликатами: %d штук\", len(candidates))\n",
    "         by_digest = defaultdict(list)\n",
    "diff --git a/tests/test_duplicates.py b/tests/test_duplicates.py\n",
    "index 9f8e7d6..2c3b4a5 100644\n",
    "--- a/tests/test_duplicates.py\n",
    "+++ b/tests/test_duplicates.py\n",
    "@@ -12,3 +12,10 @@ def test_identical_content_files_are_grouped(tmp_path):\n",
    "     assert len(groups) == 1\n",
    "     assert len(groups[0].files) == 2\n",
    "+\n",
    "+\n",
    "+def test_empty_files_are_duplicates_of_each_other(tmp_path):\n",
    "+    (tmp_path / \"a.txt\").write_text(\"\")\n",
    "+    (tmp_path / \"b.txt\").write_text(\"\")\n",
    "+    groups = find_duplicates(scan(tmp_path, Config()))\n",
    "+    assert len(groups) == 1\n",
    "\"\"\"\n",
    "\n",
    "print(PRIMER_DIFF)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-24-06",
   "metadata": {},
   "source": [
    "## Разбираем diff построчно"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-23-24-07",
   "metadata": {},
   "outputs": [],
   "source": [
    "stroki = PRIMER_DIFF.splitlines()\n",
    "\n",
    "izmenennye_fajly = [s.split()[-1][2:] for s in stroki if s.startswith(\"diff --git\")]\n",
    "dobavlennye_stroki = [s for s in stroki if s.startswith(\"+\") and not s.startswith(\"+++\")]\n",
    "udalennye_stroki = [s for s in stroki if s.startswith(\"-\") and not s.startswith(\"---\")]\n",
    "\n",
    "print(\"Изменённые файлы:\", izmenennye_fajly)\n",
    "print(\"Добавлено строк:\", len(dobavlennye_stroki))\n",
    "print(\"Удалено строк:\", len(udalennye_stroki))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-24-08",
   "metadata": {},
   "source": [
    "## Проверка"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cell-23-24-09",
   "metadata": {},
   "outputs": [],
   "source": [
    "assert izmenennye_fajly == [\"src/safesort/duplicates.py\", \"tests/test_duplicates.py\"]\n",
    "assert len(dobavlennye_stroki) > 0\n",
    "assert len(udalennye_stroki) == 0  # в этом diff ничего не удалено, только добавлено\n",
    "print(\"Верно: diff затронул два файла, и в нём только добавления.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-24-10",
   "metadata": {},
   "source": [
    "## Starter\n",
    "\n",
    "Заполните отмеченное место. Неизменённый starter не проходит tests."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "task-23-24",
   "metadata": {
    "tags": [
     "exercise",
     "starter"
    ]
   },
   "outputs": [],
   "source": [
    "moe_commit_soobshenie = \"\"\n",
    "# TODO: write a specific message with an accepted prefix.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-24-12",
   "metadata": {},
   "source": [
    "## Task\n",
    "\n",
    "Сформулируйте логическое commit message для изменения теста нулевых файлов. Не используйте `update` или `fix stuff`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-24-13",
   "metadata": {},
   "source": [
    "## Tests\n",
    "\n",
    "Запустите после task cell: есть основной пример и хотя бы один крайний случай."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "tests-23-24",
   "metadata": {
    "tags": [
     "exercise-tests"
    ]
   },
   "outputs": [],
   "source": [
    "dopustimye_prefiksy = (\"feat:\", \"fix:\", \"test:\", \"docs:\", \"refactor:\", \"chore:\")\n",
    "assert moe_commit_soobshenie.startswith(dopustimye_prefiksy)\n",
    "assert len(moe_commit_soobshenie.split()) >= 4\n",
    "assert moe_commit_soobshenie.lower() not in {\"update\", \"fix\", \"fix stuff\"}\n",
    "print(\"Tests passed\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-24-15",
   "metadata": {},
   "source": [
    "## Hint\n",
    "\n",
    "Сообщите не факт редактирования, а проверяемое изменение поведения, например `test: ...`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cell-23-24-16",
   "metadata": {},
   "source": [
    "## Solution\n",
    "\n",
    "<details><summary>Показать решение после собственной попытки</summary>\n",
    "\n",
    "```python\n",
    "moe_commit_soobshenie = \"feat: treat zero-byte files as duplicates of each other\"\n",
    "\n",
    "dopustimye_prefiksy = (\"feat:\", \"fix:\", \"test:\", \"docs:\", \"refactor:\", \"chore:\")\n",
    "\n",
    "assert moe_commit_soobshenie.strip() != \"\"\n",
    "assert moe_commit_soobshenie.startswith(dopustimye_prefiksy)\n",
    "print(\"Верно:\", moe_commit_soobshenie)\n",
    "```\n",
    "\n",
    "</details>"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Cartesian Python 3.14",
   "language": "python",
   "name": "cartesian-python314"
  },
  "language_info": {
   "name": "python",
   "version": "3.14.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
