Lección 0.2.5 · 45 min
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.
- Módulo
- 0.2 · Entorno de trabajo moderno
- Duración
- 45 min
Lecciones del nivel 0
0.1 · Bienvenida y expectativas
0.2 · Entorno de trabajo moderno
- 0.2.1 Instalación de Python con uv: versiones y ciclo de soporte
- 0.2.2 Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles
- 0.2.3 Editores (VS Code, Cursor, PyCharm) y notebooks (Jupyter, JupyterLab, marimo)
- 0.2.4 Terminal básica, rutas, variables de entorno y ficheros .env
- 0.2.5 Git y GitHub: commits, ramas, pull requests y .gitignore
- 0.2.6 Introducción a los asistentes de código con IA: prompts, revisión y tests
0.3 · Fundamentos del lenguaje
- 0.3.1 Variables, tipos y operadores; coma flotante y decimal para precios y ticks
- 0.3.2 Cadenas, f-strings y formato de números y fechas
- 0.3.3 Listas, tuplas, diccionarios, conjuntos y comprensiones
- 0.3.4 Control de flujo: if, elif, else, for, while y match-case
- 0.3.5 Funciones: argumentos, *args y **kwargs, lambdas y ámbito
- 0.3.6 Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15
0.4 · Python intermedio
- 0.4.1 Programación orientada a objetos: clases, herencia, composición y dataclasses
- 0.4.2 Tipado: type hints, mypy o ty y Pydantic para configuraciones
- 0.4.3 Iteradores, generadores, decoradores y context managers
- 0.4.4 Ficheros (CSV, JSON, YAML, TOML) y fechas con zonas horarias
- 0.4.5 Logging profesional y testing con pytest
- 0.4.6 Concurrencia: threading, multiprocessing, asyncio y free-threading
0.5 · Computación numérica y datos
- 0.5.1 NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios
- 0.5.2 pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()
- 0.5.3 Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles
- 0.5.4 Polars: API lazy frente a eager, expresiones, rendimiento y cuándo elegirlo frente a pandas
- 0.5.5 DuckDB y SQL esencial: SELECT, JOIN, GROUP BY y funciones de ventana sobre Parquet
0.6 · Visualización
0.7 · Matemáticas y estadística mínimas
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 logygit diffdicen 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.
Fuente 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:
git config --global user.name "Ana García"
git config --global user.email "[email protected]"
git config --global init.defaultBranch mainEl ciclo diario en el proyecto del curso (el repositorio lo creó uv init en la lección 0.2.2):
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 commitsUn 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 contenido1Nota 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.. Se puede
reproducir en 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"))8cecfba390d4850d0615764693d0b77c1970e220
b021c7bc604f5e715ece3c14f7ff55f77fa7ee59El 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:
# Entorno y cachés (se regeneran)
.venv/
__pycache__/
*.py[oc]
.ipynb_checkpoints/
# Secretos: nunca a Git
.env
# Datos brutos y salidas pesadas
datos/brutos/
*.logLas 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:
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 FalseCon él, los ficheros típicos de un proyecto de trading:
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)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.logPara 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.
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:
Fuente 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):
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 localgh 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:
gh auth login # una vez: autenticarse
git remote add origin https://github.com/<usuario>/ppt-curso.git
git push -u origin mainDeshacer sin miedo
Tener el historial sirve de poco si no sabes volver atrás. Cuatro comandos cubren casi todos los casos:
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 conocido2Nota 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.. 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:
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")línea 2: posible secretoMarca 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.
Ejercicios
- Convierte tu proyecto
ppt-cursoen un repositorio con un primer commit que incluyapyproject.toml,uv.lock,.python-version,README.md,src/y un.gitignorecomo el de la lección. Comprueba congit statusque ni.venv/ni.envaparecen.
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.
- Amplía
PATRONESpara que queden fuera también los informes HTML generados (informes/*.html) y comprueba conignoradoqueinformes/backtest-2026-10.htmlqueda fuera einformes/plantilla.cssno.
Solución
patrones = PATRONES + ["informes/*.html"]
print(ignorado("informes/backtest-2026-10.html", patrones))
print(ignorado("informes/plantilla.css", patrones))True
FalseEl 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.
- 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.
Footnotes
-
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. ↩
-
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. ↩
Respuesta plegada en cada pregunta
Autoevaluación
Has añadido
.enva.gitignore, perogit statussigue mostrando cambios en.env. ¿Por qué?- APorque
.gitignoresolo funciona en GitHub - BPorque
.envya estaba versionado antes de añadirlo a.gitignore: el fichero ignora lo nuevo, no lo que Git ya sigue - CPorque hay que reiniciar el ordenador
- DPorque los ficheros que empiezan por punto no se pueden ignorar
Ver respuesta
Respuesta B. Porque
.envya estaba versionado antes de añadirlo a.gitignore: el fichero ignora lo nuevo, no lo que Git ya sigue.gitignoresolo afecta a los ficheros que Git no sigue todavía. Para dejar de seguir uno ya versionado:git rm --cached .envy commit. Y si tenía claves, ya están en el historial: hay que revocarlas.- APorque
¿Para qué sirve una pull request si trabajas solo en tu repositorio?
- APara nada; solo tiene sentido en equipo
- BPara que el cambio se lea entero y pasen los tests antes de entrar en
main, la rama que debe funcionar siempre - CPara que GitHub haga copia de seguridad
- DPara poder hacer commits
Ver respuesta
Respuesta B. Para que el cambio se lea entero y pasen los tests antes de entrar en
main, la rama que debe funcionar siempreLa 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.
Git identifica cada fichero guardado con un hash SHA-1 de su contenido. ¿Qué implica?
- AQue dos ficheros con el mismo nombre tienen el mismo hash
- BQue el mismo contenido da siempre el mismo identificador y que cualquier cambio, aunque sea de un carácter, da otro
- CQue Git cifra el código
- DQue no se puede recuperar una versión antigua
Ver respuesta
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.
GitHub tiene protección contra la subida de secretos activada por defecto para los repositorios públicos. ¿Basta con eso?
- ASí: GitHub bloquea cualquier clave
- BNo: solo reconoce los tipos de secreto de su lista, y la clave de tu bróker puede no estar en ella; la defensa es
.gitignorey no escribir claves en el código - CSí, si el repositorio es privado
- DNo, porque solo funciona con Python
Ver respuesta
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
.gitignorey no escribir claves en el códigoLa 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é. maindebe funcionar siempre: los cambios se hacen en una rama y entran por pull request, con el diff revisado y los tests en verde..gitignoredeja 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.