Mercurial > hg > hg-git
changeset 150:4b63e8425565
merged in tag support branch
author | Scott Chacon <schacon@gmail.com> |
---|---|
date | Wed, 27 May 2009 16:05:22 -0700 |
parents | eb1fcdb8fc9b (current diff) 845039d8d90a (diff) |
children | 240ec7b38f98 |
files | __init__.py git_handler.py |
diffstat | 13 files changed, 275 insertions(+), 56 deletions(-) [+] |
line wrap: on
line diff
--- a/__init__.py +++ b/__init__.py @@ -45,21 +45,27 @@ git = GitHandler(repo, ui) git.push(remote_name) +def gimport(ui, repo, remote_name=None): + git = GitHandler(repo, ui) + git.import_commits(remote_name) + def gexport(ui, repo): git = GitHandler(repo, ui) - git.export() + git.export_commits() def gremote(ui, repo, *args): git = GitHandler(repo, ui) if len(args) == 0: git.remote_list() + elif len(args) < 2: + repo.ui.warn(_("must supply an action and a remote\n")) else: verb = args[0] nick = args[1] if verb == 'add': - if args[2]: + if len(args) == 3: git.remote_add(nick, args[2]) else: repo.ui.warn(_("must supply a url to add as a remote\n")) @@ -87,7 +93,9 @@ _('Clone a git repository into an hg repository.'), ), "gpush": - (gpush, [], _('hg gpush')), + (gpush, [], _('hg gpush remote')), + "gimport": + (gimport, [], _('hg gimport')), "gexport": (gexport, [], _('hg gexport')), "gfetch":
--- a/dulwich/object_store.py +++ b/dulwich/object_store.py @@ -22,6 +22,7 @@ import itertools import os +import shutil import stat import tempfile import urllib2 @@ -248,7 +249,12 @@ basename = os.path.join(self.pack_dir, "pack-%s" % iter_sha1(entry[0] for entry in entries)) write_pack_index_v2(basename+".idx", entries, p.get_stored_checksum()) - os.rename(path, basename + ".pack") + try: + os.rename(path, basename + ".pack") + except OSError: + # Hack for Windows access denied error. + # TODO: mark the original for deletion later. + shutil.copyfile(path, basename + ".pack") self._add_known_pack(basename) def add_thin_pack(self):
--- a/dulwich/objects.py +++ b/dulwich/objects.py @@ -479,15 +479,19 @@ def parse_timezone(text): offset = int(text) + signum = (offset < 0) and -1 or 1 + offset = abs(offset) hours = int(offset / 100) minutes = (offset % 100) - return (hours * 3600) + (minutes * 60) + return signum * (hours * 3600 + minutes * 60) def format_timezone(offset): if offset % 60 != 0: raise ValueError("Unable to handle non-minute offset.") - return '%+03d%02d' % (offset / 3600, (offset / 60) % 60) + sign = (offset < 0) and '-' or '+' + offset = abs(offset) + return '%c%02d%02d' % (sign, offset / 3600, (offset / 60) % 60) class Commit(ShaFile):
--- a/dulwich/pack.py +++ b/dulwich/pack.py @@ -128,7 +128,7 @@ def load_pack_index(filename): - f = open(filename, 'r') + f = open(filename, 'rb') if f.read(4) == '\377tOc': version = struct.unpack(">L", f.read(4))[0] if version == 2: @@ -181,7 +181,7 @@ # ensure that it hasn't changed. self._size = os.path.getsize(filename) if file is None: - self._file = open(filename, 'r') + self._file = open(filename, 'rb') else: self._file = file self._contents, map_offset = simple_mmap(self._file, 0, self._size) @@ -733,7 +733,7 @@ :param objects: Iterable over (object, path) tuples to write :param num_objects: Number of objects to write """ - f = open(filename + ".pack", 'w') + f = open(filename + ".pack", 'wb') try: entries, data_sum = write_pack_data(f, objects, num_objects) finally: @@ -797,7 +797,7 @@ crc32_checksum. :param pack_checksum: Checksum of the pack file. """ - f = open(filename, 'w') + f = open(filename, 'wb') f = SHA1Writer(f) fan_out_table = defaultdict(lambda: 0) for (name, offset, entry_checksum) in entries: @@ -945,7 +945,7 @@ crc32_checksum. :param pack_checksum: Checksum of the pack file. """ - f = open(filename, 'w') + f = open(filename, 'wb') f = SHA1Writer(f) f.write('\377tOc') # Magic! f.write(struct.pack(">L", 2))
--- a/dulwich/repo.py +++ b/dulwich/repo.py @@ -173,14 +173,12 @@ def _get_ref(self, file): f = open(file, 'rb') try: - contents = f.read() + contents = f.read().strip() if contents.startswith(SYMREF): ref = contents[len(SYMREF):] - if ref[-1] == '\n': - ref = ref[:-1] return self.ref(ref) - assert len(contents) == 41, 'Invalid ref in %s' % file - return contents[:-1] + assert len(contents) == 40, 'Invalid ref in %s' % file + return contents finally: f.close() @@ -291,9 +289,12 @@ def remote_refs(self, remote_name): ret = {} - for root, dirs, files in os.walk(os.path.join(self.controldir(), 'refs', 'remotes', remote_name)): + r = os.path.join(self.controldir(), 'refs', 'remotes', remote_name) + for root, dirs, files in os.walk(r): for name in files: - ret[name] = self._get_ref(os.path.join(root, name)) + if root != r: + name = os.path.join(root[len(r) + 1:], name) + ret[name] = self._get_ref(os.path.join(r, name)) return ret def head(self): @@ -424,11 +425,10 @@ basefiles = set() changes = list() csha = None - ctree = None cmode = None if basetree: for (bmode, bname, bsha) in basetree.entries(): - if bmode == 57344: # TODO : properly handle submodules + if bmode == 0160000: # TODO : properly handle submodules continue basefiles.add(bname) bobj = self.get_object(bsha) @@ -438,6 +438,7 @@ if isinstance (bobj, Blob): changes.append (prefix + bname) elif isinstance(bobj, Tree): + ctree = None if csha: ctree = self.get_object(csha) changes.extend(filenames(bobj, @@ -447,7 +448,7 @@ # handle removals if comptree: for (bmode, bname, bsha, ) in comptree.entries(): - if bmode == 57344: # TODO: hande submodles + if bmode == 0160000: # TODO: hande submodles continue if bname not in basefiles: bobj = self.get_object(bsha)
--- a/git_handler.py +++ b/git_handler.py @@ -14,7 +14,8 @@ Tag, Tree, hex_to_sha, - sha_to_hex + sha_to_hex, + format_timezone, ) import math @@ -51,7 +52,14 @@ self.ui = ui self.mapfile = 'git-mapfile' self.configfile = 'git-config' - self.gitdir = self.repo.join('git') + + if ui.config('git', 'intree'): + self.gitdir = self.repo.wjoin('.git') + else: + self.gitdir = self.repo.join('git') + + self.importbranch = ui.config('git', 'importbranch') + self.init_if_missing() self.load_git() self.load_map() @@ -109,6 +117,10 @@ ## END FILE LOAD AND SAVE METHODS + def import_commits(self, remote_name): + self.import_git_objects(remote_name) + self.save_map() + def fetch(self, remote_name): self.ui.status(_("fetching from : %s\n") % remote_name) self.export_git_objects() @@ -118,7 +130,7 @@ self.import_local_tags(refs) self.save_map() - def export(self): + def export_commits(self): self.export_git_objects() self.export_hg_tags() self.update_references() @@ -126,7 +138,8 @@ def push(self, remote_name): self.ui.status(_("pushing to : %s\n") % remote_name) - self.export() + self.export_commits() + self.update_remote_references(remote_name) self.upload_pack(remote_name) def remote_add(self, remote_name, git_url): @@ -157,7 +170,23 @@ return self._config['remote.' + remote_name + '.url'] def update_references(self): - # TODO : if bookmarks exist, add them as git branches + try: + # We only care about bookmarks of the form 'name', + # not 'remote/name'. + def is_local_ref(item): return item[0].count('/') == 0 + bms = bookmarks.parse(self.repo) + bms = dict(filter(is_local_ref, bms.items())) + + # Create a local Git branch name for each + # Mercurial bookmark. + for key in bms: + hg_sha = hex(bms[key]) + git_sha = self.map_git_get(hg_sha) + self.git.set_ref('refs/heads/' + key, git_sha) + except AttributeError: + # No bookmarks extension + pass + c = self.map_git_get(hex(self.repo.changelog.tip())) self.git.set_ref('refs/heads/master', c) @@ -167,8 +196,18 @@ continue self.git.set_ref('refs/tags/' + tag, self.map_git_get(hex(sha))) + # Make sure there's a refs/remotes/remote_name/name + # for every refs/heads/name + def update_remote_references(self, remote_name): + self.git.set_remote_refs(self.local_heads(), remote_name) + + def local_heads(self): + def is_local_head(item): return item[0].startswith('refs/heads') + refs = self.git.get_refs() + return dict(filter(is_local_head, refs.items())) + def export_git_objects(self): - self.ui.status(_("exporting git objects\n")) + self.ui.status(_("importing Hg objects into Git\n")) total = len(self.repo.changelog) if total: magnitude = int(math.log(total, 10)) + 1 @@ -215,13 +254,19 @@ author = ctx.user() if not '>' in author: # TODO : this kills losslessness - die (submodules)? author = author + ' <none@none>' - commit['author'] = author + ' ' + str(int(time)) + ' ' + seconds_to_offset(timezone) + commit['author'] = author + ' ' + str(int(time)) + ' ' + format_timezone(-timezone) message = ctx.description() commit['message'] = ctx.description() + "\n" extra = ctx.extra() if 'committer' in extra: - commit['committer'] = extra['committer'] + # fixup timezone + (name_timestamp, timezone) = extra['committer'].rsplit(' ', 1) + try: + timezone = format_timezone(-int(timezone)) + commit['committer'] = '%s %s' % (name_timestamp, timezone) + except ValueError: + self.ui.warn(_("Ignoring committer in extra, invalid timezone in r%s: '%s'.\n") % (rev, timezone)) if 'encoding' in extra: commit['encoding'] = extra['encoding'] @@ -306,12 +351,17 @@ trees['/'] = [] trees['/'].append(fileentry) - # sort by tree depth, so we write the deepest trees first dirs = trees.keys() - dirs.sort(lambda a, b: len(b.split('/'))-len(a.split('/'))) - dirs.remove('/') - dirs.append('/') - + if dirs: + # sort by tree depth, so we write the deepest trees first + dirs.sort(lambda a, b: len(b.split('/'))-len(a.split('/'))) + dirs.remove('/') + dirs.append('/') + else: + # manifest is empty => make empty root tree + trees['/'] = [] + dirs = ['/'] + # write all the trees tree_sha = None tree_shas = {} @@ -382,6 +432,13 @@ if local_ref: if not local_ref == refs[ref_name]: changed[ref_name] = local_ref + + # Also push any local branches not on the server yet + for head in self.local_heads(): + if not head in refs: + ref = self.git.ref(head) + changed[head] = ref + return changed # takes a list of shas the server wants and shas the server has @@ -406,7 +463,7 @@ changes = list() changes.append((tree, path)) for (mode, name, sha) in tree.entries(): - if mode == 57344: # TODO : properly handle submodules and document what 57344 means + if mode == 0160000: # TODO : properly handle submodules and document what 57344 means continue if sha in seen: continue @@ -473,17 +530,26 @@ self.repo.tag(ref_name, hex_to_sha(sha), '', True, None, None) - def import_git_objects(self, remote_name, refs): + def import_git_objects(self, remote_name=None, refs=None): self.ui.status(_("importing Git objects into Hg\n")) # import heads and fetched tags as remote references todo = [] done = set() convert_list = {} self.renames = {} - + # get a list of all the head shas - for head, sha in refs.iteritems(): + if refs: + for head, sha in refs.iteritems(): todo.append(sha) + else: + if remote_name: + todo = self.git.remote_refs(remote_name).values()[:] + elif self.importbranch: + branches = self.importbranch.split(',') + todo = [self.git.ref(i.strip()) for i in branches] + else: + todo = self.git.heads().values()[:] # traverse the heads getting a list of all the unique commits while todo: @@ -507,15 +573,20 @@ commits = toposort.TopoSort(convert_list).items() # import each of the commits, oldest first - for csha in commits: + total = len(commits) + magnitude = int(math.log(total, 10)) + 1 if total else 1 + for i, csha in enumerate(commits): + if i%100 == 0: + self.ui.status(_("at: %*d/%d\n") % (magnitude, i, total)) commit = convert_list[csha] if not self.map_hg_get(csha): # it's already here self.import_git_commit(commit) else: # we need to get rename info for further upstream self.pseudo_import_git_commit(commit) - - self.update_hg_bookmarks(remote_name) + + if remote_name: + self.update_hg_bookmarks(remote_name) def update_hg_bookmarks(self, remote_name): try: @@ -532,9 +603,9 @@ def convert_git_int_mode(self, mode): # TODO : make these into constants convert = { - 33188: '', - 40960: 'l', - 33261: 'x'} + 0100644: '', + 0100755: 'x', + 0120000: 'l'} if mode in convert: return convert[mode] return '' @@ -561,8 +632,8 @@ def pseudo_import_git_commit(self, commit): (strip_message, hg_renames, hg_branch) = self.extract_hg_metadata(commit.message) cs = self.map_hg_get(commit.id) - p1 = "0" * 40 - p2 = "0" * 40 + p1 = nullid + p2 = nullid if len(commit.parents) > 0: sha = commit.parents[0] p1 = self.map_hg_get(sha) @@ -573,7 +644,7 @@ # TODO : map extra parents to the extras file pass # saving rename info - if (not (p2 == "0"*40) or (p1 == "0"*40)): + if (not (p2 == nullid) or (p1 == nullid)): self.renames[cs] = {} else: self.renames[cs] = self.renames[p1].copy() @@ -599,8 +670,8 @@ copied_path = None return context.memfilectx(f, data, 'l' in e, 'x' in e, copied_path) - p1 = "0" * 40 - p2 = "0" * 40 + p1 = nullid + p2 = nullid if len(commit.parents) > 0: sha = commit.parents[0] p1 = self.map_hg_get(sha) @@ -615,7 +686,7 @@ files = self.git.get_files_changed(commit) # wierd hack for explicit file renames in first but not second branch - if not (p2 == "0"*40): + if not (p2 == nullid): vals = [item for item in self.renames[p1].values() if not item in self.renames[p2].values()] for removefile in vals: files.remove(removefile) @@ -628,7 +699,7 @@ # if committer is different than author, add it to extra if not commit._author_raw == commit._committer_raw: - extra['committer'] = commit._committer_raw + extra['committer'] = "%s %d %d" % (commit.committer, commit.commit_time, -commit.commit_timezone) if commit._encoding: extra['encoding'] = commit._encoding @@ -637,7 +708,7 @@ extra['branch'] = hg_branch text = strip_message - date = datetime.datetime.fromtimestamp(commit.author_time).strftime("%Y-%m-%d %H:%M:%S") + date = (commit.author_time, -commit.author_timezone) ctx = context.memctx(self.repo, (p1, p2), text, files, getfilectx, commit.author, date, extra) a = self.repo.commitctx(ctx) @@ -648,7 +719,7 @@ gitsha = commit.id # saving rename info - if (not (p2 == "0"*40) or (p1 == "0"*40)): + if (not (p2 == nullid) or (p1 == nullid)): self.renames[cs] = {} else: self.renames[cs] = self.renames[p1].copy()
new file mode 100755 --- /dev/null +++ b/tests/test-empty-working-tree @@ -0,0 +1,42 @@ +#!/bin/sh + +# Fails for some reason, need to investigate +# "$TESTDIR/hghave" git || exit 80 + +# bail early if the user is already running git-daemon +echo hi | nc localhost 9418 && exit 80 + +echo "[extensions]" >> $HGRCPATH +echo "hggit=$(echo $(dirname $(dirname $0)))" >> $HGRCPATH +echo 'hgext.bookmarks =' >> $HGRCPATH + +GIT_AUTHOR_NAME='test'; export GIT_AUTHOR_NAME +GIT_AUTHOR_EMAIL='test@example.org'; export GIT_AUTHOR_EMAIL +GIT_AUTHOR_DATE="2007-01-01 00:00:00 +0000"; export GIT_AUTHOR_DATE +GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"; export GIT_COMMITTER_NAME +GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"; export GIT_COMMITTER_EMAIL +GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"; export GIT_COMMITTER_DATE + +mkdir gitrepo +cd gitrepo +git init | python -c "import sys; print sys.stdin.read().replace('$(dirname $(pwd))/', '')" + +git commit --allow-empty -m empty + +# dulwich does not presently support local git repos, workaround +cd .. +git daemon --base-path="$(pwd)"\ + --listen=localhost\ + --export-all\ + --pid-file=gitdaemon.pid \ + --detach --reuseaddr + +hg gclone git://localhost/gitrepo hgrepo +cd hgrepo +hg log -r tip --template 'files: {files}\n' + +hg gclear +hg gexport + +cd .. +kill `cat gitdaemon.pid`
new file mode 100644 --- /dev/null +++ b/tests/test-empty-working-tree.out @@ -0,0 +1,14 @@ +Initialized empty Git repository in gitrepo/.git/ + +[master (root-commit) 6782568] empty +fetching from : origin +exporting git objects +Counting objects: 2, done. +Total 2 (delta 0), reused 0 (delta 0) +importing Git objects into Hg +0 files updated, 0 files merged, 0 files removed, 0 files unresolved +files: +clearing out the git cache data +exporting git objects +at: 0/1 +converting revision 0
--- a/tests/test-file-removal +++ b/tests/test-file-removal @@ -4,7 +4,7 @@ # "$TESTDIR/hghave" git || exit 80 # bail early if the user is already running git-daemon -echo hi | nc localhost 9418 && exit 80 +echo hi | nc localhost 9418 2>/dev/null && exit 80 echo "[extensions]" >> $HGRCPATH echo "hggit=$(echo $(dirname $(dirname $0)))" >> $HGRCPATH
--- a/tests/test-git-clone +++ b/tests/test-git-clone @@ -4,7 +4,7 @@ # "$TESTDIR/hghave" git || exit 80 # bail early if the user is already running git-daemon -echo hi | nc localhost 9418 && exit 80 +echo hi | nc localhost 9418 2>/dev/null && exit 80 echo "[extensions]" >> $HGRCPATH echo "hggit=$(echo $(dirname $(dirname $0)))" >> $HGRCPATH
--- a/tests/test-sane-without-bookmarks +++ b/tests/test-sane-without-bookmarks @@ -4,7 +4,7 @@ # "$TESTDIR/hghave" git || exit 80 # bail early if the user is already running git-daemon -echo hi | nc localhost 9418 && exit 80 +echo hi | nc localhost 9418 2>/dev/null && exit 80 echo "[extensions]" >> $HGRCPATH echo "hggit=$(echo $(dirname $(dirname $0)))" >> $HGRCPATH
new file mode 100755 --- /dev/null +++ b/tests/test-tree-decomposition @@ -0,0 +1,55 @@ +#!/bin/sh + +# Fails for some reason, need to investigate +# "$TESTDIR/hghave" git || exit 80 + +# bail early if the user is already running git-daemon +echo hi | nc localhost 9418 && exit 80 + +echo "[extensions]" >> $HGRCPATH +echo "hggit=$(echo $(dirname $(dirname $0)))" >> $HGRCPATH +echo 'hgext.bookmarks =' >> $HGRCPATH + +GIT_AUTHOR_NAME='test'; export GIT_AUTHOR_NAME +GIT_AUTHOR_EMAIL='test@example.org'; export GIT_AUTHOR_EMAIL +GIT_AUTHOR_DATE="2007-01-01 00:00:00 +0000"; export GIT_AUTHOR_DATE +GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"; export GIT_COMMITTER_NAME +GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"; export GIT_COMMITTER_EMAIL +GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"; export GIT_COMMITTER_DATE + +count=10 +commit() +{ + GIT_AUTHOR_DATE="2007-01-01 00:00:$count +0000" + GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE" + git commit "$@" >/dev/null 2>/dev/null || echo "git commit error" + count=`expr $count + 1` +} + +mkdir gitrepo +cd gitrepo +git init | python -c "import sys; print sys.stdin.read().replace('$(dirname $(pwd))/', '')" + +mkdir d1 +touch d1/f1 d1/f2 +git add d1/f1 d1/f2 +git commit -m initial + +mkdir d2 +git mv d1/f2 d2/f2 +git commit -m 'rename' + +# dulwich does not presently support local git repos, workaround +cd .. +git daemon --base-path="$(pwd)"\ + --listen=localhost\ + --export-all\ + --pid-file=gitdaemon.pid \ + --detach --reuseaddr + +hg gclone git://localhost/gitrepo hgrepo +cd hgrepo +hg log -r tip --template 'adds: {file_adds}\ndels: {file_dels}\n' + +cd .. +kill `cat gitdaemon.pid`
new file mode 100644 --- /dev/null +++ b/tests/test-tree-decomposition.out @@ -0,0 +1,18 @@ +Initialized empty Git repository in gitrepo/.git/ + +[master (root-commit) 60fd61f] initial + 0 files changed, 0 insertions(+), 0 deletions(-) + create mode 100644 d1/f1 + create mode 100644 d1/f2 +[master a2e8665] rename + 1 files changed, 0 insertions(+), 0 deletions(-) + rename {d1 => d2}/f2 (100%) +fetching from : origin +exporting git objects +Counting objects: 8, done. +Compressing objects: 25% (1/4) Compressing objects: 50% (2/4) Compressing objects: 75% (3/4) Compressing objects: 100% (4/4) Compressing objects: 100% (4/4), done. +Total 8 (delta 0), reused 0 (delta 0) +importing Git objects into Hg +2 files updated, 0 files merged, 0 files removed, 0 files unresolved +adds: d2/f2 +dels: d1/f2