##// END OF EJS Templates
removed trailing whitespace
Thomas Arendsen Hein -
r4957:cdd33a04 default
parent child Browse files
Show More
@@ -1,353 +1,353 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
7
8 from common import NoRepo, converter_source, converter_sink
8 from common import NoRepo, converter_source, converter_sink
9 from cvs import convert_cvs
9 from cvs import convert_cvs
10 from git import convert_git
10 from git import convert_git
11 from hg import convert_mercurial
11 from hg import convert_mercurial
12 from subversion import convert_svn
12 from subversion import convert_svn
13
13
14 import os, shutil
14 import os, shutil
15 from mercurial import hg, ui, util, commands
15 from mercurial import hg, ui, util, commands
16
16
17 commands.norepo += " convert"
17 commands.norepo += " convert"
18
18
19 converters = [convert_cvs, convert_git, convert_svn, convert_mercurial]
19 converters = [convert_cvs, convert_git, convert_svn, convert_mercurial]
20
20
21 def convertsource(ui, path, **opts):
21 def convertsource(ui, path, **opts):
22 for c in converters:
22 for c in converters:
23 if not hasattr(c, 'getcommit'):
23 if not hasattr(c, 'getcommit'):
24 continue
24 continue
25 try:
25 try:
26 return c(ui, path, **opts)
26 return c(ui, path, **opts)
27 except NoRepo:
27 except NoRepo:
28 pass
28 pass
29 raise util.Abort('%s: unknown repository type' % path)
29 raise util.Abort('%s: unknown repository type' % path)
30
30
31 def convertsink(ui, path):
31 def convertsink(ui, path):
32 if not os.path.isdir(path):
32 if not os.path.isdir(path):
33 raise util.Abort("%s: not a directory" % path)
33 raise util.Abort("%s: not a directory" % path)
34 for c in converters:
34 for c in converters:
35 if not hasattr(c, 'putcommit'):
35 if not hasattr(c, 'putcommit'):
36 continue
36 continue
37 try:
37 try:
38 return c(ui, path)
38 return c(ui, path)
39 except NoRepo:
39 except NoRepo:
40 pass
40 pass
41 raise util.Abort('%s: unknown repository type' % path)
41 raise util.Abort('%s: unknown repository type' % path)
42
42
43 class convert(object):
43 class convert(object):
44 def __init__(self, ui, source, dest, mapfile, opts):
44 def __init__(self, ui, source, dest, mapfile, opts):
45
45
46 self.source = source
46 self.source = source
47 self.dest = dest
47 self.dest = dest
48 self.ui = ui
48 self.ui = ui
49 self.opts = opts
49 self.opts = opts
50 self.commitcache = {}
50 self.commitcache = {}
51 self.mapfile = mapfile
51 self.mapfile = mapfile
52 self.mapfilefd = None
52 self.mapfilefd = None
53 self.authors = {}
53 self.authors = {}
54 self.authorfile = None
54 self.authorfile = None
55
55
56 self.map = {}
56 self.map = {}
57 try:
57 try:
58 origmapfile = open(self.mapfile, 'r')
58 origmapfile = open(self.mapfile, 'r')
59 for l in origmapfile:
59 for l in origmapfile:
60 sv, dv = l[:-1].split()
60 sv, dv = l[:-1].split()
61 self.map[sv] = dv
61 self.map[sv] = dv
62 origmapfile.close()
62 origmapfile.close()
63 except IOError:
63 except IOError:
64 pass
64 pass
65
65
66 # Read first the dst author map if any
66 # Read first the dst author map if any
67 authorfile = self.dest.authorfile()
67 authorfile = self.dest.authorfile()
68 if authorfile and os.path.exists(authorfile):
68 if authorfile and os.path.exists(authorfile):
69 self.readauthormap(authorfile)
69 self.readauthormap(authorfile)
70 # Extend/Override with new author map if necessary
70 # Extend/Override with new author map if necessary
71 if opts.get('authors'):
71 if opts.get('authors'):
72 self.readauthormap(opts.get('authors'))
72 self.readauthormap(opts.get('authors'))
73 self.authorfile = self.dest.authorfile()
73 self.authorfile = self.dest.authorfile()
74
74
75 def walktree(self, heads):
75 def walktree(self, heads):
76 '''Return a mapping that identifies the uncommitted parents of every
76 '''Return a mapping that identifies the uncommitted parents of every
77 uncommitted changeset.'''
77 uncommitted changeset.'''
78 visit = heads
78 visit = heads
79 known = {}
79 known = {}
80 parents = {}
80 parents = {}
81 while visit:
81 while visit:
82 n = visit.pop(0)
82 n = visit.pop(0)
83 if n in known or n in self.map: continue
83 if n in known or n in self.map: continue
84 known[n] = 1
84 known[n] = 1
85 self.commitcache[n] = self.source.getcommit(n)
85 self.commitcache[n] = self.source.getcommit(n)
86 cp = self.commitcache[n].parents
86 cp = self.commitcache[n].parents
87 parents[n] = []
87 parents[n] = []
88 for p in cp:
88 for p in cp:
89 parents[n].append(p)
89 parents[n].append(p)
90 visit.append(p)
90 visit.append(p)
91
91
92 return parents
92 return parents
93
93
94 def toposort(self, parents):
94 def toposort(self, parents):
95 '''Return an ordering such that every uncommitted changeset is
95 '''Return an ordering such that every uncommitted changeset is
96 preceeded by all its uncommitted ancestors.'''
96 preceeded by all its uncommitted ancestors.'''
97 visit = parents.keys()
97 visit = parents.keys()
98 seen = {}
98 seen = {}
99 children = {}
99 children = {}
100
100
101 while visit:
101 while visit:
102 n = visit.pop(0)
102 n = visit.pop(0)
103 if n in seen: continue
103 if n in seen: continue
104 seen[n] = 1
104 seen[n] = 1
105 # Ensure that nodes without parents are present in the 'children'
105 # Ensure that nodes without parents are present in the 'children'
106 # mapping.
106 # mapping.
107 children.setdefault(n, [])
107 children.setdefault(n, [])
108 for p in parents[n]:
108 for p in parents[n]:
109 if not p in self.map:
109 if not p in self.map:
110 visit.append(p)
110 visit.append(p)
111 children.setdefault(p, []).append(n)
111 children.setdefault(p, []).append(n)
112
112
113 s = []
113 s = []
114 removed = {}
114 removed = {}
115 visit = children.keys()
115 visit = children.keys()
116 while visit:
116 while visit:
117 n = visit.pop(0)
117 n = visit.pop(0)
118 if n in removed: continue
118 if n in removed: continue
119 dep = 0
119 dep = 0
120 if n in parents:
120 if n in parents:
121 for p in parents[n]:
121 for p in parents[n]:
122 if p in self.map: continue
122 if p in self.map: continue
123 if p not in removed:
123 if p not in removed:
124 # we're still dependent
124 # we're still dependent
125 visit.append(n)
125 visit.append(n)
126 dep = 1
126 dep = 1
127 break
127 break
128
128
129 if not dep:
129 if not dep:
130 # all n's parents are in the list
130 # all n's parents are in the list
131 removed[n] = 1
131 removed[n] = 1
132 if n not in self.map:
132 if n not in self.map:
133 s.append(n)
133 s.append(n)
134 if n in children:
134 if n in children:
135 for c in children[n]:
135 for c in children[n]:
136 visit.insert(0, c)
136 visit.insert(0, c)
137
137
138 if self.opts.get('datesort'):
138 if self.opts.get('datesort'):
139 depth = {}
139 depth = {}
140 for n in s:
140 for n in s:
141 depth[n] = 0
141 depth[n] = 0
142 pl = [p for p in self.commitcache[n].parents
142 pl = [p for p in self.commitcache[n].parents
143 if p not in self.map]
143 if p not in self.map]
144 if pl:
144 if pl:
145 depth[n] = max([depth[p] for p in pl]) + 1
145 depth[n] = max([depth[p] for p in pl]) + 1
146
146
147 s = [(depth[n], self.commitcache[n].date, n) for n in s]
147 s = [(depth[n], self.commitcache[n].date, n) for n in s]
148 s.sort()
148 s.sort()
149 s = [e[2] for e in s]
149 s = [e[2] for e in s]
150
150
151 return s
151 return s
152
152
153 def mapentry(self, src, dst):
153 def mapentry(self, src, dst):
154 if self.mapfilefd is None:
154 if self.mapfilefd is None:
155 try:
155 try:
156 self.mapfilefd = open(self.mapfile, "a")
156 self.mapfilefd = open(self.mapfile, "a")
157 except IOError, (errno, strerror):
157 except IOError, (errno, strerror):
158 raise util.Abort("Could not open map file %s: %s, %s\n" % (self.mapfile, errno, strerror))
158 raise util.Abort("Could not open map file %s: %s, %s\n" % (self.mapfile, errno, strerror))
159 self.map[src] = dst
159 self.map[src] = dst
160 self.mapfilefd.write("%s %s\n" % (src, dst))
160 self.mapfilefd.write("%s %s\n" % (src, dst))
161 self.mapfilefd.flush()
161 self.mapfilefd.flush()
162
162
163 def writeauthormap(self):
163 def writeauthormap(self):
164 authorfile = self.authorfile
164 authorfile = self.authorfile
165 if authorfile:
165 if authorfile:
166 self.ui.status('Writing author map file %s\n' % authorfile)
166 self.ui.status('Writing author map file %s\n' % authorfile)
167 ofile = open(authorfile, 'w+')
167 ofile = open(authorfile, 'w+')
168 for author in self.authors:
168 for author in self.authors:
169 ofile.write("%s=%s\n" % (author, self.authors[author]))
169 ofile.write("%s=%s\n" % (author, self.authors[author]))
170 ofile.close()
170 ofile.close()
171
171
172 def readauthormap(self, authorfile):
172 def readauthormap(self, authorfile):
173 afile = open(authorfile, 'r')
173 afile = open(authorfile, 'r')
174 for line in afile:
174 for line in afile:
175 try:
175 try:
176 srcauthor = line.split('=')[0].strip()
176 srcauthor = line.split('=')[0].strip()
177 dstauthor = line.split('=')[1].strip()
177 dstauthor = line.split('=')[1].strip()
178 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
178 if srcauthor in self.authors and dstauthor != self.authors[srcauthor]:
179 self.ui.status(
179 self.ui.status(
180 'Overriding mapping for author %s, was %s, will be %s\n'
180 'Overriding mapping for author %s, was %s, will be %s\n'
181 % (srcauthor, self.authors[srcauthor], dstauthor))
181 % (srcauthor, self.authors[srcauthor], dstauthor))
182 else:
182 else:
183 self.ui.debug('Mapping author %s to %s\n'
183 self.ui.debug('Mapping author %s to %s\n'
184 % (srcauthor, dstauthor))
184 % (srcauthor, dstauthor))
185 self.authors[srcauthor] = dstauthor
185 self.authors[srcauthor] = dstauthor
186 except IndexError:
186 except IndexError:
187 self.ui.warn(
187 self.ui.warn(
188 'Ignoring bad line in author file map %s: %s\n'
188 'Ignoring bad line in author file map %s: %s\n'
189 % (authorfile, line))
189 % (authorfile, line))
190 afile.close()
190 afile.close()
191
191
192 def copy(self, rev):
192 def copy(self, rev):
193 c = self.commitcache[rev]
193 c = self.commitcache[rev]
194 files = self.source.getchanges(rev)
194 files = self.source.getchanges(rev)
195
195
196 do_copies = (hasattr(c, 'copies') and hasattr(self.dest, 'copyfile'))
196 do_copies = (hasattr(c, 'copies') and hasattr(self.dest, 'copyfile'))
197
197
198 for f, v in files:
198 for f, v in files:
199 try:
199 try:
200 data = self.source.getfile(f, v)
200 data = self.source.getfile(f, v)
201 except IOError, inst:
201 except IOError, inst:
202 self.dest.delfile(f)
202 self.dest.delfile(f)
203 else:
203 else:
204 e = self.source.getmode(f, v)
204 e = self.source.getmode(f, v)
205 self.dest.putfile(f, e, data)
205 self.dest.putfile(f, e, data)
206 if do_copies:
206 if do_copies:
207 if f in c.copies:
207 if f in c.copies:
208 # Merely marks that a copy happened.
208 # Merely marks that a copy happened.
209 self.dest.copyfile(c.copies[f], f)
209 self.dest.copyfile(c.copies[f], f)
210
210
211
211
212 r = [self.map[v] for v in c.parents]
212 r = [self.map[v] for v in c.parents]
213 f = [f for f, v in files]
213 f = [f for f, v in files]
214 newnode = self.dest.putcommit(f, r, c)
214 newnode = self.dest.putcommit(f, r, c)
215 self.mapentry(rev, newnode)
215 self.mapentry(rev, newnode)
216
216
217 def convert(self):
217 def convert(self):
218 try:
218 try:
219 self.source.setrevmap(self.map)
219 self.source.setrevmap(self.map)
220 self.ui.status("scanning source...\n")
220 self.ui.status("scanning source...\n")
221 heads = self.source.getheads()
221 heads = self.source.getheads()
222 parents = self.walktree(heads)
222 parents = self.walktree(heads)
223 self.ui.status("sorting...\n")
223 self.ui.status("sorting...\n")
224 t = self.toposort(parents)
224 t = self.toposort(parents)
225 num = len(t)
225 num = len(t)
226 c = None
226 c = None
227
227
228 self.ui.status("converting...\n")
228 self.ui.status("converting...\n")
229 for c in t:
229 for c in t:
230 num -= 1
230 num -= 1
231 desc = self.commitcache[c].desc
231 desc = self.commitcache[c].desc
232 if "\n" in desc:
232 if "\n" in desc:
233 desc = desc.splitlines()[0]
233 desc = desc.splitlines()[0]
234 author = self.commitcache[c].author
234 author = self.commitcache[c].author
235 author = self.authors.get(author, author)
235 author = self.authors.get(author, author)
236 self.commitcache[c].author = author
236 self.commitcache[c].author = author
237 self.ui.status("%d %s\n" % (num, desc))
237 self.ui.status("%d %s\n" % (num, desc))
238 self.copy(c)
238 self.copy(c)
239
239
240 tags = self.source.gettags()
240 tags = self.source.gettags()
241 ctags = {}
241 ctags = {}
242 for k in tags:
242 for k in tags:
243 v = tags[k]
243 v = tags[k]
244 if v in self.map:
244 if v in self.map:
245 ctags[k] = self.map[v]
245 ctags[k] = self.map[v]
246
246
247 if c and ctags:
247 if c and ctags:
248 nrev = self.dest.puttags(ctags)
248 nrev = self.dest.puttags(ctags)
249 # write another hash correspondence to override the previous
249 # write another hash correspondence to override the previous
250 # one so we don't end up with extra tag heads
250 # one so we don't end up with extra tag heads
251 if nrev:
251 if nrev:
252 self.mapentry(c, nrev)
252 self.mapentry(c, nrev)
253
253
254 self.writeauthormap()
254 self.writeauthormap()
255 finally:
255 finally:
256 self.cleanup()
256 self.cleanup()
257
257
258 def cleanup(self):
258 def cleanup(self):
259 if self.mapfilefd:
259 if self.mapfilefd:
260 self.mapfilefd.close()
260 self.mapfilefd.close()
261
261
262 def _convert(ui, src, dest=None, mapfile=None, **opts):
262 def _convert(ui, src, dest=None, mapfile=None, **opts):
263 '''Convert a foreign SCM repository to a Mercurial one.
263 '''Convert a foreign SCM repository to a Mercurial one.
264
264
265 Accepted source formats:
265 Accepted source formats:
266 - GIT
266 - GIT
267 - CVS
267 - CVS
268 - SVN
268 - SVN
269
269
270 Accepted destination formats:
270 Accepted destination formats:
271 - Mercurial
271 - Mercurial
272
272
273 If no revision is given, all revisions will be converted. Otherwise,
273 If no revision is given, all revisions will be converted. Otherwise,
274 convert will only import up to the named revision (given in a format
274 convert will only import up to the named revision (given in a format
275 understood by the source).
275 understood by the source).
276
276
277 If no destination directory name is specified, it defaults to the
277 If no destination directory name is specified, it defaults to the
278 basename of the source with \'-hg\' appended. If the destination
278 basename of the source with \'-hg\' appended. If the destination
279 repository doesn\'t exist, it will be created.
279 repository doesn\'t exist, it will be created.
280
280
281 If <mapfile> isn\'t given, it will be put in a default location
281 If <mapfile> isn\'t given, it will be put in a default location
282 (<dest>/.hg/shamap by default). The <mapfile> is a simple text
282 (<dest>/.hg/shamap by default). The <mapfile> is a simple text
283 file that maps each source commit ID to the destination ID for
283 file that maps each source commit ID to the destination ID for
284 that revision, like so:
284 that revision, like so:
285 <source ID> <destination ID>
285 <source ID> <destination ID>
286
286
287 If the file doesn\'t exist, it\'s automatically created. It\'s updated
287 If the file doesn\'t exist, it\'s automatically created. It\'s updated
288 on each commit copied, so convert-repo can be interrupted and can
288 on each commit copied, so convert-repo can be interrupted and can
289 be run repeatedly to copy new commits.
289 be run repeatedly to copy new commits.
290
290
291 The [username mapping] file is a simple text file that maps each source
291 The [username mapping] file is a simple text file that maps each source
292 commit author to a destination commit author. It is handy for source SCMs
292 commit author to a destination commit author. It is handy for source SCMs
293 that use unix logins to identify authors (eg: CVS). One line per author
293 that use unix logins to identify authors (eg: CVS). One line per author
294 mapping and the line format is:
294 mapping and the line format is:
295 srcauthor=whatever string you want
295 srcauthor=whatever string you want
296 '''
296 '''
297
297
298 util._encoding = 'UTF-8'
298 util._encoding = 'UTF-8'
299
299
300 if not dest:
300 if not dest:
301 dest = hg.defaultdest(src) + "-hg"
301 dest = hg.defaultdest(src) + "-hg"
302 ui.status("assuming destination %s\n" % dest)
302 ui.status("assuming destination %s\n" % dest)
303
303
304 # Try to be smart and initalize things when required
304 # Try to be smart and initalize things when required
305 created = False
305 created = False
306 if os.path.isdir(dest):
306 if os.path.isdir(dest):
307 if len(os.listdir(dest)) > 0:
307 if len(os.listdir(dest)) > 0:
308 try:
308 try:
309 hg.repository(ui, dest)
309 hg.repository(ui, dest)
310 ui.status("destination %s is a Mercurial repository\n" % dest)
310 ui.status("destination %s is a Mercurial repository\n" % dest)
311 except hg.RepoError:
311 except hg.RepoError:
312 raise util.Abort(
312 raise util.Abort(
313 "destination directory %s is not empty.\n"
313 "destination directory %s is not empty.\n"
314 "Please specify an empty directory to be initialized\n"
314 "Please specify an empty directory to be initialized\n"
315 "or an already initialized mercurial repository"
315 "or an already initialized mercurial repository"
316 % dest)
316 % dest)
317 else:
317 else:
318 ui.status("initializing destination %s repository\n" % dest)
318 ui.status("initializing destination %s repository\n" % dest)
319 hg.repository(ui, dest, create=True)
319 hg.repository(ui, dest, create=True)
320 created = True
320 created = True
321 elif os.path.exists(dest):
321 elif os.path.exists(dest):
322 raise util.Abort("destination %s exists and is not a directory" % dest)
322 raise util.Abort("destination %s exists and is not a directory" % dest)
323 else:
323 else:
324 ui.status("initializing destination %s repository\n" % dest)
324 ui.status("initializing destination %s repository\n" % dest)
325 hg.repository(ui, dest, create=True)
325 hg.repository(ui, dest, create=True)
326 created = True
326 created = True
327
327
328 destc = convertsink(ui, dest)
328 destc = convertsink(ui, dest)
329
329
330 try:
330 try:
331 srcc = convertsource(ui, src, rev=opts.get('rev'))
331 srcc = convertsource(ui, src, rev=opts.get('rev'))
332 except Exception:
332 except Exception:
333 if created:
333 if created:
334 shutil.rmtree(dest, True)
334 shutil.rmtree(dest, True)
335 raise
335 raise
336
336
337 if not mapfile:
337 if not mapfile:
338 try:
338 try:
339 mapfile = destc.mapfile()
339 mapfile = destc.mapfile()
340 except:
340 except:
341 mapfile = os.path.join(destc, "map")
341 mapfile = os.path.join(destc, "map")
342
342
343 c = convert(ui, srcc, destc, mapfile, opts)
343 c = convert(ui, srcc, destc, mapfile, opts)
344 c.convert()
344 c.convert()
345
345
346 cmdtable = {
346 cmdtable = {
347 "convert":
347 "convert":
348 (_convert,
348 (_convert,
349 [('A', 'authors', '', 'username mapping filename'),
349 [('A', 'authors', '', 'username mapping filename'),
350 ('r', 'rev', '', 'import up to target revision REV'),
350 ('r', 'rev', '', 'import up to target revision REV'),
351 ('', 'datesort', None, 'try to sort changesets by date')],
351 ('', 'datesort', None, 'try to sort changesets by date')],
352 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
352 'hg convert [OPTION]... SOURCE [DEST [MAPFILE]]'),
353 }
353 }
@@ -1,121 +1,121 b''
1 # common code for the convert extension
1 # common code for the convert extension
2
2
3 class NoRepo(Exception): pass
3 class NoRepo(Exception): pass
4
4
5 class commit(object):
5 class commit(object):
6 def __init__(self, **parts):
6 def __init__(self, **parts):
7 self.rev = None
7 self.rev = None
8 self.branch = None
8 self.branch = None
9
9
10 for x in "author date desc parents".split():
10 for x in "author date desc parents".split():
11 if not x in parts:
11 if not x in parts:
12 raise util.Abort("commit missing field %s" % x)
12 raise util.Abort("commit missing field %s" % x)
13 self.__dict__.update(parts)
13 self.__dict__.update(parts)
14 if not self.desc or self.desc.isspace():
14 if not self.desc or self.desc.isspace():
15 self.desc = '*** empty log message ***'
15 self.desc = '*** empty log message ***'
16
16
17 class converter_source(object):
17 class converter_source(object):
18 """Conversion source interface"""
18 """Conversion source interface"""
19
19
20 def __init__(self, ui, path, rev=None):
20 def __init__(self, ui, path, rev=None):
21 """Initialize conversion source (or raise NoRepo("message")
21 """Initialize conversion source (or raise NoRepo("message")
22 exception if path is not a valid repository)"""
22 exception if path is not a valid repository)"""
23 self.ui = ui
23 self.ui = ui
24 self.path = path
24 self.path = path
25 self.rev = rev
25 self.rev = rev
26
26
27 self.encoding = 'utf-8'
27 self.encoding = 'utf-8'
28
28
29 def setrevmap(self, revmap):
29 def setrevmap(self, revmap):
30 """set the map of already-converted revisions"""
30 """set the map of already-converted revisions"""
31 pass
31 pass
32
32
33 def getheads(self):
33 def getheads(self):
34 """Return a list of this repository's heads"""
34 """Return a list of this repository's heads"""
35 raise NotImplementedError()
35 raise NotImplementedError()
36
36
37 def getfile(self, name, rev):
37 def getfile(self, name, rev):
38 """Return file contents as a string"""
38 """Return file contents as a string"""
39 raise NotImplementedError()
39 raise NotImplementedError()
40
40
41 def getmode(self, name, rev):
41 def getmode(self, name, rev):
42 """Return file mode, eg. '', 'x', or 'l'"""
42 """Return file mode, eg. '', 'x', or 'l'"""
43 raise NotImplementedError()
43 raise NotImplementedError()
44
44
45 def getchanges(self, version):
45 def getchanges(self, version):
46 """Return sorted list of (filename, id) tuples for all files changed in rev.
46 """Return sorted list of (filename, id) tuples for all files changed in rev.
47
47
48 id just tells us which revision to return in getfile(), e.g. in
48 id just tells us which revision to return in getfile(), e.g. in
49 git it's an object hash."""
49 git it's an object hash."""
50 raise NotImplementedError()
50 raise NotImplementedError()
51
51
52 def getcommit(self, version):
52 def getcommit(self, version):
53 """Return the commit object for version"""
53 """Return the commit object for version"""
54 raise NotImplementedError()
54 raise NotImplementedError()
55
55
56 def gettags(self):
56 def gettags(self):
57 """Return the tags as a dictionary of name: revision"""
57 """Return the tags as a dictionary of name: revision"""
58 raise NotImplementedError()
58 raise NotImplementedError()
59
59
60 def recode(self, s, encoding=None):
60 def recode(self, s, encoding=None):
61 if not encoding:
61 if not encoding:
62 encoding = self.encoding or 'utf-8'
62 encoding = self.encoding or 'utf-8'
63
63
64 try:
64 try:
65 return s.decode(encoding).encode("utf-8")
65 return s.decode(encoding).encode("utf-8")
66 except:
66 except:
67 try:
67 try:
68 return s.decode("latin-1").encode("utf-8")
68 return s.decode("latin-1").encode("utf-8")
69 except:
69 except:
70 return s.decode(encoding, "replace").encode("utf-8")
70 return s.decode(encoding, "replace").encode("utf-8")
71
71
72 class converter_sink(object):
72 class converter_sink(object):
73 """Conversion sink (target) interface"""
73 """Conversion sink (target) interface"""
74
74
75 def __init__(self, ui, path):
75 def __init__(self, ui, path):
76 """Initialize conversion sink (or raise NoRepo("message")
76 """Initialize conversion sink (or raise NoRepo("message")
77 exception if path is not a valid repository)"""
77 exception if path is not a valid repository)"""
78 raise NotImplementedError()
78 raise NotImplementedError()
79
79
80 def getheads(self):
80 def getheads(self):
81 """Return a list of this repository's heads"""
81 """Return a list of this repository's heads"""
82 raise NotImplementedError()
82 raise NotImplementedError()
83
83
84 def mapfile(self):
84 def mapfile(self):
85 """Path to a file that will contain lines
85 """Path to a file that will contain lines
86 source_rev_id sink_rev_id
86 source_rev_id sink_rev_id
87 mapping equivalent revision identifiers for each system."""
87 mapping equivalent revision identifiers for each system."""
88 raise NotImplementedError()
88 raise NotImplementedError()
89
89
90 def authorfile(self):
90 def authorfile(self):
91 """Path to a file that will contain lines
91 """Path to a file that will contain lines
92 srcauthor=dstauthor
92 srcauthor=dstauthor
93 mapping equivalent authors identifiers for each system."""
93 mapping equivalent authors identifiers for each system."""
94 return None
94 return None
95
95
96 def putfile(self, f, e, data):
96 def putfile(self, f, e, data):
97 """Put file for next putcommit().
97 """Put file for next putcommit().
98 f: path to file
98 f: path to file
99 e: '', 'x', or 'l' (regular file, executable, or symlink)
99 e: '', 'x', or 'l' (regular file, executable, or symlink)
100 data: file contents"""
100 data: file contents"""
101 raise NotImplementedError()
101 raise NotImplementedError()
102
102
103 def delfile(self, f):
103 def delfile(self, f):
104 """Delete file for next putcommit().
104 """Delete file for next putcommit().
105 f: path to file"""
105 f: path to file"""
106 raise NotImplementedError()
106 raise NotImplementedError()
107
107
108 def putcommit(self, files, parents, commit):
108 def putcommit(self, files, parents, commit):
109 """Create a revision with all changed files listed in 'files'
109 """Create a revision with all changed files listed in 'files'
110 and having listed parents. 'commit' is a commit object containing
110 and having listed parents. 'commit' is a commit object containing
111 at a minimum the author, date, and message for this changeset.
111 at a minimum the author, date, and message for this changeset.
112 Called after putfile() and delfile() calls. Note that the sink
112 Called after putfile() and delfile() calls. Note that the sink
113 repository is not told to update itself to a particular revision
113 repository is not told to update itself to a particular revision
114 (or even what that revision would be) before it receives the
114 (or even what that revision would be) before it receives the
115 file data."""
115 file data."""
116 raise NotImplementedError()
116 raise NotImplementedError()
117
117
118 def puttags(self, tags):
118 def puttags(self, tags):
119 """Put tags into sink.
119 """Put tags into sink.
120 tags: {tagname: sink_rev_id, ...}"""
120 tags: {tagname: sink_rev_id, ...}"""
121 raise NotImplementedError()
121 raise NotImplementedError()
@@ -1,97 +1,97 b''
1 # hg backend for convert extension
1 # hg backend for convert extension
2
2
3 import os, time
3 import os, time
4 from mercurial import hg
4 from mercurial import hg
5
5
6 from common import NoRepo, converter_sink
6 from common import NoRepo, converter_sink
7
7
8 class convert_mercurial(converter_sink):
8 class convert_mercurial(converter_sink):
9 def __init__(self, ui, path):
9 def __init__(self, ui, path):
10 self.path = path
10 self.path = path
11 self.ui = ui
11 self.ui = ui
12 try:
12 try:
13 self.repo = hg.repository(self.ui, path)
13 self.repo = hg.repository(self.ui, path)
14 except:
14 except:
15 raise NoRepo("could open hg repo %s" % path)
15 raise NoRepo("could open hg repo %s" % path)
16
16
17 def mapfile(self):
17 def mapfile(self):
18 return os.path.join(self.path, ".hg", "shamap")
18 return os.path.join(self.path, ".hg", "shamap")
19
19
20 def authorfile(self):
20 def authorfile(self):
21 return os.path.join(self.path, ".hg", "authormap")
21 return os.path.join(self.path, ".hg", "authormap")
22
22
23 def getheads(self):
23 def getheads(self):
24 h = self.repo.changelog.heads()
24 h = self.repo.changelog.heads()
25 return [ hg.hex(x) for x in h ]
25 return [ hg.hex(x) for x in h ]
26
26
27 def putfile(self, f, e, data):
27 def putfile(self, f, e, data):
28 self.repo.wwrite(f, data, e)
28 self.repo.wwrite(f, data, e)
29 if self.repo.dirstate.state(f) == '?':
29 if self.repo.dirstate.state(f) == '?':
30 self.repo.dirstate.update([f], "a")
30 self.repo.dirstate.update([f], "a")
31
31
32 def copyfile(self, source, dest):
32 def copyfile(self, source, dest):
33 self.repo.copy(source, dest)
33 self.repo.copy(source, dest)
34
34
35 def delfile(self, f):
35 def delfile(self, f):
36 try:
36 try:
37 os.unlink(self.repo.wjoin(f))
37 os.unlink(self.repo.wjoin(f))
38 #self.repo.remove([f])
38 #self.repo.remove([f])
39 except:
39 except:
40 pass
40 pass
41
41
42 def putcommit(self, files, parents, commit):
42 def putcommit(self, files, parents, commit):
43 seen = {}
43 seen = {}
44 pl = []
44 pl = []
45 for p in parents:
45 for p in parents:
46 if p not in seen:
46 if p not in seen:
47 pl.append(p)
47 pl.append(p)
48 seen[p] = 1
48 seen[p] = 1
49 parents = pl
49 parents = pl
50
50
51 if len(parents) < 2: parents.append("0" * 40)
51 if len(parents) < 2: parents.append("0" * 40)
52 if len(parents) < 2: parents.append("0" * 40)
52 if len(parents) < 2: parents.append("0" * 40)
53 p2 = parents.pop(0)
53 p2 = parents.pop(0)
54
54
55 text = commit.desc
55 text = commit.desc
56 extra = {}
56 extra = {}
57 if commit.branch:
57 if commit.branch:
58 extra['branch'] = commit.branch
58 extra['branch'] = commit.branch
59 if commit.rev:
59 if commit.rev:
60 extra['convert_revision'] = commit.rev
60 extra['convert_revision'] = commit.rev
61
61
62 while parents:
62 while parents:
63 p1 = p2
63 p1 = p2
64 p2 = parents.pop(0)
64 p2 = parents.pop(0)
65 a = self.repo.rawcommit(files, text, commit.author, commit.date,
65 a = self.repo.rawcommit(files, text, commit.author, commit.date,
66 hg.bin(p1), hg.bin(p2), extra=extra)
66 hg.bin(p1), hg.bin(p2), extra=extra)
67 text = "(octopus merge fixup)\n"
67 text = "(octopus merge fixup)\n"
68 p2 = hg.hex(self.repo.changelog.tip())
68 p2 = hg.hex(self.repo.changelog.tip())
69
69
70 return p2
70 return p2
71
71
72 def puttags(self, tags):
72 def puttags(self, tags):
73 try:
73 try:
74 old = self.repo.wfile(".hgtags").read()
74 old = self.repo.wfile(".hgtags").read()
75 oldlines = old.splitlines(1)
75 oldlines = old.splitlines(1)
76 oldlines.sort()
76 oldlines.sort()
77 except:
77 except:
78 oldlines = []
78 oldlines = []
79
79
80 k = tags.keys()
80 k = tags.keys()
81 k.sort()
81 k.sort()
82 newlines = []
82 newlines = []
83 for tag in k:
83 for tag in k:
84 newlines.append("%s %s\n" % (tags[tag], tag))
84 newlines.append("%s %s\n" % (tags[tag], tag))
85
85
86 newlines.sort()
86 newlines.sort()
87
87
88 if newlines != oldlines:
88 if newlines != oldlines:
89 self.ui.status("updating tags\n")
89 self.ui.status("updating tags\n")
90 f = self.repo.wfile(".hgtags", "w")
90 f = self.repo.wfile(".hgtags", "w")
91 f.write("".join(newlines))
91 f.write("".join(newlines))
92 f.close()
92 f.close()
93 if not oldlines: self.repo.add([".hgtags"])
93 if not oldlines: self.repo.add([".hgtags"])
94 date = "%s 0" % int(time.mktime(time.gmtime()))
94 date = "%s 0" % int(time.mktime(time.gmtime()))
95 self.repo.rawcommit([".hgtags"], "update tags", "convert-repo",
95 self.repo.rawcommit([".hgtags"], "update tags", "convert-repo",
96 date, self.repo.changelog.tip(), hg.nullid)
96 date, self.repo.changelog.tip(), hg.nullid)
97 return hg.hex(self.repo.changelog.tip())
97 return hg.hex(self.repo.changelog.tip())
@@ -1,669 +1,669 b''
1 # Subversion 1.4/1.5 Python API backend
1 # Subversion 1.4/1.5 Python API backend
2 #
2 #
3 # Copyright(C) 2007 Daniel Holth et al
3 # Copyright(C) 2007 Daniel Holth et al
4 #
4 #
5 # Configuration options:
5 # Configuration options:
6 #
6 #
7 # convert.svn.trunk
7 # convert.svn.trunk
8 # Relative path to the trunk (default: "trunk")
8 # Relative path to the trunk (default: "trunk")
9 # convert.svn.branches
9 # convert.svn.branches
10 # Relative path to tree of branches (default: "branches")
10 # Relative path to tree of branches (default: "branches")
11 #
11 #
12 # Set these in a hgrc, or on the command line as follows:
12 # Set these in a hgrc, or on the command line as follows:
13 #
13 #
14 # hg convert --config convert.svn.trunk=wackoname [...]
14 # hg convert --config convert.svn.trunk=wackoname [...]
15
15
16 import pprint
16 import pprint
17 import locale
17 import locale
18 import os
18 import os
19 import cPickle as pickle
19 import cPickle as pickle
20 from mercurial import util
20 from mercurial import util
21
21
22 # Subversion stuff. Works best with very recent Python SVN bindings
22 # Subversion stuff. Works best with very recent Python SVN bindings
23 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
23 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
24 # these bindings.
24 # these bindings.
25
25
26 from cStringIO import StringIO
26 from cStringIO import StringIO
27
27
28 from common import NoRepo, commit, converter_source
28 from common import NoRepo, commit, converter_source
29
29
30 try:
30 try:
31 from svn.core import SubversionException, Pool
31 from svn.core import SubversionException, Pool
32 import svn.core
32 import svn.core
33 import svn.ra
33 import svn.ra
34 import svn.delta
34 import svn.delta
35 import svn
35 import svn
36 import transport
36 import transport
37 except ImportError:
37 except ImportError:
38 pass
38 pass
39
39
40 class CompatibilityException(Exception): pass
40 class CompatibilityException(Exception): pass
41
41
42 class changedpath(object):
42 class changedpath(object):
43 def __init__(self, p):
43 def __init__(self, p):
44 self.copyfrom_path = p.copyfrom_path
44 self.copyfrom_path = p.copyfrom_path
45 self.copyfrom_rev = p.copyfrom_rev
45 self.copyfrom_rev = p.copyfrom_rev
46 self.action = p.action
46 self.action = p.action
47
47
48 # SVN conversion code stolen from bzr-svn and tailor
48 # SVN conversion code stolen from bzr-svn and tailor
49 class convert_svn(converter_source):
49 class convert_svn(converter_source):
50 def __init__(self, ui, url, rev=None):
50 def __init__(self, ui, url, rev=None):
51 super(convert_svn, self).__init__(ui, url, rev=rev)
51 super(convert_svn, self).__init__(ui, url, rev=rev)
52
52
53 try:
53 try:
54 SubversionException
54 SubversionException
55 except NameError:
55 except NameError:
56 msg = 'subversion python bindings could not be loaded\n'
56 msg = 'subversion python bindings could not be loaded\n'
57 ui.warn(msg)
57 ui.warn(msg)
58 raise NoRepo(msg)
58 raise NoRepo(msg)
59
59
60 self.encoding = locale.getpreferredencoding()
60 self.encoding = locale.getpreferredencoding()
61 self.lastrevs = {}
61 self.lastrevs = {}
62
62
63 latest = None
63 latest = None
64 if rev:
64 if rev:
65 try:
65 try:
66 latest = int(rev)
66 latest = int(rev)
67 except ValueError:
67 except ValueError:
68 raise util.Abort('svn: revision %s is not an integer' % rev)
68 raise util.Abort('svn: revision %s is not an integer' % rev)
69 try:
69 try:
70 # Support file://path@rev syntax. Useful e.g. to convert
70 # Support file://path@rev syntax. Useful e.g. to convert
71 # deleted branches.
71 # deleted branches.
72 at = url.rfind('@')
72 at = url.rfind('@')
73 if at >= 0:
73 if at >= 0:
74 latest = int(url[at+1:])
74 latest = int(url[at+1:])
75 url = url[:at]
75 url = url[:at]
76 except ValueError, e:
76 except ValueError, e:
77 pass
77 pass
78 self.url = url
78 self.url = url
79 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
79 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
80 try:
80 try:
81 self.transport = transport.SvnRaTransport(url=url)
81 self.transport = transport.SvnRaTransport(url=url)
82 self.ra = self.transport.ra
82 self.ra = self.transport.ra
83 self.ctx = self.transport.client
83 self.ctx = self.transport.client
84 self.base = svn.ra.get_repos_root(self.ra)
84 self.base = svn.ra.get_repos_root(self.ra)
85 self.module = self.url[len(self.base):]
85 self.module = self.url[len(self.base):]
86 self.modulemap = {} # revision, module
86 self.modulemap = {} # revision, module
87 self.commits = {}
87 self.commits = {}
88 self.files = {}
88 self.files = {}
89 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
89 self.uuid = svn.ra.get_uuid(self.ra).decode(self.encoding)
90 except SubversionException, e:
90 except SubversionException, e:
91 raise NoRepo("couldn't open SVN repo %s" % url)
91 raise NoRepo("couldn't open SVN repo %s" % url)
92
92
93 try:
93 try:
94 self.get_blacklist()
94 self.get_blacklist()
95 except IOError, e:
95 except IOError, e:
96 pass
96 pass
97
97
98 self.last_changed = self.latest(self.module, latest)
98 self.last_changed = self.latest(self.module, latest)
99
99
100 self.head = self.revid(self.last_changed)
100 self.head = self.revid(self.last_changed)
101
101
102 def setrevmap(self, revmap):
102 def setrevmap(self, revmap):
103 lastrevs = {}
103 lastrevs = {}
104 for revid in revmap.keys():
104 for revid in revmap.keys():
105 uuid, module, revnum = self.revsplit(revid)
105 uuid, module, revnum = self.revsplit(revid)
106 lastrevnum = lastrevs.setdefault(module, revnum)
106 lastrevnum = lastrevs.setdefault(module, revnum)
107 if revnum > lastrevnum:
107 if revnum > lastrevnum:
108 lastrevs[module] = revnum
108 lastrevs[module] = revnum
109 self.lastrevs = lastrevs
109 self.lastrevs = lastrevs
110
110
111 def exists(self, path, optrev):
111 def exists(self, path, optrev):
112 try:
112 try:
113 return svn.client.ls(self.url.rstrip('/') + '/' + path,
113 return svn.client.ls(self.url.rstrip('/') + '/' + path,
114 optrev, False, self.ctx)
114 optrev, False, self.ctx)
115 except SubversionException, err:
115 except SubversionException, err:
116 return []
116 return []
117
117
118 def getheads(self):
118 def getheads(self):
119 # detect standard /branches, /tags, /trunk layout
119 # detect standard /branches, /tags, /trunk layout
120 optrev = svn.core.svn_opt_revision_t()
120 optrev = svn.core.svn_opt_revision_t()
121 optrev.kind = svn.core.svn_opt_revision_number
121 optrev.kind = svn.core.svn_opt_revision_number
122 optrev.value.number = self.last_changed
122 optrev.value.number = self.last_changed
123 rpath = self.url.strip('/')
123 rpath = self.url.strip('/')
124 cfgtrunk = self.ui.config('convert', 'svn.trunk')
124 cfgtrunk = self.ui.config('convert', 'svn.trunk')
125 cfgbranches = self.ui.config('convert', 'svn.branches')
125 cfgbranches = self.ui.config('convert', 'svn.branches')
126 trunk = (cfgtrunk or 'trunk').strip('/')
126 trunk = (cfgtrunk or 'trunk').strip('/')
127 branches = (cfgbranches or 'branches').strip('/')
127 branches = (cfgbranches or 'branches').strip('/')
128 if self.exists(trunk, optrev) and self.exists(branches, optrev):
128 if self.exists(trunk, optrev) and self.exists(branches, optrev):
129 self.ui.note('found trunk at %r and branches at %r\n' %
129 self.ui.note('found trunk at %r and branches at %r\n' %
130 (trunk, branches))
130 (trunk, branches))
131 oldmodule = self.module
131 oldmodule = self.module
132 self.module += '/' + trunk
132 self.module += '/' + trunk
133 lt = self.latest(self.module, self.last_changed)
133 lt = self.latest(self.module, self.last_changed)
134 self.head = self.revid(lt)
134 self.head = self.revid(lt)
135 self.heads = [self.head]
135 self.heads = [self.head]
136 branchnames = svn.client.ls(rpath + '/' + branches, optrev, False,
136 branchnames = svn.client.ls(rpath + '/' + branches, optrev, False,
137 self.ctx)
137 self.ctx)
138 for branch in branchnames.keys():
138 for branch in branchnames.keys():
139 if oldmodule:
139 if oldmodule:
140 module = '/' + oldmodule + '/' + branches + '/' + branch
140 module = '/' + oldmodule + '/' + branches + '/' + branch
141 else:
141 else:
142 module = '/' + branches + '/' + branch
142 module = '/' + branches + '/' + branch
143 brevnum = self.latest(module, self.last_changed)
143 brevnum = self.latest(module, self.last_changed)
144 brev = self.revid(brevnum, module)
144 brev = self.revid(brevnum, module)
145 self.ui.note('found branch %s at %d\n' % (branch, brevnum))
145 self.ui.note('found branch %s at %d\n' % (branch, brevnum))
146 self.heads.append(brev)
146 self.heads.append(brev)
147 elif cfgtrunk or cfgbranches:
147 elif cfgtrunk or cfgbranches:
148 raise util.Abort(_('trunk/branch layout expected, '
148 raise util.Abort(_('trunk/branch layout expected, '
149 'but not found'))
149 'but not found'))
150 else:
150 else:
151 self.ui.note('working with one branch\n')
151 self.ui.note('working with one branch\n')
152 self.heads = [self.head]
152 self.heads = [self.head]
153 return self.heads
153 return self.heads
154
154
155 def getfile(self, file, rev):
155 def getfile(self, file, rev):
156 data, mode = self._getfile(file, rev)
156 data, mode = self._getfile(file, rev)
157 self.modecache[(file, rev)] = mode
157 self.modecache[(file, rev)] = mode
158 return data
158 return data
159
159
160 def getmode(self, file, rev):
160 def getmode(self, file, rev):
161 return self.modecache[(file, rev)]
161 return self.modecache[(file, rev)]
162
162
163 def getchanges(self, rev):
163 def getchanges(self, rev):
164 self.modecache = {}
164 self.modecache = {}
165 files = self.files[rev]
165 files = self.files[rev]
166 cl = files
166 cl = files
167 cl.sort()
167 cl.sort()
168 # caller caches the result, so free it here to release memory
168 # caller caches the result, so free it here to release memory
169 del self.files[rev]
169 del self.files[rev]
170 return cl
170 return cl
171
171
172 def getcommit(self, rev):
172 def getcommit(self, rev):
173 if rev not in self.commits:
173 if rev not in self.commits:
174 uuid, module, revnum = self.revsplit(rev)
174 uuid, module, revnum = self.revsplit(rev)
175 self.module = module
175 self.module = module
176 self.reparent(module)
176 self.reparent(module)
177 stop = self.lastrevs.get(module, 0)
177 stop = self.lastrevs.get(module, 0)
178 self._fetch_revisions(from_revnum=revnum, to_revnum=stop)
178 self._fetch_revisions(from_revnum=revnum, to_revnum=stop)
179 commit = self.commits[rev]
179 commit = self.commits[rev]
180 # caller caches the result, so free it here to release memory
180 # caller caches the result, so free it here to release memory
181 del self.commits[rev]
181 del self.commits[rev]
182 return commit
182 return commit
183
183
184 def get_log(self, paths, start, end, limit=0, discover_changed_paths=True,
184 def get_log(self, paths, start, end, limit=0, discover_changed_paths=True,
185 strict_node_history=False):
185 strict_node_history=False):
186 '''wrapper for svn.ra.get_log.
186 '''wrapper for svn.ra.get_log.
187 on a large repository, svn.ra.get_log pins huge amounts of
187 on a large repository, svn.ra.get_log pins huge amounts of
188 memory that cannot be recovered. work around it by forking
188 memory that cannot be recovered. work around it by forking
189 and writing results over a pipe.'''
189 and writing results over a pipe.'''
190
190
191 def child(fp):
191 def child(fp):
192 protocol = -1
192 protocol = -1
193 def receiver(orig_paths, revnum, author, date, message, pool):
193 def receiver(orig_paths, revnum, author, date, message, pool):
194 if orig_paths is not None:
194 if orig_paths is not None:
195 for k, v in orig_paths.iteritems():
195 for k, v in orig_paths.iteritems():
196 orig_paths[k] = changedpath(v)
196 orig_paths[k] = changedpath(v)
197 pickle.dump((orig_paths, revnum, author, date, message),
197 pickle.dump((orig_paths, revnum, author, date, message),
198 fp, protocol)
198 fp, protocol)
199
199
200 try:
200 try:
201 # Use an ra of our own so that our parent can consume
201 # Use an ra of our own so that our parent can consume
202 # our results without confusing the server.
202 # our results without confusing the server.
203 t = transport.SvnRaTransport(url=self.url)
203 t = transport.SvnRaTransport(url=self.url)
204 svn.ra.get_log(t.ra, paths, start, end, limit,
204 svn.ra.get_log(t.ra, paths, start, end, limit,
205 discover_changed_paths,
205 discover_changed_paths,
206 strict_node_history,
206 strict_node_history,
207 receiver)
207 receiver)
208 except SubversionException, (_, num):
208 except SubversionException, (_, num):
209 self.ui.print_exc()
209 self.ui.print_exc()
210 pickle.dump(num, fp, protocol)
210 pickle.dump(num, fp, protocol)
211 else:
211 else:
212 pickle.dump(None, fp, protocol)
212 pickle.dump(None, fp, protocol)
213 fp.close()
213 fp.close()
214
214
215 def parent(fp):
215 def parent(fp):
216 while True:
216 while True:
217 entry = pickle.load(fp)
217 entry = pickle.load(fp)
218 try:
218 try:
219 orig_paths, revnum, author, date, message = entry
219 orig_paths, revnum, author, date, message = entry
220 except:
220 except:
221 if entry is None:
221 if entry is None:
222 break
222 break
223 raise SubversionException("child raised exception", entry)
223 raise SubversionException("child raised exception", entry)
224 yield entry
224 yield entry
225
225
226 rfd, wfd = os.pipe()
226 rfd, wfd = os.pipe()
227 pid = os.fork()
227 pid = os.fork()
228 if pid:
228 if pid:
229 os.close(wfd)
229 os.close(wfd)
230 for p in parent(os.fdopen(rfd, 'rb')):
230 for p in parent(os.fdopen(rfd, 'rb')):
231 yield p
231 yield p
232 ret = os.waitpid(pid, 0)[1]
232 ret = os.waitpid(pid, 0)[1]
233 if ret:
233 if ret:
234 raise util.Abort(_('get_log %s') % util.explain_exit(ret))
234 raise util.Abort(_('get_log %s') % util.explain_exit(ret))
235 else:
235 else:
236 os.close(rfd)
236 os.close(rfd)
237 child(os.fdopen(wfd, 'wb'))
237 child(os.fdopen(wfd, 'wb'))
238 os._exit(0)
238 os._exit(0)
239
239
240 def gettags(self):
240 def gettags(self):
241 tags = {}
241 tags = {}
242 start = self.revnum(self.head)
242 start = self.revnum(self.head)
243 try:
243 try:
244 for entry in self.get_log(['/tags'], 0, start):
244 for entry in self.get_log(['/tags'], 0, start):
245 orig_paths, revnum, author, date, message = entry
245 orig_paths, revnum, author, date, message = entry
246 for path in orig_paths:
246 for path in orig_paths:
247 if not path.startswith('/tags/'):
247 if not path.startswith('/tags/'):
248 continue
248 continue
249 ent = orig_paths[path]
249 ent = orig_paths[path]
250 source = ent.copyfrom_path
250 source = ent.copyfrom_path
251 rev = ent.copyfrom_rev
251 rev = ent.copyfrom_rev
252 tag = path.split('/', 2)[2]
252 tag = path.split('/', 2)[2]
253 tags[tag] = self.revid(rev, module=source)
253 tags[tag] = self.revid(rev, module=source)
254 except SubversionException, (_, num):
254 except SubversionException, (_, num):
255 self.ui.note('no tags found at revision %d\n' % start)
255 self.ui.note('no tags found at revision %d\n' % start)
256 return tags
256 return tags
257
257
258 # -- helper functions --
258 # -- helper functions --
259
259
260 def revid(self, revnum, module=None):
260 def revid(self, revnum, module=None):
261 if not module:
261 if not module:
262 module = self.module
262 module = self.module
263 return (u"svn:%s%s@%s" % (self.uuid, module, revnum)).decode(self.encoding)
263 return (u"svn:%s%s@%s" % (self.uuid, module, revnum)).decode(self.encoding)
264
264
265 def revnum(self, rev):
265 def revnum(self, rev):
266 return int(rev.split('@')[-1])
266 return int(rev.split('@')[-1])
267
267
268 def revsplit(self, rev):
268 def revsplit(self, rev):
269 url, revnum = rev.encode(self.encoding).split('@', 1)
269 url, revnum = rev.encode(self.encoding).split('@', 1)
270 revnum = int(revnum)
270 revnum = int(revnum)
271 parts = url.split('/', 1)
271 parts = url.split('/', 1)
272 uuid = parts.pop(0)[4:]
272 uuid = parts.pop(0)[4:]
273 mod = ''
273 mod = ''
274 if parts:
274 if parts:
275 mod = '/' + parts[0]
275 mod = '/' + parts[0]
276 return uuid, mod, revnum
276 return uuid, mod, revnum
277
277
278 def latest(self, path, stop=0):
278 def latest(self, path, stop=0):
279 'find the latest revision affecting path, up to stop'
279 'find the latest revision affecting path, up to stop'
280 if not stop:
280 if not stop:
281 stop = svn.ra.get_latest_revnum(self.ra)
281 stop = svn.ra.get_latest_revnum(self.ra)
282 try:
282 try:
283 self.reparent('')
283 self.reparent('')
284 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
284 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
285 self.reparent(self.module)
285 self.reparent(self.module)
286 except SubversionException:
286 except SubversionException:
287 dirent = None
287 dirent = None
288 if not dirent:
288 if not dirent:
289 print self.base, path
289 print self.base, path
290 raise util.Abort('%s not found up to revision %d' % (path, stop))
290 raise util.Abort('%s not found up to revision %d' % (path, stop))
291
291
292 return dirent.created_rev
292 return dirent.created_rev
293
293
294 def get_blacklist(self):
294 def get_blacklist(self):
295 """Avoid certain revision numbers.
295 """Avoid certain revision numbers.
296 It is not uncommon for two nearby revisions to cancel each other
296 It is not uncommon for two nearby revisions to cancel each other
297 out, e.g. 'I copied trunk into a subdirectory of itself instead
297 out, e.g. 'I copied trunk into a subdirectory of itself instead
298 of making a branch'. The converted repository is significantly
298 of making a branch'. The converted repository is significantly
299 smaller if we ignore such revisions."""
299 smaller if we ignore such revisions."""
300 self.blacklist = set()
300 self.blacklist = set()
301 blacklist = self.blacklist
301 blacklist = self.blacklist
302 for line in file("blacklist.txt", "r"):
302 for line in file("blacklist.txt", "r"):
303 if not line.startswith("#"):
303 if not line.startswith("#"):
304 try:
304 try:
305 svn_rev = int(line.strip())
305 svn_rev = int(line.strip())
306 blacklist.add(svn_rev)
306 blacklist.add(svn_rev)
307 except ValueError, e:
307 except ValueError, e:
308 pass # not an integer or a comment
308 pass # not an integer or a comment
309
309
310 def is_blacklisted(self, svn_rev):
310 def is_blacklisted(self, svn_rev):
311 return svn_rev in self.blacklist
311 return svn_rev in self.blacklist
312
312
313 def reparent(self, module):
313 def reparent(self, module):
314 svn_url = self.base + module
314 svn_url = self.base + module
315 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
315 self.ui.debug("reparent to %s\n" % svn_url.encode(self.encoding))
316 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
316 svn.ra.reparent(self.ra, svn_url.encode(self.encoding))
317
317
318 def _fetch_revisions(self, from_revnum = 0, to_revnum = 347):
318 def _fetch_revisions(self, from_revnum = 0, to_revnum = 347):
319 def get_entry_from_path(path, module=self.module):
319 def get_entry_from_path(path, module=self.module):
320 # Given the repository url of this wc, say
320 # Given the repository url of this wc, say
321 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
321 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
322 # extract the "entry" portion (a relative path) from what
322 # extract the "entry" portion (a relative path) from what
323 # svn log --xml says, ie
323 # svn log --xml says, ie
324 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
324 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
325 # that is to say "tests/PloneTestCase.py"
325 # that is to say "tests/PloneTestCase.py"
326
326
327 if path.startswith(module):
327 if path.startswith(module):
328 relative = path[len(module):]
328 relative = path[len(module):]
329 if relative.startswith('/'):
329 if relative.startswith('/'):
330 return relative[1:]
330 return relative[1:]
331 else:
331 else:
332 return relative
332 return relative
333
333
334 # The path is outside our tracked tree...
334 # The path is outside our tracked tree...
335 self.ui.debug('Ignoring %r since it is not under %r\n' % (path, module))
335 self.ui.debug('Ignoring %r since it is not under %r\n' % (path, module))
336 return None
336 return None
337
337
338 self.child_cset = None
338 self.child_cset = None
339 def parselogentry(orig_paths, revnum, author, date, message):
339 def parselogentry(orig_paths, revnum, author, date, message):
340 self.ui.debug("parsing revision %d (%d changes)\n" %
340 self.ui.debug("parsing revision %d (%d changes)\n" %
341 (revnum, len(orig_paths)))
341 (revnum, len(orig_paths)))
342
342
343 if revnum in self.modulemap:
343 if revnum in self.modulemap:
344 new_module = self.modulemap[revnum]
344 new_module = self.modulemap[revnum]
345 if new_module != self.module:
345 if new_module != self.module:
346 self.module = new_module
346 self.module = new_module
347 self.reparent(self.module)
347 self.reparent(self.module)
348
348
349 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
349 copyfrom = {} # Map of entrypath, revision for finding source of deleted revisions.
350 copies = {}
350 copies = {}
351 entries = []
351 entries = []
352 rev = self.revid(revnum)
352 rev = self.revid(revnum)
353 parents = []
353 parents = []
354
354
355 # branch log might return entries for a parent we already have
355 # branch log might return entries for a parent we already have
356 if (rev in self.commits or
356 if (rev in self.commits or
357 (revnum < self.lastrevs.get(self.module, 0))):
357 (revnum < self.lastrevs.get(self.module, 0))):
358 return
358 return
359
359
360 try:
360 try:
361 branch = self.module.split("/")[-1]
361 branch = self.module.split("/")[-1]
362 if branch == 'trunk':
362 if branch == 'trunk':
363 branch = ''
363 branch = ''
364 except IndexError:
364 except IndexError:
365 branch = None
365 branch = None
366
366
367 orig_paths = orig_paths.items()
367 orig_paths = orig_paths.items()
368 orig_paths.sort()
368 orig_paths.sort()
369 for path, ent in orig_paths:
369 for path, ent in orig_paths:
370 # self.ui.write("path %s\n" % path)
370 # self.ui.write("path %s\n" % path)
371 if path == self.module: # Follow branching back in history
371 if path == self.module: # Follow branching back in history
372 if ent:
372 if ent:
373 if ent.copyfrom_path:
373 if ent.copyfrom_path:
374 # ent.copyfrom_rev may not be the actual last revision
374 # ent.copyfrom_rev may not be the actual last revision
375 prev = self.latest(ent.copyfrom_path, ent.copyfrom_rev)
375 prev = self.latest(ent.copyfrom_path, ent.copyfrom_rev)
376 self.modulemap[prev] = ent.copyfrom_path
376 self.modulemap[prev] = ent.copyfrom_path
377 parents = [self.revid(prev, ent.copyfrom_path)]
377 parents = [self.revid(prev, ent.copyfrom_path)]
378 self.ui.note('found parent of branch %s at %d: %s\n' % \
378 self.ui.note('found parent of branch %s at %d: %s\n' % \
379 (self.module, prev, ent.copyfrom_path))
379 (self.module, prev, ent.copyfrom_path))
380 else:
380 else:
381 self.ui.debug("No copyfrom path, don't know what to do.\n")
381 self.ui.debug("No copyfrom path, don't know what to do.\n")
382 # Maybe it was added and there is no more history.
382 # Maybe it was added and there is no more history.
383 entrypath = get_entry_from_path(path, module=self.module)
383 entrypath = get_entry_from_path(path, module=self.module)
384 # self.ui.write("entrypath %s\n" % entrypath)
384 # self.ui.write("entrypath %s\n" % entrypath)
385 if entrypath is None:
385 if entrypath is None:
386 # Outside our area of interest
386 # Outside our area of interest
387 self.ui.debug("boring@%s: %s\n" % (revnum, path))
387 self.ui.debug("boring@%s: %s\n" % (revnum, path))
388 continue
388 continue
389 entry = entrypath.decode(self.encoding)
389 entry = entrypath.decode(self.encoding)
390
390
391 kind = svn.ra.check_path(self.ra, entrypath, revnum)
391 kind = svn.ra.check_path(self.ra, entrypath, revnum)
392 if kind == svn.core.svn_node_file:
392 if kind == svn.core.svn_node_file:
393 if ent.copyfrom_path:
393 if ent.copyfrom_path:
394 copyfrom_path = get_entry_from_path(ent.copyfrom_path)
394 copyfrom_path = get_entry_from_path(ent.copyfrom_path)
395 if copyfrom_path:
395 if copyfrom_path:
396 self.ui.debug("Copied to %s from %s@%s\n" % (entry, copyfrom_path, ent.copyfrom_rev))
396 self.ui.debug("Copied to %s from %s@%s\n" % (entry, copyfrom_path, ent.copyfrom_rev))
397 # It's probably important for hg that the source
397 # It's probably important for hg that the source
398 # exists in the revision's parent, not just the
398 # exists in the revision's parent, not just the
399 # ent.copyfrom_rev
399 # ent.copyfrom_rev
400 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
400 fromkind = svn.ra.check_path(self.ra, copyfrom_path, ent.copyfrom_rev)
401 if fromkind != 0:
401 if fromkind != 0:
402 copies[self.recode(entry)] = self.recode(copyfrom_path)
402 copies[self.recode(entry)] = self.recode(copyfrom_path)
403 entries.append(self.recode(entry))
403 entries.append(self.recode(entry))
404 elif kind == 0: # gone, but had better be a deleted *file*
404 elif kind == 0: # gone, but had better be a deleted *file*
405 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
405 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
406
406
407 # if a branch is created but entries are removed in the same
407 # if a branch is created but entries are removed in the same
408 # changeset, get the right fromrev
408 # changeset, get the right fromrev
409 if parents:
409 if parents:
410 uuid, old_module, fromrev = self.revsplit(parents[0])
410 uuid, old_module, fromrev = self.revsplit(parents[0])
411 else:
411 else:
412 fromrev = revnum - 1
412 fromrev = revnum - 1
413 # might always need to be revnum - 1 in these 3 lines?
413 # might always need to be revnum - 1 in these 3 lines?
414 old_module = self.modulemap.get(fromrev, self.module)
414 old_module = self.modulemap.get(fromrev, self.module)
415
415
416 basepath = old_module + "/" + get_entry_from_path(path, module=self.module)
416 basepath = old_module + "/" + get_entry_from_path(path, module=self.module)
417 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module)
417 entrypath = old_module + "/" + get_entry_from_path(path, module=self.module)
418
418
419 def lookup_parts(p):
419 def lookup_parts(p):
420 rc = None
420 rc = None
421 parts = p.split("/")
421 parts = p.split("/")
422 for i in range(len(parts)):
422 for i in range(len(parts)):
423 part = "/".join(parts[:i])
423 part = "/".join(parts[:i])
424 info = part, copyfrom.get(part, None)
424 info = part, copyfrom.get(part, None)
425 if info[1] is not None:
425 if info[1] is not None:
426 self.ui.debug("Found parent directory %s\n" % info[1])
426 self.ui.debug("Found parent directory %s\n" % info[1])
427 rc = info
427 rc = info
428 return rc
428 return rc
429
429
430 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
430 self.ui.debug("base, entry %s %s\n" % (basepath, entrypath))
431
431
432 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
432 frompath, froment = lookup_parts(entrypath) or (None, revnum - 1)
433
433
434 # need to remove fragment from lookup_parts and replace with copyfrom_path
434 # need to remove fragment from lookup_parts and replace with copyfrom_path
435 if frompath is not None:
435 if frompath is not None:
436 self.ui.debug("munge-o-matic\n")
436 self.ui.debug("munge-o-matic\n")
437 self.ui.debug(entrypath + '\n')
437 self.ui.debug(entrypath + '\n')
438 self.ui.debug(entrypath[len(frompath):] + '\n')
438 self.ui.debug(entrypath[len(frompath):] + '\n')
439 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
439 entrypath = froment.copyfrom_path + entrypath[len(frompath):]
440 fromrev = froment.copyfrom_rev
440 fromrev = froment.copyfrom_rev
441 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
441 self.ui.debug("Info: %s %s %s %s\n" % (frompath, froment, ent, entrypath))
442
442
443 fromkind = svn.ra.check_path(self.ra, entrypath, fromrev)
443 fromkind = svn.ra.check_path(self.ra, entrypath, fromrev)
444 if fromkind == svn.core.svn_node_file: # a deleted file
444 if fromkind == svn.core.svn_node_file: # a deleted file
445 entries.append(self.recode(entry))
445 entries.append(self.recode(entry))
446 elif fromkind == svn.core.svn_node_dir:
446 elif fromkind == svn.core.svn_node_dir:
447 # print "Deleted/moved non-file:", revnum, path, ent
447 # print "Deleted/moved non-file:", revnum, path, ent
448 # children = self._find_children(path, revnum - 1)
448 # children = self._find_children(path, revnum - 1)
449 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
449 # print "find children %s@%d from %d action %s" % (path, revnum, ent.copyfrom_rev, ent.action)
450 # Sometimes this is tricky. For example: in
450 # Sometimes this is tricky. For example: in
451 # The Subversion Repository revision 6940 a dir
451 # The Subversion Repository revision 6940 a dir
452 # was copied and one of its files was deleted
452 # was copied and one of its files was deleted
453 # from the new location in the same commit. This
453 # from the new location in the same commit. This
454 # code can't deal with that yet.
454 # code can't deal with that yet.
455 if ent.action == 'C':
455 if ent.action == 'C':
456 children = self._find_children(path, fromrev)
456 children = self._find_children(path, fromrev)
457 else:
457 else:
458 oroot = entrypath.strip('/')
458 oroot = entrypath.strip('/')
459 nroot = path.strip('/')
459 nroot = path.strip('/')
460 children = self._find_children(oroot, fromrev)
460 children = self._find_children(oroot, fromrev)
461 children = [s.replace(oroot,nroot) for s in children]
461 children = [s.replace(oroot,nroot) for s in children]
462 # Mark all [files, not directories] as deleted.
462 # Mark all [files, not directories] as deleted.
463 for child in children:
463 for child in children:
464 # Can we move a child directory and its
464 # Can we move a child directory and its
465 # parent in the same commit? (probably can). Could
465 # parent in the same commit? (probably can). Could
466 # cause problems if instead of revnum -1,
466 # cause problems if instead of revnum -1,
467 # we have to look in (copyfrom_path, revnum - 1)
467 # we have to look in (copyfrom_path, revnum - 1)
468 entrypath = get_entry_from_path("/" + child, module=old_module)
468 entrypath = get_entry_from_path("/" + child, module=old_module)
469 if entrypath:
469 if entrypath:
470 entry = self.recode(entrypath.decode(self.encoding))
470 entry = self.recode(entrypath.decode(self.encoding))
471 if entry in copies:
471 if entry in copies:
472 # deleted file within a copy
472 # deleted file within a copy
473 del copies[entry]
473 del copies[entry]
474 else:
474 else:
475 entries.append(entry)
475 entries.append(entry)
476 else:
476 else:
477 self.ui.debug('unknown path in revision %d: %s\n' % \
477 self.ui.debug('unknown path in revision %d: %s\n' % \
478 (revnum, path))
478 (revnum, path))
479 elif kind == svn.core.svn_node_dir:
479 elif kind == svn.core.svn_node_dir:
480 # Should probably synthesize normal file entries
480 # Should probably synthesize normal file entries
481 # and handle as above to clean up copy/rename handling.
481 # and handle as above to clean up copy/rename handling.
482
482
483 # If the directory just had a prop change,
483 # If the directory just had a prop change,
484 # then we shouldn't need to look for its children.
484 # then we shouldn't need to look for its children.
485 # Also this could create duplicate entries. Not sure
485 # Also this could create duplicate entries. Not sure
486 # whether this will matter. Maybe should make entries a set.
486 # whether this will matter. Maybe should make entries a set.
487 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
487 # print "Changed directory", revnum, path, ent.action, ent.copyfrom_path, ent.copyfrom_rev
488 # This will fail if a directory was copied
488 # This will fail if a directory was copied
489 # from another branch and then some of its files
489 # from another branch and then some of its files
490 # were deleted in the same transaction.
490 # were deleted in the same transaction.
491 children = self._find_children(path, revnum)
491 children = self._find_children(path, revnum)
492 children.sort()
492 children.sort()
493 for child in children:
493 for child in children:
494 # Can we move a child directory and its
494 # Can we move a child directory and its
495 # parent in the same commit? (probably can). Could
495 # parent in the same commit? (probably can). Could
496 # cause problems if instead of revnum -1,
496 # cause problems if instead of revnum -1,
497 # we have to look in (copyfrom_path, revnum - 1)
497 # we have to look in (copyfrom_path, revnum - 1)
498 entrypath = get_entry_from_path("/" + child, module=self.module)
498 entrypath = get_entry_from_path("/" + child, module=self.module)
499 # print child, self.module, entrypath
499 # print child, self.module, entrypath
500 if entrypath:
500 if entrypath:
501 # Need to filter out directories here...
501 # Need to filter out directories here...
502 kind = svn.ra.check_path(self.ra, entrypath, revnum)
502 kind = svn.ra.check_path(self.ra, entrypath, revnum)
503 if kind != svn.core.svn_node_dir:
503 if kind != svn.core.svn_node_dir:
504 entries.append(self.recode(entrypath))
504 entries.append(self.recode(entrypath))
505
505
506 # Copies here (must copy all from source)
506 # Copies here (must copy all from source)
507 # Probably not a real problem for us if
507 # Probably not a real problem for us if
508 # source does not exist
508 # source does not exist
509
509
510 # Can do this with the copy command "hg copy"
510 # Can do this with the copy command "hg copy"
511 # if ent.copyfrom_path:
511 # if ent.copyfrom_path:
512 # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding),
512 # copyfrom_entry = get_entry_from_path(ent.copyfrom_path.decode(self.encoding),
513 # module=self.module)
513 # module=self.module)
514 # copyto_entry = entrypath
514 # copyto_entry = entrypath
515 #
515 #
516 # print "copy directory", copyfrom_entry, 'to', copyto_entry
516 # print "copy directory", copyfrom_entry, 'to', copyto_entry
517 #
517 #
518 # copies.append((copyfrom_entry, copyto_entry))
518 # copies.append((copyfrom_entry, copyto_entry))
519
519
520 if ent.copyfrom_path:
520 if ent.copyfrom_path:
521 copyfrom_path = ent.copyfrom_path.decode(self.encoding)
521 copyfrom_path = ent.copyfrom_path.decode(self.encoding)
522 copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module)
522 copyfrom_entry = get_entry_from_path(copyfrom_path, module=self.module)
523 if copyfrom_entry:
523 if copyfrom_entry:
524 copyfrom[path] = ent
524 copyfrom[path] = ent
525 self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path]))
525 self.ui.debug("mark %s came from %s\n" % (path, copyfrom[path]))
526
526
527 # Good, /probably/ a regular copy. Really should check
527 # Good, /probably/ a regular copy. Really should check
528 # to see whether the parent revision actually contains
528 # to see whether the parent revision actually contains
529 # the directory in question.
529 # the directory in question.
530 children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev)
530 children = self._find_children(self.recode(copyfrom_path), ent.copyfrom_rev)
531 children.sort()
531 children.sort()
532 for child in children:
532 for child in children:
533 entrypath = get_entry_from_path("/" + child, module=self.module)
533 entrypath = get_entry_from_path("/" + child, module=self.module)
534 if entrypath:
534 if entrypath:
535 entry = entrypath.decode(self.encoding)
535 entry = entrypath.decode(self.encoding)
536 # print "COPY COPY From", copyfrom_entry, entry
536 # print "COPY COPY From", copyfrom_entry, entry
537 copyto_path = path + entry[len(copyfrom_entry):]
537 copyto_path = path + entry[len(copyfrom_entry):]
538 copyto_entry = get_entry_from_path(copyto_path, module=self.module)
538 copyto_entry = get_entry_from_path(copyto_path, module=self.module)
539 # print "COPY", entry, "COPY To", copyto_entry
539 # print "COPY", entry, "COPY To", copyto_entry
540 copies[self.recode(copyto_entry)] = self.recode(entry)
540 copies[self.recode(copyto_entry)] = self.recode(entry)
541 # copy from quux splort/quuxfile
541 # copy from quux splort/quuxfile
542
542
543 self.modulemap[revnum] = self.module # track backwards in time
543 self.modulemap[revnum] = self.module # track backwards in time
544 # a list of (filename, id) where id lets us retrieve the file.
544 # a list of (filename, id) where id lets us retrieve the file.
545 # eg in git, id is the object hash. for svn it'll be the
545 # eg in git, id is the object hash. for svn it'll be the
546 self.files[rev] = zip(entries, [rev] * len(entries))
546 self.files[rev] = zip(entries, [rev] * len(entries))
547 if not entries:
547 if not entries:
548 return
548 return
549
549
550 # Example SVN datetime. Includes microseconds.
550 # Example SVN datetime. Includes microseconds.
551 # ISO-8601 conformant
551 # ISO-8601 conformant
552 # '2007-01-04T17:35:00.902377Z'
552 # '2007-01-04T17:35:00.902377Z'
553 date = util.parsedate(date[:18] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
553 date = util.parsedate(date[:18] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
554
554
555 log = message and self.recode(message)
555 log = message and self.recode(message)
556 author = author and self.recode(author) or ''
556 author = author and self.recode(author) or ''
557
557
558 cset = commit(author=author,
558 cset = commit(author=author,
559 date=util.datestr(date),
559 date=util.datestr(date),
560 desc=log,
560 desc=log,
561 parents=parents,
561 parents=parents,
562 copies=copies,
562 copies=copies,
563 branch=branch,
563 branch=branch,
564 rev=rev.encode('utf-8'))
564 rev=rev.encode('utf-8'))
565
565
566 self.commits[rev] = cset
566 self.commits[rev] = cset
567 if self.child_cset and not self.child_cset.parents:
567 if self.child_cset and not self.child_cset.parents:
568 self.child_cset.parents = [rev]
568 self.child_cset.parents = [rev]
569 self.child_cset = cset
569 self.child_cset = cset
570
570
571 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
571 self.ui.note('fetching revision log for "%s" from %d to %d\n' %
572 (self.module, from_revnum, to_revnum))
572 (self.module, from_revnum, to_revnum))
573
573
574 try:
574 try:
575 discover_changed_paths = True
575 discover_changed_paths = True
576 strict_node_history = False
576 strict_node_history = False
577 for entry in self.get_log([self.module], from_revnum, to_revnum):
577 for entry in self.get_log([self.module], from_revnum, to_revnum):
578 orig_paths, revnum, author, date, message = entry
578 orig_paths, revnum, author, date, message = entry
579 if self.is_blacklisted(revnum):
579 if self.is_blacklisted(revnum):
580 self.ui.note('skipping blacklisted revision %d\n' % revnum)
580 self.ui.note('skipping blacklisted revision %d\n' % revnum)
581 continue
581 continue
582 if orig_paths is None:
582 if orig_paths is None:
583 self.ui.debug('revision %d has no entries\n' % revnum)
583 self.ui.debug('revision %d has no entries\n' % revnum)
584 continue
584 continue
585 parselogentry(orig_paths, revnum, author, date, message)
585 parselogentry(orig_paths, revnum, author, date, message)
586 except SubversionException, (_, num):
586 except SubversionException, (_, num):
587 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
587 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
588 raise NoSuchRevision(branch=self,
588 raise NoSuchRevision(branch=self,
589 revision="Revision number %d" % to_revnum)
589 revision="Revision number %d" % to_revnum)
590 raise
590 raise
591
591
592 def _getfile(self, file, rev):
592 def _getfile(self, file, rev):
593 io = StringIO()
593 io = StringIO()
594 # TODO: ra.get_file transmits the whole file instead of diffs.
594 # TODO: ra.get_file transmits the whole file instead of diffs.
595 mode = ''
595 mode = ''
596 try:
596 try:
597 revnum = self.revnum(rev)
597 revnum = self.revnum(rev)
598 if self.module != self.modulemap[revnum]:
598 if self.module != self.modulemap[revnum]:
599 self.module = self.modulemap[revnum]
599 self.module = self.modulemap[revnum]
600 self.reparent(self.module)
600 self.reparent(self.module)
601 info = svn.ra.get_file(self.ra, file, revnum, io)
601 info = svn.ra.get_file(self.ra, file, revnum, io)
602 if isinstance(info, list):
602 if isinstance(info, list):
603 info = info[-1]
603 info = info[-1]
604 mode = ("svn:executable" in info) and 'x' or ''
604 mode = ("svn:executable" in info) and 'x' or ''
605 mode = ("svn:special" in info) and 'l' or mode
605 mode = ("svn:special" in info) and 'l' or mode
606 except SubversionException, e:
606 except SubversionException, e:
607 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
607 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
608 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
608 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
609 if e.apr_err in notfound: # File not found
609 if e.apr_err in notfound: # File not found
610 raise IOError()
610 raise IOError()
611 raise
611 raise
612 data = io.getvalue()
612 data = io.getvalue()
613 if mode == 'l':
613 if mode == 'l':
614 link_prefix = "link "
614 link_prefix = "link "
615 if data.startswith(link_prefix):
615 if data.startswith(link_prefix):
616 data = data[len(link_prefix):]
616 data = data[len(link_prefix):]
617 return data, mode
617 return data, mode
618
618
619 def _find_children(self, path, revnum):
619 def _find_children(self, path, revnum):
620 path = path.strip("/")
620 path = path.strip("/")
621
621
622 def _find_children_fallback(path, revnum):
622 def _find_children_fallback(path, revnum):
623 # SWIG python bindings for getdir are broken up to at least 1.4.3
623 # SWIG python bindings for getdir are broken up to at least 1.4.3
624 pool = Pool()
624 pool = Pool()
625 optrev = svn.core.svn_opt_revision_t()
625 optrev = svn.core.svn_opt_revision_t()
626 optrev.kind = svn.core.svn_opt_revision_number
626 optrev.kind = svn.core.svn_opt_revision_number
627 optrev.value.number = revnum
627 optrev.value.number = revnum
628 rpath = '/'.join([self.base, path]).strip('/')
628 rpath = '/'.join([self.base, path]).strip('/')
629 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev, True, self.ctx, pool).keys()]
629 return ['%s/%s' % (path, x) for x in svn.client.ls(rpath, optrev, True, self.ctx, pool).keys()]
630
630
631 if hasattr(self, '_find_children_fallback'):
631 if hasattr(self, '_find_children_fallback'):
632 return _find_children_fallback(path, revnum)
632 return _find_children_fallback(path, revnum)
633
633
634 self.reparent("/" + path)
634 self.reparent("/" + path)
635 pool = Pool()
635 pool = Pool()
636
636
637 children = []
637 children = []
638 def find_children_inner(children, path, revnum = revnum):
638 def find_children_inner(children, path, revnum = revnum):
639 if hasattr(svn.ra, 'get_dir2'): # Since SVN 1.4
639 if hasattr(svn.ra, 'get_dir2'): # Since SVN 1.4
640 fields = 0xffffffff # Binding does not provide SVN_DIRENT_ALL
640 fields = 0xffffffff # Binding does not provide SVN_DIRENT_ALL
641 getdir = svn.ra.get_dir2(self.ra, path, revnum, fields, pool)
641 getdir = svn.ra.get_dir2(self.ra, path, revnum, fields, pool)
642 else:
642 else:
643 getdir = svn.ra.get_dir(self.ra, path, revnum, pool)
643 getdir = svn.ra.get_dir(self.ra, path, revnum, pool)
644 if type(getdir) == dict:
644 if type(getdir) == dict:
645 # python binding for getdir is broken up to at least 1.4.3
645 # python binding for getdir is broken up to at least 1.4.3
646 raise CompatibilityException()
646 raise CompatibilityException()
647 dirents = getdir[0]
647 dirents = getdir[0]
648 if type(dirents) == int:
648 if type(dirents) == int:
649 # got here once due to infinite recursion bug
649 # got here once due to infinite recursion bug
650 # pprint.pprint(getdir)
650 # pprint.pprint(getdir)
651 return
651 return
652 c = dirents.keys()
652 c = dirents.keys()
653 c.sort()
653 c.sort()
654 for child in c:
654 for child in c:
655 dirent = dirents[child]
655 dirent = dirents[child]
656 if dirent.kind == svn.core.svn_node_dir:
656 if dirent.kind == svn.core.svn_node_dir:
657 find_children_inner(children, (path + "/" + child).strip("/"))
657 find_children_inner(children, (path + "/" + child).strip("/"))
658 else:
658 else:
659 children.append((path + "/" + child).strip("/"))
659 children.append((path + "/" + child).strip("/"))
660
660
661 try:
661 try:
662 find_children_inner(children, "")
662 find_children_inner(children, "")
663 except CompatibilityException:
663 except CompatibilityException:
664 self._find_children_fallback = True
664 self._find_children_fallback = True
665 self.reparent(self.module)
665 self.reparent(self.module)
666 return _find_children_fallback(path, revnum)
666 return _find_children_fallback(path, revnum)
667
667
668 self.reparent(self.module)
668 self.reparent(self.module)
669 return [path + "/" + c for c in children]
669 return [path + "/" + c for c in children]
@@ -1,125 +1,125 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2007 Daniel Holth <dholth@fastmail.fm>
3 # Copyright (C) 2007 Daniel Holth <dholth@fastmail.fm>
4 # This is a stripped-down version of the original bzr-svn transport.py,
4 # This is a stripped-down version of the original bzr-svn transport.py,
5 # Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
5 # Copyright (C) 2006 Jelmer Vernooij <jelmer@samba.org>
6
6
7 # This program is free software; you can redistribute it and/or modify
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
10 # (at your option) any later version.
11
11
12 # This program is distributed in the hope that it will be useful,
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
15 # GNU General Public License for more details.
16
16
17 # You should have received a copy of the GNU General Public License
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20
20
21 from cStringIO import StringIO
21 from cStringIO import StringIO
22 import os
22 import os
23 from tempfile import mktemp
23 from tempfile import mktemp
24
24
25 from svn.core import SubversionException, Pool
25 from svn.core import SubversionException, Pool
26 import svn.ra
26 import svn.ra
27 import svn.client
27 import svn.client
28 import svn.core
28 import svn.core
29
29
30 # Some older versions of the Python bindings need to be
30 # Some older versions of the Python bindings need to be
31 # explicitly initialized. But what we want to do probably
31 # explicitly initialized. But what we want to do probably
32 # won't work worth a darn against those libraries anyway!
32 # won't work worth a darn against those libraries anyway!
33 svn.ra.initialize()
33 svn.ra.initialize()
34
34
35 svn_config = svn.core.svn_config_get_config(None)
35 svn_config = svn.core.svn_config_get_config(None)
36
36
37
37
38 def _create_auth_baton(pool):
38 def _create_auth_baton(pool):
39 """Create a Subversion authentication baton. """
39 """Create a Subversion authentication baton. """
40 import svn.client
40 import svn.client
41 # Give the client context baton a suite of authentication
41 # Give the client context baton a suite of authentication
42 # providers.h
42 # providers.h
43 providers = [
43 providers = [
44 svn.client.get_simple_provider(pool),
44 svn.client.get_simple_provider(pool),
45 svn.client.get_username_provider(pool),
45 svn.client.get_username_provider(pool),
46 svn.client.get_ssl_client_cert_file_provider(pool),
46 svn.client.get_ssl_client_cert_file_provider(pool),
47 svn.client.get_ssl_client_cert_pw_file_provider(pool),
47 svn.client.get_ssl_client_cert_pw_file_provider(pool),
48 svn.client.get_ssl_server_trust_file_provider(pool),
48 svn.client.get_ssl_server_trust_file_provider(pool),
49 ]
49 ]
50 return svn.core.svn_auth_open(providers, pool)
50 return svn.core.svn_auth_open(providers, pool)
51
51
52 class NotBranchError(SubversionException):
52 class NotBranchError(SubversionException):
53 pass
53 pass
54
54
55 class SvnRaTransport(object):
55 class SvnRaTransport(object):
56 """
56 """
57 Open an ra connection to a Subversion repository.
57 Open an ra connection to a Subversion repository.
58 """
58 """
59 def __init__(self, url="", ra=None):
59 def __init__(self, url="", ra=None):
60 self.pool = Pool()
60 self.pool = Pool()
61 self.svn_url = url
61 self.svn_url = url
62 self.username = ''
62 self.username = ''
63 self.password = ''
63 self.password = ''
64
64
65 # Only Subversion 1.4 has reparent()
65 # Only Subversion 1.4 has reparent()
66 if ra is None or not hasattr(svn.ra, 'reparent'):
66 if ra is None or not hasattr(svn.ra, 'reparent'):
67 self.client = svn.client.create_context(self.pool)
67 self.client = svn.client.create_context(self.pool)
68 ab = _create_auth_baton(self.pool)
68 ab = _create_auth_baton(self.pool)
69 if False:
69 if False:
70 svn.core.svn_auth_set_parameter(
70 svn.core.svn_auth_set_parameter(
71 ab, svn.core.SVN_AUTH_PARAM_DEFAULT_USERNAME, self.username)
71 ab, svn.core.SVN_AUTH_PARAM_DEFAULT_USERNAME, self.username)
72 svn.core.svn_auth_set_parameter(
72 svn.core.svn_auth_set_parameter(
73 ab, svn.core.SVN_AUTH_PARAM_DEFAULT_PASSWORD, self.password)
73 ab, svn.core.SVN_AUTH_PARAM_DEFAULT_PASSWORD, self.password)
74 self.client.auth_baton = ab
74 self.client.auth_baton = ab
75 self.client.config = svn_config
75 self.client.config = svn_config
76 try:
76 try:
77 self.ra = svn.client.open_ra_session(
77 self.ra = svn.client.open_ra_session(
78 self.svn_url.encode('utf8'),
78 self.svn_url.encode('utf8'),
79 self.client, self.pool)
79 self.client, self.pool)
80 except SubversionException, (_, num):
80 except SubversionException, (_, num):
81 if num in (svn.core.SVN_ERR_RA_ILLEGAL_URL,
81 if num in (svn.core.SVN_ERR_RA_ILLEGAL_URL,
82 svn.core.SVN_ERR_RA_LOCAL_REPOS_OPEN_FAILED,
82 svn.core.SVN_ERR_RA_LOCAL_REPOS_OPEN_FAILED,
83 svn.core.SVN_ERR_BAD_URL):
83 svn.core.SVN_ERR_BAD_URL):
84 raise NotBranchError(url)
84 raise NotBranchError(url)
85 raise
85 raise
86 else:
86 else:
87 self.ra = ra
87 self.ra = ra
88 svn.ra.reparent(self.ra, self.svn_url.encode('utf8'))
88 svn.ra.reparent(self.ra, self.svn_url.encode('utf8'))
89
89
90 class Reporter:
90 class Reporter:
91 def __init__(self, (reporter, report_baton)):
91 def __init__(self, (reporter, report_baton)):
92 self._reporter = reporter
92 self._reporter = reporter
93 self._baton = report_baton
93 self._baton = report_baton
94
94
95 def set_path(self, path, revnum, start_empty, lock_token, pool=None):
95 def set_path(self, path, revnum, start_empty, lock_token, pool=None):
96 svn.ra.reporter2_invoke_set_path(self._reporter, self._baton,
96 svn.ra.reporter2_invoke_set_path(self._reporter, self._baton,
97 path, revnum, start_empty, lock_token, pool)
97 path, revnum, start_empty, lock_token, pool)
98
98
99 def delete_path(self, path, pool=None):
99 def delete_path(self, path, pool=None):
100 svn.ra.reporter2_invoke_delete_path(self._reporter, self._baton,
100 svn.ra.reporter2_invoke_delete_path(self._reporter, self._baton,
101 path, pool)
101 path, pool)
102
102
103 def link_path(self, path, url, revision, start_empty, lock_token,
103 def link_path(self, path, url, revision, start_empty, lock_token,
104 pool=None):
104 pool=None):
105 svn.ra.reporter2_invoke_link_path(self._reporter, self._baton,
105 svn.ra.reporter2_invoke_link_path(self._reporter, self._baton,
106 path, url, revision, start_empty, lock_token,
106 path, url, revision, start_empty, lock_token,
107 pool)
107 pool)
108
108
109 def finish_report(self, pool=None):
109 def finish_report(self, pool=None):
110 svn.ra.reporter2_invoke_finish_report(self._reporter,
110 svn.ra.reporter2_invoke_finish_report(self._reporter,
111 self._baton, pool)
111 self._baton, pool)
112
112
113 def abort_report(self, pool=None):
113 def abort_report(self, pool=None):
114 svn.ra.reporter2_invoke_abort_report(self._reporter,
114 svn.ra.reporter2_invoke_abort_report(self._reporter,
115 self._baton, pool)
115 self._baton, pool)
116
116
117 def do_update(self, revnum, path, *args, **kwargs):
117 def do_update(self, revnum, path, *args, **kwargs):
118 return self.Reporter(svn.ra.do_update(self.ra, revnum, path, *args, **kwargs))
118 return self.Reporter(svn.ra.do_update(self.ra, revnum, path, *args, **kwargs))
119
119
120 def clone(self, offset=None):
120 def clone(self, offset=None):
121 """See Transport.clone()."""
121 """See Transport.clone()."""
122 if offset is None:
122 if offset is None:
123 return self.__class__(self.base)
123 return self.__class__(self.base)
124
124
125 return SvnRaTransport(urlutils.join(self.base, offset), ra=self.ra)
125 return SvnRaTransport(urlutils.join(self.base, offset), ra=self.ra)
@@ -1,168 +1,168 b''
1 # Copyright (C) 2006 - Marco Barisione <marco@barisione.org>
1 # Copyright (C) 2006 - Marco Barisione <marco@barisione.org>
2 #
2 #
3 # This is a small extension for Mercurial (http://www.selenic.com/mercurial)
3 # This is a small extension for Mercurial (http://www.selenic.com/mercurial)
4 # that removes files not known to mercurial
4 # that removes files not known to mercurial
5 #
5 #
6 # This program was inspired by the "cvspurge" script contained in CVS utilities
6 # This program was inspired by the "cvspurge" script contained in CVS utilities
7 # (http://www.red-bean.com/cvsutils/).
7 # (http://www.red-bean.com/cvsutils/).
8 #
8 #
9 # To enable the "purge" extension put these lines in your ~/.hgrc:
9 # To enable the "purge" extension put these lines in your ~/.hgrc:
10 # [extensions]
10 # [extensions]
11 # hgext.purge =
11 # hgext.purge =
12 #
12 #
13 # For help on the usage of "hg purge" use:
13 # For help on the usage of "hg purge" use:
14 # hg help purge
14 # hg help purge
15 #
15 #
16 # This program is free software; you can redistribute it and/or modify
16 # This program is free software; you can redistribute it and/or modify
17 # it under the terms of the GNU General Public License as published by
17 # it under the terms of the GNU General Public License as published by
18 # the Free Software Foundation; either version 2 of the License, or
18 # the Free Software Foundation; either version 2 of the License, or
19 # (at your option) any later version.
19 # (at your option) any later version.
20 #
20 #
21 # This program is distributed in the hope that it will be useful,
21 # This program is distributed in the hope that it will be useful,
22 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # but WITHOUT ANY WARRANTY; without even the implied warranty of
23 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24 # GNU General Public License for more details.
24 # GNU General Public License for more details.
25 #
25 #
26 # You should have received a copy of the GNU General Public License
26 # You should have received a copy of the GNU General Public License
27 # along with this program; if not, write to the Free Software
27 # along with this program; if not, write to the Free Software
28 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
28 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
29
29
30 from mercurial import hg, util
30 from mercurial import hg, util
31 from mercurial.i18n import _
31 from mercurial.i18n import _
32 import os
32 import os
33
33
34 def dopurge(ui, repo, dirs=None, act=True, ignored=False,
34 def dopurge(ui, repo, dirs=None, act=True, ignored=False,
35 abort_on_err=False, eol='\n',
35 abort_on_err=False, eol='\n',
36 force=False, include=None, exclude=None):
36 force=False, include=None, exclude=None):
37 def error(msg):
37 def error(msg):
38 if abort_on_err:
38 if abort_on_err:
39 raise util.Abort(msg)
39 raise util.Abort(msg)
40 else:
40 else:
41 ui.warn(_('warning: %s\n') % msg)
41 ui.warn(_('warning: %s\n') % msg)
42
42
43 def remove(remove_func, name):
43 def remove(remove_func, name):
44 if act:
44 if act:
45 try:
45 try:
46 remove_func(os.path.join(repo.root, name))
46 remove_func(os.path.join(repo.root, name))
47 except OSError, e:
47 except OSError, e:
48 error(_('%s cannot be removed') % name)
48 error(_('%s cannot be removed') % name)
49 else:
49 else:
50 ui.write('%s%s' % (name, eol))
50 ui.write('%s%s' % (name, eol))
51
51
52 directories = []
52 directories = []
53 files = []
53 files = []
54 missing = []
54 missing = []
55 roots, match, anypats = util.cmdmatcher(repo.root, repo.getcwd(), dirs,
55 roots, match, anypats = util.cmdmatcher(repo.root, repo.getcwd(), dirs,
56 include, exclude)
56 include, exclude)
57 for src, f, st in repo.dirstate.statwalk(files=roots, match=match,
57 for src, f, st in repo.dirstate.statwalk(files=roots, match=match,
58 ignored=ignored, directories=True):
58 ignored=ignored, directories=True):
59 if src == 'd':
59 if src == 'd':
60 directories.append(f)
60 directories.append(f)
61 elif src == 'm':
61 elif src == 'm':
62 missing.append(f)
62 missing.append(f)
63 elif src == 'f' and f not in repo.dirstate:
63 elif src == 'f' and f not in repo.dirstate:
64 files.append(f)
64 files.append(f)
65
65
66 _check_missing(ui, repo, missing, force)
66 _check_missing(ui, repo, missing, force)
67
67
68 directories.sort()
68 directories.sort()
69
69
70 for f in files:
70 for f in files:
71 if f not in repo.dirstate:
71 if f not in repo.dirstate:
72 ui.note(_('Removing file %s\n') % f)
72 ui.note(_('Removing file %s\n') % f)
73 remove(os.remove, f)
73 remove(os.remove, f)
74
74
75 for f in directories[::-1]:
75 for f in directories[::-1]:
76 if match(f) and not os.listdir(repo.wjoin(f)):
76 if match(f) and not os.listdir(repo.wjoin(f)):
77 ui.note(_('Removing directory %s\n') % f)
77 ui.note(_('Removing directory %s\n') % f)
78 remove(os.rmdir, f)
78 remove(os.rmdir, f)
79
79
80 def _check_missing(ui, repo, missing, force=False):
80 def _check_missing(ui, repo, missing, force=False):
81 """Abort if there is the chance of having problems with name-mangling fs
81 """Abort if there is the chance of having problems with name-mangling fs
82
82
83 In a name mangling filesystem (e.g. a case insensitive one)
83 In a name mangling filesystem (e.g. a case insensitive one)
84 dirstate.walk() can yield filenames different from the ones
84 dirstate.walk() can yield filenames different from the ones
85 stored in the dirstate. This already confuses the status and
85 stored in the dirstate. This already confuses the status and
86 add commands, but with purge this may cause data loss.
86 add commands, but with purge this may cause data loss.
87
87
88 To prevent this, _check_missing will abort if there are missing
88 To prevent this, _check_missing will abort if there are missing
89 files. The force option will let the user skip the check if he
89 files. The force option will let the user skip the check if he
90 knows it is safe.
90 knows it is safe.
91
91
92 Even with the force option this function will check if any of the
92 Even with the force option this function will check if any of the
93 missing files is still available in the working dir: if so there
93 missing files is still available in the working dir: if so there
94 may be some problem with the underlying filesystem, so it
94 may be some problem with the underlying filesystem, so it
95 aborts unconditionally."""
95 aborts unconditionally."""
96
96
97 found = [f for f in missing if util.lexists(repo.wjoin(f))]
97 found = [f for f in missing if util.lexists(repo.wjoin(f))]
98
98
99 if found:
99 if found:
100 if not ui.quiet:
100 if not ui.quiet:
101 ui.warn(_("The following tracked files weren't listed by the "
101 ui.warn(_("The following tracked files weren't listed by the "
102 "filesystem, but could still be found:\n"))
102 "filesystem, but could still be found:\n"))
103 for f in found:
103 for f in found:
104 ui.warn("%s\n" % f)
104 ui.warn("%s\n" % f)
105 if util.checkfolding(repo.path):
105 if util.checkfolding(repo.path):
106 ui.warn(_("This is probably due to a case-insensitive "
106 ui.warn(_("This is probably due to a case-insensitive "
107 "filesystem\n"))
107 "filesystem\n"))
108 raise util.Abort(_("purging on name mangling filesystems is not "
108 raise util.Abort(_("purging on name mangling filesystems is not "
109 "yet fully supported"))
109 "yet fully supported"))
110
110
111 if missing and not force:
111 if missing and not force:
112 raise util.Abort(_("there are missing files in the working dir and "
112 raise util.Abort(_("there are missing files in the working dir and "
113 "purge still has problems with them due to name "
113 "purge still has problems with them due to name "
114 "mangling filesystems. "
114 "mangling filesystems. "
115 "Use --force if you know what you are doing"))
115 "Use --force if you know what you are doing"))
116
116
117
117
118 def purge(ui, repo, *dirs, **opts):
118 def purge(ui, repo, *dirs, **opts):
119 '''removes files not tracked by mercurial
119 '''removes files not tracked by mercurial
120
120
121 Delete files not known to mercurial, this is useful to test local and
121 Delete files not known to mercurial, this is useful to test local and
122 uncommitted changes in the otherwise clean source tree.
122 uncommitted changes in the otherwise clean source tree.
123
123
124 This means that purge will delete:
124 This means that purge will delete:
125 - Unknown files: files marked with "?" by "hg status"
125 - Unknown files: files marked with "?" by "hg status"
126 - Ignored files: files usually ignored by Mercurial because they match
126 - Ignored files: files usually ignored by Mercurial because they match
127 a pattern in a ".hgignore" file
127 a pattern in a ".hgignore" file
128 - Empty directories: in fact Mercurial ignores directories unless they
128 - Empty directories: in fact Mercurial ignores directories unless they
129 contain files under source control managment
129 contain files under source control managment
130 But it will leave untouched:
130 But it will leave untouched:
131 - Unmodified tracked files
131 - Unmodified tracked files
132 - Modified tracked files
132 - Modified tracked files
133 - New files added to the repository (with "hg add")
133 - New files added to the repository (with "hg add")
134
134
135 If directories are given on the command line, only files in these
135 If directories are given on the command line, only files in these
136 directories are considered.
136 directories are considered.
137
137
138 Be careful with purge, you could irreversibly delete some files you
138 Be careful with purge, you could irreversibly delete some files you
139 forgot to add to the repository. If you only want to print the list of
139 forgot to add to the repository. If you only want to print the list of
140 files that this program would delete use the --print option.
140 files that this program would delete use the --print option.
141 '''
141 '''
142 act = not opts['print']
142 act = not opts['print']
143 ignored = bool(opts['all'])
143 ignored = bool(opts['all'])
144 abort_on_err = bool(opts['abort_on_err'])
144 abort_on_err = bool(opts['abort_on_err'])
145 eol = opts['print0'] and '\0' or '\n'
145 eol = opts['print0'] and '\0' or '\n'
146 if eol == '\0':
146 if eol == '\0':
147 # --print0 implies --print
147 # --print0 implies --print
148 act = False
148 act = False
149 force = bool(opts['force'])
149 force = bool(opts['force'])
150 include = opts['include']
150 include = opts['include']
151 exclude = opts['exclude']
151 exclude = opts['exclude']
152 dopurge(ui, repo, dirs, act, ignored, abort_on_err,
152 dopurge(ui, repo, dirs, act, ignored, abort_on_err,
153 eol, force, include, exclude)
153 eol, force, include, exclude)
154
154
155
155
156 cmdtable = {
156 cmdtable = {
157 'purge|clean':
157 'purge|clean':
158 (purge,
158 (purge,
159 [('a', 'abort-on-err', None, _('abort if an error occurs')),
159 [('a', 'abort-on-err', None, _('abort if an error occurs')),
160 ('', 'all', None, _('purge ignored files too')),
160 ('', 'all', None, _('purge ignored files too')),
161 ('f', 'force', None, _('purge even when missing files are detected')),
161 ('f', 'force', None, _('purge even when missing files are detected')),
162 ('p', 'print', None, _('print the file names instead of deleting them')),
162 ('p', 'print', None, _('print the file names instead of deleting them')),
163 ('0', 'print0', None, _('end filenames with NUL, for use with xargs'
163 ('0', 'print0', None, _('end filenames with NUL, for use with xargs'
164 ' (implies -p)')),
164 ' (implies -p)')),
165 ('I', 'include', [], _('include names matching the given patterns')),
165 ('I', 'include', [], _('include names matching the given patterns')),
166 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
166 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
167 _('hg purge [OPTION]... [DIR]...'))
167 _('hg purge [OPTION]... [DIR]...'))
168 }
168 }
@@ -1,258 +1,258 b''
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
1 # hgweb/hgwebdir_mod.py - Web interface for a directory of repositories.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
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 from mercurial import demandimport; demandimport.enable()
9 from mercurial import demandimport; demandimport.enable()
10 import os, mimetools, cStringIO
10 import os, mimetools, cStringIO
11 from mercurial.i18n import gettext as _
11 from mercurial.i18n import gettext as _
12 from mercurial import ui, hg, util, templater
12 from mercurial import ui, hg, util, templater
13 from common import get_mtime, staticfile, style_map, paritygen
13 from common import get_mtime, staticfile, style_map, paritygen
14 from hgweb_mod import hgweb
14 from hgweb_mod import hgweb
15
15
16 # This is a stopgap
16 # This is a stopgap
17 class hgwebdir(object):
17 class hgwebdir(object):
18 def __init__(self, config, parentui=None):
18 def __init__(self, config, parentui=None):
19 def cleannames(items):
19 def cleannames(items):
20 return [(name.strip(os.sep), path) for name, path in items]
20 return [(name.strip(os.sep), path) for name, path in items]
21
21
22 self.parentui = parentui
22 self.parentui = parentui
23 self.motd = None
23 self.motd = None
24 self.style = None
24 self.style = None
25 self.stripecount = None
25 self.stripecount = None
26 self.repos_sorted = ('name', False)
26 self.repos_sorted = ('name', False)
27 if isinstance(config, (list, tuple)):
27 if isinstance(config, (list, tuple)):
28 self.repos = cleannames(config)
28 self.repos = cleannames(config)
29 self.repos_sorted = ('', False)
29 self.repos_sorted = ('', False)
30 elif isinstance(config, dict):
30 elif isinstance(config, dict):
31 self.repos = cleannames(config.items())
31 self.repos = cleannames(config.items())
32 self.repos.sort()
32 self.repos.sort()
33 else:
33 else:
34 if isinstance(config, util.configparser):
34 if isinstance(config, util.configparser):
35 cp = config
35 cp = config
36 else:
36 else:
37 cp = util.configparser()
37 cp = util.configparser()
38 cp.read(config)
38 cp.read(config)
39 self.repos = []
39 self.repos = []
40 if cp.has_section('web'):
40 if cp.has_section('web'):
41 if cp.has_option('web', 'motd'):
41 if cp.has_option('web', 'motd'):
42 self.motd = cp.get('web', 'motd')
42 self.motd = cp.get('web', 'motd')
43 if cp.has_option('web', 'style'):
43 if cp.has_option('web', 'style'):
44 self.style = cp.get('web', 'style')
44 self.style = cp.get('web', 'style')
45 if cp.has_option('web', 'stripes'):
45 if cp.has_option('web', 'stripes'):
46 self.stripecount = int(cp.get('web', 'stripes'))
46 self.stripecount = int(cp.get('web', 'stripes'))
47 if cp.has_section('paths'):
47 if cp.has_section('paths'):
48 self.repos.extend(cleannames(cp.items('paths')))
48 self.repos.extend(cleannames(cp.items('paths')))
49 if cp.has_section('collections'):
49 if cp.has_section('collections'):
50 for prefix, root in cp.items('collections'):
50 for prefix, root in cp.items('collections'):
51 for path in util.walkrepos(root):
51 for path in util.walkrepos(root):
52 repo = os.path.normpath(path)
52 repo = os.path.normpath(path)
53 name = repo
53 name = repo
54 if name.startswith(prefix):
54 if name.startswith(prefix):
55 name = name[len(prefix):]
55 name = name[len(prefix):]
56 self.repos.append((name.lstrip(os.sep), repo))
56 self.repos.append((name.lstrip(os.sep), repo))
57 self.repos.sort()
57 self.repos.sort()
58
58
59 def run(self):
59 def run(self):
60 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
60 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
61 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
61 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
62 import mercurial.hgweb.wsgicgi as wsgicgi
62 import mercurial.hgweb.wsgicgi as wsgicgi
63 from request import wsgiapplication
63 from request import wsgiapplication
64 def make_web_app():
64 def make_web_app():
65 return self
65 return self
66 wsgicgi.launch(wsgiapplication(make_web_app))
66 wsgicgi.launch(wsgiapplication(make_web_app))
67
67
68 def run_wsgi(self, req):
68 def run_wsgi(self, req):
69 def header(**map):
69 def header(**map):
70 header_file = cStringIO.StringIO(
70 header_file = cStringIO.StringIO(
71 ''.join(tmpl("header", encoding=util._encoding, **map)))
71 ''.join(tmpl("header", encoding=util._encoding, **map)))
72 msg = mimetools.Message(header_file, 0)
72 msg = mimetools.Message(header_file, 0)
73 req.header(msg.items())
73 req.header(msg.items())
74 yield header_file.read()
74 yield header_file.read()
75
75
76 def footer(**map):
76 def footer(**map):
77 yield tmpl("footer", **map)
77 yield tmpl("footer", **map)
78
78
79 def motd(**map):
79 def motd(**map):
80 if self.motd is not None:
80 if self.motd is not None:
81 yield self.motd
81 yield self.motd
82 else:
82 else:
83 yield config('web', 'motd', '')
83 yield config('web', 'motd', '')
84
84
85 parentui = self.parentui or ui.ui(report_untrusted=False)
85 parentui = self.parentui or ui.ui(report_untrusted=False)
86
86
87 def config(section, name, default=None, untrusted=True):
87 def config(section, name, default=None, untrusted=True):
88 return parentui.config(section, name, default, untrusted)
88 return parentui.config(section, name, default, untrusted)
89
89
90 url = req.env['REQUEST_URI'].split('?')[0]
90 url = req.env['REQUEST_URI'].split('?')[0]
91 if not url.endswith('/'):
91 if not url.endswith('/'):
92 url += '/'
92 url += '/'
93 pathinfo = req.env.get('PATH_INFO', '').strip('/') + '/'
93 pathinfo = req.env.get('PATH_INFO', '').strip('/') + '/'
94 base = url[:len(url) - len(pathinfo)]
94 base = url[:len(url) - len(pathinfo)]
95 if not base.endswith('/'):
95 if not base.endswith('/'):
96 base += '/'
96 base += '/'
97
97
98 staticurl = config('web', 'staticurl') or base + 'static/'
98 staticurl = config('web', 'staticurl') or base + 'static/'
99 if not staticurl.endswith('/'):
99 if not staticurl.endswith('/'):
100 staticurl += '/'
100 staticurl += '/'
101
101
102 style = self.style
102 style = self.style
103 if style is None:
103 if style is None:
104 style = config('web', 'style', '')
104 style = config('web', 'style', '')
105 if req.form.has_key('style'):
105 if req.form.has_key('style'):
106 style = req.form['style'][0]
106 style = req.form['style'][0]
107 if self.stripecount is None:
107 if self.stripecount is None:
108 self.stripecount = int(config('web', 'stripes', 1))
108 self.stripecount = int(config('web', 'stripes', 1))
109 mapfile = style_map(templater.templatepath(), style)
109 mapfile = style_map(templater.templatepath(), style)
110 tmpl = templater.templater(mapfile, templater.common_filters,
110 tmpl = templater.templater(mapfile, templater.common_filters,
111 defaults={"header": header,
111 defaults={"header": header,
112 "footer": footer,
112 "footer": footer,
113 "motd": motd,
113 "motd": motd,
114 "url": url,
114 "url": url,
115 "staticurl": staticurl})
115 "staticurl": staticurl})
116
116
117 def archivelist(ui, nodeid, url):
117 def archivelist(ui, nodeid, url):
118 allowed = ui.configlist("web", "allow_archive", untrusted=True)
118 allowed = ui.configlist("web", "allow_archive", untrusted=True)
119 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
119 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
120 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
120 if i[0] in allowed or ui.configbool("web", "allow" + i[0],
121 untrusted=True):
121 untrusted=True):
122 yield {"type" : i[0], "extension": i[1],
122 yield {"type" : i[0], "extension": i[1],
123 "node": nodeid, "url": url}
123 "node": nodeid, "url": url}
124
124
125 def entries(sortcolumn="", descending=False, subdir="", **map):
125 def entries(sortcolumn="", descending=False, subdir="", **map):
126 def sessionvars(**map):
126 def sessionvars(**map):
127 fields = []
127 fields = []
128 if req.form.has_key('style'):
128 if req.form.has_key('style'):
129 style = req.form['style'][0]
129 style = req.form['style'][0]
130 if style != get('web', 'style', ''):
130 if style != get('web', 'style', ''):
131 fields.append(('style', style))
131 fields.append(('style', style))
132
132
133 separator = url[-1] == '?' and ';' or '?'
133 separator = url[-1] == '?' and ';' or '?'
134 for name, value in fields:
134 for name, value in fields:
135 yield dict(name=name, value=value, separator=separator)
135 yield dict(name=name, value=value, separator=separator)
136 separator = ';'
136 separator = ';'
137
137
138 rows = []
138 rows = []
139 parity = paritygen(self.stripecount)
139 parity = paritygen(self.stripecount)
140 for name, path in self.repos:
140 for name, path in self.repos:
141 if not name.startswith(subdir):
141 if not name.startswith(subdir):
142 continue
142 continue
143 name = name[len(subdir):]
143 name = name[len(subdir):]
144
144
145 u = ui.ui(parentui=parentui)
145 u = ui.ui(parentui=parentui)
146 try:
146 try:
147 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
147 u.readconfig(os.path.join(path, '.hg', 'hgrc'))
148 except IOError:
148 except IOError:
149 pass
149 pass
150 def get(section, name, default=None):
150 def get(section, name, default=None):
151 return u.config(section, name, default, untrusted=True)
151 return u.config(section, name, default, untrusted=True)
152
152
153 if u.configbool("web", "hidden", untrusted=True):
153 if u.configbool("web", "hidden", untrusted=True):
154 continue
154 continue
155
155
156 url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name])
156 url = ('/'.join([req.env["REQUEST_URI"].split('?')[0], name])
157 .replace("//", "/")) + '/'
157 .replace("//", "/")) + '/'
158
158
159 # update time with local timezone
159 # update time with local timezone
160 try:
160 try:
161 d = (get_mtime(path), util.makedate()[1])
161 d = (get_mtime(path), util.makedate()[1])
162 except OSError:
162 except OSError:
163 continue
163 continue
164
164
165 contact = (get("ui", "username") or # preferred
165 contact = (get("ui", "username") or # preferred
166 get("web", "contact") or # deprecated
166 get("web", "contact") or # deprecated
167 get("web", "author", "")) # also
167 get("web", "author", "")) # also
168 description = get("web", "description", "")
168 description = get("web", "description", "")
169 name = get("web", "name", name)
169 name = get("web", "name", name)
170 row = dict(contact=contact or "unknown",
170 row = dict(contact=contact or "unknown",
171 contact_sort=contact.upper() or "unknown",
171 contact_sort=contact.upper() or "unknown",
172 name=name,
172 name=name,
173 name_sort=name,
173 name_sort=name,
174 url=url,
174 url=url,
175 description=description or "unknown",
175 description=description or "unknown",
176 description_sort=description.upper() or "unknown",
176 description_sort=description.upper() or "unknown",
177 lastchange=d,
177 lastchange=d,
178 lastchange_sort=d[1]-d[0],
178 lastchange_sort=d[1]-d[0],
179 sessionvars=sessionvars,
179 sessionvars=sessionvars,
180 archives=archivelist(u, "tip", url))
180 archives=archivelist(u, "tip", url))
181 if (not sortcolumn
181 if (not sortcolumn
182 or (sortcolumn, descending) == self.repos_sorted):
182 or (sortcolumn, descending) == self.repos_sorted):
183 # fast path for unsorted output
183 # fast path for unsorted output
184 row['parity'] = parity.next()
184 row['parity'] = parity.next()
185 yield row
185 yield row
186 else:
186 else:
187 rows.append((row["%s_sort" % sortcolumn], row))
187 rows.append((row["%s_sort" % sortcolumn], row))
188 if rows:
188 if rows:
189 rows.sort()
189 rows.sort()
190 if descending:
190 if descending:
191 rows.reverse()
191 rows.reverse()
192 for key, row in rows:
192 for key, row in rows:
193 row['parity'] = parity.next()
193 row['parity'] = parity.next()
194 yield row
194 yield row
195
195
196 def makeindex(req, subdir=""):
196 def makeindex(req, subdir=""):
197 sortable = ["name", "description", "contact", "lastchange"]
197 sortable = ["name", "description", "contact", "lastchange"]
198 sortcolumn, descending = self.repos_sorted
198 sortcolumn, descending = self.repos_sorted
199 if req.form.has_key('sort'):
199 if req.form.has_key('sort'):
200 sortcolumn = req.form['sort'][0]
200 sortcolumn = req.form['sort'][0]
201 descending = sortcolumn.startswith('-')
201 descending = sortcolumn.startswith('-')
202 if descending:
202 if descending:
203 sortcolumn = sortcolumn[1:]
203 sortcolumn = sortcolumn[1:]
204 if sortcolumn not in sortable:
204 if sortcolumn not in sortable:
205 sortcolumn = ""
205 sortcolumn = ""
206
206
207 sort = [("sort_%s" % column,
207 sort = [("sort_%s" % column,
208 "%s%s" % ((not descending and column == sortcolumn)
208 "%s%s" % ((not descending and column == sortcolumn)
209 and "-" or "", column))
209 and "-" or "", column))
210 for column in sortable]
210 for column in sortable]
211 req.write(tmpl("index", entries=entries, subdir=subdir,
211 req.write(tmpl("index", entries=entries, subdir=subdir,
212 sortcolumn=sortcolumn, descending=descending,
212 sortcolumn=sortcolumn, descending=descending,
213 **dict(sort)))
213 **dict(sort)))
214
214
215 try:
215 try:
216 virtual = req.env.get("PATH_INFO", "").strip('/')
216 virtual = req.env.get("PATH_INFO", "").strip('/')
217 if virtual.startswith('static/'):
217 if virtual.startswith('static/'):
218 static = os.path.join(templater.templatepath(), 'static')
218 static = os.path.join(templater.templatepath(), 'static')
219 fname = virtual[7:]
219 fname = virtual[7:]
220 req.write(staticfile(static, fname, req) or
220 req.write(staticfile(static, fname, req) or
221 tmpl('error', error='%r not found' % fname))
221 tmpl('error', error='%r not found' % fname))
222 elif virtual:
222 elif virtual:
223 repos = dict(self.repos)
223 repos = dict(self.repos)
224 while virtual:
224 while virtual:
225 real = repos.get(virtual)
225 real = repos.get(virtual)
226 if real:
226 if real:
227 req.env['REPO_NAME'] = virtual
227 req.env['REPO_NAME'] = virtual
228 try:
228 try:
229 repo = hg.repository(parentui, real)
229 repo = hg.repository(parentui, real)
230 hgweb(repo).run_wsgi(req)
230 hgweb(repo).run_wsgi(req)
231 except IOError, inst:
231 except IOError, inst:
232 req.write(tmpl("error", error=inst.strerror))
232 req.write(tmpl("error", error=inst.strerror))
233 except hg.RepoError, inst:
233 except hg.RepoError, inst:
234 req.write(tmpl("error", error=str(inst)))
234 req.write(tmpl("error", error=str(inst)))
235 return
235 return
236
236
237 # browse subdirectories
237 # browse subdirectories
238 subdir = virtual + '/'
238 subdir = virtual + '/'
239 if [r for r in repos if r.startswith(subdir)]:
239 if [r for r in repos if r.startswith(subdir)]:
240 makeindex(req, subdir)
240 makeindex(req, subdir)
241 return
241 return
242
242
243 up = virtual.rfind('/')
243 up = virtual.rfind('/')
244 if up < 0:
244 if up < 0:
245 break
245 break
246 virtual = virtual[:up]
246 virtual = virtual[:up]
247
247
248 req.write(tmpl("notfound", repo=virtual))
248 req.write(tmpl("notfound", repo=virtual))
249 else:
249 else:
250 if req.form.has_key('static'):
250 if req.form.has_key('static'):
251 static = os.path.join(templater.templatepath(), "static")
251 static = os.path.join(templater.templatepath(), "static")
252 fname = req.form['static'][0]
252 fname = req.form['static'][0]
253 req.write(staticfile(static, fname, req)
253 req.write(staticfile(static, fname, req)
254 or tmpl("error", error="%r not found" % fname))
254 or tmpl("error", error="%r not found" % fname))
255 else:
255 else:
256 makeindex(req)
256 makeindex(req)
257 finally:
257 finally:
258 tmpl = None
258 tmpl = None
@@ -1,289 +1,289 b''
1 # hgweb/server.py - The standalone hg web server.
1 # hgweb/server.py - The standalone hg web server.
2 #
2 #
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
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, sys, errno, urllib, BaseHTTPServer, socket, SocketServer, traceback
9 import os, sys, errno, urllib, BaseHTTPServer, socket, SocketServer, traceback
10 from mercurial import ui, hg, util, templater
10 from mercurial import ui, hg, util, templater
11 from hgweb_mod import hgweb
11 from hgweb_mod import hgweb
12 from hgwebdir_mod import hgwebdir
12 from hgwebdir_mod import hgwebdir
13 from request import wsgiapplication
13 from request import wsgiapplication
14 from mercurial.i18n import gettext as _
14 from mercurial.i18n import gettext as _
15
15
16 def _splitURI(uri):
16 def _splitURI(uri):
17 """ Return path and query splited from uri
17 """ Return path and query splited from uri
18
18
19 Just like CGI environment, the path is unquoted, the query is
19 Just like CGI environment, the path is unquoted, the query is
20 not.
20 not.
21 """
21 """
22 if '?' in uri:
22 if '?' in uri:
23 path, query = uri.split('?', 1)
23 path, query = uri.split('?', 1)
24 else:
24 else:
25 path, query = uri, ''
25 path, query = uri, ''
26 return urllib.unquote(path), query
26 return urllib.unquote(path), query
27
27
28 class _error_logger(object):
28 class _error_logger(object):
29 def __init__(self, handler):
29 def __init__(self, handler):
30 self.handler = handler
30 self.handler = handler
31 def flush(self):
31 def flush(self):
32 pass
32 pass
33 def write(self, str):
33 def write(self, str):
34 self.writelines(str.split('\n'))
34 self.writelines(str.split('\n'))
35 def writelines(self, seq):
35 def writelines(self, seq):
36 for msg in seq:
36 for msg in seq:
37 self.handler.log_error("HG error: %s", msg)
37 self.handler.log_error("HG error: %s", msg)
38
38
39 class _hgwebhandler(object, BaseHTTPServer.BaseHTTPRequestHandler):
39 class _hgwebhandler(object, BaseHTTPServer.BaseHTTPRequestHandler):
40
40
41 url_scheme = 'http'
41 url_scheme = 'http'
42
42
43 def __init__(self, *args, **kargs):
43 def __init__(self, *args, **kargs):
44 self.protocol_version = 'HTTP/1.1'
44 self.protocol_version = 'HTTP/1.1'
45 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kargs)
45 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kargs)
46
46
47 def log_error(self, format, *args):
47 def log_error(self, format, *args):
48 errorlog = self.server.errorlog
48 errorlog = self.server.errorlog
49 errorlog.write("%s - - [%s] %s\n" % (self.client_address[0],
49 errorlog.write("%s - - [%s] %s\n" % (self.client_address[0],
50 self.log_date_time_string(),
50 self.log_date_time_string(),
51 format % args))
51 format % args))
52
52
53 def log_message(self, format, *args):
53 def log_message(self, format, *args):
54 accesslog = self.server.accesslog
54 accesslog = self.server.accesslog
55 accesslog.write("%s - - [%s] %s\n" % (self.client_address[0],
55 accesslog.write("%s - - [%s] %s\n" % (self.client_address[0],
56 self.log_date_time_string(),
56 self.log_date_time_string(),
57 format % args))
57 format % args))
58
58
59 def do_write(self):
59 def do_write(self):
60 try:
60 try:
61 self.do_hgweb()
61 self.do_hgweb()
62 except socket.error, inst:
62 except socket.error, inst:
63 if inst[0] != errno.EPIPE:
63 if inst[0] != errno.EPIPE:
64 raise
64 raise
65
65
66 def do_POST(self):
66 def do_POST(self):
67 try:
67 try:
68 self.do_write()
68 self.do_write()
69 except StandardError, inst:
69 except StandardError, inst:
70 self._start_response("500 Internal Server Error", [])
70 self._start_response("500 Internal Server Error", [])
71 self._write("Internal Server Error")
71 self._write("Internal Server Error")
72 tb = "".join(traceback.format_exception(*sys.exc_info()))
72 tb = "".join(traceback.format_exception(*sys.exc_info()))
73 self.log_error("Exception happened during processing request '%s':\n%s",
73 self.log_error("Exception happened during processing request '%s':\n%s",
74 self.path, tb)
74 self.path, tb)
75
75
76 def do_GET(self):
76 def do_GET(self):
77 self.do_POST()
77 self.do_POST()
78
78
79 def do_hgweb(self):
79 def do_hgweb(self):
80 path_info, query = _splitURI(self.path)
80 path_info, query = _splitURI(self.path)
81
81
82 env = {}
82 env = {}
83 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
83 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
84 env['REQUEST_METHOD'] = self.command
84 env['REQUEST_METHOD'] = self.command
85 env['SERVER_NAME'] = self.server.server_name
85 env['SERVER_NAME'] = self.server.server_name
86 env['SERVER_PORT'] = str(self.server.server_port)
86 env['SERVER_PORT'] = str(self.server.server_port)
87 env['REQUEST_URI'] = self.path
87 env['REQUEST_URI'] = self.path
88 env['PATH_INFO'] = path_info
88 env['PATH_INFO'] = path_info
89 env['REMOTE_HOST'] = self.client_address[0]
89 env['REMOTE_HOST'] = self.client_address[0]
90 env['REMOTE_ADDR'] = self.client_address[0]
90 env['REMOTE_ADDR'] = self.client_address[0]
91 if query:
91 if query:
92 env['QUERY_STRING'] = query
92 env['QUERY_STRING'] = query
93
93
94 if self.headers.typeheader is None:
94 if self.headers.typeheader is None:
95 env['CONTENT_TYPE'] = self.headers.type
95 env['CONTENT_TYPE'] = self.headers.type
96 else:
96 else:
97 env['CONTENT_TYPE'] = self.headers.typeheader
97 env['CONTENT_TYPE'] = self.headers.typeheader
98 length = self.headers.getheader('content-length')
98 length = self.headers.getheader('content-length')
99 if length:
99 if length:
100 env['CONTENT_LENGTH'] = length
100 env['CONTENT_LENGTH'] = length
101 for header in [h for h in self.headers.keys()
101 for header in [h for h in self.headers.keys()
102 if h not in ('content-type', 'content-length')]:
102 if h not in ('content-type', 'content-length')]:
103 hkey = 'HTTP_' + header.replace('-', '_').upper()
103 hkey = 'HTTP_' + header.replace('-', '_').upper()
104 hval = self.headers.getheader(header)
104 hval = self.headers.getheader(header)
105 hval = hval.replace('\n', '').strip()
105 hval = hval.replace('\n', '').strip()
106 if hval:
106 if hval:
107 env[hkey] = hval
107 env[hkey] = hval
108 env['SERVER_PROTOCOL'] = self.request_version
108 env['SERVER_PROTOCOL'] = self.request_version
109 env['wsgi.version'] = (1, 0)
109 env['wsgi.version'] = (1, 0)
110 env['wsgi.url_scheme'] = self.url_scheme
110 env['wsgi.url_scheme'] = self.url_scheme
111 env['wsgi.input'] = self.rfile
111 env['wsgi.input'] = self.rfile
112 env['wsgi.errors'] = _error_logger(self)
112 env['wsgi.errors'] = _error_logger(self)
113 env['wsgi.multithread'] = isinstance(self.server,
113 env['wsgi.multithread'] = isinstance(self.server,
114 SocketServer.ThreadingMixIn)
114 SocketServer.ThreadingMixIn)
115 env['wsgi.multiprocess'] = isinstance(self.server,
115 env['wsgi.multiprocess'] = isinstance(self.server,
116 SocketServer.ForkingMixIn)
116 SocketServer.ForkingMixIn)
117 env['wsgi.run_once'] = 0
117 env['wsgi.run_once'] = 0
118
118
119 self.close_connection = True
119 self.close_connection = True
120 self.saved_status = None
120 self.saved_status = None
121 self.saved_headers = []
121 self.saved_headers = []
122 self.sent_headers = False
122 self.sent_headers = False
123 self.length = None
123 self.length = None
124 req = self.server.reqmaker(env, self._start_response)
124 req = self.server.reqmaker(env, self._start_response)
125 for data in req:
125 for data in req:
126 if data:
126 if data:
127 self._write(data)
127 self._write(data)
128
128
129 def send_headers(self):
129 def send_headers(self):
130 if not self.saved_status:
130 if not self.saved_status:
131 raise AssertionError("Sending headers before start_response() called")
131 raise AssertionError("Sending headers before start_response() called")
132 saved_status = self.saved_status.split(None, 1)
132 saved_status = self.saved_status.split(None, 1)
133 saved_status[0] = int(saved_status[0])
133 saved_status[0] = int(saved_status[0])
134 self.send_response(*saved_status)
134 self.send_response(*saved_status)
135 should_close = True
135 should_close = True
136 for h in self.saved_headers:
136 for h in self.saved_headers:
137 self.send_header(*h)
137 self.send_header(*h)
138 if h[0].lower() == 'content-length':
138 if h[0].lower() == 'content-length':
139 should_close = False
139 should_close = False
140 self.length = int(h[1])
140 self.length = int(h[1])
141 # The value of the Connection header is a list of case-insensitive
141 # The value of the Connection header is a list of case-insensitive
142 # tokens separated by commas and optional whitespace.
142 # tokens separated by commas and optional whitespace.
143 if 'close' in [token.strip().lower() for token in
143 if 'close' in [token.strip().lower() for token in
144 self.headers.get('connection', '').split(',')]:
144 self.headers.get('connection', '').split(',')]:
145 should_close = True
145 should_close = True
146 if should_close:
146 if should_close:
147 self.send_header('Connection', 'close')
147 self.send_header('Connection', 'close')
148 self.close_connection = should_close
148 self.close_connection = should_close
149 self.end_headers()
149 self.end_headers()
150 self.sent_headers = True
150 self.sent_headers = True
151
151
152 def _start_response(self, http_status, headers, exc_info=None):
152 def _start_response(self, http_status, headers, exc_info=None):
153 code, msg = http_status.split(None, 1)
153 code, msg = http_status.split(None, 1)
154 code = int(code)
154 code = int(code)
155 self.saved_status = http_status
155 self.saved_status = http_status
156 bad_headers = ('connection', 'transfer-encoding')
156 bad_headers = ('connection', 'transfer-encoding')
157 self.saved_headers = [h for h in headers
157 self.saved_headers = [h for h in headers
158 if h[0].lower() not in bad_headers]
158 if h[0].lower() not in bad_headers]
159 return self._write
159 return self._write
160
160
161 def _write(self, data):
161 def _write(self, data):
162 if not self.saved_status:
162 if not self.saved_status:
163 raise AssertionError("data written before start_response() called")
163 raise AssertionError("data written before start_response() called")
164 elif not self.sent_headers:
164 elif not self.sent_headers:
165 self.send_headers()
165 self.send_headers()
166 if self.length is not None:
166 if self.length is not None:
167 if len(data) > self.length:
167 if len(data) > self.length:
168 raise AssertionError("Content-length header sent, but more bytes than specified are being written.")
168 raise AssertionError("Content-length header sent, but more bytes than specified are being written.")
169 self.length = self.length - len(data)
169 self.length = self.length - len(data)
170 self.wfile.write(data)
170 self.wfile.write(data)
171 self.wfile.flush()
171 self.wfile.flush()
172
172
173 class _shgwebhandler(_hgwebhandler):
173 class _shgwebhandler(_hgwebhandler):
174
174
175 url_scheme = 'https'
175 url_scheme = 'https'
176
176
177 def setup(self):
177 def setup(self):
178 self.connection = self.request
178 self.connection = self.request
179 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
179 self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
180 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
180 self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
181
181
182 def do_write(self):
182 def do_write(self):
183 from OpenSSL.SSL import SysCallError
183 from OpenSSL.SSL import SysCallError
184 try:
184 try:
185 super(_shgwebhandler, self).do_write()
185 super(_shgwebhandler, self).do_write()
186 except SysCallError, inst:
186 except SysCallError, inst:
187 if inst.args[0] != errno.EPIPE:
187 if inst.args[0] != errno.EPIPE:
188 raise
188 raise
189
189
190 def handle_one_request(self):
190 def handle_one_request(self):
191 from OpenSSL.SSL import SysCallError, ZeroReturnError
191 from OpenSSL.SSL import SysCallError, ZeroReturnError
192 try:
192 try:
193 super(_shgwebhandler, self).handle_one_request()
193 super(_shgwebhandler, self).handle_one_request()
194 except (SysCallError, ZeroReturnError):
194 except (SysCallError, ZeroReturnError):
195 self.close_connection = True
195 self.close_connection = True
196 pass
196 pass
197
197
198 def create_server(ui, repo):
198 def create_server(ui, repo):
199 use_threads = True
199 use_threads = True
200
200
201 def openlog(opt, default):
201 def openlog(opt, default):
202 if opt and opt != '-':
202 if opt and opt != '-':
203 return open(opt, 'w')
203 return open(opt, 'w')
204 return default
204 return default
205
205
206 address = repo.ui.config("web", "address", "")
206 address = repo.ui.config("web", "address", "")
207 port = int(repo.ui.config("web", "port", 8000))
207 port = int(repo.ui.config("web", "port", 8000))
208 use_ipv6 = repo.ui.configbool("web", "ipv6")
208 use_ipv6 = repo.ui.configbool("web", "ipv6")
209 webdir_conf = repo.ui.config("web", "webdir_conf")
209 webdir_conf = repo.ui.config("web", "webdir_conf")
210 ssl_cert = repo.ui.config("web", "certificate")
210 ssl_cert = repo.ui.config("web", "certificate")
211 accesslog = openlog(repo.ui.config("web", "accesslog", "-"), sys.stdout)
211 accesslog = openlog(repo.ui.config("web", "accesslog", "-"), sys.stdout)
212 errorlog = openlog(repo.ui.config("web", "errorlog", "-"), sys.stderr)
212 errorlog = openlog(repo.ui.config("web", "errorlog", "-"), sys.stderr)
213
213
214 if use_threads:
214 if use_threads:
215 try:
215 try:
216 from threading import activeCount
216 from threading import activeCount
217 except ImportError:
217 except ImportError:
218 use_threads = False
218 use_threads = False
219
219
220 if use_threads:
220 if use_threads:
221 _mixin = SocketServer.ThreadingMixIn
221 _mixin = SocketServer.ThreadingMixIn
222 else:
222 else:
223 if hasattr(os, "fork"):
223 if hasattr(os, "fork"):
224 _mixin = SocketServer.ForkingMixIn
224 _mixin = SocketServer.ForkingMixIn
225 else:
225 else:
226 class _mixin:
226 class _mixin:
227 pass
227 pass
228
228
229 class MercurialHTTPServer(object, _mixin, BaseHTTPServer.HTTPServer):
229 class MercurialHTTPServer(object, _mixin, BaseHTTPServer.HTTPServer):
230
230
231 # SO_REUSEADDR has broken semantics on windows
231 # SO_REUSEADDR has broken semantics on windows
232 if os.name == 'nt':
232 if os.name == 'nt':
233 allow_reuse_address = 0
233 allow_reuse_address = 0
234
234
235 def __init__(self, *args, **kargs):
235 def __init__(self, *args, **kargs):
236 BaseHTTPServer.HTTPServer.__init__(self, *args, **kargs)
236 BaseHTTPServer.HTTPServer.__init__(self, *args, **kargs)
237 self.accesslog = accesslog
237 self.accesslog = accesslog
238 self.errorlog = errorlog
238 self.errorlog = errorlog
239 self.daemon_threads = True
239 self.daemon_threads = True
240 def make_handler():
240 def make_handler():
241 if webdir_conf:
241 if webdir_conf:
242 hgwebobj = hgwebdir(webdir_conf, ui)
242 hgwebobj = hgwebdir(webdir_conf, ui)
243 elif repo is not None:
243 elif repo is not None:
244 hgwebobj = hgweb(hg.repository(repo.ui, repo.root))
244 hgwebobj = hgweb(hg.repository(repo.ui, repo.root))
245 else:
245 else:
246 raise hg.RepoError(_("There is no Mercurial repository here"
246 raise hg.RepoError(_("There is no Mercurial repository here"
247 " (.hg not found)"))
247 " (.hg not found)"))
248 return hgwebobj
248 return hgwebobj
249 self.reqmaker = wsgiapplication(make_handler)
249 self.reqmaker = wsgiapplication(make_handler)
250
250
251 addr = address
251 addr = address
252 if addr in ('', '::'):
252 if addr in ('', '::'):
253 addr = socket.gethostname()
253 addr = socket.gethostname()
254
254
255 self.addr, self.port = addr, port
255 self.addr, self.port = addr, port
256
256
257 if ssl_cert:
257 if ssl_cert:
258 try:
258 try:
259 from OpenSSL import SSL
259 from OpenSSL import SSL
260 ctx = SSL.Context(SSL.SSLv23_METHOD)
260 ctx = SSL.Context(SSL.SSLv23_METHOD)
261 except ImportError:
261 except ImportError:
262 raise util.Abort("SSL support is unavailable")
262 raise util.Abort("SSL support is unavailable")
263 ctx.use_privatekey_file(ssl_cert)
263 ctx.use_privatekey_file(ssl_cert)
264 ctx.use_certificate_file(ssl_cert)
264 ctx.use_certificate_file(ssl_cert)
265 sock = socket.socket(self.address_family, self.socket_type)
265 sock = socket.socket(self.address_family, self.socket_type)
266 self.socket = SSL.Connection(ctx, sock)
266 self.socket = SSL.Connection(ctx, sock)
267 self.server_bind()
267 self.server_bind()
268 self.server_activate()
268 self.server_activate()
269
269
270 class IPv6HTTPServer(MercurialHTTPServer):
270 class IPv6HTTPServer(MercurialHTTPServer):
271 address_family = getattr(socket, 'AF_INET6', None)
271 address_family = getattr(socket, 'AF_INET6', None)
272
272
273 def __init__(self, *args, **kwargs):
273 def __init__(self, *args, **kwargs):
274 if self.address_family is None:
274 if self.address_family is None:
275 raise hg.RepoError(_('IPv6 not available on this system'))
275 raise hg.RepoError(_('IPv6 not available on this system'))
276 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
276 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
277
277
278 if ssl_cert:
278 if ssl_cert:
279 handler = _shgwebhandler
279 handler = _shgwebhandler
280 else:
280 else:
281 handler = _hgwebhandler
281 handler = _hgwebhandler
282
282
283 try:
283 try:
284 if use_ipv6:
284 if use_ipv6:
285 return IPv6HTTPServer((address, port), handler)
285 return IPv6HTTPServer((address, port), handler)
286 else:
286 else:
287 return MercurialHTTPServer((address, port), handler)
287 return MercurialHTTPServer((address, port), handler)
288 except socket.error, inst:
288 except socket.error, inst:
289 raise util.Abort(_('cannot start server: %s') % inst.args[1])
289 raise util.Abort(_('cannot start server: %s') % inst.args[1])
General Comments 0
You need to be logged in to leave comments. Login now