Guía9 min

git read-tree para coding agents: carga el índice, no el disco

Resumen

git read-tree lee un tree al índice y no toca el working tree salvo -u. Plumbing, no porcelain. Un tree reemplaza el index; -m hace merge 1/2/3-way; --reset descarta unmerged; --empty vacía. Cero autónomo con -u/--reset/--prefix. Distinto de checkout, restore y merge-tree. Git 2.50.1.

GitHub
Un tree object entra al índice; el working tree permanece intacto hasta que alguien pide -u

Qué resuelve

Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.

git read-tree escribe el índice. El man (git-read-tree(1); git-scm.com/docs/git-read-tree HTTP 200, last-modified 2026-08-31; pie del man local Git 2.50.1.428.g0e8243, 2025-07-22; binario Git 2.50.1 / Apple Git-155): Reads tree information into the index, but does not actually update any of the files it "caches". SYNOPSIS mezcla --empty, un <tree-ish>, dos trees (fast-forward) y tres trees (3-way) con -m / --reset / --prefix.

Sin -u, el working tree no cambia. El índice sí: un tree reemplaza lo staged. Con -u, además pisa archivos. --reset -u no aborta si eso borra cambios locales o untracked. --empty deja el índice en cero y deja el disco. --prefix mete un tree bajo un directorio y falla si el path ya existe.

No es checkout. checkout mueve HEAD o restaura paths. No es restore: restore pisa paths, no reconstruye el index desde un tree crudo. No es merge-tree: merge-tree fusiona sin tocar índice ni working tree. read-tree es el índice.

Contrato: no corras git read-tree autónomo sobre el índice del humano. Inspección = ls-tree. Preview de merge = merge-tree. Sparse = sparse-checkout, no skip-worktree a mano. Cero -u. Cero --reset. Cero --prefix. Cero --empty. Cero -m de tres trees en el index vivo.

Qué hace (y qué no)

Un <tree-ish>: Reads the tree information given by <tree-ish> into the index. El man apunta a git-checkout-index(1) para materializar archivos. read-tree solo cachea.

-m: Perform a merge, not just a read. Se niega si el índice ya tiene unmerged. Un tree + -m conserva stat() del index cuando el contenido coincide (menos falsos hits en git diff-files). Dos trees = fast-forward $H$M con reglas de “carry forward”. Tres trees = stages 1/2/3 (ancestro, ours, theirs); los paths idénticos colapsan a stage 0. El resto queda unmerged para que el porcelain decida.

--reset: igual que -m, pero descarta unmerged en vez de fallar. Con -u, updates leading to loss of working tree changes or untracked files or directories will not abort.

-n / --dry-run: Check if the command would error out, without updating the index or the files. Sigue siendo un comando de merge plumbing: no es un “ls” seguro.

Verificado 2026-09-06 (Git 2.50.1 / Apple Git-155). Repo basemain cambia f.txt; topic cambia el mismo archivo:

  • git read-tree HEAD^{tree}: 0. git status limpio. Working tree igual.
  • Working tree sucio (changed en f.txt), git read-tree HEAD sin -u: 0. Disco sigue changed. El index vuelve al blob del tree (a).
  • git read-tree --empty: 0. git ls-files cuenta 0. ls del disco sigue con f.txt.
  • Sin args: warning emptying the index with no arguments is deprecated; use --empty. 0. No lo uses: el default vacío es trampa.
  • git read-tree --foo: 129, unknown option `foo'.
  • Fuera de un repo: fatal: not a git repository, 128.
  • Tree inventado deadbeef…: 128, failed to unpack tree object.
  • git read-tree --prefix=nested/ HEAD (path libre): 0. Aparece nested/f.txt en el index.
  • Segundo --prefix=nested/ HEAD: 128, Entry 'nested/f.txt' overlaps with 'nested/f.txt'. Cannot bind.
  • --prefix=./ HEAD: 128, invalid path './f.txt'.
  • 3-way git read-tree -m <base> main topic con conflicto de contenido: 0. git ls-files -s muestra stages 1/2/3 de f.txt. git write-tree entonces 128 (error building trees).
  • --reset -u HEAD con working tree sucio: 0. El archivo en disco vuelve al tree. Los edits locales desaparecen.

Tres modos: un tree al index, merge -m, y --empty que vacía el cache sin tocar disco

Lo que el agente sí / no corre

QuieroComandoTrampa
Ver el treels-tree HEAD^{tree}read-tree “para mirar”
¿Estas ramas fusionan?merge-tree --quiet --write-tree-m de tres trees en el index vivo
Sparse checkoutsparse-checkout coneskip-worktree / info/sparse-checkout a mano
Restaurar un pathrestore -- <path>read-tree -u
Cambiar de ramaswitchcheckout dual o read-tree + update-ref
Índice temporal--index-output en un worktree propio, con confirmación$GIT_INDEX_FILE del humano

Prohibido en autónomo:

  • -u. Materializa el merge/read en el disco. El man lo dice: update the files in the work tree with the result of the merge.
  • --reset y --reset -u. El segundo no aborta ante pérdida de cambios o untracked. Verificado: un f.txt sucio volvió al tree.
  • --empty o cero args. Vacía el index. El disco queda huérfano respecto a Git. El default sin args está deprecated.
  • --prefix. El man: Keep the current index contents, and read the contents of the named tree-ish under <prefix>. Verificado: el segundo bind 128. No es un “vendor copiado”.
  • -m con 2 o 3 trees sobre el index del humano. Un 3-way exit 0 con stages 1/2/3 rompe write-tree (128). El humano se queda a medias.
  • -i. disables the check with the working tree. Sirve para un index temporal. En el index vivo mezcla un merge ajeno con edits locales.
  • --aggressive / --trivial. Cambian qué colapsa a stage 0. Política de porcelain, no de agente.
  • --recurse-submodules. Detacha HEAD de submódulos. No es el ticket.
  • --no-sparse-checkout para “traer todo”. El man: el skip-worktree de read-tree predates git sparse-checkout; users are encouraged to use the sparse-checkout command.
  • Encadenar a checkout-index -f -u -a o a write-tree + commit-tree. El man lo muestra como receta de scripts de más alto nivel. Un agente no mueve refs ni pisa el árbol.
  • Tratar exit 0 de un 3-way como “merge limpio”. Verificado: conflicto de contenido = 0 + unmerged. El 0 solo dice “el index ahora tiene stages”.

Receta (60 segundos)

Si el humano pide “carga este tree en un index aparte” y hay un worktree tuyo:

git rev-parse --verify --quiet "$tree^{tree}"
GIT_INDEX_FILE=/tmp/agent.idx git read-tree -n -- "$tree"
# solo si el humano confirmó el index-output:
# git read-tree --index-output=/tmp/agent.idx -- "$tree"

-n no escribe. --index-output exige el mismo filesystem que el index (man: rename(2)). No uses el path por defecto .git/index.

Si el pedido es “¿mergea?”: no entres a read-tree. Eso es merge-tree. Si el pedido es “muéstrame src/ en ese SHA”: ls-tree, no prefix.

Checklist

  • ¿Hace falta ver un tree? → git ls-tree -r --name-only <tree>.
  • ¿Hace falta probar un merge? → git merge-tree --quiet --write-tree.
  • ¿Hace falta cambiar paths? → git restore -- <path> en un worktree propio.
  • ¿Alguien pidió sparse? → git sparse-checkout, no read-tree.
  • Cero -u, --reset, --empty, --prefix, -m en el index del checkout humano.
  • Cero args = --empty deprecated. No.

FAQ

¿Puedo usarlo como git reset --mixed? No. reset mueve HEAD según el modo. read-tree no mueve HEAD. Un tree al index con HEAD intacto deja el repo en un estado que status describe como staged/unstaged raro. El humano no pidió eso.

¿--dry-run es seguro siempre? Es el único flag que el man promete sin escribir. Sigue parseando trees y reglas de merge. No lo uses como substituto de ls-tree. Un dry-run de 3-way no te da el resumen de conflictos: te da “¿abortaría?”.

¿Por qué el 3-way sale 0 con conflicto? Porque read-tree no resuelve contenido. Only trivial merges are done by git read-tree itself. Only conflicting paths will be in an unmerged state when git read-tree returns. El 0 significa “ya puse stages”. Porcelain (merge-index / merge-file) viene después. Un agente que lea “0 = ok” mentiría.

¿Sparse checkout va aquí? El man dedica una sección: $GIT_DIR/info/sparse-checkout y skip-worktree. También dice que eso predates git sparse-checkout y que el usuario debe preferir el porcelain. No reimplementes cone mode con read-tree.

¿Dónde vive el hub? En comparativas y decisiones. El curso gratis sigue en /curso/instalar-agente.

Barrera: -u, --reset y --prefix quedan fuera del loop autónomo