##// END OF EJS Templates
hgext: use templatekeyword to mark a function as template keyword...
FUJIWARA Katsunori -
r28540:012411b9 default
parent child Browse files
Show More
@@ -1,458 +1,458 b''
1 1 # convert.py Foreign SCM converter
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''import revisions from foreign VCS repositories into Mercurial'''
9 9
10 10 from __future__ import absolute_import
11 11
12 12 from mercurial import (
13 13 cmdutil,
14 templatekw,
14 registrar,
15 15 )
16 16 from mercurial.i18n import _
17 17
18 18 from . import (
19 19 convcmd,
20 20 cvsps,
21 21 subversion,
22 22 )
23 23
24 24 cmdtable = {}
25 25 command = cmdutil.command(cmdtable)
26 26 # Note for extension authors: ONLY specify testedwith = 'internal' for
27 27 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
28 28 # be specifying the version(s) of Mercurial they are tested with, or
29 29 # leave the attribute unspecified.
30 30 testedwith = 'internal'
31 31
32 32 # Commands definition was moved elsewhere to ease demandload job.
33 33
34 34 @command('convert',
35 35 [('', 'authors', '',
36 36 _('username mapping filename (DEPRECATED) (use --authormap instead)'),
37 37 _('FILE')),
38 38 ('s', 'source-type', '', _('source repository type'), _('TYPE')),
39 39 ('d', 'dest-type', '', _('destination repository type'), _('TYPE')),
40 40 ('r', 'rev', [], _('import up to source revision REV'), _('REV')),
41 41 ('A', 'authormap', '', _('remap usernames using this file'), _('FILE')),
42 42 ('', 'filemap', '', _('remap file names using contents of file'),
43 43 _('FILE')),
44 44 ('', 'full', None,
45 45 _('apply filemap changes by converting all files again')),
46 46 ('', 'splicemap', '', _('splice synthesized history into place'),
47 47 _('FILE')),
48 48 ('', 'branchmap', '', _('change branch names while converting'),
49 49 _('FILE')),
50 50 ('', 'branchsort', None, _('try to sort changesets by branches')),
51 51 ('', 'datesort', None, _('try to sort changesets by date')),
52 52 ('', 'sourcesort', None, _('preserve source changesets order')),
53 53 ('', 'closesort', None, _('try to reorder closed revisions'))],
54 54 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]'),
55 55 norepo=True)
56 56 def convert(ui, src, dest=None, revmapfile=None, **opts):
57 57 """convert a foreign SCM repository to a Mercurial one.
58 58
59 59 Accepted source formats [identifiers]:
60 60
61 61 - Mercurial [hg]
62 62 - CVS [cvs]
63 63 - Darcs [darcs]
64 64 - git [git]
65 65 - Subversion [svn]
66 66 - Monotone [mtn]
67 67 - GNU Arch [gnuarch]
68 68 - Bazaar [bzr]
69 69 - Perforce [p4]
70 70
71 71 Accepted destination formats [identifiers]:
72 72
73 73 - Mercurial [hg]
74 74 - Subversion [svn] (history on branches is not preserved)
75 75
76 76 If no revision is given, all revisions will be converted.
77 77 Otherwise, convert will only import up to the named revision
78 78 (given in a format understood by the source).
79 79
80 80 If no destination directory name is specified, it defaults to the
81 81 basename of the source with ``-hg`` appended. If the destination
82 82 repository doesn't exist, it will be created.
83 83
84 84 By default, all sources except Mercurial will use --branchsort.
85 85 Mercurial uses --sourcesort to preserve original revision numbers
86 86 order. Sort modes have the following effects:
87 87
88 88 --branchsort convert from parent to child revision when possible,
89 89 which means branches are usually converted one after
90 90 the other. It generates more compact repositories.
91 91
92 92 --datesort sort revisions by date. Converted repositories have
93 93 good-looking changelogs but are often an order of
94 94 magnitude larger than the same ones generated by
95 95 --branchsort.
96 96
97 97 --sourcesort try to preserve source revisions order, only
98 98 supported by Mercurial sources.
99 99
100 100 --closesort try to move closed revisions as close as possible
101 101 to parent branches, only supported by Mercurial
102 102 sources.
103 103
104 104 If ``REVMAP`` isn't given, it will be put in a default location
105 105 (``<dest>/.hg/shamap`` by default). The ``REVMAP`` is a simple
106 106 text file that maps each source commit ID to the destination ID
107 107 for that revision, like so::
108 108
109 109 <source ID> <destination ID>
110 110
111 111 If the file doesn't exist, it's automatically created. It's
112 112 updated on each commit copied, so :hg:`convert` can be interrupted
113 113 and can be run repeatedly to copy new commits.
114 114
115 115 The authormap is a simple text file that maps each source commit
116 116 author to a destination commit author. It is handy for source SCMs
117 117 that use unix logins to identify authors (e.g.: CVS). One line per
118 118 author mapping and the line format is::
119 119
120 120 source author = destination author
121 121
122 122 Empty lines and lines starting with a ``#`` are ignored.
123 123
124 124 The filemap is a file that allows filtering and remapping of files
125 125 and directories. Each line can contain one of the following
126 126 directives::
127 127
128 128 include path/to/file-or-dir
129 129
130 130 exclude path/to/file-or-dir
131 131
132 132 rename path/to/source path/to/destination
133 133
134 134 Comment lines start with ``#``. A specified path matches if it
135 135 equals the full relative name of a file or one of its parent
136 136 directories. The ``include`` or ``exclude`` directive with the
137 137 longest matching path applies, so line order does not matter.
138 138
139 139 The ``include`` directive causes a file, or all files under a
140 140 directory, to be included in the destination repository. The default
141 141 if there are no ``include`` statements is to include everything.
142 142 If there are any ``include`` statements, nothing else is included.
143 143 The ``exclude`` directive causes files or directories to
144 144 be omitted. The ``rename`` directive renames a file or directory if
145 145 it is converted. To rename from a subdirectory into the root of
146 146 the repository, use ``.`` as the path to rename to.
147 147
148 148 ``--full`` will make sure the converted changesets contain exactly
149 149 the right files with the right content. It will make a full
150 150 conversion of all files, not just the ones that have
151 151 changed. Files that already are correct will not be changed. This
152 152 can be used to apply filemap changes when converting
153 153 incrementally. This is currently only supported for Mercurial and
154 154 Subversion.
155 155
156 156 The splicemap is a file that allows insertion of synthetic
157 157 history, letting you specify the parents of a revision. This is
158 158 useful if you want to e.g. give a Subversion merge two parents, or
159 159 graft two disconnected series of history together. Each entry
160 160 contains a key, followed by a space, followed by one or two
161 161 comma-separated values::
162 162
163 163 key parent1, parent2
164 164
165 165 The key is the revision ID in the source
166 166 revision control system whose parents should be modified (same
167 167 format as a key in .hg/shamap). The values are the revision IDs
168 168 (in either the source or destination revision control system) that
169 169 should be used as the new parents for that node. For example, if
170 170 you have merged "release-1.0" into "trunk", then you should
171 171 specify the revision on "trunk" as the first parent and the one on
172 172 the "release-1.0" branch as the second.
173 173
174 174 The branchmap is a file that allows you to rename a branch when it is
175 175 being brought in from whatever external repository. When used in
176 176 conjunction with a splicemap, it allows for a powerful combination
177 177 to help fix even the most badly mismanaged repositories and turn them
178 178 into nicely structured Mercurial repositories. The branchmap contains
179 179 lines of the form::
180 180
181 181 original_branch_name new_branch_name
182 182
183 183 where "original_branch_name" is the name of the branch in the
184 184 source repository, and "new_branch_name" is the name of the branch
185 185 is the destination repository. No whitespace is allowed in the
186 186 branch names. This can be used to (for instance) move code in one
187 187 repository from "default" to a named branch.
188 188
189 189 Mercurial Source
190 190 ################
191 191
192 192 The Mercurial source recognizes the following configuration
193 193 options, which you can set on the command line with ``--config``:
194 194
195 195 :convert.hg.ignoreerrors: ignore integrity errors when reading.
196 196 Use it to fix Mercurial repositories with missing revlogs, by
197 197 converting from and to Mercurial. Default is False.
198 198
199 199 :convert.hg.saverev: store original revision ID in changeset
200 200 (forces target IDs to change). It takes a boolean argument and
201 201 defaults to False.
202 202
203 203 :convert.hg.startrev: specify the initial Mercurial revision.
204 204 The default is 0.
205 205
206 206 :convert.hg.revs: revset specifying the source revisions to convert.
207 207
208 208 CVS Source
209 209 ##########
210 210
211 211 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
212 212 to indicate the starting point of what will be converted. Direct
213 213 access to the repository files is not needed, unless of course the
214 214 repository is ``:local:``. The conversion uses the top level
215 215 directory in the sandbox to find the CVS repository, and then uses
216 216 CVS rlog commands to find files to convert. This means that unless
217 217 a filemap is given, all files under the starting directory will be
218 218 converted, and that any directory reorganization in the CVS
219 219 sandbox is ignored.
220 220
221 221 The following options can be used with ``--config``:
222 222
223 223 :convert.cvsps.cache: Set to False to disable remote log caching,
224 224 for testing and debugging purposes. Default is True.
225 225
226 226 :convert.cvsps.fuzz: Specify the maximum time (in seconds) that is
227 227 allowed between commits with identical user and log message in
228 228 a single changeset. When very large files were checked in as
229 229 part of a changeset then the default may not be long enough.
230 230 The default is 60.
231 231
232 232 :convert.cvsps.mergeto: Specify a regular expression to which
233 233 commit log messages are matched. If a match occurs, then the
234 234 conversion process will insert a dummy revision merging the
235 235 branch on which this log message occurs to the branch
236 236 indicated in the regex. Default is ``{{mergetobranch
237 237 ([-\\w]+)}}``
238 238
239 239 :convert.cvsps.mergefrom: Specify a regular expression to which
240 240 commit log messages are matched. If a match occurs, then the
241 241 conversion process will add the most recent revision on the
242 242 branch indicated in the regex as the second parent of the
243 243 changeset. Default is ``{{mergefrombranch ([-\\w]+)}}``
244 244
245 245 :convert.localtimezone: use local time (as determined by the TZ
246 246 environment variable) for changeset date/times. The default
247 247 is False (use UTC).
248 248
249 249 :hooks.cvslog: Specify a Python function to be called at the end of
250 250 gathering the CVS log. The function is passed a list with the
251 251 log entries, and can modify the entries in-place, or add or
252 252 delete them.
253 253
254 254 :hooks.cvschangesets: Specify a Python function to be called after
255 255 the changesets are calculated from the CVS log. The
256 256 function is passed a list with the changeset entries, and can
257 257 modify the changesets in-place, or add or delete them.
258 258
259 259 An additional "debugcvsps" Mercurial command allows the builtin
260 260 changeset merging code to be run without doing a conversion. Its
261 261 parameters and output are similar to that of cvsps 2.1. Please see
262 262 the command help for more details.
263 263
264 264 Subversion Source
265 265 #################
266 266
267 267 Subversion source detects classical trunk/branches/tags layouts.
268 268 By default, the supplied ``svn://repo/path/`` source URL is
269 269 converted as a single branch. If ``svn://repo/path/trunk`` exists
270 270 it replaces the default branch. If ``svn://repo/path/branches``
271 271 exists, its subdirectories are listed as possible branches. If
272 272 ``svn://repo/path/tags`` exists, it is looked for tags referencing
273 273 converted branches. Default ``trunk``, ``branches`` and ``tags``
274 274 values can be overridden with following options. Set them to paths
275 275 relative to the source URL, or leave them blank to disable auto
276 276 detection.
277 277
278 278 The following options can be set with ``--config``:
279 279
280 280 :convert.svn.branches: specify the directory containing branches.
281 281 The default is ``branches``.
282 282
283 283 :convert.svn.tags: specify the directory containing tags. The
284 284 default is ``tags``.
285 285
286 286 :convert.svn.trunk: specify the name of the trunk branch. The
287 287 default is ``trunk``.
288 288
289 289 :convert.localtimezone: use local time (as determined by the TZ
290 290 environment variable) for changeset date/times. The default
291 291 is False (use UTC).
292 292
293 293 Source history can be retrieved starting at a specific revision,
294 294 instead of being integrally converted. Only single branch
295 295 conversions are supported.
296 296
297 297 :convert.svn.startrev: specify start Subversion revision number.
298 298 The default is 0.
299 299
300 300 Git Source
301 301 ##########
302 302
303 303 The Git importer converts commits from all reachable branches (refs
304 304 in refs/heads) and remotes (refs in refs/remotes) to Mercurial.
305 305 Branches are converted to bookmarks with the same name, with the
306 306 leading 'refs/heads' stripped. Git submodules are converted to Git
307 307 subrepos in Mercurial.
308 308
309 309 The following options can be set with ``--config``:
310 310
311 311 :convert.git.similarity: specify how similar files modified in a
312 312 commit must be to be imported as renames or copies, as a
313 313 percentage between ``0`` (disabled) and ``100`` (files must be
314 314 identical). For example, ``90`` means that a delete/add pair will
315 315 be imported as a rename if more than 90% of the file hasn't
316 316 changed. The default is ``50``.
317 317
318 318 :convert.git.findcopiesharder: while detecting copies, look at all
319 319 files in the working copy instead of just changed ones. This
320 320 is very expensive for large projects, and is only effective when
321 321 ``convert.git.similarity`` is greater than 0. The default is False.
322 322
323 323 :convert.git.remoteprefix: remote refs are converted as bookmarks with
324 324 ``convert.git.remoteprefix`` as a prefix followed by a /. The default
325 325 is 'remote'.
326 326
327 327 :convert.git.skipsubmodules: does not convert root level .gitmodules files
328 328 or files with 160000 mode indicating a submodule. Default is False.
329 329
330 330 Perforce Source
331 331 ###############
332 332
333 333 The Perforce (P4) importer can be given a p4 depot path or a
334 334 client specification as source. It will convert all files in the
335 335 source to a flat Mercurial repository, ignoring labels, branches
336 336 and integrations. Note that when a depot path is given you then
337 337 usually should specify a target directory, because otherwise the
338 338 target may be named ``...-hg``.
339 339
340 340 The following options can be set with ``--config``:
341 341
342 342 :convert.p4.encoding: specify the encoding to use when decoding standard
343 343 output of the Perforce command line tool. The default is default system
344 344 encoding.
345 345
346 346 :convert.p4.startrev: specify initial Perforce revision (a
347 347 Perforce changelist number).
348 348
349 349 Mercurial Destination
350 350 #####################
351 351
352 352 The Mercurial destination will recognize Mercurial subrepositories in the
353 353 destination directory, and update the .hgsubstate file automatically if the
354 354 destination subrepositories contain the <dest>/<sub>/.hg/shamap file.
355 355 Converting a repository with subrepositories requires converting a single
356 356 repository at a time, from the bottom up.
357 357
358 358 .. container:: verbose
359 359
360 360 An example showing how to convert a repository with subrepositories::
361 361
362 362 # so convert knows the type when it sees a non empty destination
363 363 $ hg init converted
364 364
365 365 $ hg convert orig/sub1 converted/sub1
366 366 $ hg convert orig/sub2 converted/sub2
367 367 $ hg convert orig converted
368 368
369 369 The following options are supported:
370 370
371 371 :convert.hg.clonebranches: dispatch source branches in separate
372 372 clones. The default is False.
373 373
374 374 :convert.hg.tagsbranch: branch name for tag revisions, defaults to
375 375 ``default``.
376 376
377 377 :convert.hg.usebranchnames: preserve branch names. The default is
378 378 True.
379 379
380 380 :convert.hg.sourcename: records the given string as a 'convert_source' extra
381 381 value on each commit made in the target repository. The default is None.
382 382
383 383 All Destinations
384 384 ################
385 385
386 386 All destination types accept the following options:
387 387
388 388 :convert.skiptags: does not convert tags from the source repo to the target
389 389 repo. The default is False.
390 390 """
391 391 return convcmd.convert(ui, src, dest, revmapfile, **opts)
392 392
393 393 @command('debugsvnlog', [], 'hg debugsvnlog', norepo=True)
394 394 def debugsvnlog(ui, **opts):
395 395 return subversion.debugsvnlog(ui, **opts)
396 396
397 397 @command('debugcvsps',
398 398 [
399 399 # Main options shared with cvsps-2.1
400 400 ('b', 'branches', [], _('only return changes on specified branches')),
401 401 ('p', 'prefix', '', _('prefix to remove from file names')),
402 402 ('r', 'revisions', [],
403 403 _('only return changes after or between specified tags')),
404 404 ('u', 'update-cache', None, _("update cvs log cache")),
405 405 ('x', 'new-cache', None, _("create new cvs log cache")),
406 406 ('z', 'fuzz', 60, _('set commit time fuzz in seconds')),
407 407 ('', 'root', '', _('specify cvsroot')),
408 408 # Options specific to builtin cvsps
409 409 ('', 'parents', '', _('show parent changesets')),
410 410 ('', 'ancestors', '', _('show current changeset in ancestor branches')),
411 411 # Options that are ignored for compatibility with cvsps-2.1
412 412 ('A', 'cvs-direct', None, _('ignored for compatibility')),
413 413 ],
414 414 _('hg debugcvsps [OPTION]... [PATH]...'),
415 415 norepo=True)
416 416 def debugcvsps(ui, *args, **opts):
417 417 '''create changeset information from CVS
418 418
419 419 This command is intended as a debugging tool for the CVS to
420 420 Mercurial converter, and can be used as a direct replacement for
421 421 cvsps.
422 422
423 423 Hg debugcvsps reads the CVS rlog for current directory (or any
424 424 named directory) in the CVS repository, and converts the log to a
425 425 series of changesets based on matching commit log entries and
426 426 dates.'''
427 427 return cvsps.debugcvsps(ui, *args, **opts)
428 428
429 429 def kwconverted(ctx, name):
430 430 rev = ctx.extra().get('convert_revision', '')
431 431 if rev.startswith('svn:'):
432 432 if name == 'svnrev':
433 433 return str(subversion.revsplit(rev)[2])
434 434 elif name == 'svnpath':
435 435 return subversion.revsplit(rev)[1]
436 436 elif name == 'svnuuid':
437 437 return subversion.revsplit(rev)[0]
438 438 return rev
439 439
440 templatekeyword = registrar.templatekeyword()
441
442 @templatekeyword('svnrev')
440 443 def kwsvnrev(repo, ctx, **args):
441 """:svnrev: String. Converted subversion revision number."""
444 """String. Converted subversion revision number."""
442 445 return kwconverted(ctx, 'svnrev')
443 446
447 @templatekeyword('svnpath')
444 448 def kwsvnpath(repo, ctx, **args):
445 """:svnpath: String. Converted subversion revision project path."""
449 """String. Converted subversion revision project path."""
446 450 return kwconverted(ctx, 'svnpath')
447 451
452 @templatekeyword('svnuuid')
448 453 def kwsvnuuid(repo, ctx, **args):
449 """:svnuuid: String. Converted subversion revision repository identifier."""
454 """String. Converted subversion revision repository identifier."""
450 455 return kwconverted(ctx, 'svnuuid')
451 456
452 def extsetup(ui):
453 templatekw.keywords['svnrev'] = kwsvnrev
454 templatekw.keywords['svnpath'] = kwsvnpath
455 templatekw.keywords['svnuuid'] = kwsvnuuid
456
457 457 # tell hggettext to extract docstrings from these functions:
458 458 i18nfunctions = [kwsvnrev, kwsvnpath, kwsvnuuid]
@@ -1,742 +1,743 b''
1 1 # Patch transplanting extension for Mercurial
2 2 #
3 3 # Copyright 2006, 2007 Brendan Cully <brendan@kublai.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''command to transplant changesets from another branch
9 9
10 10 This extension allows you to transplant changes to another parent revision,
11 11 possibly in another repository. The transplant is done using 'diff' patches.
12 12
13 13 Transplanted patches are recorded in .hg/transplant/transplants, as a
14 14 map from a changeset hash to its hash in the source repository.
15 15 '''
16 16 from __future__ import absolute_import
17 17
18 18 import os
19 19 import tempfile
20 20 from mercurial.i18n import _
21 21 from mercurial import (
22 22 bundlerepo,
23 23 cmdutil,
24 24 error,
25 25 exchange,
26 26 hg,
27 27 match,
28 28 merge,
29 29 node as nodemod,
30 30 patch,
31 31 registrar,
32 32 revlog,
33 33 revset,
34 34 scmutil,
35 templatekw,
36 35 util,
37 36 )
38 37
39 38 class TransplantError(error.Abort):
40 39 pass
41 40
42 41 cmdtable = {}
43 42 command = cmdutil.command(cmdtable)
44 43 # Note for extension authors: ONLY specify testedwith = 'internal' for
45 44 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
46 45 # be specifying the version(s) of Mercurial they are tested with, or
47 46 # leave the attribute unspecified.
48 47 testedwith = 'internal'
49 48
50 49 class transplantentry(object):
51 50 def __init__(self, lnode, rnode):
52 51 self.lnode = lnode
53 52 self.rnode = rnode
54 53
55 54 class transplants(object):
56 55 def __init__(self, path=None, transplantfile=None, opener=None):
57 56 self.path = path
58 57 self.transplantfile = transplantfile
59 58 self.opener = opener
60 59
61 60 if not opener:
62 61 self.opener = scmutil.opener(self.path)
63 62 self.transplants = {}
64 63 self.dirty = False
65 64 self.read()
66 65
67 66 def read(self):
68 67 abspath = os.path.join(self.path, self.transplantfile)
69 68 if self.transplantfile and os.path.exists(abspath):
70 69 for line in self.opener.read(self.transplantfile).splitlines():
71 70 lnode, rnode = map(revlog.bin, line.split(':'))
72 71 list = self.transplants.setdefault(rnode, [])
73 72 list.append(transplantentry(lnode, rnode))
74 73
75 74 def write(self):
76 75 if self.dirty and self.transplantfile:
77 76 if not os.path.isdir(self.path):
78 77 os.mkdir(self.path)
79 78 fp = self.opener(self.transplantfile, 'w')
80 79 for list in self.transplants.itervalues():
81 80 for t in list:
82 81 l, r = map(nodemod.hex, (t.lnode, t.rnode))
83 82 fp.write(l + ':' + r + '\n')
84 83 fp.close()
85 84 self.dirty = False
86 85
87 86 def get(self, rnode):
88 87 return self.transplants.get(rnode) or []
89 88
90 89 def set(self, lnode, rnode):
91 90 list = self.transplants.setdefault(rnode, [])
92 91 list.append(transplantentry(lnode, rnode))
93 92 self.dirty = True
94 93
95 94 def remove(self, transplant):
96 95 list = self.transplants.get(transplant.rnode)
97 96 if list:
98 97 del list[list.index(transplant)]
99 98 self.dirty = True
100 99
101 100 class transplanter(object):
102 101 def __init__(self, ui, repo, opts):
103 102 self.ui = ui
104 103 self.path = repo.join('transplant')
105 104 self.opener = scmutil.opener(self.path)
106 105 self.transplants = transplants(self.path, 'transplants',
107 106 opener=self.opener)
108 107 def getcommiteditor():
109 108 editform = cmdutil.mergeeditform(repo[None], 'transplant')
110 109 return cmdutil.getcommiteditor(editform=editform, **opts)
111 110 self.getcommiteditor = getcommiteditor
112 111
113 112 def applied(self, repo, node, parent):
114 113 '''returns True if a node is already an ancestor of parent
115 114 or is parent or has already been transplanted'''
116 115 if hasnode(repo, parent):
117 116 parentrev = repo.changelog.rev(parent)
118 117 if hasnode(repo, node):
119 118 rev = repo.changelog.rev(node)
120 119 reachable = repo.changelog.ancestors([parentrev], rev,
121 120 inclusive=True)
122 121 if rev in reachable:
123 122 return True
124 123 for t in self.transplants.get(node):
125 124 # it might have been stripped
126 125 if not hasnode(repo, t.lnode):
127 126 self.transplants.remove(t)
128 127 return False
129 128 lnoderev = repo.changelog.rev(t.lnode)
130 129 if lnoderev in repo.changelog.ancestors([parentrev], lnoderev,
131 130 inclusive=True):
132 131 return True
133 132 return False
134 133
135 134 def apply(self, repo, source, revmap, merges, opts=None):
136 135 '''apply the revisions in revmap one by one in revision order'''
137 136 if opts is None:
138 137 opts = {}
139 138 revs = sorted(revmap)
140 139 p1, p2 = repo.dirstate.parents()
141 140 pulls = []
142 141 diffopts = patch.difffeatureopts(self.ui, opts)
143 142 diffopts.git = True
144 143
145 144 lock = tr = None
146 145 try:
147 146 lock = repo.lock()
148 147 tr = repo.transaction('transplant')
149 148 for rev in revs:
150 149 node = revmap[rev]
151 150 revstr = '%s:%s' % (rev, nodemod.short(node))
152 151
153 152 if self.applied(repo, node, p1):
154 153 self.ui.warn(_('skipping already applied revision %s\n') %
155 154 revstr)
156 155 continue
157 156
158 157 parents = source.changelog.parents(node)
159 158 if not (opts.get('filter') or opts.get('log')):
160 159 # If the changeset parent is the same as the
161 160 # wdir's parent, just pull it.
162 161 if parents[0] == p1:
163 162 pulls.append(node)
164 163 p1 = node
165 164 continue
166 165 if pulls:
167 166 if source != repo:
168 167 exchange.pull(repo, source.peer(), heads=pulls)
169 168 merge.update(repo, pulls[-1], False, False)
170 169 p1, p2 = repo.dirstate.parents()
171 170 pulls = []
172 171
173 172 domerge = False
174 173 if node in merges:
175 174 # pulling all the merge revs at once would mean we
176 175 # couldn't transplant after the latest even if
177 176 # transplants before them fail.
178 177 domerge = True
179 178 if not hasnode(repo, node):
180 179 exchange.pull(repo, source.peer(), heads=[node])
181 180
182 181 skipmerge = False
183 182 if parents[1] != revlog.nullid:
184 183 if not opts.get('parent'):
185 184 self.ui.note(_('skipping merge changeset %s:%s\n')
186 185 % (rev, nodemod.short(node)))
187 186 skipmerge = True
188 187 else:
189 188 parent = source.lookup(opts['parent'])
190 189 if parent not in parents:
191 190 raise error.Abort(_('%s is not a parent of %s') %
192 191 (nodemod.short(parent),
193 192 nodemod.short(node)))
194 193 else:
195 194 parent = parents[0]
196 195
197 196 if skipmerge:
198 197 patchfile = None
199 198 else:
200 199 fd, patchfile = tempfile.mkstemp(prefix='hg-transplant-')
201 200 fp = os.fdopen(fd, 'w')
202 201 gen = patch.diff(source, parent, node, opts=diffopts)
203 202 for chunk in gen:
204 203 fp.write(chunk)
205 204 fp.close()
206 205
207 206 del revmap[rev]
208 207 if patchfile or domerge:
209 208 try:
210 209 try:
211 210 n = self.applyone(repo, node,
212 211 source.changelog.read(node),
213 212 patchfile, merge=domerge,
214 213 log=opts.get('log'),
215 214 filter=opts.get('filter'))
216 215 except TransplantError:
217 216 # Do not rollback, it is up to the user to
218 217 # fix the merge or cancel everything
219 218 tr.close()
220 219 raise
221 220 if n and domerge:
222 221 self.ui.status(_('%s merged at %s\n') % (revstr,
223 222 nodemod.short(n)))
224 223 elif n:
225 224 self.ui.status(_('%s transplanted to %s\n')
226 225 % (nodemod.short(node),
227 226 nodemod.short(n)))
228 227 finally:
229 228 if patchfile:
230 229 os.unlink(patchfile)
231 230 tr.close()
232 231 if pulls:
233 232 exchange.pull(repo, source.peer(), heads=pulls)
234 233 merge.update(repo, pulls[-1], False, False)
235 234 finally:
236 235 self.saveseries(revmap, merges)
237 236 self.transplants.write()
238 237 if tr:
239 238 tr.release()
240 239 if lock:
241 240 lock.release()
242 241
243 242 def filter(self, filter, node, changelog, patchfile):
244 243 '''arbitrarily rewrite changeset before applying it'''
245 244
246 245 self.ui.status(_('filtering %s\n') % patchfile)
247 246 user, date, msg = (changelog[1], changelog[2], changelog[4])
248 247 fd, headerfile = tempfile.mkstemp(prefix='hg-transplant-')
249 248 fp = os.fdopen(fd, 'w')
250 249 fp.write("# HG changeset patch\n")
251 250 fp.write("# User %s\n" % user)
252 251 fp.write("# Date %d %d\n" % date)
253 252 fp.write(msg + '\n')
254 253 fp.close()
255 254
256 255 try:
257 256 self.ui.system('%s %s %s' % (filter, util.shellquote(headerfile),
258 257 util.shellquote(patchfile)),
259 258 environ={'HGUSER': changelog[1],
260 259 'HGREVISION': nodemod.hex(node),
261 260 },
262 261 onerr=error.Abort, errprefix=_('filter failed'))
263 262 user, date, msg = self.parselog(file(headerfile))[1:4]
264 263 finally:
265 264 os.unlink(headerfile)
266 265
267 266 return (user, date, msg)
268 267
269 268 def applyone(self, repo, node, cl, patchfile, merge=False, log=False,
270 269 filter=None):
271 270 '''apply the patch in patchfile to the repository as a transplant'''
272 271 (manifest, user, (time, timezone), files, message) = cl[:5]
273 272 date = "%d %d" % (time, timezone)
274 273 extra = {'transplant_source': node}
275 274 if filter:
276 275 (user, date, message) = self.filter(filter, node, cl, patchfile)
277 276
278 277 if log:
279 278 # we don't translate messages inserted into commits
280 279 message += '\n(transplanted from %s)' % nodemod.hex(node)
281 280
282 281 self.ui.status(_('applying %s\n') % nodemod.short(node))
283 282 self.ui.note('%s %s\n%s\n' % (user, date, message))
284 283
285 284 if not patchfile and not merge:
286 285 raise error.Abort(_('can only omit patchfile if merging'))
287 286 if patchfile:
288 287 try:
289 288 files = set()
290 289 patch.patch(self.ui, repo, patchfile, files=files, eolmode=None)
291 290 files = list(files)
292 291 except Exception as inst:
293 292 seriespath = os.path.join(self.path, 'series')
294 293 if os.path.exists(seriespath):
295 294 os.unlink(seriespath)
296 295 p1 = repo.dirstate.p1()
297 296 p2 = node
298 297 self.log(user, date, message, p1, p2, merge=merge)
299 298 self.ui.write(str(inst) + '\n')
300 299 raise TransplantError(_('fix up the working directory and run '
301 300 'hg transplant --continue'))
302 301 else:
303 302 files = None
304 303 if merge:
305 304 p1, p2 = repo.dirstate.parents()
306 305 repo.setparents(p1, node)
307 306 m = match.always(repo.root, '')
308 307 else:
309 308 m = match.exact(repo.root, '', files)
310 309
311 310 n = repo.commit(message, user, date, extra=extra, match=m,
312 311 editor=self.getcommiteditor())
313 312 if not n:
314 313 self.ui.warn(_('skipping emptied changeset %s\n') %
315 314 nodemod.short(node))
316 315 return None
317 316 if not merge:
318 317 self.transplants.set(n, node)
319 318
320 319 return n
321 320
322 321 def canresume(self):
323 322 return os.path.exists(os.path.join(self.path, 'journal'))
324 323
325 324 def resume(self, repo, source, opts):
326 325 '''recover last transaction and apply remaining changesets'''
327 326 if os.path.exists(os.path.join(self.path, 'journal')):
328 327 n, node = self.recover(repo, source, opts)
329 328 if n:
330 329 self.ui.status(_('%s transplanted as %s\n') %
331 330 (nodemod.short(node),
332 331 nodemod.short(n)))
333 332 else:
334 333 self.ui.status(_('%s skipped due to empty diff\n')
335 334 % (nodemod.short(node),))
336 335 seriespath = os.path.join(self.path, 'series')
337 336 if not os.path.exists(seriespath):
338 337 self.transplants.write()
339 338 return
340 339 nodes, merges = self.readseries()
341 340 revmap = {}
342 341 for n in nodes:
343 342 revmap[source.changelog.rev(n)] = n
344 343 os.unlink(seriespath)
345 344
346 345 self.apply(repo, source, revmap, merges, opts)
347 346
348 347 def recover(self, repo, source, opts):
349 348 '''commit working directory using journal metadata'''
350 349 node, user, date, message, parents = self.readlog()
351 350 merge = False
352 351
353 352 if not user or not date or not message or not parents[0]:
354 353 raise error.Abort(_('transplant log file is corrupt'))
355 354
356 355 parent = parents[0]
357 356 if len(parents) > 1:
358 357 if opts.get('parent'):
359 358 parent = source.lookup(opts['parent'])
360 359 if parent not in parents:
361 360 raise error.Abort(_('%s is not a parent of %s') %
362 361 (nodemod.short(parent),
363 362 nodemod.short(node)))
364 363 else:
365 364 merge = True
366 365
367 366 extra = {'transplant_source': node}
368 367 try:
369 368 p1, p2 = repo.dirstate.parents()
370 369 if p1 != parent:
371 370 raise error.Abort(_('working directory not at transplant '
372 371 'parent %s') % nodemod.hex(parent))
373 372 if merge:
374 373 repo.setparents(p1, parents[1])
375 374 modified, added, removed, deleted = repo.status()[:4]
376 375 if merge or modified or added or removed or deleted:
377 376 n = repo.commit(message, user, date, extra=extra,
378 377 editor=self.getcommiteditor())
379 378 if not n:
380 379 raise error.Abort(_('commit failed'))
381 380 if not merge:
382 381 self.transplants.set(n, node)
383 382 else:
384 383 n = None
385 384 self.unlog()
386 385
387 386 return n, node
388 387 finally:
389 388 # TODO: get rid of this meaningless try/finally enclosing.
390 389 # this is kept only to reduce changes in a patch.
391 390 pass
392 391
393 392 def readseries(self):
394 393 nodes = []
395 394 merges = []
396 395 cur = nodes
397 396 for line in self.opener.read('series').splitlines():
398 397 if line.startswith('# Merges'):
399 398 cur = merges
400 399 continue
401 400 cur.append(revlog.bin(line))
402 401
403 402 return (nodes, merges)
404 403
405 404 def saveseries(self, revmap, merges):
406 405 if not revmap:
407 406 return
408 407
409 408 if not os.path.isdir(self.path):
410 409 os.mkdir(self.path)
411 410 series = self.opener('series', 'w')
412 411 for rev in sorted(revmap):
413 412 series.write(nodemod.hex(revmap[rev]) + '\n')
414 413 if merges:
415 414 series.write('# Merges\n')
416 415 for m in merges:
417 416 series.write(nodemod.hex(m) + '\n')
418 417 series.close()
419 418
420 419 def parselog(self, fp):
421 420 parents = []
422 421 message = []
423 422 node = revlog.nullid
424 423 inmsg = False
425 424 user = None
426 425 date = None
427 426 for line in fp.read().splitlines():
428 427 if inmsg:
429 428 message.append(line)
430 429 elif line.startswith('# User '):
431 430 user = line[7:]
432 431 elif line.startswith('# Date '):
433 432 date = line[7:]
434 433 elif line.startswith('# Node ID '):
435 434 node = revlog.bin(line[10:])
436 435 elif line.startswith('# Parent '):
437 436 parents.append(revlog.bin(line[9:]))
438 437 elif not line.startswith('# '):
439 438 inmsg = True
440 439 message.append(line)
441 440 if None in (user, date):
442 441 raise error.Abort(_("filter corrupted changeset (no user or date)"))
443 442 return (node, user, date, '\n'.join(message), parents)
444 443
445 444 def log(self, user, date, message, p1, p2, merge=False):
446 445 '''journal changelog metadata for later recover'''
447 446
448 447 if not os.path.isdir(self.path):
449 448 os.mkdir(self.path)
450 449 fp = self.opener('journal', 'w')
451 450 fp.write('# User %s\n' % user)
452 451 fp.write('# Date %s\n' % date)
453 452 fp.write('# Node ID %s\n' % nodemod.hex(p2))
454 453 fp.write('# Parent ' + nodemod.hex(p1) + '\n')
455 454 if merge:
456 455 fp.write('# Parent ' + nodemod.hex(p2) + '\n')
457 456 fp.write(message.rstrip() + '\n')
458 457 fp.close()
459 458
460 459 def readlog(self):
461 460 return self.parselog(self.opener('journal'))
462 461
463 462 def unlog(self):
464 463 '''remove changelog journal'''
465 464 absdst = os.path.join(self.path, 'journal')
466 465 if os.path.exists(absdst):
467 466 os.unlink(absdst)
468 467
469 468 def transplantfilter(self, repo, source, root):
470 469 def matchfn(node):
471 470 if self.applied(repo, node, root):
472 471 return False
473 472 if source.changelog.parents(node)[1] != revlog.nullid:
474 473 return False
475 474 extra = source.changelog.read(node)[5]
476 475 cnode = extra.get('transplant_source')
477 476 if cnode and self.applied(repo, cnode, root):
478 477 return False
479 478 return True
480 479
481 480 return matchfn
482 481
483 482 def hasnode(repo, node):
484 483 try:
485 484 return repo.changelog.rev(node) is not None
486 485 except error.RevlogError:
487 486 return False
488 487
489 488 def browserevs(ui, repo, nodes, opts):
490 489 '''interactively transplant changesets'''
491 490 displayer = cmdutil.show_changeset(ui, repo, opts)
492 491 transplants = []
493 492 merges = []
494 493 prompt = _('apply changeset? [ynmpcq?]:'
495 494 '$$ &yes, transplant this changeset'
496 495 '$$ &no, skip this changeset'
497 496 '$$ &merge at this changeset'
498 497 '$$ show &patch'
499 498 '$$ &commit selected changesets'
500 499 '$$ &quit and cancel transplant'
501 500 '$$ &? (show this help)')
502 501 for node in nodes:
503 502 displayer.show(repo[node])
504 503 action = None
505 504 while not action:
506 505 action = 'ynmpcq?'[ui.promptchoice(prompt)]
507 506 if action == '?':
508 507 for c, t in ui.extractchoices(prompt)[1]:
509 508 ui.write('%s: %s\n' % (c, t))
510 509 action = None
511 510 elif action == 'p':
512 511 parent = repo.changelog.parents(node)[0]
513 512 for chunk in patch.diff(repo, parent, node):
514 513 ui.write(chunk)
515 514 action = None
516 515 if action == 'y':
517 516 transplants.append(node)
518 517 elif action == 'm':
519 518 merges.append(node)
520 519 elif action == 'c':
521 520 break
522 521 elif action == 'q':
523 522 transplants = ()
524 523 merges = ()
525 524 break
526 525 displayer.close()
527 526 return (transplants, merges)
528 527
529 528 @command('transplant',
530 529 [('s', 'source', '', _('transplant changesets from REPO'), _('REPO')),
531 530 ('b', 'branch', [], _('use this source changeset as head'), _('REV')),
532 531 ('a', 'all', None, _('pull all changesets up to the --branch revisions')),
533 532 ('p', 'prune', [], _('skip over REV'), _('REV')),
534 533 ('m', 'merge', [], _('merge at REV'), _('REV')),
535 534 ('', 'parent', '',
536 535 _('parent to choose when transplanting merge'), _('REV')),
537 536 ('e', 'edit', False, _('invoke editor on commit messages')),
538 537 ('', 'log', None, _('append transplant info to log message')),
539 538 ('c', 'continue', None, _('continue last transplant session '
540 539 'after fixing conflicts')),
541 540 ('', 'filter', '',
542 541 _('filter changesets through command'), _('CMD'))],
543 542 _('hg transplant [-s REPO] [-b BRANCH [-a]] [-p REV] '
544 543 '[-m REV] [REV]...'))
545 544 def transplant(ui, repo, *revs, **opts):
546 545 '''transplant changesets from another branch
547 546
548 547 Selected changesets will be applied on top of the current working
549 548 directory with the log of the original changeset. The changesets
550 549 are copied and will thus appear twice in the history with different
551 550 identities.
552 551
553 552 Consider using the graft command if everything is inside the same
554 553 repository - it will use merges and will usually give a better result.
555 554 Use the rebase extension if the changesets are unpublished and you want
556 555 to move them instead of copying them.
557 556
558 557 If --log is specified, log messages will have a comment appended
559 558 of the form::
560 559
561 560 (transplanted from CHANGESETHASH)
562 561
563 562 You can rewrite the changelog message with the --filter option.
564 563 Its argument will be invoked with the current changelog message as
565 564 $1 and the patch as $2.
566 565
567 566 --source/-s specifies another repository to use for selecting changesets,
568 567 just as if it temporarily had been pulled.
569 568 If --branch/-b is specified, these revisions will be used as
570 569 heads when deciding which changesets to transplant, just as if only
571 570 these revisions had been pulled.
572 571 If --all/-a is specified, all the revisions up to the heads specified
573 572 with --branch will be transplanted.
574 573
575 574 Example:
576 575
577 576 - transplant all changes up to REV on top of your current revision::
578 577
579 578 hg transplant --branch REV --all
580 579
581 580 You can optionally mark selected transplanted changesets as merge
582 581 changesets. You will not be prompted to transplant any ancestors
583 582 of a merged transplant, and you can merge descendants of them
584 583 normally instead of transplanting them.
585 584
586 585 Merge changesets may be transplanted directly by specifying the
587 586 proper parent changeset by calling :hg:`transplant --parent`.
588 587
589 588 If no merges or revisions are provided, :hg:`transplant` will
590 589 start an interactive changeset browser.
591 590
592 591 If a changeset application fails, you can fix the merge by hand
593 592 and then resume where you left off by calling :hg:`transplant
594 593 --continue/-c`.
595 594 '''
596 595 with repo.wlock():
597 596 return _dotransplant(ui, repo, *revs, **opts)
598 597
599 598 def _dotransplant(ui, repo, *revs, **opts):
600 599 def incwalk(repo, csets, match=util.always):
601 600 for node in csets:
602 601 if match(node):
603 602 yield node
604 603
605 604 def transplantwalk(repo, dest, heads, match=util.always):
606 605 '''Yield all nodes that are ancestors of a head but not ancestors
607 606 of dest.
608 607 If no heads are specified, the heads of repo will be used.'''
609 608 if not heads:
610 609 heads = repo.heads()
611 610 ancestors = []
612 611 ctx = repo[dest]
613 612 for head in heads:
614 613 ancestors.append(ctx.ancestor(repo[head]).node())
615 614 for node in repo.changelog.nodesbetween(ancestors, heads)[0]:
616 615 if match(node):
617 616 yield node
618 617
619 618 def checkopts(opts, revs):
620 619 if opts.get('continue'):
621 620 if opts.get('branch') or opts.get('all') or opts.get('merge'):
622 621 raise error.Abort(_('--continue is incompatible with '
623 622 '--branch, --all and --merge'))
624 623 return
625 624 if not (opts.get('source') or revs or
626 625 opts.get('merge') or opts.get('branch')):
627 626 raise error.Abort(_('no source URL, branch revision, or revision '
628 627 'list provided'))
629 628 if opts.get('all'):
630 629 if not opts.get('branch'):
631 630 raise error.Abort(_('--all requires a branch revision'))
632 631 if revs:
633 632 raise error.Abort(_('--all is incompatible with a '
634 633 'revision list'))
635 634
636 635 checkopts(opts, revs)
637 636
638 637 if not opts.get('log'):
639 638 # deprecated config: transplant.log
640 639 opts['log'] = ui.config('transplant', 'log')
641 640 if not opts.get('filter'):
642 641 # deprecated config: transplant.filter
643 642 opts['filter'] = ui.config('transplant', 'filter')
644 643
645 644 tp = transplanter(ui, repo, opts)
646 645
647 646 p1, p2 = repo.dirstate.parents()
648 647 if len(repo) > 0 and p1 == revlog.nullid:
649 648 raise error.Abort(_('no revision checked out'))
650 649 if opts.get('continue'):
651 650 if not tp.canresume():
652 651 raise error.Abort(_('no transplant to continue'))
653 652 else:
654 653 cmdutil.checkunfinished(repo)
655 654 if p2 != revlog.nullid:
656 655 raise error.Abort(_('outstanding uncommitted merges'))
657 656 m, a, r, d = repo.status()[:4]
658 657 if m or a or r or d:
659 658 raise error.Abort(_('outstanding local changes'))
660 659
661 660 sourcerepo = opts.get('source')
662 661 if sourcerepo:
663 662 peer = hg.peer(repo, opts, ui.expandpath(sourcerepo))
664 663 heads = map(peer.lookup, opts.get('branch', ()))
665 664 target = set(heads)
666 665 for r in revs:
667 666 try:
668 667 target.add(peer.lookup(r))
669 668 except error.RepoError:
670 669 pass
671 670 source, csets, cleanupfn = bundlerepo.getremotechanges(ui, repo, peer,
672 671 onlyheads=sorted(target), force=True)
673 672 else:
674 673 source = repo
675 674 heads = map(source.lookup, opts.get('branch', ()))
676 675 cleanupfn = None
677 676
678 677 try:
679 678 if opts.get('continue'):
680 679 tp.resume(repo, source, opts)
681 680 return
682 681
683 682 tf = tp.transplantfilter(repo, source, p1)
684 683 if opts.get('prune'):
685 684 prune = set(source.lookup(r)
686 685 for r in scmutil.revrange(source, opts.get('prune')))
687 686 matchfn = lambda x: tf(x) and x not in prune
688 687 else:
689 688 matchfn = tf
690 689 merges = map(source.lookup, opts.get('merge', ()))
691 690 revmap = {}
692 691 if revs:
693 692 for r in scmutil.revrange(source, revs):
694 693 revmap[int(r)] = source.lookup(r)
695 694 elif opts.get('all') or not merges:
696 695 if source != repo:
697 696 alltransplants = incwalk(source, csets, match=matchfn)
698 697 else:
699 698 alltransplants = transplantwalk(source, p1, heads,
700 699 match=matchfn)
701 700 if opts.get('all'):
702 701 revs = alltransplants
703 702 else:
704 703 revs, newmerges = browserevs(ui, source, alltransplants, opts)
705 704 merges.extend(newmerges)
706 705 for r in revs:
707 706 revmap[source.changelog.rev(r)] = r
708 707 for r in merges:
709 708 revmap[source.changelog.rev(r)] = r
710 709
711 710 tp.apply(repo, source, revmap, merges, opts)
712 711 finally:
713 712 if cleanupfn:
714 713 cleanupfn()
715 714
716 715 revsetpredicate = registrar.revsetpredicate()
717 716
718 717 @revsetpredicate('transplanted([set])')
719 718 def revsettransplanted(repo, subset, x):
720 719 """Transplanted changesets in set, or all transplanted changesets.
721 720 """
722 721 if x:
723 722 s = revset.getset(repo, subset, x)
724 723 else:
725 724 s = subset
726 725 return revset.baseset([r for r in s if
727 726 repo[r].extra().get('transplant_source')])
728 727
728 templatekeyword = registrar.templatekeyword()
729
730 @templatekeyword('transplanted')
729 731 def kwtransplanted(repo, ctx, **args):
730 """:transplanted: String. The node identifier of the transplanted
732 """String. The node identifier of the transplanted
731 733 changeset if any."""
732 734 n = ctx.extra().get('transplant_source')
733 735 return n and nodemod.hex(n) or ''
734 736
735 737 def extsetup(ui):
736 templatekw.keywords['transplanted'] = kwtransplanted
737 738 cmdutil.unfinishedstates.append(
738 739 ['transplant/journal', True, False, _('transplant in progress'),
739 740 _("use 'hg transplant --continue' or 'hg update' to abort")])
740 741
741 742 # tell hggettext to extract docstrings from these functions:
742 743 i18nfunctions = [revsettransplanted, kwtransplanted]
General Comments 0
You need to be logged in to leave comments. Login now