Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ba13a64ee |
No files matched your search
@@ -118,6 +118,13 @@ Tip: put this folder on your PATH (Windows), or symlink it on Linux/macOS
|
|||||||
while fetches stay on HTTPS.
|
while fetches stay on HTTPS.
|
||||||
- If the site is behind Cloudflare, SSH needs an address that reaches the server directly:
|
- If the site is behind Cloudflare, SSH needs an address that reaches the server directly:
|
||||||
a DNS-only hostname or the server's IP.
|
a DNS-only hostname or the server's IP.
|
||||||
|
- **"Filename too long" (Windows):** Unity projects often have paths past Windows' 260-character
|
||||||
|
limit. As soon as git reports one, gitdesk runs `git config --global core.longpaths true`
|
||||||
|
and tells you. Then:
|
||||||
|
- if the action hadn't changed anything yet (stash, commit, refresh), it runs it again;
|
||||||
|
- if it got part-way (e.g. switched branch but couldn't write some files), it re-creates
|
||||||
|
just the missing files;
|
||||||
|
- if a clone downloaded but couldn't write its files, it writes them and opens the repo.
|
||||||
- **Another program has the repository busy** (GitHub Desktop, VS Code, Unity, OneDrive,
|
- **Another program has the repository busy** (GitHub Desktop, VS Code, Unity, OneDrive,
|
||||||
antivirus): gitdesk waits and retries for a few seconds. If a `.git/index.lock` is left
|
antivirus): gitdesk waits and retries for a few seconds. If a `.git/index.lock` is left
|
||||||
behind by a crashed git, it offers **Remove lock file & retry**.
|
behind by a crashed git, it offers **Remove lock file & retry**.
|
||||||
|
|||||||
+135
-1
@@ -165,6 +165,7 @@ class Result:
|
|||||||
out: str = ""
|
out: str = ""
|
||||||
err: str = ""
|
err: str = ""
|
||||||
lockish: bool = False # failed because another program had the repository locked
|
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
|
handled: bool = False # the user has already been shown this failure
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -220,8 +221,14 @@ def user_ssh_command_set(path: str) -> bool:
|
|||||||
def hint_for(message: str) -> str:
|
def hint_for(message: str) -> str:
|
||||||
"""Plain-English next step for common git errors."""
|
"""Plain-English next step for common git errors."""
|
||||||
if "Filename too long" in message:
|
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, "
|
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")
|
"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:
|
if "Please tell me who you are" in message:
|
||||||
return ("git needs your name and email for commits. Run these once:\n"
|
return ("git needs your name and email for commits. Run these once:\n"
|
||||||
' git config --global user.name "Your Name"\n'
|
' git config --global user.name "Your Name"\n'
|
||||||
@@ -434,7 +441,75 @@ class GitError(Exception):
|
|||||||
pass
|
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]
|
cmd = ["git", "-c", "core.quotepath=false", "-c", "color.ui=false", *args]
|
||||||
kw = dict(cwd=cwd, capture_output=True, text=True, encoding="utf-8",
|
kw = dict(cwd=cwd, capture_output=True, text=True, encoding="utf-8",
|
||||||
errors="replace", env=env or GIT_ENV)
|
errors="replace", env=env or GIT_ENV)
|
||||||
@@ -544,6 +619,22 @@ class Git:
|
|||||||
r = self.run("status", "--porcelain=v2", "--branch", "-z", "-uall", log=False)
|
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")))
|
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:
|
def index_lock_path(self) -> str:
|
||||||
return os.path.join(self.git_dir, "index.lock")
|
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
|
return True, "Couldn't add the remote: " + r.message
|
||||||
if wrote:
|
if wrote:
|
||||||
r = run_git(path, ["add", "-A"])
|
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:
|
if r.ok:
|
||||||
r = run_git(path, ["commit", "-q", "-m", "Initial commit"])
|
r = run_git(path, ["commit", "-q", "-m", "Initial commit"])
|
||||||
if not r.ok:
|
if not r.ok:
|
||||||
@@ -2652,6 +2745,7 @@ class GitDesk(App):
|
|||||||
if g is not self.git:
|
if g is not self.git:
|
||||||
return
|
return
|
||||||
self.state = st
|
self.state = st
|
||||||
|
self.announce_longpaths()
|
||||||
try:
|
try:
|
||||||
await self.apply_state(st)
|
await self.apply_state(st)
|
||||||
except NoMatches: # app is shutting down
|
except NoMatches: # app is shutting down
|
||||||
@@ -3324,6 +3418,7 @@ class GitDesk(App):
|
|||||||
self.set_busy(False)
|
self.set_busy(False)
|
||||||
await self.refresh_state(force=True)
|
await self.refresh_state(force=True)
|
||||||
self.update_remote_button()
|
self.update_remote_button()
|
||||||
|
self.announce_longpaths()
|
||||||
|
|
||||||
async def ask(self, title: str, body, buttons, cancel=None) -> str:
|
async def ask(self, title: str, body, buttons, cancel=None) -> str:
|
||||||
return await self.push_screen_wait(ChoiceDialog(title, body, buttons, cancel))
|
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)
|
before = await asyncio.to_thread(self.git.snapshot)
|
||||||
for delay in (0.25, 0.5, 0.75, 1.0, 1.5, None):
|
for delay in (0.25, 0.5, 0.75, 1.0, 1.5, None):
|
||||||
r = await self.bg(*args, input=input)
|
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):
|
if r.ok or not looks_lockish(r):
|
||||||
return r
|
return r
|
||||||
after = await asyncio.to_thread(self.git.snapshot)
|
after = await asyncio.to_thread(self.git.snapshot)
|
||||||
@@ -3378,6 +3475,35 @@ class GitDesk(App):
|
|||||||
await asyncio.sleep(delay)
|
await asyncio.sleep(delay)
|
||||||
return r
|
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:
|
async def lock_dialog(self, r: Result) -> str:
|
||||||
lock = self.git.index_lock_path()
|
lock = self.git.index_lock_path()
|
||||||
body = Text()
|
body = Text()
|
||||||
@@ -4150,6 +4276,14 @@ class GitDesk(App):
|
|||||||
self._log_cmd(args, r)
|
self._log_cmd(args, r)
|
||||||
if r.ok:
|
if r.ok:
|
||||||
break
|
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:])
|
tail = "\n".join(r.message.splitlines()[-10:])
|
||||||
body = Text.assemble((tail, "#f97583"), "\n\n",
|
body = Text.assemble((tail, "#f97583"), "\n\n",
|
||||||
("If the server needs a password or token, an SSH passphrase, or wants you to "
|
("If the server needs a password or token, an SSH passphrase, or wants you to "
|
||||||
|
|||||||
Reference in new issue
Block a user