---
title: "0.2.5 Git y GitHub: commits, ramas y pull requests"
description: "Git y GitHub en un proyecto de trading: commits, ramas, pull requests, un .gitignore sin datos ni claves y qué hacer si se filtra una clave de API."
url: "https://pythonparatrading.com/curso-python-trading/nivel-0/git-y-github-para-trading/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-0/git-y-github-para-trading.png"
---

# 0.2.5 · Git y GitHub: commits, ramas, pull requests y .gitignore

> Git guarda cada versión de tu código y permite saber qué versión produjo cada backtest. GitHub lo guarda fuera de tu ordenador y añade la revisión antes de que un cambio llegue a la rama principal. Esta lección cubre lo que usarás a diario y la regla que no admite excepciones: las claves no se suben nunca.

- Curso: [Nivel 0: Python desde cero y mentalidad de datos](https://pythonparatrading.com/curso-python-trading/nivel-0/)
- Módulo: 0.2 · Entorno de trabajo moderno
- Duración: 45 min
- Requisitos: [0.2.2 · Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles](https://pythonparatrading.com/curso-python-trading/nivel-0/entornos-virtuales-y-dependencias-con-uv/), [0.2.4 · Terminal básica, rutas, variables de entorno y ficheros .env](https://pythonparatrading.com/curso-python-trading/nivel-0/terminal-rutas-y-variables-de-entorno/)

## Al terminar esta lección

- Hacer commits con mensajes útiles y leer el historial de un repositorio desde la terminal.
- Trabajar en una rama, abrirla como pull request en GitHub, revisarla y fusionarla en `main`.
- Escribir un `.gitignore` para un proyecto de trading y comprobar qué ficheros deja fuera.
- Explicar por qué una clave subida a Git está comprometida aunque se borre, y qué pasos seguir si ocurre.

## Por qué versionar un sistema de trading

**Git** es un sistema de control de versiones: guarda cada estado del proyecto que le pidas, con quién lo hizo, cuándo
y por qué. **GitHub** es un servicio que aloja repositorios de Git en internet y añade herramientas para revisar
cambios. Son cosas distintas: Git funciona sin GitHub, en tu ordenador.

Para un proyecto de trading, Git resuelve problemas concretos:

- **¿Qué código produjo este resultado?** Un backtest con Sharpe 1,1 del mes pasado no sirve de nada si no sabes con
  qué versión de la señal se calculó. Con Git, cada resultado se puede ligar a un commit exacto.
- **¿Qué cambió?** El sistema perdía dinero desde el martes. `git log` y `git diff` dicen qué se tocó y cuándo.
- **Volver atrás.** Un error en el código del sistema en real se deshace en segundos volviendo al commit anterior.
- **Probar sin romper.** Una idea nueva se desarrolla en una rama; la versión que opera no se toca hasta que la nueva
  está probada.

El libro de referencia, *Pro Git*, está traducido al español y es gratuito (enlaces en «Recursos»). Esta lección
cubre lo imprescindible.

## Commits: fotos del proyecto

Git trabaja en tres sitios. Los **ficheros** de tu carpeta, que editas; el **área de preparación** (*staging*), donde
eliges qué cambios formarán el siguiente commit; y el **repositorio**, que guarda los commits. Con un remoto en
GitHub, un cuarto: la copia en internet.

```mermaid
flowchart TB
  F[Ficheros] -- git add --> P[Preparación]
  P -- git commit --> R[Repositorio local]
  R -- git push --> G[GitHub]
```

La primera vez, Git necesita saber quién eres (queda en cada commit) y conviene que la rama principal se llame `main`,
como en GitHub:

```bash
git config --global user.name "Ana García"
git config --global user.email "ana@example.com"
git config --global init.defaultBranch main
```

El ciclo diario en el proyecto del curso (el repositorio lo creó `uv init` en la lección 0.2.2):

```bash
git status                       # qué ha cambiado
git diff                         # cómo ha cambiado, línea a línea
git add src/ppt/senales.py tests/test_senales.py
git commit -m "Desplaza la señal un día para operar tras el cierre"
git log --oneline -5             # los cinco últimos commits
```

Un buen mensaje de commit dice **qué cambia y por qué**, en una línea de menos de 72 caracteres, en imperativo o en
presente. «Cambios» o «arreglos» no ayudan cuando buscas, meses después, en qué commit empezó el sistema a perder.
Y un commit debe ser **pequeño**: un cambio lógico (la señal, o los costes, no las dos cosas). Así se puede deshacer
uno sin perder el otro.

¿Cómo identifica Git cada versión? Con un **hash** del contenido, la misma idea de la lección 0.2.2. Para un fichero,
Git calcula el SHA-1 de una cabecera (`blob`, el tamaño y un byte nulo) seguida del contenido[^1]. Se puede
reproducir en Python:

```python
import hashlib

def hash_git(contenido: bytes) -> str:
    cabecera = f"blob {len(contenido)}\0".encode()
    return hashlib.sha1(cabecera + contenido).hexdigest()

print(hash_git(b"posicion = senal.shift(1)\n"))
print(hash_git(b"posicion = senal.shift(2)\n"))
```
```output
8cecfba390d4850d0615764693d0b77c1970e220
b021c7bc604f5e715ece3c14f7ff55f77fa7ee59
```

El primer valor es exactamente el que da `git hash-object` para ese contenido en cualquier máquina. Un carácter
distinto (un desfase de dos días en lugar de uno) da un identificador sin nada en común. Un commit tiene a su vez un
hash calculado a partir de los de sus ficheros, del commit anterior, del autor y del mensaje: por eso el hash de un
commit (`git rev-parse --short HEAD`, siete caracteres bastan) identifica sin ambigüedad el código de un backtest.
Guárdalo junto a sus resultados, como guardaste las versiones del entorno en la lección 0.2.1.

## .gitignore: lo que no se versiona

No todo lo que hay en la carpeta del proyecto debe entrar en Git. Se queda fuera lo que es **secreto** (`.env`), lo
que se **regenera** (`.venv/`, cachés de Python, salidas) y lo que **pesa mucho o no puedes redistribuir** (datos
brutos de un proveedor). El fichero `.gitignore`, en la raíz, lista patrones:

```text title=".gitignore"
# Entorno y cachés (se regeneran)
.venv/
__pycache__/
*.py[oc]
.ipynb_checkpoints/

# Secretos: nunca a Git
.env

# Datos brutos y salidas pesadas
datos/brutos/
*.log
```

Las reglas básicas: `#` es un comentario; `*` es cualquier texto dentro de un nombre; `[oc]` es «o» o «c»; una barra
al final (`.venv/`) indica que el patrón es una carpeta y deja fuera todo lo que contiene; un patrón con una barra en
medio (`datos/brutos/`) se interpreta desde la raíz del repositorio; y un `!` delante vuelve a incluir algo que otro
patrón excluía. La documentación de Git tiene el resto.

Python trae en la biblioteca estándar `fnmatch`, que entiende los comodines de `*`, `?` y `[ ]`. No implementa las
reglas completas de `.gitignore`, pero basta para un comprobador simplificado que ayuda a entender cómo se aplican:

```python
from fnmatch import fnmatch
from pathlib import PurePosixPath

PATRONES = [".venv/", "__pycache__/", "*.py[oc]",
            ".ipynb_checkpoints/", ".env", "datos/brutos/", "*.log"]

def ignorado(ruta: str, patrones: list[str]) -> bool:
    p = PurePosixPath(ruta)
    carpetas = [c for c in p.parents if str(c) != "."]
    for patron in patrones:
        if patron.endswith("/"):                 # carpeta
            patron = patron.rstrip("/")
            for c in carpetas:
                if fnmatch(str(c), patron) or fnmatch(c.name, patron):
                    return True
        elif fnmatch(p.name, patron) or fnmatch(ruta, patron):
            return True
    return False
```

Con él, los ficheros típicos de un proyecto de trading:

```python
ficheros = [
    "src/ppt/senales.py", "tests/test_senales.py",
    "pyproject.toml", "uv.lock", ".env", ".env.example",
    "datos/brutos/sint_diario.csv", "datos/procesados/sint.parquet",
    ".venv/bin/python", "src/ppt/__pycache__/senales.cpython-314.pyc",
    "notebooks/.ipynb_checkpoints/explorar-checkpoint.ipynb",
    "logs/sistema.log",
]
for f in ficheros:
    print("fuera " if ignorado(f, PATRONES) else "a Git ", f)
```
```output
a Git  src/ppt/senales.py
a Git  tests/test_senales.py
a Git  pyproject.toml
a Git  uv.lock
fuera  .env
a Git  .env.example
fuera  datos/brutos/sint_diario.csv
a Git  datos/procesados/sint.parquet
fuera  .venv/bin/python
fuera  src/ppt/__pycache__/senales.cpython-314.pyc
fuera  notebooks/.ipynb_checkpoints/explorar-checkpoint.ipynb
fuera  logs/sistema.log
```

Para estos doce ficheros, el resultado coincide con el de Git. Dos detalles. `.env.example` entra (el patrón `.env`
es exacto): documenta las variables sin sus valores (lección 0.2.4). Y `datos/procesados/` entra con este
`.gitignore`; si los ficheros procesados pesan mucho, añade también esa carpeta. Para la comprobación de verdad, Git
tiene un comando que dice qué regla ignora un fichero: `git check-ignore -v datos/brutos/sint_diario.csv`.

<aside class="callout" data-kind="aviso">

`.gitignore` solo afecta a los ficheros que Git **no sigue todavía**. Si `.env` entró en un commit antes de añadirlo a
`.gitignore`, Git lo seguirá viendo. Hay que dejar de seguirlo con `git rm --cached .env` y hacer commit. Si tenía
claves, además, están en el historial: lee la última sección.

</aside>

## Ramas y pull requests

Una **rama** es una línea de desarrollo independiente. `main` es la versión que funciona: la que ejecutas en papel o en
real. Para probar una idea —añadir costes a la señal, cambiar el tamaño de la posición— se crea una rama, se trabaja
en ella con los commits que haga falta y, cuando está lista y probada, se fusiona (*merge*) en `main`.

En GitHub, la fusión se hace con una **pull request** (solicitud de incorporación de cambios): una página que muestra
todos los cambios de la rama frente a `main`, donde se comentan, se ejecutan los tests y, si todo está bien, se
fusiona con un botón. El ciclo completo:

```mermaid
flowchart TB
  B[Rama desde main] --> C[Commits]
  C --> P[Pull request]
  P --> R{Revisión y tests}
  R -- cambios --> C
  R -- aprobada --> M[Merge en main]
```

En la terminal (con el repositorio ya conectado a GitHub; abajo cómo hacerlo):

```bash
git switch -c costes-en-senal          # rama nueva desde main
# ... editar, probar, commits ...
git push -u origin costes-en-senal     # subir la rama a GitHub
gh pr create --fill                    # abrir la PR (o desde la web)
# tras revisar y fusionar en GitHub:
git switch main
git pull                               # traer main actualizada
git branch -d costes-en-senal          # borrar la rama local
```

`gh` es la herramienta oficial de GitHub para la terminal; todo lo que hace también se puede hacer desde la web.

¿Tiene sentido una pull request si trabajas solo? Sí, por dos razones. La primera: la página de la PR muestra el
**diff completo**, todo lo que cambia junto, y leerlo ahí detecta errores que en el editor se escapan (un `print` de
depuración, un parámetro olvidado, un fichero que no debía estar). La segunda: es el sitio donde se ejecutan los
tests automáticos antes de fusionar (integración continua, en el nivel 7). En GitHub se puede proteger `main` para
que no admita cambios si no es por pull request.

Para conectar el repositorio local con GitHub: crea en github.com un repositorio **privado** vacío (sin README, que
ya tienes uno) y sigue las instrucciones que muestra, que son de este estilo:

```bash
gh auth login                          # una vez: autenticarse
git remote add origin https://github.com/<usuario>/ppt-curso.git
git push -u origin main
```

## Deshacer sin miedo

Tener el historial sirve de poco si no sabes volver atrás. Cuatro comandos cubren casi todos los casos:

```bash
git restore src/ppt/senales.py   # descartar cambios aún sin commit
git restore --staged .env        # sacar un fichero del área de preparación
git revert a1b2c3d               # commit nuevo que deshace el a1b2c3d
git switch --detach a1b2c3d      # ver el proyecto como estaba (solo leer)
```

`git restore` actúa sobre lo que todavía no está en un commit; cuidado, porque los cambios descartados no se
recuperan. `git restore --staged` es el salvavidas si has hecho `git add` de algo que no debía entrar (un `.env`, un
fichero de datos) y aún no has hecho commit.

Para un commit que ya está en `main`, `git revert` crea un commit nuevo con el cambio inverso. No borra nada del
historial, así que es seguro aunque el commit ya esté en GitHub. Es lo que harías si el sistema en real empieza a
comportarse mal tras un cambio: revertir ese commit, desplegar `main` y analizar el error con calma en una rama.

`git switch --detach` lleva el proyecto al estado de un commit antiguo para mirarlo o ejecutarlo. Con el hash
guardado junto a un backtest y `uv sync --locked` (lección 0.2.2), reproduces ese resultado con el mismo código y el
mismo entorno. Para volver: `git switch main`.

## Nunca subir claves API

La regla ya apareció en las lecciones 0.1.3 y 0.2.4. Aquí, el porqué técnico: Git **no olvida**. Si una clave entra en
un commit y en el siguiente borras el fichero, la clave sigue en el commit anterior, y cualquiera con acceso al
repositorio puede verla con `git log -p`. Si el repositorio es público o se hace público algún día, hay que darla por
leída.

GitHub ayuda: la **protección contra el envío de secretos** está activada por defecto en las cuentas personales y
bloquea un `git push` a un repositorio público si detecta un secreto conocido[^2]. Pero solo reconoce los patrones de
su lista de proveedores; la clave de tu bróker puede no estar en ella. La defensa principal sigue siendo tuya. Una
comprobación sencilla, que puedes ejecutar sobre el diff antes de cada commit, busca líneas que asignan un valor a algo
con nombre de secreto:

```python
import re

SECRETO = re.compile(
    r"(api[_-]?key|secret|token|password)\s*[:=]\s*['\"]?[\w-]{8,}",
    re.IGNORECASE,
)

diff = """\
+def conectar():
+    API_KEY = "pk_live_93ab71c0d2e8"
+    cliente = Broker(api_key=os.environ["BROKER_API_KEY"])
+    return cliente
"""
for n, linea in enumerate(diff.splitlines(), start=1):
    if SECRETO.search(linea):
        print(f"línea {n}: posible secreto")
```
```output
línea 2: posible secreto
```

Marca la línea 2, la clave escrita en el código, y no la 3, que la lee del entorno como debe ser. Es un filtro tosco
(dará falsos positivos y se le escaparán formatos raros) y no sustituye a la regla; en el nivel 7 se automatiza con
herramientas específicas que se ejecutan antes de cada commit.

<aside class="callout" data-kind="riesgo">

Si una clave llega a un commit que ha salido de tu ordenador, en este orden: (1) **revócala** en el bróker o el
proveedor y crea otra; (2) revisa la actividad de la cuenta (órdenes, retiradas, accesos); (3) solo después, si
quieres, limpia el historial. Limpiar el historial sin revocar la clave no sirve de nada: puede haberse copiado ya.
Usa claves de cuenta de papel mientras aprendes, sin permiso de retirada de fondos.

</aside>

## Ejercicios

1. Convierte tu proyecto `ppt-curso` en un repositorio con un primer commit que incluya `pyproject.toml`, `uv.lock`,
   `.python-version`, `README.md`, `src/` y un `.gitignore` como el de la lección. Comprueba con `git status` que ni
   `.venv/` ni `.env` aparecen.

**Solución.**

Si `uv init` ya creó el repositorio, basta con `git add .` y `git commit -m "Estructura inicial del curso"` tras
completar el `.gitignore`. Antes del commit, `git status` debe listar solo los ficheros que quieres versionar; si
aparece `.env` o `.venv/`, el `.gitignore` no está en la raíz o tiene una errata. `git check-ignore -v .env` debe
responder con la línea del `.gitignore` que lo excluye.

2. Amplía `PATRONES` para que queden fuera también los informes HTML generados (`informes/*.html`) y comprueba con
   `ignorado` que `informes/backtest-2026-10.html` queda fuera e `informes/plantilla.css` no.

**Solución.**

```python
patrones = PATRONES + ["informes/*.html"]
print(ignorado("informes/backtest-2026-10.html", patrones))
print(ignorado("informes/plantilla.css", patrones))
```
```output
True
False
```

El patrón tiene una barra en medio, así que en Git se aplica desde la raíz: `informes/*.html` no ignora
`src/informes/x.html`. Si los informes se pueden regenerar desde el código y los datos, no hace falta versionarlos.

3. Crea una rama `costes-en-senal`, haz en ella un commit cualquiera, súbela a GitHub y abre una pull request. Antes
   de fusionarla, lee el diff en la pestaña «Files changed». ¿Qué tres cosas compruebas antes de pulsar «Merge»?

**Solución.**

Una lista razonable: que el diff contiene solo lo que pretendías cambiar (ningún fichero de datos, `print` de
depuración ni secreto); que los tests pasan (en tu máquina con `uv run pytest` y, cuando lo configures, en la propia
PR); y que el mensaje y la descripción de la PR explican qué cambia y por qué, para que dentro de seis meses sepas
por qué el sistema empezó a cobrar costes. Después de fusionar: `git switch main` y `git pull`.

[^1]: Pro Git, capítulo «Los entresijos internos de Git: los objetos Git» (git-scm.com/book/es/v2), consultado el 21
    de octubre de 2026.

[^2]: GitHub Docs, «Protección contra el envío de cambios» (docs.github.com/es/code-security), consultada el 21 de
    octubre de 2026: la protección para usuarios está habilitada de forma predeterminada e impide subir secretos
    reconocidos a repositorios públicos.

## Autoevaluación

1. Has añadido `.env` a `.gitignore`, pero `git status` sigue mostrando cambios en `.env`. ¿Por qué?

   - A. Porque `.gitignore` solo funciona en GitHub
   - B. Porque `.env` ya estaba versionado antes de añadirlo a `.gitignore`: el fichero ignora lo nuevo, no lo que Git ya sigue
   - C. Porque hay que reiniciar el ordenador
   - D. Porque los ficheros que empiezan por punto no se pueden ignorar

   *Respuesta B: Porque `.env` ya estaba versionado antes de añadirlo a `.gitignore`: el fichero ignora lo nuevo, no lo que Git ya sigue.* `.gitignore` solo afecta a los ficheros que Git no sigue todavía. Para dejar de seguir uno ya versionado: `git rm --cached .env` y commit. Y si tenía claves, ya están en el historial: hay que revocarlas.

2. ¿Para qué sirve una pull request si trabajas solo en tu repositorio?

   - A. Para nada; solo tiene sentido en equipo
   - B. Para que el cambio se lea entero y pasen los tests antes de entrar en `main`, la rama que debe funcionar siempre
   - C. Para que GitHub haga copia de seguridad
   - D. Para poder hacer commits

   *Respuesta B: Para que el cambio se lea entero y pasen los tests antes de entrar en `main`, la rama que debe funcionar siempre.* La pull request es el punto de control entre una idea y la rama que usa tu sistema. Ver el diff completo y los tests en verde antes de fusionar detecta errores que en el editor se escapan.

3. Git identifica cada fichero guardado con un hash SHA-1 de su contenido. ¿Qué implica?

   - A. Que dos ficheros con el mismo nombre tienen el mismo hash
   - B. Que el mismo contenido da siempre el mismo identificador y que cualquier cambio, aunque sea de un carácter, da otro
   - C. Que Git cifra el código
   - D. Que no se puede recuperar una versión antigua

   *Respuesta B: Que el mismo contenido da siempre el mismo identificador y que cualquier cambio, aunque sea de un carácter, da otro.* El identificador sale del contenido, no del nombre ni de la fecha. Por eso el hash de un commit identifica de forma exacta el código que produjo un resultado.

4. GitHub tiene protección contra la subida de secretos activada por defecto para los repositorios públicos. ¿Basta con eso?

   - A. Sí: GitHub bloquea cualquier clave
   - B. No: solo reconoce los tipos de secreto de su lista, y la clave de tu bróker puede no estar en ella; la defensa es `.gitignore` y no escribir claves en el código
   - C. Sí, si el repositorio es privado
   - D. No, porque solo funciona con Python

   *Respuesta B: No: solo reconoce los tipos de secreto de su lista, y la clave de tu bróker puede no estar en ella; la defensa es `.gitignore` y no escribir claves en el código.* La protección busca patrones de proveedores conocidos. Es una red de seguridad útil, no un sustituto de mantener las claves fuera del repositorio.

## Ideas clave

- Un commit es una foto del proyecto con un identificador que sale de su contenido. Guarda el hash del commit junto a cada resultado de backtest.
- Ciclo diario: `git status`, `git add`, `git commit`, `git push`. Commits pequeños con mensajes que digan qué cambia y por qué.
- `main` debe funcionar siempre: los cambios se hacen en una rama y entran por pull request, con el diff revisado y los tests en verde.
- `.gitignore` deja fuera `.venv/`, `.env`, datos brutos, cachés y logs. Solo afecta a lo que Git no sigue todavía.
- Una clave que ha llegado a un commit está comprometida: se revoca y se crea otra. Borrarla después no la saca del historial.

## Recursos

### Documentación

- [GitHub Docs: acerca de las solicitudes de incorporación de cambios](https://docs.github.com/es/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) · docs.github.com
- [GitHub Docs: protección contra el envío de secretos](https://docs.github.com/es/code-security/secret-scanning/introduction/about-push-protection) · docs.github.com
- [Documentación de Git: gitignore](https://git-scm.com/docs/gitignore) · git-scm.com

### Libros

- [Pro Git (en español): guardar cambios en el repositorio](https://git-scm.com/book/es/v2/Fundamentos-de-Git-Guardando-cambios-en-el-Repositorio) · git-scm.com
- [Pro Git (en español): procedimientos básicos para ramificar y fusionar](https://git-scm.com/book/es/v2/Ramificaciones-en-Git-Procedimientos-B%c3%a1sicos-para-Ramificar-y-Fusionar) · git-scm.com
- [Pro Git (en español): los objetos Git](https://git-scm.com/book/es/v2/Los-entresijos-internos-de-Git-Los-objetos-Git) · git-scm.com

Lección anterior: [0.2.4 · Terminal básica, rutas, variables de entorno y ficheros .env](https://pythonparatrading.com/curso-python-trading/nivel-0/terminal-rutas-y-variables-de-entorno/)

Lección siguiente: [0.2.6 · Introducción a los asistentes de código con IA: prompts, revisión y tests](https://pythonparatrading.com/curso-python-trading/nivel-0/asistentes-de-codigo-con-ia/)
