{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Encontrar en qué momento se rompió\n",
    "\n",
    "Bisect, que busca por la mitad y resuelve 180 commits en ocho pruebas\n",
    "\n",
    "Cuaderno de soluciones del capítulo 26 de **Git desde cero**, de Miss Yera.\n",
    "\n",
    "Corre de arriba abajo. Si lo abres en Google Colab no necesitas instalar nada.\n",
    "\n",
    "Capítulo completo: https://missyera.com/guias/git-desde-cero/encontrar-el-culpable/\n",
    "\n",
    "Este es el cuaderno de **soluciones**. Trae el código de cada ejercicio, la\n",
    "explicación de la trampa y la respuesta del quiz. Si vienes del cuaderno de\n",
    "práctica sin haberlo intentado, vuelve 🙂"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Antes de empezar\n",
    "\n",
    "Este capítulo son comandos de terminal, no Python. La celda de abajo baja el\n",
    "ayudante que los ejecuta y que **recuerda en qué carpeta quedaste**, que es lo\n",
    "que hace falta para que un `cd` de una celda siga valiendo en la siguiente.\n",
    "\n",
    "A partir de ahí, cada celda de comandos empieza por `%%consola`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import urllib.request\n",
    "\n",
    "# El ayudante de los cuadernos. Trae la corrección de los ejercicios y, en los\n",
    "# capítulos de consola, la celda mágica que ejecuta los comandos. Se baja en\n",
    "# vez de venir pegado aquí para que siempre sea el último.\n",
    "urllib.request.urlretrieve(\n",
    "    \"https://missyera.com/static/cuadernos/revisa.py\", \"revisa.py\")\n",
    "import revisa\n",
    "revisa.carga({}, lenguaje=\"bash\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "\"El reporte de ventas está dando mal el total y no sé desde cuándo.\" Esa\n",
    "frase, en un proyecto con meses de historia, suena a tarde perdida 😩\n",
    "\n",
    "Y se resuelve en cinco minutos, porque Git trae una herramienta que casi\n",
    "nadie usa y que es de las más ingeniosas que tiene."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La idea, con el diccionario"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Para buscar \"mercadería\" en un diccionario no empiezas por la A. Abres por\n",
    "el medio, ves si estás antes o después, y repites. Cada apertura descarta la\n",
    "mitad de lo que queda.\n",
    "\n",
    "Eso es `bisect`. Con 180 commits, la primera prueba descarta 90,\n",
    "la segunda 45, y en ocho pruebas ya no queda ninguno. Con mil commits serían\n",
    "diez 🎯"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Armar el escenario"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un proyecto donde el total de ventas se rompe en algún punto:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "mkdir reporte-ventas\n",
    "cd reporte-ventas\n",
    "git init -q\n",
    "printf 'ciudad,monto\\nLima,1200\\n' > ventas.csv\n",
    "git add ventas.csv\n",
    "git commit -q -m \"Primeras ventas de Lima\"\n",
    "printf 'Arequipa,890\\n' >> ventas.csv\n",
    "git add ventas.csv\n",
    "git commit -q -m \"Entran las ventas de Arequipa\"\n",
    "printf 'Cusco,760\\n' >> ventas.csv\n",
    "git add ventas.csv\n",
    "git commit -q -m \"Entran las ventas de Cusco\"\n",
    "printf 'Trujillo,mil cuatrocientos\\n' >> ventas.csv\n",
    "git add ventas.csv\n",
    "git commit -q -m \"Entran las ventas de Trujillo\"\n",
    "printf 'Piura,980\\n' >> ventas.csv\n",
    "git add ventas.csv\n",
    "git commit -q -m \"Entran las ventas de Piura\"\n",
    "printf 'Chiclayo,1120\\n' >> ventas.csv\n",
    "git add ventas.csv\n",
    "git commit -q -m \"Entran las ventas de Chiclayo\"\n",
    "git log --oneline"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí dentro hay un monto escrito con letras, que es de los errores más\n",
    "comunes cuando el CSV se llena a mano. La prueba de \"está bien\" va a ser que\n",
    "todos los montos sean números:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "cat > revisa.sh <<'FIN'\n",
    "tail -n +2 ventas.csv | cut -d, -f2 | grep -qv '^[0-9]*$' && exit 1\n",
    "exit 0\n",
    "FIN\n",
    "chmod +x revisa.sh\n",
    "if bash revisa.sh; then echo \"resultado: 0, esta bien\"; else echo \"resultado: 1, esta roto\"; fi"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Devuelve 1, o sea que hoy está roto. Y en el primer commit estaba bien."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Bisect a mano"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "git bisect start\n",
    "git bisect bad\n",
    "git bisect good HEAD~5"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Léelo: le dijiste \"aquí está mal\" y \"cinco atrás estaba bien\". Git te dejó\n",
    "parada en el commit del medio y te avisa cuántos pasos faltan 🧭"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "if bash revisa.sh; then echo \"resultado: 0, esta bien\"; else echo \"resultado: 1, esta roto\"; fi\n",
    "git bisect good"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "if bash revisa.sh; then echo \"resultado: 0, esta bien\"; else echo \"resultado: 1, esta roto\"; fi\n",
    "git bisect bad"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está: **el primer commit malo**, con su hash, su mensaje y\n",
    "el cambio exacto que lo causó. Tres pruebas 🎉"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Salir de bisect"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esto no se puede olvidar, porque mientras bisect está activo estás parada en\n",
    "un commit del pasado:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "git bisect reset\n",
    "git log --oneline -1\n",
    "git status --short --branch"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "De vuelta donde estabas. `reset` aquí no borra nada, solo cierra\n",
    "la búsqueda 🔚"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y la versión que se hace sola"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si la prueba se puede escribir como un comando que devuelve 0 cuando está\n",
    "bien, no hace falta que estés delante:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "git bisect start HEAD HEAD~5\n",
    "git bisect run bash revisa.sh"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "git bisect reset\n",
    "git log --oneline -1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Eso es lo que hace que bisect valga la pena de verdad: con una prueba\n",
    "automática, encontrar el culpable entre mil commits es escribir dos líneas e ir\n",
    "por un café ☕"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "El reporte de ventas empezó a dar mal el total y nadie sabe desde cuándo. Hay 180 commits desde la última vez que estuvo bien.\n",
    "\n",
    "```\n",
    "$ git log --oneline | wc -l\n",
    "180\n",
    "\n",
    "# el plan: revisar uno por uno hacia atras\n",
    "$ git switch --detach HEAD~1 && python3 reporte.py\n",
    "$ git switch --detach HEAD~2 && python3 reporte.py\n",
    "$ git switch --detach HEAD~3 && python3 reporte.py\n",
    "```\n",
    "\n",
    "**Qué está mal**\n",
    "\n",
    "A ese ritmo son 180 pruebas y toda la tarde 😩\n",
    "\n",
    "Buscar hacia atrás de uno en uno es la forma lenta. Git trae la forma rápida: `git bisect` parte el rango por la mitad cada vez, así que 180 commits se resuelven en **ocho pruebas**.\n",
    "\n",
    "Es la misma idea de buscar una palabra en un diccionario: no empiezas por la A, abres por el medio.\n",
    "\n",
    "Y lo mejor es que no tienes que acordarte de dónde ibas: bisect lleva la cuenta y te va poniendo en el commit que toca probar."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Comprueba que se entendió"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Hay 1.000 commits entre \"funcionaba\" y \"está roto\". ¿Cuántas pruebas necesita bisect?\n",
    "\n",
    "a) Unas 10\n",
    "\n",
    "b) 1.000, una por commit\n",
    "\n",
    "c) 500, la mitad\n",
    "\n",
    "d) Depende de dónde esté el commit malo\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Esa es la búsqueda hacia atrás de uno en uno, que es justo lo que bisect evita.\n",
    "\n",
    "*c)* La mitad se descarta en la primera prueba, y de lo que queda se descarta la mitad otra vez.\n",
    "\n",
    "*d)* No depende: partir por la mitad tarda lo mismo esté donde esté.\n",
    "\n",
    "Cada prueba descarta la mitad. Por eso mil son diez 🎯"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Arma un proyecto con un error escondido\n",
    "\n",
    "Seis commits del catálogo de bodegas, y en uno se cuela\n",
    "una ciudad vacía."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "cd ..\n",
    "mkdir catalogo\n",
    "cd catalogo\n",
    "git init -q\n",
    "printf 'bodega,ciudad\\nBodega Inti,Cusco\\n' > bodegas.csv\n",
    "git add bodegas.csv\n",
    "git commit -q -m \"Catalogo inicial de bodegas\"\n",
    "printf 'Minimarket Sol,Trujillo\\n' >> bodegas.csv\n",
    "git add bodegas.csv\n",
    "git commit -q -m \"Entra el minimarket de Trujillo\"\n",
    "printf 'Bodega Sur,Arequipa\\n' >> bodegas.csv\n",
    "git add bodegas.csv\n",
    "git commit -q -m \"Entra la bodega de Arequipa\"\n",
    "printf 'Mayorista Norte,\\n' >> bodegas.csv\n",
    "git add bodegas.csv\n",
    "git commit -q -m \"Entra el mayorista del norte\"\n",
    "printf 'Bodega Centro,Lima\\n' >> bodegas.csv\n",
    "git add bodegas.csv\n",
    "git commit -q -m \"Entra la bodega del centro\"\n",
    "printf 'Minimarket Sol 2,Piura\\n' >> bodegas.csv\n",
    "git add bodegas.csv\n",
    "git commit -q -m \"Entra el segundo minimarket\"\n",
    "git log --oneline"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "e8b7d18 Entra el segundo minimarket\n",
    "85a2a3f Entra la bodega del centro\n",
    "43b47ea Entra el mayorista del norte\n",
    "2b5abf3 Entra la bodega de Arequipa\n",
    "4cb519a Entra el minimarket de Trujillo\n",
    "a9874ae Catalogo inicial de bodegas\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Seis commits y una ciudad vacía en alguno."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Escribe la prueba\n",
    "\n",
    "Un script que falla si alguna fila tiene la ciudad\n",
    "vacía."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "cat > revisa.sh <<'FIN'\n",
    "tail -n +2 bodegas.csv | cut -d, -f2 | grep -q '^$' && exit 1\n",
    "exit 0\n",
    "FIN\n",
    "if bash revisa.sh; then echo \"resultado: 0, esta bien\"; else echo \"resultado: 1, esta roto\"; fi"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "resultado: 1, esta roto\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Devuelve 1: hoy está roto."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Arranca la búsqueda\n",
    "\n",
    "Marca dónde está mal y dónde estaba bien."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "git bisect start\n",
    "git bisect bad\n",
    "git bisect good HEAD~5"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "status: waiting for both good and bad commits\n",
    "status: waiting for good commit(s), bad commit known\n",
    "Bisecting: 2 revisions left to test after this (roughly 1 step)\n",
    "[2b5abf3e77c6739952fe60654578c14e415456d4] Entra la bodega de Arequipa\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Git te deja en el commit del medio y te dice cuántos pasos faltan."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Prueba y responde\n",
    "\n",
    "Ejecuta la prueba en el commit donde estás y marca el\n",
    "resultado."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "if bash revisa.sh; then echo \"resultado: 0, esta bien\"; else echo \"resultado: 1, esta roto\"; fi\n",
    "git bisect good"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "resultado: 0, esta bien\n",
    "Bisecting: 0 revisions left to test after this (roughly 1 step)\n",
    "[85a2a3f408afbceecf92f99bd154287cb65e9a55] Entra la bodega del centro\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Bien aquí, así que el culpable está más adelante."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. Termina la búsqueda\n",
    "\n",
    "Una prueba más y ya está."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "if bash revisa.sh; then echo \"resultado: 0, esta bien\"; else echo \"resultado: 1, esta roto\"; fi\n",
    "git bisect bad"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "resultado: 1, esta roto\n",
    "Bisecting: 0 revisions left to test after this (roughly 0 steps)\n",
    "[43b47ea2dd5b2522239b9db905134b42a2cc0c1c] Entra el mayorista del norte\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El primer commit malo, con su hash y su cambio 🎯"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Hazlo automático\n",
    "\n",
    "Sal de la búsqueda y vuelve a hacerla sin estar delante."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "git bisect reset\n",
    "git bisect start HEAD HEAD~5\n",
    "git bisect run bash revisa.sh\n",
    "git bisect reset\n",
    "git log --oneline -1"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "Previous HEAD position was 43b47ea Entra el mayorista del norte\n",
    "Switched to branch 'main'\n",
    "Bisecting: 2 revisions left to test after this (roughly 1 step)\n",
    "[2b5abf3e77c6739952fe60654578c14e415456d4] Entra la bodega de Arequipa\n",
    "running 'bash' 'revisa.sh'\n",
    "Bisecting: 0 revisions left to test after this (roughly 1 step)\n",
    "[85a2a3f408afbceecf92f99bd154287cb65e9a55] Entra la bodega del centro\n",
    "running 'bash' 'revisa.sh'\n",
    "Bisecting: 0 revisions left to test after this (roughly 0 steps)\n",
    "[43b47ea2dd5b2522239b9db905134b42a2cc0c1c] Entra el mayorista del norte\n",
    "running 'bash' 'revisa.sh'\n",
    "43b47ea2dd5b2522239b9db905134b42a2cc0c1c is the first bad commit\n",
    "commit 43b47ea2dd5b2522239b9db905134b42a2cc0c1c\n",
    "Author: Miss Yera <hola@missyera.com>\n",
    "Date:   Thu Jan 15 09:00:00 2026 -0500\n",
    "\n",
    "    Entra el mayorista del norte\n",
    "\n",
    " bodegas.csv | 1 +\n",
    " 1 file changed, 1 insertion(+)\n",
    "bisect found first bad commit\n",
    "Previous HEAD position was 43b47ea Entra el mayorista del norte\n",
    "Switched to branch 'main'\n",
    "e8b7d18 Entra el segundo minimarket\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El mismo resultado sin tocar nada en medio ☕"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. Marca sin haber arrancado\n",
    "\n",
    "Prueba a decir \"bad\" cuando no hay búsqueda abierta."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%consola\n",
    "git bisect bad"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "You need to start by \"git bisect start\"\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Git te recuerda que primero hay que arrancar la búsqueda 🛑"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Bisect parte por la mitad: mil commits son diez pruebas. Y con un\n",
    "script, ninguna.**"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 26 de **Git desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/git-desde-cero/encontrar-el-culpable/\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
