From 98137973602aa338eee811b4b2adb530189b00b1 Mon Sep 17 00:00:00 2001 From: Mark Nauwelaerts Date: Sat, 3 May 2025 17:13:06 +0200 Subject: [PATCH] Transform invalid reserved pathname components Fixes mnauw/git-remote-hg#58 --- doc/git-remote-hg.txt | 14 ++++++++++++ git-remote-hg | 52 +++++++++++++++++++++++++++++++++++++++++-- test/main.t | 35 +++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 2 deletions(-) diff --git a/doc/git-remote-hg.txt b/doc/git-remote-hg.txt index de838f6..d709e26 100644 --- a/doc/git-remote-hg.txt +++ b/doc/git-remote-hg.txt @@ -104,6 +104,20 @@ the invalid '~' % git config --global remote-hg.ignore-name ~ -------------------------------------- +Even though the "gitdir" is configurable (using `GIT_DIR`), git does not accept +certain pathname components, e.g. `.git` or `.gitmodules` (case-insensitive). +Problems arise if the hg repo contains such pathnames, and recent git versions +will reject this in a very hard way. So these pathnames are now mapped +from "hg space" to "git space" in a one-to-one way, where (e.g.) +`.git[0 or more suffix]` is mapped to `.git[1 or more suffix]` (obviously by +appending or removing a suffix). The "suffix" in question defaults to `_`, +but can be configured using + +-------------------------------------- +% git config --global remote-hg.dotfile-suffix _ +-------------------------------------- + + NOTES ----- diff --git a/git-remote-hg b/git-remote-hg index 68e0601..c38fa1a 100755 --- a/git-remote-hg +++ b/git-remote-hg @@ -438,7 +438,7 @@ def export_file(ctx, fname): puts(b"data %d" % len(d)) puts(f.data()) - path = fix_file_path(f.path()) + path = fixup_path_to_git(fix_file_path(f.path())) return (gitmode(f.flags()), mark, path) def get_filechanges(repo, ctx, parent): @@ -518,6 +518,51 @@ def fixup_user(user): return b'%s <%s>' % (name, mail) +# (recent) git fast-import does not accept .git or .gitmodule component names +# (anywhere, case-insensitive) +# in any case, surprising things may happen, so add some front-end replacement magic; +# transform of (hg) .git(0 or more suffix) to (git) .git(1 or more suffix) +# (likewise so for any invalid git keyword) +def fixup_dotfile_path(path, suffix, add): + def subst(part): + if (not part) or part[0] != ord(b'.'): + return part + for prefix in (b'.git', b'.gitmodules'): + pl = len(prefix) + tail = len(part) - pl + if tail < 0: + continue + if part[0:pl].lower() == prefix and part[pl:] == suffix * tail: + if add: + return part + suffix + elif tail == 0: + # .git should not occur in git space + # so complain + if pl == 3: + die('invalid path component %s' % part) + else: + # but .gitmodules might + # leave as-is, it is handled/ignored elsewhere + return part + else: + return part[0:-1] + return part + # quick optimization check; + if (not path) or (path[0] != ord(b'.') and path.find(b'/.') < 0): + return path + sep = b'/' + return sep.join((subst(part) for part in path.split(sep))) + +def fixup_path_to_git(path): + if not dotfile_suffix: + return path + return fixup_dotfile_path(path, dotfile_suffix, True) + +def fixup_path_from_git(path): + if not dotfile_suffix: + return path + return fixup_dotfile_path(path, dotfile_suffix, False) + def updatebookmarks(repo, peer): remotemarks = peer.listkeys(b'bookmarks') @@ -749,7 +794,7 @@ def export_ref(repo, name, kind, head): puts(b"merge :%u" % (rev_to_mark(parents[1]))) for f in removed: - puts(b"D %s" % (fix_file_path(f))) + puts(b"D %s" % fixup_path_to_git(fix_file_path(f))) for f in modified_final: puts(b"M %s :%u %s" % f) puts() @@ -1042,6 +1087,7 @@ def parse_commit(parser): else: die(b'Unknown file command: %s' % line) path = c_style_unescape(path) + path = fixup_path_from_git(path) files[path] = files.get(path, {}) files[path].update(f) @@ -1867,6 +1913,7 @@ def main(args): global capability_push global remove_username_quotes global marksdir + global dotfile_suffix marks = None is_tmp = False @@ -1886,6 +1933,7 @@ def main(args): track_branches = get_config_bool('remote-hg.track-branches', True) capability_push = get_config_bool('remote-hg.capability-push', True) remove_username_quotes = get_config_bool('remote-hg.remove-username-quotes', True) + dotfile_suffix = get_config('remote-hg.dotfile-suffix').strip() or b'_' force_push = False if hg_git_compat: diff --git a/test/main.t b/test/main.t index 2910b36..7e8dfc7 100755 --- a/test/main.t +++ b/test/main.t @@ -268,6 +268,41 @@ test_expect_success 'strip' ' test_cmp actual expected ' +test_expect_success 'dotfiles' ' + test_when_finished "rm -rf hgrepo gitrepo" && + + ( + hg init hgrepo && + cd hgrepo && + + echo one >.git && + echo ONE >.GIT && + mkdir a && echo two > a/.gitmodules && + hg add .git .GIT a/.gitmodules && + hg commit -m zero + ) && + + git clone "hg::hgrepo" gitrepo && + test_cmp gitrepo/.git_ hgrepo/.git && + test_cmp gitrepo/.GIT_ hgrepo/.GIT && + test_cmp gitrepo/a/.gitmodules_ hgrepo/a/.gitmodules && + + ( + cd gitrepo && + echo three >.git_ && + echo THREE >.GIT && + echo four >a/.gitmodules_ && + git add .git_ .GIT_ a/.gitmodules_ && + git commit -m one && + git push + ) && + + hg -R hgrepo update && + test_cmp gitrepo/.git_ hgrepo/.git && + test_cmp gitrepo/.GIT_ hgrepo/.GIT && + test_cmp gitrepo/a/.gitmodules_ hgrepo/a/.gitmodules +' + test_expect_success 'remote push with master bookmark' ' test_when_finished "rm -rf hgrepo gitrepo*" &&