##// END OF EJS Templates
convert: rewrite gitpipe to use common.commandline (SEC)...
Mateusz Kwapich -
r28662:80cac1de stable
parent child Browse files
Show More
@@ -1,478 +1,481 b''
1 # common.py - common code for the convert extension
1 # common.py - common code for the convert extension
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import base64, errno, subprocess, os, datetime, re
8 import base64, errno, subprocess, os, datetime, re
9 import cPickle as pickle
9 import cPickle as pickle
10 from mercurial import phases, util, error
10 from mercurial import phases, util, error
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12
12
13 propertycache = util.propertycache
13 propertycache = util.propertycache
14
14
15 def encodeargs(args):
15 def encodeargs(args):
16 def encodearg(s):
16 def encodearg(s):
17 lines = base64.encodestring(s)
17 lines = base64.encodestring(s)
18 lines = [l.splitlines()[0] for l in lines]
18 lines = [l.splitlines()[0] for l in lines]
19 return ''.join(lines)
19 return ''.join(lines)
20
20
21 s = pickle.dumps(args)
21 s = pickle.dumps(args)
22 return encodearg(s)
22 return encodearg(s)
23
23
24 def decodeargs(s):
24 def decodeargs(s):
25 s = base64.decodestring(s)
25 s = base64.decodestring(s)
26 return pickle.loads(s)
26 return pickle.loads(s)
27
27
28 class MissingTool(Exception):
28 class MissingTool(Exception):
29 pass
29 pass
30
30
31 def checktool(exe, name=None, abort=True):
31 def checktool(exe, name=None, abort=True):
32 name = name or exe
32 name = name or exe
33 if not util.findexe(exe):
33 if not util.findexe(exe):
34 if abort:
34 if abort:
35 exc = error.Abort
35 exc = error.Abort
36 else:
36 else:
37 exc = MissingTool
37 exc = MissingTool
38 raise exc(_('cannot find required "%s" tool') % name)
38 raise exc(_('cannot find required "%s" tool') % name)
39
39
40 class NoRepo(Exception):
40 class NoRepo(Exception):
41 pass
41 pass
42
42
43 SKIPREV = 'SKIP'
43 SKIPREV = 'SKIP'
44
44
45 class commit(object):
45 class commit(object):
46 def __init__(self, author, date, desc, parents, branch=None, rev=None,
46 def __init__(self, author, date, desc, parents, branch=None, rev=None,
47 extra={}, sortkey=None, saverev=True, phase=phases.draft):
47 extra={}, sortkey=None, saverev=True, phase=phases.draft):
48 self.author = author or 'unknown'
48 self.author = author or 'unknown'
49 self.date = date or '0 0'
49 self.date = date or '0 0'
50 self.desc = desc
50 self.desc = desc
51 self.parents = parents
51 self.parents = parents
52 self.branch = branch
52 self.branch = branch
53 self.rev = rev
53 self.rev = rev
54 self.extra = extra
54 self.extra = extra
55 self.sortkey = sortkey
55 self.sortkey = sortkey
56 self.saverev = saverev
56 self.saverev = saverev
57 self.phase = phase
57 self.phase = phase
58
58
59 class converter_source(object):
59 class converter_source(object):
60 """Conversion source interface"""
60 """Conversion source interface"""
61
61
62 def __init__(self, ui, path=None, revs=None):
62 def __init__(self, ui, path=None, revs=None):
63 """Initialize conversion source (or raise NoRepo("message")
63 """Initialize conversion source (or raise NoRepo("message")
64 exception if path is not a valid repository)"""
64 exception if path is not a valid repository)"""
65 self.ui = ui
65 self.ui = ui
66 self.path = path
66 self.path = path
67 self.revs = revs
67 self.revs = revs
68
68
69 self.encoding = 'utf-8'
69 self.encoding = 'utf-8'
70
70
71 def checkhexformat(self, revstr, mapname='splicemap'):
71 def checkhexformat(self, revstr, mapname='splicemap'):
72 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
72 """ fails if revstr is not a 40 byte hex. mercurial and git both uses
73 such format for their revision numbering
73 such format for their revision numbering
74 """
74 """
75 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
75 if not re.match(r'[0-9a-fA-F]{40,40}$', revstr):
76 raise error.Abort(_('%s entry %s is not a valid revision'
76 raise error.Abort(_('%s entry %s is not a valid revision'
77 ' identifier') % (mapname, revstr))
77 ' identifier') % (mapname, revstr))
78
78
79 def before(self):
79 def before(self):
80 pass
80 pass
81
81
82 def after(self):
82 def after(self):
83 pass
83 pass
84
84
85 def targetfilebelongstosource(self, targetfilename):
85 def targetfilebelongstosource(self, targetfilename):
86 """Returns true if the given targetfile belongs to the source repo. This
86 """Returns true if the given targetfile belongs to the source repo. This
87 is useful when only a subdirectory of the target belongs to the source
87 is useful when only a subdirectory of the target belongs to the source
88 repo."""
88 repo."""
89 # For normal full repo converts, this is always True.
89 # For normal full repo converts, this is always True.
90 return True
90 return True
91
91
92 def setrevmap(self, revmap):
92 def setrevmap(self, revmap):
93 """set the map of already-converted revisions"""
93 """set the map of already-converted revisions"""
94 pass
94 pass
95
95
96 def getheads(self):
96 def getheads(self):
97 """Return a list of this repository's heads"""
97 """Return a list of this repository's heads"""
98 raise NotImplementedError
98 raise NotImplementedError
99
99
100 def getfile(self, name, rev):
100 def getfile(self, name, rev):
101 """Return a pair (data, mode) where data is the file content
101 """Return a pair (data, mode) where data is the file content
102 as a string and mode one of '', 'x' or 'l'. rev is the
102 as a string and mode one of '', 'x' or 'l'. rev is the
103 identifier returned by a previous call to getchanges().
103 identifier returned by a previous call to getchanges().
104 Data is None if file is missing/deleted in rev.
104 Data is None if file is missing/deleted in rev.
105 """
105 """
106 raise NotImplementedError
106 raise NotImplementedError
107
107
108 def getchanges(self, version, full):
108 def getchanges(self, version, full):
109 """Returns a tuple of (files, copies, cleanp2).
109 """Returns a tuple of (files, copies, cleanp2).
110
110
111 files is a sorted list of (filename, id) tuples for all files
111 files is a sorted list of (filename, id) tuples for all files
112 changed between version and its first parent returned by
112 changed between version and its first parent returned by
113 getcommit(). If full, all files in that revision is returned.
113 getcommit(). If full, all files in that revision is returned.
114 id is the source revision id of the file.
114 id is the source revision id of the file.
115
115
116 copies is a dictionary of dest: source
116 copies is a dictionary of dest: source
117
117
118 cleanp2 is the set of files filenames that are clean against p2.
118 cleanp2 is the set of files filenames that are clean against p2.
119 (Files that are clean against p1 are already not in files (unless
119 (Files that are clean against p1 are already not in files (unless
120 full). This makes it possible to handle p2 clean files similarly.)
120 full). This makes it possible to handle p2 clean files similarly.)
121 """
121 """
122 raise NotImplementedError
122 raise NotImplementedError
123
123
124 def getcommit(self, version):
124 def getcommit(self, version):
125 """Return the commit object for version"""
125 """Return the commit object for version"""
126 raise NotImplementedError
126 raise NotImplementedError
127
127
128 def numcommits(self):
128 def numcommits(self):
129 """Return the number of commits in this source.
129 """Return the number of commits in this source.
130
130
131 If unknown, return None.
131 If unknown, return None.
132 """
132 """
133 return None
133 return None
134
134
135 def gettags(self):
135 def gettags(self):
136 """Return the tags as a dictionary of name: revision
136 """Return the tags as a dictionary of name: revision
137
137
138 Tag names must be UTF-8 strings.
138 Tag names must be UTF-8 strings.
139 """
139 """
140 raise NotImplementedError
140 raise NotImplementedError
141
141
142 def recode(self, s, encoding=None):
142 def recode(self, s, encoding=None):
143 if not encoding:
143 if not encoding:
144 encoding = self.encoding or 'utf-8'
144 encoding = self.encoding or 'utf-8'
145
145
146 if isinstance(s, unicode):
146 if isinstance(s, unicode):
147 return s.encode("utf-8")
147 return s.encode("utf-8")
148 try:
148 try:
149 return s.decode(encoding).encode("utf-8")
149 return s.decode(encoding).encode("utf-8")
150 except UnicodeError:
150 except UnicodeError:
151 try:
151 try:
152 return s.decode("latin-1").encode("utf-8")
152 return s.decode("latin-1").encode("utf-8")
153 except UnicodeError:
153 except UnicodeError:
154 return s.decode(encoding, "replace").encode("utf-8")
154 return s.decode(encoding, "replace").encode("utf-8")
155
155
156 def getchangedfiles(self, rev, i):
156 def getchangedfiles(self, rev, i):
157 """Return the files changed by rev compared to parent[i].
157 """Return the files changed by rev compared to parent[i].
158
158
159 i is an index selecting one of the parents of rev. The return
159 i is an index selecting one of the parents of rev. The return
160 value should be the list of files that are different in rev and
160 value should be the list of files that are different in rev and
161 this parent.
161 this parent.
162
162
163 If rev has no parents, i is None.
163 If rev has no parents, i is None.
164
164
165 This function is only needed to support --filemap
165 This function is only needed to support --filemap
166 """
166 """
167 raise NotImplementedError
167 raise NotImplementedError
168
168
169 def converted(self, rev, sinkrev):
169 def converted(self, rev, sinkrev):
170 '''Notify the source that a revision has been converted.'''
170 '''Notify the source that a revision has been converted.'''
171 pass
171 pass
172
172
173 def hasnativeorder(self):
173 def hasnativeorder(self):
174 """Return true if this source has a meaningful, native revision
174 """Return true if this source has a meaningful, native revision
175 order. For instance, Mercurial revisions are store sequentially
175 order. For instance, Mercurial revisions are store sequentially
176 while there is no such global ordering with Darcs.
176 while there is no such global ordering with Darcs.
177 """
177 """
178 return False
178 return False
179
179
180 def hasnativeclose(self):
180 def hasnativeclose(self):
181 """Return true if this source has ability to close branch.
181 """Return true if this source has ability to close branch.
182 """
182 """
183 return False
183 return False
184
184
185 def lookuprev(self, rev):
185 def lookuprev(self, rev):
186 """If rev is a meaningful revision reference in source, return
186 """If rev is a meaningful revision reference in source, return
187 the referenced identifier in the same format used by getcommit().
187 the referenced identifier in the same format used by getcommit().
188 return None otherwise.
188 return None otherwise.
189 """
189 """
190 return None
190 return None
191
191
192 def getbookmarks(self):
192 def getbookmarks(self):
193 """Return the bookmarks as a dictionary of name: revision
193 """Return the bookmarks as a dictionary of name: revision
194
194
195 Bookmark names are to be UTF-8 strings.
195 Bookmark names are to be UTF-8 strings.
196 """
196 """
197 return {}
197 return {}
198
198
199 def checkrevformat(self, revstr, mapname='splicemap'):
199 def checkrevformat(self, revstr, mapname='splicemap'):
200 """revstr is a string that describes a revision in the given
200 """revstr is a string that describes a revision in the given
201 source control system. Return true if revstr has correct
201 source control system. Return true if revstr has correct
202 format.
202 format.
203 """
203 """
204 return True
204 return True
205
205
206 class converter_sink(object):
206 class converter_sink(object):
207 """Conversion sink (target) interface"""
207 """Conversion sink (target) interface"""
208
208
209 def __init__(self, ui, path):
209 def __init__(self, ui, path):
210 """Initialize conversion sink (or raise NoRepo("message")
210 """Initialize conversion sink (or raise NoRepo("message")
211 exception if path is not a valid repository)
211 exception if path is not a valid repository)
212
212
213 created is a list of paths to remove if a fatal error occurs
213 created is a list of paths to remove if a fatal error occurs
214 later"""
214 later"""
215 self.ui = ui
215 self.ui = ui
216 self.path = path
216 self.path = path
217 self.created = []
217 self.created = []
218
218
219 def revmapfile(self):
219 def revmapfile(self):
220 """Path to a file that will contain lines
220 """Path to a file that will contain lines
221 source_rev_id sink_rev_id
221 source_rev_id sink_rev_id
222 mapping equivalent revision identifiers for each system."""
222 mapping equivalent revision identifiers for each system."""
223 raise NotImplementedError
223 raise NotImplementedError
224
224
225 def authorfile(self):
225 def authorfile(self):
226 """Path to a file that will contain lines
226 """Path to a file that will contain lines
227 srcauthor=dstauthor
227 srcauthor=dstauthor
228 mapping equivalent authors identifiers for each system."""
228 mapping equivalent authors identifiers for each system."""
229 return None
229 return None
230
230
231 def putcommit(self, files, copies, parents, commit, source, revmap, full,
231 def putcommit(self, files, copies, parents, commit, source, revmap, full,
232 cleanp2):
232 cleanp2):
233 """Create a revision with all changed files listed in 'files'
233 """Create a revision with all changed files listed in 'files'
234 and having listed parents. 'commit' is a commit object
234 and having listed parents. 'commit' is a commit object
235 containing at a minimum the author, date, and message for this
235 containing at a minimum the author, date, and message for this
236 changeset. 'files' is a list of (path, version) tuples,
236 changeset. 'files' is a list of (path, version) tuples,
237 'copies' is a dictionary mapping destinations to sources,
237 'copies' is a dictionary mapping destinations to sources,
238 'source' is the source repository, and 'revmap' is a mapfile
238 'source' is the source repository, and 'revmap' is a mapfile
239 of source revisions to converted revisions. Only getfile() and
239 of source revisions to converted revisions. Only getfile() and
240 lookuprev() should be called on 'source'. 'full' means that 'files'
240 lookuprev() should be called on 'source'. 'full' means that 'files'
241 is complete and all other files should be removed.
241 is complete and all other files should be removed.
242 'cleanp2' is a set of the filenames that are unchanged from p2
242 'cleanp2' is a set of the filenames that are unchanged from p2
243 (only in the common merge case where there two parents).
243 (only in the common merge case where there two parents).
244
244
245 Note that the sink repository is not told to update itself to
245 Note that the sink repository is not told to update itself to
246 a particular revision (or even what that revision would be)
246 a particular revision (or even what that revision would be)
247 before it receives the file data.
247 before it receives the file data.
248 """
248 """
249 raise NotImplementedError
249 raise NotImplementedError
250
250
251 def puttags(self, tags):
251 def puttags(self, tags):
252 """Put tags into sink.
252 """Put tags into sink.
253
253
254 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
254 tags: {tagname: sink_rev_id, ...} where tagname is an UTF-8 string.
255 Return a pair (tag_revision, tag_parent_revision), or (None, None)
255 Return a pair (tag_revision, tag_parent_revision), or (None, None)
256 if nothing was changed.
256 if nothing was changed.
257 """
257 """
258 raise NotImplementedError
258 raise NotImplementedError
259
259
260 def setbranch(self, branch, pbranches):
260 def setbranch(self, branch, pbranches):
261 """Set the current branch name. Called before the first putcommit
261 """Set the current branch name. Called before the first putcommit
262 on the branch.
262 on the branch.
263 branch: branch name for subsequent commits
263 branch: branch name for subsequent commits
264 pbranches: (converted parent revision, parent branch) tuples"""
264 pbranches: (converted parent revision, parent branch) tuples"""
265 pass
265 pass
266
266
267 def setfilemapmode(self, active):
267 def setfilemapmode(self, active):
268 """Tell the destination that we're using a filemap
268 """Tell the destination that we're using a filemap
269
269
270 Some converter_sources (svn in particular) can claim that a file
270 Some converter_sources (svn in particular) can claim that a file
271 was changed in a revision, even if there was no change. This method
271 was changed in a revision, even if there was no change. This method
272 tells the destination that we're using a filemap and that it should
272 tells the destination that we're using a filemap and that it should
273 filter empty revisions.
273 filter empty revisions.
274 """
274 """
275 pass
275 pass
276
276
277 def before(self):
277 def before(self):
278 pass
278 pass
279
279
280 def after(self):
280 def after(self):
281 pass
281 pass
282
282
283 def putbookmarks(self, bookmarks):
283 def putbookmarks(self, bookmarks):
284 """Put bookmarks into sink.
284 """Put bookmarks into sink.
285
285
286 bookmarks: {bookmarkname: sink_rev_id, ...}
286 bookmarks: {bookmarkname: sink_rev_id, ...}
287 where bookmarkname is an UTF-8 string.
287 where bookmarkname is an UTF-8 string.
288 """
288 """
289 pass
289 pass
290
290
291 def hascommitfrommap(self, rev):
291 def hascommitfrommap(self, rev):
292 """Return False if a rev mentioned in a filemap is known to not be
292 """Return False if a rev mentioned in a filemap is known to not be
293 present."""
293 present."""
294 raise NotImplementedError
294 raise NotImplementedError
295
295
296 def hascommitforsplicemap(self, rev):
296 def hascommitforsplicemap(self, rev):
297 """This method is for the special needs for splicemap handling and not
297 """This method is for the special needs for splicemap handling and not
298 for general use. Returns True if the sink contains rev, aborts on some
298 for general use. Returns True if the sink contains rev, aborts on some
299 special cases."""
299 special cases."""
300 raise NotImplementedError
300 raise NotImplementedError
301
301
302 class commandline(object):
302 class commandline(object):
303 def __init__(self, ui, command):
303 def __init__(self, ui, command):
304 self.ui = ui
304 self.ui = ui
305 self.command = command
305 self.command = command
306
306
307 def prerun(self):
307 def prerun(self):
308 pass
308 pass
309
309
310 def postrun(self):
310 def postrun(self):
311 pass
311 pass
312
312
313 def _cmdline(self, cmd, *args, **kwargs):
313 def _cmdline(self, cmd, *args, **kwargs):
314 cmdline = [self.command, cmd] + list(args)
314 cmdline = [self.command, cmd] + list(args)
315 for k, v in kwargs.iteritems():
315 for k, v in kwargs.iteritems():
316 if len(k) == 1:
316 if len(k) == 1:
317 cmdline.append('-' + k)
317 cmdline.append('-' + k)
318 else:
318 else:
319 cmdline.append('--' + k.replace('_', '-'))
319 cmdline.append('--' + k.replace('_', '-'))
320 try:
320 try:
321 if len(k) == 1:
321 if len(k) == 1:
322 cmdline.append('' + v)
322 cmdline.append('' + v)
323 else:
323 else:
324 cmdline[-1] += '=' + v
324 cmdline[-1] += '=' + v
325 except TypeError:
325 except TypeError:
326 pass
326 pass
327 cmdline = [util.shellquote(arg) for arg in cmdline]
327 cmdline = [util.shellquote(arg) for arg in cmdline]
328 if not self.ui.debugflag:
328 if not self.ui.debugflag:
329 cmdline += ['2>', os.devnull]
329 cmdline += ['2>', os.devnull]
330 cmdline = ' '.join(cmdline)
330 cmdline = ' '.join(cmdline)
331 return cmdline
331 return cmdline
332
332
333 def _run(self, cmd, *args, **kwargs):
333 def _run(self, cmd, *args, **kwargs):
334 def popen(cmdline):
334 def popen(cmdline):
335 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
335 p = subprocess.Popen(cmdline, shell=True, bufsize=-1,
336 close_fds=util.closefds,
336 close_fds=util.closefds,
337 stdout=subprocess.PIPE)
337 stdout=subprocess.PIPE)
338 return p
338 return p
339 return self._dorun(popen, cmd, *args, **kwargs)
339 return self._dorun(popen, cmd, *args, **kwargs)
340
340
341 def _run2(self, cmd, *args, **kwargs):
341 def _run2(self, cmd, *args, **kwargs):
342 return self._dorun(util.popen2, cmd, *args, **kwargs)
342 return self._dorun(util.popen2, cmd, *args, **kwargs)
343
343
344 def _run3(self, cmd, *args, **kwargs):
345 return self._dorun(util.popen3, cmd, *args, **kwargs)
346
344 def _dorun(self, openfunc, cmd, *args, **kwargs):
347 def _dorun(self, openfunc, cmd, *args, **kwargs):
345 cmdline = self._cmdline(cmd, *args, **kwargs)
348 cmdline = self._cmdline(cmd, *args, **kwargs)
346 self.ui.debug('running: %s\n' % (cmdline,))
349 self.ui.debug('running: %s\n' % (cmdline,))
347 self.prerun()
350 self.prerun()
348 try:
351 try:
349 return openfunc(cmdline)
352 return openfunc(cmdline)
350 finally:
353 finally:
351 self.postrun()
354 self.postrun()
352
355
353 def run(self, cmd, *args, **kwargs):
356 def run(self, cmd, *args, **kwargs):
354 p = self._run(cmd, *args, **kwargs)
357 p = self._run(cmd, *args, **kwargs)
355 output = p.communicate()[0]
358 output = p.communicate()[0]
356 self.ui.debug(output)
359 self.ui.debug(output)
357 return output, p.returncode
360 return output, p.returncode
358
361
359 def runlines(self, cmd, *args, **kwargs):
362 def runlines(self, cmd, *args, **kwargs):
360 p = self._run(cmd, *args, **kwargs)
363 p = self._run(cmd, *args, **kwargs)
361 output = p.stdout.readlines()
364 output = p.stdout.readlines()
362 p.wait()
365 p.wait()
363 self.ui.debug(''.join(output))
366 self.ui.debug(''.join(output))
364 return output, p.returncode
367 return output, p.returncode
365
368
366 def checkexit(self, status, output=''):
369 def checkexit(self, status, output=''):
367 if status:
370 if status:
368 if output:
371 if output:
369 self.ui.warn(_('%s error:\n') % self.command)
372 self.ui.warn(_('%s error:\n') % self.command)
370 self.ui.warn(output)
373 self.ui.warn(output)
371 msg = util.explainexit(status)[0]
374 msg = util.explainexit(status)[0]
372 raise error.Abort('%s %s' % (self.command, msg))
375 raise error.Abort('%s %s' % (self.command, msg))
373
376
374 def run0(self, cmd, *args, **kwargs):
377 def run0(self, cmd, *args, **kwargs):
375 output, status = self.run(cmd, *args, **kwargs)
378 output, status = self.run(cmd, *args, **kwargs)
376 self.checkexit(status, output)
379 self.checkexit(status, output)
377 return output
380 return output
378
381
379 def runlines0(self, cmd, *args, **kwargs):
382 def runlines0(self, cmd, *args, **kwargs):
380 output, status = self.runlines(cmd, *args, **kwargs)
383 output, status = self.runlines(cmd, *args, **kwargs)
381 self.checkexit(status, ''.join(output))
384 self.checkexit(status, ''.join(output))
382 return output
385 return output
383
386
384 @propertycache
387 @propertycache
385 def argmax(self):
388 def argmax(self):
386 # POSIX requires at least 4096 bytes for ARG_MAX
389 # POSIX requires at least 4096 bytes for ARG_MAX
387 argmax = 4096
390 argmax = 4096
388 try:
391 try:
389 argmax = os.sysconf("SC_ARG_MAX")
392 argmax = os.sysconf("SC_ARG_MAX")
390 except (AttributeError, ValueError):
393 except (AttributeError, ValueError):
391 pass
394 pass
392
395
393 # Windows shells impose their own limits on command line length,
396 # Windows shells impose their own limits on command line length,
394 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
397 # down to 2047 bytes for cmd.exe under Windows NT/2k and 2500 bytes
395 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
398 # for older 4nt.exe. See http://support.microsoft.com/kb/830473 for
396 # details about cmd.exe limitations.
399 # details about cmd.exe limitations.
397
400
398 # Since ARG_MAX is for command line _and_ environment, lower our limit
401 # Since ARG_MAX is for command line _and_ environment, lower our limit
399 # (and make happy Windows shells while doing this).
402 # (and make happy Windows shells while doing this).
400 return argmax // 2 - 1
403 return argmax // 2 - 1
401
404
402 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
405 def _limit_arglist(self, arglist, cmd, *args, **kwargs):
403 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
406 cmdlen = len(self._cmdline(cmd, *args, **kwargs))
404 limit = self.argmax - cmdlen
407 limit = self.argmax - cmdlen
405 bytes = 0
408 bytes = 0
406 fl = []
409 fl = []
407 for fn in arglist:
410 for fn in arglist:
408 b = len(fn) + 3
411 b = len(fn) + 3
409 if bytes + b < limit or len(fl) == 0:
412 if bytes + b < limit or len(fl) == 0:
410 fl.append(fn)
413 fl.append(fn)
411 bytes += b
414 bytes += b
412 else:
415 else:
413 yield fl
416 yield fl
414 fl = [fn]
417 fl = [fn]
415 bytes = b
418 bytes = b
416 if fl:
419 if fl:
417 yield fl
420 yield fl
418
421
419 def xargs(self, arglist, cmd, *args, **kwargs):
422 def xargs(self, arglist, cmd, *args, **kwargs):
420 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
423 for l in self._limit_arglist(arglist, cmd, *args, **kwargs):
421 self.run0(cmd, *(list(args) + l), **kwargs)
424 self.run0(cmd, *(list(args) + l), **kwargs)
422
425
423 class mapfile(dict):
426 class mapfile(dict):
424 def __init__(self, ui, path):
427 def __init__(self, ui, path):
425 super(mapfile, self).__init__()
428 super(mapfile, self).__init__()
426 self.ui = ui
429 self.ui = ui
427 self.path = path
430 self.path = path
428 self.fp = None
431 self.fp = None
429 self.order = []
432 self.order = []
430 self._read()
433 self._read()
431
434
432 def _read(self):
435 def _read(self):
433 if not self.path:
436 if not self.path:
434 return
437 return
435 try:
438 try:
436 fp = open(self.path, 'r')
439 fp = open(self.path, 'r')
437 except IOError as err:
440 except IOError as err:
438 if err.errno != errno.ENOENT:
441 if err.errno != errno.ENOENT:
439 raise
442 raise
440 return
443 return
441 for i, line in enumerate(fp):
444 for i, line in enumerate(fp):
442 line = line.splitlines()[0].rstrip()
445 line = line.splitlines()[0].rstrip()
443 if not line:
446 if not line:
444 # Ignore blank lines
447 # Ignore blank lines
445 continue
448 continue
446 try:
449 try:
447 key, value = line.rsplit(' ', 1)
450 key, value = line.rsplit(' ', 1)
448 except ValueError:
451 except ValueError:
449 raise error.Abort(
452 raise error.Abort(
450 _('syntax error in %s(%d): key/value pair expected')
453 _('syntax error in %s(%d): key/value pair expected')
451 % (self.path, i + 1))
454 % (self.path, i + 1))
452 if key not in self:
455 if key not in self:
453 self.order.append(key)
456 self.order.append(key)
454 super(mapfile, self).__setitem__(key, value)
457 super(mapfile, self).__setitem__(key, value)
455 fp.close()
458 fp.close()
456
459
457 def __setitem__(self, key, value):
460 def __setitem__(self, key, value):
458 if self.fp is None:
461 if self.fp is None:
459 try:
462 try:
460 self.fp = open(self.path, 'a')
463 self.fp = open(self.path, 'a')
461 except IOError as err:
464 except IOError as err:
462 raise error.Abort(_('could not open map file %r: %s') %
465 raise error.Abort(_('could not open map file %r: %s') %
463 (self.path, err.strerror))
466 (self.path, err.strerror))
464 self.fp.write('%s %s\n' % (key, value))
467 self.fp.write('%s %s\n' % (key, value))
465 self.fp.flush()
468 self.fp.flush()
466 super(mapfile, self).__setitem__(key, value)
469 super(mapfile, self).__setitem__(key, value)
467
470
468 def close(self):
471 def close(self):
469 if self.fp:
472 if self.fp:
470 self.fp.close()
473 self.fp.close()
471 self.fp = None
474 self.fp = None
472
475
473 def makedatetimestamp(t):
476 def makedatetimestamp(t):
474 """Like util.makedate() but for time t instead of current time"""
477 """Like util.makedate() but for time t instead of current time"""
475 delta = (datetime.datetime.utcfromtimestamp(t) -
478 delta = (datetime.datetime.utcfromtimestamp(t) -
476 datetime.datetime.fromtimestamp(t))
479 datetime.datetime.fromtimestamp(t))
477 tz = delta.days * 86400 + delta.seconds
480 tz = delta.days * 86400 + delta.seconds
478 return t, tz
481 return t, tz
@@ -1,402 +1,390 b''
1 # git.py - git support for the convert extension
1 # git.py - git support for the convert extension
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import os
8 import os
9 import subprocess
9 import subprocess
10 from mercurial import util, config, error
10 from mercurial import util, config, error
11 from mercurial.node import hex, nullid
11 from mercurial.node import hex, nullid
12 from mercurial.i18n import _
12 from mercurial.i18n import _
13
13
14 from common import NoRepo, commit, converter_source, checktool, commandline
14 from common import NoRepo, commit, converter_source, checktool, commandline
15
15
16 class submodule(object):
16 class submodule(object):
17 def __init__(self, path, node, url):
17 def __init__(self, path, node, url):
18 self.path = path
18 self.path = path
19 self.node = node
19 self.node = node
20 self.url = url
20 self.url = url
21
21
22 def hgsub(self):
22 def hgsub(self):
23 return "%s = [git]%s" % (self.path, self.url)
23 return "%s = [git]%s" % (self.path, self.url)
24
24
25 def hgsubstate(self):
25 def hgsubstate(self):
26 return "%s %s" % (self.node, self.path)
26 return "%s %s" % (self.node, self.path)
27
27
28 class convert_git(converter_source, commandline):
28 class convert_git(converter_source, commandline):
29 # Windows does not support GIT_DIR= construct while other systems
29 # Windows does not support GIT_DIR= construct while other systems
30 # cannot remove environment variable. Just assume none have
30 # cannot remove environment variable. Just assume none have
31 # both issues.
31 # both issues.
32 if util.safehasattr(os, 'unsetenv'):
33 def gitpipe(self, s):
34 prevgitdir = os.environ.get('GIT_DIR')
35 os.environ['GIT_DIR'] = self.path
36 try:
37 return util.popen3(s)
38 finally:
39 if prevgitdir is None:
40 del os.environ['GIT_DIR']
41 else:
42 os.environ['GIT_DIR'] = prevgitdir
43
44 else:
45 def gitpipe(self, s):
46 return util.popen3('GIT_DIR=%s %s' % (self.path, s))
47
32
48 def _gitcmd(self, cmd, *args, **kwargs):
33 def _gitcmd(self, cmd, *args, **kwargs):
49 return cmd('--git-dir=%s' % self.path, *args, **kwargs)
34 return cmd('--git-dir=%s' % self.path, *args, **kwargs)
50
35
51 def gitrun0(self, *args, **kwargs):
36 def gitrun0(self, *args, **kwargs):
52 return self._gitcmd(self.run0, *args, **kwargs)
37 return self._gitcmd(self.run0, *args, **kwargs)
53
38
54 def gitrun(self, *args, **kwargs):
39 def gitrun(self, *args, **kwargs):
55 return self._gitcmd(self.run, *args, **kwargs)
40 return self._gitcmd(self.run, *args, **kwargs)
56
41
57 def gitrunlines0(self, *args, **kwargs):
42 def gitrunlines0(self, *args, **kwargs):
58 return self._gitcmd(self.runlines0, *args, **kwargs)
43 return self._gitcmd(self.runlines0, *args, **kwargs)
59
44
60 def gitrunlines(self, *args, **kwargs):
45 def gitrunlines(self, *args, **kwargs):
61 return self._gitcmd(self.runlines, *args, **kwargs)
46 return self._gitcmd(self.runlines, *args, **kwargs)
62
47
48 def gitpipe(self, *args, **kwargs):
49 return self._gitcmd(self._run3, *args, **kwargs)
50
63 def gitread(self, s):
51 def gitread(self, s):
64 fh = self.gitopen(s)
52 fh = self.gitopen(s)
65 data = fh.read()
53 data = fh.read()
66 return data, fh.close()
54 return data, fh.close()
67
55
68 def __init__(self, ui, path, revs=None):
56 def __init__(self, ui, path, revs=None):
69 super(convert_git, self).__init__(ui, path, revs=revs)
57 super(convert_git, self).__init__(ui, path, revs=revs)
70 commandline.__init__(self, ui, 'git')
58 commandline.__init__(self, ui, 'git')
71
59
72 if os.path.isdir(path + "/.git"):
60 if os.path.isdir(path + "/.git"):
73 path += "/.git"
61 path += "/.git"
74 if not os.path.exists(path + "/objects"):
62 if not os.path.exists(path + "/objects"):
75 raise NoRepo(_("%s does not look like a Git repository") % path)
63 raise NoRepo(_("%s does not look like a Git repository") % path)
76
64
77 # The default value (50) is based on the default for 'git diff'.
65 # The default value (50) is based on the default for 'git diff'.
78 similarity = ui.configint('convert', 'git.similarity', default=50)
66 similarity = ui.configint('convert', 'git.similarity', default=50)
79 if similarity < 0 or similarity > 100:
67 if similarity < 0 or similarity > 100:
80 raise error.Abort(_('similarity must be between 0 and 100'))
68 raise error.Abort(_('similarity must be between 0 and 100'))
81 if similarity > 0:
69 if similarity > 0:
82 self.simopt = ['-C%d%%' % similarity]
70 self.simopt = ['-C%d%%' % similarity]
83 findcopiesharder = ui.configbool('convert', 'git.findcopiesharder',
71 findcopiesharder = ui.configbool('convert', 'git.findcopiesharder',
84 False)
72 False)
85 if findcopiesharder:
73 if findcopiesharder:
86 self.simopt.append('--find-copies-harder')
74 self.simopt.append('--find-copies-harder')
87 else:
75 else:
88 self.simopt = []
76 self.simopt = []
89
77
90 checktool('git', 'git')
78 checktool('git', 'git')
91
79
92 self.path = path
80 self.path = path
93 self.submodules = []
81 self.submodules = []
94
82
95 self.catfilepipe = self.gitpipe('git cat-file --batch')
83 self.catfilepipe = self.gitpipe('cat-file', '--batch')
96
84
97 def after(self):
85 def after(self):
98 for f in self.catfilepipe:
86 for f in self.catfilepipe:
99 f.close()
87 f.close()
100
88
101 def getheads(self):
89 def getheads(self):
102 if not self.revs:
90 if not self.revs:
103 output, status = self.gitrun('rev-parse', '--branches', '--remotes')
91 output, status = self.gitrun('rev-parse', '--branches', '--remotes')
104 heads = output.splitlines()
92 heads = output.splitlines()
105 if status:
93 if status:
106 raise error.Abort(_('cannot retrieve git heads'))
94 raise error.Abort(_('cannot retrieve git heads'))
107 else:
95 else:
108 heads = []
96 heads = []
109 for rev in self.revs:
97 for rev in self.revs:
110 rawhead, ret = self.gitrun('rev-parse', '--verify', rev)
98 rawhead, ret = self.gitrun('rev-parse', '--verify', rev)
111 heads.append(rawhead[:-1])
99 heads.append(rawhead[:-1])
112 if ret:
100 if ret:
113 raise error.Abort(_('cannot retrieve git head "%s"') % rev)
101 raise error.Abort(_('cannot retrieve git head "%s"') % rev)
114 return heads
102 return heads
115
103
116 def catfile(self, rev, type):
104 def catfile(self, rev, type):
117 if rev == hex(nullid):
105 if rev == hex(nullid):
118 raise IOError
106 raise IOError
119 self.catfilepipe[0].write(rev+'\n')
107 self.catfilepipe[0].write(rev+'\n')
120 self.catfilepipe[0].flush()
108 self.catfilepipe[0].flush()
121 info = self.catfilepipe[1].readline().split()
109 info = self.catfilepipe[1].readline().split()
122 if info[1] != type:
110 if info[1] != type:
123 raise error.Abort(_('cannot read %r object at %s') % (type, rev))
111 raise error.Abort(_('cannot read %r object at %s') % (type, rev))
124 size = int(info[2])
112 size = int(info[2])
125 data = self.catfilepipe[1].read(size)
113 data = self.catfilepipe[1].read(size)
126 if len(data) < size:
114 if len(data) < size:
127 raise error.Abort(_('cannot read %r object at %s: unexpected size')
115 raise error.Abort(_('cannot read %r object at %s: unexpected size')
128 % (type, rev))
116 % (type, rev))
129 # read the trailing newline
117 # read the trailing newline
130 self.catfilepipe[1].read(1)
118 self.catfilepipe[1].read(1)
131 return data
119 return data
132
120
133 def getfile(self, name, rev):
121 def getfile(self, name, rev):
134 if rev == hex(nullid):
122 if rev == hex(nullid):
135 return None, None
123 return None, None
136 if name == '.hgsub':
124 if name == '.hgsub':
137 data = '\n'.join([m.hgsub() for m in self.submoditer()])
125 data = '\n'.join([m.hgsub() for m in self.submoditer()])
138 mode = ''
126 mode = ''
139 elif name == '.hgsubstate':
127 elif name == '.hgsubstate':
140 data = '\n'.join([m.hgsubstate() for m in self.submoditer()])
128 data = '\n'.join([m.hgsubstate() for m in self.submoditer()])
141 mode = ''
129 mode = ''
142 else:
130 else:
143 data = self.catfile(rev, "blob")
131 data = self.catfile(rev, "blob")
144 mode = self.modecache[(name, rev)]
132 mode = self.modecache[(name, rev)]
145 return data, mode
133 return data, mode
146
134
147 def submoditer(self):
135 def submoditer(self):
148 null = hex(nullid)
136 null = hex(nullid)
149 for m in sorted(self.submodules, key=lambda p: p.path):
137 for m in sorted(self.submodules, key=lambda p: p.path):
150 if m.node != null:
138 if m.node != null:
151 yield m
139 yield m
152
140
153 def parsegitmodules(self, content):
141 def parsegitmodules(self, content):
154 """Parse the formatted .gitmodules file, example file format:
142 """Parse the formatted .gitmodules file, example file format:
155 [submodule "sub"]\n
143 [submodule "sub"]\n
156 \tpath = sub\n
144 \tpath = sub\n
157 \turl = git://giturl\n
145 \turl = git://giturl\n
158 """
146 """
159 self.submodules = []
147 self.submodules = []
160 c = config.config()
148 c = config.config()
161 # Each item in .gitmodules starts with whitespace that cant be parsed
149 # Each item in .gitmodules starts with whitespace that cant be parsed
162 c.parse('.gitmodules', '\n'.join(line.strip() for line in
150 c.parse('.gitmodules', '\n'.join(line.strip() for line in
163 content.split('\n')))
151 content.split('\n')))
164 for sec in c.sections():
152 for sec in c.sections():
165 s = c[sec]
153 s = c[sec]
166 if 'url' in s and 'path' in s:
154 if 'url' in s and 'path' in s:
167 self.submodules.append(submodule(s['path'], '', s['url']))
155 self.submodules.append(submodule(s['path'], '', s['url']))
168
156
169 def retrievegitmodules(self, version):
157 def retrievegitmodules(self, version):
170 modules, ret = self.gitrun('show', '%s:%s' % (version, '.gitmodules'))
158 modules, ret = self.gitrun('show', '%s:%s' % (version, '.gitmodules'))
171 if ret:
159 if ret:
172 # This can happen if a file is in the repo that has permissions
160 # This can happen if a file is in the repo that has permissions
173 # 160000, but there is no .gitmodules file.
161 # 160000, but there is no .gitmodules file.
174 self.ui.warn(_("warning: cannot read submodules config file in "
162 self.ui.warn(_("warning: cannot read submodules config file in "
175 "%s\n") % version)
163 "%s\n") % version)
176 return
164 return
177
165
178 try:
166 try:
179 self.parsegitmodules(modules)
167 self.parsegitmodules(modules)
180 except error.ParseError:
168 except error.ParseError:
181 self.ui.warn(_("warning: unable to parse .gitmodules in %s\n")
169 self.ui.warn(_("warning: unable to parse .gitmodules in %s\n")
182 % version)
170 % version)
183 return
171 return
184
172
185 for m in self.submodules:
173 for m in self.submodules:
186 node, ret = self.gitrun('rev-parse', '%s:%s' % (version, m.path))
174 node, ret = self.gitrun('rev-parse', '%s:%s' % (version, m.path))
187 if ret:
175 if ret:
188 continue
176 continue
189 m.node = node.strip()
177 m.node = node.strip()
190
178
191 def getchanges(self, version, full):
179 def getchanges(self, version, full):
192 if full:
180 if full:
193 raise error.Abort(_("convert from git does not support --full"))
181 raise error.Abort(_("convert from git does not support --full"))
194 self.modecache = {}
182 self.modecache = {}
195 cmd = ['diff-tree','-z', '--root', '-m', '-r'] + self.simopt + [version]
183 cmd = ['diff-tree','-z', '--root', '-m', '-r'] + self.simopt + [version]
196 output, status = self.gitrun(*cmd)
184 output, status = self.gitrun(*cmd)
197 if status:
185 if status:
198 raise error.Abort(_('cannot read changes in %s') % version)
186 raise error.Abort(_('cannot read changes in %s') % version)
199 changes = []
187 changes = []
200 copies = {}
188 copies = {}
201 seen = set()
189 seen = set()
202 entry = None
190 entry = None
203 subexists = [False]
191 subexists = [False]
204 subdeleted = [False]
192 subdeleted = [False]
205 difftree = output.split('\x00')
193 difftree = output.split('\x00')
206 lcount = len(difftree)
194 lcount = len(difftree)
207 i = 0
195 i = 0
208
196
209 skipsubmodules = self.ui.configbool('convert', 'git.skipsubmodules',
197 skipsubmodules = self.ui.configbool('convert', 'git.skipsubmodules',
210 False)
198 False)
211 def add(entry, f, isdest):
199 def add(entry, f, isdest):
212 seen.add(f)
200 seen.add(f)
213 h = entry[3]
201 h = entry[3]
214 p = (entry[1] == "100755")
202 p = (entry[1] == "100755")
215 s = (entry[1] == "120000")
203 s = (entry[1] == "120000")
216 renamesource = (not isdest and entry[4][0] == 'R')
204 renamesource = (not isdest and entry[4][0] == 'R')
217
205
218 if f == '.gitmodules':
206 if f == '.gitmodules':
219 if skipsubmodules:
207 if skipsubmodules:
220 return
208 return
221
209
222 subexists[0] = True
210 subexists[0] = True
223 if entry[4] == 'D' or renamesource:
211 if entry[4] == 'D' or renamesource:
224 subdeleted[0] = True
212 subdeleted[0] = True
225 changes.append(('.hgsub', hex(nullid)))
213 changes.append(('.hgsub', hex(nullid)))
226 else:
214 else:
227 changes.append(('.hgsub', ''))
215 changes.append(('.hgsub', ''))
228 elif entry[1] == '160000' or entry[0] == ':160000':
216 elif entry[1] == '160000' or entry[0] == ':160000':
229 if not skipsubmodules:
217 if not skipsubmodules:
230 subexists[0] = True
218 subexists[0] = True
231 else:
219 else:
232 if renamesource:
220 if renamesource:
233 h = hex(nullid)
221 h = hex(nullid)
234 self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
222 self.modecache[(f, h)] = (p and "x") or (s and "l") or ""
235 changes.append((f, h))
223 changes.append((f, h))
236
224
237 while i < lcount:
225 while i < lcount:
238 l = difftree[i]
226 l = difftree[i]
239 i += 1
227 i += 1
240 if not entry:
228 if not entry:
241 if not l.startswith(':'):
229 if not l.startswith(':'):
242 continue
230 continue
243 entry = l.split()
231 entry = l.split()
244 continue
232 continue
245 f = l
233 f = l
246 if entry[4][0] == 'C':
234 if entry[4][0] == 'C':
247 copysrc = f
235 copysrc = f
248 copydest = difftree[i]
236 copydest = difftree[i]
249 i += 1
237 i += 1
250 f = copydest
238 f = copydest
251 copies[copydest] = copysrc
239 copies[copydest] = copysrc
252 if f not in seen:
240 if f not in seen:
253 add(entry, f, False)
241 add(entry, f, False)
254 # A file can be copied multiple times, or modified and copied
242 # A file can be copied multiple times, or modified and copied
255 # simultaneously. So f can be repeated even if fdest isn't.
243 # simultaneously. So f can be repeated even if fdest isn't.
256 if entry[4][0] == 'R':
244 if entry[4][0] == 'R':
257 # rename: next line is the destination
245 # rename: next line is the destination
258 fdest = difftree[i]
246 fdest = difftree[i]
259 i += 1
247 i += 1
260 if fdest not in seen:
248 if fdest not in seen:
261 add(entry, fdest, True)
249 add(entry, fdest, True)
262 # .gitmodules isn't imported at all, so it being copied to
250 # .gitmodules isn't imported at all, so it being copied to
263 # and fro doesn't really make sense
251 # and fro doesn't really make sense
264 if f != '.gitmodules' and fdest != '.gitmodules':
252 if f != '.gitmodules' and fdest != '.gitmodules':
265 copies[fdest] = f
253 copies[fdest] = f
266 entry = None
254 entry = None
267
255
268 if subexists[0]:
256 if subexists[0]:
269 if subdeleted[0]:
257 if subdeleted[0]:
270 changes.append(('.hgsubstate', hex(nullid)))
258 changes.append(('.hgsubstate', hex(nullid)))
271 else:
259 else:
272 self.retrievegitmodules(version)
260 self.retrievegitmodules(version)
273 changes.append(('.hgsubstate', ''))
261 changes.append(('.hgsubstate', ''))
274 return (changes, copies, set())
262 return (changes, copies, set())
275
263
276 def getcommit(self, version):
264 def getcommit(self, version):
277 c = self.catfile(version, "commit") # read the commit hash
265 c = self.catfile(version, "commit") # read the commit hash
278 end = c.find("\n\n")
266 end = c.find("\n\n")
279 message = c[end + 2:]
267 message = c[end + 2:]
280 message = self.recode(message)
268 message = self.recode(message)
281 l = c[:end].splitlines()
269 l = c[:end].splitlines()
282 parents = []
270 parents = []
283 author = committer = None
271 author = committer = None
284 for e in l[1:]:
272 for e in l[1:]:
285 n, v = e.split(" ", 1)
273 n, v = e.split(" ", 1)
286 if n == "author":
274 if n == "author":
287 p = v.split()
275 p = v.split()
288 tm, tz = p[-2:]
276 tm, tz = p[-2:]
289 author = " ".join(p[:-2])
277 author = " ".join(p[:-2])
290 if author[0] == "<": author = author[1:-1]
278 if author[0] == "<": author = author[1:-1]
291 author = self.recode(author)
279 author = self.recode(author)
292 if n == "committer":
280 if n == "committer":
293 p = v.split()
281 p = v.split()
294 tm, tz = p[-2:]
282 tm, tz = p[-2:]
295 committer = " ".join(p[:-2])
283 committer = " ".join(p[:-2])
296 if committer[0] == "<": committer = committer[1:-1]
284 if committer[0] == "<": committer = committer[1:-1]
297 committer = self.recode(committer)
285 committer = self.recode(committer)
298 if n == "parent":
286 if n == "parent":
299 parents.append(v)
287 parents.append(v)
300
288
301 if committer and committer != author:
289 if committer and committer != author:
302 message += "\ncommitter: %s\n" % committer
290 message += "\ncommitter: %s\n" % committer
303 tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
291 tzs, tzh, tzm = tz[-5:-4] + "1", tz[-4:-2], tz[-2:]
304 tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
292 tz = -int(tzs) * (int(tzh) * 3600 + int(tzm))
305 date = tm + " " + str(tz)
293 date = tm + " " + str(tz)
306
294
307 c = commit(parents=parents, date=date, author=author, desc=message,
295 c = commit(parents=parents, date=date, author=author, desc=message,
308 rev=version)
296 rev=version)
309 return c
297 return c
310
298
311 def numcommits(self):
299 def numcommits(self):
312 output, ret = self.gitrunlines('rev-list', '--all')
300 output, ret = self.gitrunlines('rev-list', '--all')
313 if ret:
301 if ret:
314 raise error.Abort(_('cannot retrieve number of commits in %s') \
302 raise error.Abort(_('cannot retrieve number of commits in %s') \
315 % self.path)
303 % self.path)
316 return len(output)
304 return len(output)
317
305
318 def gettags(self):
306 def gettags(self):
319 tags = {}
307 tags = {}
320 alltags = {}
308 alltags = {}
321 output, status = self.gitrunlines('ls-remote', '--tags', self.path)
309 output, status = self.gitrunlines('ls-remote', '--tags', self.path)
322
310
323 if status:
311 if status:
324 raise error.Abort(_('cannot read tags from %s') % self.path)
312 raise error.Abort(_('cannot read tags from %s') % self.path)
325 prefix = 'refs/tags/'
313 prefix = 'refs/tags/'
326
314
327 # Build complete list of tags, both annotated and bare ones
315 # Build complete list of tags, both annotated and bare ones
328 for line in output:
316 for line in output:
329 line = line.strip()
317 line = line.strip()
330 if line.startswith("error:") or line.startswith("fatal:"):
318 if line.startswith("error:") or line.startswith("fatal:"):
331 raise error.Abort(_('cannot read tags from %s') % self.path)
319 raise error.Abort(_('cannot read tags from %s') % self.path)
332 node, tag = line.split(None, 1)
320 node, tag = line.split(None, 1)
333 if not tag.startswith(prefix):
321 if not tag.startswith(prefix):
334 continue
322 continue
335 alltags[tag[len(prefix):]] = node
323 alltags[tag[len(prefix):]] = node
336
324
337 # Filter out tag objects for annotated tag refs
325 # Filter out tag objects for annotated tag refs
338 for tag in alltags:
326 for tag in alltags:
339 if tag.endswith('^{}'):
327 if tag.endswith('^{}'):
340 tags[tag[:-3]] = alltags[tag]
328 tags[tag[:-3]] = alltags[tag]
341 else:
329 else:
342 if tag + '^{}' in alltags:
330 if tag + '^{}' in alltags:
343 continue
331 continue
344 else:
332 else:
345 tags[tag] = alltags[tag]
333 tags[tag] = alltags[tag]
346
334
347 return tags
335 return tags
348
336
349 def getchangedfiles(self, version, i):
337 def getchangedfiles(self, version, i):
350 changes = []
338 changes = []
351 if i is None:
339 if i is None:
352 output, status = self.gitrunlines('diff-tree', '--root', '-m',
340 output, status = self.gitrunlines('diff-tree', '--root', '-m',
353 '-r', version)
341 '-r', version)
354 if status:
342 if status:
355 raise error.Abort(_('cannot read changes in %s') % version)
343 raise error.Abort(_('cannot read changes in %s') % version)
356 for l in output:
344 for l in output:
357 if "\t" not in l:
345 if "\t" not in l:
358 continue
346 continue
359 m, f = l[:-1].split("\t")
347 m, f = l[:-1].split("\t")
360 changes.append(f)
348 changes.append(f)
361 else:
349 else:
362 output, status = self.gitrunlines('diff-tree', '--name-only',
350 output, status = self.gitrunlines('diff-tree', '--name-only',
363 '--root', '-r', version,
351 '--root', '-r', version,
364 '%s^%s' % (version, i + 1), '--')
352 '%s^%s' % (version, i + 1), '--')
365 changes = [f.rstrip('\n') for f in output]
353 changes = [f.rstrip('\n') for f in output]
366
354
367 return changes
355 return changes
368
356
369 def getbookmarks(self):
357 def getbookmarks(self):
370 bookmarks = {}
358 bookmarks = {}
371
359
372 # Handle local and remote branches
360 # Handle local and remote branches
373 remoteprefix = self.ui.config('convert', 'git.remoteprefix', 'remote')
361 remoteprefix = self.ui.config('convert', 'git.remoteprefix', 'remote')
374 reftypes = [
362 reftypes = [
375 # (git prefix, hg prefix)
363 # (git prefix, hg prefix)
376 ('refs/remotes/origin/', remoteprefix + '/'),
364 ('refs/remotes/origin/', remoteprefix + '/'),
377 ('refs/heads/', '')
365 ('refs/heads/', '')
378 ]
366 ]
379
367
380 exclude = set([
368 exclude = set([
381 'refs/remotes/origin/HEAD',
369 'refs/remotes/origin/HEAD',
382 ])
370 ])
383
371
384 try:
372 try:
385 output, status = self.gitrunlines('show-ref')
373 output, status = self.gitrunlines('show-ref')
386 for line in output:
374 for line in output:
387 line = line.strip()
375 line = line.strip()
388 rev, name = line.split(None, 1)
376 rev, name = line.split(None, 1)
389 # Process each type of branch
377 # Process each type of branch
390 for gitprefix, hgprefix in reftypes:
378 for gitprefix, hgprefix in reftypes:
391 if not name.startswith(gitprefix) or name in exclude:
379 if not name.startswith(gitprefix) or name in exclude:
392 continue
380 continue
393 name = '%s%s' % (hgprefix, name[len(gitprefix):])
381 name = '%s%s' % (hgprefix, name[len(gitprefix):])
394 bookmarks[name] = rev
382 bookmarks[name] = rev
395 except Exception:
383 except Exception:
396 pass
384 pass
397
385
398 return bookmarks
386 return bookmarks
399
387
400 def checkrevformat(self, revstr, mapname='splicemap'):
388 def checkrevformat(self, revstr, mapname='splicemap'):
401 """ git revision string is a 40 byte hex """
389 """ git revision string is a 40 byte hex """
402 self.checkhexformat(revstr, mapname)
390 self.checkhexformat(revstr, mapname)
General Comments 0
You need to be logged in to leave comments. Login now