##// END OF EJS Templates
util: drop alias for collections.deque...
Martin von Zweigbergk -
r25113:0ca8410e default
parent child Browse files
Show More
@@ -1,742 +1,743 b''
1 # shelve.py - save/restore working directory state
1 # shelve.py - save/restore working directory state
2 #
2 #
3 # Copyright 2013 Facebook, Inc.
3 # Copyright 2013 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """save and restore changes to the working directory
8 """save and restore changes to the working directory
9
9
10 The "hg shelve" command saves changes made to the working directory
10 The "hg shelve" command saves changes made to the working directory
11 and reverts those changes, resetting the working directory to a clean
11 and reverts those changes, resetting the working directory to a clean
12 state.
12 state.
13
13
14 Later on, the "hg unshelve" command restores the changes saved by "hg
14 Later on, the "hg unshelve" command restores the changes saved by "hg
15 shelve". Changes can be restored even after updating to a different
15 shelve". Changes can be restored even after updating to a different
16 parent, in which case Mercurial's merge machinery will resolve any
16 parent, in which case Mercurial's merge machinery will resolve any
17 conflicts if necessary.
17 conflicts if necessary.
18
18
19 You can have more than one shelved change outstanding at a time; each
19 You can have more than one shelved change outstanding at a time; each
20 shelved change has a distinct name. For details, see the help for "hg
20 shelved change has a distinct name. For details, see the help for "hg
21 shelve".
21 shelve".
22 """
22 """
23
23
24 import collections
24 from mercurial.i18n import _
25 from mercurial.i18n import _
25 from mercurial.node import nullid, nullrev, bin, hex
26 from mercurial.node import nullid, nullrev, bin, hex
26 from mercurial import changegroup, cmdutil, scmutil, phases, commands
27 from mercurial import changegroup, cmdutil, scmutil, phases, commands
27 from mercurial import error, hg, mdiff, merge, patch, repair, util
28 from mercurial import error, hg, mdiff, merge, patch, repair, util
28 from mercurial import templatefilters, exchange, bundlerepo
29 from mercurial import templatefilters, exchange, bundlerepo
29 from mercurial import lock as lockmod
30 from mercurial import lock as lockmod
30 from hgext import rebase
31 from hgext import rebase
31 import errno
32 import errno
32
33
33 cmdtable = {}
34 cmdtable = {}
34 command = cmdutil.command(cmdtable)
35 command = cmdutil.command(cmdtable)
35 testedwith = 'internal'
36 testedwith = 'internal'
36
37
37 class shelvedfile(object):
38 class shelvedfile(object):
38 """Helper for the file storing a single shelve
39 """Helper for the file storing a single shelve
39
40
40 Handles common functions on shelve files (.hg/.patch) using
41 Handles common functions on shelve files (.hg/.patch) using
41 the vfs layer"""
42 the vfs layer"""
42 def __init__(self, repo, name, filetype=None):
43 def __init__(self, repo, name, filetype=None):
43 self.repo = repo
44 self.repo = repo
44 self.name = name
45 self.name = name
45 self.vfs = scmutil.vfs(repo.join('shelved'))
46 self.vfs = scmutil.vfs(repo.join('shelved'))
46 self.ui = self.repo.ui
47 self.ui = self.repo.ui
47 if filetype:
48 if filetype:
48 self.fname = name + '.' + filetype
49 self.fname = name + '.' + filetype
49 else:
50 else:
50 self.fname = name
51 self.fname = name
51
52
52 def exists(self):
53 def exists(self):
53 return self.vfs.exists(self.fname)
54 return self.vfs.exists(self.fname)
54
55
55 def filename(self):
56 def filename(self):
56 return self.vfs.join(self.fname)
57 return self.vfs.join(self.fname)
57
58
58 def unlink(self):
59 def unlink(self):
59 util.unlink(self.filename())
60 util.unlink(self.filename())
60
61
61 def stat(self):
62 def stat(self):
62 return self.vfs.stat(self.fname)
63 return self.vfs.stat(self.fname)
63
64
64 def opener(self, mode='rb'):
65 def opener(self, mode='rb'):
65 try:
66 try:
66 return self.vfs(self.fname, mode)
67 return self.vfs(self.fname, mode)
67 except IOError, err:
68 except IOError, err:
68 if err.errno != errno.ENOENT:
69 if err.errno != errno.ENOENT:
69 raise
70 raise
70 raise util.Abort(_("shelved change '%s' not found") % self.name)
71 raise util.Abort(_("shelved change '%s' not found") % self.name)
71
72
72 def applybundle(self):
73 def applybundle(self):
73 fp = self.opener()
74 fp = self.opener()
74 try:
75 try:
75 gen = exchange.readbundle(self.repo.ui, fp, self.fname, self.vfs)
76 gen = exchange.readbundle(self.repo.ui, fp, self.fname, self.vfs)
76 changegroup.addchangegroup(self.repo, gen, 'unshelve',
77 changegroup.addchangegroup(self.repo, gen, 'unshelve',
77 'bundle:' + self.vfs.join(self.fname),
78 'bundle:' + self.vfs.join(self.fname),
78 targetphase=phases.secret)
79 targetphase=phases.secret)
79 finally:
80 finally:
80 fp.close()
81 fp.close()
81
82
82 def bundlerepo(self):
83 def bundlerepo(self):
83 return bundlerepo.bundlerepository(self.repo.baseui, self.repo.root,
84 return bundlerepo.bundlerepository(self.repo.baseui, self.repo.root,
84 self.vfs.join(self.fname))
85 self.vfs.join(self.fname))
85 def writebundle(self, cg):
86 def writebundle(self, cg):
86 changegroup.writebundle(self.ui, cg, self.fname, 'HG10UN', self.vfs)
87 changegroup.writebundle(self.ui, cg, self.fname, 'HG10UN', self.vfs)
87
88
88 class shelvedstate(object):
89 class shelvedstate(object):
89 """Handle persistence during unshelving operations.
90 """Handle persistence during unshelving operations.
90
91
91 Handles saving and restoring a shelved state. Ensures that different
92 Handles saving and restoring a shelved state. Ensures that different
92 versions of a shelved state are possible and handles them appropriately.
93 versions of a shelved state are possible and handles them appropriately.
93 """
94 """
94 _version = 1
95 _version = 1
95 _filename = 'shelvedstate'
96 _filename = 'shelvedstate'
96
97
97 @classmethod
98 @classmethod
98 def load(cls, repo):
99 def load(cls, repo):
99 fp = repo.vfs(cls._filename)
100 fp = repo.vfs(cls._filename)
100 try:
101 try:
101 version = int(fp.readline().strip())
102 version = int(fp.readline().strip())
102
103
103 if version != cls._version:
104 if version != cls._version:
104 raise util.Abort(_('this version of shelve is incompatible '
105 raise util.Abort(_('this version of shelve is incompatible '
105 'with the version used in this repo'))
106 'with the version used in this repo'))
106 name = fp.readline().strip()
107 name = fp.readline().strip()
107 wctx = fp.readline().strip()
108 wctx = fp.readline().strip()
108 pendingctx = fp.readline().strip()
109 pendingctx = fp.readline().strip()
109 parents = [bin(h) for h in fp.readline().split()]
110 parents = [bin(h) for h in fp.readline().split()]
110 stripnodes = [bin(h) for h in fp.readline().split()]
111 stripnodes = [bin(h) for h in fp.readline().split()]
111 finally:
112 finally:
112 fp.close()
113 fp.close()
113
114
114 obj = cls()
115 obj = cls()
115 obj.name = name
116 obj.name = name
116 obj.wctx = repo[bin(wctx)]
117 obj.wctx = repo[bin(wctx)]
117 obj.pendingctx = repo[bin(pendingctx)]
118 obj.pendingctx = repo[bin(pendingctx)]
118 obj.parents = parents
119 obj.parents = parents
119 obj.stripnodes = stripnodes
120 obj.stripnodes = stripnodes
120
121
121 return obj
122 return obj
122
123
123 @classmethod
124 @classmethod
124 def save(cls, repo, name, originalwctx, pendingctx, stripnodes):
125 def save(cls, repo, name, originalwctx, pendingctx, stripnodes):
125 fp = repo.vfs(cls._filename, 'wb')
126 fp = repo.vfs(cls._filename, 'wb')
126 fp.write('%i\n' % cls._version)
127 fp.write('%i\n' % cls._version)
127 fp.write('%s\n' % name)
128 fp.write('%s\n' % name)
128 fp.write('%s\n' % hex(originalwctx.node()))
129 fp.write('%s\n' % hex(originalwctx.node()))
129 fp.write('%s\n' % hex(pendingctx.node()))
130 fp.write('%s\n' % hex(pendingctx.node()))
130 fp.write('%s\n' % ' '.join([hex(p) for p in repo.dirstate.parents()]))
131 fp.write('%s\n' % ' '.join([hex(p) for p in repo.dirstate.parents()]))
131 fp.write('%s\n' % ' '.join([hex(n) for n in stripnodes]))
132 fp.write('%s\n' % ' '.join([hex(n) for n in stripnodes]))
132 fp.close()
133 fp.close()
133
134
134 @classmethod
135 @classmethod
135 def clear(cls, repo):
136 def clear(cls, repo):
136 util.unlinkpath(repo.join(cls._filename), ignoremissing=True)
137 util.unlinkpath(repo.join(cls._filename), ignoremissing=True)
137
138
138 def createcmd(ui, repo, pats, opts):
139 def createcmd(ui, repo, pats, opts):
139 """subcommand that creates a new shelve"""
140 """subcommand that creates a new shelve"""
140
141
141 def publicancestors(ctx):
142 def publicancestors(ctx):
142 """Compute the public ancestors of a commit.
143 """Compute the public ancestors of a commit.
143
144
144 Much faster than the revset ancestors(ctx) & draft()"""
145 Much faster than the revset ancestors(ctx) & draft()"""
145 seen = set([nullrev])
146 seen = set([nullrev])
146 visit = util.deque()
147 visit = collections.deque()
147 visit.append(ctx)
148 visit.append(ctx)
148 while visit:
149 while visit:
149 ctx = visit.popleft()
150 ctx = visit.popleft()
150 yield ctx.node()
151 yield ctx.node()
151 for parent in ctx.parents():
152 for parent in ctx.parents():
152 rev = parent.rev()
153 rev = parent.rev()
153 if rev not in seen:
154 if rev not in seen:
154 seen.add(rev)
155 seen.add(rev)
155 if parent.mutable():
156 if parent.mutable():
156 visit.append(parent)
157 visit.append(parent)
157
158
158 wctx = repo[None]
159 wctx = repo[None]
159 parents = wctx.parents()
160 parents = wctx.parents()
160 if len(parents) > 1:
161 if len(parents) > 1:
161 raise util.Abort(_('cannot shelve while merging'))
162 raise util.Abort(_('cannot shelve while merging'))
162 parent = parents[0]
163 parent = parents[0]
163
164
164 # we never need the user, so we use a generic user for all shelve operations
165 # we never need the user, so we use a generic user for all shelve operations
165 user = 'shelve@localhost'
166 user = 'shelve@localhost'
166 label = repo._activebookmark or parent.branch() or 'default'
167 label = repo._activebookmark or parent.branch() or 'default'
167
168
168 # slashes aren't allowed in filenames, therefore we rename it
169 # slashes aren't allowed in filenames, therefore we rename it
169 label = label.replace('/', '_')
170 label = label.replace('/', '_')
170
171
171 def gennames():
172 def gennames():
172 yield label
173 yield label
173 for i in xrange(1, 100):
174 for i in xrange(1, 100):
174 yield '%s-%02d' % (label, i)
175 yield '%s-%02d' % (label, i)
175
176
176 def commitfunc(ui, repo, message, match, opts):
177 def commitfunc(ui, repo, message, match, opts):
177 hasmq = util.safehasattr(repo, 'mq')
178 hasmq = util.safehasattr(repo, 'mq')
178 if hasmq:
179 if hasmq:
179 saved, repo.mq.checkapplied = repo.mq.checkapplied, False
180 saved, repo.mq.checkapplied = repo.mq.checkapplied, False
180 backup = repo.ui.backupconfig('phases', 'new-commit')
181 backup = repo.ui.backupconfig('phases', 'new-commit')
181 try:
182 try:
182 repo.ui. setconfig('phases', 'new-commit', phases.secret)
183 repo.ui. setconfig('phases', 'new-commit', phases.secret)
183 editor = cmdutil.getcommiteditor(editform='shelve.shelve', **opts)
184 editor = cmdutil.getcommiteditor(editform='shelve.shelve', **opts)
184 return repo.commit(message, user, opts.get('date'), match,
185 return repo.commit(message, user, opts.get('date'), match,
185 editor=editor)
186 editor=editor)
186 finally:
187 finally:
187 repo.ui.restoreconfig(backup)
188 repo.ui.restoreconfig(backup)
188 if hasmq:
189 if hasmq:
189 repo.mq.checkapplied = saved
190 repo.mq.checkapplied = saved
190
191
191 if parent.node() != nullid:
192 if parent.node() != nullid:
192 desc = "changes to '%s'" % parent.description().split('\n', 1)[0]
193 desc = "changes to '%s'" % parent.description().split('\n', 1)[0]
193 else:
194 else:
194 desc = '(changes in empty repository)'
195 desc = '(changes in empty repository)'
195
196
196 if not opts['message']:
197 if not opts['message']:
197 opts['message'] = desc
198 opts['message'] = desc
198
199
199 name = opts['name']
200 name = opts['name']
200
201
201 wlock = lock = tr = bms = None
202 wlock = lock = tr = bms = None
202 try:
203 try:
203 wlock = repo.wlock()
204 wlock = repo.wlock()
204 lock = repo.lock()
205 lock = repo.lock()
205
206
206 bms = repo._bookmarks.copy()
207 bms = repo._bookmarks.copy()
207 # use an uncommitted transaction to generate the bundle to avoid
208 # use an uncommitted transaction to generate the bundle to avoid
208 # pull races. ensure we don't print the abort message to stderr.
209 # pull races. ensure we don't print the abort message to stderr.
209 tr = repo.transaction('commit', report=lambda x: None)
210 tr = repo.transaction('commit', report=lambda x: None)
210
211
211 if name:
212 if name:
212 if shelvedfile(repo, name, 'hg').exists():
213 if shelvedfile(repo, name, 'hg').exists():
213 raise util.Abort(_("a shelved change named '%s' already exists")
214 raise util.Abort(_("a shelved change named '%s' already exists")
214 % name)
215 % name)
215 else:
216 else:
216 for n in gennames():
217 for n in gennames():
217 if not shelvedfile(repo, n, 'hg').exists():
218 if not shelvedfile(repo, n, 'hg').exists():
218 name = n
219 name = n
219 break
220 break
220 else:
221 else:
221 raise util.Abort(_("too many shelved changes named '%s'") %
222 raise util.Abort(_("too many shelved changes named '%s'") %
222 label)
223 label)
223
224
224 # ensure we are not creating a subdirectory or a hidden file
225 # ensure we are not creating a subdirectory or a hidden file
225 if '/' in name or '\\' in name:
226 if '/' in name or '\\' in name:
226 raise util.Abort(_('shelved change names may not contain slashes'))
227 raise util.Abort(_('shelved change names may not contain slashes'))
227 if name.startswith('.'):
228 if name.startswith('.'):
228 raise util.Abort(_("shelved change names may not start with '.'"))
229 raise util.Abort(_("shelved change names may not start with '.'"))
229 interactive = opts.get('interactive', False)
230 interactive = opts.get('interactive', False)
230
231
231 def interactivecommitfunc(ui, repo, *pats, **opts):
232 def interactivecommitfunc(ui, repo, *pats, **opts):
232 match = scmutil.match(repo['.'], pats, {})
233 match = scmutil.match(repo['.'], pats, {})
233 message = opts['message']
234 message = opts['message']
234 return commitfunc(ui, repo, message, match, opts)
235 return commitfunc(ui, repo, message, match, opts)
235 if not interactive:
236 if not interactive:
236 node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
237 node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
237 else:
238 else:
238 node = cmdutil.dorecord(ui, repo, interactivecommitfunc, 'commit',
239 node = cmdutil.dorecord(ui, repo, interactivecommitfunc, 'commit',
239 False, cmdutil.recordfilter, *pats, **opts)
240 False, cmdutil.recordfilter, *pats, **opts)
240 if not node:
241 if not node:
241 stat = repo.status(match=scmutil.match(repo[None], pats, opts))
242 stat = repo.status(match=scmutil.match(repo[None], pats, opts))
242 if stat.deleted:
243 if stat.deleted:
243 ui.status(_("nothing changed (%d missing files, see "
244 ui.status(_("nothing changed (%d missing files, see "
244 "'hg status')\n") % len(stat.deleted))
245 "'hg status')\n") % len(stat.deleted))
245 else:
246 else:
246 ui.status(_("nothing changed\n"))
247 ui.status(_("nothing changed\n"))
247 return 1
248 return 1
248
249
249 bases = list(publicancestors(repo[node]))
250 bases = list(publicancestors(repo[node]))
250 cg = changegroup.changegroupsubset(repo, bases, [node], 'shelve')
251 cg = changegroup.changegroupsubset(repo, bases, [node], 'shelve')
251 shelvedfile(repo, name, 'hg').writebundle(cg)
252 shelvedfile(repo, name, 'hg').writebundle(cg)
252 cmdutil.export(repo, [node],
253 cmdutil.export(repo, [node],
253 fp=shelvedfile(repo, name, 'patch').opener('wb'),
254 fp=shelvedfile(repo, name, 'patch').opener('wb'),
254 opts=mdiff.diffopts(git=True))
255 opts=mdiff.diffopts(git=True))
255
256
256
257
257 if ui.formatted():
258 if ui.formatted():
258 desc = util.ellipsis(desc, ui.termwidth())
259 desc = util.ellipsis(desc, ui.termwidth())
259 ui.status(_('shelved as %s\n') % name)
260 ui.status(_('shelved as %s\n') % name)
260 hg.update(repo, parent.node())
261 hg.update(repo, parent.node())
261 finally:
262 finally:
262 if bms:
263 if bms:
263 # restore old bookmarks
264 # restore old bookmarks
264 repo._bookmarks.update(bms)
265 repo._bookmarks.update(bms)
265 repo._bookmarks.write()
266 repo._bookmarks.write()
266 if tr:
267 if tr:
267 tr.abort()
268 tr.abort()
268 lockmod.release(lock, wlock)
269 lockmod.release(lock, wlock)
269
270
270 def cleanupcmd(ui, repo):
271 def cleanupcmd(ui, repo):
271 """subcommand that deletes all shelves"""
272 """subcommand that deletes all shelves"""
272
273
273 wlock = None
274 wlock = None
274 try:
275 try:
275 wlock = repo.wlock()
276 wlock = repo.wlock()
276 for (name, _type) in repo.vfs.readdir('shelved'):
277 for (name, _type) in repo.vfs.readdir('shelved'):
277 suffix = name.rsplit('.', 1)[-1]
278 suffix = name.rsplit('.', 1)[-1]
278 if suffix in ('hg', 'patch'):
279 if suffix in ('hg', 'patch'):
279 shelvedfile(repo, name).unlink()
280 shelvedfile(repo, name).unlink()
280 finally:
281 finally:
281 lockmod.release(wlock)
282 lockmod.release(wlock)
282
283
283 def deletecmd(ui, repo, pats):
284 def deletecmd(ui, repo, pats):
284 """subcommand that deletes a specific shelve"""
285 """subcommand that deletes a specific shelve"""
285 if not pats:
286 if not pats:
286 raise util.Abort(_('no shelved changes specified!'))
287 raise util.Abort(_('no shelved changes specified!'))
287 wlock = repo.wlock()
288 wlock = repo.wlock()
288 try:
289 try:
289 for name in pats:
290 for name in pats:
290 for suffix in 'hg patch'.split():
291 for suffix in 'hg patch'.split():
291 shelvedfile(repo, name, suffix).unlink()
292 shelvedfile(repo, name, suffix).unlink()
292 except OSError, err:
293 except OSError, err:
293 if err.errno != errno.ENOENT:
294 if err.errno != errno.ENOENT:
294 raise
295 raise
295 raise util.Abort(_("shelved change '%s' not found") % name)
296 raise util.Abort(_("shelved change '%s' not found") % name)
296 finally:
297 finally:
297 lockmod.release(wlock)
298 lockmod.release(wlock)
298
299
299 def listshelves(repo):
300 def listshelves(repo):
300 """return all shelves in repo as list of (time, filename)"""
301 """return all shelves in repo as list of (time, filename)"""
301 try:
302 try:
302 names = repo.vfs.readdir('shelved')
303 names = repo.vfs.readdir('shelved')
303 except OSError, err:
304 except OSError, err:
304 if err.errno != errno.ENOENT:
305 if err.errno != errno.ENOENT:
305 raise
306 raise
306 return []
307 return []
307 info = []
308 info = []
308 for (name, _type) in names:
309 for (name, _type) in names:
309 pfx, sfx = name.rsplit('.', 1)
310 pfx, sfx = name.rsplit('.', 1)
310 if not pfx or sfx != 'patch':
311 if not pfx or sfx != 'patch':
311 continue
312 continue
312 st = shelvedfile(repo, name).stat()
313 st = shelvedfile(repo, name).stat()
313 info.append((st.st_mtime, shelvedfile(repo, pfx).filename()))
314 info.append((st.st_mtime, shelvedfile(repo, pfx).filename()))
314 return sorted(info, reverse=True)
315 return sorted(info, reverse=True)
315
316
316 def listcmd(ui, repo, pats, opts):
317 def listcmd(ui, repo, pats, opts):
317 """subcommand that displays the list of shelves"""
318 """subcommand that displays the list of shelves"""
318 pats = set(pats)
319 pats = set(pats)
319 width = 80
320 width = 80
320 if not ui.plain():
321 if not ui.plain():
321 width = ui.termwidth()
322 width = ui.termwidth()
322 namelabel = 'shelve.newest'
323 namelabel = 'shelve.newest'
323 for mtime, name in listshelves(repo):
324 for mtime, name in listshelves(repo):
324 sname = util.split(name)[1]
325 sname = util.split(name)[1]
325 if pats and sname not in pats:
326 if pats and sname not in pats:
326 continue
327 continue
327 ui.write(sname, label=namelabel)
328 ui.write(sname, label=namelabel)
328 namelabel = 'shelve.name'
329 namelabel = 'shelve.name'
329 if ui.quiet:
330 if ui.quiet:
330 ui.write('\n')
331 ui.write('\n')
331 continue
332 continue
332 ui.write(' ' * (16 - len(sname)))
333 ui.write(' ' * (16 - len(sname)))
333 used = 16
334 used = 16
334 age = '(%s)' % templatefilters.age(util.makedate(mtime), abbrev=True)
335 age = '(%s)' % templatefilters.age(util.makedate(mtime), abbrev=True)
335 ui.write(age, label='shelve.age')
336 ui.write(age, label='shelve.age')
336 ui.write(' ' * (12 - len(age)))
337 ui.write(' ' * (12 - len(age)))
337 used += 12
338 used += 12
338 fp = open(name + '.patch', 'rb')
339 fp = open(name + '.patch', 'rb')
339 try:
340 try:
340 while True:
341 while True:
341 line = fp.readline()
342 line = fp.readline()
342 if not line:
343 if not line:
343 break
344 break
344 if not line.startswith('#'):
345 if not line.startswith('#'):
345 desc = line.rstrip()
346 desc = line.rstrip()
346 if ui.formatted():
347 if ui.formatted():
347 desc = util.ellipsis(desc, width - used)
348 desc = util.ellipsis(desc, width - used)
348 ui.write(desc)
349 ui.write(desc)
349 break
350 break
350 ui.write('\n')
351 ui.write('\n')
351 if not (opts['patch'] or opts['stat']):
352 if not (opts['patch'] or opts['stat']):
352 continue
353 continue
353 difflines = fp.readlines()
354 difflines = fp.readlines()
354 if opts['patch']:
355 if opts['patch']:
355 for chunk, label in patch.difflabel(iter, difflines):
356 for chunk, label in patch.difflabel(iter, difflines):
356 ui.write(chunk, label=label)
357 ui.write(chunk, label=label)
357 if opts['stat']:
358 if opts['stat']:
358 for chunk, label in patch.diffstatui(difflines, width=width,
359 for chunk, label in patch.diffstatui(difflines, width=width,
359 git=True):
360 git=True):
360 ui.write(chunk, label=label)
361 ui.write(chunk, label=label)
361 finally:
362 finally:
362 fp.close()
363 fp.close()
363
364
364 def singlepatchcmds(ui, repo, pats, opts, subcommand):
365 def singlepatchcmds(ui, repo, pats, opts, subcommand):
365 """subcommand that displays a single shelf"""
366 """subcommand that displays a single shelf"""
366 if len(pats) != 1:
367 if len(pats) != 1:
367 raise util.Abort(_("--%s expects a single shelf") % subcommand)
368 raise util.Abort(_("--%s expects a single shelf") % subcommand)
368 shelfname = pats[0]
369 shelfname = pats[0]
369
370
370 if not shelvedfile(repo, shelfname, 'patch').exists():
371 if not shelvedfile(repo, shelfname, 'patch').exists():
371 raise util.Abort(_("cannot find shelf %s") % shelfname)
372 raise util.Abort(_("cannot find shelf %s") % shelfname)
372
373
373 listcmd(ui, repo, pats, opts)
374 listcmd(ui, repo, pats, opts)
374
375
375 def checkparents(repo, state):
376 def checkparents(repo, state):
376 """check parent while resuming an unshelve"""
377 """check parent while resuming an unshelve"""
377 if state.parents != repo.dirstate.parents():
378 if state.parents != repo.dirstate.parents():
378 raise util.Abort(_('working directory parents do not match unshelve '
379 raise util.Abort(_('working directory parents do not match unshelve '
379 'state'))
380 'state'))
380
381
381 def pathtofiles(repo, files):
382 def pathtofiles(repo, files):
382 cwd = repo.getcwd()
383 cwd = repo.getcwd()
383 return [repo.pathto(f, cwd) for f in files]
384 return [repo.pathto(f, cwd) for f in files]
384
385
385 def unshelveabort(ui, repo, state, opts):
386 def unshelveabort(ui, repo, state, opts):
386 """subcommand that abort an in-progress unshelve"""
387 """subcommand that abort an in-progress unshelve"""
387 wlock = repo.wlock()
388 wlock = repo.wlock()
388 lock = None
389 lock = None
389 try:
390 try:
390 checkparents(repo, state)
391 checkparents(repo, state)
391
392
392 util.rename(repo.join('unshelverebasestate'),
393 util.rename(repo.join('unshelverebasestate'),
393 repo.join('rebasestate'))
394 repo.join('rebasestate'))
394 try:
395 try:
395 rebase.rebase(ui, repo, **{
396 rebase.rebase(ui, repo, **{
396 'abort' : True
397 'abort' : True
397 })
398 })
398 except Exception:
399 except Exception:
399 util.rename(repo.join('rebasestate'),
400 util.rename(repo.join('rebasestate'),
400 repo.join('unshelverebasestate'))
401 repo.join('unshelverebasestate'))
401 raise
402 raise
402
403
403 lock = repo.lock()
404 lock = repo.lock()
404
405
405 mergefiles(ui, repo, state.wctx, state.pendingctx)
406 mergefiles(ui, repo, state.wctx, state.pendingctx)
406
407
407 repair.strip(ui, repo, state.stripnodes, backup=False, topic='shelve')
408 repair.strip(ui, repo, state.stripnodes, backup=False, topic='shelve')
408 shelvedstate.clear(repo)
409 shelvedstate.clear(repo)
409 ui.warn(_("unshelve of '%s' aborted\n") % state.name)
410 ui.warn(_("unshelve of '%s' aborted\n") % state.name)
410 finally:
411 finally:
411 lockmod.release(lock, wlock)
412 lockmod.release(lock, wlock)
412
413
413 def mergefiles(ui, repo, wctx, shelvectx):
414 def mergefiles(ui, repo, wctx, shelvectx):
414 """updates to wctx and merges the changes from shelvectx into the
415 """updates to wctx and merges the changes from shelvectx into the
415 dirstate."""
416 dirstate."""
416 oldquiet = ui.quiet
417 oldquiet = ui.quiet
417 try:
418 try:
418 ui.quiet = True
419 ui.quiet = True
419 hg.update(repo, wctx.node())
420 hg.update(repo, wctx.node())
420 files = []
421 files = []
421 files.extend(shelvectx.files())
422 files.extend(shelvectx.files())
422 files.extend(shelvectx.parents()[0].files())
423 files.extend(shelvectx.parents()[0].files())
423
424
424 # revert will overwrite unknown files, so move them out of the way
425 # revert will overwrite unknown files, so move them out of the way
425 for file in repo.status(unknown=True).unknown:
426 for file in repo.status(unknown=True).unknown:
426 if file in files:
427 if file in files:
427 util.rename(file, file + ".orig")
428 util.rename(file, file + ".orig")
428 ui.pushbuffer(True)
429 ui.pushbuffer(True)
429 cmdutil.revert(ui, repo, shelvectx, repo.dirstate.parents(),
430 cmdutil.revert(ui, repo, shelvectx, repo.dirstate.parents(),
430 *pathtofiles(repo, files),
431 *pathtofiles(repo, files),
431 **{'no_backup': True})
432 **{'no_backup': True})
432 ui.popbuffer()
433 ui.popbuffer()
433 finally:
434 finally:
434 ui.quiet = oldquiet
435 ui.quiet = oldquiet
435
436
436 def unshelvecleanup(ui, repo, name, opts):
437 def unshelvecleanup(ui, repo, name, opts):
437 """remove related files after an unshelve"""
438 """remove related files after an unshelve"""
438 if not opts['keep']:
439 if not opts['keep']:
439 for filetype in 'hg patch'.split():
440 for filetype in 'hg patch'.split():
440 shelvedfile(repo, name, filetype).unlink()
441 shelvedfile(repo, name, filetype).unlink()
441
442
442 def unshelvecontinue(ui, repo, state, opts):
443 def unshelvecontinue(ui, repo, state, opts):
443 """subcommand to continue an in-progress unshelve"""
444 """subcommand to continue an in-progress unshelve"""
444 # We're finishing off a merge. First parent is our original
445 # We're finishing off a merge. First parent is our original
445 # parent, second is the temporary "fake" commit we're unshelving.
446 # parent, second is the temporary "fake" commit we're unshelving.
446 wlock = repo.wlock()
447 wlock = repo.wlock()
447 lock = None
448 lock = None
448 try:
449 try:
449 checkparents(repo, state)
450 checkparents(repo, state)
450 ms = merge.mergestate(repo)
451 ms = merge.mergestate(repo)
451 if [f for f in ms if ms[f] == 'u']:
452 if [f for f in ms if ms[f] == 'u']:
452 raise util.Abort(
453 raise util.Abort(
453 _("unresolved conflicts, can't continue"),
454 _("unresolved conflicts, can't continue"),
454 hint=_("see 'hg resolve', then 'hg unshelve --continue'"))
455 hint=_("see 'hg resolve', then 'hg unshelve --continue'"))
455
456
456 lock = repo.lock()
457 lock = repo.lock()
457
458
458 util.rename(repo.join('unshelverebasestate'),
459 util.rename(repo.join('unshelverebasestate'),
459 repo.join('rebasestate'))
460 repo.join('rebasestate'))
460 try:
461 try:
461 rebase.rebase(ui, repo, **{
462 rebase.rebase(ui, repo, **{
462 'continue' : True
463 'continue' : True
463 })
464 })
464 except Exception:
465 except Exception:
465 util.rename(repo.join('rebasestate'),
466 util.rename(repo.join('rebasestate'),
466 repo.join('unshelverebasestate'))
467 repo.join('unshelverebasestate'))
467 raise
468 raise
468
469
469 shelvectx = repo['tip']
470 shelvectx = repo['tip']
470 if not shelvectx in state.pendingctx.children():
471 if not shelvectx in state.pendingctx.children():
471 # rebase was a no-op, so it produced no child commit
472 # rebase was a no-op, so it produced no child commit
472 shelvectx = state.pendingctx
473 shelvectx = state.pendingctx
473 else:
474 else:
474 # only strip the shelvectx if the rebase produced it
475 # only strip the shelvectx if the rebase produced it
475 state.stripnodes.append(shelvectx.node())
476 state.stripnodes.append(shelvectx.node())
476
477
477 mergefiles(ui, repo, state.wctx, shelvectx)
478 mergefiles(ui, repo, state.wctx, shelvectx)
478
479
479 repair.strip(ui, repo, state.stripnodes, backup=False, topic='shelve')
480 repair.strip(ui, repo, state.stripnodes, backup=False, topic='shelve')
480 shelvedstate.clear(repo)
481 shelvedstate.clear(repo)
481 unshelvecleanup(ui, repo, state.name, opts)
482 unshelvecleanup(ui, repo, state.name, opts)
482 ui.status(_("unshelve of '%s' complete\n") % state.name)
483 ui.status(_("unshelve of '%s' complete\n") % state.name)
483 finally:
484 finally:
484 lockmod.release(lock, wlock)
485 lockmod.release(lock, wlock)
485
486
486 @command('unshelve',
487 @command('unshelve',
487 [('a', 'abort', None,
488 [('a', 'abort', None,
488 _('abort an incomplete unshelve operation')),
489 _('abort an incomplete unshelve operation')),
489 ('c', 'continue', None,
490 ('c', 'continue', None,
490 _('continue an incomplete unshelve operation')),
491 _('continue an incomplete unshelve operation')),
491 ('', 'keep', None,
492 ('', 'keep', None,
492 _('keep shelve after unshelving')),
493 _('keep shelve after unshelving')),
493 ('', 'date', '',
494 ('', 'date', '',
494 _('set date for temporary commits (DEPRECATED)'), _('DATE'))],
495 _('set date for temporary commits (DEPRECATED)'), _('DATE'))],
495 _('hg unshelve [SHELVED]'))
496 _('hg unshelve [SHELVED]'))
496 def unshelve(ui, repo, *shelved, **opts):
497 def unshelve(ui, repo, *shelved, **opts):
497 """restore a shelved change to the working directory
498 """restore a shelved change to the working directory
498
499
499 This command accepts an optional name of a shelved change to
500 This command accepts an optional name of a shelved change to
500 restore. If none is given, the most recent shelved change is used.
501 restore. If none is given, the most recent shelved change is used.
501
502
502 If a shelved change is applied successfully, the bundle that
503 If a shelved change is applied successfully, the bundle that
503 contains the shelved changes is deleted afterwards.
504 contains the shelved changes is deleted afterwards.
504
505
505 Since you can restore a shelved change on top of an arbitrary
506 Since you can restore a shelved change on top of an arbitrary
506 commit, it is possible that unshelving will result in a conflict
507 commit, it is possible that unshelving will result in a conflict
507 between your changes and the commits you are unshelving onto. If
508 between your changes and the commits you are unshelving onto. If
508 this occurs, you must resolve the conflict, then use
509 this occurs, you must resolve the conflict, then use
509 ``--continue`` to complete the unshelve operation. (The bundle
510 ``--continue`` to complete the unshelve operation. (The bundle
510 will not be deleted until you successfully complete the unshelve.)
511 will not be deleted until you successfully complete the unshelve.)
511
512
512 (Alternatively, you can use ``--abort`` to abandon an unshelve
513 (Alternatively, you can use ``--abort`` to abandon an unshelve
513 that causes a conflict. This reverts the unshelved changes, and
514 that causes a conflict. This reverts the unshelved changes, and
514 does not delete the bundle.)
515 does not delete the bundle.)
515 """
516 """
516 abortf = opts['abort']
517 abortf = opts['abort']
517 continuef = opts['continue']
518 continuef = opts['continue']
518 if not abortf and not continuef:
519 if not abortf and not continuef:
519 cmdutil.checkunfinished(repo)
520 cmdutil.checkunfinished(repo)
520
521
521 if abortf or continuef:
522 if abortf or continuef:
522 if abortf and continuef:
523 if abortf and continuef:
523 raise util.Abort(_('cannot use both abort and continue'))
524 raise util.Abort(_('cannot use both abort and continue'))
524 if shelved:
525 if shelved:
525 raise util.Abort(_('cannot combine abort/continue with '
526 raise util.Abort(_('cannot combine abort/continue with '
526 'naming a shelved change'))
527 'naming a shelved change'))
527
528
528 try:
529 try:
529 state = shelvedstate.load(repo)
530 state = shelvedstate.load(repo)
530 except IOError, err:
531 except IOError, err:
531 if err.errno != errno.ENOENT:
532 if err.errno != errno.ENOENT:
532 raise
533 raise
533 raise util.Abort(_('no unshelve operation underway'))
534 raise util.Abort(_('no unshelve operation underway'))
534
535
535 if abortf:
536 if abortf:
536 return unshelveabort(ui, repo, state, opts)
537 return unshelveabort(ui, repo, state, opts)
537 elif continuef:
538 elif continuef:
538 return unshelvecontinue(ui, repo, state, opts)
539 return unshelvecontinue(ui, repo, state, opts)
539 elif len(shelved) > 1:
540 elif len(shelved) > 1:
540 raise util.Abort(_('can only unshelve one change at a time'))
541 raise util.Abort(_('can only unshelve one change at a time'))
541 elif not shelved:
542 elif not shelved:
542 shelved = listshelves(repo)
543 shelved = listshelves(repo)
543 if not shelved:
544 if not shelved:
544 raise util.Abort(_('no shelved changes to apply!'))
545 raise util.Abort(_('no shelved changes to apply!'))
545 basename = util.split(shelved[0][1])[1]
546 basename = util.split(shelved[0][1])[1]
546 ui.status(_("unshelving change '%s'\n") % basename)
547 ui.status(_("unshelving change '%s'\n") % basename)
547 else:
548 else:
548 basename = shelved[0]
549 basename = shelved[0]
549
550
550 if not shelvedfile(repo, basename, 'patch').exists():
551 if not shelvedfile(repo, basename, 'patch').exists():
551 raise util.Abort(_("shelved change '%s' not found") % basename)
552 raise util.Abort(_("shelved change '%s' not found") % basename)
552
553
553 oldquiet = ui.quiet
554 oldquiet = ui.quiet
554 wlock = lock = tr = None
555 wlock = lock = tr = None
555 try:
556 try:
556 wlock = repo.wlock()
557 wlock = repo.wlock()
557 lock = repo.lock()
558 lock = repo.lock()
558
559
559 tr = repo.transaction('unshelve', report=lambda x: None)
560 tr = repo.transaction('unshelve', report=lambda x: None)
560 oldtiprev = len(repo)
561 oldtiprev = len(repo)
561
562
562 pctx = repo['.']
563 pctx = repo['.']
563 tmpwctx = pctx
564 tmpwctx = pctx
564 # The goal is to have a commit structure like so:
565 # The goal is to have a commit structure like so:
565 # ...-> pctx -> tmpwctx -> shelvectx
566 # ...-> pctx -> tmpwctx -> shelvectx
566 # where tmpwctx is an optional commit with the user's pending changes
567 # where tmpwctx is an optional commit with the user's pending changes
567 # and shelvectx is the unshelved changes. Then we merge it all down
568 # and shelvectx is the unshelved changes. Then we merge it all down
568 # to the original pctx.
569 # to the original pctx.
569
570
570 # Store pending changes in a commit
571 # Store pending changes in a commit
571 s = repo.status()
572 s = repo.status()
572 if s.modified or s.added or s.removed or s.deleted:
573 if s.modified or s.added or s.removed or s.deleted:
573 ui.status(_("temporarily committing pending changes "
574 ui.status(_("temporarily committing pending changes "
574 "(restore with 'hg unshelve --abort')\n"))
575 "(restore with 'hg unshelve --abort')\n"))
575 def commitfunc(ui, repo, message, match, opts):
576 def commitfunc(ui, repo, message, match, opts):
576 hasmq = util.safehasattr(repo, 'mq')
577 hasmq = util.safehasattr(repo, 'mq')
577 if hasmq:
578 if hasmq:
578 saved, repo.mq.checkapplied = repo.mq.checkapplied, False
579 saved, repo.mq.checkapplied = repo.mq.checkapplied, False
579
580
580 backup = repo.ui.backupconfig('phases', 'new-commit')
581 backup = repo.ui.backupconfig('phases', 'new-commit')
581 try:
582 try:
582 repo.ui. setconfig('phases', 'new-commit', phases.secret)
583 repo.ui. setconfig('phases', 'new-commit', phases.secret)
583 return repo.commit(message, 'shelve@localhost',
584 return repo.commit(message, 'shelve@localhost',
584 opts.get('date'), match)
585 opts.get('date'), match)
585 finally:
586 finally:
586 repo.ui.restoreconfig(backup)
587 repo.ui.restoreconfig(backup)
587 if hasmq:
588 if hasmq:
588 repo.mq.checkapplied = saved
589 repo.mq.checkapplied = saved
589
590
590 tempopts = {}
591 tempopts = {}
591 tempopts['message'] = "pending changes temporary commit"
592 tempopts['message'] = "pending changes temporary commit"
592 tempopts['date'] = opts.get('date')
593 tempopts['date'] = opts.get('date')
593 ui.quiet = True
594 ui.quiet = True
594 node = cmdutil.commit(ui, repo, commitfunc, [], tempopts)
595 node = cmdutil.commit(ui, repo, commitfunc, [], tempopts)
595 tmpwctx = repo[node]
596 tmpwctx = repo[node]
596
597
597 ui.quiet = True
598 ui.quiet = True
598 shelvedfile(repo, basename, 'hg').applybundle()
599 shelvedfile(repo, basename, 'hg').applybundle()
599
600
600 ui.quiet = oldquiet
601 ui.quiet = oldquiet
601
602
602 shelvectx = repo['tip']
603 shelvectx = repo['tip']
603
604
604 # If the shelve is not immediately on top of the commit
605 # If the shelve is not immediately on top of the commit
605 # we'll be merging with, rebase it to be on top.
606 # we'll be merging with, rebase it to be on top.
606 if tmpwctx.node() != shelvectx.parents()[0].node():
607 if tmpwctx.node() != shelvectx.parents()[0].node():
607 ui.status(_('rebasing shelved changes\n'))
608 ui.status(_('rebasing shelved changes\n'))
608 try:
609 try:
609 rebase.rebase(ui, repo, **{
610 rebase.rebase(ui, repo, **{
610 'rev' : [shelvectx.rev()],
611 'rev' : [shelvectx.rev()],
611 'dest' : str(tmpwctx.rev()),
612 'dest' : str(tmpwctx.rev()),
612 'keep' : True,
613 'keep' : True,
613 })
614 })
614 except error.InterventionRequired:
615 except error.InterventionRequired:
615 tr.close()
616 tr.close()
616
617
617 stripnodes = [repo.changelog.node(rev)
618 stripnodes = [repo.changelog.node(rev)
618 for rev in xrange(oldtiprev, len(repo))]
619 for rev in xrange(oldtiprev, len(repo))]
619 shelvedstate.save(repo, basename, pctx, tmpwctx, stripnodes)
620 shelvedstate.save(repo, basename, pctx, tmpwctx, stripnodes)
620
621
621 util.rename(repo.join('rebasestate'),
622 util.rename(repo.join('rebasestate'),
622 repo.join('unshelverebasestate'))
623 repo.join('unshelverebasestate'))
623 raise error.InterventionRequired(
624 raise error.InterventionRequired(
624 _("unresolved conflicts (see 'hg resolve', then "
625 _("unresolved conflicts (see 'hg resolve', then "
625 "'hg unshelve --continue')"))
626 "'hg unshelve --continue')"))
626
627
627 # refresh ctx after rebase completes
628 # refresh ctx after rebase completes
628 shelvectx = repo['tip']
629 shelvectx = repo['tip']
629
630
630 if not shelvectx in tmpwctx.children():
631 if not shelvectx in tmpwctx.children():
631 # rebase was a no-op, so it produced no child commit
632 # rebase was a no-op, so it produced no child commit
632 shelvectx = tmpwctx
633 shelvectx = tmpwctx
633
634
634 mergefiles(ui, repo, pctx, shelvectx)
635 mergefiles(ui, repo, pctx, shelvectx)
635 shelvedstate.clear(repo)
636 shelvedstate.clear(repo)
636
637
637 # The transaction aborting will strip all the commits for us,
638 # The transaction aborting will strip all the commits for us,
638 # but it doesn't update the inmemory structures, so addchangegroup
639 # but it doesn't update the inmemory structures, so addchangegroup
639 # hooks still fire and try to operate on the missing commits.
640 # hooks still fire and try to operate on the missing commits.
640 # Clean up manually to prevent this.
641 # Clean up manually to prevent this.
641 repo.unfiltered().changelog.strip(oldtiprev, tr)
642 repo.unfiltered().changelog.strip(oldtiprev, tr)
642
643
643 unshelvecleanup(ui, repo, basename, opts)
644 unshelvecleanup(ui, repo, basename, opts)
644 finally:
645 finally:
645 ui.quiet = oldquiet
646 ui.quiet = oldquiet
646 if tr:
647 if tr:
647 tr.release()
648 tr.release()
648 lockmod.release(lock, wlock)
649 lockmod.release(lock, wlock)
649
650
650 @command('shelve',
651 @command('shelve',
651 [('A', 'addremove', None,
652 [('A', 'addremove', None,
652 _('mark new/missing files as added/removed before shelving')),
653 _('mark new/missing files as added/removed before shelving')),
653 ('', 'cleanup', None,
654 ('', 'cleanup', None,
654 _('delete all shelved changes')),
655 _('delete all shelved changes')),
655 ('', 'date', '',
656 ('', 'date', '',
656 _('shelve with the specified commit date'), _('DATE')),
657 _('shelve with the specified commit date'), _('DATE')),
657 ('d', 'delete', None,
658 ('d', 'delete', None,
658 _('delete the named shelved change(s)')),
659 _('delete the named shelved change(s)')),
659 ('e', 'edit', False,
660 ('e', 'edit', False,
660 _('invoke editor on commit messages')),
661 _('invoke editor on commit messages')),
661 ('l', 'list', None,
662 ('l', 'list', None,
662 _('list current shelves')),
663 _('list current shelves')),
663 ('m', 'message', '',
664 ('m', 'message', '',
664 _('use text as shelve message'), _('TEXT')),
665 _('use text as shelve message'), _('TEXT')),
665 ('n', 'name', '',
666 ('n', 'name', '',
666 _('use the given name for the shelved commit'), _('NAME')),
667 _('use the given name for the shelved commit'), _('NAME')),
667 ('p', 'patch', None,
668 ('p', 'patch', None,
668 _('show patch')),
669 _('show patch')),
669 ('i', 'interactive', None,
670 ('i', 'interactive', None,
670 _('interactive mode, only works while creating a shelve'
671 _('interactive mode, only works while creating a shelve'
671 '(EXPERIMENTAL)')),
672 '(EXPERIMENTAL)')),
672 ('', 'stat', None,
673 ('', 'stat', None,
673 _('output diffstat-style summary of changes'))] + commands.walkopts,
674 _('output diffstat-style summary of changes'))] + commands.walkopts,
674 _('hg shelve [OPTION]... [FILE]...'))
675 _('hg shelve [OPTION]... [FILE]...'))
675 def shelvecmd(ui, repo, *pats, **opts):
676 def shelvecmd(ui, repo, *pats, **opts):
676 '''save and set aside changes from the working directory
677 '''save and set aside changes from the working directory
677
678
678 Shelving takes files that "hg status" reports as not clean, saves
679 Shelving takes files that "hg status" reports as not clean, saves
679 the modifications to a bundle (a shelved change), and reverts the
680 the modifications to a bundle (a shelved change), and reverts the
680 files so that their state in the working directory becomes clean.
681 files so that their state in the working directory becomes clean.
681
682
682 To restore these changes to the working directory, using "hg
683 To restore these changes to the working directory, using "hg
683 unshelve"; this will work even if you switch to a different
684 unshelve"; this will work even if you switch to a different
684 commit.
685 commit.
685
686
686 When no files are specified, "hg shelve" saves all not-clean
687 When no files are specified, "hg shelve" saves all not-clean
687 files. If specific files or directories are named, only changes to
688 files. If specific files or directories are named, only changes to
688 those files are shelved.
689 those files are shelved.
689
690
690 Each shelved change has a name that makes it easier to find later.
691 Each shelved change has a name that makes it easier to find later.
691 The name of a shelved change defaults to being based on the active
692 The name of a shelved change defaults to being based on the active
692 bookmark, or if there is no active bookmark, the current named
693 bookmark, or if there is no active bookmark, the current named
693 branch. To specify a different name, use ``--name``.
694 branch. To specify a different name, use ``--name``.
694
695
695 To see a list of existing shelved changes, use the ``--list``
696 To see a list of existing shelved changes, use the ``--list``
696 option. For each shelved change, this will print its name, age,
697 option. For each shelved change, this will print its name, age,
697 and description; use ``--patch`` or ``--stat`` for more details.
698 and description; use ``--patch`` or ``--stat`` for more details.
698
699
699 To delete specific shelved changes, use ``--delete``. To delete
700 To delete specific shelved changes, use ``--delete``. To delete
700 all shelved changes, use ``--cleanup``.
701 all shelved changes, use ``--cleanup``.
701 '''
702 '''
702 cmdutil.checkunfinished(repo)
703 cmdutil.checkunfinished(repo)
703
704
704 allowables = [
705 allowables = [
705 ('addremove', set(['create'])), # 'create' is pseudo action
706 ('addremove', set(['create'])), # 'create' is pseudo action
706 ('cleanup', set(['cleanup'])),
707 ('cleanup', set(['cleanup'])),
707 # ('date', set(['create'])), # ignored for passing '--date "0 0"' in tests
708 # ('date', set(['create'])), # ignored for passing '--date "0 0"' in tests
708 ('delete', set(['delete'])),
709 ('delete', set(['delete'])),
709 ('edit', set(['create'])),
710 ('edit', set(['create'])),
710 ('list', set(['list'])),
711 ('list', set(['list'])),
711 ('message', set(['create'])),
712 ('message', set(['create'])),
712 ('name', set(['create'])),
713 ('name', set(['create'])),
713 ('patch', set(['patch', 'list'])),
714 ('patch', set(['patch', 'list'])),
714 ('stat', set(['stat', 'list'])),
715 ('stat', set(['stat', 'list'])),
715 ]
716 ]
716 def checkopt(opt):
717 def checkopt(opt):
717 if opts[opt]:
718 if opts[opt]:
718 for i, allowable in allowables:
719 for i, allowable in allowables:
719 if opts[i] and opt not in allowable:
720 if opts[i] and opt not in allowable:
720 raise util.Abort(_("options '--%s' and '--%s' may not be "
721 raise util.Abort(_("options '--%s' and '--%s' may not be "
721 "used together") % (opt, i))
722 "used together") % (opt, i))
722 return True
723 return True
723 if checkopt('cleanup'):
724 if checkopt('cleanup'):
724 if pats:
725 if pats:
725 raise util.Abort(_("cannot specify names when using '--cleanup'"))
726 raise util.Abort(_("cannot specify names when using '--cleanup'"))
726 return cleanupcmd(ui, repo)
727 return cleanupcmd(ui, repo)
727 elif checkopt('delete'):
728 elif checkopt('delete'):
728 return deletecmd(ui, repo, pats)
729 return deletecmd(ui, repo, pats)
729 elif checkopt('list'):
730 elif checkopt('list'):
730 return listcmd(ui, repo, pats, opts)
731 return listcmd(ui, repo, pats, opts)
731 elif checkopt('patch'):
732 elif checkopt('patch'):
732 return singlepatchcmds(ui, repo, pats, opts, subcommand='patch')
733 return singlepatchcmds(ui, repo, pats, opts, subcommand='patch')
733 elif checkopt('stat'):
734 elif checkopt('stat'):
734 return singlepatchcmds(ui, repo, pats, opts, subcommand='stat')
735 return singlepatchcmds(ui, repo, pats, opts, subcommand='stat')
735 else:
736 else:
736 return createcmd(ui, repo, pats, opts)
737 return createcmd(ui, repo, pats, opts)
737
738
738 def extsetup(ui):
739 def extsetup(ui):
739 cmdutil.unfinishedstates.append(
740 cmdutil.unfinishedstates.append(
740 [shelvedstate._filename, False, False,
741 [shelvedstate._filename, False, False,
741 _('unshelve already in progress'),
742 _('unshelve already in progress'),
742 _("use 'hg unshelve --continue' or 'hg unshelve --abort'")])
743 _("use 'hg unshelve --continue' or 'hg unshelve --abort'")])
@@ -1,354 +1,354 b''
1 # ancestor.py - generic DAG ancestor algorithm for mercurial
1 # ancestor.py - generic DAG ancestor algorithm for mercurial
2 #
2 #
3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import collections
8 import heapq
9 import heapq
9 import util
10 from node import nullrev
10 from node import nullrev
11
11
12 def commonancestorsheads(pfunc, *nodes):
12 def commonancestorsheads(pfunc, *nodes):
13 """Returns a set with the heads of all common ancestors of all nodes,
13 """Returns a set with the heads of all common ancestors of all nodes,
14 heads(::nodes[0] and ::nodes[1] and ...) .
14 heads(::nodes[0] and ::nodes[1] and ...) .
15
15
16 pfunc must return a list of parent vertices for a given vertex.
16 pfunc must return a list of parent vertices for a given vertex.
17 """
17 """
18 if not isinstance(nodes, set):
18 if not isinstance(nodes, set):
19 nodes = set(nodes)
19 nodes = set(nodes)
20 if nullrev in nodes:
20 if nullrev in nodes:
21 return set()
21 return set()
22 if len(nodes) <= 1:
22 if len(nodes) <= 1:
23 return nodes
23 return nodes
24
24
25 allseen = (1 << len(nodes)) - 1
25 allseen = (1 << len(nodes)) - 1
26 seen = [0] * (max(nodes) + 1)
26 seen = [0] * (max(nodes) + 1)
27 for i, n in enumerate(nodes):
27 for i, n in enumerate(nodes):
28 seen[n] = 1 << i
28 seen[n] = 1 << i
29 poison = 1 << (i + 1)
29 poison = 1 << (i + 1)
30
30
31 gca = set()
31 gca = set()
32 interesting = len(nodes)
32 interesting = len(nodes)
33 nv = len(seen) - 1
33 nv = len(seen) - 1
34 while nv >= 0 and interesting:
34 while nv >= 0 and interesting:
35 v = nv
35 v = nv
36 nv -= 1
36 nv -= 1
37 if not seen[v]:
37 if not seen[v]:
38 continue
38 continue
39 sv = seen[v]
39 sv = seen[v]
40 if sv < poison:
40 if sv < poison:
41 interesting -= 1
41 interesting -= 1
42 if sv == allseen:
42 if sv == allseen:
43 gca.add(v)
43 gca.add(v)
44 sv |= poison
44 sv |= poison
45 if v in nodes:
45 if v in nodes:
46 # history is linear
46 # history is linear
47 return set([v])
47 return set([v])
48 if sv < poison:
48 if sv < poison:
49 for p in pfunc(v):
49 for p in pfunc(v):
50 sp = seen[p]
50 sp = seen[p]
51 if p == nullrev:
51 if p == nullrev:
52 continue
52 continue
53 if sp == 0:
53 if sp == 0:
54 seen[p] = sv
54 seen[p] = sv
55 interesting += 1
55 interesting += 1
56 elif sp != sv:
56 elif sp != sv:
57 seen[p] |= sv
57 seen[p] |= sv
58 else:
58 else:
59 for p in pfunc(v):
59 for p in pfunc(v):
60 if p == nullrev:
60 if p == nullrev:
61 continue
61 continue
62 sp = seen[p]
62 sp = seen[p]
63 if sp and sp < poison:
63 if sp and sp < poison:
64 interesting -= 1
64 interesting -= 1
65 seen[p] = sv
65 seen[p] = sv
66 return gca
66 return gca
67
67
68 def ancestors(pfunc, *orignodes):
68 def ancestors(pfunc, *orignodes):
69 """
69 """
70 Returns the common ancestors of a and b that are furthest from a
70 Returns the common ancestors of a and b that are furthest from a
71 root (as measured by longest path).
71 root (as measured by longest path).
72
72
73 pfunc must return a list of parent vertices for a given vertex.
73 pfunc must return a list of parent vertices for a given vertex.
74 """
74 """
75 def deepest(nodes):
75 def deepest(nodes):
76 interesting = {}
76 interesting = {}
77 count = max(nodes) + 1
77 count = max(nodes) + 1
78 depth = [0] * count
78 depth = [0] * count
79 seen = [0] * count
79 seen = [0] * count
80 mapping = []
80 mapping = []
81 for (i, n) in enumerate(sorted(nodes)):
81 for (i, n) in enumerate(sorted(nodes)):
82 depth[n] = 1
82 depth[n] = 1
83 b = 1 << i
83 b = 1 << i
84 seen[n] = b
84 seen[n] = b
85 interesting[b] = 1
85 interesting[b] = 1
86 mapping.append((b, n))
86 mapping.append((b, n))
87 nv = count - 1
87 nv = count - 1
88 while nv >= 0 and len(interesting) > 1:
88 while nv >= 0 and len(interesting) > 1:
89 v = nv
89 v = nv
90 nv -= 1
90 nv -= 1
91 dv = depth[v]
91 dv = depth[v]
92 if dv == 0:
92 if dv == 0:
93 continue
93 continue
94 sv = seen[v]
94 sv = seen[v]
95 for p in pfunc(v):
95 for p in pfunc(v):
96 if p == nullrev:
96 if p == nullrev:
97 continue
97 continue
98 dp = depth[p]
98 dp = depth[p]
99 nsp = sp = seen[p]
99 nsp = sp = seen[p]
100 if dp <= dv:
100 if dp <= dv:
101 depth[p] = dv + 1
101 depth[p] = dv + 1
102 if sp != sv:
102 if sp != sv:
103 interesting[sv] += 1
103 interesting[sv] += 1
104 nsp = seen[p] = sv
104 nsp = seen[p] = sv
105 if sp:
105 if sp:
106 interesting[sp] -= 1
106 interesting[sp] -= 1
107 if interesting[sp] == 0:
107 if interesting[sp] == 0:
108 del interesting[sp]
108 del interesting[sp]
109 elif dv == dp - 1:
109 elif dv == dp - 1:
110 nsp = sp | sv
110 nsp = sp | sv
111 if nsp == sp:
111 if nsp == sp:
112 continue
112 continue
113 seen[p] = nsp
113 seen[p] = nsp
114 interesting.setdefault(nsp, 0)
114 interesting.setdefault(nsp, 0)
115 interesting[nsp] += 1
115 interesting[nsp] += 1
116 interesting[sp] -= 1
116 interesting[sp] -= 1
117 if interesting[sp] == 0:
117 if interesting[sp] == 0:
118 del interesting[sp]
118 del interesting[sp]
119 interesting[sv] -= 1
119 interesting[sv] -= 1
120 if interesting[sv] == 0:
120 if interesting[sv] == 0:
121 del interesting[sv]
121 del interesting[sv]
122
122
123 if len(interesting) != 1:
123 if len(interesting) != 1:
124 return []
124 return []
125
125
126 k = 0
126 k = 0
127 for i in interesting:
127 for i in interesting:
128 k |= i
128 k |= i
129 return set(n for (i, n) in mapping if k & i)
129 return set(n for (i, n) in mapping if k & i)
130
130
131 gca = commonancestorsheads(pfunc, *orignodes)
131 gca = commonancestorsheads(pfunc, *orignodes)
132
132
133 if len(gca) <= 1:
133 if len(gca) <= 1:
134 return gca
134 return gca
135 return deepest(gca)
135 return deepest(gca)
136
136
137 class incrementalmissingancestors(object):
137 class incrementalmissingancestors(object):
138 '''persistent state used to calculate missing ancestors incrementally
138 '''persistent state used to calculate missing ancestors incrementally
139
139
140 Although similar in spirit to lazyancestors below, this is a separate class
140 Although similar in spirit to lazyancestors below, this is a separate class
141 because trying to support contains and missingancestors operations with the
141 because trying to support contains and missingancestors operations with the
142 same internal data structures adds needless complexity.'''
142 same internal data structures adds needless complexity.'''
143 def __init__(self, pfunc, bases):
143 def __init__(self, pfunc, bases):
144 self.bases = set(bases)
144 self.bases = set(bases)
145 if not self.bases:
145 if not self.bases:
146 self.bases.add(nullrev)
146 self.bases.add(nullrev)
147 self.pfunc = pfunc
147 self.pfunc = pfunc
148
148
149 def hasbases(self):
149 def hasbases(self):
150 '''whether the common set has any non-trivial bases'''
150 '''whether the common set has any non-trivial bases'''
151 return self.bases and self.bases != set([nullrev])
151 return self.bases and self.bases != set([nullrev])
152
152
153 def addbases(self, newbases):
153 def addbases(self, newbases):
154 '''grow the ancestor set by adding new bases'''
154 '''grow the ancestor set by adding new bases'''
155 self.bases.update(newbases)
155 self.bases.update(newbases)
156
156
157 def removeancestorsfrom(self, revs):
157 def removeancestorsfrom(self, revs):
158 '''remove all ancestors of bases from the set revs (in place)'''
158 '''remove all ancestors of bases from the set revs (in place)'''
159 bases = self.bases
159 bases = self.bases
160 pfunc = self.pfunc
160 pfunc = self.pfunc
161 revs.difference_update(bases)
161 revs.difference_update(bases)
162 # nullrev is always an ancestor
162 # nullrev is always an ancestor
163 revs.discard(nullrev)
163 revs.discard(nullrev)
164 if not revs:
164 if not revs:
165 return
165 return
166 # anything in revs > start is definitely not an ancestor of bases
166 # anything in revs > start is definitely not an ancestor of bases
167 # revs <= start needs to be investigated
167 # revs <= start needs to be investigated
168 start = max(bases)
168 start = max(bases)
169 keepcount = sum(1 for r in revs if r > start)
169 keepcount = sum(1 for r in revs if r > start)
170 if len(revs) == keepcount:
170 if len(revs) == keepcount:
171 # no revs to consider
171 # no revs to consider
172 return
172 return
173
173
174 for curr in xrange(start, min(revs) - 1, -1):
174 for curr in xrange(start, min(revs) - 1, -1):
175 if curr not in bases:
175 if curr not in bases:
176 continue
176 continue
177 revs.discard(curr)
177 revs.discard(curr)
178 bases.update(pfunc(curr))
178 bases.update(pfunc(curr))
179 if len(revs) == keepcount:
179 if len(revs) == keepcount:
180 # no more potential revs to discard
180 # no more potential revs to discard
181 break
181 break
182
182
183 def missingancestors(self, revs):
183 def missingancestors(self, revs):
184 '''return all the ancestors of revs that are not ancestors of self.bases
184 '''return all the ancestors of revs that are not ancestors of self.bases
185
185
186 This may include elements from revs.
186 This may include elements from revs.
187
187
188 Equivalent to the revset (::revs - ::self.bases). Revs are returned in
188 Equivalent to the revset (::revs - ::self.bases). Revs are returned in
189 revision number order, which is a topological order.'''
189 revision number order, which is a topological order.'''
190 revsvisit = set(revs)
190 revsvisit = set(revs)
191 basesvisit = self.bases
191 basesvisit = self.bases
192 pfunc = self.pfunc
192 pfunc = self.pfunc
193 bothvisit = revsvisit.intersection(basesvisit)
193 bothvisit = revsvisit.intersection(basesvisit)
194 revsvisit.difference_update(bothvisit)
194 revsvisit.difference_update(bothvisit)
195 if not revsvisit:
195 if not revsvisit:
196 return []
196 return []
197
197
198 start = max(max(revsvisit), max(basesvisit))
198 start = max(max(revsvisit), max(basesvisit))
199 # At this point, we hold the invariants that:
199 # At this point, we hold the invariants that:
200 # - revsvisit is the set of nodes we know are an ancestor of at least
200 # - revsvisit is the set of nodes we know are an ancestor of at least
201 # one of the nodes in revs
201 # one of the nodes in revs
202 # - basesvisit is the same for bases
202 # - basesvisit is the same for bases
203 # - bothvisit is the set of nodes we know are ancestors of at least one
203 # - bothvisit is the set of nodes we know are ancestors of at least one
204 # of the nodes in revs and one of the nodes in bases. bothvisit and
204 # of the nodes in revs and one of the nodes in bases. bothvisit and
205 # revsvisit are mutually exclusive, but bothvisit is a subset of
205 # revsvisit are mutually exclusive, but bothvisit is a subset of
206 # basesvisit.
206 # basesvisit.
207 # Now we walk down in reverse topo order, adding parents of nodes
207 # Now we walk down in reverse topo order, adding parents of nodes
208 # already visited to the sets while maintaining the invariants. When a
208 # already visited to the sets while maintaining the invariants. When a
209 # node is found in both revsvisit and basesvisit, it is removed from
209 # node is found in both revsvisit and basesvisit, it is removed from
210 # revsvisit and added to bothvisit. When revsvisit becomes empty, there
210 # revsvisit and added to bothvisit. When revsvisit becomes empty, there
211 # are no more ancestors of revs that aren't also ancestors of bases, so
211 # are no more ancestors of revs that aren't also ancestors of bases, so
212 # exit.
212 # exit.
213
213
214 missing = []
214 missing = []
215 for curr in xrange(start, nullrev, -1):
215 for curr in xrange(start, nullrev, -1):
216 if not revsvisit:
216 if not revsvisit:
217 break
217 break
218
218
219 if curr in bothvisit:
219 if curr in bothvisit:
220 bothvisit.remove(curr)
220 bothvisit.remove(curr)
221 # curr's parents might have made it into revsvisit through
221 # curr's parents might have made it into revsvisit through
222 # another path
222 # another path
223 for p in pfunc(curr):
223 for p in pfunc(curr):
224 revsvisit.discard(p)
224 revsvisit.discard(p)
225 basesvisit.add(p)
225 basesvisit.add(p)
226 bothvisit.add(p)
226 bothvisit.add(p)
227 continue
227 continue
228
228
229 if curr in revsvisit:
229 if curr in revsvisit:
230 missing.append(curr)
230 missing.append(curr)
231 revsvisit.remove(curr)
231 revsvisit.remove(curr)
232 thisvisit = revsvisit
232 thisvisit = revsvisit
233 othervisit = basesvisit
233 othervisit = basesvisit
234 elif curr in basesvisit:
234 elif curr in basesvisit:
235 thisvisit = basesvisit
235 thisvisit = basesvisit
236 othervisit = revsvisit
236 othervisit = revsvisit
237 else:
237 else:
238 # not an ancestor of revs or bases: ignore
238 # not an ancestor of revs or bases: ignore
239 continue
239 continue
240
240
241 for p in pfunc(curr):
241 for p in pfunc(curr):
242 if p == nullrev:
242 if p == nullrev:
243 pass
243 pass
244 elif p in othervisit or p in bothvisit:
244 elif p in othervisit or p in bothvisit:
245 # p is implicitly in thisvisit. This means p is or should be
245 # p is implicitly in thisvisit. This means p is or should be
246 # in bothvisit
246 # in bothvisit
247 revsvisit.discard(p)
247 revsvisit.discard(p)
248 basesvisit.add(p)
248 basesvisit.add(p)
249 bothvisit.add(p)
249 bothvisit.add(p)
250 else:
250 else:
251 # visit later
251 # visit later
252 thisvisit.add(p)
252 thisvisit.add(p)
253
253
254 missing.reverse()
254 missing.reverse()
255 return missing
255 return missing
256
256
257 class lazyancestors(object):
257 class lazyancestors(object):
258 def __init__(self, pfunc, revs, stoprev=0, inclusive=False):
258 def __init__(self, pfunc, revs, stoprev=0, inclusive=False):
259 """Create a new object generating ancestors for the given revs. Does
259 """Create a new object generating ancestors for the given revs. Does
260 not generate revs lower than stoprev.
260 not generate revs lower than stoprev.
261
261
262 This is computed lazily starting from revs. The object supports
262 This is computed lazily starting from revs. The object supports
263 iteration and membership.
263 iteration and membership.
264
264
265 cl should be a changelog and revs should be an iterable. inclusive is
265 cl should be a changelog and revs should be an iterable. inclusive is
266 a boolean that indicates whether revs should be included. Revs lower
266 a boolean that indicates whether revs should be included. Revs lower
267 than stoprev will not be generated.
267 than stoprev will not be generated.
268
268
269 Result does not include the null revision."""
269 Result does not include the null revision."""
270 self._parentrevs = pfunc
270 self._parentrevs = pfunc
271 self._initrevs = revs
271 self._initrevs = revs
272 self._stoprev = stoprev
272 self._stoprev = stoprev
273 self._inclusive = inclusive
273 self._inclusive = inclusive
274
274
275 # Initialize data structures for __contains__.
275 # Initialize data structures for __contains__.
276 # For __contains__, we use a heap rather than a deque because
276 # For __contains__, we use a heap rather than a deque because
277 # (a) it minimizes the number of parentrevs calls made
277 # (a) it minimizes the number of parentrevs calls made
278 # (b) it makes the loop termination condition obvious
278 # (b) it makes the loop termination condition obvious
279 # Python's heap is a min-heap. Multiply all values by -1 to convert it
279 # Python's heap is a min-heap. Multiply all values by -1 to convert it
280 # into a max-heap.
280 # into a max-heap.
281 self._containsvisit = [-rev for rev in revs]
281 self._containsvisit = [-rev for rev in revs]
282 heapq.heapify(self._containsvisit)
282 heapq.heapify(self._containsvisit)
283 if inclusive:
283 if inclusive:
284 self._containsseen = set(revs)
284 self._containsseen = set(revs)
285 else:
285 else:
286 self._containsseen = set()
286 self._containsseen = set()
287
287
288 def __nonzero__(self):
288 def __nonzero__(self):
289 """False if the set is empty, True otherwise."""
289 """False if the set is empty, True otherwise."""
290 try:
290 try:
291 iter(self).next()
291 iter(self).next()
292 return True
292 return True
293 except StopIteration:
293 except StopIteration:
294 return False
294 return False
295
295
296 def __iter__(self):
296 def __iter__(self):
297 """Generate the ancestors of _initrevs in reverse topological order.
297 """Generate the ancestors of _initrevs in reverse topological order.
298
298
299 If inclusive is False, yield a sequence of revision numbers starting
299 If inclusive is False, yield a sequence of revision numbers starting
300 with the parents of each revision in revs, i.e., each revision is *not*
300 with the parents of each revision in revs, i.e., each revision is *not*
301 considered an ancestor of itself. Results are in breadth-first order:
301 considered an ancestor of itself. Results are in breadth-first order:
302 parents of each rev in revs, then parents of those, etc.
302 parents of each rev in revs, then parents of those, etc.
303
303
304 If inclusive is True, yield all the revs first (ignoring stoprev),
304 If inclusive is True, yield all the revs first (ignoring stoprev),
305 then yield all the ancestors of revs as when inclusive is False.
305 then yield all the ancestors of revs as when inclusive is False.
306 If an element in revs is an ancestor of a different rev it is not
306 If an element in revs is an ancestor of a different rev it is not
307 yielded again."""
307 yielded again."""
308 seen = set()
308 seen = set()
309 revs = self._initrevs
309 revs = self._initrevs
310 if self._inclusive:
310 if self._inclusive:
311 for rev in revs:
311 for rev in revs:
312 yield rev
312 yield rev
313 seen.update(revs)
313 seen.update(revs)
314
314
315 parentrevs = self._parentrevs
315 parentrevs = self._parentrevs
316 stoprev = self._stoprev
316 stoprev = self._stoprev
317 visit = util.deque(revs)
317 visit = collections.deque(revs)
318
318
319 while visit:
319 while visit:
320 for parent in parentrevs(visit.popleft()):
320 for parent in parentrevs(visit.popleft()):
321 if parent >= stoprev and parent not in seen:
321 if parent >= stoprev and parent not in seen:
322 visit.append(parent)
322 visit.append(parent)
323 seen.add(parent)
323 seen.add(parent)
324 yield parent
324 yield parent
325
325
326 def __contains__(self, target):
326 def __contains__(self, target):
327 """Test whether target is an ancestor of self._initrevs."""
327 """Test whether target is an ancestor of self._initrevs."""
328 # Trying to do both __iter__ and __contains__ using the same visit
328 # Trying to do both __iter__ and __contains__ using the same visit
329 # heap and seen set is complex enough that it slows down both. Keep
329 # heap and seen set is complex enough that it slows down both. Keep
330 # them separate.
330 # them separate.
331 seen = self._containsseen
331 seen = self._containsseen
332 if target in seen:
332 if target in seen:
333 return True
333 return True
334
334
335 parentrevs = self._parentrevs
335 parentrevs = self._parentrevs
336 visit = self._containsvisit
336 visit = self._containsvisit
337 stoprev = self._stoprev
337 stoprev = self._stoprev
338 heappop = heapq.heappop
338 heappop = heapq.heappop
339 heappush = heapq.heappush
339 heappush = heapq.heappush
340
340
341 targetseen = False
341 targetseen = False
342
342
343 while visit and -visit[0] > target and not targetseen:
343 while visit and -visit[0] > target and not targetseen:
344 for parent in parentrevs(-heappop(visit)):
344 for parent in parentrevs(-heappop(visit)):
345 if parent < stoprev or parent in seen:
345 if parent < stoprev or parent in seen:
346 continue
346 continue
347 # We need to make sure we push all parents into the heap so
347 # We need to make sure we push all parents into the heap so
348 # that we leave it in a consistent state for future calls.
348 # that we leave it in a consistent state for future calls.
349 heappush(visit, -parent)
349 heappush(visit, -parent)
350 seen.add(parent)
350 seen.add(parent)
351 if parent == target:
351 if parent == target:
352 targetseen = True
352 targetseen = True
353
353
354 return targetseen
354 return targetseen
@@ -1,260 +1,261 b''
1 # changelog bisection for mercurial
1 # changelog bisection for mercurial
2 #
2 #
3 # Copyright 2007 Matt Mackall
3 # Copyright 2007 Matt Mackall
4 # Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
4 # Copyright 2005, 2006 Benoit Boissinot <benoit.boissinot@ens-lyon.org>
5 #
5 #
6 # Inspired by git bisect, extension skeleton taken from mq.py.
6 # Inspired by git bisect, extension skeleton taken from mq.py.
7 #
7 #
8 # This software may be used and distributed according to the terms of the
8 # This software may be used and distributed according to the terms of the
9 # GNU General Public License version 2 or any later version.
9 # GNU General Public License version 2 or any later version.
10
10
11 import collections
11 import os
12 import os
12 import error
13 import error
13 from i18n import _
14 from i18n import _
14 from node import short, hex
15 from node import short, hex
15 import util
16 import util
16
17
17 def bisect(changelog, state):
18 def bisect(changelog, state):
18 """find the next node (if any) for testing during a bisect search.
19 """find the next node (if any) for testing during a bisect search.
19 returns a (nodes, number, good) tuple.
20 returns a (nodes, number, good) tuple.
20
21
21 'nodes' is the final result of the bisect if 'number' is 0.
22 'nodes' is the final result of the bisect if 'number' is 0.
22 Otherwise 'number' indicates the remaining possible candidates for
23 Otherwise 'number' indicates the remaining possible candidates for
23 the search and 'nodes' contains the next bisect target.
24 the search and 'nodes' contains the next bisect target.
24 'good' is True if bisect is searching for a first good changeset, False
25 'good' is True if bisect is searching for a first good changeset, False
25 if searching for a first bad one.
26 if searching for a first bad one.
26 """
27 """
27
28
28 clparents = changelog.parentrevs
29 clparents = changelog.parentrevs
29 skip = set([changelog.rev(n) for n in state['skip']])
30 skip = set([changelog.rev(n) for n in state['skip']])
30
31
31 def buildancestors(bad, good):
32 def buildancestors(bad, good):
32 # only the earliest bad revision matters
33 # only the earliest bad revision matters
33 badrev = min([changelog.rev(n) for n in bad])
34 badrev = min([changelog.rev(n) for n in bad])
34 goodrevs = [changelog.rev(n) for n in good]
35 goodrevs = [changelog.rev(n) for n in good]
35 goodrev = min(goodrevs)
36 goodrev = min(goodrevs)
36 # build visit array
37 # build visit array
37 ancestors = [None] * (len(changelog) + 1) # an extra for [-1]
38 ancestors = [None] * (len(changelog) + 1) # an extra for [-1]
38
39
39 # set nodes descended from goodrevs
40 # set nodes descended from goodrevs
40 for rev in goodrevs:
41 for rev in goodrevs:
41 ancestors[rev] = []
42 ancestors[rev] = []
42 for rev in changelog.revs(goodrev + 1):
43 for rev in changelog.revs(goodrev + 1):
43 for prev in clparents(rev):
44 for prev in clparents(rev):
44 if ancestors[prev] == []:
45 if ancestors[prev] == []:
45 ancestors[rev] = []
46 ancestors[rev] = []
46
47
47 # clear good revs from array
48 # clear good revs from array
48 for rev in goodrevs:
49 for rev in goodrevs:
49 ancestors[rev] = None
50 ancestors[rev] = None
50 for rev in changelog.revs(len(changelog), goodrev):
51 for rev in changelog.revs(len(changelog), goodrev):
51 if ancestors[rev] is None:
52 if ancestors[rev] is None:
52 for prev in clparents(rev):
53 for prev in clparents(rev):
53 ancestors[prev] = None
54 ancestors[prev] = None
54
55
55 if ancestors[badrev] is None:
56 if ancestors[badrev] is None:
56 return badrev, None
57 return badrev, None
57 return badrev, ancestors
58 return badrev, ancestors
58
59
59 good = False
60 good = False
60 badrev, ancestors = buildancestors(state['bad'], state['good'])
61 badrev, ancestors = buildancestors(state['bad'], state['good'])
61 if not ancestors: # looking for bad to good transition?
62 if not ancestors: # looking for bad to good transition?
62 good = True
63 good = True
63 badrev, ancestors = buildancestors(state['good'], state['bad'])
64 badrev, ancestors = buildancestors(state['good'], state['bad'])
64 bad = changelog.node(badrev)
65 bad = changelog.node(badrev)
65 if not ancestors: # now we're confused
66 if not ancestors: # now we're confused
66 if (len(state['bad']) == 1 and len(state['good']) == 1 and
67 if (len(state['bad']) == 1 and len(state['good']) == 1 and
67 state['bad'] != state['good']):
68 state['bad'] != state['good']):
68 raise util.Abort(_("starting revisions are not directly related"))
69 raise util.Abort(_("starting revisions are not directly related"))
69 raise util.Abort(_("inconsistent state, %s:%s is good and bad")
70 raise util.Abort(_("inconsistent state, %s:%s is good and bad")
70 % (badrev, short(bad)))
71 % (badrev, short(bad)))
71
72
72 # build children dict
73 # build children dict
73 children = {}
74 children = {}
74 visit = util.deque([badrev])
75 visit = collections.deque([badrev])
75 candidates = []
76 candidates = []
76 while visit:
77 while visit:
77 rev = visit.popleft()
78 rev = visit.popleft()
78 if ancestors[rev] == []:
79 if ancestors[rev] == []:
79 candidates.append(rev)
80 candidates.append(rev)
80 for prev in clparents(rev):
81 for prev in clparents(rev):
81 if prev != -1:
82 if prev != -1:
82 if prev in children:
83 if prev in children:
83 children[prev].append(rev)
84 children[prev].append(rev)
84 else:
85 else:
85 children[prev] = [rev]
86 children[prev] = [rev]
86 visit.append(prev)
87 visit.append(prev)
87
88
88 candidates.sort()
89 candidates.sort()
89 # have we narrowed it down to one entry?
90 # have we narrowed it down to one entry?
90 # or have all other possible candidates besides 'bad' have been skipped?
91 # or have all other possible candidates besides 'bad' have been skipped?
91 tot = len(candidates)
92 tot = len(candidates)
92 unskipped = [c for c in candidates if (c not in skip) and (c != badrev)]
93 unskipped = [c for c in candidates if (c not in skip) and (c != badrev)]
93 if tot == 1 or not unskipped:
94 if tot == 1 or not unskipped:
94 return ([changelog.node(rev) for rev in candidates], 0, good)
95 return ([changelog.node(rev) for rev in candidates], 0, good)
95 perfect = tot // 2
96 perfect = tot // 2
96
97
97 # find the best node to test
98 # find the best node to test
98 best_rev = None
99 best_rev = None
99 best_len = -1
100 best_len = -1
100 poison = set()
101 poison = set()
101 for rev in candidates:
102 for rev in candidates:
102 if rev in poison:
103 if rev in poison:
103 # poison children
104 # poison children
104 poison.update(children.get(rev, []))
105 poison.update(children.get(rev, []))
105 continue
106 continue
106
107
107 a = ancestors[rev] or [rev]
108 a = ancestors[rev] or [rev]
108 ancestors[rev] = None
109 ancestors[rev] = None
109
110
110 x = len(a) # number of ancestors
111 x = len(a) # number of ancestors
111 y = tot - x # number of non-ancestors
112 y = tot - x # number of non-ancestors
112 value = min(x, y) # how good is this test?
113 value = min(x, y) # how good is this test?
113 if value > best_len and rev not in skip:
114 if value > best_len and rev not in skip:
114 best_len = value
115 best_len = value
115 best_rev = rev
116 best_rev = rev
116 if value == perfect: # found a perfect candidate? quit early
117 if value == perfect: # found a perfect candidate? quit early
117 break
118 break
118
119
119 if y < perfect and rev not in skip: # all downhill from here?
120 if y < perfect and rev not in skip: # all downhill from here?
120 # poison children
121 # poison children
121 poison.update(children.get(rev, []))
122 poison.update(children.get(rev, []))
122 continue
123 continue
123
124
124 for c in children.get(rev, []):
125 for c in children.get(rev, []):
125 if ancestors[c]:
126 if ancestors[c]:
126 ancestors[c] = list(set(ancestors[c] + a))
127 ancestors[c] = list(set(ancestors[c] + a))
127 else:
128 else:
128 ancestors[c] = a + [c]
129 ancestors[c] = a + [c]
129
130
130 assert best_rev is not None
131 assert best_rev is not None
131 best_node = changelog.node(best_rev)
132 best_node = changelog.node(best_rev)
132
133
133 return ([best_node], tot, good)
134 return ([best_node], tot, good)
134
135
135
136
136 def load_state(repo):
137 def load_state(repo):
137 state = {'current': [], 'good': [], 'bad': [], 'skip': []}
138 state = {'current': [], 'good': [], 'bad': [], 'skip': []}
138 if os.path.exists(repo.join("bisect.state")):
139 if os.path.exists(repo.join("bisect.state")):
139 for l in repo.vfs("bisect.state"):
140 for l in repo.vfs("bisect.state"):
140 kind, node = l[:-1].split()
141 kind, node = l[:-1].split()
141 node = repo.lookup(node)
142 node = repo.lookup(node)
142 if kind not in state:
143 if kind not in state:
143 raise util.Abort(_("unknown bisect kind %s") % kind)
144 raise util.Abort(_("unknown bisect kind %s") % kind)
144 state[kind].append(node)
145 state[kind].append(node)
145 return state
146 return state
146
147
147
148
148 def save_state(repo, state):
149 def save_state(repo, state):
149 f = repo.vfs("bisect.state", "w", atomictemp=True)
150 f = repo.vfs("bisect.state", "w", atomictemp=True)
150 wlock = repo.wlock()
151 wlock = repo.wlock()
151 try:
152 try:
152 for kind in sorted(state):
153 for kind in sorted(state):
153 for node in state[kind]:
154 for node in state[kind]:
154 f.write("%s %s\n" % (kind, hex(node)))
155 f.write("%s %s\n" % (kind, hex(node)))
155 f.close()
156 f.close()
156 finally:
157 finally:
157 wlock.release()
158 wlock.release()
158
159
159 def get(repo, status):
160 def get(repo, status):
160 """
161 """
161 Return a list of revision(s) that match the given status:
162 Return a list of revision(s) that match the given status:
162
163
163 - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
164 - ``good``, ``bad``, ``skip``: csets explicitly marked as good/bad/skip
164 - ``goods``, ``bads`` : csets topologically good/bad
165 - ``goods``, ``bads`` : csets topologically good/bad
165 - ``range`` : csets taking part in the bisection
166 - ``range`` : csets taking part in the bisection
166 - ``pruned`` : csets that are goods, bads or skipped
167 - ``pruned`` : csets that are goods, bads or skipped
167 - ``untested`` : csets whose fate is yet unknown
168 - ``untested`` : csets whose fate is yet unknown
168 - ``ignored`` : csets ignored due to DAG topology
169 - ``ignored`` : csets ignored due to DAG topology
169 - ``current`` : the cset currently being bisected
170 - ``current`` : the cset currently being bisected
170 """
171 """
171 state = load_state(repo)
172 state = load_state(repo)
172 if status in ('good', 'bad', 'skip', 'current'):
173 if status in ('good', 'bad', 'skip', 'current'):
173 return map(repo.changelog.rev, state[status])
174 return map(repo.changelog.rev, state[status])
174 else:
175 else:
175 # In the following sets, we do *not* call 'bisect()' with more
176 # In the following sets, we do *not* call 'bisect()' with more
176 # than one level of recursion, because that can be very, very
177 # than one level of recursion, because that can be very, very
177 # time consuming. Instead, we always develop the expression as
178 # time consuming. Instead, we always develop the expression as
178 # much as possible.
179 # much as possible.
179
180
180 # 'range' is all csets that make the bisection:
181 # 'range' is all csets that make the bisection:
181 # - have a good ancestor and a bad descendant, or conversely
182 # - have a good ancestor and a bad descendant, or conversely
182 # that's because the bisection can go either way
183 # that's because the bisection can go either way
183 range = '( bisect(bad)::bisect(good) | bisect(good)::bisect(bad) )'
184 range = '( bisect(bad)::bisect(good) | bisect(good)::bisect(bad) )'
184
185
185 _t = repo.revs('bisect(good)::bisect(bad)')
186 _t = repo.revs('bisect(good)::bisect(bad)')
186 # The sets of topologically good or bad csets
187 # The sets of topologically good or bad csets
187 if len(_t) == 0:
188 if len(_t) == 0:
188 # Goods are topologically after bads
189 # Goods are topologically after bads
189 goods = 'bisect(good)::' # Pruned good csets
190 goods = 'bisect(good)::' # Pruned good csets
190 bads = '::bisect(bad)' # Pruned bad csets
191 bads = '::bisect(bad)' # Pruned bad csets
191 else:
192 else:
192 # Goods are topologically before bads
193 # Goods are topologically before bads
193 goods = '::bisect(good)' # Pruned good csets
194 goods = '::bisect(good)' # Pruned good csets
194 bads = 'bisect(bad)::' # Pruned bad csets
195 bads = 'bisect(bad)::' # Pruned bad csets
195
196
196 # 'pruned' is all csets whose fate is already known: good, bad, skip
197 # 'pruned' is all csets whose fate is already known: good, bad, skip
197 skips = 'bisect(skip)' # Pruned skipped csets
198 skips = 'bisect(skip)' # Pruned skipped csets
198 pruned = '( (%s) | (%s) | (%s) )' % (goods, bads, skips)
199 pruned = '( (%s) | (%s) | (%s) )' % (goods, bads, skips)
199
200
200 # 'untested' is all cset that are- in 'range', but not in 'pruned'
201 # 'untested' is all cset that are- in 'range', but not in 'pruned'
201 untested = '( (%s) - (%s) )' % (range, pruned)
202 untested = '( (%s) - (%s) )' % (range, pruned)
202
203
203 # 'ignored' is all csets that were not used during the bisection
204 # 'ignored' is all csets that were not used during the bisection
204 # due to DAG topology, but may however have had an impact.
205 # due to DAG topology, but may however have had an impact.
205 # E.g., a branch merged between bads and goods, but whose branch-
206 # E.g., a branch merged between bads and goods, but whose branch-
206 # point is out-side of the range.
207 # point is out-side of the range.
207 iba = '::bisect(bad) - ::bisect(good)' # Ignored bads' ancestors
208 iba = '::bisect(bad) - ::bisect(good)' # Ignored bads' ancestors
208 iga = '::bisect(good) - ::bisect(bad)' # Ignored goods' ancestors
209 iga = '::bisect(good) - ::bisect(bad)' # Ignored goods' ancestors
209 ignored = '( ( (%s) | (%s) ) - (%s) )' % (iba, iga, range)
210 ignored = '( ( (%s) | (%s) ) - (%s) )' % (iba, iga, range)
210
211
211 if status == 'range':
212 if status == 'range':
212 return repo.revs(range)
213 return repo.revs(range)
213 elif status == 'pruned':
214 elif status == 'pruned':
214 return repo.revs(pruned)
215 return repo.revs(pruned)
215 elif status == 'untested':
216 elif status == 'untested':
216 return repo.revs(untested)
217 return repo.revs(untested)
217 elif status == 'ignored':
218 elif status == 'ignored':
218 return repo.revs(ignored)
219 return repo.revs(ignored)
219 elif status == "goods":
220 elif status == "goods":
220 return repo.revs(goods)
221 return repo.revs(goods)
221 elif status == "bads":
222 elif status == "bads":
222 return repo.revs(bads)
223 return repo.revs(bads)
223 else:
224 else:
224 raise error.ParseError(_('invalid bisect state'))
225 raise error.ParseError(_('invalid bisect state'))
225
226
226 def label(repo, node):
227 def label(repo, node):
227 rev = repo.changelog.rev(node)
228 rev = repo.changelog.rev(node)
228
229
229 # Try explicit sets
230 # Try explicit sets
230 if rev in get(repo, 'good'):
231 if rev in get(repo, 'good'):
231 # i18n: bisect changeset status
232 # i18n: bisect changeset status
232 return _('good')
233 return _('good')
233 if rev in get(repo, 'bad'):
234 if rev in get(repo, 'bad'):
234 # i18n: bisect changeset status
235 # i18n: bisect changeset status
235 return _('bad')
236 return _('bad')
236 if rev in get(repo, 'skip'):
237 if rev in get(repo, 'skip'):
237 # i18n: bisect changeset status
238 # i18n: bisect changeset status
238 return _('skipped')
239 return _('skipped')
239 if rev in get(repo, 'untested') or rev in get(repo, 'current'):
240 if rev in get(repo, 'untested') or rev in get(repo, 'current'):
240 # i18n: bisect changeset status
241 # i18n: bisect changeset status
241 return _('untested')
242 return _('untested')
242 if rev in get(repo, 'ignored'):
243 if rev in get(repo, 'ignored'):
243 # i18n: bisect changeset status
244 # i18n: bisect changeset status
244 return _('ignored')
245 return _('ignored')
245
246
246 # Try implicit sets
247 # Try implicit sets
247 if rev in get(repo, 'goods'):
248 if rev in get(repo, 'goods'):
248 # i18n: bisect changeset status
249 # i18n: bisect changeset status
249 return _('good (implicit)')
250 return _('good (implicit)')
250 if rev in get(repo, 'bads'):
251 if rev in get(repo, 'bads'):
251 # i18n: bisect changeset status
252 # i18n: bisect changeset status
252 return _('bad (implicit)')
253 return _('bad (implicit)')
253
254
254 return None
255 return None
255
256
256 def shortlabel(label):
257 def shortlabel(label):
257 if label:
258 if label:
258 return label[0].upper()
259 return label[0].upper()
259
260
260 return None
261 return None
@@ -1,2478 +1,2479 b''
1 # patch.py - patch file parsing routines
1 # patch.py - patch file parsing routines
2 #
2 #
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 import collections
9 import cStringIO, email, os, errno, re, posixpath, copy
10 import cStringIO, email, os, errno, re, posixpath, copy
10 import tempfile, zlib, shutil
11 import tempfile, zlib, shutil
11 # On python2.4 you have to import these by name or they fail to
12 # On python2.4 you have to import these by name or they fail to
12 # load. This was not a problem on Python 2.7.
13 # load. This was not a problem on Python 2.7.
13 import email.Generator
14 import email.Generator
14 import email.Parser
15 import email.Parser
15
16
16 from i18n import _
17 from i18n import _
17 from node import hex, short
18 from node import hex, short
18 import cStringIO
19 import cStringIO
19 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding, error
20 import base85, mdiff, scmutil, util, diffhelpers, copies, encoding, error
20 import pathutil
21 import pathutil
21
22
22 gitre = re.compile('diff --git a/(.*) b/(.*)')
23 gitre = re.compile('diff --git a/(.*) b/(.*)')
23 tabsplitter = re.compile(r'(\t+|[^\t]+)')
24 tabsplitter = re.compile(r'(\t+|[^\t]+)')
24
25
25 class PatchError(Exception):
26 class PatchError(Exception):
26 pass
27 pass
27
28
28
29
29 # public functions
30 # public functions
30
31
31 def split(stream):
32 def split(stream):
32 '''return an iterator of individual patches from a stream'''
33 '''return an iterator of individual patches from a stream'''
33 def isheader(line, inheader):
34 def isheader(line, inheader):
34 if inheader and line[0] in (' ', '\t'):
35 if inheader and line[0] in (' ', '\t'):
35 # continuation
36 # continuation
36 return True
37 return True
37 if line[0] in (' ', '-', '+'):
38 if line[0] in (' ', '-', '+'):
38 # diff line - don't check for header pattern in there
39 # diff line - don't check for header pattern in there
39 return False
40 return False
40 l = line.split(': ', 1)
41 l = line.split(': ', 1)
41 return len(l) == 2 and ' ' not in l[0]
42 return len(l) == 2 and ' ' not in l[0]
42
43
43 def chunk(lines):
44 def chunk(lines):
44 return cStringIO.StringIO(''.join(lines))
45 return cStringIO.StringIO(''.join(lines))
45
46
46 def hgsplit(stream, cur):
47 def hgsplit(stream, cur):
47 inheader = True
48 inheader = True
48
49
49 for line in stream:
50 for line in stream:
50 if not line.strip():
51 if not line.strip():
51 inheader = False
52 inheader = False
52 if not inheader and line.startswith('# HG changeset patch'):
53 if not inheader and line.startswith('# HG changeset patch'):
53 yield chunk(cur)
54 yield chunk(cur)
54 cur = []
55 cur = []
55 inheader = True
56 inheader = True
56
57
57 cur.append(line)
58 cur.append(line)
58
59
59 if cur:
60 if cur:
60 yield chunk(cur)
61 yield chunk(cur)
61
62
62 def mboxsplit(stream, cur):
63 def mboxsplit(stream, cur):
63 for line in stream:
64 for line in stream:
64 if line.startswith('From '):
65 if line.startswith('From '):
65 for c in split(chunk(cur[1:])):
66 for c in split(chunk(cur[1:])):
66 yield c
67 yield c
67 cur = []
68 cur = []
68
69
69 cur.append(line)
70 cur.append(line)
70
71
71 if cur:
72 if cur:
72 for c in split(chunk(cur[1:])):
73 for c in split(chunk(cur[1:])):
73 yield c
74 yield c
74
75
75 def mimesplit(stream, cur):
76 def mimesplit(stream, cur):
76 def msgfp(m):
77 def msgfp(m):
77 fp = cStringIO.StringIO()
78 fp = cStringIO.StringIO()
78 g = email.Generator.Generator(fp, mangle_from_=False)
79 g = email.Generator.Generator(fp, mangle_from_=False)
79 g.flatten(m)
80 g.flatten(m)
80 fp.seek(0)
81 fp.seek(0)
81 return fp
82 return fp
82
83
83 for line in stream:
84 for line in stream:
84 cur.append(line)
85 cur.append(line)
85 c = chunk(cur)
86 c = chunk(cur)
86
87
87 m = email.Parser.Parser().parse(c)
88 m = email.Parser.Parser().parse(c)
88 if not m.is_multipart():
89 if not m.is_multipart():
89 yield msgfp(m)
90 yield msgfp(m)
90 else:
91 else:
91 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
92 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
92 for part in m.walk():
93 for part in m.walk():
93 ct = part.get_content_type()
94 ct = part.get_content_type()
94 if ct not in ok_types:
95 if ct not in ok_types:
95 continue
96 continue
96 yield msgfp(part)
97 yield msgfp(part)
97
98
98 def headersplit(stream, cur):
99 def headersplit(stream, cur):
99 inheader = False
100 inheader = False
100
101
101 for line in stream:
102 for line in stream:
102 if not inheader and isheader(line, inheader):
103 if not inheader and isheader(line, inheader):
103 yield chunk(cur)
104 yield chunk(cur)
104 cur = []
105 cur = []
105 inheader = True
106 inheader = True
106 if inheader and not isheader(line, inheader):
107 if inheader and not isheader(line, inheader):
107 inheader = False
108 inheader = False
108
109
109 cur.append(line)
110 cur.append(line)
110
111
111 if cur:
112 if cur:
112 yield chunk(cur)
113 yield chunk(cur)
113
114
114 def remainder(cur):
115 def remainder(cur):
115 yield chunk(cur)
116 yield chunk(cur)
116
117
117 class fiter(object):
118 class fiter(object):
118 def __init__(self, fp):
119 def __init__(self, fp):
119 self.fp = fp
120 self.fp = fp
120
121
121 def __iter__(self):
122 def __iter__(self):
122 return self
123 return self
123
124
124 def next(self):
125 def next(self):
125 l = self.fp.readline()
126 l = self.fp.readline()
126 if not l:
127 if not l:
127 raise StopIteration
128 raise StopIteration
128 return l
129 return l
129
130
130 inheader = False
131 inheader = False
131 cur = []
132 cur = []
132
133
133 mimeheaders = ['content-type']
134 mimeheaders = ['content-type']
134
135
135 if not util.safehasattr(stream, 'next'):
136 if not util.safehasattr(stream, 'next'):
136 # http responses, for example, have readline but not next
137 # http responses, for example, have readline but not next
137 stream = fiter(stream)
138 stream = fiter(stream)
138
139
139 for line in stream:
140 for line in stream:
140 cur.append(line)
141 cur.append(line)
141 if line.startswith('# HG changeset patch'):
142 if line.startswith('# HG changeset patch'):
142 return hgsplit(stream, cur)
143 return hgsplit(stream, cur)
143 elif line.startswith('From '):
144 elif line.startswith('From '):
144 return mboxsplit(stream, cur)
145 return mboxsplit(stream, cur)
145 elif isheader(line, inheader):
146 elif isheader(line, inheader):
146 inheader = True
147 inheader = True
147 if line.split(':', 1)[0].lower() in mimeheaders:
148 if line.split(':', 1)[0].lower() in mimeheaders:
148 # let email parser handle this
149 # let email parser handle this
149 return mimesplit(stream, cur)
150 return mimesplit(stream, cur)
150 elif line.startswith('--- ') and inheader:
151 elif line.startswith('--- ') and inheader:
151 # No evil headers seen by diff start, split by hand
152 # No evil headers seen by diff start, split by hand
152 return headersplit(stream, cur)
153 return headersplit(stream, cur)
153 # Not enough info, keep reading
154 # Not enough info, keep reading
154
155
155 # if we are here, we have a very plain patch
156 # if we are here, we have a very plain patch
156 return remainder(cur)
157 return remainder(cur)
157
158
158 def extract(ui, fileobj):
159 def extract(ui, fileobj):
159 '''extract patch from data read from fileobj.
160 '''extract patch from data read from fileobj.
160
161
161 patch can be a normal patch or contained in an email message.
162 patch can be a normal patch or contained in an email message.
162
163
163 return tuple (filename, message, user, date, branch, node, p1, p2).
164 return tuple (filename, message, user, date, branch, node, p1, p2).
164 Any item in the returned tuple can be None. If filename is None,
165 Any item in the returned tuple can be None. If filename is None,
165 fileobj did not contain a patch. Caller must unlink filename when done.'''
166 fileobj did not contain a patch. Caller must unlink filename when done.'''
166
167
167 # attempt to detect the start of a patch
168 # attempt to detect the start of a patch
168 # (this heuristic is borrowed from quilt)
169 # (this heuristic is borrowed from quilt)
169 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
170 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |'
170 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
171 r'retrieving revision [0-9]+(\.[0-9]+)*$|'
171 r'---[ \t].*?^\+\+\+[ \t]|'
172 r'---[ \t].*?^\+\+\+[ \t]|'
172 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
173 r'\*\*\*[ \t].*?^---[ \t])', re.MULTILINE|re.DOTALL)
173
174
174 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
175 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
175 tmpfp = os.fdopen(fd, 'w')
176 tmpfp = os.fdopen(fd, 'w')
176 try:
177 try:
177 msg = email.Parser.Parser().parse(fileobj)
178 msg = email.Parser.Parser().parse(fileobj)
178
179
179 subject = msg['Subject']
180 subject = msg['Subject']
180 user = msg['From']
181 user = msg['From']
181 if not subject and not user:
182 if not subject and not user:
182 # Not an email, restore parsed headers if any
183 # Not an email, restore parsed headers if any
183 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
184 subject = '\n'.join(': '.join(h) for h in msg.items()) + '\n'
184
185
185 # should try to parse msg['Date']
186 # should try to parse msg['Date']
186 date = None
187 date = None
187 nodeid = None
188 nodeid = None
188 branch = None
189 branch = None
189 parents = []
190 parents = []
190
191
191 if subject:
192 if subject:
192 if subject.startswith('[PATCH'):
193 if subject.startswith('[PATCH'):
193 pend = subject.find(']')
194 pend = subject.find(']')
194 if pend >= 0:
195 if pend >= 0:
195 subject = subject[pend + 1:].lstrip()
196 subject = subject[pend + 1:].lstrip()
196 subject = re.sub(r'\n[ \t]+', ' ', subject)
197 subject = re.sub(r'\n[ \t]+', ' ', subject)
197 ui.debug('Subject: %s\n' % subject)
198 ui.debug('Subject: %s\n' % subject)
198 if user:
199 if user:
199 ui.debug('From: %s\n' % user)
200 ui.debug('From: %s\n' % user)
200 diffs_seen = 0
201 diffs_seen = 0
201 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
202 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
202 message = ''
203 message = ''
203 for part in msg.walk():
204 for part in msg.walk():
204 content_type = part.get_content_type()
205 content_type = part.get_content_type()
205 ui.debug('Content-Type: %s\n' % content_type)
206 ui.debug('Content-Type: %s\n' % content_type)
206 if content_type not in ok_types:
207 if content_type not in ok_types:
207 continue
208 continue
208 payload = part.get_payload(decode=True)
209 payload = part.get_payload(decode=True)
209 m = diffre.search(payload)
210 m = diffre.search(payload)
210 if m:
211 if m:
211 hgpatch = False
212 hgpatch = False
212 hgpatchheader = False
213 hgpatchheader = False
213 ignoretext = False
214 ignoretext = False
214
215
215 ui.debug('found patch at byte %d\n' % m.start(0))
216 ui.debug('found patch at byte %d\n' % m.start(0))
216 diffs_seen += 1
217 diffs_seen += 1
217 cfp = cStringIO.StringIO()
218 cfp = cStringIO.StringIO()
218 for line in payload[:m.start(0)].splitlines():
219 for line in payload[:m.start(0)].splitlines():
219 if line.startswith('# HG changeset patch') and not hgpatch:
220 if line.startswith('# HG changeset patch') and not hgpatch:
220 ui.debug('patch generated by hg export\n')
221 ui.debug('patch generated by hg export\n')
221 hgpatch = True
222 hgpatch = True
222 hgpatchheader = True
223 hgpatchheader = True
223 # drop earlier commit message content
224 # drop earlier commit message content
224 cfp.seek(0)
225 cfp.seek(0)
225 cfp.truncate()
226 cfp.truncate()
226 subject = None
227 subject = None
227 elif hgpatchheader:
228 elif hgpatchheader:
228 if line.startswith('# User '):
229 if line.startswith('# User '):
229 user = line[7:]
230 user = line[7:]
230 ui.debug('From: %s\n' % user)
231 ui.debug('From: %s\n' % user)
231 elif line.startswith("# Date "):
232 elif line.startswith("# Date "):
232 date = line[7:]
233 date = line[7:]
233 elif line.startswith("# Branch "):
234 elif line.startswith("# Branch "):
234 branch = line[9:]
235 branch = line[9:]
235 elif line.startswith("# Node ID "):
236 elif line.startswith("# Node ID "):
236 nodeid = line[10:]
237 nodeid = line[10:]
237 elif line.startswith("# Parent "):
238 elif line.startswith("# Parent "):
238 parents.append(line[9:].lstrip())
239 parents.append(line[9:].lstrip())
239 elif not line.startswith("# "):
240 elif not line.startswith("# "):
240 hgpatchheader = False
241 hgpatchheader = False
241 elif line == '---':
242 elif line == '---':
242 ignoretext = True
243 ignoretext = True
243 if not hgpatchheader and not ignoretext:
244 if not hgpatchheader and not ignoretext:
244 cfp.write(line)
245 cfp.write(line)
245 cfp.write('\n')
246 cfp.write('\n')
246 message = cfp.getvalue()
247 message = cfp.getvalue()
247 if tmpfp:
248 if tmpfp:
248 tmpfp.write(payload)
249 tmpfp.write(payload)
249 if not payload.endswith('\n'):
250 if not payload.endswith('\n'):
250 tmpfp.write('\n')
251 tmpfp.write('\n')
251 elif not diffs_seen and message and content_type == 'text/plain':
252 elif not diffs_seen and message and content_type == 'text/plain':
252 message += '\n' + payload
253 message += '\n' + payload
253 except: # re-raises
254 except: # re-raises
254 tmpfp.close()
255 tmpfp.close()
255 os.unlink(tmpname)
256 os.unlink(tmpname)
256 raise
257 raise
257
258
258 if subject and not message.startswith(subject):
259 if subject and not message.startswith(subject):
259 message = '%s\n%s' % (subject, message)
260 message = '%s\n%s' % (subject, message)
260 tmpfp.close()
261 tmpfp.close()
261 if not diffs_seen:
262 if not diffs_seen:
262 os.unlink(tmpname)
263 os.unlink(tmpname)
263 return None, message, user, date, branch, None, None, None
264 return None, message, user, date, branch, None, None, None
264
265
265 if parents:
266 if parents:
266 p1 = parents.pop(0)
267 p1 = parents.pop(0)
267 else:
268 else:
268 p1 = None
269 p1 = None
269
270
270 if parents:
271 if parents:
271 p2 = parents.pop(0)
272 p2 = parents.pop(0)
272 else:
273 else:
273 p2 = None
274 p2 = None
274
275
275 return tmpname, message, user, date, branch, nodeid, p1, p2
276 return tmpname, message, user, date, branch, nodeid, p1, p2
276
277
277 class patchmeta(object):
278 class patchmeta(object):
278 """Patched file metadata
279 """Patched file metadata
279
280
280 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
281 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
281 or COPY. 'path' is patched file path. 'oldpath' is set to the
282 or COPY. 'path' is patched file path. 'oldpath' is set to the
282 origin file when 'op' is either COPY or RENAME, None otherwise. If
283 origin file when 'op' is either COPY or RENAME, None otherwise. If
283 file mode is changed, 'mode' is a tuple (islink, isexec) where
284 file mode is changed, 'mode' is a tuple (islink, isexec) where
284 'islink' is True if the file is a symlink and 'isexec' is True if
285 'islink' is True if the file is a symlink and 'isexec' is True if
285 the file is executable. Otherwise, 'mode' is None.
286 the file is executable. Otherwise, 'mode' is None.
286 """
287 """
287 def __init__(self, path):
288 def __init__(self, path):
288 self.path = path
289 self.path = path
289 self.oldpath = None
290 self.oldpath = None
290 self.mode = None
291 self.mode = None
291 self.op = 'MODIFY'
292 self.op = 'MODIFY'
292 self.binary = False
293 self.binary = False
293
294
294 def setmode(self, mode):
295 def setmode(self, mode):
295 islink = mode & 020000
296 islink = mode & 020000
296 isexec = mode & 0100
297 isexec = mode & 0100
297 self.mode = (islink, isexec)
298 self.mode = (islink, isexec)
298
299
299 def copy(self):
300 def copy(self):
300 other = patchmeta(self.path)
301 other = patchmeta(self.path)
301 other.oldpath = self.oldpath
302 other.oldpath = self.oldpath
302 other.mode = self.mode
303 other.mode = self.mode
303 other.op = self.op
304 other.op = self.op
304 other.binary = self.binary
305 other.binary = self.binary
305 return other
306 return other
306
307
307 def _ispatchinga(self, afile):
308 def _ispatchinga(self, afile):
308 if afile == '/dev/null':
309 if afile == '/dev/null':
309 return self.op == 'ADD'
310 return self.op == 'ADD'
310 return afile == 'a/' + (self.oldpath or self.path)
311 return afile == 'a/' + (self.oldpath or self.path)
311
312
312 def _ispatchingb(self, bfile):
313 def _ispatchingb(self, bfile):
313 if bfile == '/dev/null':
314 if bfile == '/dev/null':
314 return self.op == 'DELETE'
315 return self.op == 'DELETE'
315 return bfile == 'b/' + self.path
316 return bfile == 'b/' + self.path
316
317
317 def ispatching(self, afile, bfile):
318 def ispatching(self, afile, bfile):
318 return self._ispatchinga(afile) and self._ispatchingb(bfile)
319 return self._ispatchinga(afile) and self._ispatchingb(bfile)
319
320
320 def __repr__(self):
321 def __repr__(self):
321 return "<patchmeta %s %r>" % (self.op, self.path)
322 return "<patchmeta %s %r>" % (self.op, self.path)
322
323
323 def readgitpatch(lr):
324 def readgitpatch(lr):
324 """extract git-style metadata about patches from <patchname>"""
325 """extract git-style metadata about patches from <patchname>"""
325
326
326 # Filter patch for git information
327 # Filter patch for git information
327 gp = None
328 gp = None
328 gitpatches = []
329 gitpatches = []
329 for line in lr:
330 for line in lr:
330 line = line.rstrip(' \r\n')
331 line = line.rstrip(' \r\n')
331 if line.startswith('diff --git a/'):
332 if line.startswith('diff --git a/'):
332 m = gitre.match(line)
333 m = gitre.match(line)
333 if m:
334 if m:
334 if gp:
335 if gp:
335 gitpatches.append(gp)
336 gitpatches.append(gp)
336 dst = m.group(2)
337 dst = m.group(2)
337 gp = patchmeta(dst)
338 gp = patchmeta(dst)
338 elif gp:
339 elif gp:
339 if line.startswith('--- '):
340 if line.startswith('--- '):
340 gitpatches.append(gp)
341 gitpatches.append(gp)
341 gp = None
342 gp = None
342 continue
343 continue
343 if line.startswith('rename from '):
344 if line.startswith('rename from '):
344 gp.op = 'RENAME'
345 gp.op = 'RENAME'
345 gp.oldpath = line[12:]
346 gp.oldpath = line[12:]
346 elif line.startswith('rename to '):
347 elif line.startswith('rename to '):
347 gp.path = line[10:]
348 gp.path = line[10:]
348 elif line.startswith('copy from '):
349 elif line.startswith('copy from '):
349 gp.op = 'COPY'
350 gp.op = 'COPY'
350 gp.oldpath = line[10:]
351 gp.oldpath = line[10:]
351 elif line.startswith('copy to '):
352 elif line.startswith('copy to '):
352 gp.path = line[8:]
353 gp.path = line[8:]
353 elif line.startswith('deleted file'):
354 elif line.startswith('deleted file'):
354 gp.op = 'DELETE'
355 gp.op = 'DELETE'
355 elif line.startswith('new file mode '):
356 elif line.startswith('new file mode '):
356 gp.op = 'ADD'
357 gp.op = 'ADD'
357 gp.setmode(int(line[-6:], 8))
358 gp.setmode(int(line[-6:], 8))
358 elif line.startswith('new mode '):
359 elif line.startswith('new mode '):
359 gp.setmode(int(line[-6:], 8))
360 gp.setmode(int(line[-6:], 8))
360 elif line.startswith('GIT binary patch'):
361 elif line.startswith('GIT binary patch'):
361 gp.binary = True
362 gp.binary = True
362 if gp:
363 if gp:
363 gitpatches.append(gp)
364 gitpatches.append(gp)
364
365
365 return gitpatches
366 return gitpatches
366
367
367 class linereader(object):
368 class linereader(object):
368 # simple class to allow pushing lines back into the input stream
369 # simple class to allow pushing lines back into the input stream
369 def __init__(self, fp):
370 def __init__(self, fp):
370 self.fp = fp
371 self.fp = fp
371 self.buf = []
372 self.buf = []
372
373
373 def push(self, line):
374 def push(self, line):
374 if line is not None:
375 if line is not None:
375 self.buf.append(line)
376 self.buf.append(line)
376
377
377 def readline(self):
378 def readline(self):
378 if self.buf:
379 if self.buf:
379 l = self.buf[0]
380 l = self.buf[0]
380 del self.buf[0]
381 del self.buf[0]
381 return l
382 return l
382 return self.fp.readline()
383 return self.fp.readline()
383
384
384 def __iter__(self):
385 def __iter__(self):
385 while True:
386 while True:
386 l = self.readline()
387 l = self.readline()
387 if not l:
388 if not l:
388 break
389 break
389 yield l
390 yield l
390
391
391 class abstractbackend(object):
392 class abstractbackend(object):
392 def __init__(self, ui):
393 def __init__(self, ui):
393 self.ui = ui
394 self.ui = ui
394
395
395 def getfile(self, fname):
396 def getfile(self, fname):
396 """Return target file data and flags as a (data, (islink,
397 """Return target file data and flags as a (data, (islink,
397 isexec)) tuple. Data is None if file is missing/deleted.
398 isexec)) tuple. Data is None if file is missing/deleted.
398 """
399 """
399 raise NotImplementedError
400 raise NotImplementedError
400
401
401 def setfile(self, fname, data, mode, copysource):
402 def setfile(self, fname, data, mode, copysource):
402 """Write data to target file fname and set its mode. mode is a
403 """Write data to target file fname and set its mode. mode is a
403 (islink, isexec) tuple. If data is None, the file content should
404 (islink, isexec) tuple. If data is None, the file content should
404 be left unchanged. If the file is modified after being copied,
405 be left unchanged. If the file is modified after being copied,
405 copysource is set to the original file name.
406 copysource is set to the original file name.
406 """
407 """
407 raise NotImplementedError
408 raise NotImplementedError
408
409
409 def unlink(self, fname):
410 def unlink(self, fname):
410 """Unlink target file."""
411 """Unlink target file."""
411 raise NotImplementedError
412 raise NotImplementedError
412
413
413 def writerej(self, fname, failed, total, lines):
414 def writerej(self, fname, failed, total, lines):
414 """Write rejected lines for fname. total is the number of hunks
415 """Write rejected lines for fname. total is the number of hunks
415 which failed to apply and total the total number of hunks for this
416 which failed to apply and total the total number of hunks for this
416 files.
417 files.
417 """
418 """
418 pass
419 pass
419
420
420 def exists(self, fname):
421 def exists(self, fname):
421 raise NotImplementedError
422 raise NotImplementedError
422
423
423 class fsbackend(abstractbackend):
424 class fsbackend(abstractbackend):
424 def __init__(self, ui, basedir):
425 def __init__(self, ui, basedir):
425 super(fsbackend, self).__init__(ui)
426 super(fsbackend, self).__init__(ui)
426 self.opener = scmutil.opener(basedir)
427 self.opener = scmutil.opener(basedir)
427
428
428 def _join(self, f):
429 def _join(self, f):
429 return os.path.join(self.opener.base, f)
430 return os.path.join(self.opener.base, f)
430
431
431 def getfile(self, fname):
432 def getfile(self, fname):
432 if self.opener.islink(fname):
433 if self.opener.islink(fname):
433 return (self.opener.readlink(fname), (True, False))
434 return (self.opener.readlink(fname), (True, False))
434
435
435 isexec = False
436 isexec = False
436 try:
437 try:
437 isexec = self.opener.lstat(fname).st_mode & 0100 != 0
438 isexec = self.opener.lstat(fname).st_mode & 0100 != 0
438 except OSError, e:
439 except OSError, e:
439 if e.errno != errno.ENOENT:
440 if e.errno != errno.ENOENT:
440 raise
441 raise
441 try:
442 try:
442 return (self.opener.read(fname), (False, isexec))
443 return (self.opener.read(fname), (False, isexec))
443 except IOError, e:
444 except IOError, e:
444 if e.errno != errno.ENOENT:
445 if e.errno != errno.ENOENT:
445 raise
446 raise
446 return None, None
447 return None, None
447
448
448 def setfile(self, fname, data, mode, copysource):
449 def setfile(self, fname, data, mode, copysource):
449 islink, isexec = mode
450 islink, isexec = mode
450 if data is None:
451 if data is None:
451 self.opener.setflags(fname, islink, isexec)
452 self.opener.setflags(fname, islink, isexec)
452 return
453 return
453 if islink:
454 if islink:
454 self.opener.symlink(data, fname)
455 self.opener.symlink(data, fname)
455 else:
456 else:
456 self.opener.write(fname, data)
457 self.opener.write(fname, data)
457 if isexec:
458 if isexec:
458 self.opener.setflags(fname, False, True)
459 self.opener.setflags(fname, False, True)
459
460
460 def unlink(self, fname):
461 def unlink(self, fname):
461 self.opener.unlinkpath(fname, ignoremissing=True)
462 self.opener.unlinkpath(fname, ignoremissing=True)
462
463
463 def writerej(self, fname, failed, total, lines):
464 def writerej(self, fname, failed, total, lines):
464 fname = fname + ".rej"
465 fname = fname + ".rej"
465 self.ui.warn(
466 self.ui.warn(
466 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
467 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
467 (failed, total, fname))
468 (failed, total, fname))
468 fp = self.opener(fname, 'w')
469 fp = self.opener(fname, 'w')
469 fp.writelines(lines)
470 fp.writelines(lines)
470 fp.close()
471 fp.close()
471
472
472 def exists(self, fname):
473 def exists(self, fname):
473 return self.opener.lexists(fname)
474 return self.opener.lexists(fname)
474
475
475 class workingbackend(fsbackend):
476 class workingbackend(fsbackend):
476 def __init__(self, ui, repo, similarity):
477 def __init__(self, ui, repo, similarity):
477 super(workingbackend, self).__init__(ui, repo.root)
478 super(workingbackend, self).__init__(ui, repo.root)
478 self.repo = repo
479 self.repo = repo
479 self.similarity = similarity
480 self.similarity = similarity
480 self.removed = set()
481 self.removed = set()
481 self.changed = set()
482 self.changed = set()
482 self.copied = []
483 self.copied = []
483
484
484 def _checkknown(self, fname):
485 def _checkknown(self, fname):
485 if self.repo.dirstate[fname] == '?' and self.exists(fname):
486 if self.repo.dirstate[fname] == '?' and self.exists(fname):
486 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
487 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
487
488
488 def setfile(self, fname, data, mode, copysource):
489 def setfile(self, fname, data, mode, copysource):
489 self._checkknown(fname)
490 self._checkknown(fname)
490 super(workingbackend, self).setfile(fname, data, mode, copysource)
491 super(workingbackend, self).setfile(fname, data, mode, copysource)
491 if copysource is not None:
492 if copysource is not None:
492 self.copied.append((copysource, fname))
493 self.copied.append((copysource, fname))
493 self.changed.add(fname)
494 self.changed.add(fname)
494
495
495 def unlink(self, fname):
496 def unlink(self, fname):
496 self._checkknown(fname)
497 self._checkknown(fname)
497 super(workingbackend, self).unlink(fname)
498 super(workingbackend, self).unlink(fname)
498 self.removed.add(fname)
499 self.removed.add(fname)
499 self.changed.add(fname)
500 self.changed.add(fname)
500
501
501 def close(self):
502 def close(self):
502 wctx = self.repo[None]
503 wctx = self.repo[None]
503 changed = set(self.changed)
504 changed = set(self.changed)
504 for src, dst in self.copied:
505 for src, dst in self.copied:
505 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
506 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
506 if self.removed:
507 if self.removed:
507 wctx.forget(sorted(self.removed))
508 wctx.forget(sorted(self.removed))
508 for f in self.removed:
509 for f in self.removed:
509 if f not in self.repo.dirstate:
510 if f not in self.repo.dirstate:
510 # File was deleted and no longer belongs to the
511 # File was deleted and no longer belongs to the
511 # dirstate, it was probably marked added then
512 # dirstate, it was probably marked added then
512 # deleted, and should not be considered by
513 # deleted, and should not be considered by
513 # marktouched().
514 # marktouched().
514 changed.discard(f)
515 changed.discard(f)
515 if changed:
516 if changed:
516 scmutil.marktouched(self.repo, changed, self.similarity)
517 scmutil.marktouched(self.repo, changed, self.similarity)
517 return sorted(self.changed)
518 return sorted(self.changed)
518
519
519 class filestore(object):
520 class filestore(object):
520 def __init__(self, maxsize=None):
521 def __init__(self, maxsize=None):
521 self.opener = None
522 self.opener = None
522 self.files = {}
523 self.files = {}
523 self.created = 0
524 self.created = 0
524 self.maxsize = maxsize
525 self.maxsize = maxsize
525 if self.maxsize is None:
526 if self.maxsize is None:
526 self.maxsize = 4*(2**20)
527 self.maxsize = 4*(2**20)
527 self.size = 0
528 self.size = 0
528 self.data = {}
529 self.data = {}
529
530
530 def setfile(self, fname, data, mode, copied=None):
531 def setfile(self, fname, data, mode, copied=None):
531 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
532 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
532 self.data[fname] = (data, mode, copied)
533 self.data[fname] = (data, mode, copied)
533 self.size += len(data)
534 self.size += len(data)
534 else:
535 else:
535 if self.opener is None:
536 if self.opener is None:
536 root = tempfile.mkdtemp(prefix='hg-patch-')
537 root = tempfile.mkdtemp(prefix='hg-patch-')
537 self.opener = scmutil.opener(root)
538 self.opener = scmutil.opener(root)
538 # Avoid filename issues with these simple names
539 # Avoid filename issues with these simple names
539 fn = str(self.created)
540 fn = str(self.created)
540 self.opener.write(fn, data)
541 self.opener.write(fn, data)
541 self.created += 1
542 self.created += 1
542 self.files[fname] = (fn, mode, copied)
543 self.files[fname] = (fn, mode, copied)
543
544
544 def getfile(self, fname):
545 def getfile(self, fname):
545 if fname in self.data:
546 if fname in self.data:
546 return self.data[fname]
547 return self.data[fname]
547 if not self.opener or fname not in self.files:
548 if not self.opener or fname not in self.files:
548 return None, None, None
549 return None, None, None
549 fn, mode, copied = self.files[fname]
550 fn, mode, copied = self.files[fname]
550 return self.opener.read(fn), mode, copied
551 return self.opener.read(fn), mode, copied
551
552
552 def close(self):
553 def close(self):
553 if self.opener:
554 if self.opener:
554 shutil.rmtree(self.opener.base)
555 shutil.rmtree(self.opener.base)
555
556
556 class repobackend(abstractbackend):
557 class repobackend(abstractbackend):
557 def __init__(self, ui, repo, ctx, store):
558 def __init__(self, ui, repo, ctx, store):
558 super(repobackend, self).__init__(ui)
559 super(repobackend, self).__init__(ui)
559 self.repo = repo
560 self.repo = repo
560 self.ctx = ctx
561 self.ctx = ctx
561 self.store = store
562 self.store = store
562 self.changed = set()
563 self.changed = set()
563 self.removed = set()
564 self.removed = set()
564 self.copied = {}
565 self.copied = {}
565
566
566 def _checkknown(self, fname):
567 def _checkknown(self, fname):
567 if fname not in self.ctx:
568 if fname not in self.ctx:
568 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
569 raise PatchError(_('cannot patch %s: file is not tracked') % fname)
569
570
570 def getfile(self, fname):
571 def getfile(self, fname):
571 try:
572 try:
572 fctx = self.ctx[fname]
573 fctx = self.ctx[fname]
573 except error.LookupError:
574 except error.LookupError:
574 return None, None
575 return None, None
575 flags = fctx.flags()
576 flags = fctx.flags()
576 return fctx.data(), ('l' in flags, 'x' in flags)
577 return fctx.data(), ('l' in flags, 'x' in flags)
577
578
578 def setfile(self, fname, data, mode, copysource):
579 def setfile(self, fname, data, mode, copysource):
579 if copysource:
580 if copysource:
580 self._checkknown(copysource)
581 self._checkknown(copysource)
581 if data is None:
582 if data is None:
582 data = self.ctx[fname].data()
583 data = self.ctx[fname].data()
583 self.store.setfile(fname, data, mode, copysource)
584 self.store.setfile(fname, data, mode, copysource)
584 self.changed.add(fname)
585 self.changed.add(fname)
585 if copysource:
586 if copysource:
586 self.copied[fname] = copysource
587 self.copied[fname] = copysource
587
588
588 def unlink(self, fname):
589 def unlink(self, fname):
589 self._checkknown(fname)
590 self._checkknown(fname)
590 self.removed.add(fname)
591 self.removed.add(fname)
591
592
592 def exists(self, fname):
593 def exists(self, fname):
593 return fname in self.ctx
594 return fname in self.ctx
594
595
595 def close(self):
596 def close(self):
596 return self.changed | self.removed
597 return self.changed | self.removed
597
598
598 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
599 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
599 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
600 unidesc = re.compile('@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
600 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
601 contextdesc = re.compile('(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
601 eolmodes = ['strict', 'crlf', 'lf', 'auto']
602 eolmodes = ['strict', 'crlf', 'lf', 'auto']
602
603
603 class patchfile(object):
604 class patchfile(object):
604 def __init__(self, ui, gp, backend, store, eolmode='strict'):
605 def __init__(self, ui, gp, backend, store, eolmode='strict'):
605 self.fname = gp.path
606 self.fname = gp.path
606 self.eolmode = eolmode
607 self.eolmode = eolmode
607 self.eol = None
608 self.eol = None
608 self.backend = backend
609 self.backend = backend
609 self.ui = ui
610 self.ui = ui
610 self.lines = []
611 self.lines = []
611 self.exists = False
612 self.exists = False
612 self.missing = True
613 self.missing = True
613 self.mode = gp.mode
614 self.mode = gp.mode
614 self.copysource = gp.oldpath
615 self.copysource = gp.oldpath
615 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
616 self.create = gp.op in ('ADD', 'COPY', 'RENAME')
616 self.remove = gp.op == 'DELETE'
617 self.remove = gp.op == 'DELETE'
617 if self.copysource is None:
618 if self.copysource is None:
618 data, mode = backend.getfile(self.fname)
619 data, mode = backend.getfile(self.fname)
619 else:
620 else:
620 data, mode = store.getfile(self.copysource)[:2]
621 data, mode = store.getfile(self.copysource)[:2]
621 if data is not None:
622 if data is not None:
622 self.exists = self.copysource is None or backend.exists(self.fname)
623 self.exists = self.copysource is None or backend.exists(self.fname)
623 self.missing = False
624 self.missing = False
624 if data:
625 if data:
625 self.lines = mdiff.splitnewlines(data)
626 self.lines = mdiff.splitnewlines(data)
626 if self.mode is None:
627 if self.mode is None:
627 self.mode = mode
628 self.mode = mode
628 if self.lines:
629 if self.lines:
629 # Normalize line endings
630 # Normalize line endings
630 if self.lines[0].endswith('\r\n'):
631 if self.lines[0].endswith('\r\n'):
631 self.eol = '\r\n'
632 self.eol = '\r\n'
632 elif self.lines[0].endswith('\n'):
633 elif self.lines[0].endswith('\n'):
633 self.eol = '\n'
634 self.eol = '\n'
634 if eolmode != 'strict':
635 if eolmode != 'strict':
635 nlines = []
636 nlines = []
636 for l in self.lines:
637 for l in self.lines:
637 if l.endswith('\r\n'):
638 if l.endswith('\r\n'):
638 l = l[:-2] + '\n'
639 l = l[:-2] + '\n'
639 nlines.append(l)
640 nlines.append(l)
640 self.lines = nlines
641 self.lines = nlines
641 else:
642 else:
642 if self.create:
643 if self.create:
643 self.missing = False
644 self.missing = False
644 if self.mode is None:
645 if self.mode is None:
645 self.mode = (False, False)
646 self.mode = (False, False)
646 if self.missing:
647 if self.missing:
647 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
648 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
648
649
649 self.hash = {}
650 self.hash = {}
650 self.dirty = 0
651 self.dirty = 0
651 self.offset = 0
652 self.offset = 0
652 self.skew = 0
653 self.skew = 0
653 self.rej = []
654 self.rej = []
654 self.fileprinted = False
655 self.fileprinted = False
655 self.printfile(False)
656 self.printfile(False)
656 self.hunks = 0
657 self.hunks = 0
657
658
658 def writelines(self, fname, lines, mode):
659 def writelines(self, fname, lines, mode):
659 if self.eolmode == 'auto':
660 if self.eolmode == 'auto':
660 eol = self.eol
661 eol = self.eol
661 elif self.eolmode == 'crlf':
662 elif self.eolmode == 'crlf':
662 eol = '\r\n'
663 eol = '\r\n'
663 else:
664 else:
664 eol = '\n'
665 eol = '\n'
665
666
666 if self.eolmode != 'strict' and eol and eol != '\n':
667 if self.eolmode != 'strict' and eol and eol != '\n':
667 rawlines = []
668 rawlines = []
668 for l in lines:
669 for l in lines:
669 if l and l[-1] == '\n':
670 if l and l[-1] == '\n':
670 l = l[:-1] + eol
671 l = l[:-1] + eol
671 rawlines.append(l)
672 rawlines.append(l)
672 lines = rawlines
673 lines = rawlines
673
674
674 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
675 self.backend.setfile(fname, ''.join(lines), mode, self.copysource)
675
676
676 def printfile(self, warn):
677 def printfile(self, warn):
677 if self.fileprinted:
678 if self.fileprinted:
678 return
679 return
679 if warn or self.ui.verbose:
680 if warn or self.ui.verbose:
680 self.fileprinted = True
681 self.fileprinted = True
681 s = _("patching file %s\n") % self.fname
682 s = _("patching file %s\n") % self.fname
682 if warn:
683 if warn:
683 self.ui.warn(s)
684 self.ui.warn(s)
684 else:
685 else:
685 self.ui.note(s)
686 self.ui.note(s)
686
687
687
688
688 def findlines(self, l, linenum):
689 def findlines(self, l, linenum):
689 # looks through the hash and finds candidate lines. The
690 # looks through the hash and finds candidate lines. The
690 # result is a list of line numbers sorted based on distance
691 # result is a list of line numbers sorted based on distance
691 # from linenum
692 # from linenum
692
693
693 cand = self.hash.get(l, [])
694 cand = self.hash.get(l, [])
694 if len(cand) > 1:
695 if len(cand) > 1:
695 # resort our list of potentials forward then back.
696 # resort our list of potentials forward then back.
696 cand.sort(key=lambda x: abs(x - linenum))
697 cand.sort(key=lambda x: abs(x - linenum))
697 return cand
698 return cand
698
699
699 def write_rej(self):
700 def write_rej(self):
700 # our rejects are a little different from patch(1). This always
701 # our rejects are a little different from patch(1). This always
701 # creates rejects in the same form as the original patch. A file
702 # creates rejects in the same form as the original patch. A file
702 # header is inserted so that you can run the reject through patch again
703 # header is inserted so that you can run the reject through patch again
703 # without having to type the filename.
704 # without having to type the filename.
704 if not self.rej:
705 if not self.rej:
705 return
706 return
706 base = os.path.basename(self.fname)
707 base = os.path.basename(self.fname)
707 lines = ["--- %s\n+++ %s\n" % (base, base)]
708 lines = ["--- %s\n+++ %s\n" % (base, base)]
708 for x in self.rej:
709 for x in self.rej:
709 for l in x.hunk:
710 for l in x.hunk:
710 lines.append(l)
711 lines.append(l)
711 if l[-1] != '\n':
712 if l[-1] != '\n':
712 lines.append("\n\ No newline at end of file\n")
713 lines.append("\n\ No newline at end of file\n")
713 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
714 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
714
715
715 def apply(self, h):
716 def apply(self, h):
716 if not h.complete():
717 if not h.complete():
717 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
718 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
718 (h.number, h.desc, len(h.a), h.lena, len(h.b),
719 (h.number, h.desc, len(h.a), h.lena, len(h.b),
719 h.lenb))
720 h.lenb))
720
721
721 self.hunks += 1
722 self.hunks += 1
722
723
723 if self.missing:
724 if self.missing:
724 self.rej.append(h)
725 self.rej.append(h)
725 return -1
726 return -1
726
727
727 if self.exists and self.create:
728 if self.exists and self.create:
728 if self.copysource:
729 if self.copysource:
729 self.ui.warn(_("cannot create %s: destination already "
730 self.ui.warn(_("cannot create %s: destination already "
730 "exists\n") % self.fname)
731 "exists\n") % self.fname)
731 else:
732 else:
732 self.ui.warn(_("file %s already exists\n") % self.fname)
733 self.ui.warn(_("file %s already exists\n") % self.fname)
733 self.rej.append(h)
734 self.rej.append(h)
734 return -1
735 return -1
735
736
736 if isinstance(h, binhunk):
737 if isinstance(h, binhunk):
737 if self.remove:
738 if self.remove:
738 self.backend.unlink(self.fname)
739 self.backend.unlink(self.fname)
739 else:
740 else:
740 l = h.new(self.lines)
741 l = h.new(self.lines)
741 self.lines[:] = l
742 self.lines[:] = l
742 self.offset += len(l)
743 self.offset += len(l)
743 self.dirty = True
744 self.dirty = True
744 return 0
745 return 0
745
746
746 horig = h
747 horig = h
747 if (self.eolmode in ('crlf', 'lf')
748 if (self.eolmode in ('crlf', 'lf')
748 or self.eolmode == 'auto' and self.eol):
749 or self.eolmode == 'auto' and self.eol):
749 # If new eols are going to be normalized, then normalize
750 # If new eols are going to be normalized, then normalize
750 # hunk data before patching. Otherwise, preserve input
751 # hunk data before patching. Otherwise, preserve input
751 # line-endings.
752 # line-endings.
752 h = h.getnormalized()
753 h = h.getnormalized()
753
754
754 # fast case first, no offsets, no fuzz
755 # fast case first, no offsets, no fuzz
755 old, oldstart, new, newstart = h.fuzzit(0, False)
756 old, oldstart, new, newstart = h.fuzzit(0, False)
756 oldstart += self.offset
757 oldstart += self.offset
757 orig_start = oldstart
758 orig_start = oldstart
758 # if there's skew we want to emit the "(offset %d lines)" even
759 # if there's skew we want to emit the "(offset %d lines)" even
759 # when the hunk cleanly applies at start + skew, so skip the
760 # when the hunk cleanly applies at start + skew, so skip the
760 # fast case code
761 # fast case code
761 if (self.skew == 0 and
762 if (self.skew == 0 and
762 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
763 diffhelpers.testhunk(old, self.lines, oldstart) == 0):
763 if self.remove:
764 if self.remove:
764 self.backend.unlink(self.fname)
765 self.backend.unlink(self.fname)
765 else:
766 else:
766 self.lines[oldstart:oldstart + len(old)] = new
767 self.lines[oldstart:oldstart + len(old)] = new
767 self.offset += len(new) - len(old)
768 self.offset += len(new) - len(old)
768 self.dirty = True
769 self.dirty = True
769 return 0
770 return 0
770
771
771 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
772 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
772 self.hash = {}
773 self.hash = {}
773 for x, s in enumerate(self.lines):
774 for x, s in enumerate(self.lines):
774 self.hash.setdefault(s, []).append(x)
775 self.hash.setdefault(s, []).append(x)
775
776
776 for fuzzlen in xrange(3):
777 for fuzzlen in xrange(3):
777 for toponly in [True, False]:
778 for toponly in [True, False]:
778 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
779 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
779 oldstart = oldstart + self.offset + self.skew
780 oldstart = oldstart + self.offset + self.skew
780 oldstart = min(oldstart, len(self.lines))
781 oldstart = min(oldstart, len(self.lines))
781 if old:
782 if old:
782 cand = self.findlines(old[0][1:], oldstart)
783 cand = self.findlines(old[0][1:], oldstart)
783 else:
784 else:
784 # Only adding lines with no or fuzzed context, just
785 # Only adding lines with no or fuzzed context, just
785 # take the skew in account
786 # take the skew in account
786 cand = [oldstart]
787 cand = [oldstart]
787
788
788 for l in cand:
789 for l in cand:
789 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
790 if not old or diffhelpers.testhunk(old, self.lines, l) == 0:
790 self.lines[l : l + len(old)] = new
791 self.lines[l : l + len(old)] = new
791 self.offset += len(new) - len(old)
792 self.offset += len(new) - len(old)
792 self.skew = l - orig_start
793 self.skew = l - orig_start
793 self.dirty = True
794 self.dirty = True
794 offset = l - orig_start - fuzzlen
795 offset = l - orig_start - fuzzlen
795 if fuzzlen:
796 if fuzzlen:
796 msg = _("Hunk #%d succeeded at %d "
797 msg = _("Hunk #%d succeeded at %d "
797 "with fuzz %d "
798 "with fuzz %d "
798 "(offset %d lines).\n")
799 "(offset %d lines).\n")
799 self.printfile(True)
800 self.printfile(True)
800 self.ui.warn(msg %
801 self.ui.warn(msg %
801 (h.number, l + 1, fuzzlen, offset))
802 (h.number, l + 1, fuzzlen, offset))
802 else:
803 else:
803 msg = _("Hunk #%d succeeded at %d "
804 msg = _("Hunk #%d succeeded at %d "
804 "(offset %d lines).\n")
805 "(offset %d lines).\n")
805 self.ui.note(msg % (h.number, l + 1, offset))
806 self.ui.note(msg % (h.number, l + 1, offset))
806 return fuzzlen
807 return fuzzlen
807 self.printfile(True)
808 self.printfile(True)
808 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
809 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
809 self.rej.append(horig)
810 self.rej.append(horig)
810 return -1
811 return -1
811
812
812 def close(self):
813 def close(self):
813 if self.dirty:
814 if self.dirty:
814 self.writelines(self.fname, self.lines, self.mode)
815 self.writelines(self.fname, self.lines, self.mode)
815 self.write_rej()
816 self.write_rej()
816 return len(self.rej)
817 return len(self.rej)
817
818
818 class header(object):
819 class header(object):
819 """patch header
820 """patch header
820 """
821 """
821 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
822 diffgit_re = re.compile('diff --git a/(.*) b/(.*)$')
822 diff_re = re.compile('diff -r .* (.*)$')
823 diff_re = re.compile('diff -r .* (.*)$')
823 allhunks_re = re.compile('(?:index|deleted file) ')
824 allhunks_re = re.compile('(?:index|deleted file) ')
824 pretty_re = re.compile('(?:new file|deleted file) ')
825 pretty_re = re.compile('(?:new file|deleted file) ')
825 special_re = re.compile('(?:index|deleted|copy|rename) ')
826 special_re = re.compile('(?:index|deleted|copy|rename) ')
826 newfile_re = re.compile('(?:new file)')
827 newfile_re = re.compile('(?:new file)')
827
828
828 def __init__(self, header):
829 def __init__(self, header):
829 self.header = header
830 self.header = header
830 self.hunks = []
831 self.hunks = []
831
832
832 def binary(self):
833 def binary(self):
833 return util.any(h.startswith('index ') for h in self.header)
834 return util.any(h.startswith('index ') for h in self.header)
834
835
835 def pretty(self, fp):
836 def pretty(self, fp):
836 for h in self.header:
837 for h in self.header:
837 if h.startswith('index '):
838 if h.startswith('index '):
838 fp.write(_('this modifies a binary file (all or nothing)\n'))
839 fp.write(_('this modifies a binary file (all or nothing)\n'))
839 break
840 break
840 if self.pretty_re.match(h):
841 if self.pretty_re.match(h):
841 fp.write(h)
842 fp.write(h)
842 if self.binary():
843 if self.binary():
843 fp.write(_('this is a binary file\n'))
844 fp.write(_('this is a binary file\n'))
844 break
845 break
845 if h.startswith('---'):
846 if h.startswith('---'):
846 fp.write(_('%d hunks, %d lines changed\n') %
847 fp.write(_('%d hunks, %d lines changed\n') %
847 (len(self.hunks),
848 (len(self.hunks),
848 sum([max(h.added, h.removed) for h in self.hunks])))
849 sum([max(h.added, h.removed) for h in self.hunks])))
849 break
850 break
850 fp.write(h)
851 fp.write(h)
851
852
852 def write(self, fp):
853 def write(self, fp):
853 fp.write(''.join(self.header))
854 fp.write(''.join(self.header))
854
855
855 def allhunks(self):
856 def allhunks(self):
856 return util.any(self.allhunks_re.match(h) for h in self.header)
857 return util.any(self.allhunks_re.match(h) for h in self.header)
857
858
858 def files(self):
859 def files(self):
859 match = self.diffgit_re.match(self.header[0])
860 match = self.diffgit_re.match(self.header[0])
860 if match:
861 if match:
861 fromfile, tofile = match.groups()
862 fromfile, tofile = match.groups()
862 if fromfile == tofile:
863 if fromfile == tofile:
863 return [fromfile]
864 return [fromfile]
864 return [fromfile, tofile]
865 return [fromfile, tofile]
865 else:
866 else:
866 return self.diff_re.match(self.header[0]).groups()
867 return self.diff_re.match(self.header[0]).groups()
867
868
868 def filename(self):
869 def filename(self):
869 return self.files()[-1]
870 return self.files()[-1]
870
871
871 def __repr__(self):
872 def __repr__(self):
872 return '<header %s>' % (' '.join(map(repr, self.files())))
873 return '<header %s>' % (' '.join(map(repr, self.files())))
873
874
874 def isnewfile(self):
875 def isnewfile(self):
875 return util.any(self.newfile_re.match(h) for h in self.header)
876 return util.any(self.newfile_re.match(h) for h in self.header)
876
877
877 def special(self):
878 def special(self):
878 # Special files are shown only at the header level and not at the hunk
879 # Special files are shown only at the header level and not at the hunk
879 # level for example a file that has been deleted is a special file.
880 # level for example a file that has been deleted is a special file.
880 # The user cannot change the content of the operation, in the case of
881 # The user cannot change the content of the operation, in the case of
881 # the deleted file he has to take the deletion or not take it, he
882 # the deleted file he has to take the deletion or not take it, he
882 # cannot take some of it.
883 # cannot take some of it.
883 # Newly added files are special if they are empty, they are not special
884 # Newly added files are special if they are empty, they are not special
884 # if they have some content as we want to be able to change it
885 # if they have some content as we want to be able to change it
885 nocontent = len(self.header) == 2
886 nocontent = len(self.header) == 2
886 emptynewfile = self.isnewfile() and nocontent
887 emptynewfile = self.isnewfile() and nocontent
887 return emptynewfile or \
888 return emptynewfile or \
888 util.any(self.special_re.match(h) for h in self.header)
889 util.any(self.special_re.match(h) for h in self.header)
889
890
890 class recordhunk(object):
891 class recordhunk(object):
891 """patch hunk
892 """patch hunk
892
893
893 XXX shouldn't we merge this with the other hunk class?
894 XXX shouldn't we merge this with the other hunk class?
894 """
895 """
895 maxcontext = 3
896 maxcontext = 3
896
897
897 def __init__(self, header, fromline, toline, proc, before, hunk, after):
898 def __init__(self, header, fromline, toline, proc, before, hunk, after):
898 def trimcontext(number, lines):
899 def trimcontext(number, lines):
899 delta = len(lines) - self.maxcontext
900 delta = len(lines) - self.maxcontext
900 if False and delta > 0:
901 if False and delta > 0:
901 return number + delta, lines[:self.maxcontext]
902 return number + delta, lines[:self.maxcontext]
902 return number, lines
903 return number, lines
903
904
904 self.header = header
905 self.header = header
905 self.fromline, self.before = trimcontext(fromline, before)
906 self.fromline, self.before = trimcontext(fromline, before)
906 self.toline, self.after = trimcontext(toline, after)
907 self.toline, self.after = trimcontext(toline, after)
907 self.proc = proc
908 self.proc = proc
908 self.hunk = hunk
909 self.hunk = hunk
909 self.added, self.removed = self.countchanges(self.hunk)
910 self.added, self.removed = self.countchanges(self.hunk)
910
911
911 def __eq__(self, v):
912 def __eq__(self, v):
912 if not isinstance(v, recordhunk):
913 if not isinstance(v, recordhunk):
913 return False
914 return False
914
915
915 return ((v.hunk == self.hunk) and
916 return ((v.hunk == self.hunk) and
916 (v.proc == self.proc) and
917 (v.proc == self.proc) and
917 (self.fromline == v.fromline) and
918 (self.fromline == v.fromline) and
918 (self.header.files() == v.header.files()))
919 (self.header.files() == v.header.files()))
919
920
920 def __hash__(self):
921 def __hash__(self):
921 return hash((tuple(self.hunk),
922 return hash((tuple(self.hunk),
922 tuple(self.header.files()),
923 tuple(self.header.files()),
923 self.fromline,
924 self.fromline,
924 self.proc))
925 self.proc))
925
926
926 def countchanges(self, hunk):
927 def countchanges(self, hunk):
927 """hunk -> (n+,n-)"""
928 """hunk -> (n+,n-)"""
928 add = len([h for h in hunk if h[0] == '+'])
929 add = len([h for h in hunk if h[0] == '+'])
929 rem = len([h for h in hunk if h[0] == '-'])
930 rem = len([h for h in hunk if h[0] == '-'])
930 return add, rem
931 return add, rem
931
932
932 def write(self, fp):
933 def write(self, fp):
933 delta = len(self.before) + len(self.after)
934 delta = len(self.before) + len(self.after)
934 if self.after and self.after[-1] == '\\ No newline at end of file\n':
935 if self.after and self.after[-1] == '\\ No newline at end of file\n':
935 delta -= 1
936 delta -= 1
936 fromlen = delta + self.removed
937 fromlen = delta + self.removed
937 tolen = delta + self.added
938 tolen = delta + self.added
938 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
939 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
939 (self.fromline, fromlen, self.toline, tolen,
940 (self.fromline, fromlen, self.toline, tolen,
940 self.proc and (' ' + self.proc)))
941 self.proc and (' ' + self.proc)))
941 fp.write(''.join(self.before + self.hunk + self.after))
942 fp.write(''.join(self.before + self.hunk + self.after))
942
943
943 pretty = write
944 pretty = write
944
945
945 def filename(self):
946 def filename(self):
946 return self.header.filename()
947 return self.header.filename()
947
948
948 def __repr__(self):
949 def __repr__(self):
949 return '<hunk %r@%d>' % (self.filename(), self.fromline)
950 return '<hunk %r@%d>' % (self.filename(), self.fromline)
950
951
951 def filterpatch(ui, headers):
952 def filterpatch(ui, headers):
952 """Interactively filter patch chunks into applied-only chunks"""
953 """Interactively filter patch chunks into applied-only chunks"""
953
954
954 def prompt(skipfile, skipall, query, chunk):
955 def prompt(skipfile, skipall, query, chunk):
955 """prompt query, and process base inputs
956 """prompt query, and process base inputs
956
957
957 - y/n for the rest of file
958 - y/n for the rest of file
958 - y/n for the rest
959 - y/n for the rest
959 - ? (help)
960 - ? (help)
960 - q (quit)
961 - q (quit)
961
962
962 Return True/False and possibly updated skipfile and skipall.
963 Return True/False and possibly updated skipfile and skipall.
963 """
964 """
964 newpatches = None
965 newpatches = None
965 if skipall is not None:
966 if skipall is not None:
966 return skipall, skipfile, skipall, newpatches
967 return skipall, skipfile, skipall, newpatches
967 if skipfile is not None:
968 if skipfile is not None:
968 return skipfile, skipfile, skipall, newpatches
969 return skipfile, skipfile, skipall, newpatches
969 while True:
970 while True:
970 resps = _('[Ynesfdaq?]'
971 resps = _('[Ynesfdaq?]'
971 '$$ &Yes, record this change'
972 '$$ &Yes, record this change'
972 '$$ &No, skip this change'
973 '$$ &No, skip this change'
973 '$$ &Edit this change manually'
974 '$$ &Edit this change manually'
974 '$$ &Skip remaining changes to this file'
975 '$$ &Skip remaining changes to this file'
975 '$$ Record remaining changes to this &file'
976 '$$ Record remaining changes to this &file'
976 '$$ &Done, skip remaining changes and files'
977 '$$ &Done, skip remaining changes and files'
977 '$$ Record &all changes to all remaining files'
978 '$$ Record &all changes to all remaining files'
978 '$$ &Quit, recording no changes'
979 '$$ &Quit, recording no changes'
979 '$$ &? (display help)')
980 '$$ &? (display help)')
980 r = ui.promptchoice("%s %s" % (query, resps))
981 r = ui.promptchoice("%s %s" % (query, resps))
981 ui.write("\n")
982 ui.write("\n")
982 if r == 8: # ?
983 if r == 8: # ?
983 for c, t in ui.extractchoices(resps)[1]:
984 for c, t in ui.extractchoices(resps)[1]:
984 ui.write('%s - %s\n' % (c, t.lower()))
985 ui.write('%s - %s\n' % (c, t.lower()))
985 continue
986 continue
986 elif r == 0: # yes
987 elif r == 0: # yes
987 ret = True
988 ret = True
988 elif r == 1: # no
989 elif r == 1: # no
989 ret = False
990 ret = False
990 elif r == 2: # Edit patch
991 elif r == 2: # Edit patch
991 if chunk is None:
992 if chunk is None:
992 ui.write(_('cannot edit patch for whole file'))
993 ui.write(_('cannot edit patch for whole file'))
993 ui.write("\n")
994 ui.write("\n")
994 continue
995 continue
995 if chunk.header.binary():
996 if chunk.header.binary():
996 ui.write(_('cannot edit patch for binary file'))
997 ui.write(_('cannot edit patch for binary file'))
997 ui.write("\n")
998 ui.write("\n")
998 continue
999 continue
999 # Patch comment based on the Git one (based on comment at end of
1000 # Patch comment based on the Git one (based on comment at end of
1000 # http://mercurial.selenic.com/wiki/RecordExtension)
1001 # http://mercurial.selenic.com/wiki/RecordExtension)
1001 phelp = '---' + _("""
1002 phelp = '---' + _("""
1002 To remove '-' lines, make them ' ' lines (context).
1003 To remove '-' lines, make them ' ' lines (context).
1003 To remove '+' lines, delete them.
1004 To remove '+' lines, delete them.
1004 Lines starting with # will be removed from the patch.
1005 Lines starting with # will be removed from the patch.
1005
1006
1006 If the patch applies cleanly, the edited hunk will immediately be
1007 If the patch applies cleanly, the edited hunk will immediately be
1007 added to the record list. If it does not apply cleanly, a rejects
1008 added to the record list. If it does not apply cleanly, a rejects
1008 file will be generated: you can use that when you try again. If
1009 file will be generated: you can use that when you try again. If
1009 all lines of the hunk are removed, then the edit is aborted and
1010 all lines of the hunk are removed, then the edit is aborted and
1010 the hunk is left unchanged.
1011 the hunk is left unchanged.
1011 """)
1012 """)
1012 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
1013 (patchfd, patchfn) = tempfile.mkstemp(prefix="hg-editor-",
1013 suffix=".diff", text=True)
1014 suffix=".diff", text=True)
1014 ncpatchfp = None
1015 ncpatchfp = None
1015 try:
1016 try:
1016 # Write the initial patch
1017 # Write the initial patch
1017 f = os.fdopen(patchfd, "w")
1018 f = os.fdopen(patchfd, "w")
1018 chunk.header.write(f)
1019 chunk.header.write(f)
1019 chunk.write(f)
1020 chunk.write(f)
1020 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
1021 f.write('\n'.join(['# ' + i for i in phelp.splitlines()]))
1021 f.close()
1022 f.close()
1022 # Start the editor and wait for it to complete
1023 # Start the editor and wait for it to complete
1023 editor = ui.geteditor()
1024 editor = ui.geteditor()
1024 ui.system("%s \"%s\"" % (editor, patchfn),
1025 ui.system("%s \"%s\"" % (editor, patchfn),
1025 environ={'HGUSER': ui.username()},
1026 environ={'HGUSER': ui.username()},
1026 onerr=util.Abort, errprefix=_("edit failed"))
1027 onerr=util.Abort, errprefix=_("edit failed"))
1027 # Remove comment lines
1028 # Remove comment lines
1028 patchfp = open(patchfn)
1029 patchfp = open(patchfn)
1029 ncpatchfp = cStringIO.StringIO()
1030 ncpatchfp = cStringIO.StringIO()
1030 for line in patchfp:
1031 for line in patchfp:
1031 if not line.startswith('#'):
1032 if not line.startswith('#'):
1032 ncpatchfp.write(line)
1033 ncpatchfp.write(line)
1033 patchfp.close()
1034 patchfp.close()
1034 ncpatchfp.seek(0)
1035 ncpatchfp.seek(0)
1035 newpatches = parsepatch(ncpatchfp)
1036 newpatches = parsepatch(ncpatchfp)
1036 finally:
1037 finally:
1037 os.unlink(patchfn)
1038 os.unlink(patchfn)
1038 del ncpatchfp
1039 del ncpatchfp
1039 # Signal that the chunk shouldn't be applied as-is, but
1040 # Signal that the chunk shouldn't be applied as-is, but
1040 # provide the new patch to be used instead.
1041 # provide the new patch to be used instead.
1041 ret = False
1042 ret = False
1042 elif r == 3: # Skip
1043 elif r == 3: # Skip
1043 ret = skipfile = False
1044 ret = skipfile = False
1044 elif r == 4: # file (Record remaining)
1045 elif r == 4: # file (Record remaining)
1045 ret = skipfile = True
1046 ret = skipfile = True
1046 elif r == 5: # done, skip remaining
1047 elif r == 5: # done, skip remaining
1047 ret = skipall = False
1048 ret = skipall = False
1048 elif r == 6: # all
1049 elif r == 6: # all
1049 ret = skipall = True
1050 ret = skipall = True
1050 elif r == 7: # quit
1051 elif r == 7: # quit
1051 raise util.Abort(_('user quit'))
1052 raise util.Abort(_('user quit'))
1052 return ret, skipfile, skipall, newpatches
1053 return ret, skipfile, skipall, newpatches
1053
1054
1054 seen = set()
1055 seen = set()
1055 applied = {} # 'filename' -> [] of chunks
1056 applied = {} # 'filename' -> [] of chunks
1056 skipfile, skipall = None, None
1057 skipfile, skipall = None, None
1057 pos, total = 1, sum(len(h.hunks) for h in headers)
1058 pos, total = 1, sum(len(h.hunks) for h in headers)
1058 for h in headers:
1059 for h in headers:
1059 pos += len(h.hunks)
1060 pos += len(h.hunks)
1060 skipfile = None
1061 skipfile = None
1061 fixoffset = 0
1062 fixoffset = 0
1062 hdr = ''.join(h.header)
1063 hdr = ''.join(h.header)
1063 if hdr in seen:
1064 if hdr in seen:
1064 continue
1065 continue
1065 seen.add(hdr)
1066 seen.add(hdr)
1066 if skipall is None:
1067 if skipall is None:
1067 h.pretty(ui)
1068 h.pretty(ui)
1068 msg = (_('examine changes to %s?') %
1069 msg = (_('examine changes to %s?') %
1069 _(' and ').join("'%s'" % f for f in h.files()))
1070 _(' and ').join("'%s'" % f for f in h.files()))
1070 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
1071 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
1071 if not r:
1072 if not r:
1072 continue
1073 continue
1073 applied[h.filename()] = [h]
1074 applied[h.filename()] = [h]
1074 if h.allhunks():
1075 if h.allhunks():
1075 applied[h.filename()] += h.hunks
1076 applied[h.filename()] += h.hunks
1076 continue
1077 continue
1077 for i, chunk in enumerate(h.hunks):
1078 for i, chunk in enumerate(h.hunks):
1078 if skipfile is None and skipall is None:
1079 if skipfile is None and skipall is None:
1079 chunk.pretty(ui)
1080 chunk.pretty(ui)
1080 if total == 1:
1081 if total == 1:
1081 msg = _("record this change to '%s'?") % chunk.filename()
1082 msg = _("record this change to '%s'?") % chunk.filename()
1082 else:
1083 else:
1083 idx = pos - len(h.hunks) + i
1084 idx = pos - len(h.hunks) + i
1084 msg = _("record change %d/%d to '%s'?") % (idx, total,
1085 msg = _("record change %d/%d to '%s'?") % (idx, total,
1085 chunk.filename())
1086 chunk.filename())
1086 r, skipfile, skipall, newpatches = prompt(skipfile,
1087 r, skipfile, skipall, newpatches = prompt(skipfile,
1087 skipall, msg, chunk)
1088 skipall, msg, chunk)
1088 if r:
1089 if r:
1089 if fixoffset:
1090 if fixoffset:
1090 chunk = copy.copy(chunk)
1091 chunk = copy.copy(chunk)
1091 chunk.toline += fixoffset
1092 chunk.toline += fixoffset
1092 applied[chunk.filename()].append(chunk)
1093 applied[chunk.filename()].append(chunk)
1093 elif newpatches is not None:
1094 elif newpatches is not None:
1094 for newpatch in newpatches:
1095 for newpatch in newpatches:
1095 for newhunk in newpatch.hunks:
1096 for newhunk in newpatch.hunks:
1096 if fixoffset:
1097 if fixoffset:
1097 newhunk.toline += fixoffset
1098 newhunk.toline += fixoffset
1098 applied[newhunk.filename()].append(newhunk)
1099 applied[newhunk.filename()].append(newhunk)
1099 else:
1100 else:
1100 fixoffset += chunk.removed - chunk.added
1101 fixoffset += chunk.removed - chunk.added
1101 return sum([h for h in applied.itervalues()
1102 return sum([h for h in applied.itervalues()
1102 if h[0].special() or len(h) > 1], [])
1103 if h[0].special() or len(h) > 1], [])
1103 class hunk(object):
1104 class hunk(object):
1104 def __init__(self, desc, num, lr, context):
1105 def __init__(self, desc, num, lr, context):
1105 self.number = num
1106 self.number = num
1106 self.desc = desc
1107 self.desc = desc
1107 self.hunk = [desc]
1108 self.hunk = [desc]
1108 self.a = []
1109 self.a = []
1109 self.b = []
1110 self.b = []
1110 self.starta = self.lena = None
1111 self.starta = self.lena = None
1111 self.startb = self.lenb = None
1112 self.startb = self.lenb = None
1112 if lr is not None:
1113 if lr is not None:
1113 if context:
1114 if context:
1114 self.read_context_hunk(lr)
1115 self.read_context_hunk(lr)
1115 else:
1116 else:
1116 self.read_unified_hunk(lr)
1117 self.read_unified_hunk(lr)
1117
1118
1118 def getnormalized(self):
1119 def getnormalized(self):
1119 """Return a copy with line endings normalized to LF."""
1120 """Return a copy with line endings normalized to LF."""
1120
1121
1121 def normalize(lines):
1122 def normalize(lines):
1122 nlines = []
1123 nlines = []
1123 for line in lines:
1124 for line in lines:
1124 if line.endswith('\r\n'):
1125 if line.endswith('\r\n'):
1125 line = line[:-2] + '\n'
1126 line = line[:-2] + '\n'
1126 nlines.append(line)
1127 nlines.append(line)
1127 return nlines
1128 return nlines
1128
1129
1129 # Dummy object, it is rebuilt manually
1130 # Dummy object, it is rebuilt manually
1130 nh = hunk(self.desc, self.number, None, None)
1131 nh = hunk(self.desc, self.number, None, None)
1131 nh.number = self.number
1132 nh.number = self.number
1132 nh.desc = self.desc
1133 nh.desc = self.desc
1133 nh.hunk = self.hunk
1134 nh.hunk = self.hunk
1134 nh.a = normalize(self.a)
1135 nh.a = normalize(self.a)
1135 nh.b = normalize(self.b)
1136 nh.b = normalize(self.b)
1136 nh.starta = self.starta
1137 nh.starta = self.starta
1137 nh.startb = self.startb
1138 nh.startb = self.startb
1138 nh.lena = self.lena
1139 nh.lena = self.lena
1139 nh.lenb = self.lenb
1140 nh.lenb = self.lenb
1140 return nh
1141 return nh
1141
1142
1142 def read_unified_hunk(self, lr):
1143 def read_unified_hunk(self, lr):
1143 m = unidesc.match(self.desc)
1144 m = unidesc.match(self.desc)
1144 if not m:
1145 if not m:
1145 raise PatchError(_("bad hunk #%d") % self.number)
1146 raise PatchError(_("bad hunk #%d") % self.number)
1146 self.starta, self.lena, self.startb, self.lenb = m.groups()
1147 self.starta, self.lena, self.startb, self.lenb = m.groups()
1147 if self.lena is None:
1148 if self.lena is None:
1148 self.lena = 1
1149 self.lena = 1
1149 else:
1150 else:
1150 self.lena = int(self.lena)
1151 self.lena = int(self.lena)
1151 if self.lenb is None:
1152 if self.lenb is None:
1152 self.lenb = 1
1153 self.lenb = 1
1153 else:
1154 else:
1154 self.lenb = int(self.lenb)
1155 self.lenb = int(self.lenb)
1155 self.starta = int(self.starta)
1156 self.starta = int(self.starta)
1156 self.startb = int(self.startb)
1157 self.startb = int(self.startb)
1157 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a,
1158 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a,
1158 self.b)
1159 self.b)
1159 # if we hit eof before finishing out the hunk, the last line will
1160 # if we hit eof before finishing out the hunk, the last line will
1160 # be zero length. Lets try to fix it up.
1161 # be zero length. Lets try to fix it up.
1161 while len(self.hunk[-1]) == 0:
1162 while len(self.hunk[-1]) == 0:
1162 del self.hunk[-1]
1163 del self.hunk[-1]
1163 del self.a[-1]
1164 del self.a[-1]
1164 del self.b[-1]
1165 del self.b[-1]
1165 self.lena -= 1
1166 self.lena -= 1
1166 self.lenb -= 1
1167 self.lenb -= 1
1167 self._fixnewline(lr)
1168 self._fixnewline(lr)
1168
1169
1169 def read_context_hunk(self, lr):
1170 def read_context_hunk(self, lr):
1170 self.desc = lr.readline()
1171 self.desc = lr.readline()
1171 m = contextdesc.match(self.desc)
1172 m = contextdesc.match(self.desc)
1172 if not m:
1173 if not m:
1173 raise PatchError(_("bad hunk #%d") % self.number)
1174 raise PatchError(_("bad hunk #%d") % self.number)
1174 self.starta, aend = m.groups()
1175 self.starta, aend = m.groups()
1175 self.starta = int(self.starta)
1176 self.starta = int(self.starta)
1176 if aend is None:
1177 if aend is None:
1177 aend = self.starta
1178 aend = self.starta
1178 self.lena = int(aend) - self.starta
1179 self.lena = int(aend) - self.starta
1179 if self.starta:
1180 if self.starta:
1180 self.lena += 1
1181 self.lena += 1
1181 for x in xrange(self.lena):
1182 for x in xrange(self.lena):
1182 l = lr.readline()
1183 l = lr.readline()
1183 if l.startswith('---'):
1184 if l.startswith('---'):
1184 # lines addition, old block is empty
1185 # lines addition, old block is empty
1185 lr.push(l)
1186 lr.push(l)
1186 break
1187 break
1187 s = l[2:]
1188 s = l[2:]
1188 if l.startswith('- ') or l.startswith('! '):
1189 if l.startswith('- ') or l.startswith('! '):
1189 u = '-' + s
1190 u = '-' + s
1190 elif l.startswith(' '):
1191 elif l.startswith(' '):
1191 u = ' ' + s
1192 u = ' ' + s
1192 else:
1193 else:
1193 raise PatchError(_("bad hunk #%d old text line %d") %
1194 raise PatchError(_("bad hunk #%d old text line %d") %
1194 (self.number, x))
1195 (self.number, x))
1195 self.a.append(u)
1196 self.a.append(u)
1196 self.hunk.append(u)
1197 self.hunk.append(u)
1197
1198
1198 l = lr.readline()
1199 l = lr.readline()
1199 if l.startswith('\ '):
1200 if l.startswith('\ '):
1200 s = self.a[-1][:-1]
1201 s = self.a[-1][:-1]
1201 self.a[-1] = s
1202 self.a[-1] = s
1202 self.hunk[-1] = s
1203 self.hunk[-1] = s
1203 l = lr.readline()
1204 l = lr.readline()
1204 m = contextdesc.match(l)
1205 m = contextdesc.match(l)
1205 if not m:
1206 if not m:
1206 raise PatchError(_("bad hunk #%d") % self.number)
1207 raise PatchError(_("bad hunk #%d") % self.number)
1207 self.startb, bend = m.groups()
1208 self.startb, bend = m.groups()
1208 self.startb = int(self.startb)
1209 self.startb = int(self.startb)
1209 if bend is None:
1210 if bend is None:
1210 bend = self.startb
1211 bend = self.startb
1211 self.lenb = int(bend) - self.startb
1212 self.lenb = int(bend) - self.startb
1212 if self.startb:
1213 if self.startb:
1213 self.lenb += 1
1214 self.lenb += 1
1214 hunki = 1
1215 hunki = 1
1215 for x in xrange(self.lenb):
1216 for x in xrange(self.lenb):
1216 l = lr.readline()
1217 l = lr.readline()
1217 if l.startswith('\ '):
1218 if l.startswith('\ '):
1218 # XXX: the only way to hit this is with an invalid line range.
1219 # XXX: the only way to hit this is with an invalid line range.
1219 # The no-eol marker is not counted in the line range, but I
1220 # The no-eol marker is not counted in the line range, but I
1220 # guess there are diff(1) out there which behave differently.
1221 # guess there are diff(1) out there which behave differently.
1221 s = self.b[-1][:-1]
1222 s = self.b[-1][:-1]
1222 self.b[-1] = s
1223 self.b[-1] = s
1223 self.hunk[hunki - 1] = s
1224 self.hunk[hunki - 1] = s
1224 continue
1225 continue
1225 if not l:
1226 if not l:
1226 # line deletions, new block is empty and we hit EOF
1227 # line deletions, new block is empty and we hit EOF
1227 lr.push(l)
1228 lr.push(l)
1228 break
1229 break
1229 s = l[2:]
1230 s = l[2:]
1230 if l.startswith('+ ') or l.startswith('! '):
1231 if l.startswith('+ ') or l.startswith('! '):
1231 u = '+' + s
1232 u = '+' + s
1232 elif l.startswith(' '):
1233 elif l.startswith(' '):
1233 u = ' ' + s
1234 u = ' ' + s
1234 elif len(self.b) == 0:
1235 elif len(self.b) == 0:
1235 # line deletions, new block is empty
1236 # line deletions, new block is empty
1236 lr.push(l)
1237 lr.push(l)
1237 break
1238 break
1238 else:
1239 else:
1239 raise PatchError(_("bad hunk #%d old text line %d") %
1240 raise PatchError(_("bad hunk #%d old text line %d") %
1240 (self.number, x))
1241 (self.number, x))
1241 self.b.append(s)
1242 self.b.append(s)
1242 while True:
1243 while True:
1243 if hunki >= len(self.hunk):
1244 if hunki >= len(self.hunk):
1244 h = ""
1245 h = ""
1245 else:
1246 else:
1246 h = self.hunk[hunki]
1247 h = self.hunk[hunki]
1247 hunki += 1
1248 hunki += 1
1248 if h == u:
1249 if h == u:
1249 break
1250 break
1250 elif h.startswith('-'):
1251 elif h.startswith('-'):
1251 continue
1252 continue
1252 else:
1253 else:
1253 self.hunk.insert(hunki - 1, u)
1254 self.hunk.insert(hunki - 1, u)
1254 break
1255 break
1255
1256
1256 if not self.a:
1257 if not self.a:
1257 # this happens when lines were only added to the hunk
1258 # this happens when lines were only added to the hunk
1258 for x in self.hunk:
1259 for x in self.hunk:
1259 if x.startswith('-') or x.startswith(' '):
1260 if x.startswith('-') or x.startswith(' '):
1260 self.a.append(x)
1261 self.a.append(x)
1261 if not self.b:
1262 if not self.b:
1262 # this happens when lines were only deleted from the hunk
1263 # this happens when lines were only deleted from the hunk
1263 for x in self.hunk:
1264 for x in self.hunk:
1264 if x.startswith('+') or x.startswith(' '):
1265 if x.startswith('+') or x.startswith(' '):
1265 self.b.append(x[1:])
1266 self.b.append(x[1:])
1266 # @@ -start,len +start,len @@
1267 # @@ -start,len +start,len @@
1267 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
1268 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
1268 self.startb, self.lenb)
1269 self.startb, self.lenb)
1269 self.hunk[0] = self.desc
1270 self.hunk[0] = self.desc
1270 self._fixnewline(lr)
1271 self._fixnewline(lr)
1271
1272
1272 def _fixnewline(self, lr):
1273 def _fixnewline(self, lr):
1273 l = lr.readline()
1274 l = lr.readline()
1274 if l.startswith('\ '):
1275 if l.startswith('\ '):
1275 diffhelpers.fix_newline(self.hunk, self.a, self.b)
1276 diffhelpers.fix_newline(self.hunk, self.a, self.b)
1276 else:
1277 else:
1277 lr.push(l)
1278 lr.push(l)
1278
1279
1279 def complete(self):
1280 def complete(self):
1280 return len(self.a) == self.lena and len(self.b) == self.lenb
1281 return len(self.a) == self.lena and len(self.b) == self.lenb
1281
1282
1282 def _fuzzit(self, old, new, fuzz, toponly):
1283 def _fuzzit(self, old, new, fuzz, toponly):
1283 # this removes context lines from the top and bottom of list 'l'. It
1284 # this removes context lines from the top and bottom of list 'l'. It
1284 # checks the hunk to make sure only context lines are removed, and then
1285 # checks the hunk to make sure only context lines are removed, and then
1285 # returns a new shortened list of lines.
1286 # returns a new shortened list of lines.
1286 fuzz = min(fuzz, len(old))
1287 fuzz = min(fuzz, len(old))
1287 if fuzz:
1288 if fuzz:
1288 top = 0
1289 top = 0
1289 bot = 0
1290 bot = 0
1290 hlen = len(self.hunk)
1291 hlen = len(self.hunk)
1291 for x in xrange(hlen - 1):
1292 for x in xrange(hlen - 1):
1292 # the hunk starts with the @@ line, so use x+1
1293 # the hunk starts with the @@ line, so use x+1
1293 if self.hunk[x + 1][0] == ' ':
1294 if self.hunk[x + 1][0] == ' ':
1294 top += 1
1295 top += 1
1295 else:
1296 else:
1296 break
1297 break
1297 if not toponly:
1298 if not toponly:
1298 for x in xrange(hlen - 1):
1299 for x in xrange(hlen - 1):
1299 if self.hunk[hlen - bot - 1][0] == ' ':
1300 if self.hunk[hlen - bot - 1][0] == ' ':
1300 bot += 1
1301 bot += 1
1301 else:
1302 else:
1302 break
1303 break
1303
1304
1304 bot = min(fuzz, bot)
1305 bot = min(fuzz, bot)
1305 top = min(fuzz, top)
1306 top = min(fuzz, top)
1306 return old[top:len(old) - bot], new[top:len(new) - bot], top
1307 return old[top:len(old) - bot], new[top:len(new) - bot], top
1307 return old, new, 0
1308 return old, new, 0
1308
1309
1309 def fuzzit(self, fuzz, toponly):
1310 def fuzzit(self, fuzz, toponly):
1310 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1311 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1311 oldstart = self.starta + top
1312 oldstart = self.starta + top
1312 newstart = self.startb + top
1313 newstart = self.startb + top
1313 # zero length hunk ranges already have their start decremented
1314 # zero length hunk ranges already have their start decremented
1314 if self.lena and oldstart > 0:
1315 if self.lena and oldstart > 0:
1315 oldstart -= 1
1316 oldstart -= 1
1316 if self.lenb and newstart > 0:
1317 if self.lenb and newstart > 0:
1317 newstart -= 1
1318 newstart -= 1
1318 return old, oldstart, new, newstart
1319 return old, oldstart, new, newstart
1319
1320
1320 class binhunk(object):
1321 class binhunk(object):
1321 'A binary patch file.'
1322 'A binary patch file.'
1322 def __init__(self, lr, fname):
1323 def __init__(self, lr, fname):
1323 self.text = None
1324 self.text = None
1324 self.delta = False
1325 self.delta = False
1325 self.hunk = ['GIT binary patch\n']
1326 self.hunk = ['GIT binary patch\n']
1326 self._fname = fname
1327 self._fname = fname
1327 self._read(lr)
1328 self._read(lr)
1328
1329
1329 def complete(self):
1330 def complete(self):
1330 return self.text is not None
1331 return self.text is not None
1331
1332
1332 def new(self, lines):
1333 def new(self, lines):
1333 if self.delta:
1334 if self.delta:
1334 return [applybindelta(self.text, ''.join(lines))]
1335 return [applybindelta(self.text, ''.join(lines))]
1335 return [self.text]
1336 return [self.text]
1336
1337
1337 def _read(self, lr):
1338 def _read(self, lr):
1338 def getline(lr, hunk):
1339 def getline(lr, hunk):
1339 l = lr.readline()
1340 l = lr.readline()
1340 hunk.append(l)
1341 hunk.append(l)
1341 return l.rstrip('\r\n')
1342 return l.rstrip('\r\n')
1342
1343
1343 size = 0
1344 size = 0
1344 while True:
1345 while True:
1345 line = getline(lr, self.hunk)
1346 line = getline(lr, self.hunk)
1346 if not line:
1347 if not line:
1347 raise PatchError(_('could not extract "%s" binary data')
1348 raise PatchError(_('could not extract "%s" binary data')
1348 % self._fname)
1349 % self._fname)
1349 if line.startswith('literal '):
1350 if line.startswith('literal '):
1350 size = int(line[8:].rstrip())
1351 size = int(line[8:].rstrip())
1351 break
1352 break
1352 if line.startswith('delta '):
1353 if line.startswith('delta '):
1353 size = int(line[6:].rstrip())
1354 size = int(line[6:].rstrip())
1354 self.delta = True
1355 self.delta = True
1355 break
1356 break
1356 dec = []
1357 dec = []
1357 line = getline(lr, self.hunk)
1358 line = getline(lr, self.hunk)
1358 while len(line) > 1:
1359 while len(line) > 1:
1359 l = line[0]
1360 l = line[0]
1360 if l <= 'Z' and l >= 'A':
1361 if l <= 'Z' and l >= 'A':
1361 l = ord(l) - ord('A') + 1
1362 l = ord(l) - ord('A') + 1
1362 else:
1363 else:
1363 l = ord(l) - ord('a') + 27
1364 l = ord(l) - ord('a') + 27
1364 try:
1365 try:
1365 dec.append(base85.b85decode(line[1:])[:l])
1366 dec.append(base85.b85decode(line[1:])[:l])
1366 except ValueError, e:
1367 except ValueError, e:
1367 raise PatchError(_('could not decode "%s" binary patch: %s')
1368 raise PatchError(_('could not decode "%s" binary patch: %s')
1368 % (self._fname, str(e)))
1369 % (self._fname, str(e)))
1369 line = getline(lr, self.hunk)
1370 line = getline(lr, self.hunk)
1370 text = zlib.decompress(''.join(dec))
1371 text = zlib.decompress(''.join(dec))
1371 if len(text) != size:
1372 if len(text) != size:
1372 raise PatchError(_('"%s" length is %d bytes, should be %d')
1373 raise PatchError(_('"%s" length is %d bytes, should be %d')
1373 % (self._fname, len(text), size))
1374 % (self._fname, len(text), size))
1374 self.text = text
1375 self.text = text
1375
1376
1376 def parsefilename(str):
1377 def parsefilename(str):
1377 # --- filename \t|space stuff
1378 # --- filename \t|space stuff
1378 s = str[4:].rstrip('\r\n')
1379 s = str[4:].rstrip('\r\n')
1379 i = s.find('\t')
1380 i = s.find('\t')
1380 if i < 0:
1381 if i < 0:
1381 i = s.find(' ')
1382 i = s.find(' ')
1382 if i < 0:
1383 if i < 0:
1383 return s
1384 return s
1384 return s[:i]
1385 return s[:i]
1385
1386
1386 def parsepatch(originalchunks):
1387 def parsepatch(originalchunks):
1387 """patch -> [] of headers -> [] of hunks """
1388 """patch -> [] of headers -> [] of hunks """
1388 class parser(object):
1389 class parser(object):
1389 """patch parsing state machine"""
1390 """patch parsing state machine"""
1390 def __init__(self):
1391 def __init__(self):
1391 self.fromline = 0
1392 self.fromline = 0
1392 self.toline = 0
1393 self.toline = 0
1393 self.proc = ''
1394 self.proc = ''
1394 self.header = None
1395 self.header = None
1395 self.context = []
1396 self.context = []
1396 self.before = []
1397 self.before = []
1397 self.hunk = []
1398 self.hunk = []
1398 self.headers = []
1399 self.headers = []
1399
1400
1400 def addrange(self, limits):
1401 def addrange(self, limits):
1401 fromstart, fromend, tostart, toend, proc = limits
1402 fromstart, fromend, tostart, toend, proc = limits
1402 self.fromline = int(fromstart)
1403 self.fromline = int(fromstart)
1403 self.toline = int(tostart)
1404 self.toline = int(tostart)
1404 self.proc = proc
1405 self.proc = proc
1405
1406
1406 def addcontext(self, context):
1407 def addcontext(self, context):
1407 if self.hunk:
1408 if self.hunk:
1408 h = recordhunk(self.header, self.fromline, self.toline,
1409 h = recordhunk(self.header, self.fromline, self.toline,
1409 self.proc, self.before, self.hunk, context)
1410 self.proc, self.before, self.hunk, context)
1410 self.header.hunks.append(h)
1411 self.header.hunks.append(h)
1411 self.fromline += len(self.before) + h.removed
1412 self.fromline += len(self.before) + h.removed
1412 self.toline += len(self.before) + h.added
1413 self.toline += len(self.before) + h.added
1413 self.before = []
1414 self.before = []
1414 self.hunk = []
1415 self.hunk = []
1415 self.proc = ''
1416 self.proc = ''
1416 self.context = context
1417 self.context = context
1417
1418
1418 def addhunk(self, hunk):
1419 def addhunk(self, hunk):
1419 if self.context:
1420 if self.context:
1420 self.before = self.context
1421 self.before = self.context
1421 self.context = []
1422 self.context = []
1422 self.hunk = hunk
1423 self.hunk = hunk
1423
1424
1424 def newfile(self, hdr):
1425 def newfile(self, hdr):
1425 self.addcontext([])
1426 self.addcontext([])
1426 h = header(hdr)
1427 h = header(hdr)
1427 self.headers.append(h)
1428 self.headers.append(h)
1428 self.header = h
1429 self.header = h
1429
1430
1430 def addother(self, line):
1431 def addother(self, line):
1431 pass # 'other' lines are ignored
1432 pass # 'other' lines are ignored
1432
1433
1433 def finished(self):
1434 def finished(self):
1434 self.addcontext([])
1435 self.addcontext([])
1435 return self.headers
1436 return self.headers
1436
1437
1437 transitions = {
1438 transitions = {
1438 'file': {'context': addcontext,
1439 'file': {'context': addcontext,
1439 'file': newfile,
1440 'file': newfile,
1440 'hunk': addhunk,
1441 'hunk': addhunk,
1441 'range': addrange},
1442 'range': addrange},
1442 'context': {'file': newfile,
1443 'context': {'file': newfile,
1443 'hunk': addhunk,
1444 'hunk': addhunk,
1444 'range': addrange,
1445 'range': addrange,
1445 'other': addother},
1446 'other': addother},
1446 'hunk': {'context': addcontext,
1447 'hunk': {'context': addcontext,
1447 'file': newfile,
1448 'file': newfile,
1448 'range': addrange},
1449 'range': addrange},
1449 'range': {'context': addcontext,
1450 'range': {'context': addcontext,
1450 'hunk': addhunk},
1451 'hunk': addhunk},
1451 'other': {'other': addother},
1452 'other': {'other': addother},
1452 }
1453 }
1453
1454
1454 p = parser()
1455 p = parser()
1455 fp = cStringIO.StringIO()
1456 fp = cStringIO.StringIO()
1456 fp.write(''.join(originalchunks))
1457 fp.write(''.join(originalchunks))
1457 fp.seek(0)
1458 fp.seek(0)
1458
1459
1459 state = 'context'
1460 state = 'context'
1460 for newstate, data in scanpatch(fp):
1461 for newstate, data in scanpatch(fp):
1461 try:
1462 try:
1462 p.transitions[state][newstate](p, data)
1463 p.transitions[state][newstate](p, data)
1463 except KeyError:
1464 except KeyError:
1464 raise PatchError('unhandled transition: %s -> %s' %
1465 raise PatchError('unhandled transition: %s -> %s' %
1465 (state, newstate))
1466 (state, newstate))
1466 state = newstate
1467 state = newstate
1467 del fp
1468 del fp
1468 return p.finished()
1469 return p.finished()
1469
1470
1470 def pathtransform(path, strip, prefix):
1471 def pathtransform(path, strip, prefix):
1471 '''turn a path from a patch into a path suitable for the repository
1472 '''turn a path from a patch into a path suitable for the repository
1472
1473
1473 prefix, if not empty, is expected to be normalized with a / at the end.
1474 prefix, if not empty, is expected to be normalized with a / at the end.
1474
1475
1475 Returns (stripped components, path in repository).
1476 Returns (stripped components, path in repository).
1476
1477
1477 >>> pathtransform('a/b/c', 0, '')
1478 >>> pathtransform('a/b/c', 0, '')
1478 ('', 'a/b/c')
1479 ('', 'a/b/c')
1479 >>> pathtransform(' a/b/c ', 0, '')
1480 >>> pathtransform(' a/b/c ', 0, '')
1480 ('', ' a/b/c')
1481 ('', ' a/b/c')
1481 >>> pathtransform(' a/b/c ', 2, '')
1482 >>> pathtransform(' a/b/c ', 2, '')
1482 ('a/b/', 'c')
1483 ('a/b/', 'c')
1483 >>> pathtransform('a/b/c', 0, 'd/e/')
1484 >>> pathtransform('a/b/c', 0, 'd/e/')
1484 ('', 'd/e/a/b/c')
1485 ('', 'd/e/a/b/c')
1485 >>> pathtransform(' a//b/c ', 2, 'd/e/')
1486 >>> pathtransform(' a//b/c ', 2, 'd/e/')
1486 ('a//b/', 'd/e/c')
1487 ('a//b/', 'd/e/c')
1487 >>> pathtransform('a/b/c', 3, '')
1488 >>> pathtransform('a/b/c', 3, '')
1488 Traceback (most recent call last):
1489 Traceback (most recent call last):
1489 PatchError: unable to strip away 1 of 3 dirs from a/b/c
1490 PatchError: unable to strip away 1 of 3 dirs from a/b/c
1490 '''
1491 '''
1491 pathlen = len(path)
1492 pathlen = len(path)
1492 i = 0
1493 i = 0
1493 if strip == 0:
1494 if strip == 0:
1494 return '', prefix + path.rstrip()
1495 return '', prefix + path.rstrip()
1495 count = strip
1496 count = strip
1496 while count > 0:
1497 while count > 0:
1497 i = path.find('/', i)
1498 i = path.find('/', i)
1498 if i == -1:
1499 if i == -1:
1499 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1500 raise PatchError(_("unable to strip away %d of %d dirs from %s") %
1500 (count, strip, path))
1501 (count, strip, path))
1501 i += 1
1502 i += 1
1502 # consume '//' in the path
1503 # consume '//' in the path
1503 while i < pathlen - 1 and path[i] == '/':
1504 while i < pathlen - 1 and path[i] == '/':
1504 i += 1
1505 i += 1
1505 count -= 1
1506 count -= 1
1506 return path[:i].lstrip(), prefix + path[i:].rstrip()
1507 return path[:i].lstrip(), prefix + path[i:].rstrip()
1507
1508
1508 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1509 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1509 nulla = afile_orig == "/dev/null"
1510 nulla = afile_orig == "/dev/null"
1510 nullb = bfile_orig == "/dev/null"
1511 nullb = bfile_orig == "/dev/null"
1511 create = nulla and hunk.starta == 0 and hunk.lena == 0
1512 create = nulla and hunk.starta == 0 and hunk.lena == 0
1512 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1513 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1513 abase, afile = pathtransform(afile_orig, strip, prefix)
1514 abase, afile = pathtransform(afile_orig, strip, prefix)
1514 gooda = not nulla and backend.exists(afile)
1515 gooda = not nulla and backend.exists(afile)
1515 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1516 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1516 if afile == bfile:
1517 if afile == bfile:
1517 goodb = gooda
1518 goodb = gooda
1518 else:
1519 else:
1519 goodb = not nullb and backend.exists(bfile)
1520 goodb = not nullb and backend.exists(bfile)
1520 missing = not goodb and not gooda and not create
1521 missing = not goodb and not gooda and not create
1521
1522
1522 # some diff programs apparently produce patches where the afile is
1523 # some diff programs apparently produce patches where the afile is
1523 # not /dev/null, but afile starts with bfile
1524 # not /dev/null, but afile starts with bfile
1524 abasedir = afile[:afile.rfind('/') + 1]
1525 abasedir = afile[:afile.rfind('/') + 1]
1525 bbasedir = bfile[:bfile.rfind('/') + 1]
1526 bbasedir = bfile[:bfile.rfind('/') + 1]
1526 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1527 if (missing and abasedir == bbasedir and afile.startswith(bfile)
1527 and hunk.starta == 0 and hunk.lena == 0):
1528 and hunk.starta == 0 and hunk.lena == 0):
1528 create = True
1529 create = True
1529 missing = False
1530 missing = False
1530
1531
1531 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1532 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1532 # diff is between a file and its backup. In this case, the original
1533 # diff is between a file and its backup. In this case, the original
1533 # file should be patched (see original mpatch code).
1534 # file should be patched (see original mpatch code).
1534 isbackup = (abase == bbase and bfile.startswith(afile))
1535 isbackup = (abase == bbase and bfile.startswith(afile))
1535 fname = None
1536 fname = None
1536 if not missing:
1537 if not missing:
1537 if gooda and goodb:
1538 if gooda and goodb:
1538 if isbackup:
1539 if isbackup:
1539 fname = afile
1540 fname = afile
1540 else:
1541 else:
1541 fname = bfile
1542 fname = bfile
1542 elif gooda:
1543 elif gooda:
1543 fname = afile
1544 fname = afile
1544
1545
1545 if not fname:
1546 if not fname:
1546 if not nullb:
1547 if not nullb:
1547 if isbackup:
1548 if isbackup:
1548 fname = afile
1549 fname = afile
1549 else:
1550 else:
1550 fname = bfile
1551 fname = bfile
1551 elif not nulla:
1552 elif not nulla:
1552 fname = afile
1553 fname = afile
1553 else:
1554 else:
1554 raise PatchError(_("undefined source and destination files"))
1555 raise PatchError(_("undefined source and destination files"))
1555
1556
1556 gp = patchmeta(fname)
1557 gp = patchmeta(fname)
1557 if create:
1558 if create:
1558 gp.op = 'ADD'
1559 gp.op = 'ADD'
1559 elif remove:
1560 elif remove:
1560 gp.op = 'DELETE'
1561 gp.op = 'DELETE'
1561 return gp
1562 return gp
1562
1563
1563 def scanpatch(fp):
1564 def scanpatch(fp):
1564 """like patch.iterhunks, but yield different events
1565 """like patch.iterhunks, but yield different events
1565
1566
1566 - ('file', [header_lines + fromfile + tofile])
1567 - ('file', [header_lines + fromfile + tofile])
1567 - ('context', [context_lines])
1568 - ('context', [context_lines])
1568 - ('hunk', [hunk_lines])
1569 - ('hunk', [hunk_lines])
1569 - ('range', (-start,len, +start,len, proc))
1570 - ('range', (-start,len, +start,len, proc))
1570 """
1571 """
1571 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
1572 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
1572 lr = linereader(fp)
1573 lr = linereader(fp)
1573
1574
1574 def scanwhile(first, p):
1575 def scanwhile(first, p):
1575 """scan lr while predicate holds"""
1576 """scan lr while predicate holds"""
1576 lines = [first]
1577 lines = [first]
1577 while True:
1578 while True:
1578 line = lr.readline()
1579 line = lr.readline()
1579 if not line:
1580 if not line:
1580 break
1581 break
1581 if p(line):
1582 if p(line):
1582 lines.append(line)
1583 lines.append(line)
1583 else:
1584 else:
1584 lr.push(line)
1585 lr.push(line)
1585 break
1586 break
1586 return lines
1587 return lines
1587
1588
1588 while True:
1589 while True:
1589 line = lr.readline()
1590 line = lr.readline()
1590 if not line:
1591 if not line:
1591 break
1592 break
1592 if line.startswith('diff --git a/') or line.startswith('diff -r '):
1593 if line.startswith('diff --git a/') or line.startswith('diff -r '):
1593 def notheader(line):
1594 def notheader(line):
1594 s = line.split(None, 1)
1595 s = line.split(None, 1)
1595 return not s or s[0] not in ('---', 'diff')
1596 return not s or s[0] not in ('---', 'diff')
1596 header = scanwhile(line, notheader)
1597 header = scanwhile(line, notheader)
1597 fromfile = lr.readline()
1598 fromfile = lr.readline()
1598 if fromfile.startswith('---'):
1599 if fromfile.startswith('---'):
1599 tofile = lr.readline()
1600 tofile = lr.readline()
1600 header += [fromfile, tofile]
1601 header += [fromfile, tofile]
1601 else:
1602 else:
1602 lr.push(fromfile)
1603 lr.push(fromfile)
1603 yield 'file', header
1604 yield 'file', header
1604 elif line[0] == ' ':
1605 elif line[0] == ' ':
1605 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
1606 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
1606 elif line[0] in '-+':
1607 elif line[0] in '-+':
1607 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
1608 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
1608 else:
1609 else:
1609 m = lines_re.match(line)
1610 m = lines_re.match(line)
1610 if m:
1611 if m:
1611 yield 'range', m.groups()
1612 yield 'range', m.groups()
1612 else:
1613 else:
1613 yield 'other', line
1614 yield 'other', line
1614
1615
1615 def scangitpatch(lr, firstline):
1616 def scangitpatch(lr, firstline):
1616 """
1617 """
1617 Git patches can emit:
1618 Git patches can emit:
1618 - rename a to b
1619 - rename a to b
1619 - change b
1620 - change b
1620 - copy a to c
1621 - copy a to c
1621 - change c
1622 - change c
1622
1623
1623 We cannot apply this sequence as-is, the renamed 'a' could not be
1624 We cannot apply this sequence as-is, the renamed 'a' could not be
1624 found for it would have been renamed already. And we cannot copy
1625 found for it would have been renamed already. And we cannot copy
1625 from 'b' instead because 'b' would have been changed already. So
1626 from 'b' instead because 'b' would have been changed already. So
1626 we scan the git patch for copy and rename commands so we can
1627 we scan the git patch for copy and rename commands so we can
1627 perform the copies ahead of time.
1628 perform the copies ahead of time.
1628 """
1629 """
1629 pos = 0
1630 pos = 0
1630 try:
1631 try:
1631 pos = lr.fp.tell()
1632 pos = lr.fp.tell()
1632 fp = lr.fp
1633 fp = lr.fp
1633 except IOError:
1634 except IOError:
1634 fp = cStringIO.StringIO(lr.fp.read())
1635 fp = cStringIO.StringIO(lr.fp.read())
1635 gitlr = linereader(fp)
1636 gitlr = linereader(fp)
1636 gitlr.push(firstline)
1637 gitlr.push(firstline)
1637 gitpatches = readgitpatch(gitlr)
1638 gitpatches = readgitpatch(gitlr)
1638 fp.seek(pos)
1639 fp.seek(pos)
1639 return gitpatches
1640 return gitpatches
1640
1641
1641 def iterhunks(fp):
1642 def iterhunks(fp):
1642 """Read a patch and yield the following events:
1643 """Read a patch and yield the following events:
1643 - ("file", afile, bfile, firsthunk): select a new target file.
1644 - ("file", afile, bfile, firsthunk): select a new target file.
1644 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1645 - ("hunk", hunk): a new hunk is ready to be applied, follows a
1645 "file" event.
1646 "file" event.
1646 - ("git", gitchanges): current diff is in git format, gitchanges
1647 - ("git", gitchanges): current diff is in git format, gitchanges
1647 maps filenames to gitpatch records. Unique event.
1648 maps filenames to gitpatch records. Unique event.
1648 """
1649 """
1649 afile = ""
1650 afile = ""
1650 bfile = ""
1651 bfile = ""
1651 state = None
1652 state = None
1652 hunknum = 0
1653 hunknum = 0
1653 emitfile = newfile = False
1654 emitfile = newfile = False
1654 gitpatches = None
1655 gitpatches = None
1655
1656
1656 # our states
1657 # our states
1657 BFILE = 1
1658 BFILE = 1
1658 context = None
1659 context = None
1659 lr = linereader(fp)
1660 lr = linereader(fp)
1660
1661
1661 while True:
1662 while True:
1662 x = lr.readline()
1663 x = lr.readline()
1663 if not x:
1664 if not x:
1664 break
1665 break
1665 if state == BFILE and (
1666 if state == BFILE and (
1666 (not context and x[0] == '@')
1667 (not context and x[0] == '@')
1667 or (context is not False and x.startswith('***************'))
1668 or (context is not False and x.startswith('***************'))
1668 or x.startswith('GIT binary patch')):
1669 or x.startswith('GIT binary patch')):
1669 gp = None
1670 gp = None
1670 if (gitpatches and
1671 if (gitpatches and
1671 gitpatches[-1].ispatching(afile, bfile)):
1672 gitpatches[-1].ispatching(afile, bfile)):
1672 gp = gitpatches.pop()
1673 gp = gitpatches.pop()
1673 if x.startswith('GIT binary patch'):
1674 if x.startswith('GIT binary patch'):
1674 h = binhunk(lr, gp.path)
1675 h = binhunk(lr, gp.path)
1675 else:
1676 else:
1676 if context is None and x.startswith('***************'):
1677 if context is None and x.startswith('***************'):
1677 context = True
1678 context = True
1678 h = hunk(x, hunknum + 1, lr, context)
1679 h = hunk(x, hunknum + 1, lr, context)
1679 hunknum += 1
1680 hunknum += 1
1680 if emitfile:
1681 if emitfile:
1681 emitfile = False
1682 emitfile = False
1682 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1683 yield 'file', (afile, bfile, h, gp and gp.copy() or None)
1683 yield 'hunk', h
1684 yield 'hunk', h
1684 elif x.startswith('diff --git a/'):
1685 elif x.startswith('diff --git a/'):
1685 m = gitre.match(x.rstrip(' \r\n'))
1686 m = gitre.match(x.rstrip(' \r\n'))
1686 if not m:
1687 if not m:
1687 continue
1688 continue
1688 if gitpatches is None:
1689 if gitpatches is None:
1689 # scan whole input for git metadata
1690 # scan whole input for git metadata
1690 gitpatches = scangitpatch(lr, x)
1691 gitpatches = scangitpatch(lr, x)
1691 yield 'git', [g.copy() for g in gitpatches
1692 yield 'git', [g.copy() for g in gitpatches
1692 if g.op in ('COPY', 'RENAME')]
1693 if g.op in ('COPY', 'RENAME')]
1693 gitpatches.reverse()
1694 gitpatches.reverse()
1694 afile = 'a/' + m.group(1)
1695 afile = 'a/' + m.group(1)
1695 bfile = 'b/' + m.group(2)
1696 bfile = 'b/' + m.group(2)
1696 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1697 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
1697 gp = gitpatches.pop()
1698 gp = gitpatches.pop()
1698 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1699 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1699 if not gitpatches:
1700 if not gitpatches:
1700 raise PatchError(_('failed to synchronize metadata for "%s"')
1701 raise PatchError(_('failed to synchronize metadata for "%s"')
1701 % afile[2:])
1702 % afile[2:])
1702 gp = gitpatches[-1]
1703 gp = gitpatches[-1]
1703 newfile = True
1704 newfile = True
1704 elif x.startswith('---'):
1705 elif x.startswith('---'):
1705 # check for a unified diff
1706 # check for a unified diff
1706 l2 = lr.readline()
1707 l2 = lr.readline()
1707 if not l2.startswith('+++'):
1708 if not l2.startswith('+++'):
1708 lr.push(l2)
1709 lr.push(l2)
1709 continue
1710 continue
1710 newfile = True
1711 newfile = True
1711 context = False
1712 context = False
1712 afile = parsefilename(x)
1713 afile = parsefilename(x)
1713 bfile = parsefilename(l2)
1714 bfile = parsefilename(l2)
1714 elif x.startswith('***'):
1715 elif x.startswith('***'):
1715 # check for a context diff
1716 # check for a context diff
1716 l2 = lr.readline()
1717 l2 = lr.readline()
1717 if not l2.startswith('---'):
1718 if not l2.startswith('---'):
1718 lr.push(l2)
1719 lr.push(l2)
1719 continue
1720 continue
1720 l3 = lr.readline()
1721 l3 = lr.readline()
1721 lr.push(l3)
1722 lr.push(l3)
1722 if not l3.startswith("***************"):
1723 if not l3.startswith("***************"):
1723 lr.push(l2)
1724 lr.push(l2)
1724 continue
1725 continue
1725 newfile = True
1726 newfile = True
1726 context = True
1727 context = True
1727 afile = parsefilename(x)
1728 afile = parsefilename(x)
1728 bfile = parsefilename(l2)
1729 bfile = parsefilename(l2)
1729
1730
1730 if newfile:
1731 if newfile:
1731 newfile = False
1732 newfile = False
1732 emitfile = True
1733 emitfile = True
1733 state = BFILE
1734 state = BFILE
1734 hunknum = 0
1735 hunknum = 0
1735
1736
1736 while gitpatches:
1737 while gitpatches:
1737 gp = gitpatches.pop()
1738 gp = gitpatches.pop()
1738 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1739 yield 'file', ('a/' + gp.path, 'b/' + gp.path, None, gp.copy())
1739
1740
1740 def applybindelta(binchunk, data):
1741 def applybindelta(binchunk, data):
1741 """Apply a binary delta hunk
1742 """Apply a binary delta hunk
1742 The algorithm used is the algorithm from git's patch-delta.c
1743 The algorithm used is the algorithm from git's patch-delta.c
1743 """
1744 """
1744 def deltahead(binchunk):
1745 def deltahead(binchunk):
1745 i = 0
1746 i = 0
1746 for c in binchunk:
1747 for c in binchunk:
1747 i += 1
1748 i += 1
1748 if not (ord(c) & 0x80):
1749 if not (ord(c) & 0x80):
1749 return i
1750 return i
1750 return i
1751 return i
1751 out = ""
1752 out = ""
1752 s = deltahead(binchunk)
1753 s = deltahead(binchunk)
1753 binchunk = binchunk[s:]
1754 binchunk = binchunk[s:]
1754 s = deltahead(binchunk)
1755 s = deltahead(binchunk)
1755 binchunk = binchunk[s:]
1756 binchunk = binchunk[s:]
1756 i = 0
1757 i = 0
1757 while i < len(binchunk):
1758 while i < len(binchunk):
1758 cmd = ord(binchunk[i])
1759 cmd = ord(binchunk[i])
1759 i += 1
1760 i += 1
1760 if (cmd & 0x80):
1761 if (cmd & 0x80):
1761 offset = 0
1762 offset = 0
1762 size = 0
1763 size = 0
1763 if (cmd & 0x01):
1764 if (cmd & 0x01):
1764 offset = ord(binchunk[i])
1765 offset = ord(binchunk[i])
1765 i += 1
1766 i += 1
1766 if (cmd & 0x02):
1767 if (cmd & 0x02):
1767 offset |= ord(binchunk[i]) << 8
1768 offset |= ord(binchunk[i]) << 8
1768 i += 1
1769 i += 1
1769 if (cmd & 0x04):
1770 if (cmd & 0x04):
1770 offset |= ord(binchunk[i]) << 16
1771 offset |= ord(binchunk[i]) << 16
1771 i += 1
1772 i += 1
1772 if (cmd & 0x08):
1773 if (cmd & 0x08):
1773 offset |= ord(binchunk[i]) << 24
1774 offset |= ord(binchunk[i]) << 24
1774 i += 1
1775 i += 1
1775 if (cmd & 0x10):
1776 if (cmd & 0x10):
1776 size = ord(binchunk[i])
1777 size = ord(binchunk[i])
1777 i += 1
1778 i += 1
1778 if (cmd & 0x20):
1779 if (cmd & 0x20):
1779 size |= ord(binchunk[i]) << 8
1780 size |= ord(binchunk[i]) << 8
1780 i += 1
1781 i += 1
1781 if (cmd & 0x40):
1782 if (cmd & 0x40):
1782 size |= ord(binchunk[i]) << 16
1783 size |= ord(binchunk[i]) << 16
1783 i += 1
1784 i += 1
1784 if size == 0:
1785 if size == 0:
1785 size = 0x10000
1786 size = 0x10000
1786 offset_end = offset + size
1787 offset_end = offset + size
1787 out += data[offset:offset_end]
1788 out += data[offset:offset_end]
1788 elif cmd != 0:
1789 elif cmd != 0:
1789 offset_end = i + cmd
1790 offset_end = i + cmd
1790 out += binchunk[i:offset_end]
1791 out += binchunk[i:offset_end]
1791 i += cmd
1792 i += cmd
1792 else:
1793 else:
1793 raise PatchError(_('unexpected delta opcode 0'))
1794 raise PatchError(_('unexpected delta opcode 0'))
1794 return out
1795 return out
1795
1796
1796 def applydiff(ui, fp, backend, store, strip=1, prefix='', eolmode='strict'):
1797 def applydiff(ui, fp, backend, store, strip=1, prefix='', eolmode='strict'):
1797 """Reads a patch from fp and tries to apply it.
1798 """Reads a patch from fp and tries to apply it.
1798
1799
1799 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1800 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
1800 there was any fuzz.
1801 there was any fuzz.
1801
1802
1802 If 'eolmode' is 'strict', the patch content and patched file are
1803 If 'eolmode' is 'strict', the patch content and patched file are
1803 read in binary mode. Otherwise, line endings are ignored when
1804 read in binary mode. Otherwise, line endings are ignored when
1804 patching then normalized according to 'eolmode'.
1805 patching then normalized according to 'eolmode'.
1805 """
1806 """
1806 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1807 return _applydiff(ui, fp, patchfile, backend, store, strip=strip,
1807 prefix=prefix, eolmode=eolmode)
1808 prefix=prefix, eolmode=eolmode)
1808
1809
1809 def _applydiff(ui, fp, patcher, backend, store, strip=1, prefix='',
1810 def _applydiff(ui, fp, patcher, backend, store, strip=1, prefix='',
1810 eolmode='strict'):
1811 eolmode='strict'):
1811
1812
1812 if prefix:
1813 if prefix:
1813 prefix = pathutil.canonpath(backend.repo.root, backend.repo.getcwd(),
1814 prefix = pathutil.canonpath(backend.repo.root, backend.repo.getcwd(),
1814 prefix)
1815 prefix)
1815 if prefix != '':
1816 if prefix != '':
1816 prefix += '/'
1817 prefix += '/'
1817 def pstrip(p):
1818 def pstrip(p):
1818 return pathtransform(p, strip - 1, prefix)[1]
1819 return pathtransform(p, strip - 1, prefix)[1]
1819
1820
1820 rejects = 0
1821 rejects = 0
1821 err = 0
1822 err = 0
1822 current_file = None
1823 current_file = None
1823
1824
1824 for state, values in iterhunks(fp):
1825 for state, values in iterhunks(fp):
1825 if state == 'hunk':
1826 if state == 'hunk':
1826 if not current_file:
1827 if not current_file:
1827 continue
1828 continue
1828 ret = current_file.apply(values)
1829 ret = current_file.apply(values)
1829 if ret > 0:
1830 if ret > 0:
1830 err = 1
1831 err = 1
1831 elif state == 'file':
1832 elif state == 'file':
1832 if current_file:
1833 if current_file:
1833 rejects += current_file.close()
1834 rejects += current_file.close()
1834 current_file = None
1835 current_file = None
1835 afile, bfile, first_hunk, gp = values
1836 afile, bfile, first_hunk, gp = values
1836 if gp:
1837 if gp:
1837 gp.path = pstrip(gp.path)
1838 gp.path = pstrip(gp.path)
1838 if gp.oldpath:
1839 if gp.oldpath:
1839 gp.oldpath = pstrip(gp.oldpath)
1840 gp.oldpath = pstrip(gp.oldpath)
1840 else:
1841 else:
1841 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
1842 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
1842 prefix)
1843 prefix)
1843 if gp.op == 'RENAME':
1844 if gp.op == 'RENAME':
1844 backend.unlink(gp.oldpath)
1845 backend.unlink(gp.oldpath)
1845 if not first_hunk:
1846 if not first_hunk:
1846 if gp.op == 'DELETE':
1847 if gp.op == 'DELETE':
1847 backend.unlink(gp.path)
1848 backend.unlink(gp.path)
1848 continue
1849 continue
1849 data, mode = None, None
1850 data, mode = None, None
1850 if gp.op in ('RENAME', 'COPY'):
1851 if gp.op in ('RENAME', 'COPY'):
1851 data, mode = store.getfile(gp.oldpath)[:2]
1852 data, mode = store.getfile(gp.oldpath)[:2]
1852 # FIXME: failing getfile has never been handled here
1853 # FIXME: failing getfile has never been handled here
1853 assert data is not None
1854 assert data is not None
1854 if gp.mode:
1855 if gp.mode:
1855 mode = gp.mode
1856 mode = gp.mode
1856 if gp.op == 'ADD':
1857 if gp.op == 'ADD':
1857 # Added files without content have no hunk and
1858 # Added files without content have no hunk and
1858 # must be created
1859 # must be created
1859 data = ''
1860 data = ''
1860 if data or mode:
1861 if data or mode:
1861 if (gp.op in ('ADD', 'RENAME', 'COPY')
1862 if (gp.op in ('ADD', 'RENAME', 'COPY')
1862 and backend.exists(gp.path)):
1863 and backend.exists(gp.path)):
1863 raise PatchError(_("cannot create %s: destination "
1864 raise PatchError(_("cannot create %s: destination "
1864 "already exists") % gp.path)
1865 "already exists") % gp.path)
1865 backend.setfile(gp.path, data, mode, gp.oldpath)
1866 backend.setfile(gp.path, data, mode, gp.oldpath)
1866 continue
1867 continue
1867 try:
1868 try:
1868 current_file = patcher(ui, gp, backend, store,
1869 current_file = patcher(ui, gp, backend, store,
1869 eolmode=eolmode)
1870 eolmode=eolmode)
1870 except PatchError, inst:
1871 except PatchError, inst:
1871 ui.warn(str(inst) + '\n')
1872 ui.warn(str(inst) + '\n')
1872 current_file = None
1873 current_file = None
1873 rejects += 1
1874 rejects += 1
1874 continue
1875 continue
1875 elif state == 'git':
1876 elif state == 'git':
1876 for gp in values:
1877 for gp in values:
1877 path = pstrip(gp.oldpath)
1878 path = pstrip(gp.oldpath)
1878 data, mode = backend.getfile(path)
1879 data, mode = backend.getfile(path)
1879 if data is None:
1880 if data is None:
1880 # The error ignored here will trigger a getfile()
1881 # The error ignored here will trigger a getfile()
1881 # error in a place more appropriate for error
1882 # error in a place more appropriate for error
1882 # handling, and will not interrupt the patching
1883 # handling, and will not interrupt the patching
1883 # process.
1884 # process.
1884 pass
1885 pass
1885 else:
1886 else:
1886 store.setfile(path, data, mode)
1887 store.setfile(path, data, mode)
1887 else:
1888 else:
1888 raise util.Abort(_('unsupported parser state: %s') % state)
1889 raise util.Abort(_('unsupported parser state: %s') % state)
1889
1890
1890 if current_file:
1891 if current_file:
1891 rejects += current_file.close()
1892 rejects += current_file.close()
1892
1893
1893 if rejects:
1894 if rejects:
1894 return -1
1895 return -1
1895 return err
1896 return err
1896
1897
1897 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1898 def _externalpatch(ui, repo, patcher, patchname, strip, files,
1898 similarity):
1899 similarity):
1899 """use <patcher> to apply <patchname> to the working directory.
1900 """use <patcher> to apply <patchname> to the working directory.
1900 returns whether patch was applied with fuzz factor."""
1901 returns whether patch was applied with fuzz factor."""
1901
1902
1902 fuzz = False
1903 fuzz = False
1903 args = []
1904 args = []
1904 cwd = repo.root
1905 cwd = repo.root
1905 if cwd:
1906 if cwd:
1906 args.append('-d %s' % util.shellquote(cwd))
1907 args.append('-d %s' % util.shellquote(cwd))
1907 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1908 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1908 util.shellquote(patchname)))
1909 util.shellquote(patchname)))
1909 try:
1910 try:
1910 for line in fp:
1911 for line in fp:
1911 line = line.rstrip()
1912 line = line.rstrip()
1912 ui.note(line + '\n')
1913 ui.note(line + '\n')
1913 if line.startswith('patching file '):
1914 if line.startswith('patching file '):
1914 pf = util.parsepatchoutput(line)
1915 pf = util.parsepatchoutput(line)
1915 printed_file = False
1916 printed_file = False
1916 files.add(pf)
1917 files.add(pf)
1917 elif line.find('with fuzz') >= 0:
1918 elif line.find('with fuzz') >= 0:
1918 fuzz = True
1919 fuzz = True
1919 if not printed_file:
1920 if not printed_file:
1920 ui.warn(pf + '\n')
1921 ui.warn(pf + '\n')
1921 printed_file = True
1922 printed_file = True
1922 ui.warn(line + '\n')
1923 ui.warn(line + '\n')
1923 elif line.find('saving rejects to file') >= 0:
1924 elif line.find('saving rejects to file') >= 0:
1924 ui.warn(line + '\n')
1925 ui.warn(line + '\n')
1925 elif line.find('FAILED') >= 0:
1926 elif line.find('FAILED') >= 0:
1926 if not printed_file:
1927 if not printed_file:
1927 ui.warn(pf + '\n')
1928 ui.warn(pf + '\n')
1928 printed_file = True
1929 printed_file = True
1929 ui.warn(line + '\n')
1930 ui.warn(line + '\n')
1930 finally:
1931 finally:
1931 if files:
1932 if files:
1932 scmutil.marktouched(repo, files, similarity)
1933 scmutil.marktouched(repo, files, similarity)
1933 code = fp.close()
1934 code = fp.close()
1934 if code:
1935 if code:
1935 raise PatchError(_("patch command failed: %s") %
1936 raise PatchError(_("patch command failed: %s") %
1936 util.explainexit(code)[0])
1937 util.explainexit(code)[0])
1937 return fuzz
1938 return fuzz
1938
1939
1939 def patchbackend(ui, backend, patchobj, strip, prefix, files=None,
1940 def patchbackend(ui, backend, patchobj, strip, prefix, files=None,
1940 eolmode='strict'):
1941 eolmode='strict'):
1941 if files is None:
1942 if files is None:
1942 files = set()
1943 files = set()
1943 if eolmode is None:
1944 if eolmode is None:
1944 eolmode = ui.config('patch', 'eol', 'strict')
1945 eolmode = ui.config('patch', 'eol', 'strict')
1945 if eolmode.lower() not in eolmodes:
1946 if eolmode.lower() not in eolmodes:
1946 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1947 raise util.Abort(_('unsupported line endings type: %s') % eolmode)
1947 eolmode = eolmode.lower()
1948 eolmode = eolmode.lower()
1948
1949
1949 store = filestore()
1950 store = filestore()
1950 try:
1951 try:
1951 fp = open(patchobj, 'rb')
1952 fp = open(patchobj, 'rb')
1952 except TypeError:
1953 except TypeError:
1953 fp = patchobj
1954 fp = patchobj
1954 try:
1955 try:
1955 ret = applydiff(ui, fp, backend, store, strip=strip, prefix=prefix,
1956 ret = applydiff(ui, fp, backend, store, strip=strip, prefix=prefix,
1956 eolmode=eolmode)
1957 eolmode=eolmode)
1957 finally:
1958 finally:
1958 if fp != patchobj:
1959 if fp != patchobj:
1959 fp.close()
1960 fp.close()
1960 files.update(backend.close())
1961 files.update(backend.close())
1961 store.close()
1962 store.close()
1962 if ret < 0:
1963 if ret < 0:
1963 raise PatchError(_('patch failed to apply'))
1964 raise PatchError(_('patch failed to apply'))
1964 return ret > 0
1965 return ret > 0
1965
1966
1966 def internalpatch(ui, repo, patchobj, strip, prefix='', files=None,
1967 def internalpatch(ui, repo, patchobj, strip, prefix='', files=None,
1967 eolmode='strict', similarity=0):
1968 eolmode='strict', similarity=0):
1968 """use builtin patch to apply <patchobj> to the working directory.
1969 """use builtin patch to apply <patchobj> to the working directory.
1969 returns whether patch was applied with fuzz factor."""
1970 returns whether patch was applied with fuzz factor."""
1970 backend = workingbackend(ui, repo, similarity)
1971 backend = workingbackend(ui, repo, similarity)
1971 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
1972 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
1972
1973
1973 def patchrepo(ui, repo, ctx, store, patchobj, strip, prefix, files=None,
1974 def patchrepo(ui, repo, ctx, store, patchobj, strip, prefix, files=None,
1974 eolmode='strict'):
1975 eolmode='strict'):
1975 backend = repobackend(ui, repo, ctx, store)
1976 backend = repobackend(ui, repo, ctx, store)
1976 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
1977 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
1977
1978
1978 def patch(ui, repo, patchname, strip=1, prefix='', files=None, eolmode='strict',
1979 def patch(ui, repo, patchname, strip=1, prefix='', files=None, eolmode='strict',
1979 similarity=0):
1980 similarity=0):
1980 """Apply <patchname> to the working directory.
1981 """Apply <patchname> to the working directory.
1981
1982
1982 'eolmode' specifies how end of lines should be handled. It can be:
1983 'eolmode' specifies how end of lines should be handled. It can be:
1983 - 'strict': inputs are read in binary mode, EOLs are preserved
1984 - 'strict': inputs are read in binary mode, EOLs are preserved
1984 - 'crlf': EOLs are ignored when patching and reset to CRLF
1985 - 'crlf': EOLs are ignored when patching and reset to CRLF
1985 - 'lf': EOLs are ignored when patching and reset to LF
1986 - 'lf': EOLs are ignored when patching and reset to LF
1986 - None: get it from user settings, default to 'strict'
1987 - None: get it from user settings, default to 'strict'
1987 'eolmode' is ignored when using an external patcher program.
1988 'eolmode' is ignored when using an external patcher program.
1988
1989
1989 Returns whether patch was applied with fuzz factor.
1990 Returns whether patch was applied with fuzz factor.
1990 """
1991 """
1991 patcher = ui.config('ui', 'patch')
1992 patcher = ui.config('ui', 'patch')
1992 if files is None:
1993 if files is None:
1993 files = set()
1994 files = set()
1994 if patcher:
1995 if patcher:
1995 return _externalpatch(ui, repo, patcher, patchname, strip,
1996 return _externalpatch(ui, repo, patcher, patchname, strip,
1996 files, similarity)
1997 files, similarity)
1997 return internalpatch(ui, repo, patchname, strip, prefix, files, eolmode,
1998 return internalpatch(ui, repo, patchname, strip, prefix, files, eolmode,
1998 similarity)
1999 similarity)
1999
2000
2000 def changedfiles(ui, repo, patchpath, strip=1):
2001 def changedfiles(ui, repo, patchpath, strip=1):
2001 backend = fsbackend(ui, repo.root)
2002 backend = fsbackend(ui, repo.root)
2002 fp = open(patchpath, 'rb')
2003 fp = open(patchpath, 'rb')
2003 try:
2004 try:
2004 changed = set()
2005 changed = set()
2005 for state, values in iterhunks(fp):
2006 for state, values in iterhunks(fp):
2006 if state == 'file':
2007 if state == 'file':
2007 afile, bfile, first_hunk, gp = values
2008 afile, bfile, first_hunk, gp = values
2008 if gp:
2009 if gp:
2009 gp.path = pathtransform(gp.path, strip - 1, '')[1]
2010 gp.path = pathtransform(gp.path, strip - 1, '')[1]
2010 if gp.oldpath:
2011 if gp.oldpath:
2011 gp.oldpath = pathtransform(gp.oldpath, strip - 1, '')[1]
2012 gp.oldpath = pathtransform(gp.oldpath, strip - 1, '')[1]
2012 else:
2013 else:
2013 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
2014 gp = makepatchmeta(backend, afile, bfile, first_hunk, strip,
2014 '')
2015 '')
2015 changed.add(gp.path)
2016 changed.add(gp.path)
2016 if gp.op == 'RENAME':
2017 if gp.op == 'RENAME':
2017 changed.add(gp.oldpath)
2018 changed.add(gp.oldpath)
2018 elif state not in ('hunk', 'git'):
2019 elif state not in ('hunk', 'git'):
2019 raise util.Abort(_('unsupported parser state: %s') % state)
2020 raise util.Abort(_('unsupported parser state: %s') % state)
2020 return changed
2021 return changed
2021 finally:
2022 finally:
2022 fp.close()
2023 fp.close()
2023
2024
2024 class GitDiffRequired(Exception):
2025 class GitDiffRequired(Exception):
2025 pass
2026 pass
2026
2027
2027 def diffallopts(ui, opts=None, untrusted=False, section='diff'):
2028 def diffallopts(ui, opts=None, untrusted=False, section='diff'):
2028 '''return diffopts with all features supported and parsed'''
2029 '''return diffopts with all features supported and parsed'''
2029 return difffeatureopts(ui, opts=opts, untrusted=untrusted, section=section,
2030 return difffeatureopts(ui, opts=opts, untrusted=untrusted, section=section,
2030 git=True, whitespace=True, formatchanging=True)
2031 git=True, whitespace=True, formatchanging=True)
2031
2032
2032 diffopts = diffallopts
2033 diffopts = diffallopts
2033
2034
2034 def difffeatureopts(ui, opts=None, untrusted=False, section='diff', git=False,
2035 def difffeatureopts(ui, opts=None, untrusted=False, section='diff', git=False,
2035 whitespace=False, formatchanging=False):
2036 whitespace=False, formatchanging=False):
2036 '''return diffopts with only opted-in features parsed
2037 '''return diffopts with only opted-in features parsed
2037
2038
2038 Features:
2039 Features:
2039 - git: git-style diffs
2040 - git: git-style diffs
2040 - whitespace: whitespace options like ignoreblanklines and ignorews
2041 - whitespace: whitespace options like ignoreblanklines and ignorews
2041 - formatchanging: options that will likely break or cause correctness issues
2042 - formatchanging: options that will likely break or cause correctness issues
2042 with most diff parsers
2043 with most diff parsers
2043 '''
2044 '''
2044 def get(key, name=None, getter=ui.configbool, forceplain=None):
2045 def get(key, name=None, getter=ui.configbool, forceplain=None):
2045 if opts:
2046 if opts:
2046 v = opts.get(key)
2047 v = opts.get(key)
2047 if v:
2048 if v:
2048 return v
2049 return v
2049 if forceplain is not None and ui.plain():
2050 if forceplain is not None and ui.plain():
2050 return forceplain
2051 return forceplain
2051 return getter(section, name or key, None, untrusted=untrusted)
2052 return getter(section, name or key, None, untrusted=untrusted)
2052
2053
2053 # core options, expected to be understood by every diff parser
2054 # core options, expected to be understood by every diff parser
2054 buildopts = {
2055 buildopts = {
2055 'nodates': get('nodates'),
2056 'nodates': get('nodates'),
2056 'showfunc': get('show_function', 'showfunc'),
2057 'showfunc': get('show_function', 'showfunc'),
2057 'context': get('unified', getter=ui.config),
2058 'context': get('unified', getter=ui.config),
2058 }
2059 }
2059
2060
2060 if git:
2061 if git:
2061 buildopts['git'] = get('git')
2062 buildopts['git'] = get('git')
2062 if whitespace:
2063 if whitespace:
2063 buildopts['ignorews'] = get('ignore_all_space', 'ignorews')
2064 buildopts['ignorews'] = get('ignore_all_space', 'ignorews')
2064 buildopts['ignorewsamount'] = get('ignore_space_change',
2065 buildopts['ignorewsamount'] = get('ignore_space_change',
2065 'ignorewsamount')
2066 'ignorewsamount')
2066 buildopts['ignoreblanklines'] = get('ignore_blank_lines',
2067 buildopts['ignoreblanklines'] = get('ignore_blank_lines',
2067 'ignoreblanklines')
2068 'ignoreblanklines')
2068 if formatchanging:
2069 if formatchanging:
2069 buildopts['text'] = opts and opts.get('text')
2070 buildopts['text'] = opts and opts.get('text')
2070 buildopts['nobinary'] = get('nobinary')
2071 buildopts['nobinary'] = get('nobinary')
2071 buildopts['noprefix'] = get('noprefix', forceplain=False)
2072 buildopts['noprefix'] = get('noprefix', forceplain=False)
2072
2073
2073 return mdiff.diffopts(**buildopts)
2074 return mdiff.diffopts(**buildopts)
2074
2075
2075 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
2076 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None,
2076 losedatafn=None, prefix='', relroot=''):
2077 losedatafn=None, prefix='', relroot=''):
2077 '''yields diff of changes to files between two nodes, or node and
2078 '''yields diff of changes to files between two nodes, or node and
2078 working directory.
2079 working directory.
2079
2080
2080 if node1 is None, use first dirstate parent instead.
2081 if node1 is None, use first dirstate parent instead.
2081 if node2 is None, compare node1 with working directory.
2082 if node2 is None, compare node1 with working directory.
2082
2083
2083 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
2084 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
2084 every time some change cannot be represented with the current
2085 every time some change cannot be represented with the current
2085 patch format. Return False to upgrade to git patch format, True to
2086 patch format. Return False to upgrade to git patch format, True to
2086 accept the loss or raise an exception to abort the diff. It is
2087 accept the loss or raise an exception to abort the diff. It is
2087 called with the name of current file being diffed as 'fn'. If set
2088 called with the name of current file being diffed as 'fn'. If set
2088 to None, patches will always be upgraded to git format when
2089 to None, patches will always be upgraded to git format when
2089 necessary.
2090 necessary.
2090
2091
2091 prefix is a filename prefix that is prepended to all filenames on
2092 prefix is a filename prefix that is prepended to all filenames on
2092 display (used for subrepos).
2093 display (used for subrepos).
2093
2094
2094 relroot, if not empty, must be normalized with a trailing /. Any match
2095 relroot, if not empty, must be normalized with a trailing /. Any match
2095 patterns that fall outside it will be ignored.'''
2096 patterns that fall outside it will be ignored.'''
2096
2097
2097 if opts is None:
2098 if opts is None:
2098 opts = mdiff.defaultopts
2099 opts = mdiff.defaultopts
2099
2100
2100 if not node1 and not node2:
2101 if not node1 and not node2:
2101 node1 = repo.dirstate.p1()
2102 node1 = repo.dirstate.p1()
2102
2103
2103 def lrugetfilectx():
2104 def lrugetfilectx():
2104 cache = {}
2105 cache = {}
2105 order = util.deque()
2106 order = collections.deque()
2106 def getfilectx(f, ctx):
2107 def getfilectx(f, ctx):
2107 fctx = ctx.filectx(f, filelog=cache.get(f))
2108 fctx = ctx.filectx(f, filelog=cache.get(f))
2108 if f not in cache:
2109 if f not in cache:
2109 if len(cache) > 20:
2110 if len(cache) > 20:
2110 del cache[order.popleft()]
2111 del cache[order.popleft()]
2111 cache[f] = fctx.filelog()
2112 cache[f] = fctx.filelog()
2112 else:
2113 else:
2113 order.remove(f)
2114 order.remove(f)
2114 order.append(f)
2115 order.append(f)
2115 return fctx
2116 return fctx
2116 return getfilectx
2117 return getfilectx
2117 getfilectx = lrugetfilectx()
2118 getfilectx = lrugetfilectx()
2118
2119
2119 ctx1 = repo[node1]
2120 ctx1 = repo[node1]
2120 ctx2 = repo[node2]
2121 ctx2 = repo[node2]
2121
2122
2122 relfiltered = False
2123 relfiltered = False
2123 if relroot != '' and match.always():
2124 if relroot != '' and match.always():
2124 # as a special case, create a new matcher with just the relroot
2125 # as a special case, create a new matcher with just the relroot
2125 pats = [relroot]
2126 pats = [relroot]
2126 match = scmutil.match(ctx2, pats, default='path')
2127 match = scmutil.match(ctx2, pats, default='path')
2127 relfiltered = True
2128 relfiltered = True
2128
2129
2129 if not changes:
2130 if not changes:
2130 changes = repo.status(ctx1, ctx2, match=match)
2131 changes = repo.status(ctx1, ctx2, match=match)
2131 modified, added, removed = changes[:3]
2132 modified, added, removed = changes[:3]
2132
2133
2133 if not modified and not added and not removed:
2134 if not modified and not added and not removed:
2134 return []
2135 return []
2135
2136
2136 if repo.ui.debugflag:
2137 if repo.ui.debugflag:
2137 hexfunc = hex
2138 hexfunc = hex
2138 else:
2139 else:
2139 hexfunc = short
2140 hexfunc = short
2140 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
2141 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
2141
2142
2142 copy = {}
2143 copy = {}
2143 if opts.git or opts.upgrade:
2144 if opts.git or opts.upgrade:
2144 copy = copies.pathcopies(ctx1, ctx2, match=match)
2145 copy = copies.pathcopies(ctx1, ctx2, match=match)
2145
2146
2146 if relroot is not None:
2147 if relroot is not None:
2147 if not relfiltered:
2148 if not relfiltered:
2148 # XXX this would ideally be done in the matcher, but that is
2149 # XXX this would ideally be done in the matcher, but that is
2149 # generally meant to 'or' patterns, not 'and' them. In this case we
2150 # generally meant to 'or' patterns, not 'and' them. In this case we
2150 # need to 'and' all the patterns from the matcher with relroot.
2151 # need to 'and' all the patterns from the matcher with relroot.
2151 def filterrel(l):
2152 def filterrel(l):
2152 return [f for f in l if f.startswith(relroot)]
2153 return [f for f in l if f.startswith(relroot)]
2153 modified = filterrel(modified)
2154 modified = filterrel(modified)
2154 added = filterrel(added)
2155 added = filterrel(added)
2155 removed = filterrel(removed)
2156 removed = filterrel(removed)
2156 relfiltered = True
2157 relfiltered = True
2157 # filter out copies where either side isn't inside the relative root
2158 # filter out copies where either side isn't inside the relative root
2158 copy = dict(((dst, src) for (dst, src) in copy.iteritems()
2159 copy = dict(((dst, src) for (dst, src) in copy.iteritems()
2159 if dst.startswith(relroot)
2160 if dst.startswith(relroot)
2160 and src.startswith(relroot)))
2161 and src.startswith(relroot)))
2161
2162
2162 def difffn(opts, losedata):
2163 def difffn(opts, losedata):
2163 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2164 return trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2164 copy, getfilectx, opts, losedata, prefix, relroot)
2165 copy, getfilectx, opts, losedata, prefix, relroot)
2165 if opts.upgrade and not opts.git:
2166 if opts.upgrade and not opts.git:
2166 try:
2167 try:
2167 def losedata(fn):
2168 def losedata(fn):
2168 if not losedatafn or not losedatafn(fn=fn):
2169 if not losedatafn or not losedatafn(fn=fn):
2169 raise GitDiffRequired
2170 raise GitDiffRequired
2170 # Buffer the whole output until we are sure it can be generated
2171 # Buffer the whole output until we are sure it can be generated
2171 return list(difffn(opts.copy(git=False), losedata))
2172 return list(difffn(opts.copy(git=False), losedata))
2172 except GitDiffRequired:
2173 except GitDiffRequired:
2173 return difffn(opts.copy(git=True), None)
2174 return difffn(opts.copy(git=True), None)
2174 else:
2175 else:
2175 return difffn(opts, None)
2176 return difffn(opts, None)
2176
2177
2177 def difflabel(func, *args, **kw):
2178 def difflabel(func, *args, **kw):
2178 '''yields 2-tuples of (output, label) based on the output of func()'''
2179 '''yields 2-tuples of (output, label) based on the output of func()'''
2179 headprefixes = [('diff', 'diff.diffline'),
2180 headprefixes = [('diff', 'diff.diffline'),
2180 ('copy', 'diff.extended'),
2181 ('copy', 'diff.extended'),
2181 ('rename', 'diff.extended'),
2182 ('rename', 'diff.extended'),
2182 ('old', 'diff.extended'),
2183 ('old', 'diff.extended'),
2183 ('new', 'diff.extended'),
2184 ('new', 'diff.extended'),
2184 ('deleted', 'diff.extended'),
2185 ('deleted', 'diff.extended'),
2185 ('---', 'diff.file_a'),
2186 ('---', 'diff.file_a'),
2186 ('+++', 'diff.file_b')]
2187 ('+++', 'diff.file_b')]
2187 textprefixes = [('@', 'diff.hunk'),
2188 textprefixes = [('@', 'diff.hunk'),
2188 ('-', 'diff.deleted'),
2189 ('-', 'diff.deleted'),
2189 ('+', 'diff.inserted')]
2190 ('+', 'diff.inserted')]
2190 head = False
2191 head = False
2191 for chunk in func(*args, **kw):
2192 for chunk in func(*args, **kw):
2192 lines = chunk.split('\n')
2193 lines = chunk.split('\n')
2193 for i, line in enumerate(lines):
2194 for i, line in enumerate(lines):
2194 if i != 0:
2195 if i != 0:
2195 yield ('\n', '')
2196 yield ('\n', '')
2196 if head:
2197 if head:
2197 if line.startswith('@'):
2198 if line.startswith('@'):
2198 head = False
2199 head = False
2199 else:
2200 else:
2200 if line and line[0] not in ' +-@\\':
2201 if line and line[0] not in ' +-@\\':
2201 head = True
2202 head = True
2202 stripline = line
2203 stripline = line
2203 diffline = False
2204 diffline = False
2204 if not head and line and line[0] in '+-':
2205 if not head and line and line[0] in '+-':
2205 # highlight tabs and trailing whitespace, but only in
2206 # highlight tabs and trailing whitespace, but only in
2206 # changed lines
2207 # changed lines
2207 stripline = line.rstrip()
2208 stripline = line.rstrip()
2208 diffline = True
2209 diffline = True
2209
2210
2210 prefixes = textprefixes
2211 prefixes = textprefixes
2211 if head:
2212 if head:
2212 prefixes = headprefixes
2213 prefixes = headprefixes
2213 for prefix, label in prefixes:
2214 for prefix, label in prefixes:
2214 if stripline.startswith(prefix):
2215 if stripline.startswith(prefix):
2215 if diffline:
2216 if diffline:
2216 for token in tabsplitter.findall(stripline):
2217 for token in tabsplitter.findall(stripline):
2217 if '\t' == token[0]:
2218 if '\t' == token[0]:
2218 yield (token, 'diff.tab')
2219 yield (token, 'diff.tab')
2219 else:
2220 else:
2220 yield (token, label)
2221 yield (token, label)
2221 else:
2222 else:
2222 yield (stripline, label)
2223 yield (stripline, label)
2223 break
2224 break
2224 else:
2225 else:
2225 yield (line, '')
2226 yield (line, '')
2226 if line != stripline:
2227 if line != stripline:
2227 yield (line[len(stripline):], 'diff.trailingwhitespace')
2228 yield (line[len(stripline):], 'diff.trailingwhitespace')
2228
2229
2229 def diffui(*args, **kw):
2230 def diffui(*args, **kw):
2230 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
2231 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
2231 return difflabel(diff, *args, **kw)
2232 return difflabel(diff, *args, **kw)
2232
2233
2233 def _filepairs(ctx1, modified, added, removed, copy, opts):
2234 def _filepairs(ctx1, modified, added, removed, copy, opts):
2234 '''generates tuples (f1, f2, copyop), where f1 is the name of the file
2235 '''generates tuples (f1, f2, copyop), where f1 is the name of the file
2235 before and f2 is the the name after. For added files, f1 will be None,
2236 before and f2 is the the name after. For added files, f1 will be None,
2236 and for removed files, f2 will be None. copyop may be set to None, 'copy'
2237 and for removed files, f2 will be None. copyop may be set to None, 'copy'
2237 or 'rename' (the latter two only if opts.git is set).'''
2238 or 'rename' (the latter two only if opts.git is set).'''
2238 gone = set()
2239 gone = set()
2239
2240
2240 copyto = dict([(v, k) for k, v in copy.items()])
2241 copyto = dict([(v, k) for k, v in copy.items()])
2241
2242
2242 addedset, removedset = set(added), set(removed)
2243 addedset, removedset = set(added), set(removed)
2243 # Fix up added, since merged-in additions appear as
2244 # Fix up added, since merged-in additions appear as
2244 # modifications during merges
2245 # modifications during merges
2245 for f in modified:
2246 for f in modified:
2246 if f not in ctx1:
2247 if f not in ctx1:
2247 addedset.add(f)
2248 addedset.add(f)
2248
2249
2249 for f in sorted(modified + added + removed):
2250 for f in sorted(modified + added + removed):
2250 copyop = None
2251 copyop = None
2251 f1, f2 = f, f
2252 f1, f2 = f, f
2252 if f in addedset:
2253 if f in addedset:
2253 f1 = None
2254 f1 = None
2254 if f in copy:
2255 if f in copy:
2255 if opts.git:
2256 if opts.git:
2256 f1 = copy[f]
2257 f1 = copy[f]
2257 if f1 in removedset and f1 not in gone:
2258 if f1 in removedset and f1 not in gone:
2258 copyop = 'rename'
2259 copyop = 'rename'
2259 gone.add(f1)
2260 gone.add(f1)
2260 else:
2261 else:
2261 copyop = 'copy'
2262 copyop = 'copy'
2262 elif f in removedset:
2263 elif f in removedset:
2263 f2 = None
2264 f2 = None
2264 if opts.git:
2265 if opts.git:
2265 # have we already reported a copy above?
2266 # have we already reported a copy above?
2266 if (f in copyto and copyto[f] in addedset
2267 if (f in copyto and copyto[f] in addedset
2267 and copy[copyto[f]] == f):
2268 and copy[copyto[f]] == f):
2268 continue
2269 continue
2269 yield f1, f2, copyop
2270 yield f1, f2, copyop
2270
2271
2271 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2272 def trydiff(repo, revs, ctx1, ctx2, modified, added, removed,
2272 copy, getfilectx, opts, losedatafn, prefix, relroot):
2273 copy, getfilectx, opts, losedatafn, prefix, relroot):
2273 '''given input data, generate a diff and yield it in blocks
2274 '''given input data, generate a diff and yield it in blocks
2274
2275
2275 If generating a diff would lose data like flags or binary data and
2276 If generating a diff would lose data like flags or binary data and
2276 losedatafn is not None, it will be called.
2277 losedatafn is not None, it will be called.
2277
2278
2278 relroot is removed and prefix is added to every path in the diff output.
2279 relroot is removed and prefix is added to every path in the diff output.
2279
2280
2280 If relroot is not empty, this function expects every path in modified,
2281 If relroot is not empty, this function expects every path in modified,
2281 added, removed and copy to start with it.'''
2282 added, removed and copy to start with it.'''
2282
2283
2283 def gitindex(text):
2284 def gitindex(text):
2284 if not text:
2285 if not text:
2285 text = ""
2286 text = ""
2286 l = len(text)
2287 l = len(text)
2287 s = util.sha1('blob %d\0' % l)
2288 s = util.sha1('blob %d\0' % l)
2288 s.update(text)
2289 s.update(text)
2289 return s.hexdigest()
2290 return s.hexdigest()
2290
2291
2291 if opts.noprefix:
2292 if opts.noprefix:
2292 aprefix = bprefix = ''
2293 aprefix = bprefix = ''
2293 else:
2294 else:
2294 aprefix = 'a/'
2295 aprefix = 'a/'
2295 bprefix = 'b/'
2296 bprefix = 'b/'
2296
2297
2297 def diffline(f, revs):
2298 def diffline(f, revs):
2298 revinfo = ' '.join(["-r %s" % rev for rev in revs])
2299 revinfo = ' '.join(["-r %s" % rev for rev in revs])
2299 return 'diff %s %s' % (revinfo, f)
2300 return 'diff %s %s' % (revinfo, f)
2300
2301
2301 date1 = util.datestr(ctx1.date())
2302 date1 = util.datestr(ctx1.date())
2302 date2 = util.datestr(ctx2.date())
2303 date2 = util.datestr(ctx2.date())
2303
2304
2304 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
2305 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
2305
2306
2306 if relroot != '' and (repo.ui.configbool('devel', 'all')
2307 if relroot != '' and (repo.ui.configbool('devel', 'all')
2307 or repo.ui.configbool('devel', 'check-relroot')):
2308 or repo.ui.configbool('devel', 'check-relroot')):
2308 for f in modified + added + removed + copy.keys() + copy.values():
2309 for f in modified + added + removed + copy.keys() + copy.values():
2309 if f is not None and not f.startswith(relroot):
2310 if f is not None and not f.startswith(relroot):
2310 raise AssertionError(
2311 raise AssertionError(
2311 "file %s doesn't start with relroot %s" % (f, relroot))
2312 "file %s doesn't start with relroot %s" % (f, relroot))
2312
2313
2313 for f1, f2, copyop in _filepairs(
2314 for f1, f2, copyop in _filepairs(
2314 ctx1, modified, added, removed, copy, opts):
2315 ctx1, modified, added, removed, copy, opts):
2315 content1 = None
2316 content1 = None
2316 content2 = None
2317 content2 = None
2317 flag1 = None
2318 flag1 = None
2318 flag2 = None
2319 flag2 = None
2319 if f1:
2320 if f1:
2320 content1 = getfilectx(f1, ctx1).data()
2321 content1 = getfilectx(f1, ctx1).data()
2321 if opts.git or losedatafn:
2322 if opts.git or losedatafn:
2322 flag1 = ctx1.flags(f1)
2323 flag1 = ctx1.flags(f1)
2323 if f2:
2324 if f2:
2324 content2 = getfilectx(f2, ctx2).data()
2325 content2 = getfilectx(f2, ctx2).data()
2325 if opts.git or losedatafn:
2326 if opts.git or losedatafn:
2326 flag2 = ctx2.flags(f2)
2327 flag2 = ctx2.flags(f2)
2327 binary = False
2328 binary = False
2328 if opts.git or losedatafn:
2329 if opts.git or losedatafn:
2329 binary = util.binary(content1) or util.binary(content2)
2330 binary = util.binary(content1) or util.binary(content2)
2330
2331
2331 if losedatafn and not opts.git:
2332 if losedatafn and not opts.git:
2332 if (binary or
2333 if (binary or
2333 # copy/rename
2334 # copy/rename
2334 f2 in copy or
2335 f2 in copy or
2335 # empty file creation
2336 # empty file creation
2336 (not f1 and not content2) or
2337 (not f1 and not content2) or
2337 # empty file deletion
2338 # empty file deletion
2338 (not content1 and not f2) or
2339 (not content1 and not f2) or
2339 # create with flags
2340 # create with flags
2340 (not f1 and flag2) or
2341 (not f1 and flag2) or
2341 # change flags
2342 # change flags
2342 (f1 and f2 and flag1 != flag2)):
2343 (f1 and f2 and flag1 != flag2)):
2343 losedatafn(f2 or f1)
2344 losedatafn(f2 or f1)
2344
2345
2345 path1 = f1 or f2
2346 path1 = f1 or f2
2346 path2 = f2 or f1
2347 path2 = f2 or f1
2347 path1 = posixpath.join(prefix, path1[len(relroot):])
2348 path1 = posixpath.join(prefix, path1[len(relroot):])
2348 path2 = posixpath.join(prefix, path2[len(relroot):])
2349 path2 = posixpath.join(prefix, path2[len(relroot):])
2349 header = []
2350 header = []
2350 if opts.git:
2351 if opts.git:
2351 header.append('diff --git %s%s %s%s' %
2352 header.append('diff --git %s%s %s%s' %
2352 (aprefix, path1, bprefix, path2))
2353 (aprefix, path1, bprefix, path2))
2353 if not f1: # added
2354 if not f1: # added
2354 header.append('new file mode %s' % gitmode[flag2])
2355 header.append('new file mode %s' % gitmode[flag2])
2355 elif not f2: # removed
2356 elif not f2: # removed
2356 header.append('deleted file mode %s' % gitmode[flag1])
2357 header.append('deleted file mode %s' % gitmode[flag1])
2357 else: # modified/copied/renamed
2358 else: # modified/copied/renamed
2358 mode1, mode2 = gitmode[flag1], gitmode[flag2]
2359 mode1, mode2 = gitmode[flag1], gitmode[flag2]
2359 if mode1 != mode2:
2360 if mode1 != mode2:
2360 header.append('old mode %s' % mode1)
2361 header.append('old mode %s' % mode1)
2361 header.append('new mode %s' % mode2)
2362 header.append('new mode %s' % mode2)
2362 if copyop is not None:
2363 if copyop is not None:
2363 header.append('%s from %s' % (copyop, path1))
2364 header.append('%s from %s' % (copyop, path1))
2364 header.append('%s to %s' % (copyop, path2))
2365 header.append('%s to %s' % (copyop, path2))
2365 elif revs and not repo.ui.quiet:
2366 elif revs and not repo.ui.quiet:
2366 header.append(diffline(path1, revs))
2367 header.append(diffline(path1, revs))
2367
2368
2368 if binary and opts.git and not opts.nobinary:
2369 if binary and opts.git and not opts.nobinary:
2369 text = mdiff.b85diff(content1, content2)
2370 text = mdiff.b85diff(content1, content2)
2370 if text:
2371 if text:
2371 header.append('index %s..%s' %
2372 header.append('index %s..%s' %
2372 (gitindex(content1), gitindex(content2)))
2373 (gitindex(content1), gitindex(content2)))
2373 else:
2374 else:
2374 text = mdiff.unidiff(content1, date1,
2375 text = mdiff.unidiff(content1, date1,
2375 content2, date2,
2376 content2, date2,
2376 path1, path2, opts=opts)
2377 path1, path2, opts=opts)
2377 if header and (text or len(header) > 1):
2378 if header and (text or len(header) > 1):
2378 yield '\n'.join(header) + '\n'
2379 yield '\n'.join(header) + '\n'
2379 if text:
2380 if text:
2380 yield text
2381 yield text
2381
2382
2382 def diffstatsum(stats):
2383 def diffstatsum(stats):
2383 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
2384 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
2384 for f, a, r, b in stats:
2385 for f, a, r, b in stats:
2385 maxfile = max(maxfile, encoding.colwidth(f))
2386 maxfile = max(maxfile, encoding.colwidth(f))
2386 maxtotal = max(maxtotal, a + r)
2387 maxtotal = max(maxtotal, a + r)
2387 addtotal += a
2388 addtotal += a
2388 removetotal += r
2389 removetotal += r
2389 binary = binary or b
2390 binary = binary or b
2390
2391
2391 return maxfile, maxtotal, addtotal, removetotal, binary
2392 return maxfile, maxtotal, addtotal, removetotal, binary
2392
2393
2393 def diffstatdata(lines):
2394 def diffstatdata(lines):
2394 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
2395 diffre = re.compile('^diff .*-r [a-z0-9]+\s(.*)$')
2395
2396
2396 results = []
2397 results = []
2397 filename, adds, removes, isbinary = None, 0, 0, False
2398 filename, adds, removes, isbinary = None, 0, 0, False
2398
2399
2399 def addresult():
2400 def addresult():
2400 if filename:
2401 if filename:
2401 results.append((filename, adds, removes, isbinary))
2402 results.append((filename, adds, removes, isbinary))
2402
2403
2403 for line in lines:
2404 for line in lines:
2404 if line.startswith('diff'):
2405 if line.startswith('diff'):
2405 addresult()
2406 addresult()
2406 # set numbers to 0 anyway when starting new file
2407 # set numbers to 0 anyway when starting new file
2407 adds, removes, isbinary = 0, 0, False
2408 adds, removes, isbinary = 0, 0, False
2408 if line.startswith('diff --git a/'):
2409 if line.startswith('diff --git a/'):
2409 filename = gitre.search(line).group(2)
2410 filename = gitre.search(line).group(2)
2410 elif line.startswith('diff -r'):
2411 elif line.startswith('diff -r'):
2411 # format: "diff -r ... -r ... filename"
2412 # format: "diff -r ... -r ... filename"
2412 filename = diffre.search(line).group(1)
2413 filename = diffre.search(line).group(1)
2413 elif line.startswith('+') and not line.startswith('+++ '):
2414 elif line.startswith('+') and not line.startswith('+++ '):
2414 adds += 1
2415 adds += 1
2415 elif line.startswith('-') and not line.startswith('--- '):
2416 elif line.startswith('-') and not line.startswith('--- '):
2416 removes += 1
2417 removes += 1
2417 elif (line.startswith('GIT binary patch') or
2418 elif (line.startswith('GIT binary patch') or
2418 line.startswith('Binary file')):
2419 line.startswith('Binary file')):
2419 isbinary = True
2420 isbinary = True
2420 addresult()
2421 addresult()
2421 return results
2422 return results
2422
2423
2423 def diffstat(lines, width=80, git=False):
2424 def diffstat(lines, width=80, git=False):
2424 output = []
2425 output = []
2425 stats = diffstatdata(lines)
2426 stats = diffstatdata(lines)
2426 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
2427 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
2427
2428
2428 countwidth = len(str(maxtotal))
2429 countwidth = len(str(maxtotal))
2429 if hasbinary and countwidth < 3:
2430 if hasbinary and countwidth < 3:
2430 countwidth = 3
2431 countwidth = 3
2431 graphwidth = width - countwidth - maxname - 6
2432 graphwidth = width - countwidth - maxname - 6
2432 if graphwidth < 10:
2433 if graphwidth < 10:
2433 graphwidth = 10
2434 graphwidth = 10
2434
2435
2435 def scale(i):
2436 def scale(i):
2436 if maxtotal <= graphwidth:
2437 if maxtotal <= graphwidth:
2437 return i
2438 return i
2438 # If diffstat runs out of room it doesn't print anything,
2439 # If diffstat runs out of room it doesn't print anything,
2439 # which isn't very useful, so always print at least one + or -
2440 # which isn't very useful, so always print at least one + or -
2440 # if there were at least some changes.
2441 # if there were at least some changes.
2441 return max(i * graphwidth // maxtotal, int(bool(i)))
2442 return max(i * graphwidth // maxtotal, int(bool(i)))
2442
2443
2443 for filename, adds, removes, isbinary in stats:
2444 for filename, adds, removes, isbinary in stats:
2444 if isbinary:
2445 if isbinary:
2445 count = 'Bin'
2446 count = 'Bin'
2446 else:
2447 else:
2447 count = adds + removes
2448 count = adds + removes
2448 pluses = '+' * scale(adds)
2449 pluses = '+' * scale(adds)
2449 minuses = '-' * scale(removes)
2450 minuses = '-' * scale(removes)
2450 output.append(' %s%s | %*s %s%s\n' %
2451 output.append(' %s%s | %*s %s%s\n' %
2451 (filename, ' ' * (maxname - encoding.colwidth(filename)),
2452 (filename, ' ' * (maxname - encoding.colwidth(filename)),
2452 countwidth, count, pluses, minuses))
2453 countwidth, count, pluses, minuses))
2453
2454
2454 if stats:
2455 if stats:
2455 output.append(_(' %d files changed, %d insertions(+), '
2456 output.append(_(' %d files changed, %d insertions(+), '
2456 '%d deletions(-)\n')
2457 '%d deletions(-)\n')
2457 % (len(stats), totaladds, totalremoves))
2458 % (len(stats), totaladds, totalremoves))
2458
2459
2459 return ''.join(output)
2460 return ''.join(output)
2460
2461
2461 def diffstatui(*args, **kw):
2462 def diffstatui(*args, **kw):
2462 '''like diffstat(), but yields 2-tuples of (output, label) for
2463 '''like diffstat(), but yields 2-tuples of (output, label) for
2463 ui.write()
2464 ui.write()
2464 '''
2465 '''
2465
2466
2466 for line in diffstat(*args, **kw).splitlines():
2467 for line in diffstat(*args, **kw).splitlines():
2467 if line and line[-1] in '+-':
2468 if line and line[-1] in '+-':
2468 name, graph = line.rsplit(' ', 1)
2469 name, graph = line.rsplit(' ', 1)
2469 yield (name + ' ', '')
2470 yield (name + ' ', '')
2470 m = re.search(r'\++', graph)
2471 m = re.search(r'\++', graph)
2471 if m:
2472 if m:
2472 yield (m.group(0), 'diffstat.inserted')
2473 yield (m.group(0), 'diffstat.inserted')
2473 m = re.search(r'-+', graph)
2474 m = re.search(r'-+', graph)
2474 if m:
2475 if m:
2475 yield (m.group(0), 'diffstat.deleted')
2476 yield (m.group(0), 'diffstat.deleted')
2476 else:
2477 else:
2477 yield (line, '')
2478 yield (line, '')
2478 yield ('\n', '')
2479 yield ('\n', '')
@@ -1,1593 +1,1594 b''
1 # revlog.py - storage back-end for mercurial
1 # revlog.py - storage back-end for mercurial
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 of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """Storage back-end for Mercurial.
8 """Storage back-end for Mercurial.
9
9
10 This provides efficient delta storage with O(1) retrieve and append
10 This provides efficient delta storage with O(1) retrieve and append
11 and O(changes) merge between branches.
11 and O(changes) merge between branches.
12 """
12 """
13
13
14 # import stuff from node for others to import from revlog
14 # import stuff from node for others to import from revlog
15 import collections
15 from node import bin, hex, nullid, nullrev
16 from node import bin, hex, nullid, nullrev
16 from i18n import _
17 from i18n import _
17 import ancestor, mdiff, parsers, error, util, templatefilters
18 import ancestor, mdiff, parsers, error, util, templatefilters
18 import struct, zlib, errno
19 import struct, zlib, errno
19
20
20 _pack = struct.pack
21 _pack = struct.pack
21 _unpack = struct.unpack
22 _unpack = struct.unpack
22 _compress = zlib.compress
23 _compress = zlib.compress
23 _decompress = zlib.decompress
24 _decompress = zlib.decompress
24 _sha = util.sha1
25 _sha = util.sha1
25
26
26 # revlog header flags
27 # revlog header flags
27 REVLOGV0 = 0
28 REVLOGV0 = 0
28 REVLOGNG = 1
29 REVLOGNG = 1
29 REVLOGNGINLINEDATA = (1 << 16)
30 REVLOGNGINLINEDATA = (1 << 16)
30 REVLOGGENERALDELTA = (1 << 17)
31 REVLOGGENERALDELTA = (1 << 17)
31 REVLOG_DEFAULT_FLAGS = REVLOGNGINLINEDATA
32 REVLOG_DEFAULT_FLAGS = REVLOGNGINLINEDATA
32 REVLOG_DEFAULT_FORMAT = REVLOGNG
33 REVLOG_DEFAULT_FORMAT = REVLOGNG
33 REVLOG_DEFAULT_VERSION = REVLOG_DEFAULT_FORMAT | REVLOG_DEFAULT_FLAGS
34 REVLOG_DEFAULT_VERSION = REVLOG_DEFAULT_FORMAT | REVLOG_DEFAULT_FLAGS
34 REVLOGNG_FLAGS = REVLOGNGINLINEDATA | REVLOGGENERALDELTA
35 REVLOGNG_FLAGS = REVLOGNGINLINEDATA | REVLOGGENERALDELTA
35
36
36 # revlog index flags
37 # revlog index flags
37 REVIDX_ISCENSORED = (1 << 15) # revision has censor metadata, must be verified
38 REVIDX_ISCENSORED = (1 << 15) # revision has censor metadata, must be verified
38 REVIDX_DEFAULT_FLAGS = 0
39 REVIDX_DEFAULT_FLAGS = 0
39 REVIDX_KNOWN_FLAGS = REVIDX_ISCENSORED
40 REVIDX_KNOWN_FLAGS = REVIDX_ISCENSORED
40
41
41 # max size of revlog with inline data
42 # max size of revlog with inline data
42 _maxinline = 131072
43 _maxinline = 131072
43 _chunksize = 1048576
44 _chunksize = 1048576
44
45
45 RevlogError = error.RevlogError
46 RevlogError = error.RevlogError
46 LookupError = error.LookupError
47 LookupError = error.LookupError
47 CensoredNodeError = error.CensoredNodeError
48 CensoredNodeError = error.CensoredNodeError
48
49
49 def getoffset(q):
50 def getoffset(q):
50 return int(q >> 16)
51 return int(q >> 16)
51
52
52 def gettype(q):
53 def gettype(q):
53 return int(q & 0xFFFF)
54 return int(q & 0xFFFF)
54
55
55 def offset_type(offset, type):
56 def offset_type(offset, type):
56 return long(long(offset) << 16 | type)
57 return long(long(offset) << 16 | type)
57
58
58 _nullhash = _sha(nullid)
59 _nullhash = _sha(nullid)
59
60
60 def hash(text, p1, p2):
61 def hash(text, p1, p2):
61 """generate a hash from the given text and its parent hashes
62 """generate a hash from the given text and its parent hashes
62
63
63 This hash combines both the current file contents and its history
64 This hash combines both the current file contents and its history
64 in a manner that makes it easy to distinguish nodes with the same
65 in a manner that makes it easy to distinguish nodes with the same
65 content in the revision graph.
66 content in the revision graph.
66 """
67 """
67 # As of now, if one of the parent node is null, p2 is null
68 # As of now, if one of the parent node is null, p2 is null
68 if p2 == nullid:
69 if p2 == nullid:
69 # deep copy of a hash is faster than creating one
70 # deep copy of a hash is faster than creating one
70 s = _nullhash.copy()
71 s = _nullhash.copy()
71 s.update(p1)
72 s.update(p1)
72 else:
73 else:
73 # none of the parent nodes are nullid
74 # none of the parent nodes are nullid
74 l = [p1, p2]
75 l = [p1, p2]
75 l.sort()
76 l.sort()
76 s = _sha(l[0])
77 s = _sha(l[0])
77 s.update(l[1])
78 s.update(l[1])
78 s.update(text)
79 s.update(text)
79 return s.digest()
80 return s.digest()
80
81
81 def decompress(bin):
82 def decompress(bin):
82 """ decompress the given input """
83 """ decompress the given input """
83 if not bin:
84 if not bin:
84 return bin
85 return bin
85 t = bin[0]
86 t = bin[0]
86 if t == '\0':
87 if t == '\0':
87 return bin
88 return bin
88 if t == 'x':
89 if t == 'x':
89 try:
90 try:
90 return _decompress(bin)
91 return _decompress(bin)
91 except zlib.error, e:
92 except zlib.error, e:
92 raise RevlogError(_("revlog decompress error: %s") % str(e))
93 raise RevlogError(_("revlog decompress error: %s") % str(e))
93 if t == 'u':
94 if t == 'u':
94 return bin[1:]
95 return bin[1:]
95 raise RevlogError(_("unknown compression type %r") % t)
96 raise RevlogError(_("unknown compression type %r") % t)
96
97
97 # index v0:
98 # index v0:
98 # 4 bytes: offset
99 # 4 bytes: offset
99 # 4 bytes: compressed length
100 # 4 bytes: compressed length
100 # 4 bytes: base rev
101 # 4 bytes: base rev
101 # 4 bytes: link rev
102 # 4 bytes: link rev
102 # 32 bytes: parent 1 nodeid
103 # 32 bytes: parent 1 nodeid
103 # 32 bytes: parent 2 nodeid
104 # 32 bytes: parent 2 nodeid
104 # 32 bytes: nodeid
105 # 32 bytes: nodeid
105 indexformatv0 = ">4l20s20s20s"
106 indexformatv0 = ">4l20s20s20s"
106 v0shaoffset = 56
107 v0shaoffset = 56
107
108
108 class revlogoldio(object):
109 class revlogoldio(object):
109 def __init__(self):
110 def __init__(self):
110 self.size = struct.calcsize(indexformatv0)
111 self.size = struct.calcsize(indexformatv0)
111
112
112 def parseindex(self, data, inline):
113 def parseindex(self, data, inline):
113 s = self.size
114 s = self.size
114 index = []
115 index = []
115 nodemap = {nullid: nullrev}
116 nodemap = {nullid: nullrev}
116 n = off = 0
117 n = off = 0
117 l = len(data)
118 l = len(data)
118 while off + s <= l:
119 while off + s <= l:
119 cur = data[off:off + s]
120 cur = data[off:off + s]
120 off += s
121 off += s
121 e = _unpack(indexformatv0, cur)
122 e = _unpack(indexformatv0, cur)
122 # transform to revlogv1 format
123 # transform to revlogv1 format
123 e2 = (offset_type(e[0], 0), e[1], -1, e[2], e[3],
124 e2 = (offset_type(e[0], 0), e[1], -1, e[2], e[3],
124 nodemap.get(e[4], nullrev), nodemap.get(e[5], nullrev), e[6])
125 nodemap.get(e[4], nullrev), nodemap.get(e[5], nullrev), e[6])
125 index.append(e2)
126 index.append(e2)
126 nodemap[e[6]] = n
127 nodemap[e[6]] = n
127 n += 1
128 n += 1
128
129
129 # add the magic null revision at -1
130 # add the magic null revision at -1
130 index.append((0, 0, 0, -1, -1, -1, -1, nullid))
131 index.append((0, 0, 0, -1, -1, -1, -1, nullid))
131
132
132 return index, nodemap, None
133 return index, nodemap, None
133
134
134 def packentry(self, entry, node, version, rev):
135 def packentry(self, entry, node, version, rev):
135 if gettype(entry[0]):
136 if gettype(entry[0]):
136 raise RevlogError(_("index entry flags need RevlogNG"))
137 raise RevlogError(_("index entry flags need RevlogNG"))
137 e2 = (getoffset(entry[0]), entry[1], entry[3], entry[4],
138 e2 = (getoffset(entry[0]), entry[1], entry[3], entry[4],
138 node(entry[5]), node(entry[6]), entry[7])
139 node(entry[5]), node(entry[6]), entry[7])
139 return _pack(indexformatv0, *e2)
140 return _pack(indexformatv0, *e2)
140
141
141 # index ng:
142 # index ng:
142 # 6 bytes: offset
143 # 6 bytes: offset
143 # 2 bytes: flags
144 # 2 bytes: flags
144 # 4 bytes: compressed length
145 # 4 bytes: compressed length
145 # 4 bytes: uncompressed length
146 # 4 bytes: uncompressed length
146 # 4 bytes: base rev
147 # 4 bytes: base rev
147 # 4 bytes: link rev
148 # 4 bytes: link rev
148 # 4 bytes: parent 1 rev
149 # 4 bytes: parent 1 rev
149 # 4 bytes: parent 2 rev
150 # 4 bytes: parent 2 rev
150 # 32 bytes: nodeid
151 # 32 bytes: nodeid
151 indexformatng = ">Qiiiiii20s12x"
152 indexformatng = ">Qiiiiii20s12x"
152 ngshaoffset = 32
153 ngshaoffset = 32
153 versionformat = ">I"
154 versionformat = ">I"
154
155
155 class revlogio(object):
156 class revlogio(object):
156 def __init__(self):
157 def __init__(self):
157 self.size = struct.calcsize(indexformatng)
158 self.size = struct.calcsize(indexformatng)
158
159
159 def parseindex(self, data, inline):
160 def parseindex(self, data, inline):
160 # call the C implementation to parse the index data
161 # call the C implementation to parse the index data
161 index, cache = parsers.parse_index2(data, inline)
162 index, cache = parsers.parse_index2(data, inline)
162 return index, getattr(index, 'nodemap', None), cache
163 return index, getattr(index, 'nodemap', None), cache
163
164
164 def packentry(self, entry, node, version, rev):
165 def packentry(self, entry, node, version, rev):
165 p = _pack(indexformatng, *entry)
166 p = _pack(indexformatng, *entry)
166 if rev == 0:
167 if rev == 0:
167 p = _pack(versionformat, version) + p[4:]
168 p = _pack(versionformat, version) + p[4:]
168 return p
169 return p
169
170
170 class revlog(object):
171 class revlog(object):
171 """
172 """
172 the underlying revision storage object
173 the underlying revision storage object
173
174
174 A revlog consists of two parts, an index and the revision data.
175 A revlog consists of two parts, an index and the revision data.
175
176
176 The index is a file with a fixed record size containing
177 The index is a file with a fixed record size containing
177 information on each revision, including its nodeid (hash), the
178 information on each revision, including its nodeid (hash), the
178 nodeids of its parents, the position and offset of its data within
179 nodeids of its parents, the position and offset of its data within
179 the data file, and the revision it's based on. Finally, each entry
180 the data file, and the revision it's based on. Finally, each entry
180 contains a linkrev entry that can serve as a pointer to external
181 contains a linkrev entry that can serve as a pointer to external
181 data.
182 data.
182
183
183 The revision data itself is a linear collection of data chunks.
184 The revision data itself is a linear collection of data chunks.
184 Each chunk represents a revision and is usually represented as a
185 Each chunk represents a revision and is usually represented as a
185 delta against the previous chunk. To bound lookup time, runs of
186 delta against the previous chunk. To bound lookup time, runs of
186 deltas are limited to about 2 times the length of the original
187 deltas are limited to about 2 times the length of the original
187 version data. This makes retrieval of a version proportional to
188 version data. This makes retrieval of a version proportional to
188 its size, or O(1) relative to the number of revisions.
189 its size, or O(1) relative to the number of revisions.
189
190
190 Both pieces of the revlog are written to in an append-only
191 Both pieces of the revlog are written to in an append-only
191 fashion, which means we never need to rewrite a file to insert or
192 fashion, which means we never need to rewrite a file to insert or
192 remove data, and can use some simple techniques to avoid the need
193 remove data, and can use some simple techniques to avoid the need
193 for locking while reading.
194 for locking while reading.
194 """
195 """
195 def __init__(self, opener, indexfile):
196 def __init__(self, opener, indexfile):
196 """
197 """
197 create a revlog object
198 create a revlog object
198
199
199 opener is a function that abstracts the file opening operation
200 opener is a function that abstracts the file opening operation
200 and can be used to implement COW semantics or the like.
201 and can be used to implement COW semantics or the like.
201 """
202 """
202 self.indexfile = indexfile
203 self.indexfile = indexfile
203 self.datafile = indexfile[:-2] + ".d"
204 self.datafile = indexfile[:-2] + ".d"
204 self.opener = opener
205 self.opener = opener
205 self._cache = None
206 self._cache = None
206 self._basecache = None
207 self._basecache = None
207 self._chunkcache = (0, '')
208 self._chunkcache = (0, '')
208 self._chunkcachesize = 65536
209 self._chunkcachesize = 65536
209 self._maxchainlen = None
210 self._maxchainlen = None
210 self.index = []
211 self.index = []
211 self._pcache = {}
212 self._pcache = {}
212 self._nodecache = {nullid: nullrev}
213 self._nodecache = {nullid: nullrev}
213 self._nodepos = None
214 self._nodepos = None
214
215
215 v = REVLOG_DEFAULT_VERSION
216 v = REVLOG_DEFAULT_VERSION
216 opts = getattr(opener, 'options', None)
217 opts = getattr(opener, 'options', None)
217 if opts is not None:
218 if opts is not None:
218 if 'revlogv1' in opts:
219 if 'revlogv1' in opts:
219 if 'generaldelta' in opts:
220 if 'generaldelta' in opts:
220 v |= REVLOGGENERALDELTA
221 v |= REVLOGGENERALDELTA
221 else:
222 else:
222 v = 0
223 v = 0
223 if 'chunkcachesize' in opts:
224 if 'chunkcachesize' in opts:
224 self._chunkcachesize = opts['chunkcachesize']
225 self._chunkcachesize = opts['chunkcachesize']
225 if 'maxchainlen' in opts:
226 if 'maxchainlen' in opts:
226 self._maxchainlen = opts['maxchainlen']
227 self._maxchainlen = opts['maxchainlen']
227
228
228 if self._chunkcachesize <= 0:
229 if self._chunkcachesize <= 0:
229 raise RevlogError(_('revlog chunk cache size %r is not greater '
230 raise RevlogError(_('revlog chunk cache size %r is not greater '
230 'than 0') % self._chunkcachesize)
231 'than 0') % self._chunkcachesize)
231 elif self._chunkcachesize & (self._chunkcachesize - 1):
232 elif self._chunkcachesize & (self._chunkcachesize - 1):
232 raise RevlogError(_('revlog chunk cache size %r is not a power '
233 raise RevlogError(_('revlog chunk cache size %r is not a power '
233 'of 2') % self._chunkcachesize)
234 'of 2') % self._chunkcachesize)
234
235
235 i = ''
236 i = ''
236 self._initempty = True
237 self._initempty = True
237 try:
238 try:
238 f = self.opener(self.indexfile)
239 f = self.opener(self.indexfile)
239 i = f.read()
240 i = f.read()
240 f.close()
241 f.close()
241 if len(i) > 0:
242 if len(i) > 0:
242 v = struct.unpack(versionformat, i[:4])[0]
243 v = struct.unpack(versionformat, i[:4])[0]
243 self._initempty = False
244 self._initempty = False
244 except IOError, inst:
245 except IOError, inst:
245 if inst.errno != errno.ENOENT:
246 if inst.errno != errno.ENOENT:
246 raise
247 raise
247
248
248 self.version = v
249 self.version = v
249 self._inline = v & REVLOGNGINLINEDATA
250 self._inline = v & REVLOGNGINLINEDATA
250 self._generaldelta = v & REVLOGGENERALDELTA
251 self._generaldelta = v & REVLOGGENERALDELTA
251 flags = v & ~0xFFFF
252 flags = v & ~0xFFFF
252 fmt = v & 0xFFFF
253 fmt = v & 0xFFFF
253 if fmt == REVLOGV0 and flags:
254 if fmt == REVLOGV0 and flags:
254 raise RevlogError(_("index %s unknown flags %#04x for format v0")
255 raise RevlogError(_("index %s unknown flags %#04x for format v0")
255 % (self.indexfile, flags >> 16))
256 % (self.indexfile, flags >> 16))
256 elif fmt == REVLOGNG and flags & ~REVLOGNG_FLAGS:
257 elif fmt == REVLOGNG and flags & ~REVLOGNG_FLAGS:
257 raise RevlogError(_("index %s unknown flags %#04x for revlogng")
258 raise RevlogError(_("index %s unknown flags %#04x for revlogng")
258 % (self.indexfile, flags >> 16))
259 % (self.indexfile, flags >> 16))
259 elif fmt > REVLOGNG:
260 elif fmt > REVLOGNG:
260 raise RevlogError(_("index %s unknown format %d")
261 raise RevlogError(_("index %s unknown format %d")
261 % (self.indexfile, fmt))
262 % (self.indexfile, fmt))
262
263
263 self._io = revlogio()
264 self._io = revlogio()
264 if self.version == REVLOGV0:
265 if self.version == REVLOGV0:
265 self._io = revlogoldio()
266 self._io = revlogoldio()
266 try:
267 try:
267 d = self._io.parseindex(i, self._inline)
268 d = self._io.parseindex(i, self._inline)
268 except (ValueError, IndexError):
269 except (ValueError, IndexError):
269 raise RevlogError(_("index %s is corrupted") % (self.indexfile))
270 raise RevlogError(_("index %s is corrupted") % (self.indexfile))
270 self.index, nodemap, self._chunkcache = d
271 self.index, nodemap, self._chunkcache = d
271 if nodemap is not None:
272 if nodemap is not None:
272 self.nodemap = self._nodecache = nodemap
273 self.nodemap = self._nodecache = nodemap
273 if not self._chunkcache:
274 if not self._chunkcache:
274 self._chunkclear()
275 self._chunkclear()
275 # revnum -> (chain-length, sum-delta-length)
276 # revnum -> (chain-length, sum-delta-length)
276 self._chaininfocache = {}
277 self._chaininfocache = {}
277
278
278 def tip(self):
279 def tip(self):
279 return self.node(len(self.index) - 2)
280 return self.node(len(self.index) - 2)
280 def __contains__(self, rev):
281 def __contains__(self, rev):
281 return 0 <= rev < len(self)
282 return 0 <= rev < len(self)
282 def __len__(self):
283 def __len__(self):
283 return len(self.index) - 1
284 return len(self.index) - 1
284 def __iter__(self):
285 def __iter__(self):
285 return iter(xrange(len(self)))
286 return iter(xrange(len(self)))
286 def revs(self, start=0, stop=None):
287 def revs(self, start=0, stop=None):
287 """iterate over all rev in this revlog (from start to stop)"""
288 """iterate over all rev in this revlog (from start to stop)"""
288 step = 1
289 step = 1
289 if stop is not None:
290 if stop is not None:
290 if start > stop:
291 if start > stop:
291 step = -1
292 step = -1
292 stop += step
293 stop += step
293 else:
294 else:
294 stop = len(self)
295 stop = len(self)
295 return xrange(start, stop, step)
296 return xrange(start, stop, step)
296
297
297 @util.propertycache
298 @util.propertycache
298 def nodemap(self):
299 def nodemap(self):
299 self.rev(self.node(0))
300 self.rev(self.node(0))
300 return self._nodecache
301 return self._nodecache
301
302
302 def hasnode(self, node):
303 def hasnode(self, node):
303 try:
304 try:
304 self.rev(node)
305 self.rev(node)
305 return True
306 return True
306 except KeyError:
307 except KeyError:
307 return False
308 return False
308
309
309 def clearcaches(self):
310 def clearcaches(self):
310 try:
311 try:
311 self._nodecache.clearcaches()
312 self._nodecache.clearcaches()
312 except AttributeError:
313 except AttributeError:
313 self._nodecache = {nullid: nullrev}
314 self._nodecache = {nullid: nullrev}
314 self._nodepos = None
315 self._nodepos = None
315
316
316 def rev(self, node):
317 def rev(self, node):
317 try:
318 try:
318 return self._nodecache[node]
319 return self._nodecache[node]
319 except TypeError:
320 except TypeError:
320 raise
321 raise
321 except RevlogError:
322 except RevlogError:
322 # parsers.c radix tree lookup failed
323 # parsers.c radix tree lookup failed
323 raise LookupError(node, self.indexfile, _('no node'))
324 raise LookupError(node, self.indexfile, _('no node'))
324 except KeyError:
325 except KeyError:
325 # pure python cache lookup failed
326 # pure python cache lookup failed
326 n = self._nodecache
327 n = self._nodecache
327 i = self.index
328 i = self.index
328 p = self._nodepos
329 p = self._nodepos
329 if p is None:
330 if p is None:
330 p = len(i) - 2
331 p = len(i) - 2
331 for r in xrange(p, -1, -1):
332 for r in xrange(p, -1, -1):
332 v = i[r][7]
333 v = i[r][7]
333 n[v] = r
334 n[v] = r
334 if v == node:
335 if v == node:
335 self._nodepos = r - 1
336 self._nodepos = r - 1
336 return r
337 return r
337 raise LookupError(node, self.indexfile, _('no node'))
338 raise LookupError(node, self.indexfile, _('no node'))
338
339
339 def node(self, rev):
340 def node(self, rev):
340 return self.index[rev][7]
341 return self.index[rev][7]
341 def linkrev(self, rev):
342 def linkrev(self, rev):
342 return self.index[rev][4]
343 return self.index[rev][4]
343 def parents(self, node):
344 def parents(self, node):
344 i = self.index
345 i = self.index
345 d = i[self.rev(node)]
346 d = i[self.rev(node)]
346 return i[d[5]][7], i[d[6]][7] # map revisions to nodes inline
347 return i[d[5]][7], i[d[6]][7] # map revisions to nodes inline
347 def parentrevs(self, rev):
348 def parentrevs(self, rev):
348 return self.index[rev][5:7]
349 return self.index[rev][5:7]
349 def start(self, rev):
350 def start(self, rev):
350 return int(self.index[rev][0] >> 16)
351 return int(self.index[rev][0] >> 16)
351 def end(self, rev):
352 def end(self, rev):
352 return self.start(rev) + self.length(rev)
353 return self.start(rev) + self.length(rev)
353 def length(self, rev):
354 def length(self, rev):
354 return self.index[rev][1]
355 return self.index[rev][1]
355 def chainbase(self, rev):
356 def chainbase(self, rev):
356 index = self.index
357 index = self.index
357 base = index[rev][3]
358 base = index[rev][3]
358 while base != rev:
359 while base != rev:
359 rev = base
360 rev = base
360 base = index[rev][3]
361 base = index[rev][3]
361 return base
362 return base
362 def chainlen(self, rev):
363 def chainlen(self, rev):
363 return self._chaininfo(rev)[0]
364 return self._chaininfo(rev)[0]
364
365
365 def _chaininfo(self, rev):
366 def _chaininfo(self, rev):
366 chaininfocache = self._chaininfocache
367 chaininfocache = self._chaininfocache
367 if rev in chaininfocache:
368 if rev in chaininfocache:
368 return chaininfocache[rev]
369 return chaininfocache[rev]
369 index = self.index
370 index = self.index
370 generaldelta = self._generaldelta
371 generaldelta = self._generaldelta
371 iterrev = rev
372 iterrev = rev
372 e = index[iterrev]
373 e = index[iterrev]
373 clen = 0
374 clen = 0
374 compresseddeltalen = 0
375 compresseddeltalen = 0
375 while iterrev != e[3]:
376 while iterrev != e[3]:
376 clen += 1
377 clen += 1
377 compresseddeltalen += e[1]
378 compresseddeltalen += e[1]
378 if generaldelta:
379 if generaldelta:
379 iterrev = e[3]
380 iterrev = e[3]
380 else:
381 else:
381 iterrev -= 1
382 iterrev -= 1
382 if iterrev in chaininfocache:
383 if iterrev in chaininfocache:
383 t = chaininfocache[iterrev]
384 t = chaininfocache[iterrev]
384 clen += t[0]
385 clen += t[0]
385 compresseddeltalen += t[1]
386 compresseddeltalen += t[1]
386 break
387 break
387 e = index[iterrev]
388 e = index[iterrev]
388 else:
389 else:
389 # Add text length of base since decompressing that also takes
390 # Add text length of base since decompressing that also takes
390 # work. For cache hits the length is already included.
391 # work. For cache hits the length is already included.
391 compresseddeltalen += e[1]
392 compresseddeltalen += e[1]
392 r = (clen, compresseddeltalen)
393 r = (clen, compresseddeltalen)
393 chaininfocache[rev] = r
394 chaininfocache[rev] = r
394 return r
395 return r
395
396
396 def flags(self, rev):
397 def flags(self, rev):
397 return self.index[rev][0] & 0xFFFF
398 return self.index[rev][0] & 0xFFFF
398 def rawsize(self, rev):
399 def rawsize(self, rev):
399 """return the length of the uncompressed text for a given revision"""
400 """return the length of the uncompressed text for a given revision"""
400 l = self.index[rev][2]
401 l = self.index[rev][2]
401 if l >= 0:
402 if l >= 0:
402 return l
403 return l
403
404
404 t = self.revision(self.node(rev))
405 t = self.revision(self.node(rev))
405 return len(t)
406 return len(t)
406 size = rawsize
407 size = rawsize
407
408
408 def ancestors(self, revs, stoprev=0, inclusive=False):
409 def ancestors(self, revs, stoprev=0, inclusive=False):
409 """Generate the ancestors of 'revs' in reverse topological order.
410 """Generate the ancestors of 'revs' in reverse topological order.
410 Does not generate revs lower than stoprev.
411 Does not generate revs lower than stoprev.
411
412
412 See the documentation for ancestor.lazyancestors for more details."""
413 See the documentation for ancestor.lazyancestors for more details."""
413
414
414 return ancestor.lazyancestors(self.parentrevs, revs, stoprev=stoprev,
415 return ancestor.lazyancestors(self.parentrevs, revs, stoprev=stoprev,
415 inclusive=inclusive)
416 inclusive=inclusive)
416
417
417 def descendants(self, revs):
418 def descendants(self, revs):
418 """Generate the descendants of 'revs' in revision order.
419 """Generate the descendants of 'revs' in revision order.
419
420
420 Yield a sequence of revision numbers starting with a child of
421 Yield a sequence of revision numbers starting with a child of
421 some rev in revs, i.e., each revision is *not* considered a
422 some rev in revs, i.e., each revision is *not* considered a
422 descendant of itself. Results are ordered by revision number (a
423 descendant of itself. Results are ordered by revision number (a
423 topological sort)."""
424 topological sort)."""
424 first = min(revs)
425 first = min(revs)
425 if first == nullrev:
426 if first == nullrev:
426 for i in self:
427 for i in self:
427 yield i
428 yield i
428 return
429 return
429
430
430 seen = set(revs)
431 seen = set(revs)
431 for i in self.revs(start=first + 1):
432 for i in self.revs(start=first + 1):
432 for x in self.parentrevs(i):
433 for x in self.parentrevs(i):
433 if x != nullrev and x in seen:
434 if x != nullrev and x in seen:
434 seen.add(i)
435 seen.add(i)
435 yield i
436 yield i
436 break
437 break
437
438
438 def findcommonmissing(self, common=None, heads=None):
439 def findcommonmissing(self, common=None, heads=None):
439 """Return a tuple of the ancestors of common and the ancestors of heads
440 """Return a tuple of the ancestors of common and the ancestors of heads
440 that are not ancestors of common. In revset terminology, we return the
441 that are not ancestors of common. In revset terminology, we return the
441 tuple:
442 tuple:
442
443
443 ::common, (::heads) - (::common)
444 ::common, (::heads) - (::common)
444
445
445 The list is sorted by revision number, meaning it is
446 The list is sorted by revision number, meaning it is
446 topologically sorted.
447 topologically sorted.
447
448
448 'heads' and 'common' are both lists of node IDs. If heads is
449 'heads' and 'common' are both lists of node IDs. If heads is
449 not supplied, uses all of the revlog's heads. If common is not
450 not supplied, uses all of the revlog's heads. If common is not
450 supplied, uses nullid."""
451 supplied, uses nullid."""
451 if common is None:
452 if common is None:
452 common = [nullid]
453 common = [nullid]
453 if heads is None:
454 if heads is None:
454 heads = self.heads()
455 heads = self.heads()
455
456
456 common = [self.rev(n) for n in common]
457 common = [self.rev(n) for n in common]
457 heads = [self.rev(n) for n in heads]
458 heads = [self.rev(n) for n in heads]
458
459
459 # we want the ancestors, but inclusive
460 # we want the ancestors, but inclusive
460 class lazyset(object):
461 class lazyset(object):
461 def __init__(self, lazyvalues):
462 def __init__(self, lazyvalues):
462 self.addedvalues = set()
463 self.addedvalues = set()
463 self.lazyvalues = lazyvalues
464 self.lazyvalues = lazyvalues
464
465
465 def __contains__(self, value):
466 def __contains__(self, value):
466 return value in self.addedvalues or value in self.lazyvalues
467 return value in self.addedvalues or value in self.lazyvalues
467
468
468 def __iter__(self):
469 def __iter__(self):
469 added = self.addedvalues
470 added = self.addedvalues
470 for r in added:
471 for r in added:
471 yield r
472 yield r
472 for r in self.lazyvalues:
473 for r in self.lazyvalues:
473 if not r in added:
474 if not r in added:
474 yield r
475 yield r
475
476
476 def add(self, value):
477 def add(self, value):
477 self.addedvalues.add(value)
478 self.addedvalues.add(value)
478
479
479 def update(self, values):
480 def update(self, values):
480 self.addedvalues.update(values)
481 self.addedvalues.update(values)
481
482
482 has = lazyset(self.ancestors(common))
483 has = lazyset(self.ancestors(common))
483 has.add(nullrev)
484 has.add(nullrev)
484 has.update(common)
485 has.update(common)
485
486
486 # take all ancestors from heads that aren't in has
487 # take all ancestors from heads that aren't in has
487 missing = set()
488 missing = set()
488 visit = util.deque(r for r in heads if r not in has)
489 visit = collections.deque(r for r in heads if r not in has)
489 while visit:
490 while visit:
490 r = visit.popleft()
491 r = visit.popleft()
491 if r in missing:
492 if r in missing:
492 continue
493 continue
493 else:
494 else:
494 missing.add(r)
495 missing.add(r)
495 for p in self.parentrevs(r):
496 for p in self.parentrevs(r):
496 if p not in has:
497 if p not in has:
497 visit.append(p)
498 visit.append(p)
498 missing = list(missing)
499 missing = list(missing)
499 missing.sort()
500 missing.sort()
500 return has, [self.node(r) for r in missing]
501 return has, [self.node(r) for r in missing]
501
502
502 def incrementalmissingrevs(self, common=None):
503 def incrementalmissingrevs(self, common=None):
503 """Return an object that can be used to incrementally compute the
504 """Return an object that can be used to incrementally compute the
504 revision numbers of the ancestors of arbitrary sets that are not
505 revision numbers of the ancestors of arbitrary sets that are not
505 ancestors of common. This is an ancestor.incrementalmissingancestors
506 ancestors of common. This is an ancestor.incrementalmissingancestors
506 object.
507 object.
507
508
508 'common' is a list of revision numbers. If common is not supplied, uses
509 'common' is a list of revision numbers. If common is not supplied, uses
509 nullrev.
510 nullrev.
510 """
511 """
511 if common is None:
512 if common is None:
512 common = [nullrev]
513 common = [nullrev]
513
514
514 return ancestor.incrementalmissingancestors(self.parentrevs, common)
515 return ancestor.incrementalmissingancestors(self.parentrevs, common)
515
516
516 def findmissingrevs(self, common=None, heads=None):
517 def findmissingrevs(self, common=None, heads=None):
517 """Return the revision numbers of the ancestors of heads that
518 """Return the revision numbers of the ancestors of heads that
518 are not ancestors of common.
519 are not ancestors of common.
519
520
520 More specifically, return a list of revision numbers corresponding to
521 More specifically, return a list of revision numbers corresponding to
521 nodes N such that every N satisfies the following constraints:
522 nodes N such that every N satisfies the following constraints:
522
523
523 1. N is an ancestor of some node in 'heads'
524 1. N is an ancestor of some node in 'heads'
524 2. N is not an ancestor of any node in 'common'
525 2. N is not an ancestor of any node in 'common'
525
526
526 The list is sorted by revision number, meaning it is
527 The list is sorted by revision number, meaning it is
527 topologically sorted.
528 topologically sorted.
528
529
529 'heads' and 'common' are both lists of revision numbers. If heads is
530 'heads' and 'common' are both lists of revision numbers. If heads is
530 not supplied, uses all of the revlog's heads. If common is not
531 not supplied, uses all of the revlog's heads. If common is not
531 supplied, uses nullid."""
532 supplied, uses nullid."""
532 if common is None:
533 if common is None:
533 common = [nullrev]
534 common = [nullrev]
534 if heads is None:
535 if heads is None:
535 heads = self.headrevs()
536 heads = self.headrevs()
536
537
537 inc = self.incrementalmissingrevs(common=common)
538 inc = self.incrementalmissingrevs(common=common)
538 return inc.missingancestors(heads)
539 return inc.missingancestors(heads)
539
540
540 def findmissing(self, common=None, heads=None):
541 def findmissing(self, common=None, heads=None):
541 """Return the ancestors of heads that are not ancestors of common.
542 """Return the ancestors of heads that are not ancestors of common.
542
543
543 More specifically, return a list of nodes N such that every N
544 More specifically, return a list of nodes N such that every N
544 satisfies the following constraints:
545 satisfies the following constraints:
545
546
546 1. N is an ancestor of some node in 'heads'
547 1. N is an ancestor of some node in 'heads'
547 2. N is not an ancestor of any node in 'common'
548 2. N is not an ancestor of any node in 'common'
548
549
549 The list is sorted by revision number, meaning it is
550 The list is sorted by revision number, meaning it is
550 topologically sorted.
551 topologically sorted.
551
552
552 'heads' and 'common' are both lists of node IDs. If heads is
553 'heads' and 'common' are both lists of node IDs. If heads is
553 not supplied, uses all of the revlog's heads. If common is not
554 not supplied, uses all of the revlog's heads. If common is not
554 supplied, uses nullid."""
555 supplied, uses nullid."""
555 if common is None:
556 if common is None:
556 common = [nullid]
557 common = [nullid]
557 if heads is None:
558 if heads is None:
558 heads = self.heads()
559 heads = self.heads()
559
560
560 common = [self.rev(n) for n in common]
561 common = [self.rev(n) for n in common]
561 heads = [self.rev(n) for n in heads]
562 heads = [self.rev(n) for n in heads]
562
563
563 inc = self.incrementalmissingrevs(common=common)
564 inc = self.incrementalmissingrevs(common=common)
564 return [self.node(r) for r in inc.missingancestors(heads)]
565 return [self.node(r) for r in inc.missingancestors(heads)]
565
566
566 def nodesbetween(self, roots=None, heads=None):
567 def nodesbetween(self, roots=None, heads=None):
567 """Return a topological path from 'roots' to 'heads'.
568 """Return a topological path from 'roots' to 'heads'.
568
569
569 Return a tuple (nodes, outroots, outheads) where 'nodes' is a
570 Return a tuple (nodes, outroots, outheads) where 'nodes' is a
570 topologically sorted list of all nodes N that satisfy both of
571 topologically sorted list of all nodes N that satisfy both of
571 these constraints:
572 these constraints:
572
573
573 1. N is a descendant of some node in 'roots'
574 1. N is a descendant of some node in 'roots'
574 2. N is an ancestor of some node in 'heads'
575 2. N is an ancestor of some node in 'heads'
575
576
576 Every node is considered to be both a descendant and an ancestor
577 Every node is considered to be both a descendant and an ancestor
577 of itself, so every reachable node in 'roots' and 'heads' will be
578 of itself, so every reachable node in 'roots' and 'heads' will be
578 included in 'nodes'.
579 included in 'nodes'.
579
580
580 'outroots' is the list of reachable nodes in 'roots', i.e., the
581 'outroots' is the list of reachable nodes in 'roots', i.e., the
581 subset of 'roots' that is returned in 'nodes'. Likewise,
582 subset of 'roots' that is returned in 'nodes'. Likewise,
582 'outheads' is the subset of 'heads' that is also in 'nodes'.
583 'outheads' is the subset of 'heads' that is also in 'nodes'.
583
584
584 'roots' and 'heads' are both lists of node IDs. If 'roots' is
585 'roots' and 'heads' are both lists of node IDs. If 'roots' is
585 unspecified, uses nullid as the only root. If 'heads' is
586 unspecified, uses nullid as the only root. If 'heads' is
586 unspecified, uses list of all of the revlog's heads."""
587 unspecified, uses list of all of the revlog's heads."""
587 nonodes = ([], [], [])
588 nonodes = ([], [], [])
588 if roots is not None:
589 if roots is not None:
589 roots = list(roots)
590 roots = list(roots)
590 if not roots:
591 if not roots:
591 return nonodes
592 return nonodes
592 lowestrev = min([self.rev(n) for n in roots])
593 lowestrev = min([self.rev(n) for n in roots])
593 else:
594 else:
594 roots = [nullid] # Everybody's a descendant of nullid
595 roots = [nullid] # Everybody's a descendant of nullid
595 lowestrev = nullrev
596 lowestrev = nullrev
596 if (lowestrev == nullrev) and (heads is None):
597 if (lowestrev == nullrev) and (heads is None):
597 # We want _all_ the nodes!
598 # We want _all_ the nodes!
598 return ([self.node(r) for r in self], [nullid], list(self.heads()))
599 return ([self.node(r) for r in self], [nullid], list(self.heads()))
599 if heads is None:
600 if heads is None:
600 # All nodes are ancestors, so the latest ancestor is the last
601 # All nodes are ancestors, so the latest ancestor is the last
601 # node.
602 # node.
602 highestrev = len(self) - 1
603 highestrev = len(self) - 1
603 # Set ancestors to None to signal that every node is an ancestor.
604 # Set ancestors to None to signal that every node is an ancestor.
604 ancestors = None
605 ancestors = None
605 # Set heads to an empty dictionary for later discovery of heads
606 # Set heads to an empty dictionary for later discovery of heads
606 heads = {}
607 heads = {}
607 else:
608 else:
608 heads = list(heads)
609 heads = list(heads)
609 if not heads:
610 if not heads:
610 return nonodes
611 return nonodes
611 ancestors = set()
612 ancestors = set()
612 # Turn heads into a dictionary so we can remove 'fake' heads.
613 # Turn heads into a dictionary so we can remove 'fake' heads.
613 # Also, later we will be using it to filter out the heads we can't
614 # Also, later we will be using it to filter out the heads we can't
614 # find from roots.
615 # find from roots.
615 heads = dict.fromkeys(heads, False)
616 heads = dict.fromkeys(heads, False)
616 # Start at the top and keep marking parents until we're done.
617 # Start at the top and keep marking parents until we're done.
617 nodestotag = set(heads)
618 nodestotag = set(heads)
618 # Remember where the top was so we can use it as a limit later.
619 # Remember where the top was so we can use it as a limit later.
619 highestrev = max([self.rev(n) for n in nodestotag])
620 highestrev = max([self.rev(n) for n in nodestotag])
620 while nodestotag:
621 while nodestotag:
621 # grab a node to tag
622 # grab a node to tag
622 n = nodestotag.pop()
623 n = nodestotag.pop()
623 # Never tag nullid
624 # Never tag nullid
624 if n == nullid:
625 if n == nullid:
625 continue
626 continue
626 # A node's revision number represents its place in a
627 # A node's revision number represents its place in a
627 # topologically sorted list of nodes.
628 # topologically sorted list of nodes.
628 r = self.rev(n)
629 r = self.rev(n)
629 if r >= lowestrev:
630 if r >= lowestrev:
630 if n not in ancestors:
631 if n not in ancestors:
631 # If we are possibly a descendant of one of the roots
632 # If we are possibly a descendant of one of the roots
632 # and we haven't already been marked as an ancestor
633 # and we haven't already been marked as an ancestor
633 ancestors.add(n) # Mark as ancestor
634 ancestors.add(n) # Mark as ancestor
634 # Add non-nullid parents to list of nodes to tag.
635 # Add non-nullid parents to list of nodes to tag.
635 nodestotag.update([p for p in self.parents(n) if
636 nodestotag.update([p for p in self.parents(n) if
636 p != nullid])
637 p != nullid])
637 elif n in heads: # We've seen it before, is it a fake head?
638 elif n in heads: # We've seen it before, is it a fake head?
638 # So it is, real heads should not be the ancestors of
639 # So it is, real heads should not be the ancestors of
639 # any other heads.
640 # any other heads.
640 heads.pop(n)
641 heads.pop(n)
641 if not ancestors:
642 if not ancestors:
642 return nonodes
643 return nonodes
643 # Now that we have our set of ancestors, we want to remove any
644 # Now that we have our set of ancestors, we want to remove any
644 # roots that are not ancestors.
645 # roots that are not ancestors.
645
646
646 # If one of the roots was nullid, everything is included anyway.
647 # If one of the roots was nullid, everything is included anyway.
647 if lowestrev > nullrev:
648 if lowestrev > nullrev:
648 # But, since we weren't, let's recompute the lowest rev to not
649 # But, since we weren't, let's recompute the lowest rev to not
649 # include roots that aren't ancestors.
650 # include roots that aren't ancestors.
650
651
651 # Filter out roots that aren't ancestors of heads
652 # Filter out roots that aren't ancestors of heads
652 roots = [n for n in roots if n in ancestors]
653 roots = [n for n in roots if n in ancestors]
653 # Recompute the lowest revision
654 # Recompute the lowest revision
654 if roots:
655 if roots:
655 lowestrev = min([self.rev(n) for n in roots])
656 lowestrev = min([self.rev(n) for n in roots])
656 else:
657 else:
657 # No more roots? Return empty list
658 # No more roots? Return empty list
658 return nonodes
659 return nonodes
659 else:
660 else:
660 # We are descending from nullid, and don't need to care about
661 # We are descending from nullid, and don't need to care about
661 # any other roots.
662 # any other roots.
662 lowestrev = nullrev
663 lowestrev = nullrev
663 roots = [nullid]
664 roots = [nullid]
664 # Transform our roots list into a set.
665 # Transform our roots list into a set.
665 descendants = set(roots)
666 descendants = set(roots)
666 # Also, keep the original roots so we can filter out roots that aren't
667 # Also, keep the original roots so we can filter out roots that aren't
667 # 'real' roots (i.e. are descended from other roots).
668 # 'real' roots (i.e. are descended from other roots).
668 roots = descendants.copy()
669 roots = descendants.copy()
669 # Our topologically sorted list of output nodes.
670 # Our topologically sorted list of output nodes.
670 orderedout = []
671 orderedout = []
671 # Don't start at nullid since we don't want nullid in our output list,
672 # Don't start at nullid since we don't want nullid in our output list,
672 # and if nullid shows up in descendants, empty parents will look like
673 # and if nullid shows up in descendants, empty parents will look like
673 # they're descendants.
674 # they're descendants.
674 for r in self.revs(start=max(lowestrev, 0), stop=highestrev + 1):
675 for r in self.revs(start=max(lowestrev, 0), stop=highestrev + 1):
675 n = self.node(r)
676 n = self.node(r)
676 isdescendant = False
677 isdescendant = False
677 if lowestrev == nullrev: # Everybody is a descendant of nullid
678 if lowestrev == nullrev: # Everybody is a descendant of nullid
678 isdescendant = True
679 isdescendant = True
679 elif n in descendants:
680 elif n in descendants:
680 # n is already a descendant
681 # n is already a descendant
681 isdescendant = True
682 isdescendant = True
682 # This check only needs to be done here because all the roots
683 # This check only needs to be done here because all the roots
683 # will start being marked is descendants before the loop.
684 # will start being marked is descendants before the loop.
684 if n in roots:
685 if n in roots:
685 # If n was a root, check if it's a 'real' root.
686 # If n was a root, check if it's a 'real' root.
686 p = tuple(self.parents(n))
687 p = tuple(self.parents(n))
687 # If any of its parents are descendants, it's not a root.
688 # If any of its parents are descendants, it's not a root.
688 if (p[0] in descendants) or (p[1] in descendants):
689 if (p[0] in descendants) or (p[1] in descendants):
689 roots.remove(n)
690 roots.remove(n)
690 else:
691 else:
691 p = tuple(self.parents(n))
692 p = tuple(self.parents(n))
692 # A node is a descendant if either of its parents are
693 # A node is a descendant if either of its parents are
693 # descendants. (We seeded the dependents list with the roots
694 # descendants. (We seeded the dependents list with the roots
694 # up there, remember?)
695 # up there, remember?)
695 if (p[0] in descendants) or (p[1] in descendants):
696 if (p[0] in descendants) or (p[1] in descendants):
696 descendants.add(n)
697 descendants.add(n)
697 isdescendant = True
698 isdescendant = True
698 if isdescendant and ((ancestors is None) or (n in ancestors)):
699 if isdescendant and ((ancestors is None) or (n in ancestors)):
699 # Only include nodes that are both descendants and ancestors.
700 # Only include nodes that are both descendants and ancestors.
700 orderedout.append(n)
701 orderedout.append(n)
701 if (ancestors is not None) and (n in heads):
702 if (ancestors is not None) and (n in heads):
702 # We're trying to figure out which heads are reachable
703 # We're trying to figure out which heads are reachable
703 # from roots.
704 # from roots.
704 # Mark this head as having been reached
705 # Mark this head as having been reached
705 heads[n] = True
706 heads[n] = True
706 elif ancestors is None:
707 elif ancestors is None:
707 # Otherwise, we're trying to discover the heads.
708 # Otherwise, we're trying to discover the heads.
708 # Assume this is a head because if it isn't, the next step
709 # Assume this is a head because if it isn't, the next step
709 # will eventually remove it.
710 # will eventually remove it.
710 heads[n] = True
711 heads[n] = True
711 # But, obviously its parents aren't.
712 # But, obviously its parents aren't.
712 for p in self.parents(n):
713 for p in self.parents(n):
713 heads.pop(p, None)
714 heads.pop(p, None)
714 heads = [n for n, flag in heads.iteritems() if flag]
715 heads = [n for n, flag in heads.iteritems() if flag]
715 roots = list(roots)
716 roots = list(roots)
716 assert orderedout
717 assert orderedout
717 assert roots
718 assert roots
718 assert heads
719 assert heads
719 return (orderedout, roots, heads)
720 return (orderedout, roots, heads)
720
721
721 def headrevs(self):
722 def headrevs(self):
722 try:
723 try:
723 return self.index.headrevs()
724 return self.index.headrevs()
724 except AttributeError:
725 except AttributeError:
725 return self._headrevs()
726 return self._headrevs()
726
727
727 def computephases(self, roots):
728 def computephases(self, roots):
728 return self.index.computephases(roots)
729 return self.index.computephases(roots)
729
730
730 def _headrevs(self):
731 def _headrevs(self):
731 count = len(self)
732 count = len(self)
732 if not count:
733 if not count:
733 return [nullrev]
734 return [nullrev]
734 # we won't iter over filtered rev so nobody is a head at start
735 # we won't iter over filtered rev so nobody is a head at start
735 ishead = [0] * (count + 1)
736 ishead = [0] * (count + 1)
736 index = self.index
737 index = self.index
737 for r in self:
738 for r in self:
738 ishead[r] = 1 # I may be an head
739 ishead[r] = 1 # I may be an head
739 e = index[r]
740 e = index[r]
740 ishead[e[5]] = ishead[e[6]] = 0 # my parent are not
741 ishead[e[5]] = ishead[e[6]] = 0 # my parent are not
741 return [r for r, val in enumerate(ishead) if val]
742 return [r for r, val in enumerate(ishead) if val]
742
743
743 def heads(self, start=None, stop=None):
744 def heads(self, start=None, stop=None):
744 """return the list of all nodes that have no children
745 """return the list of all nodes that have no children
745
746
746 if start is specified, only heads that are descendants of
747 if start is specified, only heads that are descendants of
747 start will be returned
748 start will be returned
748 if stop is specified, it will consider all the revs from stop
749 if stop is specified, it will consider all the revs from stop
749 as if they had no children
750 as if they had no children
750 """
751 """
751 if start is None and stop is None:
752 if start is None and stop is None:
752 if not len(self):
753 if not len(self):
753 return [nullid]
754 return [nullid]
754 return [self.node(r) for r in self.headrevs()]
755 return [self.node(r) for r in self.headrevs()]
755
756
756 if start is None:
757 if start is None:
757 start = nullid
758 start = nullid
758 if stop is None:
759 if stop is None:
759 stop = []
760 stop = []
760 stoprevs = set([self.rev(n) for n in stop])
761 stoprevs = set([self.rev(n) for n in stop])
761 startrev = self.rev(start)
762 startrev = self.rev(start)
762 reachable = set((startrev,))
763 reachable = set((startrev,))
763 heads = set((startrev,))
764 heads = set((startrev,))
764
765
765 parentrevs = self.parentrevs
766 parentrevs = self.parentrevs
766 for r in self.revs(start=startrev + 1):
767 for r in self.revs(start=startrev + 1):
767 for p in parentrevs(r):
768 for p in parentrevs(r):
768 if p in reachable:
769 if p in reachable:
769 if r not in stoprevs:
770 if r not in stoprevs:
770 reachable.add(r)
771 reachable.add(r)
771 heads.add(r)
772 heads.add(r)
772 if p in heads and p not in stoprevs:
773 if p in heads and p not in stoprevs:
773 heads.remove(p)
774 heads.remove(p)
774
775
775 return [self.node(r) for r in heads]
776 return [self.node(r) for r in heads]
776
777
777 def children(self, node):
778 def children(self, node):
778 """find the children of a given node"""
779 """find the children of a given node"""
779 c = []
780 c = []
780 p = self.rev(node)
781 p = self.rev(node)
781 for r in self.revs(start=p + 1):
782 for r in self.revs(start=p + 1):
782 prevs = [pr for pr in self.parentrevs(r) if pr != nullrev]
783 prevs = [pr for pr in self.parentrevs(r) if pr != nullrev]
783 if prevs:
784 if prevs:
784 for pr in prevs:
785 for pr in prevs:
785 if pr == p:
786 if pr == p:
786 c.append(self.node(r))
787 c.append(self.node(r))
787 elif p == nullrev:
788 elif p == nullrev:
788 c.append(self.node(r))
789 c.append(self.node(r))
789 return c
790 return c
790
791
791 def descendant(self, start, end):
792 def descendant(self, start, end):
792 if start == nullrev:
793 if start == nullrev:
793 return True
794 return True
794 for i in self.descendants([start]):
795 for i in self.descendants([start]):
795 if i == end:
796 if i == end:
796 return True
797 return True
797 elif i > end:
798 elif i > end:
798 break
799 break
799 return False
800 return False
800
801
801 def commonancestorsheads(self, a, b):
802 def commonancestorsheads(self, a, b):
802 """calculate all the heads of the common ancestors of nodes a and b"""
803 """calculate all the heads of the common ancestors of nodes a and b"""
803 a, b = self.rev(a), self.rev(b)
804 a, b = self.rev(a), self.rev(b)
804 try:
805 try:
805 ancs = self.index.commonancestorsheads(a, b)
806 ancs = self.index.commonancestorsheads(a, b)
806 except (AttributeError, OverflowError): # C implementation failed
807 except (AttributeError, OverflowError): # C implementation failed
807 ancs = ancestor.commonancestorsheads(self.parentrevs, a, b)
808 ancs = ancestor.commonancestorsheads(self.parentrevs, a, b)
808 return map(self.node, ancs)
809 return map(self.node, ancs)
809
810
810 def isancestor(self, a, b):
811 def isancestor(self, a, b):
811 """return True if node a is an ancestor of node b
812 """return True if node a is an ancestor of node b
812
813
813 The implementation of this is trivial but the use of
814 The implementation of this is trivial but the use of
814 commonancestorsheads is not."""
815 commonancestorsheads is not."""
815 return a in self.commonancestorsheads(a, b)
816 return a in self.commonancestorsheads(a, b)
816
817
817 def ancestor(self, a, b):
818 def ancestor(self, a, b):
818 """calculate the "best" common ancestor of nodes a and b"""
819 """calculate the "best" common ancestor of nodes a and b"""
819
820
820 a, b = self.rev(a), self.rev(b)
821 a, b = self.rev(a), self.rev(b)
821 try:
822 try:
822 ancs = self.index.ancestors(a, b)
823 ancs = self.index.ancestors(a, b)
823 except (AttributeError, OverflowError):
824 except (AttributeError, OverflowError):
824 ancs = ancestor.ancestors(self.parentrevs, a, b)
825 ancs = ancestor.ancestors(self.parentrevs, a, b)
825 if ancs:
826 if ancs:
826 # choose a consistent winner when there's a tie
827 # choose a consistent winner when there's a tie
827 return min(map(self.node, ancs))
828 return min(map(self.node, ancs))
828 return nullid
829 return nullid
829
830
830 def _match(self, id):
831 def _match(self, id):
831 if isinstance(id, int):
832 if isinstance(id, int):
832 # rev
833 # rev
833 return self.node(id)
834 return self.node(id)
834 if len(id) == 20:
835 if len(id) == 20:
835 # possibly a binary node
836 # possibly a binary node
836 # odds of a binary node being all hex in ASCII are 1 in 10**25
837 # odds of a binary node being all hex in ASCII are 1 in 10**25
837 try:
838 try:
838 node = id
839 node = id
839 self.rev(node) # quick search the index
840 self.rev(node) # quick search the index
840 return node
841 return node
841 except LookupError:
842 except LookupError:
842 pass # may be partial hex id
843 pass # may be partial hex id
843 try:
844 try:
844 # str(rev)
845 # str(rev)
845 rev = int(id)
846 rev = int(id)
846 if str(rev) != id:
847 if str(rev) != id:
847 raise ValueError
848 raise ValueError
848 if rev < 0:
849 if rev < 0:
849 rev = len(self) + rev
850 rev = len(self) + rev
850 if rev < 0 or rev >= len(self):
851 if rev < 0 or rev >= len(self):
851 raise ValueError
852 raise ValueError
852 return self.node(rev)
853 return self.node(rev)
853 except (ValueError, OverflowError):
854 except (ValueError, OverflowError):
854 pass
855 pass
855 if len(id) == 40:
856 if len(id) == 40:
856 try:
857 try:
857 # a full hex nodeid?
858 # a full hex nodeid?
858 node = bin(id)
859 node = bin(id)
859 self.rev(node)
860 self.rev(node)
860 return node
861 return node
861 except (TypeError, LookupError):
862 except (TypeError, LookupError):
862 pass
863 pass
863
864
864 def _partialmatch(self, id):
865 def _partialmatch(self, id):
865 try:
866 try:
866 n = self.index.partialmatch(id)
867 n = self.index.partialmatch(id)
867 if n and self.hasnode(n):
868 if n and self.hasnode(n):
868 return n
869 return n
869 return None
870 return None
870 except RevlogError:
871 except RevlogError:
871 # parsers.c radix tree lookup gave multiple matches
872 # parsers.c radix tree lookup gave multiple matches
872 # fall through to slow path that filters hidden revisions
873 # fall through to slow path that filters hidden revisions
873 pass
874 pass
874 except (AttributeError, ValueError):
875 except (AttributeError, ValueError):
875 # we are pure python, or key was too short to search radix tree
876 # we are pure python, or key was too short to search radix tree
876 pass
877 pass
877
878
878 if id in self._pcache:
879 if id in self._pcache:
879 return self._pcache[id]
880 return self._pcache[id]
880
881
881 if len(id) < 40:
882 if len(id) < 40:
882 try:
883 try:
883 # hex(node)[:...]
884 # hex(node)[:...]
884 l = len(id) // 2 # grab an even number of digits
885 l = len(id) // 2 # grab an even number of digits
885 prefix = bin(id[:l * 2])
886 prefix = bin(id[:l * 2])
886 nl = [e[7] for e in self.index if e[7].startswith(prefix)]
887 nl = [e[7] for e in self.index if e[7].startswith(prefix)]
887 nl = [n for n in nl if hex(n).startswith(id) and
888 nl = [n for n in nl if hex(n).startswith(id) and
888 self.hasnode(n)]
889 self.hasnode(n)]
889 if len(nl) > 0:
890 if len(nl) > 0:
890 if len(nl) == 1:
891 if len(nl) == 1:
891 self._pcache[id] = nl[0]
892 self._pcache[id] = nl[0]
892 return nl[0]
893 return nl[0]
893 raise LookupError(id, self.indexfile,
894 raise LookupError(id, self.indexfile,
894 _('ambiguous identifier'))
895 _('ambiguous identifier'))
895 return None
896 return None
896 except TypeError:
897 except TypeError:
897 pass
898 pass
898
899
899 def lookup(self, id):
900 def lookup(self, id):
900 """locate a node based on:
901 """locate a node based on:
901 - revision number or str(revision number)
902 - revision number or str(revision number)
902 - nodeid or subset of hex nodeid
903 - nodeid or subset of hex nodeid
903 """
904 """
904 n = self._match(id)
905 n = self._match(id)
905 if n is not None:
906 if n is not None:
906 return n
907 return n
907 n = self._partialmatch(id)
908 n = self._partialmatch(id)
908 if n:
909 if n:
909 return n
910 return n
910
911
911 raise LookupError(id, self.indexfile, _('no match found'))
912 raise LookupError(id, self.indexfile, _('no match found'))
912
913
913 def cmp(self, node, text):
914 def cmp(self, node, text):
914 """compare text with a given file revision
915 """compare text with a given file revision
915
916
916 returns True if text is different than what is stored.
917 returns True if text is different than what is stored.
917 """
918 """
918 p1, p2 = self.parents(node)
919 p1, p2 = self.parents(node)
919 return hash(text, p1, p2) != node
920 return hash(text, p1, p2) != node
920
921
921 def _addchunk(self, offset, data):
922 def _addchunk(self, offset, data):
922 o, d = self._chunkcache
923 o, d = self._chunkcache
923 # try to add to existing cache
924 # try to add to existing cache
924 if o + len(d) == offset and len(d) + len(data) < _chunksize:
925 if o + len(d) == offset and len(d) + len(data) < _chunksize:
925 self._chunkcache = o, d + data
926 self._chunkcache = o, d + data
926 else:
927 else:
927 self._chunkcache = offset, data
928 self._chunkcache = offset, data
928
929
929 def _loadchunk(self, offset, length):
930 def _loadchunk(self, offset, length):
930 if self._inline:
931 if self._inline:
931 df = self.opener(self.indexfile)
932 df = self.opener(self.indexfile)
932 else:
933 else:
933 df = self.opener(self.datafile)
934 df = self.opener(self.datafile)
934
935
935 # Cache data both forward and backward around the requested
936 # Cache data both forward and backward around the requested
936 # data, in a fixed size window. This helps speed up operations
937 # data, in a fixed size window. This helps speed up operations
937 # involving reading the revlog backwards.
938 # involving reading the revlog backwards.
938 cachesize = self._chunkcachesize
939 cachesize = self._chunkcachesize
939 realoffset = offset & ~(cachesize - 1)
940 realoffset = offset & ~(cachesize - 1)
940 reallength = (((offset + length + cachesize) & ~(cachesize - 1))
941 reallength = (((offset + length + cachesize) & ~(cachesize - 1))
941 - realoffset)
942 - realoffset)
942 df.seek(realoffset)
943 df.seek(realoffset)
943 d = df.read(reallength)
944 d = df.read(reallength)
944 df.close()
945 df.close()
945 self._addchunk(realoffset, d)
946 self._addchunk(realoffset, d)
946 if offset != realoffset or reallength != length:
947 if offset != realoffset or reallength != length:
947 return util.buffer(d, offset - realoffset, length)
948 return util.buffer(d, offset - realoffset, length)
948 return d
949 return d
949
950
950 def _getchunk(self, offset, length):
951 def _getchunk(self, offset, length):
951 o, d = self._chunkcache
952 o, d = self._chunkcache
952 l = len(d)
953 l = len(d)
953
954
954 # is it in the cache?
955 # is it in the cache?
955 cachestart = offset - o
956 cachestart = offset - o
956 cacheend = cachestart + length
957 cacheend = cachestart + length
957 if cachestart >= 0 and cacheend <= l:
958 if cachestart >= 0 and cacheend <= l:
958 if cachestart == 0 and cacheend == l:
959 if cachestart == 0 and cacheend == l:
959 return d # avoid a copy
960 return d # avoid a copy
960 return util.buffer(d, cachestart, cacheend - cachestart)
961 return util.buffer(d, cachestart, cacheend - cachestart)
961
962
962 return self._loadchunk(offset, length)
963 return self._loadchunk(offset, length)
963
964
964 def _chunkraw(self, startrev, endrev):
965 def _chunkraw(self, startrev, endrev):
965 start = self.start(startrev)
966 start = self.start(startrev)
966 end = self.end(endrev)
967 end = self.end(endrev)
967 if self._inline:
968 if self._inline:
968 start += (startrev + 1) * self._io.size
969 start += (startrev + 1) * self._io.size
969 end += (endrev + 1) * self._io.size
970 end += (endrev + 1) * self._io.size
970 length = end - start
971 length = end - start
971 return self._getchunk(start, length)
972 return self._getchunk(start, length)
972
973
973 def _chunk(self, rev):
974 def _chunk(self, rev):
974 return decompress(self._chunkraw(rev, rev))
975 return decompress(self._chunkraw(rev, rev))
975
976
976 def _chunks(self, revs):
977 def _chunks(self, revs):
977 '''faster version of [self._chunk(rev) for rev in revs]
978 '''faster version of [self._chunk(rev) for rev in revs]
978
979
979 Assumes that revs is in ascending order.'''
980 Assumes that revs is in ascending order.'''
980 if not revs:
981 if not revs:
981 return []
982 return []
982 start = self.start
983 start = self.start
983 length = self.length
984 length = self.length
984 inline = self._inline
985 inline = self._inline
985 iosize = self._io.size
986 iosize = self._io.size
986 buffer = util.buffer
987 buffer = util.buffer
987
988
988 l = []
989 l = []
989 ladd = l.append
990 ladd = l.append
990
991
991 # preload the cache
992 # preload the cache
992 try:
993 try:
993 while True:
994 while True:
994 # ensure that the cache doesn't change out from under us
995 # ensure that the cache doesn't change out from under us
995 _cache = self._chunkcache
996 _cache = self._chunkcache
996 self._chunkraw(revs[0], revs[-1])
997 self._chunkraw(revs[0], revs[-1])
997 if _cache == self._chunkcache:
998 if _cache == self._chunkcache:
998 break
999 break
999 offset, data = _cache
1000 offset, data = _cache
1000 except OverflowError:
1001 except OverflowError:
1001 # issue4215 - we can't cache a run of chunks greater than
1002 # issue4215 - we can't cache a run of chunks greater than
1002 # 2G on Windows
1003 # 2G on Windows
1003 return [self._chunk(rev) for rev in revs]
1004 return [self._chunk(rev) for rev in revs]
1004
1005
1005 for rev in revs:
1006 for rev in revs:
1006 chunkstart = start(rev)
1007 chunkstart = start(rev)
1007 if inline:
1008 if inline:
1008 chunkstart += (rev + 1) * iosize
1009 chunkstart += (rev + 1) * iosize
1009 chunklength = length(rev)
1010 chunklength = length(rev)
1010 ladd(decompress(buffer(data, chunkstart - offset, chunklength)))
1011 ladd(decompress(buffer(data, chunkstart - offset, chunklength)))
1011
1012
1012 return l
1013 return l
1013
1014
1014 def _chunkclear(self):
1015 def _chunkclear(self):
1015 self._chunkcache = (0, '')
1016 self._chunkcache = (0, '')
1016
1017
1017 def deltaparent(self, rev):
1018 def deltaparent(self, rev):
1018 """return deltaparent of the given revision"""
1019 """return deltaparent of the given revision"""
1019 base = self.index[rev][3]
1020 base = self.index[rev][3]
1020 if base == rev:
1021 if base == rev:
1021 return nullrev
1022 return nullrev
1022 elif self._generaldelta:
1023 elif self._generaldelta:
1023 return base
1024 return base
1024 else:
1025 else:
1025 return rev - 1
1026 return rev - 1
1026
1027
1027 def revdiff(self, rev1, rev2):
1028 def revdiff(self, rev1, rev2):
1028 """return or calculate a delta between two revisions"""
1029 """return or calculate a delta between two revisions"""
1029 if rev1 != nullrev and self.deltaparent(rev2) == rev1:
1030 if rev1 != nullrev and self.deltaparent(rev2) == rev1:
1030 return str(self._chunk(rev2))
1031 return str(self._chunk(rev2))
1031
1032
1032 return mdiff.textdiff(self.revision(rev1),
1033 return mdiff.textdiff(self.revision(rev1),
1033 self.revision(rev2))
1034 self.revision(rev2))
1034
1035
1035 def revision(self, nodeorrev):
1036 def revision(self, nodeorrev):
1036 """return an uncompressed revision of a given node or revision
1037 """return an uncompressed revision of a given node or revision
1037 number.
1038 number.
1038 """
1039 """
1039 if isinstance(nodeorrev, int):
1040 if isinstance(nodeorrev, int):
1040 rev = nodeorrev
1041 rev = nodeorrev
1041 node = self.node(rev)
1042 node = self.node(rev)
1042 else:
1043 else:
1043 node = nodeorrev
1044 node = nodeorrev
1044 rev = None
1045 rev = None
1045
1046
1046 _cache = self._cache # grab local copy of cache to avoid thread race
1047 _cache = self._cache # grab local copy of cache to avoid thread race
1047 cachedrev = None
1048 cachedrev = None
1048 if node == nullid:
1049 if node == nullid:
1049 return ""
1050 return ""
1050 if _cache:
1051 if _cache:
1051 if _cache[0] == node:
1052 if _cache[0] == node:
1052 return _cache[2]
1053 return _cache[2]
1053 cachedrev = _cache[1]
1054 cachedrev = _cache[1]
1054
1055
1055 # look up what we need to read
1056 # look up what we need to read
1056 text = None
1057 text = None
1057 if rev is None:
1058 if rev is None:
1058 rev = self.rev(node)
1059 rev = self.rev(node)
1059
1060
1060 # check rev flags
1061 # check rev flags
1061 if self.flags(rev) & ~REVIDX_KNOWN_FLAGS:
1062 if self.flags(rev) & ~REVIDX_KNOWN_FLAGS:
1062 raise RevlogError(_('incompatible revision flag %x') %
1063 raise RevlogError(_('incompatible revision flag %x') %
1063 (self.flags(rev) & ~REVIDX_KNOWN_FLAGS))
1064 (self.flags(rev) & ~REVIDX_KNOWN_FLAGS))
1064
1065
1065 # build delta chain
1066 # build delta chain
1066 chain = []
1067 chain = []
1067 index = self.index # for performance
1068 index = self.index # for performance
1068 generaldelta = self._generaldelta
1069 generaldelta = self._generaldelta
1069 iterrev = rev
1070 iterrev = rev
1070 e = index[iterrev]
1071 e = index[iterrev]
1071 while iterrev != e[3] and iterrev != cachedrev:
1072 while iterrev != e[3] and iterrev != cachedrev:
1072 chain.append(iterrev)
1073 chain.append(iterrev)
1073 if generaldelta:
1074 if generaldelta:
1074 iterrev = e[3]
1075 iterrev = e[3]
1075 else:
1076 else:
1076 iterrev -= 1
1077 iterrev -= 1
1077 e = index[iterrev]
1078 e = index[iterrev]
1078
1079
1079 if iterrev == cachedrev:
1080 if iterrev == cachedrev:
1080 # cache hit
1081 # cache hit
1081 text = _cache[2]
1082 text = _cache[2]
1082 else:
1083 else:
1083 chain.append(iterrev)
1084 chain.append(iterrev)
1084 chain.reverse()
1085 chain.reverse()
1085
1086
1086 # drop cache to save memory
1087 # drop cache to save memory
1087 self._cache = None
1088 self._cache = None
1088
1089
1089 bins = self._chunks(chain)
1090 bins = self._chunks(chain)
1090 if text is None:
1091 if text is None:
1091 text = str(bins[0])
1092 text = str(bins[0])
1092 bins = bins[1:]
1093 bins = bins[1:]
1093
1094
1094 text = mdiff.patches(text, bins)
1095 text = mdiff.patches(text, bins)
1095
1096
1096 text = self._checkhash(text, node, rev)
1097 text = self._checkhash(text, node, rev)
1097
1098
1098 self._cache = (node, rev, text)
1099 self._cache = (node, rev, text)
1099 return text
1100 return text
1100
1101
1101 def hash(self, text, p1, p2):
1102 def hash(self, text, p1, p2):
1102 """Compute a node hash.
1103 """Compute a node hash.
1103
1104
1104 Available as a function so that subclasses can replace the hash
1105 Available as a function so that subclasses can replace the hash
1105 as needed.
1106 as needed.
1106 """
1107 """
1107 return hash(text, p1, p2)
1108 return hash(text, p1, p2)
1108
1109
1109 def _checkhash(self, text, node, rev):
1110 def _checkhash(self, text, node, rev):
1110 p1, p2 = self.parents(node)
1111 p1, p2 = self.parents(node)
1111 self.checkhash(text, p1, p2, node, rev)
1112 self.checkhash(text, p1, p2, node, rev)
1112 return text
1113 return text
1113
1114
1114 def checkhash(self, text, p1, p2, node, rev=None):
1115 def checkhash(self, text, p1, p2, node, rev=None):
1115 if node != self.hash(text, p1, p2):
1116 if node != self.hash(text, p1, p2):
1116 revornode = rev
1117 revornode = rev
1117 if revornode is None:
1118 if revornode is None:
1118 revornode = templatefilters.short(hex(node))
1119 revornode = templatefilters.short(hex(node))
1119 raise RevlogError(_("integrity check failed on %s:%s")
1120 raise RevlogError(_("integrity check failed on %s:%s")
1120 % (self.indexfile, revornode))
1121 % (self.indexfile, revornode))
1121
1122
1122 def checkinlinesize(self, tr, fp=None):
1123 def checkinlinesize(self, tr, fp=None):
1123 if not self._inline or (self.start(-2) + self.length(-2)) < _maxinline:
1124 if not self._inline or (self.start(-2) + self.length(-2)) < _maxinline:
1124 return
1125 return
1125
1126
1126 trinfo = tr.find(self.indexfile)
1127 trinfo = tr.find(self.indexfile)
1127 if trinfo is None:
1128 if trinfo is None:
1128 raise RevlogError(_("%s not found in the transaction")
1129 raise RevlogError(_("%s not found in the transaction")
1129 % self.indexfile)
1130 % self.indexfile)
1130
1131
1131 trindex = trinfo[2]
1132 trindex = trinfo[2]
1132 if trindex is not None:
1133 if trindex is not None:
1133 dataoff = self.start(trindex)
1134 dataoff = self.start(trindex)
1134 else:
1135 else:
1135 # revlog was stripped at start of transaction, use all leftover data
1136 # revlog was stripped at start of transaction, use all leftover data
1136 trindex = len(self) - 1
1137 trindex = len(self) - 1
1137 dataoff = self.end(-2)
1138 dataoff = self.end(-2)
1138
1139
1139 tr.add(self.datafile, dataoff)
1140 tr.add(self.datafile, dataoff)
1140
1141
1141 if fp:
1142 if fp:
1142 fp.flush()
1143 fp.flush()
1143 fp.close()
1144 fp.close()
1144
1145
1145 df = self.opener(self.datafile, 'w')
1146 df = self.opener(self.datafile, 'w')
1146 try:
1147 try:
1147 for r in self:
1148 for r in self:
1148 df.write(self._chunkraw(r, r))
1149 df.write(self._chunkraw(r, r))
1149 finally:
1150 finally:
1150 df.close()
1151 df.close()
1151
1152
1152 fp = self.opener(self.indexfile, 'w', atomictemp=True)
1153 fp = self.opener(self.indexfile, 'w', atomictemp=True)
1153 self.version &= ~(REVLOGNGINLINEDATA)
1154 self.version &= ~(REVLOGNGINLINEDATA)
1154 self._inline = False
1155 self._inline = False
1155 for i in self:
1156 for i in self:
1156 e = self._io.packentry(self.index[i], self.node, self.version, i)
1157 e = self._io.packentry(self.index[i], self.node, self.version, i)
1157 fp.write(e)
1158 fp.write(e)
1158
1159
1159 # if we don't call close, the temp file will never replace the
1160 # if we don't call close, the temp file will never replace the
1160 # real index
1161 # real index
1161 fp.close()
1162 fp.close()
1162
1163
1163 tr.replace(self.indexfile, trindex * self._io.size)
1164 tr.replace(self.indexfile, trindex * self._io.size)
1164 self._chunkclear()
1165 self._chunkclear()
1165
1166
1166 def addrevision(self, text, transaction, link, p1, p2, cachedelta=None,
1167 def addrevision(self, text, transaction, link, p1, p2, cachedelta=None,
1167 node=None):
1168 node=None):
1168 """add a revision to the log
1169 """add a revision to the log
1169
1170
1170 text - the revision data to add
1171 text - the revision data to add
1171 transaction - the transaction object used for rollback
1172 transaction - the transaction object used for rollback
1172 link - the linkrev data to add
1173 link - the linkrev data to add
1173 p1, p2 - the parent nodeids of the revision
1174 p1, p2 - the parent nodeids of the revision
1174 cachedelta - an optional precomputed delta
1175 cachedelta - an optional precomputed delta
1175 node - nodeid of revision; typically node is not specified, and it is
1176 node - nodeid of revision; typically node is not specified, and it is
1176 computed by default as hash(text, p1, p2), however subclasses might
1177 computed by default as hash(text, p1, p2), however subclasses might
1177 use different hashing method (and override checkhash() in such case)
1178 use different hashing method (and override checkhash() in such case)
1178 """
1179 """
1179 if link == nullrev:
1180 if link == nullrev:
1180 raise RevlogError(_("attempted to add linkrev -1 to %s")
1181 raise RevlogError(_("attempted to add linkrev -1 to %s")
1181 % self.indexfile)
1182 % self.indexfile)
1182 node = node or self.hash(text, p1, p2)
1183 node = node or self.hash(text, p1, p2)
1183 if node in self.nodemap:
1184 if node in self.nodemap:
1184 return node
1185 return node
1185
1186
1186 dfh = None
1187 dfh = None
1187 if not self._inline:
1188 if not self._inline:
1188 dfh = self.opener(self.datafile, "a")
1189 dfh = self.opener(self.datafile, "a")
1189 ifh = self.opener(self.indexfile, "a+")
1190 ifh = self.opener(self.indexfile, "a+")
1190 try:
1191 try:
1191 return self._addrevision(node, text, transaction, link, p1, p2,
1192 return self._addrevision(node, text, transaction, link, p1, p2,
1192 REVIDX_DEFAULT_FLAGS, cachedelta, ifh, dfh)
1193 REVIDX_DEFAULT_FLAGS, cachedelta, ifh, dfh)
1193 finally:
1194 finally:
1194 if dfh:
1195 if dfh:
1195 dfh.close()
1196 dfh.close()
1196 ifh.close()
1197 ifh.close()
1197
1198
1198 def compress(self, text):
1199 def compress(self, text):
1199 """ generate a possibly-compressed representation of text """
1200 """ generate a possibly-compressed representation of text """
1200 if not text:
1201 if not text:
1201 return ("", text)
1202 return ("", text)
1202 l = len(text)
1203 l = len(text)
1203 bin = None
1204 bin = None
1204 if l < 44:
1205 if l < 44:
1205 pass
1206 pass
1206 elif l > 1000000:
1207 elif l > 1000000:
1207 # zlib makes an internal copy, thus doubling memory usage for
1208 # zlib makes an internal copy, thus doubling memory usage for
1208 # large files, so lets do this in pieces
1209 # large files, so lets do this in pieces
1209 z = zlib.compressobj()
1210 z = zlib.compressobj()
1210 p = []
1211 p = []
1211 pos = 0
1212 pos = 0
1212 while pos < l:
1213 while pos < l:
1213 pos2 = pos + 2**20
1214 pos2 = pos + 2**20
1214 p.append(z.compress(text[pos:pos2]))
1215 p.append(z.compress(text[pos:pos2]))
1215 pos = pos2
1216 pos = pos2
1216 p.append(z.flush())
1217 p.append(z.flush())
1217 if sum(map(len, p)) < l:
1218 if sum(map(len, p)) < l:
1218 bin = "".join(p)
1219 bin = "".join(p)
1219 else:
1220 else:
1220 bin = _compress(text)
1221 bin = _compress(text)
1221 if bin is None or len(bin) > l:
1222 if bin is None or len(bin) > l:
1222 if text[0] == '\0':
1223 if text[0] == '\0':
1223 return ("", text)
1224 return ("", text)
1224 return ('u', text)
1225 return ('u', text)
1225 return ("", bin)
1226 return ("", bin)
1226
1227
1227 def _addrevision(self, node, text, transaction, link, p1, p2, flags,
1228 def _addrevision(self, node, text, transaction, link, p1, p2, flags,
1228 cachedelta, ifh, dfh):
1229 cachedelta, ifh, dfh):
1229 """internal function to add revisions to the log
1230 """internal function to add revisions to the log
1230
1231
1231 see addrevision for argument descriptions.
1232 see addrevision for argument descriptions.
1232 invariants:
1233 invariants:
1233 - text is optional (can be None); if not set, cachedelta must be set.
1234 - text is optional (can be None); if not set, cachedelta must be set.
1234 if both are set, they must correspond to each other.
1235 if both are set, they must correspond to each other.
1235 """
1236 """
1236 btext = [text]
1237 btext = [text]
1237 def buildtext():
1238 def buildtext():
1238 if btext[0] is not None:
1239 if btext[0] is not None:
1239 return btext[0]
1240 return btext[0]
1240 # flush any pending writes here so we can read it in revision
1241 # flush any pending writes here so we can read it in revision
1241 if dfh:
1242 if dfh:
1242 dfh.flush()
1243 dfh.flush()
1243 ifh.flush()
1244 ifh.flush()
1244 baserev = cachedelta[0]
1245 baserev = cachedelta[0]
1245 delta = cachedelta[1]
1246 delta = cachedelta[1]
1246 # special case deltas which replace entire base; no need to decode
1247 # special case deltas which replace entire base; no need to decode
1247 # base revision. this neatly avoids censored bases, which throw when
1248 # base revision. this neatly avoids censored bases, which throw when
1248 # they're decoded.
1249 # they're decoded.
1249 hlen = struct.calcsize(">lll")
1250 hlen = struct.calcsize(">lll")
1250 if delta[:hlen] == mdiff.replacediffheader(self.rawsize(baserev),
1251 if delta[:hlen] == mdiff.replacediffheader(self.rawsize(baserev),
1251 len(delta) - hlen):
1252 len(delta) - hlen):
1252 btext[0] = delta[hlen:]
1253 btext[0] = delta[hlen:]
1253 else:
1254 else:
1254 basetext = self.revision(self.node(baserev))
1255 basetext = self.revision(self.node(baserev))
1255 btext[0] = mdiff.patch(basetext, delta)
1256 btext[0] = mdiff.patch(basetext, delta)
1256 try:
1257 try:
1257 self.checkhash(btext[0], p1, p2, node)
1258 self.checkhash(btext[0], p1, p2, node)
1258 if flags & REVIDX_ISCENSORED:
1259 if flags & REVIDX_ISCENSORED:
1259 raise RevlogError(_('node %s is not censored') % node)
1260 raise RevlogError(_('node %s is not censored') % node)
1260 except CensoredNodeError:
1261 except CensoredNodeError:
1261 # must pass the censored index flag to add censored revisions
1262 # must pass the censored index flag to add censored revisions
1262 if not flags & REVIDX_ISCENSORED:
1263 if not flags & REVIDX_ISCENSORED:
1263 raise
1264 raise
1264 return btext[0]
1265 return btext[0]
1265
1266
1266 def builddelta(rev):
1267 def builddelta(rev):
1267 # can we use the cached delta?
1268 # can we use the cached delta?
1268 if cachedelta and cachedelta[0] == rev:
1269 if cachedelta and cachedelta[0] == rev:
1269 delta = cachedelta[1]
1270 delta = cachedelta[1]
1270 else:
1271 else:
1271 t = buildtext()
1272 t = buildtext()
1272 if self.iscensored(rev):
1273 if self.iscensored(rev):
1273 # deltas based on a censored revision must replace the
1274 # deltas based on a censored revision must replace the
1274 # full content in one patch, so delta works everywhere
1275 # full content in one patch, so delta works everywhere
1275 header = mdiff.replacediffheader(self.rawsize(rev), len(t))
1276 header = mdiff.replacediffheader(self.rawsize(rev), len(t))
1276 delta = header + t
1277 delta = header + t
1277 else:
1278 else:
1278 ptext = self.revision(self.node(rev))
1279 ptext = self.revision(self.node(rev))
1279 delta = mdiff.textdiff(ptext, t)
1280 delta = mdiff.textdiff(ptext, t)
1280 data = self.compress(delta)
1281 data = self.compress(delta)
1281 l = len(data[1]) + len(data[0])
1282 l = len(data[1]) + len(data[0])
1282 if basecache[0] == rev:
1283 if basecache[0] == rev:
1283 chainbase = basecache[1]
1284 chainbase = basecache[1]
1284 else:
1285 else:
1285 chainbase = self.chainbase(rev)
1286 chainbase = self.chainbase(rev)
1286 dist = l + offset - self.start(chainbase)
1287 dist = l + offset - self.start(chainbase)
1287 if self._generaldelta:
1288 if self._generaldelta:
1288 base = rev
1289 base = rev
1289 else:
1290 else:
1290 base = chainbase
1291 base = chainbase
1291 chainlen, compresseddeltalen = self._chaininfo(rev)
1292 chainlen, compresseddeltalen = self._chaininfo(rev)
1292 chainlen += 1
1293 chainlen += 1
1293 compresseddeltalen += l
1294 compresseddeltalen += l
1294 return dist, l, data, base, chainbase, chainlen, compresseddeltalen
1295 return dist, l, data, base, chainbase, chainlen, compresseddeltalen
1295
1296
1296 curr = len(self)
1297 curr = len(self)
1297 prev = curr - 1
1298 prev = curr - 1
1298 base = chainbase = curr
1299 base = chainbase = curr
1299 chainlen = None
1300 chainlen = None
1300 offset = self.end(prev)
1301 offset = self.end(prev)
1301 d = None
1302 d = None
1302 if self._basecache is None:
1303 if self._basecache is None:
1303 self._basecache = (prev, self.chainbase(prev))
1304 self._basecache = (prev, self.chainbase(prev))
1304 basecache = self._basecache
1305 basecache = self._basecache
1305 p1r, p2r = self.rev(p1), self.rev(p2)
1306 p1r, p2r = self.rev(p1), self.rev(p2)
1306
1307
1307 # should we try to build a delta?
1308 # should we try to build a delta?
1308 if prev != nullrev:
1309 if prev != nullrev:
1309 if self._generaldelta:
1310 if self._generaldelta:
1310 if p1r >= basecache[1]:
1311 if p1r >= basecache[1]:
1311 d = builddelta(p1r)
1312 d = builddelta(p1r)
1312 elif p2r >= basecache[1]:
1313 elif p2r >= basecache[1]:
1313 d = builddelta(p2r)
1314 d = builddelta(p2r)
1314 else:
1315 else:
1315 d = builddelta(prev)
1316 d = builddelta(prev)
1316 else:
1317 else:
1317 d = builddelta(prev)
1318 d = builddelta(prev)
1318 dist, l, data, base, chainbase, chainlen, compresseddeltalen = d
1319 dist, l, data, base, chainbase, chainlen, compresseddeltalen = d
1319
1320
1320 # full versions are inserted when the needed deltas
1321 # full versions are inserted when the needed deltas
1321 # become comparable to the uncompressed text
1322 # become comparable to the uncompressed text
1322 if text is None:
1323 if text is None:
1323 textlen = mdiff.patchedsize(self.rawsize(cachedelta[0]),
1324 textlen = mdiff.patchedsize(self.rawsize(cachedelta[0]),
1324 cachedelta[1])
1325 cachedelta[1])
1325 else:
1326 else:
1326 textlen = len(text)
1327 textlen = len(text)
1327
1328
1328 # - 'dist' is the distance from the base revision -- bounding it limits
1329 # - 'dist' is the distance from the base revision -- bounding it limits
1329 # the amount of I/O we need to do.
1330 # the amount of I/O we need to do.
1330 # - 'compresseddeltalen' is the sum of the total size of deltas we need
1331 # - 'compresseddeltalen' is the sum of the total size of deltas we need
1331 # to apply -- bounding it limits the amount of CPU we consume.
1332 # to apply -- bounding it limits the amount of CPU we consume.
1332 if (d is None or dist > textlen * 4 or l > textlen or
1333 if (d is None or dist > textlen * 4 or l > textlen or
1333 compresseddeltalen > textlen * 2 or
1334 compresseddeltalen > textlen * 2 or
1334 (self._maxchainlen and chainlen > self._maxchainlen)):
1335 (self._maxchainlen and chainlen > self._maxchainlen)):
1335 text = buildtext()
1336 text = buildtext()
1336 data = self.compress(text)
1337 data = self.compress(text)
1337 l = len(data[1]) + len(data[0])
1338 l = len(data[1]) + len(data[0])
1338 base = chainbase = curr
1339 base = chainbase = curr
1339
1340
1340 e = (offset_type(offset, flags), l, textlen,
1341 e = (offset_type(offset, flags), l, textlen,
1341 base, link, p1r, p2r, node)
1342 base, link, p1r, p2r, node)
1342 self.index.insert(-1, e)
1343 self.index.insert(-1, e)
1343 self.nodemap[node] = curr
1344 self.nodemap[node] = curr
1344
1345
1345 entry = self._io.packentry(e, self.node, self.version, curr)
1346 entry = self._io.packentry(e, self.node, self.version, curr)
1346 self._writeentry(transaction, ifh, dfh, entry, data, link, offset)
1347 self._writeentry(transaction, ifh, dfh, entry, data, link, offset)
1347
1348
1348 if type(text) == str: # only accept immutable objects
1349 if type(text) == str: # only accept immutable objects
1349 self._cache = (node, curr, text)
1350 self._cache = (node, curr, text)
1350 self._basecache = (curr, chainbase)
1351 self._basecache = (curr, chainbase)
1351 return node
1352 return node
1352
1353
1353 def _writeentry(self, transaction, ifh, dfh, entry, data, link, offset):
1354 def _writeentry(self, transaction, ifh, dfh, entry, data, link, offset):
1354 curr = len(self) - 1
1355 curr = len(self) - 1
1355 if not self._inline:
1356 if not self._inline:
1356 transaction.add(self.datafile, offset)
1357 transaction.add(self.datafile, offset)
1357 transaction.add(self.indexfile, curr * len(entry))
1358 transaction.add(self.indexfile, curr * len(entry))
1358 if data[0]:
1359 if data[0]:
1359 dfh.write(data[0])
1360 dfh.write(data[0])
1360 dfh.write(data[1])
1361 dfh.write(data[1])
1361 dfh.flush()
1362 dfh.flush()
1362 ifh.write(entry)
1363 ifh.write(entry)
1363 else:
1364 else:
1364 offset += curr * self._io.size
1365 offset += curr * self._io.size
1365 transaction.add(self.indexfile, offset, curr)
1366 transaction.add(self.indexfile, offset, curr)
1366 ifh.write(entry)
1367 ifh.write(entry)
1367 ifh.write(data[0])
1368 ifh.write(data[0])
1368 ifh.write(data[1])
1369 ifh.write(data[1])
1369 self.checkinlinesize(transaction, ifh)
1370 self.checkinlinesize(transaction, ifh)
1370
1371
1371 def addgroup(self, bundle, linkmapper, transaction):
1372 def addgroup(self, bundle, linkmapper, transaction):
1372 """
1373 """
1373 add a delta group
1374 add a delta group
1374
1375
1375 given a set of deltas, add them to the revision log. the
1376 given a set of deltas, add them to the revision log. the
1376 first delta is against its parent, which should be in our
1377 first delta is against its parent, which should be in our
1377 log, the rest are against the previous delta.
1378 log, the rest are against the previous delta.
1378 """
1379 """
1379
1380
1380 # track the base of the current delta log
1381 # track the base of the current delta log
1381 content = []
1382 content = []
1382 node = None
1383 node = None
1383
1384
1384 r = len(self)
1385 r = len(self)
1385 end = 0
1386 end = 0
1386 if r:
1387 if r:
1387 end = self.end(r - 1)
1388 end = self.end(r - 1)
1388 ifh = self.opener(self.indexfile, "a+")
1389 ifh = self.opener(self.indexfile, "a+")
1389 isize = r * self._io.size
1390 isize = r * self._io.size
1390 if self._inline:
1391 if self._inline:
1391 transaction.add(self.indexfile, end + isize, r)
1392 transaction.add(self.indexfile, end + isize, r)
1392 dfh = None
1393 dfh = None
1393 else:
1394 else:
1394 transaction.add(self.indexfile, isize, r)
1395 transaction.add(self.indexfile, isize, r)
1395 transaction.add(self.datafile, end)
1396 transaction.add(self.datafile, end)
1396 dfh = self.opener(self.datafile, "a")
1397 dfh = self.opener(self.datafile, "a")
1397 def flush():
1398 def flush():
1398 if dfh:
1399 if dfh:
1399 dfh.flush()
1400 dfh.flush()
1400 ifh.flush()
1401 ifh.flush()
1401 try:
1402 try:
1402 # loop through our set of deltas
1403 # loop through our set of deltas
1403 chain = None
1404 chain = None
1404 while True:
1405 while True:
1405 chunkdata = bundle.deltachunk(chain)
1406 chunkdata = bundle.deltachunk(chain)
1406 if not chunkdata:
1407 if not chunkdata:
1407 break
1408 break
1408 node = chunkdata['node']
1409 node = chunkdata['node']
1409 p1 = chunkdata['p1']
1410 p1 = chunkdata['p1']
1410 p2 = chunkdata['p2']
1411 p2 = chunkdata['p2']
1411 cs = chunkdata['cs']
1412 cs = chunkdata['cs']
1412 deltabase = chunkdata['deltabase']
1413 deltabase = chunkdata['deltabase']
1413 delta = chunkdata['delta']
1414 delta = chunkdata['delta']
1414
1415
1415 content.append(node)
1416 content.append(node)
1416
1417
1417 link = linkmapper(cs)
1418 link = linkmapper(cs)
1418 if node in self.nodemap:
1419 if node in self.nodemap:
1419 # this can happen if two branches make the same change
1420 # this can happen if two branches make the same change
1420 chain = node
1421 chain = node
1421 continue
1422 continue
1422
1423
1423 for p in (p1, p2):
1424 for p in (p1, p2):
1424 if p not in self.nodemap:
1425 if p not in self.nodemap:
1425 raise LookupError(p, self.indexfile,
1426 raise LookupError(p, self.indexfile,
1426 _('unknown parent'))
1427 _('unknown parent'))
1427
1428
1428 if deltabase not in self.nodemap:
1429 if deltabase not in self.nodemap:
1429 raise LookupError(deltabase, self.indexfile,
1430 raise LookupError(deltabase, self.indexfile,
1430 _('unknown delta base'))
1431 _('unknown delta base'))
1431
1432
1432 baserev = self.rev(deltabase)
1433 baserev = self.rev(deltabase)
1433
1434
1434 if baserev != nullrev and self.iscensored(baserev):
1435 if baserev != nullrev and self.iscensored(baserev):
1435 # if base is censored, delta must be full replacement in a
1436 # if base is censored, delta must be full replacement in a
1436 # single patch operation
1437 # single patch operation
1437 hlen = struct.calcsize(">lll")
1438 hlen = struct.calcsize(">lll")
1438 oldlen = self.rawsize(baserev)
1439 oldlen = self.rawsize(baserev)
1439 newlen = len(delta) - hlen
1440 newlen = len(delta) - hlen
1440 if delta[:hlen] != mdiff.replacediffheader(oldlen, newlen):
1441 if delta[:hlen] != mdiff.replacediffheader(oldlen, newlen):
1441 raise error.CensoredBaseError(self.indexfile,
1442 raise error.CensoredBaseError(self.indexfile,
1442 self.node(baserev))
1443 self.node(baserev))
1443
1444
1444 flags = REVIDX_DEFAULT_FLAGS
1445 flags = REVIDX_DEFAULT_FLAGS
1445 if self._peek_iscensored(baserev, delta, flush):
1446 if self._peek_iscensored(baserev, delta, flush):
1446 flags |= REVIDX_ISCENSORED
1447 flags |= REVIDX_ISCENSORED
1447
1448
1448 chain = self._addrevision(node, None, transaction, link,
1449 chain = self._addrevision(node, None, transaction, link,
1449 p1, p2, flags, (baserev, delta),
1450 p1, p2, flags, (baserev, delta),
1450 ifh, dfh)
1451 ifh, dfh)
1451 if not dfh and not self._inline:
1452 if not dfh and not self._inline:
1452 # addrevision switched from inline to conventional
1453 # addrevision switched from inline to conventional
1453 # reopen the index
1454 # reopen the index
1454 ifh.close()
1455 ifh.close()
1455 dfh = self.opener(self.datafile, "a")
1456 dfh = self.opener(self.datafile, "a")
1456 ifh = self.opener(self.indexfile, "a")
1457 ifh = self.opener(self.indexfile, "a")
1457 finally:
1458 finally:
1458 if dfh:
1459 if dfh:
1459 dfh.close()
1460 dfh.close()
1460 ifh.close()
1461 ifh.close()
1461
1462
1462 return content
1463 return content
1463
1464
1464 def iscensored(self, rev):
1465 def iscensored(self, rev):
1465 """Check if a file revision is censored."""
1466 """Check if a file revision is censored."""
1466 return False
1467 return False
1467
1468
1468 def _peek_iscensored(self, baserev, delta, flush):
1469 def _peek_iscensored(self, baserev, delta, flush):
1469 """Quickly check if a delta produces a censored revision."""
1470 """Quickly check if a delta produces a censored revision."""
1470 return False
1471 return False
1471
1472
1472 def getstrippoint(self, minlink):
1473 def getstrippoint(self, minlink):
1473 """find the minimum rev that must be stripped to strip the linkrev
1474 """find the minimum rev that must be stripped to strip the linkrev
1474
1475
1475 Returns a tuple containing the minimum rev and a set of all revs that
1476 Returns a tuple containing the minimum rev and a set of all revs that
1476 have linkrevs that will be broken by this strip.
1477 have linkrevs that will be broken by this strip.
1477 """
1478 """
1478 brokenrevs = set()
1479 brokenrevs = set()
1479 strippoint = len(self)
1480 strippoint = len(self)
1480
1481
1481 heads = {}
1482 heads = {}
1482 futurelargelinkrevs = set()
1483 futurelargelinkrevs = set()
1483 for head in self.headrevs():
1484 for head in self.headrevs():
1484 headlinkrev = self.linkrev(head)
1485 headlinkrev = self.linkrev(head)
1485 heads[head] = headlinkrev
1486 heads[head] = headlinkrev
1486 if headlinkrev >= minlink:
1487 if headlinkrev >= minlink:
1487 futurelargelinkrevs.add(headlinkrev)
1488 futurelargelinkrevs.add(headlinkrev)
1488
1489
1489 # This algorithm involves walking down the rev graph, starting at the
1490 # This algorithm involves walking down the rev graph, starting at the
1490 # heads. Since the revs are topologically sorted according to linkrev,
1491 # heads. Since the revs are topologically sorted according to linkrev,
1491 # once all head linkrevs are below the minlink, we know there are
1492 # once all head linkrevs are below the minlink, we know there are
1492 # no more revs that could have a linkrev greater than minlink.
1493 # no more revs that could have a linkrev greater than minlink.
1493 # So we can stop walking.
1494 # So we can stop walking.
1494 while futurelargelinkrevs:
1495 while futurelargelinkrevs:
1495 strippoint -= 1
1496 strippoint -= 1
1496 linkrev = heads.pop(strippoint)
1497 linkrev = heads.pop(strippoint)
1497
1498
1498 if linkrev < minlink:
1499 if linkrev < minlink:
1499 brokenrevs.add(strippoint)
1500 brokenrevs.add(strippoint)
1500 else:
1501 else:
1501 futurelargelinkrevs.remove(linkrev)
1502 futurelargelinkrevs.remove(linkrev)
1502
1503
1503 for p in self.parentrevs(strippoint):
1504 for p in self.parentrevs(strippoint):
1504 if p != nullrev:
1505 if p != nullrev:
1505 plinkrev = self.linkrev(p)
1506 plinkrev = self.linkrev(p)
1506 heads[p] = plinkrev
1507 heads[p] = plinkrev
1507 if plinkrev >= minlink:
1508 if plinkrev >= minlink:
1508 futurelargelinkrevs.add(plinkrev)
1509 futurelargelinkrevs.add(plinkrev)
1509
1510
1510 return strippoint, brokenrevs
1511 return strippoint, brokenrevs
1511
1512
1512 def strip(self, minlink, transaction):
1513 def strip(self, minlink, transaction):
1513 """truncate the revlog on the first revision with a linkrev >= minlink
1514 """truncate the revlog on the first revision with a linkrev >= minlink
1514
1515
1515 This function is called when we're stripping revision minlink and
1516 This function is called when we're stripping revision minlink and
1516 its descendants from the repository.
1517 its descendants from the repository.
1517
1518
1518 We have to remove all revisions with linkrev >= minlink, because
1519 We have to remove all revisions with linkrev >= minlink, because
1519 the equivalent changelog revisions will be renumbered after the
1520 the equivalent changelog revisions will be renumbered after the
1520 strip.
1521 strip.
1521
1522
1522 So we truncate the revlog on the first of these revisions, and
1523 So we truncate the revlog on the first of these revisions, and
1523 trust that the caller has saved the revisions that shouldn't be
1524 trust that the caller has saved the revisions that shouldn't be
1524 removed and that it'll re-add them after this truncation.
1525 removed and that it'll re-add them after this truncation.
1525 """
1526 """
1526 if len(self) == 0:
1527 if len(self) == 0:
1527 return
1528 return
1528
1529
1529 rev, _ = self.getstrippoint(minlink)
1530 rev, _ = self.getstrippoint(minlink)
1530 if rev == len(self):
1531 if rev == len(self):
1531 return
1532 return
1532
1533
1533 # first truncate the files on disk
1534 # first truncate the files on disk
1534 end = self.start(rev)
1535 end = self.start(rev)
1535 if not self._inline:
1536 if not self._inline:
1536 transaction.add(self.datafile, end)
1537 transaction.add(self.datafile, end)
1537 end = rev * self._io.size
1538 end = rev * self._io.size
1538 else:
1539 else:
1539 end += rev * self._io.size
1540 end += rev * self._io.size
1540
1541
1541 transaction.add(self.indexfile, end)
1542 transaction.add(self.indexfile, end)
1542
1543
1543 # then reset internal state in memory to forget those revisions
1544 # then reset internal state in memory to forget those revisions
1544 self._cache = None
1545 self._cache = None
1545 self._chaininfocache = {}
1546 self._chaininfocache = {}
1546 self._chunkclear()
1547 self._chunkclear()
1547 for x in xrange(rev, len(self)):
1548 for x in xrange(rev, len(self)):
1548 del self.nodemap[self.node(x)]
1549 del self.nodemap[self.node(x)]
1549
1550
1550 del self.index[rev:-1]
1551 del self.index[rev:-1]
1551
1552
1552 def checksize(self):
1553 def checksize(self):
1553 expected = 0
1554 expected = 0
1554 if len(self):
1555 if len(self):
1555 expected = max(0, self.end(len(self) - 1))
1556 expected = max(0, self.end(len(self) - 1))
1556
1557
1557 try:
1558 try:
1558 f = self.opener(self.datafile)
1559 f = self.opener(self.datafile)
1559 f.seek(0, 2)
1560 f.seek(0, 2)
1560 actual = f.tell()
1561 actual = f.tell()
1561 f.close()
1562 f.close()
1562 dd = actual - expected
1563 dd = actual - expected
1563 except IOError, inst:
1564 except IOError, inst:
1564 if inst.errno != errno.ENOENT:
1565 if inst.errno != errno.ENOENT:
1565 raise
1566 raise
1566 dd = 0
1567 dd = 0
1567
1568
1568 try:
1569 try:
1569 f = self.opener(self.indexfile)
1570 f = self.opener(self.indexfile)
1570 f.seek(0, 2)
1571 f.seek(0, 2)
1571 actual = f.tell()
1572 actual = f.tell()
1572 f.close()
1573 f.close()
1573 s = self._io.size
1574 s = self._io.size
1574 i = max(0, actual // s)
1575 i = max(0, actual // s)
1575 di = actual - (i * s)
1576 di = actual - (i * s)
1576 if self._inline:
1577 if self._inline:
1577 databytes = 0
1578 databytes = 0
1578 for r in self:
1579 for r in self:
1579 databytes += max(0, self.length(r))
1580 databytes += max(0, self.length(r))
1580 dd = 0
1581 dd = 0
1581 di = actual - len(self) * s - databytes
1582 di = actual - len(self) * s - databytes
1582 except IOError, inst:
1583 except IOError, inst:
1583 if inst.errno != errno.ENOENT:
1584 if inst.errno != errno.ENOENT:
1584 raise
1585 raise
1585 di = 0
1586 di = 0
1586
1587
1587 return (dd, di)
1588 return (dd, di)
1588
1589
1589 def files(self):
1590 def files(self):
1590 res = [self.indexfile]
1591 res = [self.indexfile]
1591 if not self._inline:
1592 if not self._inline:
1592 res.append(self.datafile)
1593 res.append(self.datafile)
1593 return res
1594 return res
@@ -1,250 +1,251 b''
1 # setdiscovery.py - improved discovery of common nodeset for mercurial
1 # setdiscovery.py - improved discovery of common nodeset for mercurial
2 #
2 #
3 # Copyright 2010 Benoit Boissinot <bboissin@gmail.com>
3 # Copyright 2010 Benoit Boissinot <bboissin@gmail.com>
4 # and Peter Arrenbrecht <peter@arrenbrecht.ch>
4 # and Peter Arrenbrecht <peter@arrenbrecht.ch>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8 """
8 """
9 Algorithm works in the following way. You have two repository: local and
9 Algorithm works in the following way. You have two repository: local and
10 remote. They both contains a DAG of changelists.
10 remote. They both contains a DAG of changelists.
11
11
12 The goal of the discovery protocol is to find one set of node *common*,
12 The goal of the discovery protocol is to find one set of node *common*,
13 the set of nodes shared by local and remote.
13 the set of nodes shared by local and remote.
14
14
15 One of the issue with the original protocol was latency, it could
15 One of the issue with the original protocol was latency, it could
16 potentially require lots of roundtrips to discover that the local repo was a
16 potentially require lots of roundtrips to discover that the local repo was a
17 subset of remote (which is a very common case, you usually have few changes
17 subset of remote (which is a very common case, you usually have few changes
18 compared to upstream, while upstream probably had lots of development).
18 compared to upstream, while upstream probably had lots of development).
19
19
20 The new protocol only requires one interface for the remote repo: `known()`,
20 The new protocol only requires one interface for the remote repo: `known()`,
21 which given a set of changelists tells you if they are present in the DAG.
21 which given a set of changelists tells you if they are present in the DAG.
22
22
23 The algorithm then works as follow:
23 The algorithm then works as follow:
24
24
25 - We will be using three sets, `common`, `missing`, `unknown`. Originally
25 - We will be using three sets, `common`, `missing`, `unknown`. Originally
26 all nodes are in `unknown`.
26 all nodes are in `unknown`.
27 - Take a sample from `unknown`, call `remote.known(sample)`
27 - Take a sample from `unknown`, call `remote.known(sample)`
28 - For each node that remote knows, move it and all its ancestors to `common`
28 - For each node that remote knows, move it and all its ancestors to `common`
29 - For each node that remote doesn't know, move it and all its descendants
29 - For each node that remote doesn't know, move it and all its descendants
30 to `missing`
30 to `missing`
31 - Iterate until `unknown` is empty
31 - Iterate until `unknown` is empty
32
32
33 There are a couple optimizations, first is instead of starting with a random
33 There are a couple optimizations, first is instead of starting with a random
34 sample of missing, start by sending all heads, in the case where the local
34 sample of missing, start by sending all heads, in the case where the local
35 repo is a subset, you computed the answer in one round trip.
35 repo is a subset, you computed the answer in one round trip.
36
36
37 Then you can do something similar to the bisecting strategy used when
37 Then you can do something similar to the bisecting strategy used when
38 finding faulty changesets. Instead of random samples, you can try picking
38 finding faulty changesets. Instead of random samples, you can try picking
39 nodes that will maximize the number of nodes that will be
39 nodes that will maximize the number of nodes that will be
40 classified with it (since all ancestors or descendants will be marked as well).
40 classified with it (since all ancestors or descendants will be marked as well).
41 """
41 """
42
42
43 import collections
43 from node import nullid, nullrev
44 from node import nullid, nullrev
44 from i18n import _
45 from i18n import _
45 import random
46 import random
46 import util, dagutil
47 import util, dagutil
47
48
48 def _updatesample(dag, nodes, sample, quicksamplesize=0):
49 def _updatesample(dag, nodes, sample, quicksamplesize=0):
49 """update an existing sample to match the expected size
50 """update an existing sample to match the expected size
50
51
51 The sample is updated with nodes exponentially distant from each head of the
52 The sample is updated with nodes exponentially distant from each head of the
52 <nodes> set. (H~1, H~2, H~4, H~8, etc).
53 <nodes> set. (H~1, H~2, H~4, H~8, etc).
53
54
54 If a target size is specified, the sampling will stop once this size is
55 If a target size is specified, the sampling will stop once this size is
55 reached. Otherwise sampling will happen until roots of the <nodes> set are
56 reached. Otherwise sampling will happen until roots of the <nodes> set are
56 reached.
57 reached.
57
58
58 :dag: a dag object from dagutil
59 :dag: a dag object from dagutil
59 :nodes: set of nodes we want to discover (if None, assume the whole dag)
60 :nodes: set of nodes we want to discover (if None, assume the whole dag)
60 :sample: a sample to update
61 :sample: a sample to update
61 :quicksamplesize: optional target size of the sample"""
62 :quicksamplesize: optional target size of the sample"""
62 # if nodes is empty we scan the entire graph
63 # if nodes is empty we scan the entire graph
63 if nodes:
64 if nodes:
64 heads = dag.headsetofconnecteds(nodes)
65 heads = dag.headsetofconnecteds(nodes)
65 else:
66 else:
66 heads = dag.heads()
67 heads = dag.heads()
67 dist = {}
68 dist = {}
68 visit = util.deque(heads)
69 visit = collections.deque(heads)
69 seen = set()
70 seen = set()
70 factor = 1
71 factor = 1
71 while visit:
72 while visit:
72 curr = visit.popleft()
73 curr = visit.popleft()
73 if curr in seen:
74 if curr in seen:
74 continue
75 continue
75 d = dist.setdefault(curr, 1)
76 d = dist.setdefault(curr, 1)
76 if d > factor:
77 if d > factor:
77 factor *= 2
78 factor *= 2
78 if d == factor:
79 if d == factor:
79 sample.add(curr)
80 sample.add(curr)
80 if quicksamplesize and (len(sample) >= quicksamplesize):
81 if quicksamplesize and (len(sample) >= quicksamplesize):
81 return
82 return
82 seen.add(curr)
83 seen.add(curr)
83 for p in dag.parents(curr):
84 for p in dag.parents(curr):
84 if not nodes or p in nodes:
85 if not nodes or p in nodes:
85 dist.setdefault(p, d + 1)
86 dist.setdefault(p, d + 1)
86 visit.append(p)
87 visit.append(p)
87
88
88 def _takequicksample(dag, nodes, size):
89 def _takequicksample(dag, nodes, size):
89 """takes a quick sample of size <size>
90 """takes a quick sample of size <size>
90
91
91 It is meant for initial sampling and focuses on querying heads and close
92 It is meant for initial sampling and focuses on querying heads and close
92 ancestors of heads.
93 ancestors of heads.
93
94
94 :dag: a dag object
95 :dag: a dag object
95 :nodes: set of nodes to discover
96 :nodes: set of nodes to discover
96 :size: the maximum size of the sample"""
97 :size: the maximum size of the sample"""
97 sample = dag.headsetofconnecteds(nodes)
98 sample = dag.headsetofconnecteds(nodes)
98 if size <= len(sample):
99 if size <= len(sample):
99 return _limitsample(sample, size)
100 return _limitsample(sample, size)
100 _updatesample(dag, None, sample, quicksamplesize=size)
101 _updatesample(dag, None, sample, quicksamplesize=size)
101 return sample
102 return sample
102
103
103 def _takefullsample(dag, nodes, size):
104 def _takefullsample(dag, nodes, size):
104 sample = dag.headsetofconnecteds(nodes)
105 sample = dag.headsetofconnecteds(nodes)
105 # update from heads
106 # update from heads
106 _updatesample(dag, nodes, sample)
107 _updatesample(dag, nodes, sample)
107 # update from roots
108 # update from roots
108 _updatesample(dag.inverse(), nodes, sample)
109 _updatesample(dag.inverse(), nodes, sample)
109 assert sample
110 assert sample
110 sample = _limitsample(sample, size)
111 sample = _limitsample(sample, size)
111 if len(sample) < size:
112 if len(sample) < size:
112 more = size - len(sample)
113 more = size - len(sample)
113 sample.update(random.sample(list(nodes - sample), more))
114 sample.update(random.sample(list(nodes - sample), more))
114 return sample
115 return sample
115
116
116 def _limitsample(sample, desiredlen):
117 def _limitsample(sample, desiredlen):
117 """return a random subset of sample of at most desiredlen item"""
118 """return a random subset of sample of at most desiredlen item"""
118 if len(sample) > desiredlen:
119 if len(sample) > desiredlen:
119 sample = set(random.sample(sample, desiredlen))
120 sample = set(random.sample(sample, desiredlen))
120 return sample
121 return sample
121
122
122 def findcommonheads(ui, local, remote,
123 def findcommonheads(ui, local, remote,
123 initialsamplesize=100,
124 initialsamplesize=100,
124 fullsamplesize=200,
125 fullsamplesize=200,
125 abortwhenunrelated=True):
126 abortwhenunrelated=True):
126 '''Return a tuple (common, anyincoming, remoteheads) used to identify
127 '''Return a tuple (common, anyincoming, remoteheads) used to identify
127 missing nodes from or in remote.
128 missing nodes from or in remote.
128 '''
129 '''
129 roundtrips = 0
130 roundtrips = 0
130 cl = local.changelog
131 cl = local.changelog
131 dag = dagutil.revlogdag(cl)
132 dag = dagutil.revlogdag(cl)
132
133
133 # early exit if we know all the specified remote heads already
134 # early exit if we know all the specified remote heads already
134 ui.debug("query 1; heads\n")
135 ui.debug("query 1; heads\n")
135 roundtrips += 1
136 roundtrips += 1
136 ownheads = dag.heads()
137 ownheads = dag.heads()
137 sample = _limitsample(ownheads, initialsamplesize)
138 sample = _limitsample(ownheads, initialsamplesize)
138 # indices between sample and externalized version must match
139 # indices between sample and externalized version must match
139 sample = list(sample)
140 sample = list(sample)
140 if remote.local():
141 if remote.local():
141 # stopgap until we have a proper localpeer that supports batch()
142 # stopgap until we have a proper localpeer that supports batch()
142 srvheadhashes = remote.heads()
143 srvheadhashes = remote.heads()
143 yesno = remote.known(dag.externalizeall(sample))
144 yesno = remote.known(dag.externalizeall(sample))
144 elif remote.capable('batch'):
145 elif remote.capable('batch'):
145 batch = remote.batch()
146 batch = remote.batch()
146 srvheadhashesref = batch.heads()
147 srvheadhashesref = batch.heads()
147 yesnoref = batch.known(dag.externalizeall(sample))
148 yesnoref = batch.known(dag.externalizeall(sample))
148 batch.submit()
149 batch.submit()
149 srvheadhashes = srvheadhashesref.value
150 srvheadhashes = srvheadhashesref.value
150 yesno = yesnoref.value
151 yesno = yesnoref.value
151 else:
152 else:
152 # compatibility with pre-batch, but post-known remotes during 1.9
153 # compatibility with pre-batch, but post-known remotes during 1.9
153 # development
154 # development
154 srvheadhashes = remote.heads()
155 srvheadhashes = remote.heads()
155 sample = []
156 sample = []
156
157
157 if cl.tip() == nullid:
158 if cl.tip() == nullid:
158 if srvheadhashes != [nullid]:
159 if srvheadhashes != [nullid]:
159 return [nullid], True, srvheadhashes
160 return [nullid], True, srvheadhashes
160 return [nullid], False, []
161 return [nullid], False, []
161
162
162 # start actual discovery (we note this before the next "if" for
163 # start actual discovery (we note this before the next "if" for
163 # compatibility reasons)
164 # compatibility reasons)
164 ui.status(_("searching for changes\n"))
165 ui.status(_("searching for changes\n"))
165
166
166 srvheads = dag.internalizeall(srvheadhashes, filterunknown=True)
167 srvheads = dag.internalizeall(srvheadhashes, filterunknown=True)
167 if len(srvheads) == len(srvheadhashes):
168 if len(srvheads) == len(srvheadhashes):
168 ui.debug("all remote heads known locally\n")
169 ui.debug("all remote heads known locally\n")
169 return (srvheadhashes, False, srvheadhashes,)
170 return (srvheadhashes, False, srvheadhashes,)
170
171
171 if sample and len(ownheads) <= initialsamplesize and util.all(yesno):
172 if sample and len(ownheads) <= initialsamplesize and util.all(yesno):
172 ui.note(_("all local heads known remotely\n"))
173 ui.note(_("all local heads known remotely\n"))
173 ownheadhashes = dag.externalizeall(ownheads)
174 ownheadhashes = dag.externalizeall(ownheads)
174 return (ownheadhashes, True, srvheadhashes,)
175 return (ownheadhashes, True, srvheadhashes,)
175
176
176 # full blown discovery
177 # full blown discovery
177
178
178 # own nodes I know we both know
179 # own nodes I know we both know
179 # treat remote heads (and maybe own heads) as a first implicit sample
180 # treat remote heads (and maybe own heads) as a first implicit sample
180 # response
181 # response
181 common = cl.incrementalmissingrevs(srvheads)
182 common = cl.incrementalmissingrevs(srvheads)
182 commoninsample = set(n for i, n in enumerate(sample) if yesno[i])
183 commoninsample = set(n for i, n in enumerate(sample) if yesno[i])
183 common.addbases(commoninsample)
184 common.addbases(commoninsample)
184 # own nodes where I don't know if remote knows them
185 # own nodes where I don't know if remote knows them
185 undecided = set(common.missingancestors(ownheads))
186 undecided = set(common.missingancestors(ownheads))
186 # own nodes I know remote lacks
187 # own nodes I know remote lacks
187 missing = set()
188 missing = set()
188
189
189 full = False
190 full = False
190 while undecided:
191 while undecided:
191
192
192 if sample:
193 if sample:
193 missinginsample = [n for i, n in enumerate(sample) if not yesno[i]]
194 missinginsample = [n for i, n in enumerate(sample) if not yesno[i]]
194 missing.update(dag.descendantset(missinginsample, missing))
195 missing.update(dag.descendantset(missinginsample, missing))
195
196
196 undecided.difference_update(missing)
197 undecided.difference_update(missing)
197
198
198 if not undecided:
199 if not undecided:
199 break
200 break
200
201
201 if full or common.hasbases():
202 if full or common.hasbases():
202 if full:
203 if full:
203 ui.note(_("sampling from both directions\n"))
204 ui.note(_("sampling from both directions\n"))
204 else:
205 else:
205 ui.debug("taking initial sample\n")
206 ui.debug("taking initial sample\n")
206 samplefunc = _takefullsample
207 samplefunc = _takefullsample
207 targetsize = fullsamplesize
208 targetsize = fullsamplesize
208 else:
209 else:
209 # use even cheaper initial sample
210 # use even cheaper initial sample
210 ui.debug("taking quick initial sample\n")
211 ui.debug("taking quick initial sample\n")
211 samplefunc = _takequicksample
212 samplefunc = _takequicksample
212 targetsize = initialsamplesize
213 targetsize = initialsamplesize
213 if len(undecided) < targetsize:
214 if len(undecided) < targetsize:
214 sample = list(undecided)
215 sample = list(undecided)
215 else:
216 else:
216 sample = samplefunc(dag, undecided, targetsize)
217 sample = samplefunc(dag, undecided, targetsize)
217 sample = _limitsample(sample, targetsize)
218 sample = _limitsample(sample, targetsize)
218
219
219 roundtrips += 1
220 roundtrips += 1
220 ui.progress(_('searching'), roundtrips, unit=_('queries'))
221 ui.progress(_('searching'), roundtrips, unit=_('queries'))
221 ui.debug("query %i; still undecided: %i, sample size is: %i\n"
222 ui.debug("query %i; still undecided: %i, sample size is: %i\n"
222 % (roundtrips, len(undecided), len(sample)))
223 % (roundtrips, len(undecided), len(sample)))
223 # indices between sample and externalized version must match
224 # indices between sample and externalized version must match
224 sample = list(sample)
225 sample = list(sample)
225 yesno = remote.known(dag.externalizeall(sample))
226 yesno = remote.known(dag.externalizeall(sample))
226 full = True
227 full = True
227
228
228 if sample:
229 if sample:
229 commoninsample = set(n for i, n in enumerate(sample) if yesno[i])
230 commoninsample = set(n for i, n in enumerate(sample) if yesno[i])
230 common.addbases(commoninsample)
231 common.addbases(commoninsample)
231 common.removeancestorsfrom(undecided)
232 common.removeancestorsfrom(undecided)
232
233
233 # heads(common) == heads(common.bases) since common represents common.bases
234 # heads(common) == heads(common.bases) since common represents common.bases
234 # and all its ancestors
235 # and all its ancestors
235 result = dag.headsetofconnecteds(common.bases)
236 result = dag.headsetofconnecteds(common.bases)
236 # common.bases can include nullrev, but our contract requires us to not
237 # common.bases can include nullrev, but our contract requires us to not
237 # return any heads in that case, so discard that
238 # return any heads in that case, so discard that
238 result.discard(nullrev)
239 result.discard(nullrev)
239 ui.progress(_('searching'), None)
240 ui.progress(_('searching'), None)
240 ui.debug("%d total queries\n" % roundtrips)
241 ui.debug("%d total queries\n" % roundtrips)
241
242
242 if not result and srvheadhashes != [nullid]:
243 if not result and srvheadhashes != [nullid]:
243 if abortwhenunrelated:
244 if abortwhenunrelated:
244 raise util.Abort(_("repository is unrelated"))
245 raise util.Abort(_("repository is unrelated"))
245 else:
246 else:
246 ui.warn(_("warning: repository is unrelated\n"))
247 ui.warn(_("warning: repository is unrelated\n"))
247 return (set([nullid]), True, srvheadhashes,)
248 return (set([nullid]), True, srvheadhashes,)
248
249
249 anyincoming = (srvheadhashes != [nullid])
250 anyincoming = (srvheadhashes != [nullid])
250 return dag.externalizeall(result), anyincoming, srvheadhashes
251 return dag.externalizeall(result), anyincoming, srvheadhashes
@@ -1,150 +1,151 b''
1 # discovery.py - protocol changeset discovery functions
1 # discovery.py - protocol changeset discovery functions
2 #
2 #
3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import collections
8 from node import nullid, short
9 from node import nullid, short
9 from i18n import _
10 from i18n import _
10 import util, error
11 import util, error
11
12
12 def findcommonincoming(repo, remote, heads=None, force=False):
13 def findcommonincoming(repo, remote, heads=None, force=False):
13 """Return a tuple (common, fetch, heads) used to identify the common
14 """Return a tuple (common, fetch, heads) used to identify the common
14 subset of nodes between repo and remote.
15 subset of nodes between repo and remote.
15
16
16 "common" is a list of (at least) the heads of the common subset.
17 "common" is a list of (at least) the heads of the common subset.
17 "fetch" is a list of roots of the nodes that would be incoming, to be
18 "fetch" is a list of roots of the nodes that would be incoming, to be
18 supplied to changegroupsubset.
19 supplied to changegroupsubset.
19 "heads" is either the supplied heads, or else the remote's heads.
20 "heads" is either the supplied heads, or else the remote's heads.
20 """
21 """
21
22
22 knownnode = repo.changelog.hasnode
23 knownnode = repo.changelog.hasnode
23 search = []
24 search = []
24 fetch = set()
25 fetch = set()
25 seen = set()
26 seen = set()
26 seenbranch = set()
27 seenbranch = set()
27 base = set()
28 base = set()
28
29
29 if not heads:
30 if not heads:
30 heads = remote.heads()
31 heads = remote.heads()
31
32
32 if repo.changelog.tip() == nullid:
33 if repo.changelog.tip() == nullid:
33 base.add(nullid)
34 base.add(nullid)
34 if heads != [nullid]:
35 if heads != [nullid]:
35 return [nullid], [nullid], list(heads)
36 return [nullid], [nullid], list(heads)
36 return [nullid], [], heads
37 return [nullid], [], heads
37
38
38 # assume we're closer to the tip than the root
39 # assume we're closer to the tip than the root
39 # and start by examining the heads
40 # and start by examining the heads
40 repo.ui.status(_("searching for changes\n"))
41 repo.ui.status(_("searching for changes\n"))
41
42
42 unknown = []
43 unknown = []
43 for h in heads:
44 for h in heads:
44 if not knownnode(h):
45 if not knownnode(h):
45 unknown.append(h)
46 unknown.append(h)
46 else:
47 else:
47 base.add(h)
48 base.add(h)
48
49
49 if not unknown:
50 if not unknown:
50 return list(base), [], list(heads)
51 return list(base), [], list(heads)
51
52
52 req = set(unknown)
53 req = set(unknown)
53 reqcnt = 0
54 reqcnt = 0
54
55
55 # search through remote branches
56 # search through remote branches
56 # a 'branch' here is a linear segment of history, with four parts:
57 # a 'branch' here is a linear segment of history, with four parts:
57 # head, root, first parent, second parent
58 # head, root, first parent, second parent
58 # (a branch always has two parents (or none) by definition)
59 # (a branch always has two parents (or none) by definition)
59 unknown = util.deque(remote.branches(unknown))
60 unknown = collections.deque(remote.branches(unknown))
60 while unknown:
61 while unknown:
61 r = []
62 r = []
62 while unknown:
63 while unknown:
63 n = unknown.popleft()
64 n = unknown.popleft()
64 if n[0] in seen:
65 if n[0] in seen:
65 continue
66 continue
66
67
67 repo.ui.debug("examining %s:%s\n"
68 repo.ui.debug("examining %s:%s\n"
68 % (short(n[0]), short(n[1])))
69 % (short(n[0]), short(n[1])))
69 if n[0] == nullid: # found the end of the branch
70 if n[0] == nullid: # found the end of the branch
70 pass
71 pass
71 elif n in seenbranch:
72 elif n in seenbranch:
72 repo.ui.debug("branch already found\n")
73 repo.ui.debug("branch already found\n")
73 continue
74 continue
74 elif n[1] and knownnode(n[1]): # do we know the base?
75 elif n[1] and knownnode(n[1]): # do we know the base?
75 repo.ui.debug("found incomplete branch %s:%s\n"
76 repo.ui.debug("found incomplete branch %s:%s\n"
76 % (short(n[0]), short(n[1])))
77 % (short(n[0]), short(n[1])))
77 search.append(n[0:2]) # schedule branch range for scanning
78 search.append(n[0:2]) # schedule branch range for scanning
78 seenbranch.add(n)
79 seenbranch.add(n)
79 else:
80 else:
80 if n[1] not in seen and n[1] not in fetch:
81 if n[1] not in seen and n[1] not in fetch:
81 if knownnode(n[2]) and knownnode(n[3]):
82 if knownnode(n[2]) and knownnode(n[3]):
82 repo.ui.debug("found new changeset %s\n" %
83 repo.ui.debug("found new changeset %s\n" %
83 short(n[1]))
84 short(n[1]))
84 fetch.add(n[1]) # earliest unknown
85 fetch.add(n[1]) # earliest unknown
85 for p in n[2:4]:
86 for p in n[2:4]:
86 if knownnode(p):
87 if knownnode(p):
87 base.add(p) # latest known
88 base.add(p) # latest known
88
89
89 for p in n[2:4]:
90 for p in n[2:4]:
90 if p not in req and not knownnode(p):
91 if p not in req and not knownnode(p):
91 r.append(p)
92 r.append(p)
92 req.add(p)
93 req.add(p)
93 seen.add(n[0])
94 seen.add(n[0])
94
95
95 if r:
96 if r:
96 reqcnt += 1
97 reqcnt += 1
97 repo.ui.progress(_('searching'), reqcnt, unit=_('queries'))
98 repo.ui.progress(_('searching'), reqcnt, unit=_('queries'))
98 repo.ui.debug("request %d: %s\n" %
99 repo.ui.debug("request %d: %s\n" %
99 (reqcnt, " ".join(map(short, r))))
100 (reqcnt, " ".join(map(short, r))))
100 for p in xrange(0, len(r), 10):
101 for p in xrange(0, len(r), 10):
101 for b in remote.branches(r[p:p + 10]):
102 for b in remote.branches(r[p:p + 10]):
102 repo.ui.debug("received %s:%s\n" %
103 repo.ui.debug("received %s:%s\n" %
103 (short(b[0]), short(b[1])))
104 (short(b[0]), short(b[1])))
104 unknown.append(b)
105 unknown.append(b)
105
106
106 # do binary search on the branches we found
107 # do binary search on the branches we found
107 while search:
108 while search:
108 newsearch = []
109 newsearch = []
109 reqcnt += 1
110 reqcnt += 1
110 repo.ui.progress(_('searching'), reqcnt, unit=_('queries'))
111 repo.ui.progress(_('searching'), reqcnt, unit=_('queries'))
111 for n, l in zip(search, remote.between(search)):
112 for n, l in zip(search, remote.between(search)):
112 l.append(n[1])
113 l.append(n[1])
113 p = n[0]
114 p = n[0]
114 f = 1
115 f = 1
115 for i in l:
116 for i in l:
116 repo.ui.debug("narrowing %d:%d %s\n" % (f, len(l), short(i)))
117 repo.ui.debug("narrowing %d:%d %s\n" % (f, len(l), short(i)))
117 if knownnode(i):
118 if knownnode(i):
118 if f <= 2:
119 if f <= 2:
119 repo.ui.debug("found new branch changeset %s\n" %
120 repo.ui.debug("found new branch changeset %s\n" %
120 short(p))
121 short(p))
121 fetch.add(p)
122 fetch.add(p)
122 base.add(i)
123 base.add(i)
123 else:
124 else:
124 repo.ui.debug("narrowed branch search to %s:%s\n"
125 repo.ui.debug("narrowed branch search to %s:%s\n"
125 % (short(p), short(i)))
126 % (short(p), short(i)))
126 newsearch.append((p, i))
127 newsearch.append((p, i))
127 break
128 break
128 p, f = i, f * 2
129 p, f = i, f * 2
129 search = newsearch
130 search = newsearch
130
131
131 # sanity check our fetch list
132 # sanity check our fetch list
132 for f in fetch:
133 for f in fetch:
133 if knownnode(f):
134 if knownnode(f):
134 raise error.RepoError(_("already have changeset ")
135 raise error.RepoError(_("already have changeset ")
135 + short(f[:4]))
136 + short(f[:4]))
136
137
137 base = list(base)
138 base = list(base)
138 if base == [nullid]:
139 if base == [nullid]:
139 if force:
140 if force:
140 repo.ui.warn(_("warning: repository is unrelated\n"))
141 repo.ui.warn(_("warning: repository is unrelated\n"))
141 else:
142 else:
142 raise util.Abort(_("repository is unrelated"))
143 raise util.Abort(_("repository is unrelated"))
143
144
144 repo.ui.debug("found new changesets starting at " +
145 repo.ui.debug("found new changesets starting at " +
145 " ".join([short(f) for f in fetch]) + "\n")
146 " ".join([short(f) for f in fetch]) + "\n")
146
147
147 repo.ui.progress(_('searching'), None)
148 repo.ui.progress(_('searching'), None)
148 repo.ui.debug("%d total queries\n" % reqcnt)
149 repo.ui.debug("%d total queries\n" % reqcnt)
149
150
150 return base, list(fetch), heads
151 return base, list(fetch), heads
@@ -1,2278 +1,2276 b''
1 # util.py - Mercurial utility functions and platform specific implementations
1 # util.py - Mercurial utility functions and platform specific implementations
2 #
2 #
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 """Mercurial utility functions and platform specific implementations.
10 """Mercurial utility functions and platform specific implementations.
11
11
12 This contains helper routines that are independent of the SCM core and
12 This contains helper routines that are independent of the SCM core and
13 hide platform-specific details from the core.
13 hide platform-specific details from the core.
14 """
14 """
15
15
16 import i18n
16 import i18n
17 _ = i18n._
17 _ = i18n._
18 import error, osutil, encoding, parsers
18 import error, osutil, encoding, parsers
19 import errno, shutil, sys, tempfile, traceback
19 import errno, shutil, sys, tempfile, traceback
20 import re as remod
20 import re as remod
21 import os, time, datetime, calendar, textwrap, signal, collections
21 import os, time, datetime, calendar, textwrap, signal, collections
22 import imp, socket, urllib, struct
22 import imp, socket, urllib, struct
23 import gc
23 import gc
24
24
25 if os.name == 'nt':
25 if os.name == 'nt':
26 import windows as platform
26 import windows as platform
27 else:
27 else:
28 import posix as platform
28 import posix as platform
29
29
30 cachestat = platform.cachestat
30 cachestat = platform.cachestat
31 checkexec = platform.checkexec
31 checkexec = platform.checkexec
32 checklink = platform.checklink
32 checklink = platform.checklink
33 copymode = platform.copymode
33 copymode = platform.copymode
34 executablepath = platform.executablepath
34 executablepath = platform.executablepath
35 expandglobs = platform.expandglobs
35 expandglobs = platform.expandglobs
36 explainexit = platform.explainexit
36 explainexit = platform.explainexit
37 findexe = platform.findexe
37 findexe = platform.findexe
38 gethgcmd = platform.gethgcmd
38 gethgcmd = platform.gethgcmd
39 getuser = platform.getuser
39 getuser = platform.getuser
40 groupmembers = platform.groupmembers
40 groupmembers = platform.groupmembers
41 groupname = platform.groupname
41 groupname = platform.groupname
42 hidewindow = platform.hidewindow
42 hidewindow = platform.hidewindow
43 isexec = platform.isexec
43 isexec = platform.isexec
44 isowner = platform.isowner
44 isowner = platform.isowner
45 localpath = platform.localpath
45 localpath = platform.localpath
46 lookupreg = platform.lookupreg
46 lookupreg = platform.lookupreg
47 makedir = platform.makedir
47 makedir = platform.makedir
48 nlinks = platform.nlinks
48 nlinks = platform.nlinks
49 normpath = platform.normpath
49 normpath = platform.normpath
50 normcase = platform.normcase
50 normcase = platform.normcase
51 normcasespec = platform.normcasespec
51 normcasespec = platform.normcasespec
52 normcasefallback = platform.normcasefallback
52 normcasefallback = platform.normcasefallback
53 openhardlinks = platform.openhardlinks
53 openhardlinks = platform.openhardlinks
54 oslink = platform.oslink
54 oslink = platform.oslink
55 parsepatchoutput = platform.parsepatchoutput
55 parsepatchoutput = platform.parsepatchoutput
56 pconvert = platform.pconvert
56 pconvert = platform.pconvert
57 popen = platform.popen
57 popen = platform.popen
58 posixfile = platform.posixfile
58 posixfile = platform.posixfile
59 quotecommand = platform.quotecommand
59 quotecommand = platform.quotecommand
60 readpipe = platform.readpipe
60 readpipe = platform.readpipe
61 rename = platform.rename
61 rename = platform.rename
62 removedirs = platform.removedirs
62 removedirs = platform.removedirs
63 samedevice = platform.samedevice
63 samedevice = platform.samedevice
64 samefile = platform.samefile
64 samefile = platform.samefile
65 samestat = platform.samestat
65 samestat = platform.samestat
66 setbinary = platform.setbinary
66 setbinary = platform.setbinary
67 setflags = platform.setflags
67 setflags = platform.setflags
68 setsignalhandler = platform.setsignalhandler
68 setsignalhandler = platform.setsignalhandler
69 shellquote = platform.shellquote
69 shellquote = platform.shellquote
70 spawndetached = platform.spawndetached
70 spawndetached = platform.spawndetached
71 split = platform.split
71 split = platform.split
72 sshargs = platform.sshargs
72 sshargs = platform.sshargs
73 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
73 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
74 statisexec = platform.statisexec
74 statisexec = platform.statisexec
75 statislink = platform.statislink
75 statislink = platform.statislink
76 termwidth = platform.termwidth
76 termwidth = platform.termwidth
77 testpid = platform.testpid
77 testpid = platform.testpid
78 umask = platform.umask
78 umask = platform.umask
79 unlink = platform.unlink
79 unlink = platform.unlink
80 unlinkpath = platform.unlinkpath
80 unlinkpath = platform.unlinkpath
81 username = platform.username
81 username = platform.username
82
82
83 # Python compatibility
83 # Python compatibility
84
84
85 _notset = object()
85 _notset = object()
86
86
87 def safehasattr(thing, attr):
87 def safehasattr(thing, attr):
88 return getattr(thing, attr, _notset) is not _notset
88 return getattr(thing, attr, _notset) is not _notset
89
89
90 def sha1(s=''):
90 def sha1(s=''):
91 '''
91 '''
92 Low-overhead wrapper around Python's SHA support
92 Low-overhead wrapper around Python's SHA support
93
93
94 >>> f = _fastsha1
94 >>> f = _fastsha1
95 >>> a = sha1()
95 >>> a = sha1()
96 >>> a = f()
96 >>> a = f()
97 >>> a.hexdigest()
97 >>> a.hexdigest()
98 'da39a3ee5e6b4b0d3255bfef95601890afd80709'
98 'da39a3ee5e6b4b0d3255bfef95601890afd80709'
99 '''
99 '''
100
100
101 return _fastsha1(s)
101 return _fastsha1(s)
102
102
103 def _fastsha1(s=''):
103 def _fastsha1(s=''):
104 # This function will import sha1 from hashlib or sha (whichever is
104 # This function will import sha1 from hashlib or sha (whichever is
105 # available) and overwrite itself with it on the first call.
105 # available) and overwrite itself with it on the first call.
106 # Subsequent calls will go directly to the imported function.
106 # Subsequent calls will go directly to the imported function.
107 if sys.version_info >= (2, 5):
107 if sys.version_info >= (2, 5):
108 from hashlib import sha1 as _sha1
108 from hashlib import sha1 as _sha1
109 else:
109 else:
110 from sha import sha as _sha1
110 from sha import sha as _sha1
111 global _fastsha1, sha1
111 global _fastsha1, sha1
112 _fastsha1 = sha1 = _sha1
112 _fastsha1 = sha1 = _sha1
113 return _sha1(s)
113 return _sha1(s)
114
114
115 def md5(s=''):
115 def md5(s=''):
116 try:
116 try:
117 from hashlib import md5 as _md5
117 from hashlib import md5 as _md5
118 except ImportError:
118 except ImportError:
119 from md5 import md5 as _md5
119 from md5 import md5 as _md5
120 global md5
120 global md5
121 md5 = _md5
121 md5 = _md5
122 return _md5(s)
122 return _md5(s)
123
123
124 DIGESTS = {
124 DIGESTS = {
125 'md5': md5,
125 'md5': md5,
126 'sha1': sha1,
126 'sha1': sha1,
127 }
127 }
128 # List of digest types from strongest to weakest
128 # List of digest types from strongest to weakest
129 DIGESTS_BY_STRENGTH = ['sha1', 'md5']
129 DIGESTS_BY_STRENGTH = ['sha1', 'md5']
130
130
131 try:
131 try:
132 import hashlib
132 import hashlib
133 DIGESTS.update({
133 DIGESTS.update({
134 'sha512': hashlib.sha512,
134 'sha512': hashlib.sha512,
135 })
135 })
136 DIGESTS_BY_STRENGTH.insert(0, 'sha512')
136 DIGESTS_BY_STRENGTH.insert(0, 'sha512')
137 except ImportError:
137 except ImportError:
138 pass
138 pass
139
139
140 for k in DIGESTS_BY_STRENGTH:
140 for k in DIGESTS_BY_STRENGTH:
141 assert k in DIGESTS
141 assert k in DIGESTS
142
142
143 class digester(object):
143 class digester(object):
144 """helper to compute digests.
144 """helper to compute digests.
145
145
146 This helper can be used to compute one or more digests given their name.
146 This helper can be used to compute one or more digests given their name.
147
147
148 >>> d = digester(['md5', 'sha1'])
148 >>> d = digester(['md5', 'sha1'])
149 >>> d.update('foo')
149 >>> d.update('foo')
150 >>> [k for k in sorted(d)]
150 >>> [k for k in sorted(d)]
151 ['md5', 'sha1']
151 ['md5', 'sha1']
152 >>> d['md5']
152 >>> d['md5']
153 'acbd18db4cc2f85cedef654fccc4a4d8'
153 'acbd18db4cc2f85cedef654fccc4a4d8'
154 >>> d['sha1']
154 >>> d['sha1']
155 '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
155 '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
156 >>> digester.preferred(['md5', 'sha1'])
156 >>> digester.preferred(['md5', 'sha1'])
157 'sha1'
157 'sha1'
158 """
158 """
159
159
160 def __init__(self, digests, s=''):
160 def __init__(self, digests, s=''):
161 self._hashes = {}
161 self._hashes = {}
162 for k in digests:
162 for k in digests:
163 if k not in DIGESTS:
163 if k not in DIGESTS:
164 raise Abort(_('unknown digest type: %s') % k)
164 raise Abort(_('unknown digest type: %s') % k)
165 self._hashes[k] = DIGESTS[k]()
165 self._hashes[k] = DIGESTS[k]()
166 if s:
166 if s:
167 self.update(s)
167 self.update(s)
168
168
169 def update(self, data):
169 def update(self, data):
170 for h in self._hashes.values():
170 for h in self._hashes.values():
171 h.update(data)
171 h.update(data)
172
172
173 def __getitem__(self, key):
173 def __getitem__(self, key):
174 if key not in DIGESTS:
174 if key not in DIGESTS:
175 raise Abort(_('unknown digest type: %s') % k)
175 raise Abort(_('unknown digest type: %s') % k)
176 return self._hashes[key].hexdigest()
176 return self._hashes[key].hexdigest()
177
177
178 def __iter__(self):
178 def __iter__(self):
179 return iter(self._hashes)
179 return iter(self._hashes)
180
180
181 @staticmethod
181 @staticmethod
182 def preferred(supported):
182 def preferred(supported):
183 """returns the strongest digest type in both supported and DIGESTS."""
183 """returns the strongest digest type in both supported and DIGESTS."""
184
184
185 for k in DIGESTS_BY_STRENGTH:
185 for k in DIGESTS_BY_STRENGTH:
186 if k in supported:
186 if k in supported:
187 return k
187 return k
188 return None
188 return None
189
189
190 class digestchecker(object):
190 class digestchecker(object):
191 """file handle wrapper that additionally checks content against a given
191 """file handle wrapper that additionally checks content against a given
192 size and digests.
192 size and digests.
193
193
194 d = digestchecker(fh, size, {'md5': '...'})
194 d = digestchecker(fh, size, {'md5': '...'})
195
195
196 When multiple digests are given, all of them are validated.
196 When multiple digests are given, all of them are validated.
197 """
197 """
198
198
199 def __init__(self, fh, size, digests):
199 def __init__(self, fh, size, digests):
200 self._fh = fh
200 self._fh = fh
201 self._size = size
201 self._size = size
202 self._got = 0
202 self._got = 0
203 self._digests = dict(digests)
203 self._digests = dict(digests)
204 self._digester = digester(self._digests.keys())
204 self._digester = digester(self._digests.keys())
205
205
206 def read(self, length=-1):
206 def read(self, length=-1):
207 content = self._fh.read(length)
207 content = self._fh.read(length)
208 self._digester.update(content)
208 self._digester.update(content)
209 self._got += len(content)
209 self._got += len(content)
210 return content
210 return content
211
211
212 def validate(self):
212 def validate(self):
213 if self._size != self._got:
213 if self._size != self._got:
214 raise Abort(_('size mismatch: expected %d, got %d') %
214 raise Abort(_('size mismatch: expected %d, got %d') %
215 (self._size, self._got))
215 (self._size, self._got))
216 for k, v in self._digests.items():
216 for k, v in self._digests.items():
217 if v != self._digester[k]:
217 if v != self._digester[k]:
218 # i18n: first parameter is a digest name
218 # i18n: first parameter is a digest name
219 raise Abort(_('%s mismatch: expected %s, got %s') %
219 raise Abort(_('%s mismatch: expected %s, got %s') %
220 (k, v, self._digester[k]))
220 (k, v, self._digester[k]))
221
221
222 try:
222 try:
223 buffer = buffer
223 buffer = buffer
224 except NameError:
224 except NameError:
225 if sys.version_info[0] < 3:
225 if sys.version_info[0] < 3:
226 def buffer(sliceable, offset=0):
226 def buffer(sliceable, offset=0):
227 return sliceable[offset:]
227 return sliceable[offset:]
228 else:
228 else:
229 def buffer(sliceable, offset=0):
229 def buffer(sliceable, offset=0):
230 return memoryview(sliceable)[offset:]
230 return memoryview(sliceable)[offset:]
231
231
232 import subprocess
232 import subprocess
233 closefds = os.name == 'posix'
233 closefds = os.name == 'posix'
234
234
235 def unpacker(fmt):
235 def unpacker(fmt):
236 """create a struct unpacker for the specified format"""
236 """create a struct unpacker for the specified format"""
237 try:
237 try:
238 # 2.5+
238 # 2.5+
239 return struct.Struct(fmt).unpack
239 return struct.Struct(fmt).unpack
240 except AttributeError:
240 except AttributeError:
241 # 2.4
241 # 2.4
242 return lambda buf: struct.unpack(fmt, buf)
242 return lambda buf: struct.unpack(fmt, buf)
243
243
244 def popen2(cmd, env=None, newlines=False):
244 def popen2(cmd, env=None, newlines=False):
245 # Setting bufsize to -1 lets the system decide the buffer size.
245 # Setting bufsize to -1 lets the system decide the buffer size.
246 # The default for bufsize is 0, meaning unbuffered. This leads to
246 # The default for bufsize is 0, meaning unbuffered. This leads to
247 # poor performance on Mac OS X: http://bugs.python.org/issue4194
247 # poor performance on Mac OS X: http://bugs.python.org/issue4194
248 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
248 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
249 close_fds=closefds,
249 close_fds=closefds,
250 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
250 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
251 universal_newlines=newlines,
251 universal_newlines=newlines,
252 env=env)
252 env=env)
253 return p.stdin, p.stdout
253 return p.stdin, p.stdout
254
254
255 def popen3(cmd, env=None, newlines=False):
255 def popen3(cmd, env=None, newlines=False):
256 stdin, stdout, stderr, p = popen4(cmd, env, newlines)
256 stdin, stdout, stderr, p = popen4(cmd, env, newlines)
257 return stdin, stdout, stderr
257 return stdin, stdout, stderr
258
258
259 def popen4(cmd, env=None, newlines=False):
259 def popen4(cmd, env=None, newlines=False):
260 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
260 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
261 close_fds=closefds,
261 close_fds=closefds,
262 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
262 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
263 stderr=subprocess.PIPE,
263 stderr=subprocess.PIPE,
264 universal_newlines=newlines,
264 universal_newlines=newlines,
265 env=env)
265 env=env)
266 return p.stdin, p.stdout, p.stderr, p
266 return p.stdin, p.stdout, p.stderr, p
267
267
268 def version():
268 def version():
269 """Return version information if available."""
269 """Return version information if available."""
270 try:
270 try:
271 import __version__
271 import __version__
272 return __version__.version
272 return __version__.version
273 except ImportError:
273 except ImportError:
274 return 'unknown'
274 return 'unknown'
275
275
276 # used by parsedate
276 # used by parsedate
277 defaultdateformats = (
277 defaultdateformats = (
278 '%Y-%m-%d %H:%M:%S',
278 '%Y-%m-%d %H:%M:%S',
279 '%Y-%m-%d %I:%M:%S%p',
279 '%Y-%m-%d %I:%M:%S%p',
280 '%Y-%m-%d %H:%M',
280 '%Y-%m-%d %H:%M',
281 '%Y-%m-%d %I:%M%p',
281 '%Y-%m-%d %I:%M%p',
282 '%Y-%m-%d',
282 '%Y-%m-%d',
283 '%m-%d',
283 '%m-%d',
284 '%m/%d',
284 '%m/%d',
285 '%m/%d/%y',
285 '%m/%d/%y',
286 '%m/%d/%Y',
286 '%m/%d/%Y',
287 '%a %b %d %H:%M:%S %Y',
287 '%a %b %d %H:%M:%S %Y',
288 '%a %b %d %I:%M:%S%p %Y',
288 '%a %b %d %I:%M:%S%p %Y',
289 '%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
289 '%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
290 '%b %d %H:%M:%S %Y',
290 '%b %d %H:%M:%S %Y',
291 '%b %d %I:%M:%S%p %Y',
291 '%b %d %I:%M:%S%p %Y',
292 '%b %d %H:%M:%S',
292 '%b %d %H:%M:%S',
293 '%b %d %I:%M:%S%p',
293 '%b %d %I:%M:%S%p',
294 '%b %d %H:%M',
294 '%b %d %H:%M',
295 '%b %d %I:%M%p',
295 '%b %d %I:%M%p',
296 '%b %d %Y',
296 '%b %d %Y',
297 '%b %d',
297 '%b %d',
298 '%H:%M:%S',
298 '%H:%M:%S',
299 '%I:%M:%S%p',
299 '%I:%M:%S%p',
300 '%H:%M',
300 '%H:%M',
301 '%I:%M%p',
301 '%I:%M%p',
302 )
302 )
303
303
304 extendeddateformats = defaultdateformats + (
304 extendeddateformats = defaultdateformats + (
305 "%Y",
305 "%Y",
306 "%Y-%m",
306 "%Y-%m",
307 "%b",
307 "%b",
308 "%b %Y",
308 "%b %Y",
309 )
309 )
310
310
311 def cachefunc(func):
311 def cachefunc(func):
312 '''cache the result of function calls'''
312 '''cache the result of function calls'''
313 # XXX doesn't handle keywords args
313 # XXX doesn't handle keywords args
314 if func.func_code.co_argcount == 0:
314 if func.func_code.co_argcount == 0:
315 cache = []
315 cache = []
316 def f():
316 def f():
317 if len(cache) == 0:
317 if len(cache) == 0:
318 cache.append(func())
318 cache.append(func())
319 return cache[0]
319 return cache[0]
320 return f
320 return f
321 cache = {}
321 cache = {}
322 if func.func_code.co_argcount == 1:
322 if func.func_code.co_argcount == 1:
323 # we gain a small amount of time because
323 # we gain a small amount of time because
324 # we don't need to pack/unpack the list
324 # we don't need to pack/unpack the list
325 def f(arg):
325 def f(arg):
326 if arg not in cache:
326 if arg not in cache:
327 cache[arg] = func(arg)
327 cache[arg] = func(arg)
328 return cache[arg]
328 return cache[arg]
329 else:
329 else:
330 def f(*args):
330 def f(*args):
331 if args not in cache:
331 if args not in cache:
332 cache[args] = func(*args)
332 cache[args] = func(*args)
333 return cache[args]
333 return cache[args]
334
334
335 return f
335 return f
336
336
337 deque = collections.deque
338
339 class sortdict(dict):
337 class sortdict(dict):
340 '''a simple sorted dictionary'''
338 '''a simple sorted dictionary'''
341 def __init__(self, data=None):
339 def __init__(self, data=None):
342 self._list = []
340 self._list = []
343 if data:
341 if data:
344 self.update(data)
342 self.update(data)
345 def copy(self):
343 def copy(self):
346 return sortdict(self)
344 return sortdict(self)
347 def __setitem__(self, key, val):
345 def __setitem__(self, key, val):
348 if key in self:
346 if key in self:
349 self._list.remove(key)
347 self._list.remove(key)
350 self._list.append(key)
348 self._list.append(key)
351 dict.__setitem__(self, key, val)
349 dict.__setitem__(self, key, val)
352 def __iter__(self):
350 def __iter__(self):
353 return self._list.__iter__()
351 return self._list.__iter__()
354 def update(self, src):
352 def update(self, src):
355 if isinstance(src, dict):
353 if isinstance(src, dict):
356 src = src.iteritems()
354 src = src.iteritems()
357 for k, v in src:
355 for k, v in src:
358 self[k] = v
356 self[k] = v
359 def clear(self):
357 def clear(self):
360 dict.clear(self)
358 dict.clear(self)
361 self._list = []
359 self._list = []
362 def items(self):
360 def items(self):
363 return [(k, self[k]) for k in self._list]
361 return [(k, self[k]) for k in self._list]
364 def __delitem__(self, key):
362 def __delitem__(self, key):
365 dict.__delitem__(self, key)
363 dict.__delitem__(self, key)
366 self._list.remove(key)
364 self._list.remove(key)
367 def pop(self, key, *args, **kwargs):
365 def pop(self, key, *args, **kwargs):
368 dict.pop(self, key, *args, **kwargs)
366 dict.pop(self, key, *args, **kwargs)
369 try:
367 try:
370 self._list.remove(key)
368 self._list.remove(key)
371 except ValueError:
369 except ValueError:
372 pass
370 pass
373 def keys(self):
371 def keys(self):
374 return self._list
372 return self._list
375 def iterkeys(self):
373 def iterkeys(self):
376 return self._list.__iter__()
374 return self._list.__iter__()
377 def iteritems(self):
375 def iteritems(self):
378 for k in self._list:
376 for k in self._list:
379 yield k, self[k]
377 yield k, self[k]
380 def insert(self, index, key, val):
378 def insert(self, index, key, val):
381 self._list.insert(index, key)
379 self._list.insert(index, key)
382 dict.__setitem__(self, key, val)
380 dict.__setitem__(self, key, val)
383
381
384 class lrucachedict(object):
382 class lrucachedict(object):
385 '''cache most recent gets from or sets to this dictionary'''
383 '''cache most recent gets from or sets to this dictionary'''
386 def __init__(self, maxsize):
384 def __init__(self, maxsize):
387 self._cache = {}
385 self._cache = {}
388 self._maxsize = maxsize
386 self._maxsize = maxsize
389 self._order = deque()
387 self._order = collections.deque()
390
388
391 def __getitem__(self, key):
389 def __getitem__(self, key):
392 value = self._cache[key]
390 value = self._cache[key]
393 self._order.remove(key)
391 self._order.remove(key)
394 self._order.append(key)
392 self._order.append(key)
395 return value
393 return value
396
394
397 def __setitem__(self, key, value):
395 def __setitem__(self, key, value):
398 if key not in self._cache:
396 if key not in self._cache:
399 if len(self._cache) >= self._maxsize:
397 if len(self._cache) >= self._maxsize:
400 del self._cache[self._order.popleft()]
398 del self._cache[self._order.popleft()]
401 else:
399 else:
402 self._order.remove(key)
400 self._order.remove(key)
403 self._cache[key] = value
401 self._cache[key] = value
404 self._order.append(key)
402 self._order.append(key)
405
403
406 def __contains__(self, key):
404 def __contains__(self, key):
407 return key in self._cache
405 return key in self._cache
408
406
409 def clear(self):
407 def clear(self):
410 self._cache.clear()
408 self._cache.clear()
411 self._order = deque()
409 self._order = collections.deque()
412
410
413 def lrucachefunc(func):
411 def lrucachefunc(func):
414 '''cache most recent results of function calls'''
412 '''cache most recent results of function calls'''
415 cache = {}
413 cache = {}
416 order = deque()
414 order = collections.deque()
417 if func.func_code.co_argcount == 1:
415 if func.func_code.co_argcount == 1:
418 def f(arg):
416 def f(arg):
419 if arg not in cache:
417 if arg not in cache:
420 if len(cache) > 20:
418 if len(cache) > 20:
421 del cache[order.popleft()]
419 del cache[order.popleft()]
422 cache[arg] = func(arg)
420 cache[arg] = func(arg)
423 else:
421 else:
424 order.remove(arg)
422 order.remove(arg)
425 order.append(arg)
423 order.append(arg)
426 return cache[arg]
424 return cache[arg]
427 else:
425 else:
428 def f(*args):
426 def f(*args):
429 if args not in cache:
427 if args not in cache:
430 if len(cache) > 20:
428 if len(cache) > 20:
431 del cache[order.popleft()]
429 del cache[order.popleft()]
432 cache[args] = func(*args)
430 cache[args] = func(*args)
433 else:
431 else:
434 order.remove(args)
432 order.remove(args)
435 order.append(args)
433 order.append(args)
436 return cache[args]
434 return cache[args]
437
435
438 return f
436 return f
439
437
440 class propertycache(object):
438 class propertycache(object):
441 def __init__(self, func):
439 def __init__(self, func):
442 self.func = func
440 self.func = func
443 self.name = func.__name__
441 self.name = func.__name__
444 def __get__(self, obj, type=None):
442 def __get__(self, obj, type=None):
445 result = self.func(obj)
443 result = self.func(obj)
446 self.cachevalue(obj, result)
444 self.cachevalue(obj, result)
447 return result
445 return result
448
446
449 def cachevalue(self, obj, value):
447 def cachevalue(self, obj, value):
450 # __dict__ assignment required to bypass __setattr__ (eg: repoview)
448 # __dict__ assignment required to bypass __setattr__ (eg: repoview)
451 obj.__dict__[self.name] = value
449 obj.__dict__[self.name] = value
452
450
453 def pipefilter(s, cmd):
451 def pipefilter(s, cmd):
454 '''filter string S through command CMD, returning its output'''
452 '''filter string S through command CMD, returning its output'''
455 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
453 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
456 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
454 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
457 pout, perr = p.communicate(s)
455 pout, perr = p.communicate(s)
458 return pout
456 return pout
459
457
460 def tempfilter(s, cmd):
458 def tempfilter(s, cmd):
461 '''filter string S through a pair of temporary files with CMD.
459 '''filter string S through a pair of temporary files with CMD.
462 CMD is used as a template to create the real command to be run,
460 CMD is used as a template to create the real command to be run,
463 with the strings INFILE and OUTFILE replaced by the real names of
461 with the strings INFILE and OUTFILE replaced by the real names of
464 the temporary files generated.'''
462 the temporary files generated.'''
465 inname, outname = None, None
463 inname, outname = None, None
466 try:
464 try:
467 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
465 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
468 fp = os.fdopen(infd, 'wb')
466 fp = os.fdopen(infd, 'wb')
469 fp.write(s)
467 fp.write(s)
470 fp.close()
468 fp.close()
471 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
469 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
472 os.close(outfd)
470 os.close(outfd)
473 cmd = cmd.replace('INFILE', inname)
471 cmd = cmd.replace('INFILE', inname)
474 cmd = cmd.replace('OUTFILE', outname)
472 cmd = cmd.replace('OUTFILE', outname)
475 code = os.system(cmd)
473 code = os.system(cmd)
476 if sys.platform == 'OpenVMS' and code & 1:
474 if sys.platform == 'OpenVMS' and code & 1:
477 code = 0
475 code = 0
478 if code:
476 if code:
479 raise Abort(_("command '%s' failed: %s") %
477 raise Abort(_("command '%s' failed: %s") %
480 (cmd, explainexit(code)))
478 (cmd, explainexit(code)))
481 fp = open(outname, 'rb')
479 fp = open(outname, 'rb')
482 r = fp.read()
480 r = fp.read()
483 fp.close()
481 fp.close()
484 return r
482 return r
485 finally:
483 finally:
486 try:
484 try:
487 if inname:
485 if inname:
488 os.unlink(inname)
486 os.unlink(inname)
489 except OSError:
487 except OSError:
490 pass
488 pass
491 try:
489 try:
492 if outname:
490 if outname:
493 os.unlink(outname)
491 os.unlink(outname)
494 except OSError:
492 except OSError:
495 pass
493 pass
496
494
497 filtertable = {
495 filtertable = {
498 'tempfile:': tempfilter,
496 'tempfile:': tempfilter,
499 'pipe:': pipefilter,
497 'pipe:': pipefilter,
500 }
498 }
501
499
502 def filter(s, cmd):
500 def filter(s, cmd):
503 "filter a string through a command that transforms its input to its output"
501 "filter a string through a command that transforms its input to its output"
504 for name, fn in filtertable.iteritems():
502 for name, fn in filtertable.iteritems():
505 if cmd.startswith(name):
503 if cmd.startswith(name):
506 return fn(s, cmd[len(name):].lstrip())
504 return fn(s, cmd[len(name):].lstrip())
507 return pipefilter(s, cmd)
505 return pipefilter(s, cmd)
508
506
509 def binary(s):
507 def binary(s):
510 """return true if a string is binary data"""
508 """return true if a string is binary data"""
511 return bool(s and '\0' in s)
509 return bool(s and '\0' in s)
512
510
513 def increasingchunks(source, min=1024, max=65536):
511 def increasingchunks(source, min=1024, max=65536):
514 '''return no less than min bytes per chunk while data remains,
512 '''return no less than min bytes per chunk while data remains,
515 doubling min after each chunk until it reaches max'''
513 doubling min after each chunk until it reaches max'''
516 def log2(x):
514 def log2(x):
517 if not x:
515 if not x:
518 return 0
516 return 0
519 i = 0
517 i = 0
520 while x:
518 while x:
521 x >>= 1
519 x >>= 1
522 i += 1
520 i += 1
523 return i - 1
521 return i - 1
524
522
525 buf = []
523 buf = []
526 blen = 0
524 blen = 0
527 for chunk in source:
525 for chunk in source:
528 buf.append(chunk)
526 buf.append(chunk)
529 blen += len(chunk)
527 blen += len(chunk)
530 if blen >= min:
528 if blen >= min:
531 if min < max:
529 if min < max:
532 min = min << 1
530 min = min << 1
533 nmin = 1 << log2(blen)
531 nmin = 1 << log2(blen)
534 if nmin > min:
532 if nmin > min:
535 min = nmin
533 min = nmin
536 if min > max:
534 if min > max:
537 min = max
535 min = max
538 yield ''.join(buf)
536 yield ''.join(buf)
539 blen = 0
537 blen = 0
540 buf = []
538 buf = []
541 if buf:
539 if buf:
542 yield ''.join(buf)
540 yield ''.join(buf)
543
541
544 Abort = error.Abort
542 Abort = error.Abort
545
543
546 def always(fn):
544 def always(fn):
547 return True
545 return True
548
546
549 def never(fn):
547 def never(fn):
550 return False
548 return False
551
549
552 def nogc(func):
550 def nogc(func):
553 """disable garbage collector
551 """disable garbage collector
554
552
555 Python's garbage collector triggers a GC each time a certain number of
553 Python's garbage collector triggers a GC each time a certain number of
556 container objects (the number being defined by gc.get_threshold()) are
554 container objects (the number being defined by gc.get_threshold()) are
557 allocated even when marked not to be tracked by the collector. Tracking has
555 allocated even when marked not to be tracked by the collector. Tracking has
558 no effect on when GCs are triggered, only on what objects the GC looks
556 no effect on when GCs are triggered, only on what objects the GC looks
559 into. As a workaround, disable GC while building complex (huge)
557 into. As a workaround, disable GC while building complex (huge)
560 containers.
558 containers.
561
559
562 This garbage collector issue have been fixed in 2.7.
560 This garbage collector issue have been fixed in 2.7.
563 """
561 """
564 def wrapper(*args, **kwargs):
562 def wrapper(*args, **kwargs):
565 gcenabled = gc.isenabled()
563 gcenabled = gc.isenabled()
566 gc.disable()
564 gc.disable()
567 try:
565 try:
568 return func(*args, **kwargs)
566 return func(*args, **kwargs)
569 finally:
567 finally:
570 if gcenabled:
568 if gcenabled:
571 gc.enable()
569 gc.enable()
572 return wrapper
570 return wrapper
573
571
574 def pathto(root, n1, n2):
572 def pathto(root, n1, n2):
575 '''return the relative path from one place to another.
573 '''return the relative path from one place to another.
576 root should use os.sep to separate directories
574 root should use os.sep to separate directories
577 n1 should use os.sep to separate directories
575 n1 should use os.sep to separate directories
578 n2 should use "/" to separate directories
576 n2 should use "/" to separate directories
579 returns an os.sep-separated path.
577 returns an os.sep-separated path.
580
578
581 If n1 is a relative path, it's assumed it's
579 If n1 is a relative path, it's assumed it's
582 relative to root.
580 relative to root.
583 n2 should always be relative to root.
581 n2 should always be relative to root.
584 '''
582 '''
585 if not n1:
583 if not n1:
586 return localpath(n2)
584 return localpath(n2)
587 if os.path.isabs(n1):
585 if os.path.isabs(n1):
588 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
586 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
589 return os.path.join(root, localpath(n2))
587 return os.path.join(root, localpath(n2))
590 n2 = '/'.join((pconvert(root), n2))
588 n2 = '/'.join((pconvert(root), n2))
591 a, b = splitpath(n1), n2.split('/')
589 a, b = splitpath(n1), n2.split('/')
592 a.reverse()
590 a.reverse()
593 b.reverse()
591 b.reverse()
594 while a and b and a[-1] == b[-1]:
592 while a and b and a[-1] == b[-1]:
595 a.pop()
593 a.pop()
596 b.pop()
594 b.pop()
597 b.reverse()
595 b.reverse()
598 return os.sep.join((['..'] * len(a)) + b) or '.'
596 return os.sep.join((['..'] * len(a)) + b) or '.'
599
597
600 def mainfrozen():
598 def mainfrozen():
601 """return True if we are a frozen executable.
599 """return True if we are a frozen executable.
602
600
603 The code supports py2exe (most common, Windows only) and tools/freeze
601 The code supports py2exe (most common, Windows only) and tools/freeze
604 (portable, not much used).
602 (portable, not much used).
605 """
603 """
606 return (safehasattr(sys, "frozen") or # new py2exe
604 return (safehasattr(sys, "frozen") or # new py2exe
607 safehasattr(sys, "importers") or # old py2exe
605 safehasattr(sys, "importers") or # old py2exe
608 imp.is_frozen("__main__")) # tools/freeze
606 imp.is_frozen("__main__")) # tools/freeze
609
607
610 # the location of data files matching the source code
608 # the location of data files matching the source code
611 if mainfrozen():
609 if mainfrozen():
612 # executable version (py2exe) doesn't support __file__
610 # executable version (py2exe) doesn't support __file__
613 datapath = os.path.dirname(sys.executable)
611 datapath = os.path.dirname(sys.executable)
614 else:
612 else:
615 datapath = os.path.dirname(__file__)
613 datapath = os.path.dirname(__file__)
616
614
617 i18n.setdatapath(datapath)
615 i18n.setdatapath(datapath)
618
616
619 _hgexecutable = None
617 _hgexecutable = None
620
618
621 def hgexecutable():
619 def hgexecutable():
622 """return location of the 'hg' executable.
620 """return location of the 'hg' executable.
623
621
624 Defaults to $HG or 'hg' in the search path.
622 Defaults to $HG or 'hg' in the search path.
625 """
623 """
626 if _hgexecutable is None:
624 if _hgexecutable is None:
627 hg = os.environ.get('HG')
625 hg = os.environ.get('HG')
628 mainmod = sys.modules['__main__']
626 mainmod = sys.modules['__main__']
629 if hg:
627 if hg:
630 _sethgexecutable(hg)
628 _sethgexecutable(hg)
631 elif mainfrozen():
629 elif mainfrozen():
632 _sethgexecutable(sys.executable)
630 _sethgexecutable(sys.executable)
633 elif os.path.basename(getattr(mainmod, '__file__', '')) == 'hg':
631 elif os.path.basename(getattr(mainmod, '__file__', '')) == 'hg':
634 _sethgexecutable(mainmod.__file__)
632 _sethgexecutable(mainmod.__file__)
635 else:
633 else:
636 exe = findexe('hg') or os.path.basename(sys.argv[0])
634 exe = findexe('hg') or os.path.basename(sys.argv[0])
637 _sethgexecutable(exe)
635 _sethgexecutable(exe)
638 return _hgexecutable
636 return _hgexecutable
639
637
640 def _sethgexecutable(path):
638 def _sethgexecutable(path):
641 """set location of the 'hg' executable"""
639 """set location of the 'hg' executable"""
642 global _hgexecutable
640 global _hgexecutable
643 _hgexecutable = path
641 _hgexecutable = path
644
642
645 def system(cmd, environ={}, cwd=None, onerr=None, errprefix=None, out=None):
643 def system(cmd, environ={}, cwd=None, onerr=None, errprefix=None, out=None):
646 '''enhanced shell command execution.
644 '''enhanced shell command execution.
647 run with environment maybe modified, maybe in different dir.
645 run with environment maybe modified, maybe in different dir.
648
646
649 if command fails and onerr is None, return status, else raise onerr
647 if command fails and onerr is None, return status, else raise onerr
650 object as exception.
648 object as exception.
651
649
652 if out is specified, it is assumed to be a file-like object that has a
650 if out is specified, it is assumed to be a file-like object that has a
653 write() method. stdout and stderr will be redirected to out.'''
651 write() method. stdout and stderr will be redirected to out.'''
654 try:
652 try:
655 sys.stdout.flush()
653 sys.stdout.flush()
656 except Exception:
654 except Exception:
657 pass
655 pass
658 def py2shell(val):
656 def py2shell(val):
659 'convert python object into string that is useful to shell'
657 'convert python object into string that is useful to shell'
660 if val is None or val is False:
658 if val is None or val is False:
661 return '0'
659 return '0'
662 if val is True:
660 if val is True:
663 return '1'
661 return '1'
664 return str(val)
662 return str(val)
665 origcmd = cmd
663 origcmd = cmd
666 cmd = quotecommand(cmd)
664 cmd = quotecommand(cmd)
667 if sys.platform == 'plan9' and (sys.version_info[0] == 2
665 if sys.platform == 'plan9' and (sys.version_info[0] == 2
668 and sys.version_info[1] < 7):
666 and sys.version_info[1] < 7):
669 # subprocess kludge to work around issues in half-baked Python
667 # subprocess kludge to work around issues in half-baked Python
670 # ports, notably bichued/python:
668 # ports, notably bichued/python:
671 if not cwd is None:
669 if not cwd is None:
672 os.chdir(cwd)
670 os.chdir(cwd)
673 rc = os.system(cmd)
671 rc = os.system(cmd)
674 else:
672 else:
675 env = dict(os.environ)
673 env = dict(os.environ)
676 env.update((k, py2shell(v)) for k, v in environ.iteritems())
674 env.update((k, py2shell(v)) for k, v in environ.iteritems())
677 env['HG'] = hgexecutable()
675 env['HG'] = hgexecutable()
678 if out is None or out == sys.__stdout__:
676 if out is None or out == sys.__stdout__:
679 rc = subprocess.call(cmd, shell=True, close_fds=closefds,
677 rc = subprocess.call(cmd, shell=True, close_fds=closefds,
680 env=env, cwd=cwd)
678 env=env, cwd=cwd)
681 else:
679 else:
682 proc = subprocess.Popen(cmd, shell=True, close_fds=closefds,
680 proc = subprocess.Popen(cmd, shell=True, close_fds=closefds,
683 env=env, cwd=cwd, stdout=subprocess.PIPE,
681 env=env, cwd=cwd, stdout=subprocess.PIPE,
684 stderr=subprocess.STDOUT)
682 stderr=subprocess.STDOUT)
685 while True:
683 while True:
686 line = proc.stdout.readline()
684 line = proc.stdout.readline()
687 if not line:
685 if not line:
688 break
686 break
689 out.write(line)
687 out.write(line)
690 proc.wait()
688 proc.wait()
691 rc = proc.returncode
689 rc = proc.returncode
692 if sys.platform == 'OpenVMS' and rc & 1:
690 if sys.platform == 'OpenVMS' and rc & 1:
693 rc = 0
691 rc = 0
694 if rc and onerr:
692 if rc and onerr:
695 errmsg = '%s %s' % (os.path.basename(origcmd.split(None, 1)[0]),
693 errmsg = '%s %s' % (os.path.basename(origcmd.split(None, 1)[0]),
696 explainexit(rc)[0])
694 explainexit(rc)[0])
697 if errprefix:
695 if errprefix:
698 errmsg = '%s: %s' % (errprefix, errmsg)
696 errmsg = '%s: %s' % (errprefix, errmsg)
699 raise onerr(errmsg)
697 raise onerr(errmsg)
700 return rc
698 return rc
701
699
702 def checksignature(func):
700 def checksignature(func):
703 '''wrap a function with code to check for calling errors'''
701 '''wrap a function with code to check for calling errors'''
704 def check(*args, **kwargs):
702 def check(*args, **kwargs):
705 try:
703 try:
706 return func(*args, **kwargs)
704 return func(*args, **kwargs)
707 except TypeError:
705 except TypeError:
708 if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
706 if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
709 raise error.SignatureError
707 raise error.SignatureError
710 raise
708 raise
711
709
712 return check
710 return check
713
711
714 def copyfile(src, dest, hardlink=False):
712 def copyfile(src, dest, hardlink=False):
715 "copy a file, preserving mode and atime/mtime"
713 "copy a file, preserving mode and atime/mtime"
716 if os.path.lexists(dest):
714 if os.path.lexists(dest):
717 unlink(dest)
715 unlink(dest)
718 # hardlinks are problematic on CIFS, quietly ignore this flag
716 # hardlinks are problematic on CIFS, quietly ignore this flag
719 # until we find a way to work around it cleanly (issue4546)
717 # until we find a way to work around it cleanly (issue4546)
720 if False and hardlink:
718 if False and hardlink:
721 try:
719 try:
722 oslink(src, dest)
720 oslink(src, dest)
723 return
721 return
724 except (IOError, OSError):
722 except (IOError, OSError):
725 pass # fall back to normal copy
723 pass # fall back to normal copy
726 if os.path.islink(src):
724 if os.path.islink(src):
727 os.symlink(os.readlink(src), dest)
725 os.symlink(os.readlink(src), dest)
728 else:
726 else:
729 try:
727 try:
730 shutil.copyfile(src, dest)
728 shutil.copyfile(src, dest)
731 shutil.copymode(src, dest)
729 shutil.copymode(src, dest)
732 except shutil.Error, inst:
730 except shutil.Error, inst:
733 raise Abort(str(inst))
731 raise Abort(str(inst))
734
732
735 def copyfiles(src, dst, hardlink=None, progress=lambda t, pos: None):
733 def copyfiles(src, dst, hardlink=None, progress=lambda t, pos: None):
736 """Copy a directory tree using hardlinks if possible."""
734 """Copy a directory tree using hardlinks if possible."""
737 num = 0
735 num = 0
738
736
739 if hardlink is None:
737 if hardlink is None:
740 hardlink = (os.stat(src).st_dev ==
738 hardlink = (os.stat(src).st_dev ==
741 os.stat(os.path.dirname(dst)).st_dev)
739 os.stat(os.path.dirname(dst)).st_dev)
742 if hardlink:
740 if hardlink:
743 topic = _('linking')
741 topic = _('linking')
744 else:
742 else:
745 topic = _('copying')
743 topic = _('copying')
746
744
747 if os.path.isdir(src):
745 if os.path.isdir(src):
748 os.mkdir(dst)
746 os.mkdir(dst)
749 for name, kind in osutil.listdir(src):
747 for name, kind in osutil.listdir(src):
750 srcname = os.path.join(src, name)
748 srcname = os.path.join(src, name)
751 dstname = os.path.join(dst, name)
749 dstname = os.path.join(dst, name)
752 def nprog(t, pos):
750 def nprog(t, pos):
753 if pos is not None:
751 if pos is not None:
754 return progress(t, pos + num)
752 return progress(t, pos + num)
755 hardlink, n = copyfiles(srcname, dstname, hardlink, progress=nprog)
753 hardlink, n = copyfiles(srcname, dstname, hardlink, progress=nprog)
756 num += n
754 num += n
757 else:
755 else:
758 if hardlink:
756 if hardlink:
759 try:
757 try:
760 oslink(src, dst)
758 oslink(src, dst)
761 except (IOError, OSError):
759 except (IOError, OSError):
762 hardlink = False
760 hardlink = False
763 shutil.copy(src, dst)
761 shutil.copy(src, dst)
764 else:
762 else:
765 shutil.copy(src, dst)
763 shutil.copy(src, dst)
766 num += 1
764 num += 1
767 progress(topic, num)
765 progress(topic, num)
768 progress(topic, None)
766 progress(topic, None)
769
767
770 return hardlink, num
768 return hardlink, num
771
769
772 _winreservednames = '''con prn aux nul
770 _winreservednames = '''con prn aux nul
773 com1 com2 com3 com4 com5 com6 com7 com8 com9
771 com1 com2 com3 com4 com5 com6 com7 com8 com9
774 lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9'''.split()
772 lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9'''.split()
775 _winreservedchars = ':*?"<>|'
773 _winreservedchars = ':*?"<>|'
776 def checkwinfilename(path):
774 def checkwinfilename(path):
777 r'''Check that the base-relative path is a valid filename on Windows.
775 r'''Check that the base-relative path is a valid filename on Windows.
778 Returns None if the path is ok, or a UI string describing the problem.
776 Returns None if the path is ok, or a UI string describing the problem.
779
777
780 >>> checkwinfilename("just/a/normal/path")
778 >>> checkwinfilename("just/a/normal/path")
781 >>> checkwinfilename("foo/bar/con.xml")
779 >>> checkwinfilename("foo/bar/con.xml")
782 "filename contains 'con', which is reserved on Windows"
780 "filename contains 'con', which is reserved on Windows"
783 >>> checkwinfilename("foo/con.xml/bar")
781 >>> checkwinfilename("foo/con.xml/bar")
784 "filename contains 'con', which is reserved on Windows"
782 "filename contains 'con', which is reserved on Windows"
785 >>> checkwinfilename("foo/bar/xml.con")
783 >>> checkwinfilename("foo/bar/xml.con")
786 >>> checkwinfilename("foo/bar/AUX/bla.txt")
784 >>> checkwinfilename("foo/bar/AUX/bla.txt")
787 "filename contains 'AUX', which is reserved on Windows"
785 "filename contains 'AUX', which is reserved on Windows"
788 >>> checkwinfilename("foo/bar/bla:.txt")
786 >>> checkwinfilename("foo/bar/bla:.txt")
789 "filename contains ':', which is reserved on Windows"
787 "filename contains ':', which is reserved on Windows"
790 >>> checkwinfilename("foo/bar/b\07la.txt")
788 >>> checkwinfilename("foo/bar/b\07la.txt")
791 "filename contains '\\x07', which is invalid on Windows"
789 "filename contains '\\x07', which is invalid on Windows"
792 >>> checkwinfilename("foo/bar/bla ")
790 >>> checkwinfilename("foo/bar/bla ")
793 "filename ends with ' ', which is not allowed on Windows"
791 "filename ends with ' ', which is not allowed on Windows"
794 >>> checkwinfilename("../bar")
792 >>> checkwinfilename("../bar")
795 >>> checkwinfilename("foo\\")
793 >>> checkwinfilename("foo\\")
796 "filename ends with '\\', which is invalid on Windows"
794 "filename ends with '\\', which is invalid on Windows"
797 >>> checkwinfilename("foo\\/bar")
795 >>> checkwinfilename("foo\\/bar")
798 "directory name ends with '\\', which is invalid on Windows"
796 "directory name ends with '\\', which is invalid on Windows"
799 '''
797 '''
800 if path.endswith('\\'):
798 if path.endswith('\\'):
801 return _("filename ends with '\\', which is invalid on Windows")
799 return _("filename ends with '\\', which is invalid on Windows")
802 if '\\/' in path:
800 if '\\/' in path:
803 return _("directory name ends with '\\', which is invalid on Windows")
801 return _("directory name ends with '\\', which is invalid on Windows")
804 for n in path.replace('\\', '/').split('/'):
802 for n in path.replace('\\', '/').split('/'):
805 if not n:
803 if not n:
806 continue
804 continue
807 for c in n:
805 for c in n:
808 if c in _winreservedchars:
806 if c in _winreservedchars:
809 return _("filename contains '%s', which is reserved "
807 return _("filename contains '%s', which is reserved "
810 "on Windows") % c
808 "on Windows") % c
811 if ord(c) <= 31:
809 if ord(c) <= 31:
812 return _("filename contains %r, which is invalid "
810 return _("filename contains %r, which is invalid "
813 "on Windows") % c
811 "on Windows") % c
814 base = n.split('.')[0]
812 base = n.split('.')[0]
815 if base and base.lower() in _winreservednames:
813 if base and base.lower() in _winreservednames:
816 return _("filename contains '%s', which is reserved "
814 return _("filename contains '%s', which is reserved "
817 "on Windows") % base
815 "on Windows") % base
818 t = n[-1]
816 t = n[-1]
819 if t in '. ' and n not in '..':
817 if t in '. ' and n not in '..':
820 return _("filename ends with '%s', which is not allowed "
818 return _("filename ends with '%s', which is not allowed "
821 "on Windows") % t
819 "on Windows") % t
822
820
823 if os.name == 'nt':
821 if os.name == 'nt':
824 checkosfilename = checkwinfilename
822 checkosfilename = checkwinfilename
825 else:
823 else:
826 checkosfilename = platform.checkosfilename
824 checkosfilename = platform.checkosfilename
827
825
828 def makelock(info, pathname):
826 def makelock(info, pathname):
829 try:
827 try:
830 return os.symlink(info, pathname)
828 return os.symlink(info, pathname)
831 except OSError, why:
829 except OSError, why:
832 if why.errno == errno.EEXIST:
830 if why.errno == errno.EEXIST:
833 raise
831 raise
834 except AttributeError: # no symlink in os
832 except AttributeError: # no symlink in os
835 pass
833 pass
836
834
837 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
835 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
838 os.write(ld, info)
836 os.write(ld, info)
839 os.close(ld)
837 os.close(ld)
840
838
841 def readlock(pathname):
839 def readlock(pathname):
842 try:
840 try:
843 return os.readlink(pathname)
841 return os.readlink(pathname)
844 except OSError, why:
842 except OSError, why:
845 if why.errno not in (errno.EINVAL, errno.ENOSYS):
843 if why.errno not in (errno.EINVAL, errno.ENOSYS):
846 raise
844 raise
847 except AttributeError: # no symlink in os
845 except AttributeError: # no symlink in os
848 pass
846 pass
849 fp = posixfile(pathname)
847 fp = posixfile(pathname)
850 r = fp.read()
848 r = fp.read()
851 fp.close()
849 fp.close()
852 return r
850 return r
853
851
854 def fstat(fp):
852 def fstat(fp):
855 '''stat file object that may not have fileno method.'''
853 '''stat file object that may not have fileno method.'''
856 try:
854 try:
857 return os.fstat(fp.fileno())
855 return os.fstat(fp.fileno())
858 except AttributeError:
856 except AttributeError:
859 return os.stat(fp.name)
857 return os.stat(fp.name)
860
858
861 # File system features
859 # File system features
862
860
863 def checkcase(path):
861 def checkcase(path):
864 """
862 """
865 Return true if the given path is on a case-sensitive filesystem
863 Return true if the given path is on a case-sensitive filesystem
866
864
867 Requires a path (like /foo/.hg) ending with a foldable final
865 Requires a path (like /foo/.hg) ending with a foldable final
868 directory component.
866 directory component.
869 """
867 """
870 s1 = os.lstat(path)
868 s1 = os.lstat(path)
871 d, b = os.path.split(path)
869 d, b = os.path.split(path)
872 b2 = b.upper()
870 b2 = b.upper()
873 if b == b2:
871 if b == b2:
874 b2 = b.lower()
872 b2 = b.lower()
875 if b == b2:
873 if b == b2:
876 return True # no evidence against case sensitivity
874 return True # no evidence against case sensitivity
877 p2 = os.path.join(d, b2)
875 p2 = os.path.join(d, b2)
878 try:
876 try:
879 s2 = os.lstat(p2)
877 s2 = os.lstat(p2)
880 if s2 == s1:
878 if s2 == s1:
881 return False
879 return False
882 return True
880 return True
883 except OSError:
881 except OSError:
884 return True
882 return True
885
883
886 try:
884 try:
887 import re2
885 import re2
888 _re2 = None
886 _re2 = None
889 except ImportError:
887 except ImportError:
890 _re2 = False
888 _re2 = False
891
889
892 class _re(object):
890 class _re(object):
893 def _checkre2(self):
891 def _checkre2(self):
894 global _re2
892 global _re2
895 try:
893 try:
896 # check if match works, see issue3964
894 # check if match works, see issue3964
897 _re2 = bool(re2.match(r'\[([^\[]+)\]', '[ui]'))
895 _re2 = bool(re2.match(r'\[([^\[]+)\]', '[ui]'))
898 except ImportError:
896 except ImportError:
899 _re2 = False
897 _re2 = False
900
898
901 def compile(self, pat, flags=0):
899 def compile(self, pat, flags=0):
902 '''Compile a regular expression, using re2 if possible
900 '''Compile a regular expression, using re2 if possible
903
901
904 For best performance, use only re2-compatible regexp features. The
902 For best performance, use only re2-compatible regexp features. The
905 only flags from the re module that are re2-compatible are
903 only flags from the re module that are re2-compatible are
906 IGNORECASE and MULTILINE.'''
904 IGNORECASE and MULTILINE.'''
907 if _re2 is None:
905 if _re2 is None:
908 self._checkre2()
906 self._checkre2()
909 if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0:
907 if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0:
910 if flags & remod.IGNORECASE:
908 if flags & remod.IGNORECASE:
911 pat = '(?i)' + pat
909 pat = '(?i)' + pat
912 if flags & remod.MULTILINE:
910 if flags & remod.MULTILINE:
913 pat = '(?m)' + pat
911 pat = '(?m)' + pat
914 try:
912 try:
915 return re2.compile(pat)
913 return re2.compile(pat)
916 except re2.error:
914 except re2.error:
917 pass
915 pass
918 return remod.compile(pat, flags)
916 return remod.compile(pat, flags)
919
917
920 @propertycache
918 @propertycache
921 def escape(self):
919 def escape(self):
922 '''Return the version of escape corresponding to self.compile.
920 '''Return the version of escape corresponding to self.compile.
923
921
924 This is imperfect because whether re2 or re is used for a particular
922 This is imperfect because whether re2 or re is used for a particular
925 function depends on the flags, etc, but it's the best we can do.
923 function depends on the flags, etc, but it's the best we can do.
926 '''
924 '''
927 global _re2
925 global _re2
928 if _re2 is None:
926 if _re2 is None:
929 self._checkre2()
927 self._checkre2()
930 if _re2:
928 if _re2:
931 return re2.escape
929 return re2.escape
932 else:
930 else:
933 return remod.escape
931 return remod.escape
934
932
935 re = _re()
933 re = _re()
936
934
937 _fspathcache = {}
935 _fspathcache = {}
938 def fspath(name, root):
936 def fspath(name, root):
939 '''Get name in the case stored in the filesystem
937 '''Get name in the case stored in the filesystem
940
938
941 The name should be relative to root, and be normcase-ed for efficiency.
939 The name should be relative to root, and be normcase-ed for efficiency.
942
940
943 Note that this function is unnecessary, and should not be
941 Note that this function is unnecessary, and should not be
944 called, for case-sensitive filesystems (simply because it's expensive).
942 called, for case-sensitive filesystems (simply because it's expensive).
945
943
946 The root should be normcase-ed, too.
944 The root should be normcase-ed, too.
947 '''
945 '''
948 def _makefspathcacheentry(dir):
946 def _makefspathcacheentry(dir):
949 return dict((normcase(n), n) for n in os.listdir(dir))
947 return dict((normcase(n), n) for n in os.listdir(dir))
950
948
951 seps = os.sep
949 seps = os.sep
952 if os.altsep:
950 if os.altsep:
953 seps = seps + os.altsep
951 seps = seps + os.altsep
954 # Protect backslashes. This gets silly very quickly.
952 # Protect backslashes. This gets silly very quickly.
955 seps.replace('\\','\\\\')
953 seps.replace('\\','\\\\')
956 pattern = remod.compile(r'([^%s]+)|([%s]+)' % (seps, seps))
954 pattern = remod.compile(r'([^%s]+)|([%s]+)' % (seps, seps))
957 dir = os.path.normpath(root)
955 dir = os.path.normpath(root)
958 result = []
956 result = []
959 for part, sep in pattern.findall(name):
957 for part, sep in pattern.findall(name):
960 if sep:
958 if sep:
961 result.append(sep)
959 result.append(sep)
962 continue
960 continue
963
961
964 if dir not in _fspathcache:
962 if dir not in _fspathcache:
965 _fspathcache[dir] = _makefspathcacheentry(dir)
963 _fspathcache[dir] = _makefspathcacheentry(dir)
966 contents = _fspathcache[dir]
964 contents = _fspathcache[dir]
967
965
968 found = contents.get(part)
966 found = contents.get(part)
969 if not found:
967 if not found:
970 # retry "once per directory" per "dirstate.walk" which
968 # retry "once per directory" per "dirstate.walk" which
971 # may take place for each patches of "hg qpush", for example
969 # may take place for each patches of "hg qpush", for example
972 _fspathcache[dir] = contents = _makefspathcacheentry(dir)
970 _fspathcache[dir] = contents = _makefspathcacheentry(dir)
973 found = contents.get(part)
971 found = contents.get(part)
974
972
975 result.append(found or part)
973 result.append(found or part)
976 dir = os.path.join(dir, part)
974 dir = os.path.join(dir, part)
977
975
978 return ''.join(result)
976 return ''.join(result)
979
977
980 def checknlink(testfile):
978 def checknlink(testfile):
981 '''check whether hardlink count reporting works properly'''
979 '''check whether hardlink count reporting works properly'''
982
980
983 # testfile may be open, so we need a separate file for checking to
981 # testfile may be open, so we need a separate file for checking to
984 # work around issue2543 (or testfile may get lost on Samba shares)
982 # work around issue2543 (or testfile may get lost on Samba shares)
985 f1 = testfile + ".hgtmp1"
983 f1 = testfile + ".hgtmp1"
986 if os.path.lexists(f1):
984 if os.path.lexists(f1):
987 return False
985 return False
988 try:
986 try:
989 posixfile(f1, 'w').close()
987 posixfile(f1, 'w').close()
990 except IOError:
988 except IOError:
991 return False
989 return False
992
990
993 f2 = testfile + ".hgtmp2"
991 f2 = testfile + ".hgtmp2"
994 fd = None
992 fd = None
995 try:
993 try:
996 oslink(f1, f2)
994 oslink(f1, f2)
997 # nlinks() may behave differently for files on Windows shares if
995 # nlinks() may behave differently for files on Windows shares if
998 # the file is open.
996 # the file is open.
999 fd = posixfile(f2)
997 fd = posixfile(f2)
1000 return nlinks(f2) > 1
998 return nlinks(f2) > 1
1001 except OSError:
999 except OSError:
1002 return False
1000 return False
1003 finally:
1001 finally:
1004 if fd is not None:
1002 if fd is not None:
1005 fd.close()
1003 fd.close()
1006 for f in (f1, f2):
1004 for f in (f1, f2):
1007 try:
1005 try:
1008 os.unlink(f)
1006 os.unlink(f)
1009 except OSError:
1007 except OSError:
1010 pass
1008 pass
1011
1009
1012 def endswithsep(path):
1010 def endswithsep(path):
1013 '''Check path ends with os.sep or os.altsep.'''
1011 '''Check path ends with os.sep or os.altsep.'''
1014 return path.endswith(os.sep) or os.altsep and path.endswith(os.altsep)
1012 return path.endswith(os.sep) or os.altsep and path.endswith(os.altsep)
1015
1013
1016 def splitpath(path):
1014 def splitpath(path):
1017 '''Split path by os.sep.
1015 '''Split path by os.sep.
1018 Note that this function does not use os.altsep because this is
1016 Note that this function does not use os.altsep because this is
1019 an alternative of simple "xxx.split(os.sep)".
1017 an alternative of simple "xxx.split(os.sep)".
1020 It is recommended to use os.path.normpath() before using this
1018 It is recommended to use os.path.normpath() before using this
1021 function if need.'''
1019 function if need.'''
1022 return path.split(os.sep)
1020 return path.split(os.sep)
1023
1021
1024 def gui():
1022 def gui():
1025 '''Are we running in a GUI?'''
1023 '''Are we running in a GUI?'''
1026 if sys.platform == 'darwin':
1024 if sys.platform == 'darwin':
1027 if 'SSH_CONNECTION' in os.environ:
1025 if 'SSH_CONNECTION' in os.environ:
1028 # handle SSH access to a box where the user is logged in
1026 # handle SSH access to a box where the user is logged in
1029 return False
1027 return False
1030 elif getattr(osutil, 'isgui', None):
1028 elif getattr(osutil, 'isgui', None):
1031 # check if a CoreGraphics session is available
1029 # check if a CoreGraphics session is available
1032 return osutil.isgui()
1030 return osutil.isgui()
1033 else:
1031 else:
1034 # pure build; use a safe default
1032 # pure build; use a safe default
1035 return True
1033 return True
1036 else:
1034 else:
1037 return os.name == "nt" or os.environ.get("DISPLAY")
1035 return os.name == "nt" or os.environ.get("DISPLAY")
1038
1036
1039 def mktempcopy(name, emptyok=False, createmode=None):
1037 def mktempcopy(name, emptyok=False, createmode=None):
1040 """Create a temporary file with the same contents from name
1038 """Create a temporary file with the same contents from name
1041
1039
1042 The permission bits are copied from the original file.
1040 The permission bits are copied from the original file.
1043
1041
1044 If the temporary file is going to be truncated immediately, you
1042 If the temporary file is going to be truncated immediately, you
1045 can use emptyok=True as an optimization.
1043 can use emptyok=True as an optimization.
1046
1044
1047 Returns the name of the temporary file.
1045 Returns the name of the temporary file.
1048 """
1046 """
1049 d, fn = os.path.split(name)
1047 d, fn = os.path.split(name)
1050 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
1048 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
1051 os.close(fd)
1049 os.close(fd)
1052 # Temporary files are created with mode 0600, which is usually not
1050 # Temporary files are created with mode 0600, which is usually not
1053 # what we want. If the original file already exists, just copy
1051 # what we want. If the original file already exists, just copy
1054 # its mode. Otherwise, manually obey umask.
1052 # its mode. Otherwise, manually obey umask.
1055 copymode(name, temp, createmode)
1053 copymode(name, temp, createmode)
1056 if emptyok:
1054 if emptyok:
1057 return temp
1055 return temp
1058 try:
1056 try:
1059 try:
1057 try:
1060 ifp = posixfile(name, "rb")
1058 ifp = posixfile(name, "rb")
1061 except IOError, inst:
1059 except IOError, inst:
1062 if inst.errno == errno.ENOENT:
1060 if inst.errno == errno.ENOENT:
1063 return temp
1061 return temp
1064 if not getattr(inst, 'filename', None):
1062 if not getattr(inst, 'filename', None):
1065 inst.filename = name
1063 inst.filename = name
1066 raise
1064 raise
1067 ofp = posixfile(temp, "wb")
1065 ofp = posixfile(temp, "wb")
1068 for chunk in filechunkiter(ifp):
1066 for chunk in filechunkiter(ifp):
1069 ofp.write(chunk)
1067 ofp.write(chunk)
1070 ifp.close()
1068 ifp.close()
1071 ofp.close()
1069 ofp.close()
1072 except: # re-raises
1070 except: # re-raises
1073 try: os.unlink(temp)
1071 try: os.unlink(temp)
1074 except OSError: pass
1072 except OSError: pass
1075 raise
1073 raise
1076 return temp
1074 return temp
1077
1075
1078 class atomictempfile(object):
1076 class atomictempfile(object):
1079 '''writable file object that atomically updates a file
1077 '''writable file object that atomically updates a file
1080
1078
1081 All writes will go to a temporary copy of the original file. Call
1079 All writes will go to a temporary copy of the original file. Call
1082 close() when you are done writing, and atomictempfile will rename
1080 close() when you are done writing, and atomictempfile will rename
1083 the temporary copy to the original name, making the changes
1081 the temporary copy to the original name, making the changes
1084 visible. If the object is destroyed without being closed, all your
1082 visible. If the object is destroyed without being closed, all your
1085 writes are discarded.
1083 writes are discarded.
1086 '''
1084 '''
1087 def __init__(self, name, mode='w+b', createmode=None):
1085 def __init__(self, name, mode='w+b', createmode=None):
1088 self.__name = name # permanent name
1086 self.__name = name # permanent name
1089 self._tempname = mktempcopy(name, emptyok=('w' in mode),
1087 self._tempname = mktempcopy(name, emptyok=('w' in mode),
1090 createmode=createmode)
1088 createmode=createmode)
1091 self._fp = posixfile(self._tempname, mode)
1089 self._fp = posixfile(self._tempname, mode)
1092
1090
1093 # delegated methods
1091 # delegated methods
1094 self.write = self._fp.write
1092 self.write = self._fp.write
1095 self.seek = self._fp.seek
1093 self.seek = self._fp.seek
1096 self.tell = self._fp.tell
1094 self.tell = self._fp.tell
1097 self.fileno = self._fp.fileno
1095 self.fileno = self._fp.fileno
1098
1096
1099 def close(self):
1097 def close(self):
1100 if not self._fp.closed:
1098 if not self._fp.closed:
1101 self._fp.close()
1099 self._fp.close()
1102 rename(self._tempname, localpath(self.__name))
1100 rename(self._tempname, localpath(self.__name))
1103
1101
1104 def discard(self):
1102 def discard(self):
1105 if not self._fp.closed:
1103 if not self._fp.closed:
1106 try:
1104 try:
1107 os.unlink(self._tempname)
1105 os.unlink(self._tempname)
1108 except OSError:
1106 except OSError:
1109 pass
1107 pass
1110 self._fp.close()
1108 self._fp.close()
1111
1109
1112 def __del__(self):
1110 def __del__(self):
1113 if safehasattr(self, '_fp'): # constructor actually did something
1111 if safehasattr(self, '_fp'): # constructor actually did something
1114 self.discard()
1112 self.discard()
1115
1113
1116 def makedirs(name, mode=None, notindexed=False):
1114 def makedirs(name, mode=None, notindexed=False):
1117 """recursive directory creation with parent mode inheritance"""
1115 """recursive directory creation with parent mode inheritance"""
1118 try:
1116 try:
1119 makedir(name, notindexed)
1117 makedir(name, notindexed)
1120 except OSError, err:
1118 except OSError, err:
1121 if err.errno == errno.EEXIST:
1119 if err.errno == errno.EEXIST:
1122 return
1120 return
1123 if err.errno != errno.ENOENT or not name:
1121 if err.errno != errno.ENOENT or not name:
1124 raise
1122 raise
1125 parent = os.path.dirname(os.path.abspath(name))
1123 parent = os.path.dirname(os.path.abspath(name))
1126 if parent == name:
1124 if parent == name:
1127 raise
1125 raise
1128 makedirs(parent, mode, notindexed)
1126 makedirs(parent, mode, notindexed)
1129 makedir(name, notindexed)
1127 makedir(name, notindexed)
1130 if mode is not None:
1128 if mode is not None:
1131 os.chmod(name, mode)
1129 os.chmod(name, mode)
1132
1130
1133 def ensuredirs(name, mode=None, notindexed=False):
1131 def ensuredirs(name, mode=None, notindexed=False):
1134 """race-safe recursive directory creation
1132 """race-safe recursive directory creation
1135
1133
1136 Newly created directories are marked as "not to be indexed by
1134 Newly created directories are marked as "not to be indexed by
1137 the content indexing service", if ``notindexed`` is specified
1135 the content indexing service", if ``notindexed`` is specified
1138 for "write" mode access.
1136 for "write" mode access.
1139 """
1137 """
1140 if os.path.isdir(name):
1138 if os.path.isdir(name):
1141 return
1139 return
1142 parent = os.path.dirname(os.path.abspath(name))
1140 parent = os.path.dirname(os.path.abspath(name))
1143 if parent != name:
1141 if parent != name:
1144 ensuredirs(parent, mode, notindexed)
1142 ensuredirs(parent, mode, notindexed)
1145 try:
1143 try:
1146 makedir(name, notindexed)
1144 makedir(name, notindexed)
1147 except OSError, err:
1145 except OSError, err:
1148 if err.errno == errno.EEXIST and os.path.isdir(name):
1146 if err.errno == errno.EEXIST and os.path.isdir(name):
1149 # someone else seems to have won a directory creation race
1147 # someone else seems to have won a directory creation race
1150 return
1148 return
1151 raise
1149 raise
1152 if mode is not None:
1150 if mode is not None:
1153 os.chmod(name, mode)
1151 os.chmod(name, mode)
1154
1152
1155 def readfile(path):
1153 def readfile(path):
1156 fp = open(path, 'rb')
1154 fp = open(path, 'rb')
1157 try:
1155 try:
1158 return fp.read()
1156 return fp.read()
1159 finally:
1157 finally:
1160 fp.close()
1158 fp.close()
1161
1159
1162 def writefile(path, text):
1160 def writefile(path, text):
1163 fp = open(path, 'wb')
1161 fp = open(path, 'wb')
1164 try:
1162 try:
1165 fp.write(text)
1163 fp.write(text)
1166 finally:
1164 finally:
1167 fp.close()
1165 fp.close()
1168
1166
1169 def appendfile(path, text):
1167 def appendfile(path, text):
1170 fp = open(path, 'ab')
1168 fp = open(path, 'ab')
1171 try:
1169 try:
1172 fp.write(text)
1170 fp.write(text)
1173 finally:
1171 finally:
1174 fp.close()
1172 fp.close()
1175
1173
1176 class chunkbuffer(object):
1174 class chunkbuffer(object):
1177 """Allow arbitrary sized chunks of data to be efficiently read from an
1175 """Allow arbitrary sized chunks of data to be efficiently read from an
1178 iterator over chunks of arbitrary size."""
1176 iterator over chunks of arbitrary size."""
1179
1177
1180 def __init__(self, in_iter):
1178 def __init__(self, in_iter):
1181 """in_iter is the iterator that's iterating over the input chunks.
1179 """in_iter is the iterator that's iterating over the input chunks.
1182 targetsize is how big a buffer to try to maintain."""
1180 targetsize is how big a buffer to try to maintain."""
1183 def splitbig(chunks):
1181 def splitbig(chunks):
1184 for chunk in chunks:
1182 for chunk in chunks:
1185 if len(chunk) > 2**20:
1183 if len(chunk) > 2**20:
1186 pos = 0
1184 pos = 0
1187 while pos < len(chunk):
1185 while pos < len(chunk):
1188 end = pos + 2 ** 18
1186 end = pos + 2 ** 18
1189 yield chunk[pos:end]
1187 yield chunk[pos:end]
1190 pos = end
1188 pos = end
1191 else:
1189 else:
1192 yield chunk
1190 yield chunk
1193 self.iter = splitbig(in_iter)
1191 self.iter = splitbig(in_iter)
1194 self._queue = deque()
1192 self._queue = collections.deque()
1195
1193
1196 def read(self, l=None):
1194 def read(self, l=None):
1197 """Read L bytes of data from the iterator of chunks of data.
1195 """Read L bytes of data from the iterator of chunks of data.
1198 Returns less than L bytes if the iterator runs dry.
1196 Returns less than L bytes if the iterator runs dry.
1199
1197
1200 If size parameter is omitted, read everything"""
1198 If size parameter is omitted, read everything"""
1201 left = l
1199 left = l
1202 buf = []
1200 buf = []
1203 queue = self._queue
1201 queue = self._queue
1204 while left is None or left > 0:
1202 while left is None or left > 0:
1205 # refill the queue
1203 # refill the queue
1206 if not queue:
1204 if not queue:
1207 target = 2**18
1205 target = 2**18
1208 for chunk in self.iter:
1206 for chunk in self.iter:
1209 queue.append(chunk)
1207 queue.append(chunk)
1210 target -= len(chunk)
1208 target -= len(chunk)
1211 if target <= 0:
1209 if target <= 0:
1212 break
1210 break
1213 if not queue:
1211 if not queue:
1214 break
1212 break
1215
1213
1216 chunk = queue.popleft()
1214 chunk = queue.popleft()
1217 if left is not None:
1215 if left is not None:
1218 left -= len(chunk)
1216 left -= len(chunk)
1219 if left is not None and left < 0:
1217 if left is not None and left < 0:
1220 queue.appendleft(chunk[left:])
1218 queue.appendleft(chunk[left:])
1221 buf.append(chunk[:left])
1219 buf.append(chunk[:left])
1222 else:
1220 else:
1223 buf.append(chunk)
1221 buf.append(chunk)
1224
1222
1225 return ''.join(buf)
1223 return ''.join(buf)
1226
1224
1227 def filechunkiter(f, size=65536, limit=None):
1225 def filechunkiter(f, size=65536, limit=None):
1228 """Create a generator that produces the data in the file size
1226 """Create a generator that produces the data in the file size
1229 (default 65536) bytes at a time, up to optional limit (default is
1227 (default 65536) bytes at a time, up to optional limit (default is
1230 to read all data). Chunks may be less than size bytes if the
1228 to read all data). Chunks may be less than size bytes if the
1231 chunk is the last chunk in the file, or the file is a socket or
1229 chunk is the last chunk in the file, or the file is a socket or
1232 some other type of file that sometimes reads less data than is
1230 some other type of file that sometimes reads less data than is
1233 requested."""
1231 requested."""
1234 assert size >= 0
1232 assert size >= 0
1235 assert limit is None or limit >= 0
1233 assert limit is None or limit >= 0
1236 while True:
1234 while True:
1237 if limit is None:
1235 if limit is None:
1238 nbytes = size
1236 nbytes = size
1239 else:
1237 else:
1240 nbytes = min(limit, size)
1238 nbytes = min(limit, size)
1241 s = nbytes and f.read(nbytes)
1239 s = nbytes and f.read(nbytes)
1242 if not s:
1240 if not s:
1243 break
1241 break
1244 if limit:
1242 if limit:
1245 limit -= len(s)
1243 limit -= len(s)
1246 yield s
1244 yield s
1247
1245
1248 def makedate(timestamp=None):
1246 def makedate(timestamp=None):
1249 '''Return a unix timestamp (or the current time) as a (unixtime,
1247 '''Return a unix timestamp (or the current time) as a (unixtime,
1250 offset) tuple based off the local timezone.'''
1248 offset) tuple based off the local timezone.'''
1251 if timestamp is None:
1249 if timestamp is None:
1252 timestamp = time.time()
1250 timestamp = time.time()
1253 if timestamp < 0:
1251 if timestamp < 0:
1254 hint = _("check your clock")
1252 hint = _("check your clock")
1255 raise Abort(_("negative timestamp: %d") % timestamp, hint=hint)
1253 raise Abort(_("negative timestamp: %d") % timestamp, hint=hint)
1256 delta = (datetime.datetime.utcfromtimestamp(timestamp) -
1254 delta = (datetime.datetime.utcfromtimestamp(timestamp) -
1257 datetime.datetime.fromtimestamp(timestamp))
1255 datetime.datetime.fromtimestamp(timestamp))
1258 tz = delta.days * 86400 + delta.seconds
1256 tz = delta.days * 86400 + delta.seconds
1259 return timestamp, tz
1257 return timestamp, tz
1260
1258
1261 def datestr(date=None, format='%a %b %d %H:%M:%S %Y %1%2'):
1259 def datestr(date=None, format='%a %b %d %H:%M:%S %Y %1%2'):
1262 """represent a (unixtime, offset) tuple as a localized time.
1260 """represent a (unixtime, offset) tuple as a localized time.
1263 unixtime is seconds since the epoch, and offset is the time zone's
1261 unixtime is seconds since the epoch, and offset is the time zone's
1264 number of seconds away from UTC. if timezone is false, do not
1262 number of seconds away from UTC. if timezone is false, do not
1265 append time zone to string."""
1263 append time zone to string."""
1266 t, tz = date or makedate()
1264 t, tz = date or makedate()
1267 if t < 0:
1265 if t < 0:
1268 t = 0 # time.gmtime(lt) fails on Windows for lt < -43200
1266 t = 0 # time.gmtime(lt) fails on Windows for lt < -43200
1269 tz = 0
1267 tz = 0
1270 if "%1" in format or "%2" in format or "%z" in format:
1268 if "%1" in format or "%2" in format or "%z" in format:
1271 sign = (tz > 0) and "-" or "+"
1269 sign = (tz > 0) and "-" or "+"
1272 minutes = abs(tz) // 60
1270 minutes = abs(tz) // 60
1273 format = format.replace("%z", "%1%2")
1271 format = format.replace("%z", "%1%2")
1274 format = format.replace("%1", "%c%02d" % (sign, minutes // 60))
1272 format = format.replace("%1", "%c%02d" % (sign, minutes // 60))
1275 format = format.replace("%2", "%02d" % (minutes % 60))
1273 format = format.replace("%2", "%02d" % (minutes % 60))
1276 try:
1274 try:
1277 t = time.gmtime(float(t) - tz)
1275 t = time.gmtime(float(t) - tz)
1278 except ValueError:
1276 except ValueError:
1279 # time was out of range
1277 # time was out of range
1280 t = time.gmtime(sys.maxint)
1278 t = time.gmtime(sys.maxint)
1281 s = time.strftime(format, t)
1279 s = time.strftime(format, t)
1282 return s
1280 return s
1283
1281
1284 def shortdate(date=None):
1282 def shortdate(date=None):
1285 """turn (timestamp, tzoff) tuple into iso 8631 date."""
1283 """turn (timestamp, tzoff) tuple into iso 8631 date."""
1286 return datestr(date, format='%Y-%m-%d')
1284 return datestr(date, format='%Y-%m-%d')
1287
1285
1288 def strdate(string, format, defaults=[]):
1286 def strdate(string, format, defaults=[]):
1289 """parse a localized time string and return a (unixtime, offset) tuple.
1287 """parse a localized time string and return a (unixtime, offset) tuple.
1290 if the string cannot be parsed, ValueError is raised."""
1288 if the string cannot be parsed, ValueError is raised."""
1291 def timezone(string):
1289 def timezone(string):
1292 tz = string.split()[-1]
1290 tz = string.split()[-1]
1293 if tz[0] in "+-" and len(tz) == 5 and tz[1:].isdigit():
1291 if tz[0] in "+-" and len(tz) == 5 and tz[1:].isdigit():
1294 sign = (tz[0] == "+") and 1 or -1
1292 sign = (tz[0] == "+") and 1 or -1
1295 hours = int(tz[1:3])
1293 hours = int(tz[1:3])
1296 minutes = int(tz[3:5])
1294 minutes = int(tz[3:5])
1297 return -sign * (hours * 60 + minutes) * 60
1295 return -sign * (hours * 60 + minutes) * 60
1298 if tz == "GMT" or tz == "UTC":
1296 if tz == "GMT" or tz == "UTC":
1299 return 0
1297 return 0
1300 return None
1298 return None
1301
1299
1302 # NOTE: unixtime = localunixtime + offset
1300 # NOTE: unixtime = localunixtime + offset
1303 offset, date = timezone(string), string
1301 offset, date = timezone(string), string
1304 if offset is not None:
1302 if offset is not None:
1305 date = " ".join(string.split()[:-1])
1303 date = " ".join(string.split()[:-1])
1306
1304
1307 # add missing elements from defaults
1305 # add missing elements from defaults
1308 usenow = False # default to using biased defaults
1306 usenow = False # default to using biased defaults
1309 for part in ("S", "M", "HI", "d", "mb", "yY"): # decreasing specificity
1307 for part in ("S", "M", "HI", "d", "mb", "yY"): # decreasing specificity
1310 found = [True for p in part if ("%"+p) in format]
1308 found = [True for p in part if ("%"+p) in format]
1311 if not found:
1309 if not found:
1312 date += "@" + defaults[part][usenow]
1310 date += "@" + defaults[part][usenow]
1313 format += "@%" + part[0]
1311 format += "@%" + part[0]
1314 else:
1312 else:
1315 # We've found a specific time element, less specific time
1313 # We've found a specific time element, less specific time
1316 # elements are relative to today
1314 # elements are relative to today
1317 usenow = True
1315 usenow = True
1318
1316
1319 timetuple = time.strptime(date, format)
1317 timetuple = time.strptime(date, format)
1320 localunixtime = int(calendar.timegm(timetuple))
1318 localunixtime = int(calendar.timegm(timetuple))
1321 if offset is None:
1319 if offset is None:
1322 # local timezone
1320 # local timezone
1323 unixtime = int(time.mktime(timetuple))
1321 unixtime = int(time.mktime(timetuple))
1324 offset = unixtime - localunixtime
1322 offset = unixtime - localunixtime
1325 else:
1323 else:
1326 unixtime = localunixtime + offset
1324 unixtime = localunixtime + offset
1327 return unixtime, offset
1325 return unixtime, offset
1328
1326
1329 def parsedate(date, formats=None, bias={}):
1327 def parsedate(date, formats=None, bias={}):
1330 """parse a localized date/time and return a (unixtime, offset) tuple.
1328 """parse a localized date/time and return a (unixtime, offset) tuple.
1331
1329
1332 The date may be a "unixtime offset" string or in one of the specified
1330 The date may be a "unixtime offset" string or in one of the specified
1333 formats. If the date already is a (unixtime, offset) tuple, it is returned.
1331 formats. If the date already is a (unixtime, offset) tuple, it is returned.
1334
1332
1335 >>> parsedate(' today ') == parsedate(\
1333 >>> parsedate(' today ') == parsedate(\
1336 datetime.date.today().strftime('%b %d'))
1334 datetime.date.today().strftime('%b %d'))
1337 True
1335 True
1338 >>> parsedate( 'yesterday ') == parsedate((datetime.date.today() -\
1336 >>> parsedate( 'yesterday ') == parsedate((datetime.date.today() -\
1339 datetime.timedelta(days=1)\
1337 datetime.timedelta(days=1)\
1340 ).strftime('%b %d'))
1338 ).strftime('%b %d'))
1341 True
1339 True
1342 >>> now, tz = makedate()
1340 >>> now, tz = makedate()
1343 >>> strnow, strtz = parsedate('now')
1341 >>> strnow, strtz = parsedate('now')
1344 >>> (strnow - now) < 1
1342 >>> (strnow - now) < 1
1345 True
1343 True
1346 >>> tz == strtz
1344 >>> tz == strtz
1347 True
1345 True
1348 """
1346 """
1349 if not date:
1347 if not date:
1350 return 0, 0
1348 return 0, 0
1351 if isinstance(date, tuple) and len(date) == 2:
1349 if isinstance(date, tuple) and len(date) == 2:
1352 return date
1350 return date
1353 if not formats:
1351 if not formats:
1354 formats = defaultdateformats
1352 formats = defaultdateformats
1355 date = date.strip()
1353 date = date.strip()
1356
1354
1357 if date == 'now' or date == _('now'):
1355 if date == 'now' or date == _('now'):
1358 return makedate()
1356 return makedate()
1359 if date == 'today' or date == _('today'):
1357 if date == 'today' or date == _('today'):
1360 date = datetime.date.today().strftime('%b %d')
1358 date = datetime.date.today().strftime('%b %d')
1361 elif date == 'yesterday' or date == _('yesterday'):
1359 elif date == 'yesterday' or date == _('yesterday'):
1362 date = (datetime.date.today() -
1360 date = (datetime.date.today() -
1363 datetime.timedelta(days=1)).strftime('%b %d')
1361 datetime.timedelta(days=1)).strftime('%b %d')
1364
1362
1365 try:
1363 try:
1366 when, offset = map(int, date.split(' '))
1364 when, offset = map(int, date.split(' '))
1367 except ValueError:
1365 except ValueError:
1368 # fill out defaults
1366 # fill out defaults
1369 now = makedate()
1367 now = makedate()
1370 defaults = {}
1368 defaults = {}
1371 for part in ("d", "mb", "yY", "HI", "M", "S"):
1369 for part in ("d", "mb", "yY", "HI", "M", "S"):
1372 # this piece is for rounding the specific end of unknowns
1370 # this piece is for rounding the specific end of unknowns
1373 b = bias.get(part)
1371 b = bias.get(part)
1374 if b is None:
1372 if b is None:
1375 if part[0] in "HMS":
1373 if part[0] in "HMS":
1376 b = "00"
1374 b = "00"
1377 else:
1375 else:
1378 b = "0"
1376 b = "0"
1379
1377
1380 # this piece is for matching the generic end to today's date
1378 # this piece is for matching the generic end to today's date
1381 n = datestr(now, "%" + part[0])
1379 n = datestr(now, "%" + part[0])
1382
1380
1383 defaults[part] = (b, n)
1381 defaults[part] = (b, n)
1384
1382
1385 for format in formats:
1383 for format in formats:
1386 try:
1384 try:
1387 when, offset = strdate(date, format, defaults)
1385 when, offset = strdate(date, format, defaults)
1388 except (ValueError, OverflowError):
1386 except (ValueError, OverflowError):
1389 pass
1387 pass
1390 else:
1388 else:
1391 break
1389 break
1392 else:
1390 else:
1393 raise Abort(_('invalid date: %r') % date)
1391 raise Abort(_('invalid date: %r') % date)
1394 # validate explicit (probably user-specified) date and
1392 # validate explicit (probably user-specified) date and
1395 # time zone offset. values must fit in signed 32 bits for
1393 # time zone offset. values must fit in signed 32 bits for
1396 # current 32-bit linux runtimes. timezones go from UTC-12
1394 # current 32-bit linux runtimes. timezones go from UTC-12
1397 # to UTC+14
1395 # to UTC+14
1398 if abs(when) > 0x7fffffff:
1396 if abs(when) > 0x7fffffff:
1399 raise Abort(_('date exceeds 32 bits: %d') % when)
1397 raise Abort(_('date exceeds 32 bits: %d') % when)
1400 if when < 0:
1398 if when < 0:
1401 raise Abort(_('negative date value: %d') % when)
1399 raise Abort(_('negative date value: %d') % when)
1402 if offset < -50400 or offset > 43200:
1400 if offset < -50400 or offset > 43200:
1403 raise Abort(_('impossible time zone offset: %d') % offset)
1401 raise Abort(_('impossible time zone offset: %d') % offset)
1404 return when, offset
1402 return when, offset
1405
1403
1406 def matchdate(date):
1404 def matchdate(date):
1407 """Return a function that matches a given date match specifier
1405 """Return a function that matches a given date match specifier
1408
1406
1409 Formats include:
1407 Formats include:
1410
1408
1411 '{date}' match a given date to the accuracy provided
1409 '{date}' match a given date to the accuracy provided
1412
1410
1413 '<{date}' on or before a given date
1411 '<{date}' on or before a given date
1414
1412
1415 '>{date}' on or after a given date
1413 '>{date}' on or after a given date
1416
1414
1417 >>> p1 = parsedate("10:29:59")
1415 >>> p1 = parsedate("10:29:59")
1418 >>> p2 = parsedate("10:30:00")
1416 >>> p2 = parsedate("10:30:00")
1419 >>> p3 = parsedate("10:30:59")
1417 >>> p3 = parsedate("10:30:59")
1420 >>> p4 = parsedate("10:31:00")
1418 >>> p4 = parsedate("10:31:00")
1421 >>> p5 = parsedate("Sep 15 10:30:00 1999")
1419 >>> p5 = parsedate("Sep 15 10:30:00 1999")
1422 >>> f = matchdate("10:30")
1420 >>> f = matchdate("10:30")
1423 >>> f(p1[0])
1421 >>> f(p1[0])
1424 False
1422 False
1425 >>> f(p2[0])
1423 >>> f(p2[0])
1426 True
1424 True
1427 >>> f(p3[0])
1425 >>> f(p3[0])
1428 True
1426 True
1429 >>> f(p4[0])
1427 >>> f(p4[0])
1430 False
1428 False
1431 >>> f(p5[0])
1429 >>> f(p5[0])
1432 False
1430 False
1433 """
1431 """
1434
1432
1435 def lower(date):
1433 def lower(date):
1436 d = {'mb': "1", 'd': "1"}
1434 d = {'mb': "1", 'd': "1"}
1437 return parsedate(date, extendeddateformats, d)[0]
1435 return parsedate(date, extendeddateformats, d)[0]
1438
1436
1439 def upper(date):
1437 def upper(date):
1440 d = {'mb': "12", 'HI': "23", 'M': "59", 'S': "59"}
1438 d = {'mb': "12", 'HI': "23", 'M': "59", 'S': "59"}
1441 for days in ("31", "30", "29"):
1439 for days in ("31", "30", "29"):
1442 try:
1440 try:
1443 d["d"] = days
1441 d["d"] = days
1444 return parsedate(date, extendeddateformats, d)[0]
1442 return parsedate(date, extendeddateformats, d)[0]
1445 except Abort:
1443 except Abort:
1446 pass
1444 pass
1447 d["d"] = "28"
1445 d["d"] = "28"
1448 return parsedate(date, extendeddateformats, d)[0]
1446 return parsedate(date, extendeddateformats, d)[0]
1449
1447
1450 date = date.strip()
1448 date = date.strip()
1451
1449
1452 if not date:
1450 if not date:
1453 raise Abort(_("dates cannot consist entirely of whitespace"))
1451 raise Abort(_("dates cannot consist entirely of whitespace"))
1454 elif date[0] == "<":
1452 elif date[0] == "<":
1455 if not date[1:]:
1453 if not date[1:]:
1456 raise Abort(_("invalid day spec, use '<DATE'"))
1454 raise Abort(_("invalid day spec, use '<DATE'"))
1457 when = upper(date[1:])
1455 when = upper(date[1:])
1458 return lambda x: x <= when
1456 return lambda x: x <= when
1459 elif date[0] == ">":
1457 elif date[0] == ">":
1460 if not date[1:]:
1458 if not date[1:]:
1461 raise Abort(_("invalid day spec, use '>DATE'"))
1459 raise Abort(_("invalid day spec, use '>DATE'"))
1462 when = lower(date[1:])
1460 when = lower(date[1:])
1463 return lambda x: x >= when
1461 return lambda x: x >= when
1464 elif date[0] == "-":
1462 elif date[0] == "-":
1465 try:
1463 try:
1466 days = int(date[1:])
1464 days = int(date[1:])
1467 except ValueError:
1465 except ValueError:
1468 raise Abort(_("invalid day spec: %s") % date[1:])
1466 raise Abort(_("invalid day spec: %s") % date[1:])
1469 if days < 0:
1467 if days < 0:
1470 raise Abort(_('%s must be nonnegative (see "hg help dates")')
1468 raise Abort(_('%s must be nonnegative (see "hg help dates")')
1471 % date[1:])
1469 % date[1:])
1472 when = makedate()[0] - days * 3600 * 24
1470 when = makedate()[0] - days * 3600 * 24
1473 return lambda x: x >= when
1471 return lambda x: x >= when
1474 elif " to " in date:
1472 elif " to " in date:
1475 a, b = date.split(" to ")
1473 a, b = date.split(" to ")
1476 start, stop = lower(a), upper(b)
1474 start, stop = lower(a), upper(b)
1477 return lambda x: x >= start and x <= stop
1475 return lambda x: x >= start and x <= stop
1478 else:
1476 else:
1479 start, stop = lower(date), upper(date)
1477 start, stop = lower(date), upper(date)
1480 return lambda x: x >= start and x <= stop
1478 return lambda x: x >= start and x <= stop
1481
1479
1482 def shortuser(user):
1480 def shortuser(user):
1483 """Return a short representation of a user name or email address."""
1481 """Return a short representation of a user name or email address."""
1484 f = user.find('@')
1482 f = user.find('@')
1485 if f >= 0:
1483 if f >= 0:
1486 user = user[:f]
1484 user = user[:f]
1487 f = user.find('<')
1485 f = user.find('<')
1488 if f >= 0:
1486 if f >= 0:
1489 user = user[f + 1:]
1487 user = user[f + 1:]
1490 f = user.find(' ')
1488 f = user.find(' ')
1491 if f >= 0:
1489 if f >= 0:
1492 user = user[:f]
1490 user = user[:f]
1493 f = user.find('.')
1491 f = user.find('.')
1494 if f >= 0:
1492 if f >= 0:
1495 user = user[:f]
1493 user = user[:f]
1496 return user
1494 return user
1497
1495
1498 def emailuser(user):
1496 def emailuser(user):
1499 """Return the user portion of an email address."""
1497 """Return the user portion of an email address."""
1500 f = user.find('@')
1498 f = user.find('@')
1501 if f >= 0:
1499 if f >= 0:
1502 user = user[:f]
1500 user = user[:f]
1503 f = user.find('<')
1501 f = user.find('<')
1504 if f >= 0:
1502 if f >= 0:
1505 user = user[f + 1:]
1503 user = user[f + 1:]
1506 return user
1504 return user
1507
1505
1508 def email(author):
1506 def email(author):
1509 '''get email of author.'''
1507 '''get email of author.'''
1510 r = author.find('>')
1508 r = author.find('>')
1511 if r == -1:
1509 if r == -1:
1512 r = None
1510 r = None
1513 return author[author.find('<') + 1:r]
1511 return author[author.find('<') + 1:r]
1514
1512
1515 def ellipsis(text, maxlength=400):
1513 def ellipsis(text, maxlength=400):
1516 """Trim string to at most maxlength (default: 400) columns in display."""
1514 """Trim string to at most maxlength (default: 400) columns in display."""
1517 return encoding.trim(text, maxlength, ellipsis='...')
1515 return encoding.trim(text, maxlength, ellipsis='...')
1518
1516
1519 def unitcountfn(*unittable):
1517 def unitcountfn(*unittable):
1520 '''return a function that renders a readable count of some quantity'''
1518 '''return a function that renders a readable count of some quantity'''
1521
1519
1522 def go(count):
1520 def go(count):
1523 for multiplier, divisor, format in unittable:
1521 for multiplier, divisor, format in unittable:
1524 if count >= divisor * multiplier:
1522 if count >= divisor * multiplier:
1525 return format % (count / float(divisor))
1523 return format % (count / float(divisor))
1526 return unittable[-1][2] % count
1524 return unittable[-1][2] % count
1527
1525
1528 return go
1526 return go
1529
1527
1530 bytecount = unitcountfn(
1528 bytecount = unitcountfn(
1531 (100, 1 << 30, _('%.0f GB')),
1529 (100, 1 << 30, _('%.0f GB')),
1532 (10, 1 << 30, _('%.1f GB')),
1530 (10, 1 << 30, _('%.1f GB')),
1533 (1, 1 << 30, _('%.2f GB')),
1531 (1, 1 << 30, _('%.2f GB')),
1534 (100, 1 << 20, _('%.0f MB')),
1532 (100, 1 << 20, _('%.0f MB')),
1535 (10, 1 << 20, _('%.1f MB')),
1533 (10, 1 << 20, _('%.1f MB')),
1536 (1, 1 << 20, _('%.2f MB')),
1534 (1, 1 << 20, _('%.2f MB')),
1537 (100, 1 << 10, _('%.0f KB')),
1535 (100, 1 << 10, _('%.0f KB')),
1538 (10, 1 << 10, _('%.1f KB')),
1536 (10, 1 << 10, _('%.1f KB')),
1539 (1, 1 << 10, _('%.2f KB')),
1537 (1, 1 << 10, _('%.2f KB')),
1540 (1, 1, _('%.0f bytes')),
1538 (1, 1, _('%.0f bytes')),
1541 )
1539 )
1542
1540
1543 def uirepr(s):
1541 def uirepr(s):
1544 # Avoid double backslash in Windows path repr()
1542 # Avoid double backslash in Windows path repr()
1545 return repr(s).replace('\\\\', '\\')
1543 return repr(s).replace('\\\\', '\\')
1546
1544
1547 # delay import of textwrap
1545 # delay import of textwrap
1548 def MBTextWrapper(**kwargs):
1546 def MBTextWrapper(**kwargs):
1549 class tw(textwrap.TextWrapper):
1547 class tw(textwrap.TextWrapper):
1550 """
1548 """
1551 Extend TextWrapper for width-awareness.
1549 Extend TextWrapper for width-awareness.
1552
1550
1553 Neither number of 'bytes' in any encoding nor 'characters' is
1551 Neither number of 'bytes' in any encoding nor 'characters' is
1554 appropriate to calculate terminal columns for specified string.
1552 appropriate to calculate terminal columns for specified string.
1555
1553
1556 Original TextWrapper implementation uses built-in 'len()' directly,
1554 Original TextWrapper implementation uses built-in 'len()' directly,
1557 so overriding is needed to use width information of each characters.
1555 so overriding is needed to use width information of each characters.
1558
1556
1559 In addition, characters classified into 'ambiguous' width are
1557 In addition, characters classified into 'ambiguous' width are
1560 treated as wide in East Asian area, but as narrow in other.
1558 treated as wide in East Asian area, but as narrow in other.
1561
1559
1562 This requires use decision to determine width of such characters.
1560 This requires use decision to determine width of such characters.
1563 """
1561 """
1564 def __init__(self, **kwargs):
1562 def __init__(self, **kwargs):
1565 textwrap.TextWrapper.__init__(self, **kwargs)
1563 textwrap.TextWrapper.__init__(self, **kwargs)
1566
1564
1567 # for compatibility between 2.4 and 2.6
1565 # for compatibility between 2.4 and 2.6
1568 if getattr(self, 'drop_whitespace', None) is None:
1566 if getattr(self, 'drop_whitespace', None) is None:
1569 self.drop_whitespace = kwargs.get('drop_whitespace', True)
1567 self.drop_whitespace = kwargs.get('drop_whitespace', True)
1570
1568
1571 def _cutdown(self, ucstr, space_left):
1569 def _cutdown(self, ucstr, space_left):
1572 l = 0
1570 l = 0
1573 colwidth = encoding.ucolwidth
1571 colwidth = encoding.ucolwidth
1574 for i in xrange(len(ucstr)):
1572 for i in xrange(len(ucstr)):
1575 l += colwidth(ucstr[i])
1573 l += colwidth(ucstr[i])
1576 if space_left < l:
1574 if space_left < l:
1577 return (ucstr[:i], ucstr[i:])
1575 return (ucstr[:i], ucstr[i:])
1578 return ucstr, ''
1576 return ucstr, ''
1579
1577
1580 # overriding of base class
1578 # overriding of base class
1581 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
1579 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
1582 space_left = max(width - cur_len, 1)
1580 space_left = max(width - cur_len, 1)
1583
1581
1584 if self.break_long_words:
1582 if self.break_long_words:
1585 cut, res = self._cutdown(reversed_chunks[-1], space_left)
1583 cut, res = self._cutdown(reversed_chunks[-1], space_left)
1586 cur_line.append(cut)
1584 cur_line.append(cut)
1587 reversed_chunks[-1] = res
1585 reversed_chunks[-1] = res
1588 elif not cur_line:
1586 elif not cur_line:
1589 cur_line.append(reversed_chunks.pop())
1587 cur_line.append(reversed_chunks.pop())
1590
1588
1591 # this overriding code is imported from TextWrapper of python 2.6
1589 # this overriding code is imported from TextWrapper of python 2.6
1592 # to calculate columns of string by 'encoding.ucolwidth()'
1590 # to calculate columns of string by 'encoding.ucolwidth()'
1593 def _wrap_chunks(self, chunks):
1591 def _wrap_chunks(self, chunks):
1594 colwidth = encoding.ucolwidth
1592 colwidth = encoding.ucolwidth
1595
1593
1596 lines = []
1594 lines = []
1597 if self.width <= 0:
1595 if self.width <= 0:
1598 raise ValueError("invalid width %r (must be > 0)" % self.width)
1596 raise ValueError("invalid width %r (must be > 0)" % self.width)
1599
1597
1600 # Arrange in reverse order so items can be efficiently popped
1598 # Arrange in reverse order so items can be efficiently popped
1601 # from a stack of chucks.
1599 # from a stack of chucks.
1602 chunks.reverse()
1600 chunks.reverse()
1603
1601
1604 while chunks:
1602 while chunks:
1605
1603
1606 # Start the list of chunks that will make up the current line.
1604 # Start the list of chunks that will make up the current line.
1607 # cur_len is just the length of all the chunks in cur_line.
1605 # cur_len is just the length of all the chunks in cur_line.
1608 cur_line = []
1606 cur_line = []
1609 cur_len = 0
1607 cur_len = 0
1610
1608
1611 # Figure out which static string will prefix this line.
1609 # Figure out which static string will prefix this line.
1612 if lines:
1610 if lines:
1613 indent = self.subsequent_indent
1611 indent = self.subsequent_indent
1614 else:
1612 else:
1615 indent = self.initial_indent
1613 indent = self.initial_indent
1616
1614
1617 # Maximum width for this line.
1615 # Maximum width for this line.
1618 width = self.width - len(indent)
1616 width = self.width - len(indent)
1619
1617
1620 # First chunk on line is whitespace -- drop it, unless this
1618 # First chunk on line is whitespace -- drop it, unless this
1621 # is the very beginning of the text (i.e. no lines started yet).
1619 # is the very beginning of the text (i.e. no lines started yet).
1622 if self.drop_whitespace and chunks[-1].strip() == '' and lines:
1620 if self.drop_whitespace and chunks[-1].strip() == '' and lines:
1623 del chunks[-1]
1621 del chunks[-1]
1624
1622
1625 while chunks:
1623 while chunks:
1626 l = colwidth(chunks[-1])
1624 l = colwidth(chunks[-1])
1627
1625
1628 # Can at least squeeze this chunk onto the current line.
1626 # Can at least squeeze this chunk onto the current line.
1629 if cur_len + l <= width:
1627 if cur_len + l <= width:
1630 cur_line.append(chunks.pop())
1628 cur_line.append(chunks.pop())
1631 cur_len += l
1629 cur_len += l
1632
1630
1633 # Nope, this line is full.
1631 # Nope, this line is full.
1634 else:
1632 else:
1635 break
1633 break
1636
1634
1637 # The current line is full, and the next chunk is too big to
1635 # The current line is full, and the next chunk is too big to
1638 # fit on *any* line (not just this one).
1636 # fit on *any* line (not just this one).
1639 if chunks and colwidth(chunks[-1]) > width:
1637 if chunks and colwidth(chunks[-1]) > width:
1640 self._handle_long_word(chunks, cur_line, cur_len, width)
1638 self._handle_long_word(chunks, cur_line, cur_len, width)
1641
1639
1642 # If the last chunk on this line is all whitespace, drop it.
1640 # If the last chunk on this line is all whitespace, drop it.
1643 if (self.drop_whitespace and
1641 if (self.drop_whitespace and
1644 cur_line and cur_line[-1].strip() == ''):
1642 cur_line and cur_line[-1].strip() == ''):
1645 del cur_line[-1]
1643 del cur_line[-1]
1646
1644
1647 # Convert current line back to a string and store it in list
1645 # Convert current line back to a string and store it in list
1648 # of all lines (return value).
1646 # of all lines (return value).
1649 if cur_line:
1647 if cur_line:
1650 lines.append(indent + ''.join(cur_line))
1648 lines.append(indent + ''.join(cur_line))
1651
1649
1652 return lines
1650 return lines
1653
1651
1654 global MBTextWrapper
1652 global MBTextWrapper
1655 MBTextWrapper = tw
1653 MBTextWrapper = tw
1656 return tw(**kwargs)
1654 return tw(**kwargs)
1657
1655
1658 def wrap(line, width, initindent='', hangindent=''):
1656 def wrap(line, width, initindent='', hangindent=''):
1659 maxindent = max(len(hangindent), len(initindent))
1657 maxindent = max(len(hangindent), len(initindent))
1660 if width <= maxindent:
1658 if width <= maxindent:
1661 # adjust for weird terminal size
1659 # adjust for weird terminal size
1662 width = max(78, maxindent + 1)
1660 width = max(78, maxindent + 1)
1663 line = line.decode(encoding.encoding, encoding.encodingmode)
1661 line = line.decode(encoding.encoding, encoding.encodingmode)
1664 initindent = initindent.decode(encoding.encoding, encoding.encodingmode)
1662 initindent = initindent.decode(encoding.encoding, encoding.encodingmode)
1665 hangindent = hangindent.decode(encoding.encoding, encoding.encodingmode)
1663 hangindent = hangindent.decode(encoding.encoding, encoding.encodingmode)
1666 wrapper = MBTextWrapper(width=width,
1664 wrapper = MBTextWrapper(width=width,
1667 initial_indent=initindent,
1665 initial_indent=initindent,
1668 subsequent_indent=hangindent)
1666 subsequent_indent=hangindent)
1669 return wrapper.fill(line).encode(encoding.encoding)
1667 return wrapper.fill(line).encode(encoding.encoding)
1670
1668
1671 def iterlines(iterator):
1669 def iterlines(iterator):
1672 for chunk in iterator:
1670 for chunk in iterator:
1673 for line in chunk.splitlines():
1671 for line in chunk.splitlines():
1674 yield line
1672 yield line
1675
1673
1676 def expandpath(path):
1674 def expandpath(path):
1677 return os.path.expanduser(os.path.expandvars(path))
1675 return os.path.expanduser(os.path.expandvars(path))
1678
1676
1679 def hgcmd():
1677 def hgcmd():
1680 """Return the command used to execute current hg
1678 """Return the command used to execute current hg
1681
1679
1682 This is different from hgexecutable() because on Windows we want
1680 This is different from hgexecutable() because on Windows we want
1683 to avoid things opening new shell windows like batch files, so we
1681 to avoid things opening new shell windows like batch files, so we
1684 get either the python call or current executable.
1682 get either the python call or current executable.
1685 """
1683 """
1686 if mainfrozen():
1684 if mainfrozen():
1687 return [sys.executable]
1685 return [sys.executable]
1688 return gethgcmd()
1686 return gethgcmd()
1689
1687
1690 def rundetached(args, condfn):
1688 def rundetached(args, condfn):
1691 """Execute the argument list in a detached process.
1689 """Execute the argument list in a detached process.
1692
1690
1693 condfn is a callable which is called repeatedly and should return
1691 condfn is a callable which is called repeatedly and should return
1694 True once the child process is known to have started successfully.
1692 True once the child process is known to have started successfully.
1695 At this point, the child process PID is returned. If the child
1693 At this point, the child process PID is returned. If the child
1696 process fails to start or finishes before condfn() evaluates to
1694 process fails to start or finishes before condfn() evaluates to
1697 True, return -1.
1695 True, return -1.
1698 """
1696 """
1699 # Windows case is easier because the child process is either
1697 # Windows case is easier because the child process is either
1700 # successfully starting and validating the condition or exiting
1698 # successfully starting and validating the condition or exiting
1701 # on failure. We just poll on its PID. On Unix, if the child
1699 # on failure. We just poll on its PID. On Unix, if the child
1702 # process fails to start, it will be left in a zombie state until
1700 # process fails to start, it will be left in a zombie state until
1703 # the parent wait on it, which we cannot do since we expect a long
1701 # the parent wait on it, which we cannot do since we expect a long
1704 # running process on success. Instead we listen for SIGCHLD telling
1702 # running process on success. Instead we listen for SIGCHLD telling
1705 # us our child process terminated.
1703 # us our child process terminated.
1706 terminated = set()
1704 terminated = set()
1707 def handler(signum, frame):
1705 def handler(signum, frame):
1708 terminated.add(os.wait())
1706 terminated.add(os.wait())
1709 prevhandler = None
1707 prevhandler = None
1710 SIGCHLD = getattr(signal, 'SIGCHLD', None)
1708 SIGCHLD = getattr(signal, 'SIGCHLD', None)
1711 if SIGCHLD is not None:
1709 if SIGCHLD is not None:
1712 prevhandler = signal.signal(SIGCHLD, handler)
1710 prevhandler = signal.signal(SIGCHLD, handler)
1713 try:
1711 try:
1714 pid = spawndetached(args)
1712 pid = spawndetached(args)
1715 while not condfn():
1713 while not condfn():
1716 if ((pid in terminated or not testpid(pid))
1714 if ((pid in terminated or not testpid(pid))
1717 and not condfn()):
1715 and not condfn()):
1718 return -1
1716 return -1
1719 time.sleep(0.1)
1717 time.sleep(0.1)
1720 return pid
1718 return pid
1721 finally:
1719 finally:
1722 if prevhandler is not None:
1720 if prevhandler is not None:
1723 signal.signal(signal.SIGCHLD, prevhandler)
1721 signal.signal(signal.SIGCHLD, prevhandler)
1724
1722
1725 try:
1723 try:
1726 any, all = any, all
1724 any, all = any, all
1727 except NameError:
1725 except NameError:
1728 def any(iterable):
1726 def any(iterable):
1729 for i in iterable:
1727 for i in iterable:
1730 if i:
1728 if i:
1731 return True
1729 return True
1732 return False
1730 return False
1733
1731
1734 def all(iterable):
1732 def all(iterable):
1735 for i in iterable:
1733 for i in iterable:
1736 if not i:
1734 if not i:
1737 return False
1735 return False
1738 return True
1736 return True
1739
1737
1740 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
1738 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
1741 """Return the result of interpolating items in the mapping into string s.
1739 """Return the result of interpolating items in the mapping into string s.
1742
1740
1743 prefix is a single character string, or a two character string with
1741 prefix is a single character string, or a two character string with
1744 a backslash as the first character if the prefix needs to be escaped in
1742 a backslash as the first character if the prefix needs to be escaped in
1745 a regular expression.
1743 a regular expression.
1746
1744
1747 fn is an optional function that will be applied to the replacement text
1745 fn is an optional function that will be applied to the replacement text
1748 just before replacement.
1746 just before replacement.
1749
1747
1750 escape_prefix is an optional flag that allows using doubled prefix for
1748 escape_prefix is an optional flag that allows using doubled prefix for
1751 its escaping.
1749 its escaping.
1752 """
1750 """
1753 fn = fn or (lambda s: s)
1751 fn = fn or (lambda s: s)
1754 patterns = '|'.join(mapping.keys())
1752 patterns = '|'.join(mapping.keys())
1755 if escape_prefix:
1753 if escape_prefix:
1756 patterns += '|' + prefix
1754 patterns += '|' + prefix
1757 if len(prefix) > 1:
1755 if len(prefix) > 1:
1758 prefix_char = prefix[1:]
1756 prefix_char = prefix[1:]
1759 else:
1757 else:
1760 prefix_char = prefix
1758 prefix_char = prefix
1761 mapping[prefix_char] = prefix_char
1759 mapping[prefix_char] = prefix_char
1762 r = remod.compile(r'%s(%s)' % (prefix, patterns))
1760 r = remod.compile(r'%s(%s)' % (prefix, patterns))
1763 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
1761 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
1764
1762
1765 def getport(port):
1763 def getport(port):
1766 """Return the port for a given network service.
1764 """Return the port for a given network service.
1767
1765
1768 If port is an integer, it's returned as is. If it's a string, it's
1766 If port is an integer, it's returned as is. If it's a string, it's
1769 looked up using socket.getservbyname(). If there's no matching
1767 looked up using socket.getservbyname(). If there's no matching
1770 service, util.Abort is raised.
1768 service, util.Abort is raised.
1771 """
1769 """
1772 try:
1770 try:
1773 return int(port)
1771 return int(port)
1774 except ValueError:
1772 except ValueError:
1775 pass
1773 pass
1776
1774
1777 try:
1775 try:
1778 return socket.getservbyname(port)
1776 return socket.getservbyname(port)
1779 except socket.error:
1777 except socket.error:
1780 raise Abort(_("no port number associated with service '%s'") % port)
1778 raise Abort(_("no port number associated with service '%s'") % port)
1781
1779
1782 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, 'always': True,
1780 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, 'always': True,
1783 '0': False, 'no': False, 'false': False, 'off': False,
1781 '0': False, 'no': False, 'false': False, 'off': False,
1784 'never': False}
1782 'never': False}
1785
1783
1786 def parsebool(s):
1784 def parsebool(s):
1787 """Parse s into a boolean.
1785 """Parse s into a boolean.
1788
1786
1789 If s is not a valid boolean, returns None.
1787 If s is not a valid boolean, returns None.
1790 """
1788 """
1791 return _booleans.get(s.lower(), None)
1789 return _booleans.get(s.lower(), None)
1792
1790
1793 _hexdig = '0123456789ABCDEFabcdef'
1791 _hexdig = '0123456789ABCDEFabcdef'
1794 _hextochr = dict((a + b, chr(int(a + b, 16)))
1792 _hextochr = dict((a + b, chr(int(a + b, 16)))
1795 for a in _hexdig for b in _hexdig)
1793 for a in _hexdig for b in _hexdig)
1796
1794
1797 def _urlunquote(s):
1795 def _urlunquote(s):
1798 """Decode HTTP/HTML % encoding.
1796 """Decode HTTP/HTML % encoding.
1799
1797
1800 >>> _urlunquote('abc%20def')
1798 >>> _urlunquote('abc%20def')
1801 'abc def'
1799 'abc def'
1802 """
1800 """
1803 res = s.split('%')
1801 res = s.split('%')
1804 # fastpath
1802 # fastpath
1805 if len(res) == 1:
1803 if len(res) == 1:
1806 return s
1804 return s
1807 s = res[0]
1805 s = res[0]
1808 for item in res[1:]:
1806 for item in res[1:]:
1809 try:
1807 try:
1810 s += _hextochr[item[:2]] + item[2:]
1808 s += _hextochr[item[:2]] + item[2:]
1811 except KeyError:
1809 except KeyError:
1812 s += '%' + item
1810 s += '%' + item
1813 except UnicodeDecodeError:
1811 except UnicodeDecodeError:
1814 s += unichr(int(item[:2], 16)) + item[2:]
1812 s += unichr(int(item[:2], 16)) + item[2:]
1815 return s
1813 return s
1816
1814
1817 class url(object):
1815 class url(object):
1818 r"""Reliable URL parser.
1816 r"""Reliable URL parser.
1819
1817
1820 This parses URLs and provides attributes for the following
1818 This parses URLs and provides attributes for the following
1821 components:
1819 components:
1822
1820
1823 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
1821 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
1824
1822
1825 Missing components are set to None. The only exception is
1823 Missing components are set to None. The only exception is
1826 fragment, which is set to '' if present but empty.
1824 fragment, which is set to '' if present but empty.
1827
1825
1828 If parsefragment is False, fragment is included in query. If
1826 If parsefragment is False, fragment is included in query. If
1829 parsequery is False, query is included in path. If both are
1827 parsequery is False, query is included in path. If both are
1830 False, both fragment and query are included in path.
1828 False, both fragment and query are included in path.
1831
1829
1832 See http://www.ietf.org/rfc/rfc2396.txt for more information.
1830 See http://www.ietf.org/rfc/rfc2396.txt for more information.
1833
1831
1834 Note that for backward compatibility reasons, bundle URLs do not
1832 Note that for backward compatibility reasons, bundle URLs do not
1835 take host names. That means 'bundle://../' has a path of '../'.
1833 take host names. That means 'bundle://../' has a path of '../'.
1836
1834
1837 Examples:
1835 Examples:
1838
1836
1839 >>> url('http://www.ietf.org/rfc/rfc2396.txt')
1837 >>> url('http://www.ietf.org/rfc/rfc2396.txt')
1840 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
1838 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
1841 >>> url('ssh://[::1]:2200//home/joe/repo')
1839 >>> url('ssh://[::1]:2200//home/joe/repo')
1842 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
1840 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
1843 >>> url('file:///home/joe/repo')
1841 >>> url('file:///home/joe/repo')
1844 <url scheme: 'file', path: '/home/joe/repo'>
1842 <url scheme: 'file', path: '/home/joe/repo'>
1845 >>> url('file:///c:/temp/foo/')
1843 >>> url('file:///c:/temp/foo/')
1846 <url scheme: 'file', path: 'c:/temp/foo/'>
1844 <url scheme: 'file', path: 'c:/temp/foo/'>
1847 >>> url('bundle:foo')
1845 >>> url('bundle:foo')
1848 <url scheme: 'bundle', path: 'foo'>
1846 <url scheme: 'bundle', path: 'foo'>
1849 >>> url('bundle://../foo')
1847 >>> url('bundle://../foo')
1850 <url scheme: 'bundle', path: '../foo'>
1848 <url scheme: 'bundle', path: '../foo'>
1851 >>> url(r'c:\foo\bar')
1849 >>> url(r'c:\foo\bar')
1852 <url path: 'c:\\foo\\bar'>
1850 <url path: 'c:\\foo\\bar'>
1853 >>> url(r'\\blah\blah\blah')
1851 >>> url(r'\\blah\blah\blah')
1854 <url path: '\\\\blah\\blah\\blah'>
1852 <url path: '\\\\blah\\blah\\blah'>
1855 >>> url(r'\\blah\blah\blah#baz')
1853 >>> url(r'\\blah\blah\blah#baz')
1856 <url path: '\\\\blah\\blah\\blah', fragment: 'baz'>
1854 <url path: '\\\\blah\\blah\\blah', fragment: 'baz'>
1857 >>> url(r'file:///C:\users\me')
1855 >>> url(r'file:///C:\users\me')
1858 <url scheme: 'file', path: 'C:\\users\\me'>
1856 <url scheme: 'file', path: 'C:\\users\\me'>
1859
1857
1860 Authentication credentials:
1858 Authentication credentials:
1861
1859
1862 >>> url('ssh://joe:xyz@x/repo')
1860 >>> url('ssh://joe:xyz@x/repo')
1863 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
1861 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
1864 >>> url('ssh://joe@x/repo')
1862 >>> url('ssh://joe@x/repo')
1865 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
1863 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
1866
1864
1867 Query strings and fragments:
1865 Query strings and fragments:
1868
1866
1869 >>> url('http://host/a?b#c')
1867 >>> url('http://host/a?b#c')
1870 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
1868 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
1871 >>> url('http://host/a?b#c', parsequery=False, parsefragment=False)
1869 >>> url('http://host/a?b#c', parsequery=False, parsefragment=False)
1872 <url scheme: 'http', host: 'host', path: 'a?b#c'>
1870 <url scheme: 'http', host: 'host', path: 'a?b#c'>
1873 """
1871 """
1874
1872
1875 _safechars = "!~*'()+"
1873 _safechars = "!~*'()+"
1876 _safepchars = "/!~*'()+:\\"
1874 _safepchars = "/!~*'()+:\\"
1877 _matchscheme = remod.compile(r'^[a-zA-Z0-9+.\-]+:').match
1875 _matchscheme = remod.compile(r'^[a-zA-Z0-9+.\-]+:').match
1878
1876
1879 def __init__(self, path, parsequery=True, parsefragment=True):
1877 def __init__(self, path, parsequery=True, parsefragment=True):
1880 # We slowly chomp away at path until we have only the path left
1878 # We slowly chomp away at path until we have only the path left
1881 self.scheme = self.user = self.passwd = self.host = None
1879 self.scheme = self.user = self.passwd = self.host = None
1882 self.port = self.path = self.query = self.fragment = None
1880 self.port = self.path = self.query = self.fragment = None
1883 self._localpath = True
1881 self._localpath = True
1884 self._hostport = ''
1882 self._hostport = ''
1885 self._origpath = path
1883 self._origpath = path
1886
1884
1887 if parsefragment and '#' in path:
1885 if parsefragment and '#' in path:
1888 path, self.fragment = path.split('#', 1)
1886 path, self.fragment = path.split('#', 1)
1889 if not path:
1887 if not path:
1890 path = None
1888 path = None
1891
1889
1892 # special case for Windows drive letters and UNC paths
1890 # special case for Windows drive letters and UNC paths
1893 if hasdriveletter(path) or path.startswith(r'\\'):
1891 if hasdriveletter(path) or path.startswith(r'\\'):
1894 self.path = path
1892 self.path = path
1895 return
1893 return
1896
1894
1897 # For compatibility reasons, we can't handle bundle paths as
1895 # For compatibility reasons, we can't handle bundle paths as
1898 # normal URLS
1896 # normal URLS
1899 if path.startswith('bundle:'):
1897 if path.startswith('bundle:'):
1900 self.scheme = 'bundle'
1898 self.scheme = 'bundle'
1901 path = path[7:]
1899 path = path[7:]
1902 if path.startswith('//'):
1900 if path.startswith('//'):
1903 path = path[2:]
1901 path = path[2:]
1904 self.path = path
1902 self.path = path
1905 return
1903 return
1906
1904
1907 if self._matchscheme(path):
1905 if self._matchscheme(path):
1908 parts = path.split(':', 1)
1906 parts = path.split(':', 1)
1909 if parts[0]:
1907 if parts[0]:
1910 self.scheme, path = parts
1908 self.scheme, path = parts
1911 self._localpath = False
1909 self._localpath = False
1912
1910
1913 if not path:
1911 if not path:
1914 path = None
1912 path = None
1915 if self._localpath:
1913 if self._localpath:
1916 self.path = ''
1914 self.path = ''
1917 return
1915 return
1918 else:
1916 else:
1919 if self._localpath:
1917 if self._localpath:
1920 self.path = path
1918 self.path = path
1921 return
1919 return
1922
1920
1923 if parsequery and '?' in path:
1921 if parsequery and '?' in path:
1924 path, self.query = path.split('?', 1)
1922 path, self.query = path.split('?', 1)
1925 if not path:
1923 if not path:
1926 path = None
1924 path = None
1927 if not self.query:
1925 if not self.query:
1928 self.query = None
1926 self.query = None
1929
1927
1930 # // is required to specify a host/authority
1928 # // is required to specify a host/authority
1931 if path and path.startswith('//'):
1929 if path and path.startswith('//'):
1932 parts = path[2:].split('/', 1)
1930 parts = path[2:].split('/', 1)
1933 if len(parts) > 1:
1931 if len(parts) > 1:
1934 self.host, path = parts
1932 self.host, path = parts
1935 else:
1933 else:
1936 self.host = parts[0]
1934 self.host = parts[0]
1937 path = None
1935 path = None
1938 if not self.host:
1936 if not self.host:
1939 self.host = None
1937 self.host = None
1940 # path of file:///d is /d
1938 # path of file:///d is /d
1941 # path of file:///d:/ is d:/, not /d:/
1939 # path of file:///d:/ is d:/, not /d:/
1942 if path and not hasdriveletter(path):
1940 if path and not hasdriveletter(path):
1943 path = '/' + path
1941 path = '/' + path
1944
1942
1945 if self.host and '@' in self.host:
1943 if self.host and '@' in self.host:
1946 self.user, self.host = self.host.rsplit('@', 1)
1944 self.user, self.host = self.host.rsplit('@', 1)
1947 if ':' in self.user:
1945 if ':' in self.user:
1948 self.user, self.passwd = self.user.split(':', 1)
1946 self.user, self.passwd = self.user.split(':', 1)
1949 if not self.host:
1947 if not self.host:
1950 self.host = None
1948 self.host = None
1951
1949
1952 # Don't split on colons in IPv6 addresses without ports
1950 # Don't split on colons in IPv6 addresses without ports
1953 if (self.host and ':' in self.host and
1951 if (self.host and ':' in self.host and
1954 not (self.host.startswith('[') and self.host.endswith(']'))):
1952 not (self.host.startswith('[') and self.host.endswith(']'))):
1955 self._hostport = self.host
1953 self._hostport = self.host
1956 self.host, self.port = self.host.rsplit(':', 1)
1954 self.host, self.port = self.host.rsplit(':', 1)
1957 if not self.host:
1955 if not self.host:
1958 self.host = None
1956 self.host = None
1959
1957
1960 if (self.host and self.scheme == 'file' and
1958 if (self.host and self.scheme == 'file' and
1961 self.host not in ('localhost', '127.0.0.1', '[::1]')):
1959 self.host not in ('localhost', '127.0.0.1', '[::1]')):
1962 raise Abort(_('file:// URLs can only refer to localhost'))
1960 raise Abort(_('file:// URLs can only refer to localhost'))
1963
1961
1964 self.path = path
1962 self.path = path
1965
1963
1966 # leave the query string escaped
1964 # leave the query string escaped
1967 for a in ('user', 'passwd', 'host', 'port',
1965 for a in ('user', 'passwd', 'host', 'port',
1968 'path', 'fragment'):
1966 'path', 'fragment'):
1969 v = getattr(self, a)
1967 v = getattr(self, a)
1970 if v is not None:
1968 if v is not None:
1971 setattr(self, a, _urlunquote(v))
1969 setattr(self, a, _urlunquote(v))
1972
1970
1973 def __repr__(self):
1971 def __repr__(self):
1974 attrs = []
1972 attrs = []
1975 for a in ('scheme', 'user', 'passwd', 'host', 'port', 'path',
1973 for a in ('scheme', 'user', 'passwd', 'host', 'port', 'path',
1976 'query', 'fragment'):
1974 'query', 'fragment'):
1977 v = getattr(self, a)
1975 v = getattr(self, a)
1978 if v is not None:
1976 if v is not None:
1979 attrs.append('%s: %r' % (a, v))
1977 attrs.append('%s: %r' % (a, v))
1980 return '<url %s>' % ', '.join(attrs)
1978 return '<url %s>' % ', '.join(attrs)
1981
1979
1982 def __str__(self):
1980 def __str__(self):
1983 r"""Join the URL's components back into a URL string.
1981 r"""Join the URL's components back into a URL string.
1984
1982
1985 Examples:
1983 Examples:
1986
1984
1987 >>> str(url('http://user:pw@host:80/c:/bob?fo:oo#ba:ar'))
1985 >>> str(url('http://user:pw@host:80/c:/bob?fo:oo#ba:ar'))
1988 'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'
1986 'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'
1989 >>> str(url('http://user:pw@host:80/?foo=bar&baz=42'))
1987 >>> str(url('http://user:pw@host:80/?foo=bar&baz=42'))
1990 'http://user:pw@host:80/?foo=bar&baz=42'
1988 'http://user:pw@host:80/?foo=bar&baz=42'
1991 >>> str(url('http://user:pw@host:80/?foo=bar%3dbaz'))
1989 >>> str(url('http://user:pw@host:80/?foo=bar%3dbaz'))
1992 'http://user:pw@host:80/?foo=bar%3dbaz'
1990 'http://user:pw@host:80/?foo=bar%3dbaz'
1993 >>> str(url('ssh://user:pw@[::1]:2200//home/joe#'))
1991 >>> str(url('ssh://user:pw@[::1]:2200//home/joe#'))
1994 'ssh://user:pw@[::1]:2200//home/joe#'
1992 'ssh://user:pw@[::1]:2200//home/joe#'
1995 >>> str(url('http://localhost:80//'))
1993 >>> str(url('http://localhost:80//'))
1996 'http://localhost:80//'
1994 'http://localhost:80//'
1997 >>> str(url('http://localhost:80/'))
1995 >>> str(url('http://localhost:80/'))
1998 'http://localhost:80/'
1996 'http://localhost:80/'
1999 >>> str(url('http://localhost:80'))
1997 >>> str(url('http://localhost:80'))
2000 'http://localhost:80/'
1998 'http://localhost:80/'
2001 >>> str(url('bundle:foo'))
1999 >>> str(url('bundle:foo'))
2002 'bundle:foo'
2000 'bundle:foo'
2003 >>> str(url('bundle://../foo'))
2001 >>> str(url('bundle://../foo'))
2004 'bundle:../foo'
2002 'bundle:../foo'
2005 >>> str(url('path'))
2003 >>> str(url('path'))
2006 'path'
2004 'path'
2007 >>> str(url('file:///tmp/foo/bar'))
2005 >>> str(url('file:///tmp/foo/bar'))
2008 'file:///tmp/foo/bar'
2006 'file:///tmp/foo/bar'
2009 >>> str(url('file:///c:/tmp/foo/bar'))
2007 >>> str(url('file:///c:/tmp/foo/bar'))
2010 'file:///c:/tmp/foo/bar'
2008 'file:///c:/tmp/foo/bar'
2011 >>> print url(r'bundle:foo\bar')
2009 >>> print url(r'bundle:foo\bar')
2012 bundle:foo\bar
2010 bundle:foo\bar
2013 >>> print url(r'file:///D:\data\hg')
2011 >>> print url(r'file:///D:\data\hg')
2014 file:///D:\data\hg
2012 file:///D:\data\hg
2015 """
2013 """
2016 if self._localpath:
2014 if self._localpath:
2017 s = self.path
2015 s = self.path
2018 if self.scheme == 'bundle':
2016 if self.scheme == 'bundle':
2019 s = 'bundle:' + s
2017 s = 'bundle:' + s
2020 if self.fragment:
2018 if self.fragment:
2021 s += '#' + self.fragment
2019 s += '#' + self.fragment
2022 return s
2020 return s
2023
2021
2024 s = self.scheme + ':'
2022 s = self.scheme + ':'
2025 if self.user or self.passwd or self.host:
2023 if self.user or self.passwd or self.host:
2026 s += '//'
2024 s += '//'
2027 elif self.scheme and (not self.path or self.path.startswith('/')
2025 elif self.scheme and (not self.path or self.path.startswith('/')
2028 or hasdriveletter(self.path)):
2026 or hasdriveletter(self.path)):
2029 s += '//'
2027 s += '//'
2030 if hasdriveletter(self.path):
2028 if hasdriveletter(self.path):
2031 s += '/'
2029 s += '/'
2032 if self.user:
2030 if self.user:
2033 s += urllib.quote(self.user, safe=self._safechars)
2031 s += urllib.quote(self.user, safe=self._safechars)
2034 if self.passwd:
2032 if self.passwd:
2035 s += ':' + urllib.quote(self.passwd, safe=self._safechars)
2033 s += ':' + urllib.quote(self.passwd, safe=self._safechars)
2036 if self.user or self.passwd:
2034 if self.user or self.passwd:
2037 s += '@'
2035 s += '@'
2038 if self.host:
2036 if self.host:
2039 if not (self.host.startswith('[') and self.host.endswith(']')):
2037 if not (self.host.startswith('[') and self.host.endswith(']')):
2040 s += urllib.quote(self.host)
2038 s += urllib.quote(self.host)
2041 else:
2039 else:
2042 s += self.host
2040 s += self.host
2043 if self.port:
2041 if self.port:
2044 s += ':' + urllib.quote(self.port)
2042 s += ':' + urllib.quote(self.port)
2045 if self.host:
2043 if self.host:
2046 s += '/'
2044 s += '/'
2047 if self.path:
2045 if self.path:
2048 # TODO: similar to the query string, we should not unescape the
2046 # TODO: similar to the query string, we should not unescape the
2049 # path when we store it, the path might contain '%2f' = '/',
2047 # path when we store it, the path might contain '%2f' = '/',
2050 # which we should *not* escape.
2048 # which we should *not* escape.
2051 s += urllib.quote(self.path, safe=self._safepchars)
2049 s += urllib.quote(self.path, safe=self._safepchars)
2052 if self.query:
2050 if self.query:
2053 # we store the query in escaped form.
2051 # we store the query in escaped form.
2054 s += '?' + self.query
2052 s += '?' + self.query
2055 if self.fragment is not None:
2053 if self.fragment is not None:
2056 s += '#' + urllib.quote(self.fragment, safe=self._safepchars)
2054 s += '#' + urllib.quote(self.fragment, safe=self._safepchars)
2057 return s
2055 return s
2058
2056
2059 def authinfo(self):
2057 def authinfo(self):
2060 user, passwd = self.user, self.passwd
2058 user, passwd = self.user, self.passwd
2061 try:
2059 try:
2062 self.user, self.passwd = None, None
2060 self.user, self.passwd = None, None
2063 s = str(self)
2061 s = str(self)
2064 finally:
2062 finally:
2065 self.user, self.passwd = user, passwd
2063 self.user, self.passwd = user, passwd
2066 if not self.user:
2064 if not self.user:
2067 return (s, None)
2065 return (s, None)
2068 # authinfo[1] is passed to urllib2 password manager, and its
2066 # authinfo[1] is passed to urllib2 password manager, and its
2069 # URIs must not contain credentials. The host is passed in the
2067 # URIs must not contain credentials. The host is passed in the
2070 # URIs list because Python < 2.4.3 uses only that to search for
2068 # URIs list because Python < 2.4.3 uses only that to search for
2071 # a password.
2069 # a password.
2072 return (s, (None, (s, self.host),
2070 return (s, (None, (s, self.host),
2073 self.user, self.passwd or ''))
2071 self.user, self.passwd or ''))
2074
2072
2075 def isabs(self):
2073 def isabs(self):
2076 if self.scheme and self.scheme != 'file':
2074 if self.scheme and self.scheme != 'file':
2077 return True # remote URL
2075 return True # remote URL
2078 if hasdriveletter(self.path):
2076 if hasdriveletter(self.path):
2079 return True # absolute for our purposes - can't be joined()
2077 return True # absolute for our purposes - can't be joined()
2080 if self.path.startswith(r'\\'):
2078 if self.path.startswith(r'\\'):
2081 return True # Windows UNC path
2079 return True # Windows UNC path
2082 if self.path.startswith('/'):
2080 if self.path.startswith('/'):
2083 return True # POSIX-style
2081 return True # POSIX-style
2084 return False
2082 return False
2085
2083
2086 def localpath(self):
2084 def localpath(self):
2087 if self.scheme == 'file' or self.scheme == 'bundle':
2085 if self.scheme == 'file' or self.scheme == 'bundle':
2088 path = self.path or '/'
2086 path = self.path or '/'
2089 # For Windows, we need to promote hosts containing drive
2087 # For Windows, we need to promote hosts containing drive
2090 # letters to paths with drive letters.
2088 # letters to paths with drive letters.
2091 if hasdriveletter(self._hostport):
2089 if hasdriveletter(self._hostport):
2092 path = self._hostport + '/' + self.path
2090 path = self._hostport + '/' + self.path
2093 elif (self.host is not None and self.path
2091 elif (self.host is not None and self.path
2094 and not hasdriveletter(path)):
2092 and not hasdriveletter(path)):
2095 path = '/' + path
2093 path = '/' + path
2096 return path
2094 return path
2097 return self._origpath
2095 return self._origpath
2098
2096
2099 def islocal(self):
2097 def islocal(self):
2100 '''whether localpath will return something that posixfile can open'''
2098 '''whether localpath will return something that posixfile can open'''
2101 return (not self.scheme or self.scheme == 'file'
2099 return (not self.scheme or self.scheme == 'file'
2102 or self.scheme == 'bundle')
2100 or self.scheme == 'bundle')
2103
2101
2104 def hasscheme(path):
2102 def hasscheme(path):
2105 return bool(url(path).scheme)
2103 return bool(url(path).scheme)
2106
2104
2107 def hasdriveletter(path):
2105 def hasdriveletter(path):
2108 return path and path[1:2] == ':' and path[0:1].isalpha()
2106 return path and path[1:2] == ':' and path[0:1].isalpha()
2109
2107
2110 def urllocalpath(path):
2108 def urllocalpath(path):
2111 return url(path, parsequery=False, parsefragment=False).localpath()
2109 return url(path, parsequery=False, parsefragment=False).localpath()
2112
2110
2113 def hidepassword(u):
2111 def hidepassword(u):
2114 '''hide user credential in a url string'''
2112 '''hide user credential in a url string'''
2115 u = url(u)
2113 u = url(u)
2116 if u.passwd:
2114 if u.passwd:
2117 u.passwd = '***'
2115 u.passwd = '***'
2118 return str(u)
2116 return str(u)
2119
2117
2120 def removeauth(u):
2118 def removeauth(u):
2121 '''remove all authentication information from a url string'''
2119 '''remove all authentication information from a url string'''
2122 u = url(u)
2120 u = url(u)
2123 u.user = u.passwd = None
2121 u.user = u.passwd = None
2124 return str(u)
2122 return str(u)
2125
2123
2126 def isatty(fd):
2124 def isatty(fd):
2127 try:
2125 try:
2128 return fd.isatty()
2126 return fd.isatty()
2129 except AttributeError:
2127 except AttributeError:
2130 return False
2128 return False
2131
2129
2132 timecount = unitcountfn(
2130 timecount = unitcountfn(
2133 (1, 1e3, _('%.0f s')),
2131 (1, 1e3, _('%.0f s')),
2134 (100, 1, _('%.1f s')),
2132 (100, 1, _('%.1f s')),
2135 (10, 1, _('%.2f s')),
2133 (10, 1, _('%.2f s')),
2136 (1, 1, _('%.3f s')),
2134 (1, 1, _('%.3f s')),
2137 (100, 0.001, _('%.1f ms')),
2135 (100, 0.001, _('%.1f ms')),
2138 (10, 0.001, _('%.2f ms')),
2136 (10, 0.001, _('%.2f ms')),
2139 (1, 0.001, _('%.3f ms')),
2137 (1, 0.001, _('%.3f ms')),
2140 (100, 0.000001, _('%.1f us')),
2138 (100, 0.000001, _('%.1f us')),
2141 (10, 0.000001, _('%.2f us')),
2139 (10, 0.000001, _('%.2f us')),
2142 (1, 0.000001, _('%.3f us')),
2140 (1, 0.000001, _('%.3f us')),
2143 (100, 0.000000001, _('%.1f ns')),
2141 (100, 0.000000001, _('%.1f ns')),
2144 (10, 0.000000001, _('%.2f ns')),
2142 (10, 0.000000001, _('%.2f ns')),
2145 (1, 0.000000001, _('%.3f ns')),
2143 (1, 0.000000001, _('%.3f ns')),
2146 )
2144 )
2147
2145
2148 _timenesting = [0]
2146 _timenesting = [0]
2149
2147
2150 def timed(func):
2148 def timed(func):
2151 '''Report the execution time of a function call to stderr.
2149 '''Report the execution time of a function call to stderr.
2152
2150
2153 During development, use as a decorator when you need to measure
2151 During development, use as a decorator when you need to measure
2154 the cost of a function, e.g. as follows:
2152 the cost of a function, e.g. as follows:
2155
2153
2156 @util.timed
2154 @util.timed
2157 def foo(a, b, c):
2155 def foo(a, b, c):
2158 pass
2156 pass
2159 '''
2157 '''
2160
2158
2161 def wrapper(*args, **kwargs):
2159 def wrapper(*args, **kwargs):
2162 start = time.time()
2160 start = time.time()
2163 indent = 2
2161 indent = 2
2164 _timenesting[0] += indent
2162 _timenesting[0] += indent
2165 try:
2163 try:
2166 return func(*args, **kwargs)
2164 return func(*args, **kwargs)
2167 finally:
2165 finally:
2168 elapsed = time.time() - start
2166 elapsed = time.time() - start
2169 _timenesting[0] -= indent
2167 _timenesting[0] -= indent
2170 sys.stderr.write('%s%s: %s\n' %
2168 sys.stderr.write('%s%s: %s\n' %
2171 (' ' * _timenesting[0], func.__name__,
2169 (' ' * _timenesting[0], func.__name__,
2172 timecount(elapsed)))
2170 timecount(elapsed)))
2173 return wrapper
2171 return wrapper
2174
2172
2175 _sizeunits = (('m', 2**20), ('k', 2**10), ('g', 2**30),
2173 _sizeunits = (('m', 2**20), ('k', 2**10), ('g', 2**30),
2176 ('kb', 2**10), ('mb', 2**20), ('gb', 2**30), ('b', 1))
2174 ('kb', 2**10), ('mb', 2**20), ('gb', 2**30), ('b', 1))
2177
2175
2178 def sizetoint(s):
2176 def sizetoint(s):
2179 '''Convert a space specifier to a byte count.
2177 '''Convert a space specifier to a byte count.
2180
2178
2181 >>> sizetoint('30')
2179 >>> sizetoint('30')
2182 30
2180 30
2183 >>> sizetoint('2.2kb')
2181 >>> sizetoint('2.2kb')
2184 2252
2182 2252
2185 >>> sizetoint('6M')
2183 >>> sizetoint('6M')
2186 6291456
2184 6291456
2187 '''
2185 '''
2188 t = s.strip().lower()
2186 t = s.strip().lower()
2189 try:
2187 try:
2190 for k, u in _sizeunits:
2188 for k, u in _sizeunits:
2191 if t.endswith(k):
2189 if t.endswith(k):
2192 return int(float(t[:-len(k)]) * u)
2190 return int(float(t[:-len(k)]) * u)
2193 return int(t)
2191 return int(t)
2194 except ValueError:
2192 except ValueError:
2195 raise error.ParseError(_("couldn't parse size: %s") % s)
2193 raise error.ParseError(_("couldn't parse size: %s") % s)
2196
2194
2197 class hooks(object):
2195 class hooks(object):
2198 '''A collection of hook functions that can be used to extend a
2196 '''A collection of hook functions that can be used to extend a
2199 function's behaviour. Hooks are called in lexicographic order,
2197 function's behaviour. Hooks are called in lexicographic order,
2200 based on the names of their sources.'''
2198 based on the names of their sources.'''
2201
2199
2202 def __init__(self):
2200 def __init__(self):
2203 self._hooks = []
2201 self._hooks = []
2204
2202
2205 def add(self, source, hook):
2203 def add(self, source, hook):
2206 self._hooks.append((source, hook))
2204 self._hooks.append((source, hook))
2207
2205
2208 def __call__(self, *args):
2206 def __call__(self, *args):
2209 self._hooks.sort(key=lambda x: x[0])
2207 self._hooks.sort(key=lambda x: x[0])
2210 results = []
2208 results = []
2211 for source, hook in self._hooks:
2209 for source, hook in self._hooks:
2212 results.append(hook(*args))
2210 results.append(hook(*args))
2213 return results
2211 return results
2214
2212
2215 def debugstacktrace(msg='stacktrace', skip=0, f=sys.stderr, otherf=sys.stdout):
2213 def debugstacktrace(msg='stacktrace', skip=0, f=sys.stderr, otherf=sys.stdout):
2216 '''Writes a message to f (stderr) with a nicely formatted stacktrace.
2214 '''Writes a message to f (stderr) with a nicely formatted stacktrace.
2217 Skips the 'skip' last entries. By default it will flush stdout first.
2215 Skips the 'skip' last entries. By default it will flush stdout first.
2218 It can be used everywhere and do intentionally not require an ui object.
2216 It can be used everywhere and do intentionally not require an ui object.
2219 Not be used in production code but very convenient while developing.
2217 Not be used in production code but very convenient while developing.
2220 '''
2218 '''
2221 if otherf:
2219 if otherf:
2222 otherf.flush()
2220 otherf.flush()
2223 f.write('%s at:\n' % msg)
2221 f.write('%s at:\n' % msg)
2224 entries = [('%s:%s' % (fn, ln), func)
2222 entries = [('%s:%s' % (fn, ln), func)
2225 for fn, ln, func, _text in traceback.extract_stack()[:-skip - 1]]
2223 for fn, ln, func, _text in traceback.extract_stack()[:-skip - 1]]
2226 if entries:
2224 if entries:
2227 fnmax = max(len(entry[0]) for entry in entries)
2225 fnmax = max(len(entry[0]) for entry in entries)
2228 for fnln, func in entries:
2226 for fnln, func in entries:
2229 f.write(' %-*s in %s\n' % (fnmax, fnln, func))
2227 f.write(' %-*s in %s\n' % (fnmax, fnln, func))
2230 f.flush()
2228 f.flush()
2231
2229
2232 class dirs(object):
2230 class dirs(object):
2233 '''a multiset of directory names from a dirstate or manifest'''
2231 '''a multiset of directory names from a dirstate or manifest'''
2234
2232
2235 def __init__(self, map, skip=None):
2233 def __init__(self, map, skip=None):
2236 self._dirs = {}
2234 self._dirs = {}
2237 addpath = self.addpath
2235 addpath = self.addpath
2238 if safehasattr(map, 'iteritems') and skip is not None:
2236 if safehasattr(map, 'iteritems') and skip is not None:
2239 for f, s in map.iteritems():
2237 for f, s in map.iteritems():
2240 if s[0] != skip:
2238 if s[0] != skip:
2241 addpath(f)
2239 addpath(f)
2242 else:
2240 else:
2243 for f in map:
2241 for f in map:
2244 addpath(f)
2242 addpath(f)
2245
2243
2246 def addpath(self, path):
2244 def addpath(self, path):
2247 dirs = self._dirs
2245 dirs = self._dirs
2248 for base in finddirs(path):
2246 for base in finddirs(path):
2249 if base in dirs:
2247 if base in dirs:
2250 dirs[base] += 1
2248 dirs[base] += 1
2251 return
2249 return
2252 dirs[base] = 1
2250 dirs[base] = 1
2253
2251
2254 def delpath(self, path):
2252 def delpath(self, path):
2255 dirs = self._dirs
2253 dirs = self._dirs
2256 for base in finddirs(path):
2254 for base in finddirs(path):
2257 if dirs[base] > 1:
2255 if dirs[base] > 1:
2258 dirs[base] -= 1
2256 dirs[base] -= 1
2259 return
2257 return
2260 del dirs[base]
2258 del dirs[base]
2261
2259
2262 def __iter__(self):
2260 def __iter__(self):
2263 return self._dirs.iterkeys()
2261 return self._dirs.iterkeys()
2264
2262
2265 def __contains__(self, d):
2263 def __contains__(self, d):
2266 return d in self._dirs
2264 return d in self._dirs
2267
2265
2268 if safehasattr(parsers, 'dirs'):
2266 if safehasattr(parsers, 'dirs'):
2269 dirs = parsers.dirs
2267 dirs = parsers.dirs
2270
2268
2271 def finddirs(path):
2269 def finddirs(path):
2272 pos = path.rfind('/')
2270 pos = path.rfind('/')
2273 while pos != -1:
2271 while pos != -1:
2274 yield path[:pos]
2272 yield path[:pos]
2275 pos = path.rfind('/', 0, pos)
2273 pos = path.rfind('/', 0, pos)
2276
2274
2277 # convenient shortcut
2275 # convenient shortcut
2278 dst = debugstacktrace
2276 dst = debugstacktrace
General Comments 0
You need to be logged in to leave comments. Login now