Mercurial > hg
view hgext/convert/git.py @ 25805:584044e5ad57
convert: use 'default' for specifying branch name in branchmap (issue4753)
A fix for issue2653 with 1d155582a8ea introduced a discrepancy how default
branch should be denoted when converting with branchmap from different SCM.
E.g. for Git and Mercurial you need to use 'default' whilst for Perforce and
SVN you had to use 'None'. This changeset unifies 'default' for such purposes
whilst falling back to 'None' when no 'default' mapping specified.
author | Eugene Baranov <eug.baranov@gmail.com> |
---|---|
date | Tue, 14 Jul 2015 14:40:56 +0100 |
parents | d9133e89d39d |
children | 80149d0b6842 d4e1e947444b |
line wrap: on
line source
# git.py - git support for the convert extension # # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others # # This software may be used and distributed according to the terms of the # GNU General Public License version 2 or any later version. import os import subprocess from mercurial import util, config, error from mercurial.node import hex, nullid from mercurial.i18n import _ from common import NoRepo, commit, converter_source, checktool class submodule(object): def __init__(self, path, node, url): self.path = path self.node = node self.url = url def hgsub(self): return "%s = [git]%s" % (self.path, self.url) def hgsubstate(self): return "%s %s" % (self.node, self.path) class convert_git(converter_source): # Windows does not support GIT_DIR= construct while other systems # cannot remove environment variable. Just assume none have # both issues. if util.safehasattr(os, 'unsetenv'): def gitopen(self, s, err=None): prevgitdir = os.environ.get('GIT_DIR') os.environ['GIT_DIR'] = self.path try: if err == subprocess.PIPE: (stdin, stdout, stderr) = util.popen3(s) return stdout elif err == subprocess.STDOUT: return self.popen_with_stderr(s) else: return util.popen(s, 'rb') finally: if prevgitdir is None: del os.environ['GIT_DIR'] else: os.environ['GIT_DIR'] = prevgitdir def gitpipe(self, s): prevgitdir = os.environ.get('GIT_DIR') os.environ['GIT_DIR'] = self.path try: return util.popen3(s) finally: if prevgitdir is None: del os.environ['GIT_DIR'] else: os.environ['GIT_DIR'] = prevgitdir else: def gitopen(self, s, err=None): if err == subprocess.PIPE: (sin, so, se) = util.popen3('GIT_DIR=%s %s' % (self.path, s)) return so elif err == subprocess.STDOUT: return self.popen_with_stderr(s) else: return util.popen('GIT_DIR=%s %s' % (self.path, s), 'rb') def gitpipe(self, s): return util.popen3('GIT_DIR=%s %s' % (self.path, s)) def popen_with_stderr(self, s): p = subprocess.Popen(s, shell=True, bufsize=-1, close_fds=util.closefds, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=False, env=None) return p.stdout def gitread(self, s): fh = self.gitopen(s) data = fh.read() return data, fh.close() def __init__(self, ui, path, revs=None): super(convert_git, self).__init__(ui, path, revs=revs) if os.path.isdir(path + "/.git"): path += "/.git" if not os.path.exists(path + "/objects"): raise NoRepo(_("%s does not look like a Git repository") % path) # The default value (50) is based on the default for 'git diff'. similarity = ui.configint('convert', 'git.similarity', default=50) if similarity < 0 or similarity > 100: raise util.Abort(_('similarity must be between 0 and 100')) if similarity > 0: self.simopt = '-C%d%%' % similarity findcopiesharder = ui.configbool('convert', 'git.findcopiesharder', False) if findcopiesharder: self.simopt += ' --find-copies-harder' else: self.simopt = '' checktool('git', 'git') self.path = path self.submodules = [] self.catfilepipe = self.gitpipe('git cat-file --batch') def after(self): for f in self.catfilepipe: f.close() def getheads(self): if not self.revs: heads, ret = self.gitread('git rev-parse --branches --remotes') heads = heads.splitlines() if ret: raise util.Abort(_('cannot retrieve git heads')) else: heads = [] for rev in self.revs: rawhead, ret = self.gitread("git rev-parse --verify %s" % rev) heads.append(rawhead[:-1]) if ret: raise util.Abort(_('cannot retrieve git head "%s"') % rev) return heads def catfile(self, rev, type): if rev == hex(nullid): raise IOError self.catfilepipe[0].write(rev+'\n') self.catfilepipe[0].flush() info = self.catfilepipe[1].readline().split() if info[1] != type: raise util.Abort(_('cannot read %r object at %s') % (type, rev)) size = int(info[2]) data = self.catfilepipe[1].read(size) if len(data) < size: raise util.Abort(_('cannot read %r object at %s: unexpected size') % (type, rev)) # read the trailing newline self.catfilepipe[1].read(1) return data def getfile(self, name, rev): if rev == hex(nullid): return None, None if name == '.hgsub': data = '\n'.join([m.hgsub() for m in self.submoditer()]) mode = '' elif name == '.hgsubstate': data = '\n'.join([m.hgsubstate() for m in self.submoditer()]) mode = '' else: data = self.catfile(rev, "blob") mode = self.modecache[(name, rev)] return data, mode def submoditer(self): null = hex(nullid) for m in sorted(self.submodules, key=lambda p: p.path): if m.node != null: yield m def parsegitmodules(self, content): """Parse the formatted .gitmodules file, example file format: [submodule "sub"]\n \tpath = sub\n \turl = git://giturl\n """ self.submodules = [] c = config.config() # Each item in .gitmodules starts with whitespace that cant be parsed c.parse('.gitmodules', '\n'.join(line.strip() for line in content.split('\n'))) for sec in c.sections(): s = c[sec] if 'url' in s and 'path' in s: self.submodules.append(submodule(s['path'], '', s['url'])) def retrievegitmodules(self, version): modules, ret = self.gitread("git show %s:%s" % (version, '.gitmodules')) if ret: # This can happen if a file is in the repo that has permissions # 160000, but there is no .gitmodules file. self.ui.warn(_("warning: cannot read submodules config file in " "%s\n") % version) return try: self.parsegitmodules(modules) except error.ParseError: self.ui.warn(_("warning: unable to parse .gitmodules in %s\n") % version) return for m in self.submodules: node, ret = self.gitread("git rev-parse %s:%s" % (version, m.path)) if ret: continue m.node = node.strip() def getchanges(self, version, full): if full: raise util.Abort(_("convert from git do not support --full")) self.modecache = {} fh = self.gitopen("git diff-tree -z --root -m -r %s %s" % ( self.simopt, version)) changes = [] copies = {} seen = set() entry = None subexists = [False] subdeleted = [False] difftree = fh.read().split('\x00') lcount = len(difftree) i = 0 def add(entry, f, isdest): seen.add(f) h = entry[3] p = (entry[1] == "100755") s = (entry[1] == "120000") renamesource = (not isdest and entry[4][0] == 'R') if f == '.gitmodules': subexists[0] = True if entry[4] == 'D' or renamesource: subdeleted[0] = True changes.append(('.hgsub', hex(nullid))) else: changes.append(('.hgsub', '')) elif entry[1] == '160000' or entry[0] == ':160000': subexists[0] = True else: if renamesource: h = hex(nullid) self.modecache[(f, h)] = (p and "x") or (s and "l") or "" changes.append((f, h)) while i < lcount: l = difftree[i] i += 1 if not entry: if not l.startswith(':'): continue entry = l.split() continue f = l if f not in seen: add(entry, f, False) # A file can be copied multiple times, or modified and copied # simultaneously. So f can be repeated even if fdest isn't. if entry[4][0] in 'RC': # rename or copy: next line is the destination fdest = difftree[i] i += 1 if fdest not in seen: add(entry, fdest, True) # .gitmodules isn't imported at all, so it being copied to # and fro doesn't really make sense if f != '.gitmodules' and fdest != '.gitmodules': copies[fdest] = f entry = None if fh.close(): raise util.Abort(_('cannot read changes in %s') % version) if subexists[0]: if subdeleted[0]: changes.append(('.hgsubstate', hex(nullid))) else: self.retrievegitmodules(version) changes.append(('.hgsubstate', '')) return (changes, copies, set()) def getcommit(self, version): c = self.catfile(version, "commit") # read the commit hash end = c.find("\n\n") message = c[end + 2:] message = self.recode(message) l = c[:end].splitlines() parents = [] author = committer = None for e in l[1:]: n, v = e.split(" ", 1) if n == "author": p = v.split() tm, tz = p[-2:] author = " ".join(p[:-2]) if author[0] == "<": author = author[1:-1] author = self.recode(author) if n == "committer": p = v.split() tm, tz = p[-2:] committer = " ".join(p[:-2]) if committer[0] == "<": committer = committer[1:-1] committer = self.recode(committer) if n == "parent": parents.append(v) if committer and committer != author: message += "\ncommitter: %s\n" % committer tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:] tz = -int(tzs) * (int(tzh) * 3600 + int(tzm)) date = tm + " " + str(tz) c = commit(parents=parents, date=date, author=author, desc=message, rev=version) return c def numcommits(self): return len([None for _ in self.gitopen('git rev-list --all')]) def gettags(self): tags = {} alltags = {} fh = self.gitopen('git ls-remote --tags "%s"' % self.path, err=subprocess.STDOUT) prefix = 'refs/tags/' # Build complete list of tags, both annotated and bare ones for line in fh: line = line.strip() if line.startswith("error:") or line.startswith("fatal:"): raise util.Abort(_('cannot read tags from %s') % self.path) node, tag = line.split(None, 1) if not tag.startswith(prefix): continue alltags[tag[len(prefix):]] = node if fh.close(): raise util.Abort(_('cannot read tags from %s') % self.path) # Filter out tag objects for annotated tag refs for tag in alltags: if tag.endswith('^{}'): tags[tag[:-3]] = alltags[tag] else: if tag + '^{}' in alltags: continue else: tags[tag] = alltags[tag] return tags def getchangedfiles(self, version, i): changes = [] if i is None: fh = self.gitopen("git diff-tree --root -m -r %s" % version) for l in fh: if "\t" not in l: continue m, f = l[:-1].split("\t") changes.append(f) else: fh = self.gitopen('git diff-tree --name-only --root -r %s ' '"%s^%s" --' % (version, version, i + 1)) changes = [f.rstrip('\n') for f in fh] if fh.close(): raise util.Abort(_('cannot read changes in %s') % version) return changes def getbookmarks(self): bookmarks = {} # Interesting references in git are prefixed prefix = 'refs/heads/' prefixlen = len(prefix) # factor two commands remoteprefix = self.ui.config('convert', 'git.remoteprefix', 'remote') gitcmd = { remoteprefix + '/': 'git ls-remote --heads origin', '': 'git show-ref'} # Origin heads for reftype in gitcmd: try: fh = self.gitopen(gitcmd[reftype], err=subprocess.PIPE) for line in fh: line = line.strip() rev, name = line.split(None, 1) if not name.startswith(prefix): continue name = '%s%s' % (reftype, name[prefixlen:]) bookmarks[name] = rev except Exception: pass return bookmarks def checkrevformat(self, revstr, mapname='splicemap'): """ git revision string is a 40 byte hex """ self.checkhexformat(revstr, mapname)