edit to contain long filepath names
This commit is contained in:
1 parent
41b2f67c4a
commit
3ba13a64ee
2 files changed
+142
-1
No files matched your search
+135
-1
@@ -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 "
|
||||
|
||||
Reference in new issue
Block a user