##// END OF EJS Templates
Add debugcvsps command, replacing cvsps script
Frank Kingswood -
r7502:16905fc2 default
parent child Browse files
Show More
@@ -1,203 +1,233 b''
1 # convert.py Foreign SCM converter
1 # convert.py Foreign SCM converter
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7 '''converting foreign VCS repositories to Mercurial'''
7 '''converting foreign VCS repositories to Mercurial'''
8
8
9 import convcmd
9 import convcmd
10 import cvsps
10 from mercurial import commands
11 from mercurial import commands
11 from mercurial.i18n import _
12 from mercurial.i18n import _
12
13
13 # Commands definition was moved elsewhere to ease demandload job.
14 # Commands definition was moved elsewhere to ease demandload job.
14
15
15 def convert(ui, src, dest=None, revmapfile=None, **opts):
16 def convert(ui, src, dest=None, revmapfile=None, **opts):
16 """Convert a foreign SCM repository to a Mercurial one.
17 """Convert a foreign SCM repository to a Mercurial one.
17
18
18 Accepted source formats [identifiers]:
19 Accepted source formats [identifiers]:
19 - Mercurial [hg]
20 - Mercurial [hg]
20 - CVS [cvs]
21 - CVS [cvs]
21 - Darcs [darcs]
22 - Darcs [darcs]
22 - git [git]
23 - git [git]
23 - Subversion [svn]
24 - Subversion [svn]
24 - Monotone [mtn]
25 - Monotone [mtn]
25 - GNU Arch [gnuarch]
26 - GNU Arch [gnuarch]
26 - Bazaar [bzr]
27 - Bazaar [bzr]
27
28
28 Accepted destination formats [identifiers]:
29 Accepted destination formats [identifiers]:
29 - Mercurial [hg]
30 - Mercurial [hg]
30 - Subversion [svn] (history on branches is not preserved)
31 - Subversion [svn] (history on branches is not preserved)
31
32
32 If no revision is given, all revisions will be converted. Otherwise,
33 If no revision is given, all revisions will be converted. Otherwise,
33 convert will only import up to the named revision (given in a format
34 convert will only import up to the named revision (given in a format
34 understood by the source).
35 understood by the source).
35
36
36 If no destination directory name is specified, it defaults to the
37 If no destination directory name is specified, it defaults to the
37 basename of the source with '-hg' appended. If the destination
38 basename of the source with '-hg' appended. If the destination
38 repository doesn't exist, it will be created.
39 repository doesn't exist, it will be created.
39
40
40 If <REVMAP> isn't given, it will be put in a default location
41 If <REVMAP> isn't given, it will be put in a default location
41 (<dest>/.hg/shamap by default). The <REVMAP> is a simple text
42 (<dest>/.hg/shamap by default). The <REVMAP> is a simple text
42 file that maps each source commit ID to the destination ID for
43 file that maps each source commit ID to the destination ID for
43 that revision, like so:
44 that revision, like so:
44 <source ID> <destination ID>
45 <source ID> <destination ID>
45
46
46 If the file doesn't exist, it's automatically created. It's updated
47 If the file doesn't exist, it's automatically created. It's updated
47 on each commit copied, so convert-repo can be interrupted and can
48 on each commit copied, so convert-repo can be interrupted and can
48 be run repeatedly to copy new commits.
49 be run repeatedly to copy new commits.
49
50
50 The [username mapping] file is a simple text file that maps each source
51 The [username mapping] file is a simple text file that maps each source
51 commit author to a destination commit author. It is handy for source SCMs
52 commit author to a destination commit author. It is handy for source SCMs
52 that use unix logins to identify authors (eg: CVS). One line per author
53 that use unix logins to identify authors (eg: CVS). One line per author
53 mapping and the line format is:
54 mapping and the line format is:
54 srcauthor=whatever string you want
55 srcauthor=whatever string you want
55
56
56 The filemap is a file that allows filtering and remapping of files
57 The filemap is a file that allows filtering and remapping of files
57 and directories. Comment lines start with '#'. Each line can
58 and directories. Comment lines start with '#'. Each line can
58 contain one of the following directives:
59 contain one of the following directives:
59
60
60 include path/to/file
61 include path/to/file
61
62
62 exclude path/to/file
63 exclude path/to/file
63
64
64 rename from/file to/file
65 rename from/file to/file
65
66
66 The 'include' directive causes a file, or all files under a
67 The 'include' directive causes a file, or all files under a
67 directory, to be included in the destination repository, and the
68 directory, to be included in the destination repository, and the
68 exclusion of all other files and dirs not explicitely included.
69 exclusion of all other files and dirs not explicitely included.
69 The 'exclude' directive causes files or directories to be omitted.
70 The 'exclude' directive causes files or directories to be omitted.
70 The 'rename' directive renames a file or directory. To rename from a
71 The 'rename' directive renames a file or directory. To rename from a
71 subdirectory into the root of the repository, use '.' as the path to
72 subdirectory into the root of the repository, use '.' as the path to
72 rename to.
73 rename to.
73
74
74 The splicemap is a file that allows insertion of synthetic
75 The splicemap is a file that allows insertion of synthetic
75 history, letting you specify the parents of a revision. This is
76 history, letting you specify the parents of a revision. This is
76 useful if you want to e.g. give a Subversion merge two parents, or
77 useful if you want to e.g. give a Subversion merge two parents, or
77 graft two disconnected series of history together. Each entry
78 graft two disconnected series of history together. Each entry
78 contains a key, followed by a space, followed by one or two
79 contains a key, followed by a space, followed by one or two
79 values, separated by spaces. The key is the revision ID in the
80 values, separated by spaces. The key is the revision ID in the
80 source revision control system whose parents should be modified
81 source revision control system whose parents should be modified
81 (same format as a key in .hg/shamap). The values are the revision
82 (same format as a key in .hg/shamap). The values are the revision
82 IDs (in either the source or destination revision control system)
83 IDs (in either the source or destination revision control system)
83 that should be used as the new parents for that node.
84 that should be used as the new parents for that node.
84
85
85 Mercurial Source
86 Mercurial Source
86 -----------------
87 -----------------
87
88
88 --config convert.hg.ignoreerrors=False (boolean)
89 --config convert.hg.ignoreerrors=False (boolean)
89 ignore integrity errors when reading. Use it to fix Mercurial
90 ignore integrity errors when reading. Use it to fix Mercurial
90 repositories with missing revlogs, by converting from and to
91 repositories with missing revlogs, by converting from and to
91 Mercurial.
92 Mercurial.
92 --config convert.hg.saverev=True (boolean)
93 --config convert.hg.saverev=True (boolean)
93 allow target to preserve source revision ID
94 allow target to preserve source revision ID
94 --config convert.hg.startrev=0 (hg revision identifier)
95 --config convert.hg.startrev=0 (hg revision identifier)
95 convert start revision and its descendants
96 convert start revision and its descendants
96
97
97 CVS Source
98 CVS Source
98 ----------
99 ----------
99
100
100 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
101 CVS source will use a sandbox (i.e. a checked-out copy) from CVS
101 to indicate the starting point of what will be converted. Direct
102 to indicate the starting point of what will be converted. Direct
102 access to the repository files is not needed, unless of course
103 access to the repository files is not needed, unless of course
103 the repository is :local:. The conversion uses the top level
104 the repository is :local:. The conversion uses the top level
104 directory in the sandbox to find the CVS repository, and then uses
105 directory in the sandbox to find the CVS repository, and then uses
105 CVS rlog commands to find files to convert. This means that unless
106 CVS rlog commands to find files to convert. This means that unless
106 a filemap is given, all files under the starting directory will be
107 a filemap is given, all files under the starting directory will be
107 converted, and that any directory reorganisation in the CVS
108 converted, and that any directory reorganisation in the CVS
108 sandbox is ignored.
109 sandbox is ignored.
109
110
110 Because CVS does not have changesets, it is necessary to collect
111 Because CVS does not have changesets, it is necessary to collect
111 individual commits to CVS and merge them into changesets. CVS
112 individual commits to CVS and merge them into changesets. CVS
112 source uses its internal changeset merging code by default but can
113 source uses its internal changeset merging code by default but can
113 be configured to call the external 'cvsps' program by setting:
114 be configured to call the external 'cvsps' program by setting:
114 --config convert.cvsps='cvsps -A -u --cvs-direct -q'
115 --config convert.cvsps='cvsps -A -u --cvs-direct -q'
115 This is a legacy option and may be removed in future.
116 This is a legacy option and may be removed in future.
116
117
117 The options shown are the defaults.
118 The options shown are the defaults.
118
119
119 Internal cvsps is selected by setting
120 Internal cvsps is selected by setting
120 --config convert.cvsps=builtin
121 --config convert.cvsps=builtin
121 and has a few more configurable options:
122 and has a few more configurable options:
122 --config convert.cvsps.fuzz=60 (integer)
123 --config convert.cvsps.fuzz=60 (integer)
123 Specify the maximum time (in seconds) that is allowed between
124 Specify the maximum time (in seconds) that is allowed between
124 commits with identical user and log message in a single
125 commits with identical user and log message in a single
125 changeset. When very large files were checked in as part
126 changeset. When very large files were checked in as part
126 of a changeset then the default may not be long enough.
127 of a changeset then the default may not be long enough.
127 --config convert.cvsps.mergeto='{{mergetobranch ([-\w]+)}}'
128 --config convert.cvsps.mergeto='{{mergetobranch ([-\w]+)}}'
128 Specify a regular expression to which commit log messages are
129 Specify a regular expression to which commit log messages are
129 matched. If a match occurs, then the conversion process will
130 matched. If a match occurs, then the conversion process will
130 insert a dummy revision merging the branch on which this log
131 insert a dummy revision merging the branch on which this log
131 message occurs to the branch indicated in the regex.
132 message occurs to the branch indicated in the regex.
132 --config convert.cvsps.mergefrom='{{mergefrombranch ([-\w]+)}}'
133 --config convert.cvsps.mergefrom='{{mergefrombranch ([-\w]+)}}'
133 Specify a regular expression to which commit log messages are
134 Specify a regular expression to which commit log messages are
134 matched. If a match occurs, then the conversion process will
135 matched. If a match occurs, then the conversion process will
135 add the most recent revision on the branch indicated in the
136 add the most recent revision on the branch indicated in the
136 regex as the second parent of the changeset.
137 regex as the second parent of the changeset.
137
138
138 The hgext/convert/cvsps wrapper script allows the builtin changeset
139 The hgext/convert/cvsps wrapper script allows the builtin changeset
139 merging code to be run without doing a conversion. Its parameters and
140 merging code to be run without doing a conversion. Its parameters and
140 output are similar to that of cvsps 2.1.
141 output are similar to that of cvsps 2.1.
141
142
142 Subversion Source
143 Subversion Source
143 -----------------
144 -----------------
144
145
145 Subversion source detects classical trunk/branches/tags layouts.
146 Subversion source detects classical trunk/branches/tags layouts.
146 By default, the supplied "svn://repo/path/" source URL is
147 By default, the supplied "svn://repo/path/" source URL is
147 converted as a single branch. If "svn://repo/path/trunk" exists
148 converted as a single branch. If "svn://repo/path/trunk" exists
148 it replaces the default branch. If "svn://repo/path/branches"
149 it replaces the default branch. If "svn://repo/path/branches"
149 exists, its subdirectories are listed as possible branches. If
150 exists, its subdirectories are listed as possible branches. If
150 "svn://repo/path/tags" exists, it is looked for tags referencing
151 "svn://repo/path/tags" exists, it is looked for tags referencing
151 converted branches. Default "trunk", "branches" and "tags" values
152 converted branches. Default "trunk", "branches" and "tags" values
152 can be overriden with following options. Set them to paths
153 can be overriden with following options. Set them to paths
153 relative to the source URL, or leave them blank to disable
154 relative to the source URL, or leave them blank to disable
154 autodetection.
155 autodetection.
155
156
156 --config convert.svn.branches=branches (directory name)
157 --config convert.svn.branches=branches (directory name)
157 specify the directory containing branches
158 specify the directory containing branches
158 --config convert.svn.tags=tags (directory name)
159 --config convert.svn.tags=tags (directory name)
159 specify the directory containing tags
160 specify the directory containing tags
160 --config convert.svn.trunk=trunk (directory name)
161 --config convert.svn.trunk=trunk (directory name)
161 specify the name of the trunk branch
162 specify the name of the trunk branch
162
163
163 Source history can be retrieved starting at a specific revision,
164 Source history can be retrieved starting at a specific revision,
164 instead of being integrally converted. Only single branch
165 instead of being integrally converted. Only single branch
165 conversions are supported.
166 conversions are supported.
166
167
167 --config convert.svn.startrev=0 (svn revision number)
168 --config convert.svn.startrev=0 (svn revision number)
168 specify start Subversion revision.
169 specify start Subversion revision.
169
170
170 Mercurial Destination
171 Mercurial Destination
171 ---------------------
172 ---------------------
172
173
173 --config convert.hg.clonebranches=False (boolean)
174 --config convert.hg.clonebranches=False (boolean)
174 dispatch source branches in separate clones.
175 dispatch source branches in separate clones.
175 --config convert.hg.tagsbranch=default (branch name)
176 --config convert.hg.tagsbranch=default (branch name)
176 tag revisions branch name
177 tag revisions branch name
177 --config convert.hg.usebranchnames=True (boolean)
178 --config convert.hg.usebranchnames=True (boolean)
178 preserve branch names
179 preserve branch names
179
180
180 """
181 """
181 return convcmd.convert(ui, src, dest, revmapfile, **opts)
182 return convcmd.convert(ui, src, dest, revmapfile, **opts)
182
183
183 def debugsvnlog(ui, **opts):
184 def debugsvnlog(ui, **opts):
184 return convcmd.debugsvnlog(ui, **opts)
185 return convcmd.debugsvnlog(ui, **opts)
185
186
186 commands.norepo += " convert debugsvnlog"
187 def debugcvsps(ui, *args, **opts):
188 '''Create changeset information from CVS
189
190 This command is intended as a debugging tool for the CVS to Mercurial
191 converter, and can be used as a direct replacement for cvsps.
192
193 Hg debugcvsps reads the CVS rlog for current directory (or any named
194 directory) in the CVS repository, and converts the log to a series of
195 changesets based on matching commit log entries and dates.'''
196 return cvsps.debugcvsps(ui, *args, **opts)
197
198 commands.norepo += " convert debugsvnlog debugcvsps"
187
199
188 cmdtable = {
200 cmdtable = {
189 "convert":
201 "convert":
190 (convert,
202 (convert,
191 [('A', 'authors', '', _('username mapping filename')),
203 [('A', 'authors', '', _('username mapping filename')),
192 ('d', 'dest-type', '', _('destination repository type')),
204 ('d', 'dest-type', '', _('destination repository type')),
193 ('', 'filemap', '', _('remap file names using contents of file')),
205 ('', 'filemap', '', _('remap file names using contents of file')),
194 ('r', 'rev', '', _('import up to target revision REV')),
206 ('r', 'rev', '', _('import up to target revision REV')),
195 ('s', 'source-type', '', _('source repository type')),
207 ('s', 'source-type', '', _('source repository type')),
196 ('', 'splicemap', '', _('splice synthesized history into place')),
208 ('', 'splicemap', '', _('splice synthesized history into place')),
197 ('', 'datesort', None, _('try to sort changesets by date'))],
209 ('', 'datesort', None, _('try to sort changesets by date'))],
198 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]')),
210 _('hg convert [OPTION]... SOURCE [DEST [REVMAP]]')),
199 "debugsvnlog":
211 "debugsvnlog":
200 (debugsvnlog,
212 (debugsvnlog,
201 [],
213 [],
202 'hg debugsvnlog'),
214 'hg debugsvnlog'),
215 "debugcvsps":
216 (debugcvsps,
217 [
218 # Main options shared with cvsps-2.1
219 ('b', 'branches', [], _('Only return changes on specified branches')),
220 ('p', 'prefix', '', _('Prefix to remove from file names')),
221 ('r', 'revisions', [], _('Only return changes after or between specified tags')),
222 ('u', 'update-cache', None, _("Update cvs log cache")),
223 ('x', 'new-cache', None, _("Create new cvs log cache")),
224 ('z', 'fuzz', 60, _('Set commit time fuzz in seconds')),
225 ('', 'root', '', _('Specify cvsroot')),
226 # Options specific to builtin cvsps
227 ('', 'parents', '', _('Show parent changesets')),
228 ('', 'ancestors', '', _('Show current changeset in ancestor branches')),
229 # Options that are ignored for compatibility with cvsps-2.1
230 ('A', 'cvs-direct', None, 'Ignored for compatibility'),
231 ],
232 'hg debugcvsps [OPTION]... [PATH]...'),
203 }
233 }
@@ -1,586 +1,678 b''
1 #
1 #
2 # Mercurial built-in replacement for cvsps.
2 # Mercurial built-in replacement for cvsps.
3 #
3 #
4 # Copyright 2008, Frank Kingswood <frank@kingswood-consulting.co.uk>
4 # Copyright 2008, Frank Kingswood <frank@kingswood-consulting.co.uk>
5 #
5 #
6 # This software may be used and distributed according to the terms
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 import os
9 import os
10 import re
10 import re
11 import cPickle as pickle
11 import cPickle as pickle
12 from mercurial import util
12 from mercurial import util
13 from mercurial.i18n import _
13 from mercurial.i18n import _
14
14
15 def listsort(list, key):
15 def listsort(list, key):
16 "helper to sort by key in Python 2.3"
16 "helper to sort by key in Python 2.3"
17 try:
17 try:
18 list.sort(key=key)
18 list.sort(key=key)
19 except TypeError:
19 except TypeError:
20 list.sort(lambda l, r: cmp(key(l), key(r)))
20 list.sort(lambda l, r: cmp(key(l), key(r)))
21
21
22 class logentry(object):
22 class logentry(object):
23 '''Class logentry has the following attributes:
23 '''Class logentry has the following attributes:
24 .author - author name as CVS knows it
24 .author - author name as CVS knows it
25 .branch - name of branch this revision is on
25 .branch - name of branch this revision is on
26 .branches - revision tuple of branches starting at this revision
26 .branches - revision tuple of branches starting at this revision
27 .comment - commit message
27 .comment - commit message
28 .date - the commit date as a (time, tz) tuple
28 .date - the commit date as a (time, tz) tuple
29 .dead - true if file revision is dead
29 .dead - true if file revision is dead
30 .file - Name of file
30 .file - Name of file
31 .lines - a tuple (+lines, -lines) or None
31 .lines - a tuple (+lines, -lines) or None
32 .parent - Previous revision of this entry
32 .parent - Previous revision of this entry
33 .rcs - name of file as returned from CVS
33 .rcs - name of file as returned from CVS
34 .revision - revision number as tuple
34 .revision - revision number as tuple
35 .tags - list of tags on the file
35 .tags - list of tags on the file
36 '''
36 '''
37 def __init__(self, **entries):
37 def __init__(self, **entries):
38 self.__dict__.update(entries)
38 self.__dict__.update(entries)
39
39
40 class logerror(Exception):
40 class logerror(Exception):
41 pass
41 pass
42
42
43 def getrepopath(cvspath):
43 def getrepopath(cvspath):
44 """Return the repository path from a CVS path.
44 """Return the repository path from a CVS path.
45
45
46 >>> getrepopath('/foo/bar')
46 >>> getrepopath('/foo/bar')
47 '/foo/bar'
47 '/foo/bar'
48 >>> getrepopath('c:/foo/bar')
48 >>> getrepopath('c:/foo/bar')
49 'c:/foo/bar'
49 'c:/foo/bar'
50 >>> getrepopath(':pserver:10/foo/bar')
50 >>> getrepopath(':pserver:10/foo/bar')
51 '/foo/bar'
51 '/foo/bar'
52 >>> getrepopath(':pserver:10c:/foo/bar')
52 >>> getrepopath(':pserver:10c:/foo/bar')
53 '/foo/bar'
53 '/foo/bar'
54 >>> getrepopath(':pserver:/foo/bar')
54 >>> getrepopath(':pserver:/foo/bar')
55 '/foo/bar'
55 '/foo/bar'
56 >>> getrepopath(':pserver:c:/foo/bar')
56 >>> getrepopath(':pserver:c:/foo/bar')
57 'c:/foo/bar'
57 'c:/foo/bar'
58 >>> getrepopath(':pserver:truc@foo.bar:/foo/bar')
58 >>> getrepopath(':pserver:truc@foo.bar:/foo/bar')
59 '/foo/bar'
59 '/foo/bar'
60 >>> getrepopath(':pserver:truc@foo.bar:c:/foo/bar')
60 >>> getrepopath(':pserver:truc@foo.bar:c:/foo/bar')
61 'c:/foo/bar'
61 'c:/foo/bar'
62 """
62 """
63 # According to CVS manual, CVS paths are expressed like:
63 # According to CVS manual, CVS paths are expressed like:
64 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
64 # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository
65 #
65 #
66 # Unfortunately, Windows absolute paths start with a drive letter
66 # Unfortunately, Windows absolute paths start with a drive letter
67 # like 'c:' making it harder to parse. Here we assume that drive
67 # like 'c:' making it harder to parse. Here we assume that drive
68 # letters are only one character long and any CVS component before
68 # letters are only one character long and any CVS component before
69 # the repository path is at least 2 characters long, and use this
69 # the repository path is at least 2 characters long, and use this
70 # to disambiguate.
70 # to disambiguate.
71 parts = cvspath.split(':')
71 parts = cvspath.split(':')
72 if len(parts) == 1:
72 if len(parts) == 1:
73 return parts[0]
73 return parts[0]
74 # Here there is an ambiguous case if we have a port number
74 # Here there is an ambiguous case if we have a port number
75 # immediately followed by a Windows driver letter. We assume this
75 # immediately followed by a Windows driver letter. We assume this
76 # never happens and decide it must be CVS path component,
76 # never happens and decide it must be CVS path component,
77 # therefore ignoring it.
77 # therefore ignoring it.
78 if len(parts[-2]) > 1:
78 if len(parts[-2]) > 1:
79 return parts[-1].lstrip('0123456789')
79 return parts[-1].lstrip('0123456789')
80 return parts[-2] + ':' + parts[-1]
80 return parts[-2] + ':' + parts[-1]
81
81
82 def createlog(ui, directory=None, root="", rlog=True, cache=None):
82 def createlog(ui, directory=None, root="", rlog=True, cache=None):
83 '''Collect the CVS rlog'''
83 '''Collect the CVS rlog'''
84
84
85 # Because we store many duplicate commit log messages, reusing strings
85 # Because we store many duplicate commit log messages, reusing strings
86 # saves a lot of memory and pickle storage space.
86 # saves a lot of memory and pickle storage space.
87 _scache = {}
87 _scache = {}
88 def scache(s):
88 def scache(s):
89 "return a shared version of a string"
89 "return a shared version of a string"
90 return _scache.setdefault(s, s)
90 return _scache.setdefault(s, s)
91
91
92 ui.status(_('collecting CVS rlog\n'))
92 ui.status(_('collecting CVS rlog\n'))
93
93
94 log = [] # list of logentry objects containing the CVS state
94 log = [] # list of logentry objects containing the CVS state
95
95
96 # patterns to match in CVS (r)log output, by state of use
96 # patterns to match in CVS (r)log output, by state of use
97 re_00 = re.compile('RCS file: (.+)$')
97 re_00 = re.compile('RCS file: (.+)$')
98 re_01 = re.compile('cvs \\[r?log aborted\\]: (.+)$')
98 re_01 = re.compile('cvs \\[r?log aborted\\]: (.+)$')
99 re_02 = re.compile('cvs (r?log|server): (.+)\n$')
99 re_02 = re.compile('cvs (r?log|server): (.+)\n$')
100 re_03 = re.compile("(Cannot access.+CVSROOT)|(can't create temporary directory.+)$")
100 re_03 = re.compile("(Cannot access.+CVSROOT)|(can't create temporary directory.+)$")
101 re_10 = re.compile('Working file: (.+)$')
101 re_10 = re.compile('Working file: (.+)$')
102 re_20 = re.compile('symbolic names:')
102 re_20 = re.compile('symbolic names:')
103 re_30 = re.compile('\t(.+): ([\\d.]+)$')
103 re_30 = re.compile('\t(.+): ([\\d.]+)$')
104 re_31 = re.compile('----------------------------$')
104 re_31 = re.compile('----------------------------$')
105 re_32 = re.compile('=============================================================================$')
105 re_32 = re.compile('=============================================================================$')
106 re_50 = re.compile('revision ([\\d.]+)(\s+locked by:\s+.+;)?$')
106 re_50 = re.compile('revision ([\\d.]+)(\s+locked by:\s+.+;)?$')
107 re_60 = re.compile(r'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?')
107 re_60 = re.compile(r'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?')
108 re_70 = re.compile('branches: (.+);$')
108 re_70 = re.compile('branches: (.+);$')
109
109
110 prefix = '' # leading path to strip of what we get from CVS
110 prefix = '' # leading path to strip of what we get from CVS
111
111
112 if directory is None:
112 if directory is None:
113 # Current working directory
113 # Current working directory
114
114
115 # Get the real directory in the repository
115 # Get the real directory in the repository
116 try:
116 try:
117 prefix = file(os.path.join('CVS','Repository')).read().strip()
117 prefix = file(os.path.join('CVS','Repository')).read().strip()
118 if prefix == ".":
118 if prefix == ".":
119 prefix = ""
119 prefix = ""
120 directory = prefix
120 directory = prefix
121 except IOError:
121 except IOError:
122 raise logerror('Not a CVS sandbox')
122 raise logerror('Not a CVS sandbox')
123
123
124 if prefix and not prefix.endswith(os.sep):
124 if prefix and not prefix.endswith(os.sep):
125 prefix += os.sep
125 prefix += os.sep
126
126
127 # Use the Root file in the sandbox, if it exists
127 # Use the Root file in the sandbox, if it exists
128 try:
128 try:
129 root = file(os.path.join('CVS','Root')).read().strip()
129 root = file(os.path.join('CVS','Root')).read().strip()
130 except IOError:
130 except IOError:
131 pass
131 pass
132
132
133 if not root:
133 if not root:
134 root = os.environ.get('CVSROOT', '')
134 root = os.environ.get('CVSROOT', '')
135
135
136 # read log cache if one exists
136 # read log cache if one exists
137 oldlog = []
137 oldlog = []
138 date = None
138 date = None
139
139
140 if cache:
140 if cache:
141 cachedir = os.path.expanduser('~/.hg.cvsps')
141 cachedir = os.path.expanduser('~/.hg.cvsps')
142 if not os.path.exists(cachedir):
142 if not os.path.exists(cachedir):
143 os.mkdir(cachedir)
143 os.mkdir(cachedir)
144
144
145 # The cvsps cache pickle needs a uniquified name, based on the
145 # The cvsps cache pickle needs a uniquified name, based on the
146 # repository location. The address may have all sort of nasties
146 # repository location. The address may have all sort of nasties
147 # in it, slashes, colons and such. So here we take just the
147 # in it, slashes, colons and such. So here we take just the
148 # alphanumerics, concatenated in a way that does not mix up the
148 # alphanumerics, concatenated in a way that does not mix up the
149 # various components, so that
149 # various components, so that
150 # :pserver:user@server:/path
150 # :pserver:user@server:/path
151 # and
151 # and
152 # /pserver/user/server/path
152 # /pserver/user/server/path
153 # are mapped to different cache file names.
153 # are mapped to different cache file names.
154 cachefile = root.split(":") + [directory, "cache"]
154 cachefile = root.split(":") + [directory, "cache"]
155 cachefile = ['-'.join(re.findall(r'\w+', s)) for s in cachefile if s]
155 cachefile = ['-'.join(re.findall(r'\w+', s)) for s in cachefile if s]
156 cachefile = os.path.join(cachedir,
156 cachefile = os.path.join(cachedir,
157 '.'.join([s for s in cachefile if s]))
157 '.'.join([s for s in cachefile if s]))
158
158
159 if cache == 'update':
159 if cache == 'update':
160 try:
160 try:
161 ui.note(_('reading cvs log cache %s\n') % cachefile)
161 ui.note(_('reading cvs log cache %s\n') % cachefile)
162 oldlog = pickle.load(file(cachefile))
162 oldlog = pickle.load(file(cachefile))
163 ui.note(_('cache has %d log entries\n') % len(oldlog))
163 ui.note(_('cache has %d log entries\n') % len(oldlog))
164 except Exception, e:
164 except Exception, e:
165 ui.note(_('error reading cache: %r\n') % e)
165 ui.note(_('error reading cache: %r\n') % e)
166
166
167 if oldlog:
167 if oldlog:
168 date = oldlog[-1].date # last commit date as a (time,tz) tuple
168 date = oldlog[-1].date # last commit date as a (time,tz) tuple
169 date = util.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
169 date = util.datestr(date, '%Y/%m/%d %H:%M:%S %1%2')
170
170
171 # build the CVS commandline
171 # build the CVS commandline
172 cmd = ['cvs', '-q']
172 cmd = ['cvs', '-q']
173 if root:
173 if root:
174 cmd.append('-d%s' % root)
174 cmd.append('-d%s' % root)
175 p = util.normpath(getrepopath(root))
175 p = util.normpath(getrepopath(root))
176 if not p.endswith('/'):
176 if not p.endswith('/'):
177 p += '/'
177 p += '/'
178 prefix = p + util.normpath(prefix)
178 prefix = p + util.normpath(prefix)
179 cmd.append(['log', 'rlog'][rlog])
179 cmd.append(['log', 'rlog'][rlog])
180 if date:
180 if date:
181 # no space between option and date string
181 # no space between option and date string
182 cmd.append('-d>%s' % date)
182 cmd.append('-d>%s' % date)
183 cmd.append(directory)
183 cmd.append(directory)
184
184
185 # state machine begins here
185 # state machine begins here
186 tags = {} # dictionary of revisions on current file with their tags
186 tags = {} # dictionary of revisions on current file with their tags
187 state = 0
187 state = 0
188 store = False # set when a new record can be appended
188 store = False # set when a new record can be appended
189
189
190 cmd = [util.shellquote(arg) for arg in cmd]
190 cmd = [util.shellquote(arg) for arg in cmd]
191 ui.note(_("running %s\n") % (' '.join(cmd)))
191 ui.note(_("running %s\n") % (' '.join(cmd)))
192 ui.debug(_("prefix=%r directory=%r root=%r\n") % (prefix, directory, root))
192 ui.debug(_("prefix=%r directory=%r root=%r\n") % (prefix, directory, root))
193
193
194 for line in util.popen(' '.join(cmd)):
194 for line in util.popen(' '.join(cmd)):
195 if line.endswith('\n'):
195 if line.endswith('\n'):
196 line = line[:-1]
196 line = line[:-1]
197 #ui.debug('state=%d line=%r\n' % (state, line))
197 #ui.debug('state=%d line=%r\n' % (state, line))
198
198
199 if state == 0:
199 if state == 0:
200 # initial state, consume input until we see 'RCS file'
200 # initial state, consume input until we see 'RCS file'
201 match = re_00.match(line)
201 match = re_00.match(line)
202 if match:
202 if match:
203 rcs = match.group(1)
203 rcs = match.group(1)
204 tags = {}
204 tags = {}
205 if rlog:
205 if rlog:
206 filename = util.normpath(rcs[:-2])
206 filename = util.normpath(rcs[:-2])
207 if filename.startswith(prefix):
207 if filename.startswith(prefix):
208 filename = filename[len(prefix):]
208 filename = filename[len(prefix):]
209 if filename.startswith('/'):
209 if filename.startswith('/'):
210 filename = filename[1:]
210 filename = filename[1:]
211 if filename.startswith('Attic/'):
211 if filename.startswith('Attic/'):
212 filename = filename[6:]
212 filename = filename[6:]
213 else:
213 else:
214 filename = filename.replace('/Attic/', '/')
214 filename = filename.replace('/Attic/', '/')
215 state = 2
215 state = 2
216 continue
216 continue
217 state = 1
217 state = 1
218 continue
218 continue
219 match = re_01.match(line)
219 match = re_01.match(line)
220 if match:
220 if match:
221 raise Exception(match.group(1))
221 raise Exception(match.group(1))
222 match = re_02.match(line)
222 match = re_02.match(line)
223 if match:
223 if match:
224 raise Exception(match.group(2))
224 raise Exception(match.group(2))
225 if re_03.match(line):
225 if re_03.match(line):
226 raise Exception(line)
226 raise Exception(line)
227
227
228 elif state == 1:
228 elif state == 1:
229 # expect 'Working file' (only when using log instead of rlog)
229 # expect 'Working file' (only when using log instead of rlog)
230 match = re_10.match(line)
230 match = re_10.match(line)
231 assert match, _('RCS file must be followed by working file')
231 assert match, _('RCS file must be followed by working file')
232 filename = util.normpath(match.group(1))
232 filename = util.normpath(match.group(1))
233 state = 2
233 state = 2
234
234
235 elif state == 2:
235 elif state == 2:
236 # expect 'symbolic names'
236 # expect 'symbolic names'
237 if re_20.match(line):
237 if re_20.match(line):
238 state = 3
238 state = 3
239
239
240 elif state == 3:
240 elif state == 3:
241 # read the symbolic names and store as tags
241 # read the symbolic names and store as tags
242 match = re_30.match(line)
242 match = re_30.match(line)
243 if match:
243 if match:
244 rev = [int(x) for x in match.group(2).split('.')]
244 rev = [int(x) for x in match.group(2).split('.')]
245
245
246 # Convert magic branch number to an odd-numbered one
246 # Convert magic branch number to an odd-numbered one
247 revn = len(rev)
247 revn = len(rev)
248 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
248 if revn > 3 and (revn % 2) == 0 and rev[-2] == 0:
249 rev = rev[:-2] + rev[-1:]
249 rev = rev[:-2] + rev[-1:]
250 rev = tuple(rev)
250 rev = tuple(rev)
251
251
252 if rev not in tags:
252 if rev not in tags:
253 tags[rev] = []
253 tags[rev] = []
254 tags[rev].append(match.group(1))
254 tags[rev].append(match.group(1))
255
255
256 elif re_31.match(line):
256 elif re_31.match(line):
257 state = 5
257 state = 5
258 elif re_32.match(line):
258 elif re_32.match(line):
259 state = 0
259 state = 0
260
260
261 elif state == 4:
261 elif state == 4:
262 # expecting '------' separator before first revision
262 # expecting '------' separator before first revision
263 if re_31.match(line):
263 if re_31.match(line):
264 state = 5
264 state = 5
265 else:
265 else:
266 assert not re_32.match(line), _('Must have at least some revisions')
266 assert not re_32.match(line), _('Must have at least some revisions')
267
267
268 elif state == 5:
268 elif state == 5:
269 # expecting revision number and possibly (ignored) lock indication
269 # expecting revision number and possibly (ignored) lock indication
270 # we create the logentry here from values stored in states 0 to 4,
270 # we create the logentry here from values stored in states 0 to 4,
271 # as this state is re-entered for subsequent revisions of a file.
271 # as this state is re-entered for subsequent revisions of a file.
272 match = re_50.match(line)
272 match = re_50.match(line)
273 assert match, _('expected revision number')
273 assert match, _('expected revision number')
274 e = logentry(rcs=scache(rcs), file=scache(filename),
274 e = logentry(rcs=scache(rcs), file=scache(filename),
275 revision=tuple([int(x) for x in match.group(1).split('.')]),
275 revision=tuple([int(x) for x in match.group(1).split('.')]),
276 branches=[], parent=None)
276 branches=[], parent=None)
277 state = 6
277 state = 6
278
278
279 elif state == 6:
279 elif state == 6:
280 # expecting date, author, state, lines changed
280 # expecting date, author, state, lines changed
281 match = re_60.match(line)
281 match = re_60.match(line)
282 assert match, _('revision must be followed by date line')
282 assert match, _('revision must be followed by date line')
283 d = match.group(1)
283 d = match.group(1)
284 if d[2] == '/':
284 if d[2] == '/':
285 # Y2K
285 # Y2K
286 d = '19' + d
286 d = '19' + d
287
287
288 if len(d.split()) != 3:
288 if len(d.split()) != 3:
289 # cvs log dates always in GMT
289 # cvs log dates always in GMT
290 d = d + ' UTC'
290 d = d + ' UTC'
291 e.date = util.parsedate(d, ['%y/%m/%d %H:%M:%S', '%Y/%m/%d %H:%M:%S', '%Y-%m-%d %H:%M:%S'])
291 e.date = util.parsedate(d, ['%y/%m/%d %H:%M:%S', '%Y/%m/%d %H:%M:%S', '%Y-%m-%d %H:%M:%S'])
292 e.author = scache(match.group(2))
292 e.author = scache(match.group(2))
293 e.dead = match.group(3).lower() == 'dead'
293 e.dead = match.group(3).lower() == 'dead'
294
294
295 if match.group(5):
295 if match.group(5):
296 if match.group(6):
296 if match.group(6):
297 e.lines = (int(match.group(5)), int(match.group(6)))
297 e.lines = (int(match.group(5)), int(match.group(6)))
298 else:
298 else:
299 e.lines = (int(match.group(5)), 0)
299 e.lines = (int(match.group(5)), 0)
300 elif match.group(6):
300 elif match.group(6):
301 e.lines = (0, int(match.group(6)))
301 e.lines = (0, int(match.group(6)))
302 else:
302 else:
303 e.lines = None
303 e.lines = None
304 e.comment = []
304 e.comment = []
305 state = 7
305 state = 7
306
306
307 elif state == 7:
307 elif state == 7:
308 # read the revision numbers of branches that start at this revision
308 # read the revision numbers of branches that start at this revision
309 # or store the commit log message otherwise
309 # or store the commit log message otherwise
310 m = re_70.match(line)
310 m = re_70.match(line)
311 if m:
311 if m:
312 e.branches = [tuple([int(y) for y in x.strip().split('.')])
312 e.branches = [tuple([int(y) for y in x.strip().split('.')])
313 for x in m.group(1).split(';')]
313 for x in m.group(1).split(';')]
314 state = 8
314 state = 8
315 elif re_31.match(line):
315 elif re_31.match(line):
316 state = 5
316 state = 5
317 store = True
317 store = True
318 elif re_32.match(line):
318 elif re_32.match(line):
319 state = 0
319 state = 0
320 store = True
320 store = True
321 else:
321 else:
322 e.comment.append(line)
322 e.comment.append(line)
323
323
324 elif state == 8:
324 elif state == 8:
325 # store commit log message
325 # store commit log message
326 if re_31.match(line):
326 if re_31.match(line):
327 state = 5
327 state = 5
328 store = True
328 store = True
329 elif re_32.match(line):
329 elif re_32.match(line):
330 state = 0
330 state = 0
331 store = True
331 store = True
332 else:
332 else:
333 e.comment.append(line)
333 e.comment.append(line)
334
334
335 if store:
335 if store:
336 # clean up the results and save in the log.
336 # clean up the results and save in the log.
337 store = False
337 store = False
338 e.tags = util.sort([scache(x) for x in tags.get(e.revision, [])])
338 e.tags = util.sort([scache(x) for x in tags.get(e.revision, [])])
339 e.comment = scache('\n'.join(e.comment))
339 e.comment = scache('\n'.join(e.comment))
340
340
341 revn = len(e.revision)
341 revn = len(e.revision)
342 if revn > 3 and (revn % 2) == 0:
342 if revn > 3 and (revn % 2) == 0:
343 e.branch = tags.get(e.revision[:-1], [None])[0]
343 e.branch = tags.get(e.revision[:-1], [None])[0]
344 else:
344 else:
345 e.branch = None
345 e.branch = None
346
346
347 log.append(e)
347 log.append(e)
348
348
349 if len(log) % 100 == 0:
349 if len(log) % 100 == 0:
350 ui.status(util.ellipsis('%d %s' % (len(log), e.file), 80)+'\n')
350 ui.status(util.ellipsis('%d %s' % (len(log), e.file), 80)+'\n')
351
351
352 listsort(log, key=lambda x:(x.rcs, x.revision))
352 listsort(log, key=lambda x:(x.rcs, x.revision))
353
353
354 # find parent revisions of individual files
354 # find parent revisions of individual files
355 versions = {}
355 versions = {}
356 for e in log:
356 for e in log:
357 branch = e.revision[:-1]
357 branch = e.revision[:-1]
358 p = versions.get((e.rcs, branch), None)
358 p = versions.get((e.rcs, branch), None)
359 if p is None:
359 if p is None:
360 p = e.revision[:-2]
360 p = e.revision[:-2]
361 e.parent = p
361 e.parent = p
362 versions[(e.rcs, branch)] = e.revision
362 versions[(e.rcs, branch)] = e.revision
363
363
364 # update the log cache
364 # update the log cache
365 if cache:
365 if cache:
366 if log:
366 if log:
367 # join up the old and new logs
367 # join up the old and new logs
368 listsort(log, key=lambda x:x.date)
368 listsort(log, key=lambda x:x.date)
369
369
370 if oldlog and oldlog[-1].date >= log[0].date:
370 if oldlog and oldlog[-1].date >= log[0].date:
371 raise logerror('Log cache overlaps with new log entries,'
371 raise logerror('Log cache overlaps with new log entries,'
372 ' re-run without cache.')
372 ' re-run without cache.')
373
373
374 log = oldlog + log
374 log = oldlog + log
375
375
376 # write the new cachefile
376 # write the new cachefile
377 ui.note(_('writing cvs log cache %s\n') % cachefile)
377 ui.note(_('writing cvs log cache %s\n') % cachefile)
378 pickle.dump(log, file(cachefile, 'w'))
378 pickle.dump(log, file(cachefile, 'w'))
379 else:
379 else:
380 log = oldlog
380 log = oldlog
381
381
382 ui.status(_('%d log entries\n') % len(log))
382 ui.status(_('%d log entries\n') % len(log))
383
383
384 return log
384 return log
385
385
386
386
387 class changeset(object):
387 class changeset(object):
388 '''Class changeset has the following attributes:
388 '''Class changeset has the following attributes:
389 .author - author name as CVS knows it
389 .author - author name as CVS knows it
390 .branch - name of branch this changeset is on, or None
390 .branch - name of branch this changeset is on, or None
391 .comment - commit message
391 .comment - commit message
392 .date - the commit date as a (time,tz) tuple
392 .date - the commit date as a (time,tz) tuple
393 .entries - list of logentry objects in this changeset
393 .entries - list of logentry objects in this changeset
394 .parents - list of one or two parent changesets
394 .parents - list of one or two parent changesets
395 .tags - list of tags on this changeset
395 .tags - list of tags on this changeset
396 '''
396 '''
397 def __init__(self, **entries):
397 def __init__(self, **entries):
398 self.__dict__.update(entries)
398 self.__dict__.update(entries)
399
399
400 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
400 def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None):
401 '''Convert log into changesets.'''
401 '''Convert log into changesets.'''
402
402
403 ui.status(_('creating changesets\n'))
403 ui.status(_('creating changesets\n'))
404
404
405 # Merge changesets
405 # Merge changesets
406
406
407 listsort(log, key=lambda x:(x.comment, x.author, x.branch, x.date))
407 listsort(log, key=lambda x:(x.comment, x.author, x.branch, x.date))
408
408
409 changesets = []
409 changesets = []
410 files = {}
410 files = {}
411 c = None
411 c = None
412 for i, e in enumerate(log):
412 for i, e in enumerate(log):
413
413
414 # Check if log entry belongs to the current changeset or not.
414 # Check if log entry belongs to the current changeset or not.
415 if not (c and
415 if not (c and
416 e.comment == c.comment and
416 e.comment == c.comment and
417 e.author == c.author and
417 e.author == c.author and
418 e.branch == c.branch and
418 e.branch == c.branch and
419 ((c.date[0] + c.date[1]) <=
419 ((c.date[0] + c.date[1]) <=
420 (e.date[0] + e.date[1]) <=
420 (e.date[0] + e.date[1]) <=
421 (c.date[0] + c.date[1]) + fuzz) and
421 (c.date[0] + c.date[1]) + fuzz) and
422 e.file not in files):
422 e.file not in files):
423 c = changeset(comment=e.comment, author=e.author,
423 c = changeset(comment=e.comment, author=e.author,
424 branch=e.branch, date=e.date, entries=[])
424 branch=e.branch, date=e.date, entries=[])
425 changesets.append(c)
425 changesets.append(c)
426 files = {}
426 files = {}
427 if len(changesets) % 100 == 0:
427 if len(changesets) % 100 == 0:
428 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
428 t = '%d %s' % (len(changesets), repr(e.comment)[1:-1])
429 ui.status(util.ellipsis(t, 80) + '\n')
429 ui.status(util.ellipsis(t, 80) + '\n')
430
430
431 c.entries.append(e)
431 c.entries.append(e)
432 files[e.file] = True
432 files[e.file] = True
433 c.date = e.date # changeset date is date of latest commit in it
433 c.date = e.date # changeset date is date of latest commit in it
434
434
435 # Sort files in each changeset
435 # Sort files in each changeset
436
436
437 for c in changesets:
437 for c in changesets:
438 def pathcompare(l, r):
438 def pathcompare(l, r):
439 'Mimic cvsps sorting order'
439 'Mimic cvsps sorting order'
440 l = l.split('/')
440 l = l.split('/')
441 r = r.split('/')
441 r = r.split('/')
442 nl = len(l)
442 nl = len(l)
443 nr = len(r)
443 nr = len(r)
444 n = min(nl, nr)
444 n = min(nl, nr)
445 for i in range(n):
445 for i in range(n):
446 if i + 1 == nl and nl < nr:
446 if i + 1 == nl and nl < nr:
447 return -1
447 return -1
448 elif i + 1 == nr and nl > nr:
448 elif i + 1 == nr and nl > nr:
449 return +1
449 return +1
450 elif l[i] < r[i]:
450 elif l[i] < r[i]:
451 return -1
451 return -1
452 elif l[i] > r[i]:
452 elif l[i] > r[i]:
453 return +1
453 return +1
454 return 0
454 return 0
455 def entitycompare(l, r):
455 def entitycompare(l, r):
456 return pathcompare(l.file, r.file)
456 return pathcompare(l.file, r.file)
457
457
458 c.entries.sort(entitycompare)
458 c.entries.sort(entitycompare)
459
459
460 # Sort changesets by date
460 # Sort changesets by date
461
461
462 def cscmp(l, r):
462 def cscmp(l, r):
463 d = sum(l.date) - sum(r.date)
463 d = sum(l.date) - sum(r.date)
464 if d:
464 if d:
465 return d
465 return d
466
466
467 # detect vendor branches and initial commits on a branch
467 # detect vendor branches and initial commits on a branch
468 le = {}
468 le = {}
469 for e in l.entries:
469 for e in l.entries:
470 le[e.rcs] = e.revision
470 le[e.rcs] = e.revision
471 re = {}
471 re = {}
472 for e in r.entries:
472 for e in r.entries:
473 re[e.rcs] = e.revision
473 re[e.rcs] = e.revision
474
474
475 d = 0
475 d = 0
476 for e in l.entries:
476 for e in l.entries:
477 if re.get(e.rcs, None) == e.parent:
477 if re.get(e.rcs, None) == e.parent:
478 assert not d
478 assert not d
479 d = 1
479 d = 1
480 break
480 break
481
481
482 for e in r.entries:
482 for e in r.entries:
483 if le.get(e.rcs, None) == e.parent:
483 if le.get(e.rcs, None) == e.parent:
484 assert not d
484 assert not d
485 d = -1
485 d = -1
486 break
486 break
487
487
488 return d
488 return d
489
489
490 changesets.sort(cscmp)
490 changesets.sort(cscmp)
491
491
492 # Collect tags
492 # Collect tags
493
493
494 globaltags = {}
494 globaltags = {}
495 for c in changesets:
495 for c in changesets:
496 tags = {}
496 tags = {}
497 for e in c.entries:
497 for e in c.entries:
498 for tag in e.tags:
498 for tag in e.tags:
499 # remember which is the latest changeset to have this tag
499 # remember which is the latest changeset to have this tag
500 globaltags[tag] = c
500 globaltags[tag] = c
501
501
502 for c in changesets:
502 for c in changesets:
503 tags = {}
503 tags = {}
504 for e in c.entries:
504 for e in c.entries:
505 for tag in e.tags:
505 for tag in e.tags:
506 tags[tag] = True
506 tags[tag] = True
507 # remember tags only if this is the latest changeset to have it
507 # remember tags only if this is the latest changeset to have it
508 c.tags = util.sort([tag for tag in tags if globaltags[tag] is c])
508 c.tags = util.sort([tag for tag in tags if globaltags[tag] is c])
509
509
510 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
510 # Find parent changesets, handle {{mergetobranch BRANCHNAME}}
511 # by inserting dummy changesets with two parents, and handle
511 # by inserting dummy changesets with two parents, and handle
512 # {{mergefrombranch BRANCHNAME}} by setting two parents.
512 # {{mergefrombranch BRANCHNAME}} by setting two parents.
513
513
514 if mergeto is None:
514 if mergeto is None:
515 mergeto = r'{{mergetobranch ([-\w]+)}}'
515 mergeto = r'{{mergetobranch ([-\w]+)}}'
516 if mergeto:
516 if mergeto:
517 mergeto = re.compile(mergeto)
517 mergeto = re.compile(mergeto)
518
518
519 if mergefrom is None:
519 if mergefrom is None:
520 mergefrom = r'{{mergefrombranch ([-\w]+)}}'
520 mergefrom = r'{{mergefrombranch ([-\w]+)}}'
521 if mergefrom:
521 if mergefrom:
522 mergefrom = re.compile(mergefrom)
522 mergefrom = re.compile(mergefrom)
523
523
524 versions = {} # changeset index where we saw any particular file version
524 versions = {} # changeset index where we saw any particular file version
525 branches = {} # changeset index where we saw a branch
525 branches = {} # changeset index where we saw a branch
526 n = len(changesets)
526 n = len(changesets)
527 i = 0
527 i = 0
528 while i<n:
528 while i<n:
529 c = changesets[i]
529 c = changesets[i]
530
530
531 for f in c.entries:
531 for f in c.entries:
532 versions[(f.rcs, f.revision)] = i
532 versions[(f.rcs, f.revision)] = i
533
533
534 p = None
534 p = None
535 if c.branch in branches:
535 if c.branch in branches:
536 p = branches[c.branch]
536 p = branches[c.branch]
537 else:
537 else:
538 for f in c.entries:
538 for f in c.entries:
539 p = max(p, versions.get((f.rcs, f.parent), None))
539 p = max(p, versions.get((f.rcs, f.parent), None))
540
540
541 c.parents = []
541 c.parents = []
542 if p is not None:
542 if p is not None:
543 c.parents.append(changesets[p])
543 c.parents.append(changesets[p])
544
544
545 if mergefrom:
545 if mergefrom:
546 m = mergefrom.search(c.comment)
546 m = mergefrom.search(c.comment)
547 if m:
547 if m:
548 m = m.group(1)
548 m = m.group(1)
549 if m == 'HEAD':
549 if m == 'HEAD':
550 m = None
550 m = None
551 if m in branches and c.branch != m:
551 if m in branches and c.branch != m:
552 c.parents.append(changesets[branches[m]])
552 c.parents.append(changesets[branches[m]])
553
553
554 if mergeto:
554 if mergeto:
555 m = mergeto.search(c.comment)
555 m = mergeto.search(c.comment)
556 if m:
556 if m:
557 try:
557 try:
558 m = m.group(1)
558 m = m.group(1)
559 if m == 'HEAD':
559 if m == 'HEAD':
560 m = None
560 m = None
561 except:
561 except:
562 m = None # if no group found then merge to HEAD
562 m = None # if no group found then merge to HEAD
563 if m in branches and c.branch != m:
563 if m in branches and c.branch != m:
564 # insert empty changeset for merge
564 # insert empty changeset for merge
565 cc = changeset(author=c.author, branch=m, date=c.date,
565 cc = changeset(author=c.author, branch=m, date=c.date,
566 comment='convert-repo: CVS merge from branch %s' % c.branch,
566 comment='convert-repo: CVS merge from branch %s' % c.branch,
567 entries=[], tags=[], parents=[changesets[branches[m]], c])
567 entries=[], tags=[], parents=[changesets[branches[m]], c])
568 changesets.insert(i + 1, cc)
568 changesets.insert(i + 1, cc)
569 branches[m] = i + 1
569 branches[m] = i + 1
570
570
571 # adjust our loop counters now we have inserted a new entry
571 # adjust our loop counters now we have inserted a new entry
572 n += 1
572 n += 1
573 i += 2
573 i += 2
574 continue
574 continue
575
575
576 branches[c.branch] = i
576 branches[c.branch] = i
577 i += 1
577 i += 1
578
578
579 # Number changesets
579 # Number changesets
580
580
581 for i, c in enumerate(changesets):
581 for i, c in enumerate(changesets):
582 c.id = i + 1
582 c.id = i + 1
583
583
584 ui.status(_('%d changeset entries\n') % len(changesets))
584 ui.status(_('%d changeset entries\n') % len(changesets))
585
585
586 return changesets
586 return changesets
587
588
589 def debugcvsps(ui, *args, **opts):
590 '''Read CVS rlog for current directory or named path in repository, and
591 convert the log to changesets based on matching commit log entries and dates.'''
592
593 if opts["new_cache"]:
594 cache = "write"
595 elif opts["update_cache"]:
596 cache = "update"
597 else:
598 cache = None
599
600 revisions = opts["revisions"]
601
602 try:
603 if args:
604 log = []
605 for d in args:
606 log += createlog(ui, d, root=opts["root"], cache=cache)
607 else:
608 log = createlog(ui, root=opts["root"], cache=cache)
609 except logerror, e:
610 ui.write("%r\n"%e)
611 return
612
613 changesets = createchangeset(ui, log, opts["fuzz"])
614 del log
615
616 # Print changesets (optionally filtered)
617
618 off = len(revisions)
619 branches = {} # latest version number in each branch
620 ancestors = {} # parent branch
621 for cs in changesets:
622
623 if opts["ancestors"]:
624 if cs.branch not in branches and cs.parents and cs.parents[0].id:
625 ancestors[cs.branch] = changesets[cs.parents[0].id-1].branch, cs.parents[0].id
626 branches[cs.branch] = cs.id
627
628 # limit by branches
629 if opts["branches"] and (cs.branch or 'HEAD') not in opts["branches"]:
630 continue
631
632 if not off:
633 # Note: trailing spaces on several lines here are needed to have
634 # bug-for-bug compatibility with cvsps.
635 ui.write('---------------------\n')
636 ui.write('PatchSet %d \n' % cs.id)
637 ui.write('Date: %s\n' % util.datestr(cs.date, '%Y/%m/%d %H:%M:%S %1%2'))
638 ui.write('Author: %s\n' % cs.author)
639 ui.write('Branch: %s\n' % (cs.branch or 'HEAD'))
640 ui.write('Tag%s: %s \n' % (['', 's'][len(cs.tags)>1],
641 ','.join(cs.tags) or '(none)'))
642 if opts["parents"] and cs.parents:
643 if len(cs.parents)>1:
644 ui.write('Parents: %s\n' % (','.join([str(p.id) for p in cs.parents])))
645 else:
646 ui.write('Parent: %d\n' % cs.parents[0].id)
647
648 if opts["ancestors"]:
649 b = cs.branch
650 r = []
651 while b:
652 b, c = ancestors[b]
653 r.append('%s:%d:%d' % (b or "HEAD", c, branches[b]))
654 if r:
655 ui.write('Ancestors: %s\n' % (','.join(r)))
656
657 ui.write('Log:\n')
658 ui.write('%s\n\n' % cs.comment)
659 ui.write('Members: \n')
660 for f in cs.entries:
661 fn = f.file
662 if fn.startswith(opts["prefix"]):
663 fn = fn[len(opts["prefix"]):]
664 ui.write('\t%s:%s->%s%s \n' % (fn, '.'.join([str(x) for x in f.parent]) or 'INITIAL',
665 '.'.join([str(x) for x in f.revision]), ['', '(DEAD)'][f.dead]))
666 ui.write('\n')
667
668 # have we seen the start tag?
669 if revisions and off:
670 if revisions[0] == str(cs.id) or \
671 revisions[0] in cs.tags:
672 off = False
673
674 # see if we reached the end tag
675 if len(revisions)>1 and not off:
676 if revisions[1] == str(cs.id) or \
677 revisions[1] in cs.tags:
678 break
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now