Git: Permission denied por falta del bit de ejecución

Subes unos scripts desde Windows, el servidor Ubuntu los descarga con git pull y ninguno se puede ejecutar:

$ ls -l tools/
-rw-r--r-- 1 deploy deploy  412 oct 11 10:00 build.py
$ ./tools/build.py
bash: ./tools/build.py: Permission denied

Si los lanza systemd u otro agente, el servicio falla al arrancar el proceso (en el estado de systemd suele verse status=203/EXEC). Lo habitual es arreglarlo en el servidor con chmod +x, y entonces aparece el segundo problema: en el siguiente despliegue, git pull se aborta.

error: Your local changes to the following files would be overwritten by merge:
	tools/build.py
Please commit your changes or stash them before you merge.
Aborting

Este artículo explica por qué los archivos llegan sin permiso de ejecución, cómo corregirlo en el repositorio (para que no dependa de nadie en el servidor) y cómo salir del bloqueo del git pull.

Por qué pasa

  • Git solo guarda un bit de permisos. La documentación de git fast-import lista los únicos modos que admite: 100644 (archivo normal, no ejecutable), 100755 (archivo ejecutable), 120000 (enlace simbólico) y 160000 (gitlink). Para un script, la diferencia entre funcionar o no en Linux es el modo 100644 frente a 100755.
  • En Windows el bit no se puede leer del sistema de archivos. Al ejecutar git init o git clone, Git comprueba si el sistema de archivos maneja bien el bit ejecutable y fija core.fileMode en consecuencia (esto lo describe la documentación de git config). En un repositorio nuevo creado en Windows se observa core.fileMode = false. Con ese valor, Git ignora el bit ejecutable y git add registra los archivos nuevos como 100644, incluso si empiezan con una línea #!.
  • En Linux el valor por defecto es true. El git pull del servidor crea los scripts con el modo registrado (100644), es decir, sin permiso de ejecución.
  • Por eso se aborta el git pull después de un chmod manual. Con core.fileMode = true, un cambio de modo cuenta como modificación local. Si el siguiente commit toca esos mismos archivos (por ejemplo, el commit que corrige el modo), Git se niega a sobrescribirlos.

Solución

  1. Confirma el modo registrado. Desde Windows o desde cualquier equipo con el repositorio:

    git ls-files -s tools/
    git ls-tree HEAD tools/

    La primera columna es el modo. Si ves 100644 en los scripts, esa es la causa.

  2. Marca los scripts como ejecutables en el índice. En Git Bash, Linux o macOS, el shell expande el comodín:

    git update-index --chmod=+x tools/*.py

    En PowerShell el comodín no se expande, y git update-index lo trata como un nombre literal y falla con fatal: git update-index: cannot chmod +x 'tools/*.py'. Deja que Git resuelva el patrón:

    git ls-files "tools/*.py" | ForEach-Object { git update-index --chmod=+x $_ }

    Para un archivo que todavía no está en el repositorio, usa git add --chmod=+x tools/nuevo.py. Según la documentación, la opción solo cambia el bit en el índice y deja intactos los archivos en disco; por eso funciona aunque estés en Windows.

  3. Revisa el cambio, haz commit y sube.

    git diff --cached --summary
    git commit -m "fix: marcar scripts como ejecutables"
    git push

    El resumen debe listar una línea mode change 100644 => 100755 por cada script y ninguna modificación de contenido.

  4. Despliega y comprueba en el servidor. Tras el git pull, ls -l tools/ debe mostrar -rwxr-xr-x en los scripts.

  5. Si el servidor tiene cambios de modo locales y el pull se aborta, primero confirma que son solo de modo:

    git diff --summary
    git diff --stat

    El primer comando muestra líneas mode change; el segundo indica 0 inserciones y 0 eliminaciones si no cambió ninguna línea de contenido. Con eso confirmado, tienes dos salidas:

    • Ignorar el modo solo en ese pull (no descarta nada): git -c core.fileMode=false pull. En una prueba con un repositorio desechable, este comando completó el pull que antes se abortaba. Si los modos del servidor coinciden con los que ahora registra el repositorio, git status debe quedar limpio.
    • Restaurar los archivos: git checkout -- .. Esto descarta todos los cambios locales, también los de contenido, así que úsalo solo si git diff --stat no mostró ninguna línea cambiada.
  6. Opcional: una red de seguridad en el script de despliegue.

    git pull --ff-only
    chmod +x tools/*.py

    Mientras el repositorio tenga el modo 100755, ese chmod no cambia nada. Pero si alguien añade un script nuevo como 100644 y el despliegue lo hace ejecutable, el servidor acumula otra vez cambios de modo locales y puede repetirse el bloqueo del pull. Trátalo como un parche temporal y corrige siempre el modo en el repositorio.

Cómo verificar que quedó resuelto

git ls-files -s tools/
git ls-tree HEAD tools/

Ambos deben mostrar 100755. En el servidor, después del pull, ls -l debe mostrar el bit x y git status debe estar limpio. Una prueba de humo que comprueba que cada herramienta existe, es ejecutable y arranca (ajusta la opción a lo que acepten tus scripts):

for f in tools/*.py; do
  [ -x "$f" ] || { echo "No ejecutable: $f"; exit 1; }
  "$f" --help > /dev/null || { echo "Falla: $f"; exit 1; }
done
echo "OK"

Para que no vuelva a ocurrir, puedes añadir esta comprobación a tu integración continua. Falla si algún script del repositorio no está registrado como ejecutable:

if git ls-files -s -- 'tools/*.py' | grep -v '^100755'; then
  echo "Hay scripts sin bit de ejecución"; exit 1
fi

Variantes y errores relacionados

Quitar el bit ejecutable

La opción simétrica es git update-index --chmod=-x archivo.

El script tiene finales de línea CRLF

Es otro problema con síntomas distintos: el error no será Permission denied sino un fallo con el intérprete indicado en la línea #!. Para que los scripts mantengan siempre finales de línea LF, usa .gitattributes con una regla como *.sh text eol=lf; la documentación de gitattributes indica que eol=lf deja en el directorio de trabajo los mismos finales de línea que hay en el índice.

No silencies el problema con core.fileMode = false en el servidor

La documentación lo prevé para sistemas de archivos donde el bit ejecutable no es fiable. En un servidor Linux, desactivarlo oculta cambios de modo reales y no arregla el modo registrado en el repositorio. Para comprobar el valor y de dónde viene, usa git config --show-origin core.fileMode.

Ejecutar con el intérprete

Si no puedes cambiar el repositorio, python3 tools/build.py o bash deploy.sh funcionan sin el bit ejecutable, porque lo que se ejecuta es el intérprete. Solo es un rodeo: las unidades de systemd o los agentes que lanzan el archivo directamente seguirán fallando.

Resumen

  • Git registra solo dos modos para archivos normales: 100644 y 100755; desde Windows los scripts nuevos entran como 100644 porque core.fileMode suele ser false.
  • Corrige el modo en el repositorio: git update-index --chmod=+x (o git add --chmod=+x para archivos nuevos), commit y push.
  • En PowerShell el comodín no se expande: pasa los nombres con git ls-files y ForEach-Object.
  • Si un chmod manual bloquea el git pull, comprueba con git diff --summary que solo hay cambios de modo y usa git -c core.fileMode=false pull.
  • Verifica con git ls-files -s (modo 100755) y con una prueba de humo en el servidor.

Referencias

Comentarios