git.py
476 lines
| 16.5 KiB
| text/x-python
|
PythonLexer
Martin Geisler
|
r8250 | # 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 | ||||
Matt Mackall
|
r10263 | # GNU General Public License version 2 or any later version. | ||
timeless
|
r28365 | from __future__ import absolute_import | ||
Brendan Cully
|
r4536 | |||
import os | ||||
Yuya Nishihara
|
r29205 | |||
from mercurial.i18n import _ | ||||
timeless
|
r28365 | from mercurial import ( | ||
config, | ||||
error, | ||||
node as nodemod, | ||||
) | ||||
Brendan Cully
|
r4536 | |||
timeless
|
r28365 | from . import ( | ||
common, | ||||
) | ||||
Brendan Cully
|
r4536 | |||
YaNan Xu
|
r17929 | 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) | ||||
Gregory Szorc
|
r30660 | # Keys in extra fields that should not be copied if the user requests. | ||
Martin von Zweigbergk
|
r32291 | bannedextrakeys = { | ||
Gregory Szorc
|
r30660 | # Git commit object built-ins. | ||
'tree', | ||||
'parent', | ||||
'author', | ||||
'committer', | ||||
# Mercurial built-ins. | ||||
'branch', | ||||
'close', | ||||
Martin von Zweigbergk
|
r32291 | } | ||
Gregory Szorc
|
r30660 | |||
Matt Mackall
|
r28670 | class convert_git(common.converter_source, common.commandline): | ||
Patrick Mezard
|
r5217 | # Windows does not support GIT_DIR= construct while other systems | ||
# cannot remove environment variable. Just assume none have | ||||
# both issues. | ||||
David Schleimer
|
r21630 | |||
Mateusz Kwapich
|
r28659 | def _gitcmd(self, cmd, *args, **kwargs): | ||
return cmd('--git-dir=%s' % self.path, *args, **kwargs) | ||||
def gitrun0(self, *args, **kwargs): | ||||
return self._gitcmd(self.run0, *args, **kwargs) | ||||
def gitrun(self, *args, **kwargs): | ||||
return self._gitcmd(self.run, *args, **kwargs) | ||||
def gitrunlines0(self, *args, **kwargs): | ||||
return self._gitcmd(self.runlines0, *args, **kwargs) | ||||
def gitrunlines(self, *args, **kwargs): | ||||
return self._gitcmd(self.runlines, *args, **kwargs) | ||||
Mateusz Kwapich
|
r28662 | def gitpipe(self, *args, **kwargs): | ||
return self._gitcmd(self._run3, *args, **kwargs) | ||||
Matt Harbison
|
r35168 | def __init__(self, ui, repotype, path, revs=None): | ||
super(convert_git, self).__init__(ui, repotype, path, revs=revs) | ||||
Matt Mackall
|
r28670 | common.commandline.__init__(self, ui, 'git') | ||
Durham Goode
|
r25748 | |||
Blake Burkhart
|
r29051 | # Pass an absolute path to git to prevent from ever being interpreted | ||
# as a URL | ||||
path = os.path.abspath(path) | ||||
Brendan Cully
|
r4536 | if os.path.isdir(path + "/.git"): | ||
path += "/.git" | ||||
if not os.path.exists(path + "/objects"): | ||||
timeless
|
r28365 | raise common.NoRepo(_("%s does not look like a Git repository") % | ||
path) | ||||
Patrick Mezard
|
r5497 | |||
Siddharth Agarwal
|
r22512 | # The default value (50) is based on the default for 'git diff'. | ||
Boris Feld
|
r34162 | similarity = ui.configint('convert', 'git.similarity') | ||
Siddharth Agarwal
|
r22470 | if similarity < 0 or similarity > 100: | ||
Pierre-Yves David
|
r26587 | raise error.Abort(_('similarity must be between 0 and 100')) | ||
Siddharth Agarwal
|
r22470 | if similarity > 0: | ||
Mateusz Kwapich
|
r28660 | self.simopt = ['-C%d%%' % similarity] | ||
Boris Feld
|
r34158 | findcopiesharder = ui.configbool('convert', 'git.findcopiesharder') | ||
Siddharth Agarwal
|
r22471 | if findcopiesharder: | ||
Mateusz Kwapich
|
r28660 | self.simopt.append('--find-copies-harder') | ||
Gregory Szorc
|
r30646 | |||
Boris Feld
|
r34160 | renamelimit = ui.configint('convert', 'git.renamelimit') | ||
Gregory Szorc
|
r30646 | self.simopt.append('-l%d' % renamelimit) | ||
Siddharth Agarwal
|
r22470 | else: | ||
Mateusz Kwapich
|
r28660 | self.simopt = [] | ||
Siddharth Agarwal
|
r22470 | |||
timeless
|
r28365 | common.checktool('git', 'git') | ||
Patrick Mezard
|
r5497 | |||
Brendan Cully
|
r4536 | self.path = path | ||
YaNan Xu
|
r17929 | self.submodules = [] | ||
Brendan Cully
|
r4536 | |||
Mateusz Kwapich
|
r28662 | self.catfilepipe = self.gitpipe('cat-file', '--batch') | ||
David Schleimer
|
r21630 | |||
Gregory Szorc
|
r30660 | self.copyextrakeys = self.ui.configlist('convert', 'git.extrakeys') | ||
banned = set(self.copyextrakeys) & bannedextrakeys | ||||
if banned: | ||||
raise error.Abort(_('copying of extra key is forbidden: %s') % | ||||
_(', ').join(sorted(banned))) | ||||
Boris Feld
|
r34156 | committeractions = self.ui.configlist('convert', 'git.committeractions') | ||
Gregory Szorc
|
r30813 | |||
messagedifferent = None | ||||
messagealways = None | ||||
for a in committeractions: | ||||
if a.startswith(('messagedifferent', 'messagealways')): | ||||
k = a | ||||
v = None | ||||
if '=' in a: | ||||
k, v = a.split('=', 1) | ||||
if k == 'messagedifferent': | ||||
messagedifferent = v or 'committer:' | ||||
elif k == 'messagealways': | ||||
messagealways = v or 'committer:' | ||||
if messagedifferent and messagealways: | ||||
raise error.Abort(_('committeractions cannot define both ' | ||||
'messagedifferent and messagealways')) | ||||
dropcommitter = 'dropcommitter' in committeractions | ||||
replaceauthor = 'replaceauthor' in committeractions | ||||
Gregory Szorc
|
r30815 | if dropcommitter and replaceauthor: | ||
Gregory Szorc
|
r30813 | raise error.Abort(_('committeractions cannot define both ' | ||
Gregory Szorc
|
r30815 | 'dropcommitter and replaceauthor')) | ||
Gregory Szorc
|
r30813 | |||
if dropcommitter and messagealways: | ||||
raise error.Abort(_('committeractions cannot define both ' | ||||
'dropcommitter and messagealways')) | ||||
if not messagedifferent and not messagealways: | ||||
messagedifferent = 'committer:' | ||||
self.committeractions = { | ||||
'dropcommitter': dropcommitter, | ||||
'replaceauthor': replaceauthor, | ||||
'messagedifferent': messagedifferent, | ||||
'messagealways': messagealways, | ||||
} | ||||
David Schleimer
|
r21630 | def after(self): | ||
for f in self.catfilepipe: | ||||
f.close() | ||||
Brendan Cully
|
r4536 | def getheads(self): | ||
Durham Goode
|
r25748 | if not self.revs: | ||
Mateusz Kwapich
|
r28660 | output, status = self.gitrun('rev-parse', '--branches', '--remotes') | ||
heads = output.splitlines() | ||||
if status: | ||||
Pierre-Yves David
|
r26587 | raise error.Abort(_('cannot retrieve git heads')) | ||
Brendan Cully
|
r4768 | else: | ||
Durham Goode
|
r25749 | heads = [] | ||
for rev in self.revs: | ||||
Mateusz Kwapich
|
r28660 | rawhead, ret = self.gitrun('rev-parse', '--verify', rev) | ||
Durham Goode
|
r25749 | heads.append(rawhead[:-1]) | ||
if ret: | ||||
Pierre-Yves David
|
r26587 | raise error.Abort(_('cannot retrieve git head "%s"') % rev) | ||
Patrick Mezard
|
r10986 | return heads | ||
Brendan Cully
|
r4536 | |||
def catfile(self, rev, type): | ||||
timeless
|
r28365 | if rev == nodemod.nullhex: | ||
Brodie Rao
|
r16687 | raise IOError | ||
David Schleimer
|
r21630 | self.catfilepipe[0].write(rev+'\n') | ||
self.catfilepipe[0].flush() | ||||
info = self.catfilepipe[1].readline().split() | ||||
if info[1] != type: | ||||
Pierre-Yves David
|
r26587 | raise error.Abort(_('cannot read %r object at %s') % (type, rev)) | ||
David Schleimer
|
r21630 | size = int(info[2]) | ||
data = self.catfilepipe[1].read(size) | ||||
if len(data) < size: | ||||
Pierre-Yves David
|
r26587 | raise error.Abort(_('cannot read %r object at %s: unexpected size') | ||
FUJIWARA Katsunori
|
r21958 | % (type, rev)) | ||
David Schleimer
|
r21630 | # read the trailing newline | ||
self.catfilepipe[1].read(1) | ||||
Patrick Mezard
|
r10986 | return data | ||
Brendan Cully
|
r4536 | |||
def getfile(self, name, rev): | ||||
timeless
|
r28365 | if rev == nodemod.nullhex: | ||
Mads Kiilerich
|
r22296 | return None, None | ||
YaNan Xu
|
r17929 | 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)] | ||||
Patrick Mezard
|
r11134 | return data, mode | ||
Brendan Cully
|
r4536 | |||
YaNan Xu
|
r17929 | def submoditer(self): | ||
timeless
|
r28365 | null = nodemod.nullhex | ||
YaNan Xu
|
r17929 | 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() | ||||
Durham Goode
|
r25698 | # Each item in .gitmodules starts with whitespace that cant be parsed | ||
c.parse('.gitmodules', '\n'.join(line.strip() for line in | ||||
content.split('\n'))) | ||||
YaNan Xu
|
r17929 | 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): | ||||
Mateusz Kwapich
|
r28660 | modules, ret = self.gitrun('show', '%s:%s' % (version, '.gitmodules')) | ||
YaNan Xu
|
r17929 | if ret: | ||
Durham Goode
|
r25699 | # 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 | ||||
YaNan Xu
|
r17929 | for m in self.submodules: | ||
Mateusz Kwapich
|
r28660 | node, ret = self.gitrun('rev-parse', '%s:%s' % (version, m.path)) | ||
YaNan Xu
|
r17929 | if ret: | ||
continue | ||||
m.node = node.strip() | ||||
Mads Kiilerich
|
r22300 | def getchanges(self, version, full): | ||
if full: | ||||
timeless@mozdev.org
|
r26779 | raise error.Abort(_("convert from git does not support --full")) | ||
Brendan Cully
|
r4536 | self.modecache = {} | ||
Mateusz Kwapich
|
r28660 | cmd = ['diff-tree','-z', '--root', '-m', '-r'] + self.simopt + [version] | ||
output, status = self.gitrun(*cmd) | ||||
if status: | ||||
raise error.Abort(_('cannot read changes in %s') % version) | ||||
Brendan Cully
|
r4536 | changes = [] | ||
Siddharth Agarwal
|
r22470 | copies = {} | ||
Benoit Boissinot
|
r8456 | seen = set() | ||
Patrick Mezard
|
r7242 | entry = None | ||
Siddharth Agarwal
|
r22469 | subexists = [False] | ||
subdeleted = [False] | ||||
Mateusz Kwapich
|
r28660 | difftree = output.split('\x00') | ||
Siddharth Agarwal
|
r22467 | lcount = len(difftree) | ||
i = 0 | ||||
Siddharth Agarwal
|
r22469 | |||
Boris Feld
|
r34163 | skipsubmodules = self.ui.configbool('convert', 'git.skipsubmodules') | ||
Siddharth Agarwal
|
r22470 | def add(entry, f, isdest): | ||
Siddharth Agarwal
|
r22469 | seen.add(f) | ||
h = entry[3] | ||||
p = (entry[1] == "100755") | ||||
s = (entry[1] == "120000") | ||||
Siddharth Agarwal
|
r22470 | renamesource = (not isdest and entry[4][0] == 'R') | ||
Siddharth Agarwal
|
r22469 | |||
if f == '.gitmodules': | ||||
Durham Goode
|
r26077 | if skipsubmodules: | ||
return | ||||
Siddharth Agarwal
|
r22469 | subexists[0] = True | ||
Siddharth Agarwal
|
r22470 | if entry[4] == 'D' or renamesource: | ||
Siddharth Agarwal
|
r22469 | subdeleted[0] = True | ||
timeless
|
r28365 | changes.append(('.hgsub', nodemod.nullhex)) | ||
Siddharth Agarwal
|
r22469 | else: | ||
changes.append(('.hgsub', '')) | ||||
elif entry[1] == '160000' or entry[0] == ':160000': | ||||
Durham Goode
|
r26077 | if not skipsubmodules: | ||
subexists[0] = True | ||||
Siddharth Agarwal
|
r22469 | else: | ||
Siddharth Agarwal
|
r22470 | if renamesource: | ||
timeless
|
r28365 | h = nodemod.nullhex | ||
Siddharth Agarwal
|
r22469 | self.modecache[(f, h)] = (p and "x") or (s and "l") or "" | ||
changes.append((f, h)) | ||||
Siddharth Agarwal
|
r22467 | while i < lcount: | ||
l = difftree[i] | ||||
i += 1 | ||||
Patrick Mezard
|
r7242 | if not entry: | ||
if not l.startswith(':'): | ||||
continue | ||||
Siddharth Agarwal
|
r22468 | entry = l.split() | ||
Alexis S. L. Carvalho
|
r5335 | continue | ||
Patrick Mezard
|
r7242 | f = l | ||
Durham Goode
|
r25997 | if entry[4][0] == 'C': | ||
copysrc = f | ||||
copydest = difftree[i] | ||||
i += 1 | ||||
f = copydest | ||||
copies[copydest] = copysrc | ||||
Patrick Mezard
|
r7242 | if f not in seen: | ||
Siddharth Agarwal
|
r22470 | 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. | ||||
Durham Goode
|
r25997 | if entry[4][0] == 'R': | ||
# rename: next line is the destination | ||||
Siddharth Agarwal
|
r22470 | 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 | ||||
Patrick Mezard
|
r7242 | entry = None | ||
YaNan Xu
|
r17929 | |||
Siddharth Agarwal
|
r22469 | if subexists[0]: | ||
if subdeleted[0]: | ||||
timeless
|
r28365 | changes.append(('.hgsubstate', nodemod.nullhex)) | ||
FUJIWARA Katsunori
|
r21868 | else: | ||
self.retrievegitmodules(version) | ||||
changes.append(('.hgsubstate', '')) | ||||
Mads Kiilerich
|
r24395 | return (changes, copies, set()) | ||
Brendan Cully
|
r4536 | |||
def getcommit(self, version): | ||||
c = self.catfile(version, "commit") # read the commit hash | ||||
end = c.find("\n\n") | ||||
Matt Mackall
|
r10282 | message = c[end + 2:] | ||
Brendan Cully
|
r4759 | message = self.recode(message) | ||
Brendan Cully
|
r4536 | l = c[:end].splitlines() | ||
parents = [] | ||||
Richard Quirk
|
r8271 | author = committer = None | ||
Gregory Szorc
|
r30660 | extra = {} | ||
Brendan Cully
|
r4536 | 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] | ||||
Brendan Cully
|
r4759 | author = self.recode(author) | ||
Brendan Cully
|
r4536 | if n == "committer": | ||
p = v.split() | ||||
tm, tz = p[-2:] | ||||
committer = " ".join(p[:-2]) | ||||
if committer[0] == "<": committer = committer[1:-1] | ||||
Brendan Cully
|
r4759 | committer = self.recode(committer) | ||
Matt Mackall
|
r10282 | if n == "parent": | ||
parents.append(v) | ||||
Gregory Szorc
|
r30660 | if n in self.copyextrakeys: | ||
extra[n] = v | ||||
Brendan Cully
|
r4536 | |||
Gregory Szorc
|
r30813 | if self.committeractions['dropcommitter']: | ||
committer = None | ||||
elif self.committeractions['replaceauthor']: | ||||
author = committer | ||||
if committer: | ||||
messagealways = self.committeractions['messagealways'] | ||||
messagedifferent = self.committeractions['messagedifferent'] | ||||
if messagealways: | ||||
message += '\n%s %s\n' % (messagealways, committer) | ||||
elif messagedifferent and author != committer: | ||||
message += '\n%s %s\n' % (messagedifferent, committer) | ||||
Brendan Cully
|
r4536 | tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:] | ||
tz = -int(tzs) * (int(tzh) * 3600 + int(tzm)) | ||||
date = tm + " " + str(tz) | ||||
Boris Feld
|
r34161 | saverev = self.ui.configbool('convert', 'git.saverev') | ||
Brendan Cully
|
r4536 | |||
timeless
|
r28365 | c = common.commit(parents=parents, date=date, author=author, | ||
desc=message, | ||||
Gregory Szorc
|
r30660 | rev=version, | ||
Gregory Szorc
|
r30661 | extra=extra, | ||
saverev=saverev) | ||||
Brendan Cully
|
r4536 | return c | ||
Augie Fackler
|
r22413 | def numcommits(self): | ||
Mateusz Kwapich
|
r28660 | output, ret = self.gitrunlines('rev-list', '--all') | ||
if ret: | ||||
raise error.Abort(_('cannot retrieve number of commits in %s') \ | ||||
% self.path) | ||||
return len(output) | ||||
Augie Fackler
|
r22413 | |||
Brendan Cully
|
r4536 | def gettags(self): | ||
tags = {} | ||||
Edouard Gomez
|
r16259 | alltags = {} | ||
Mateusz Kwapich
|
r28660 | output, status = self.gitrunlines('ls-remote', '--tags', self.path) | ||
if status: | ||||
raise error.Abort(_('cannot read tags from %s') % self.path) | ||||
Brendan Cully
|
r4536 | prefix = 'refs/tags/' | ||
Edouard Gomez
|
r16259 | |||
# Build complete list of tags, both annotated and bare ones | ||||
Mateusz Kwapich
|
r28660 | for line in output: | ||
Brendan Cully
|
r4536 | line = line.strip() | ||
Augie Fackler
|
r18572 | if line.startswith("error:") or line.startswith("fatal:"): | ||
Pierre-Yves David
|
r26587 | raise error.Abort(_('cannot read tags from %s') % self.path) | ||
Brendan Cully
|
r4536 | node, tag = line.split(None, 1) | ||
if not tag.startswith(prefix): | ||||
continue | ||||
Edouard Gomez
|
r16259 | alltags[tag[len(prefix):]] = node | ||
Brendan Cully
|
r4536 | |||
Edouard Gomez
|
r16259 | # 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] | ||||
Brendan Cully
|
r4536 | return tags | ||
Alexis S. L. Carvalho
|
r5380 | |||
def getchangedfiles(self, version, i): | ||||
changes = [] | ||||
if i is None: | ||||
Mateusz Kwapich
|
r28660 | output, status = self.gitrunlines('diff-tree', '--root', '-m', | ||
'-r', version) | ||||
if status: | ||||
raise error.Abort(_('cannot read changes in %s') % version) | ||||
for l in output: | ||||
Alexis S. L. Carvalho
|
r5380 | if "\t" not in l: | ||
continue | ||||
m, f = l[:-1].split("\t") | ||||
changes.append(f) | ||||
else: | ||||
Mateusz Kwapich
|
r28660 | output, status = self.gitrunlines('diff-tree', '--name-only', | ||
'--root', '-r', version, | ||||
'%s^%s' % (version, i + 1), '--') | ||||
Julien Cristau
|
r28816 | if status: | ||
raise error.Abort(_('cannot read changes in %s') % version) | ||||
Mateusz Kwapich
|
r28660 | changes = [f.rstrip('\n') for f in output] | ||
Alexis S. L. Carvalho
|
r5380 | |||
return changes | ||||
Edouard Gomez
|
r13756 | |||
def getbookmarks(self): | ||||
bookmarks = {} | ||||
Durham Goode
|
r25905 | # Handle local and remote branches | ||
Boris Feld
|
r34159 | remoteprefix = self.ui.config('convert', 'git.remoteprefix') | ||
Durham Goode
|
r25905 | reftypes = [ | ||
# (git prefix, hg prefix) | ||||
('refs/remotes/origin/', remoteprefix + '/'), | ||||
('refs/heads/', '') | ||||
] | ||||
Edouard Gomez
|
r13756 | |||
Martin von Zweigbergk
|
r32291 | exclude = { | ||
Durham Goode
|
r25905 | 'refs/remotes/origin/HEAD', | ||
Martin von Zweigbergk
|
r32291 | } | ||
Edouard Gomez
|
r13756 | |||
Durham Goode
|
r25905 | try: | ||
Mateusz Kwapich
|
r28660 | output, status = self.gitrunlines('show-ref') | ||
for line in output: | ||||
Durham Goode
|
r25905 | line = line.strip() | ||
rev, name = line.split(None, 1) | ||||
# Process each type of branch | ||||
for gitprefix, hgprefix in reftypes: | ||||
if not name.startswith(gitprefix) or name in exclude: | ||||
Edouard Gomez
|
r13756 | continue | ||
Durham Goode
|
r25905 | name = '%s%s' % (hgprefix, name[len(gitprefix):]) | ||
Edouard Gomez
|
r13756 | bookmarks[name] = rev | ||
Durham Goode
|
r25905 | except Exception: | ||
pass | ||||
Edouard Gomez
|
r13756 | |||
return bookmarks | ||||
Ben Goswami
|
r19121 | |||
Sean Farley
|
r20373 | def checkrevformat(self, revstr, mapname='splicemap'): | ||
Ben Goswami
|
r19121 | """ git revision string is a 40 byte hex """ | ||
Sean Farley
|
r20373 | self.checkhexformat(revstr, mapname) | ||