Arregla l'esborrat a Windows: lleva el bit de només-lectura del .git

This commit is contained in:
Sergio Valor Martinez
2026-06-01 12:55:50 +02:00
parent 9147eb56fb
commit 1e530a413d
+29 -2
View File
@@ -10,7 +10,9 @@ import datetime as _dt
import json import json
import os import os
import shutil import shutil
import stat
import subprocess import subprocess
import sys
import urllib.error import urllib.error
import urllib.request import urllib.request
from collections.abc import Callable from collections.abc import Callable
@@ -49,6 +51,31 @@ def _noop(_: str) -> None:
pass pass
def _force_rmtree(path: Path, log: LogFn = _noop) -> None:
"""Esborra un arbre de fitxers, fins i tot amb fitxers de només-lectura.
A Windows els objectes de ``.git`` es creen com a només-lectura i fan que
``shutil.rmtree`` falli amb ``PermissionError``; cal llevar el bit d'escriptura
i reintentar. A diferència de ``ignore_errors=True``, aquí els errors que no
puguem resoldre es registren al log en lloc d'empassar-se en silenci.
"""
if not path.exists():
return
def handle(func: Callable, p: str, _exc: object) -> None:
try:
os.chmod(p, stat.S_IWRITE)
func(p) # reintenta l'operació (unlink/rmdir) que havia fallat
except OSError as exc:
log(f"No s'ha pogut esborrar {p}: {exc}")
# Python 3.12 va renombrar el paràmetre `onerror` a `onexc`; suportem tots dos.
if sys.version_info >= (3, 12):
shutil.rmtree(path, onexc=handle)
else:
shutil.rmtree(path, onerror=handle)
def _auth_args(token: str) -> list[str]: def _auth_args(token: str) -> list[str]:
"""Args -c para autenticar git ante Gitea con un token, sin tocar .git/config.""" """Args -c para autenticar git ante Gitea con un token, sin tocar .git/config."""
if not token: if not token:
@@ -166,7 +193,7 @@ def delete_local(root: Path, game: Game, log: LogFn = _noop) -> None:
log(f"{game.name}: no hi ha res a esborrar") log(f"{game.name}: no hi ha res a esborrar")
return return
log(f"Esborrant la descàrrega local de {game.name}") log(f"Esborrant la descàrrega local de {game.name}")
shutil.rmtree(target, ignore_errors=True) _force_rmtree(target, log)
def download( def download(
@@ -201,7 +228,7 @@ def download(
else: else:
log(f"Clonant {game.name}") log(f"Clonant {game.name}")
if repo.exists(): # carpeta a medias sin .git: limpiarla if repo.exists(): # carpeta a medias sin .git: limpiarla
shutil.rmtree(repo, ignore_errors=True) _force_rmtree(repo, log)
_run_git( _run_git(
["clone", game.clone_url, str(repo)], ["clone", game.clone_url, str(repo)],
None, None,