edit to contain long filepath names

This commit is contained in:
Lenrui committed 2026-11-07 12:12:44 -08:00
1 parent 41b2f67c4a
commit 3ba13a64ee
2 files changed
+142 -1

No files matched your search

+135 -1
View File
@@ -165,6 +165,7 @@ class Result:
out: str = ""
err: str = ""
lockish: bool = False # failed because another program had the repository locked
longpath_fixed: bool = False # hit "Filename too long", and core.longpaths was just turned on
handled: bool = False # the user has already been shown this failure
@property
@@ -220,8 +221,14 @@ def user_ssh_command_set(path: str) -> bool:
def hint_for(message: str) -> str:
"""Plain-English next step for common git errors."""
if "Filename too long" in message:
if LONGPATHS["on"]:
return ("Long path support (core.longpaths) is on, so this path is too long for something else - "
"a folder name may be over 255 characters, or another program can't handle it. Moving the "
"repository to a shorter folder (e.g. C:\\dev\\) usually helps.")
return ("Windows limits paths to 260 characters unless git is told otherwise. Run this once, "
"then try again:\n git config --global core.longpaths true")
if "File name too long" in message:
return "A file or folder name is longer than this file system allows (usually 255 characters)."
if "Please tell me who you are" in message:
return ("git needs your name and email for commits. Run these once:\n"
' git config --global user.name "Your Name"\n'
@@ -434,7 +441,75 @@ class GitError(Exception):
pass
def run_git(cwd: str, args, input: Optional[str] = None, env=None) -> Result:
# ---- Windows long paths ------------------------------------------------------
# Windows limits paths to 260 characters unless git is told to work around it.
# When git says "Filename too long", gitdesk turns on core.longpaths (globally),
# re-runs read-only commands straight away, and lets gitop() recover the rest.
AUTO_LONGPATHS = IS_WIN
LONGPATH_RE = re.compile(r"Filename too long", re.I)
UNWRITTEN_RE = re.compile(r"unable to create file (.+?): Filename too long"
r"|cannot create directory at '(.+?)': Filename too long", re.I)
LONGPATHS = {"on": False, "just_enabled": False}
READ_ONLY_COMMANDS = {"status", "diff", "log", "ls-files", "show", "rev-parse", "rev-list", "for-each-ref",
"remote", "check-ignore", "cat-file", "config"}
def _subcommand(args) -> Tuple[str, List[str]]:
i = 0
while i < len(args):
a = args[i]
if a == "-c":
i += 2
elif a.startswith("-"):
i += 1
else:
return a, list(args[i + 1:])
return "", []
def is_read_only(args) -> bool:
sub, rest = _subcommand(args)
if sub == "stash":
return bool(rest) and rest[0] in ("list", "show")
return sub in READ_ONLY_COMMANDS
def enable_longpaths() -> bool:
"""Run `git config --global core.longpaths true`. True if it was off and is now on."""
if LONGPATHS["on"]:
return False
home = os.path.expanduser("~")
current = run_git(home, ["config", "--global", "--get", "core.longpaths"], longpath_check=False)
if current.out.strip().lower() in ("true", "yes", "on", "1"):
LONGPATHS["on"] = True # already on - the problem is something else
return False
r = run_git(home, ["config", "--global", "core.longpaths", "true"], longpath_check=False)
if r.ok:
LONGPATHS.update(on=True, just_enabled=True)
return True
return False
def long_path_exists(path: str) -> bool:
"""os.path.exists that also works for paths over 260 characters on Windows."""
if IS_WIN:
full = os.path.abspath(path)
if not full.startswith("\\\\?\\"):
full = "\\\\?\\" + full
return os.path.exists(full)
return os.path.exists(path)
def run_git(cwd: str, args, input: Optional[str] = None, env=None, longpath_check: bool = True) -> Result:
r = _run_git_once(cwd, args, input, env)
if longpath_check and AUTO_LONGPATHS and LONGPATH_RE.search(f"{r.err}\n{r.out}") and enable_longpaths():
if is_read_only(args): # safe to just ask again
r = _run_git_once(cwd, args, input, env)
r.longpath_fixed = True
return r
def _run_git_once(cwd: str, args, input: Optional[str] = None, env=None) -> Result:
cmd = ["git", "-c", "core.quotepath=false", "-c", "color.ui=false", *args]
kw = dict(cwd=cwd, capture_output=True, text=True, encoding="utf-8",
errors="replace", env=env or GIT_ENV)
@@ -544,6 +619,22 @@ class Git:
r = self.run("status", "--porcelain=v2", "--branch", "-z", "-uall", log=False)
return (r.out, self.stash_count(), os.path.exists(os.path.join(self.git_dir, "MERGE_HEAD")))
def restore_unwritten(self, r: Result) -> List[str]:
"""Re-create files git couldn't write because their paths were too long.
Only paths that don't exist on disk are touched, so nothing of the user's
is overwritten. Returns the paths restored.
"""
paths = []
for m in UNWRITTEN_RE.finditer(f"{r.err}\n{r.out}"):
p = (m.group(1) or m.group(2) or "").strip().strip("'\"")
if p and p not in paths and not long_path_exists(os.path.join(self.path, p)):
paths.append(p)
if not paths:
return []
rr = self.run("--literal-pathspecs", "restore", "--worktree", "--", *paths[:500])
return paths if rr.ok else []
def index_lock_path(self) -> str:
return os.path.join(self.git_dir, "index.lock")
@@ -1289,6 +1380,8 @@ def create_repository(o: dict) -> Tuple[bool, str]:
return True, "Couldn't add the remote: " + r.message
if wrote:
r = run_git(path, ["add", "-A"])
if not r.ok and r.longpath_fixed: # long paths were just turned on - try again
r = run_git(path, ["add", "-A"])
if r.ok:
r = run_git(path, ["commit", "-q", "-m", "Initial commit"])
if not r.ok:
@@ -2652,6 +2745,7 @@ class GitDesk(App):
if g is not self.git:
return
self.state = st
self.announce_longpaths()
try:
await self.apply_state(st)
except NoMatches: # app is shutting down
@@ -3324,6 +3418,7 @@ class GitDesk(App):
self.set_busy(False)
await self.refresh_state(force=True)
self.update_remote_button()
self.announce_longpaths()
async def ask(self, title: str, body, buttons, cancel=None) -> str:
return await self.push_screen_wait(ChoiceDialog(title, body, buttons, cancel))
@@ -3367,6 +3462,8 @@ class GitDesk(App):
before = await asyncio.to_thread(self.git.snapshot)
for delay in (0.25, 0.5, 0.75, 1.0, 1.5, None):
r = await self.bg(*args, input=input)
if r.longpath_fixed and not r.ok:
r = await self.after_longpath_fix(args, input, r, before)
if r.ok or not looks_lockish(r):
return r
after = await asyncio.to_thread(self.git.snapshot)
@@ -3378,6 +3475,35 @@ class GitDesk(App):
await asyncio.sleep(delay)
return r
async def after_longpath_fix(self, args, input, r: Result, before) -> Result:
"""A command hit "Filename too long" and core.longpaths has just been turned on."""
if await asyncio.to_thread(self.git.snapshot) == before:
return await self.bg(*args, input=input) # nothing happened yet - just do it again
# It got part-way (e.g. switched branch but couldn't write some files): put those files back.
restored = await asyncio.to_thread(self.git.restore_unwritten, r)
body = Text("Some file paths in this repository are longer than Windows normally allows, so git "
"couldn't write them. gitdesk has turned on long path support:\n\n")
body.append(" git config --global core.longpaths true\n\n", style="bold")
if restored:
body.append(f"and re-created {plural(len(restored), 'file')} that couldn't be written:\n")
for p in restored[:8]:
body.append(f" {p}\n", style=C_MUTED)
if len(restored) > 8:
body.append(f" … and {len(restored) - 8} more\n", style=C_MUTED)
body.append("\n")
body.append("Check the Changes list. If the last action didn't finish, just run it again.")
LONGPATHS["just_enabled"] = False # this popup is the announcement
await self.ask("Turned on long file paths", body, [("ok", "OK", "primary")])
r.handled = True
return r
def announce_longpaths(self) -> None:
if LONGPATHS["just_enabled"]:
LONGPATHS["just_enabled"] = False
self._log_cmd(["config", "--global", "core.longpaths", "true"], Result(0, "(turned on by gitdesk)"))
self.notify("This repository has file paths longer than Windows normally allows, so gitdesk turned "
"on long path support: git config --global core.longpaths true", timeout=12)
async def lock_dialog(self, r: Result) -> str:
lock = self.git.index_lock_path()
body = Text()
@@ -4150,6 +4276,14 @@ class GitDesk(App):
self._log_cmd(args, r)
if r.ok:
break
if AUTO_LONGPATHS and LONGPATH_RE.search(r.message) and enable_longpaths():
if os.path.isdir(os.path.join(path, ".git")):
# "Clone succeeded, but checkout failed": write the files now that long paths work
fix = await asyncio.to_thread(run_git, path, ["restore", "--source=HEAD", ":/"])
self._log_cmd(["restore", "--source=HEAD", ":/"], fix)
if fix.ok:
break
continue # nothing was kept - clone again
tail = "\n".join(r.message.splitlines()[-10:])
body = Text.assemble((tail, "#f97583"), "\n\n",
("If the server needs a password or token, an SSH passphrase, or wants you to "