##// END OF EJS Templates
remove unused imports
Brodie Rao -
r10463:5ddde896 stable
parent child Browse files
Show More
@@ -1,327 +1,327
1 1 # Mercurial extension to provide the 'hg bookmark' command
2 2 #
3 3 # Copyright 2008 David Soria Parra <dsp@php.net>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''track a line of development with movable markers
9 9
10 10 Bookmarks are local movable markers to changesets. Every bookmark
11 11 points to a changeset identified by its hash. If you commit a
12 12 changeset that is based on a changeset that has a bookmark on it, the
13 13 bookmark shifts to the new changeset.
14 14
15 15 It is possible to use bookmark names in every revision lookup (e.g. hg
16 16 merge, hg update).
17 17
18 18 By default, when several bookmarks point to the same changeset, they
19 19 will all move forward together. It is possible to obtain a more
20 20 git-like experience by adding the following configuration option to
21 21 your .hgrc::
22 22
23 23 [bookmarks]
24 24 track.current = True
25 25
26 26 This will cause Mercurial to track the bookmark that you are currently
27 27 using, and only update it. This is similar to git's approach to
28 28 branching.
29 29 '''
30 30
31 31 from mercurial.i18n import _
32 32 from mercurial.node import nullid, nullrev, hex, short
33 from mercurial import util, commands, localrepo, repair, extensions
33 from mercurial import util, commands, repair, extensions
34 34 import os
35 35
36 36 def write(repo):
37 37 '''Write bookmarks
38 38
39 39 Write the given bookmark => hash dictionary to the .hg/bookmarks file
40 40 in a format equal to those of localtags.
41 41
42 42 We also store a backup of the previous state in undo.bookmarks that
43 43 can be copied back on rollback.
44 44 '''
45 45 refs = repo._bookmarks
46 46 if os.path.exists(repo.join('bookmarks')):
47 47 util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks'))
48 48 if repo._bookmarkcurrent not in refs:
49 49 setcurrent(repo, None)
50 50 wlock = repo.wlock()
51 51 try:
52 52 file = repo.opener('bookmarks', 'w', atomictemp=True)
53 53 for refspec, node in refs.iteritems():
54 54 file.write("%s %s\n" % (hex(node), refspec))
55 55 file.rename()
56 56 finally:
57 57 wlock.release()
58 58
59 59 def setcurrent(repo, mark):
60 60 '''Set the name of the bookmark that we are currently on
61 61
62 62 Set the name of the bookmark that we are on (hg update <bookmark>).
63 63 The name is recorded in .hg/bookmarks.current
64 64 '''
65 65 current = repo._bookmarkcurrent
66 66 if current == mark:
67 67 return
68 68
69 69 refs = repo._bookmarks
70 70
71 71 # do not update if we do update to a rev equal to the current bookmark
72 72 if (mark and mark not in refs and
73 73 current and refs[current] == repo.changectx('.').node()):
74 74 return
75 75 if mark not in refs:
76 76 mark = ''
77 77 wlock = repo.wlock()
78 78 try:
79 79 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
80 80 file.write(mark)
81 81 file.rename()
82 82 finally:
83 83 wlock.release()
84 84 repo._bookmarkcurrent = mark
85 85
86 86 def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None):
87 87 '''track a line of development with movable markers
88 88
89 89 Bookmarks are pointers to certain commits that move when
90 90 committing. Bookmarks are local. They can be renamed, copied and
91 91 deleted. It is possible to use bookmark names in 'hg merge' and
92 92 'hg update' to merge and update respectively to a given bookmark.
93 93
94 94 You can use 'hg bookmark NAME' to set a bookmark on the working
95 95 directory's parent revision with the given name. If you specify
96 96 a revision using -r REV (where REV may be an existing bookmark),
97 97 the bookmark is assigned to that revision.
98 98 '''
99 99 hexfn = ui.debugflag and hex or short
100 100 marks = repo._bookmarks
101 101 cur = repo.changectx('.').node()
102 102
103 103 if rename:
104 104 if rename not in marks:
105 105 raise util.Abort(_("a bookmark of this name does not exist"))
106 106 if mark in marks and not force:
107 107 raise util.Abort(_("a bookmark of the same name already exists"))
108 108 if mark is None:
109 109 raise util.Abort(_("new bookmark name required"))
110 110 marks[mark] = marks[rename]
111 111 del marks[rename]
112 112 if repo._bookmarkcurrent == rename:
113 113 setcurrent(repo, mark)
114 114 write(repo)
115 115 return
116 116
117 117 if delete:
118 118 if mark is None:
119 119 raise util.Abort(_("bookmark name required"))
120 120 if mark not in marks:
121 121 raise util.Abort(_("a bookmark of this name does not exist"))
122 122 if mark == repo._bookmarkcurrent:
123 123 setcurrent(repo, None)
124 124 del marks[mark]
125 125 write(repo)
126 126 return
127 127
128 128 if mark != None:
129 129 if "\n" in mark:
130 130 raise util.Abort(_("bookmark name cannot contain newlines"))
131 131 mark = mark.strip()
132 132 if mark in marks and not force:
133 133 raise util.Abort(_("a bookmark of the same name already exists"))
134 134 if ((mark in repo.branchtags() or mark == repo.dirstate.branch())
135 135 and not force):
136 136 raise util.Abort(
137 137 _("a bookmark cannot have the name of an existing branch"))
138 138 if rev:
139 139 marks[mark] = repo.lookup(rev)
140 140 else:
141 141 marks[mark] = repo.changectx('.').node()
142 142 setcurrent(repo, mark)
143 143 write(repo)
144 144 return
145 145
146 146 if mark is None:
147 147 if rev:
148 148 raise util.Abort(_("bookmark name required"))
149 149 if len(marks) == 0:
150 150 ui.status("no bookmarks set\n")
151 151 else:
152 152 for bmark, n in marks.iteritems():
153 153 if ui.configbool('bookmarks', 'track.current'):
154 154 current = repo._bookmarkcurrent
155 155 prefix = (bmark == current and n == cur) and '*' or ' '
156 156 else:
157 157 prefix = (n == cur) and '*' or ' '
158 158
159 159 if ui.quiet:
160 160 ui.write("%s\n" % bmark)
161 161 else:
162 162 ui.write(" %s %-25s %d:%s\n" % (
163 163 prefix, bmark, repo.changelog.rev(n), hexfn(n)))
164 164 return
165 165
166 166 def _revstostrip(changelog, node):
167 167 srev = changelog.rev(node)
168 168 tostrip = [srev]
169 169 saveheads = []
170 170 for r in xrange(srev, len(changelog)):
171 171 parents = changelog.parentrevs(r)
172 172 if parents[0] in tostrip or parents[1] in tostrip:
173 173 tostrip.append(r)
174 174 if parents[1] != nullrev:
175 175 for p in parents:
176 176 if p not in tostrip and p > srev:
177 177 saveheads.append(p)
178 178 return [r for r in tostrip if r not in saveheads]
179 179
180 180 def strip(oldstrip, ui, repo, node, backup="all"):
181 181 """Strip bookmarks if revisions are stripped using
182 182 the mercurial.strip method. This usually happens during
183 183 qpush and qpop"""
184 184 revisions = _revstostrip(repo.changelog, node)
185 185 marks = repo._bookmarks
186 186 update = []
187 187 for mark, n in marks.iteritems():
188 188 if repo.changelog.rev(n) in revisions:
189 189 update.append(mark)
190 190 oldstrip(ui, repo, node, backup)
191 191 if len(update) > 0:
192 192 for m in update:
193 193 marks[m] = repo.changectx('.').node()
194 194 write(repo)
195 195
196 196 def reposetup(ui, repo):
197 197 if not repo.local():
198 198 return
199 199
200 200 class bookmark_repo(repo.__class__):
201 201
202 202 @util.propertycache
203 203 def _bookmarks(self):
204 204 '''Parse .hg/bookmarks file and return a dictionary
205 205
206 206 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
207 207 in the .hg/bookmarks file. They are read returned as a dictionary
208 208 with name => hash values.
209 209 '''
210 210 try:
211 211 bookmarks = {}
212 212 for line in self.opener('bookmarks'):
213 213 sha, refspec = line.strip().split(' ', 1)
214 214 bookmarks[refspec] = super(bookmark_repo, self).lookup(sha)
215 215 except:
216 216 pass
217 217 return bookmarks
218 218
219 219 @util.propertycache
220 220 def _bookmarkcurrent(self):
221 221 '''Get the current bookmark
222 222
223 223 If we use gittishsh branches we have a current bookmark that
224 224 we are on. This function returns the name of the bookmark. It
225 225 is stored in .hg/bookmarks.current
226 226 '''
227 227 mark = None
228 228 if os.path.exists(self.join('bookmarks.current')):
229 229 file = self.opener('bookmarks.current')
230 230 # No readline() in posixfile_nt, reading everything is cheap
231 231 mark = (file.readlines() or [''])[0]
232 232 if mark == '':
233 233 mark = None
234 234 file.close()
235 235 return mark
236 236
237 237 def rollback(self):
238 238 if os.path.exists(self.join('undo.bookmarks')):
239 239 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
240 240 return super(bookmark_repo, self).rollback()
241 241
242 242 def lookup(self, key):
243 243 if key in self._bookmarks:
244 244 key = self._bookmarks[key]
245 245 return super(bookmark_repo, self).lookup(key)
246 246
247 247 def _bookmarksupdate(self, parents, node):
248 248 marks = self._bookmarks
249 249 update = False
250 250 if ui.configbool('bookmarks', 'track.current'):
251 251 mark = self._bookmarkcurrent
252 252 if mark and marks[mark] in parents:
253 253 marks[mark] = node
254 254 update = True
255 255 else:
256 256 for mark, n in marks.items():
257 257 if n in parents:
258 258 marks[mark] = node
259 259 update = True
260 260 if update:
261 261 write(self)
262 262
263 263 def commitctx(self, ctx, error=False):
264 264 """Add a revision to the repository and
265 265 move the bookmark"""
266 266 wlock = self.wlock() # do both commit and bookmark with lock held
267 267 try:
268 268 node = super(bookmark_repo, self).commitctx(ctx, error)
269 269 if node is None:
270 270 return None
271 271 parents = self.changelog.parents(node)
272 272 if parents[1] == nullid:
273 273 parents = (parents[0],)
274 274
275 275 self._bookmarksupdate(parents, node)
276 276 return node
277 277 finally:
278 278 wlock.release()
279 279
280 280 def addchangegroup(self, source, srctype, url, emptyok=False):
281 281 parents = self.dirstate.parents()
282 282
283 283 result = super(bookmark_repo, self).addchangegroup(
284 284 source, srctype, url, emptyok)
285 285 if result > 1:
286 286 # We have more heads than before
287 287 return result
288 288 node = self.changelog.tip()
289 289
290 290 self._bookmarksupdate(parents, node)
291 291 return result
292 292
293 293 def _findtags(self):
294 294 """Merge bookmarks with normal tags"""
295 295 (tags, tagtypes) = super(bookmark_repo, self)._findtags()
296 296 tags.update(self._bookmarks)
297 297 return (tags, tagtypes)
298 298
299 299 repo.__class__ = bookmark_repo
300 300
301 301 def uisetup(ui):
302 302 extensions.wrapfunction(repair, "strip", strip)
303 303 if ui.configbool('bookmarks', 'track.current'):
304 304 extensions.wrapcommand(commands.table, 'update', updatecurbookmark)
305 305
306 306 def updatecurbookmark(orig, ui, repo, *args, **opts):
307 307 '''Set the current bookmark
308 308
309 309 If the user updates to a bookmark we update the .hg/bookmarks.current
310 310 file.
311 311 '''
312 312 res = orig(ui, repo, *args, **opts)
313 313 rev = opts['rev']
314 314 if not rev and len(args) > 0:
315 315 rev = args[0]
316 316 setcurrent(repo, rev)
317 317 return res
318 318
319 319 cmdtable = {
320 320 "bookmarks":
321 321 (bookmark,
322 322 [('f', 'force', False, _('force')),
323 323 ('r', 'rev', '', _('revision')),
324 324 ('d', 'delete', False, _('delete a given bookmark')),
325 325 ('m', 'rename', '', _('rename a given bookmark'))],
326 326 _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),
327 327 }
@@ -1,368 +1,368
1 1 # color.py color output for the status and qseries commands
2 2 #
3 3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.com>
4 4 #
5 5 # This program is free software; you can redistribute it and/or modify it
6 6 # under the terms of the GNU General Public License as published by the
7 7 # Free Software Foundation; either version 2 of the License, or (at your
8 8 # option) any later version.
9 9 #
10 10 # This program is distributed in the hope that it will be useful, but
11 11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
13 13 # Public License for more details.
14 14 #
15 15 # You should have received a copy of the GNU General Public License along
16 16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 18
19 19 '''colorize output from some commands
20 20
21 21 This extension modifies the status and resolve commands to add color to their
22 22 output to reflect file status, the qseries command to add color to reflect
23 23 patch status (applied, unapplied, missing), and to diff-related
24 24 commands to highlight additions, removals, diff headers, and trailing
25 25 whitespace.
26 26
27 27 Other effects in addition to color, like bold and underlined text, are
28 28 also available. Effects are rendered with the ECMA-48 SGR control
29 29 function (aka ANSI escape codes). This module also provides the
30 30 render_text function, which can be used to add effects to any text.
31 31
32 32 Default effects may be overridden from the .hgrc file::
33 33
34 34 [color]
35 35 status.modified = blue bold underline red_background
36 36 status.added = green bold
37 37 status.removed = red bold blue_background
38 38 status.deleted = cyan bold underline
39 39 status.unknown = magenta bold underline
40 40 status.ignored = black bold
41 41
42 42 # 'none' turns off all effects
43 43 status.clean = none
44 44 status.copied = none
45 45
46 46 qseries.applied = blue bold underline
47 47 qseries.unapplied = black bold
48 48 qseries.missing = red bold
49 49
50 50 diff.diffline = bold
51 51 diff.extended = cyan bold
52 52 diff.file_a = red bold
53 53 diff.file_b = green bold
54 54 diff.hunk = magenta
55 55 diff.deleted = red
56 56 diff.inserted = green
57 57 diff.changed = white
58 58 diff.trailingwhitespace = bold red_background
59 59
60 60 resolve.unresolved = red bold
61 61 resolve.resolved = green bold
62 62
63 63 bookmarks.current = green
64 64 '''
65 65
66 66 import os, sys
67 67
68 from mercurial import cmdutil, commands, extensions, error
68 from mercurial import cmdutil, commands, extensions
69 69 from mercurial.i18n import _
70 70
71 71 # start and stop parameters for effects
72 72 _effect_params = {'none': 0,
73 73 'black': 30,
74 74 'red': 31,
75 75 'green': 32,
76 76 'yellow': 33,
77 77 'blue': 34,
78 78 'magenta': 35,
79 79 'cyan': 36,
80 80 'white': 37,
81 81 'bold': 1,
82 82 'italic': 3,
83 83 'underline': 4,
84 84 'inverse': 7,
85 85 'black_background': 40,
86 86 'red_background': 41,
87 87 'green_background': 42,
88 88 'yellow_background': 43,
89 89 'blue_background': 44,
90 90 'purple_background': 45,
91 91 'cyan_background': 46,
92 92 'white_background': 47}
93 93
94 94 def render_effects(text, effects):
95 95 'Wrap text in commands to turn on each effect.'
96 96 start = [str(_effect_params[e]) for e in ['none'] + effects]
97 97 start = '\033[' + ';'.join(start) + 'm'
98 98 stop = '\033[' + str(_effect_params['none']) + 'm'
99 99 return ''.join([start, text, stop])
100 100
101 101 def _colorstatuslike(abbreviations, effectdefs, orig, ui, repo, *pats, **opts):
102 102 '''run a status-like command with colorized output'''
103 103 delimiter = opts.get('print0') and '\0' or '\n'
104 104
105 105 nostatus = opts.get('no_status')
106 106 opts['no_status'] = False
107 107 # run original command and capture its output
108 108 ui.pushbuffer()
109 109 retval = orig(ui, repo, *pats, **opts)
110 110 # filter out empty strings
111 111 lines_with_status = [line for line in ui.popbuffer().split(delimiter) if line]
112 112
113 113 if nostatus:
114 114 lines = [l[2:] for l in lines_with_status]
115 115 else:
116 116 lines = lines_with_status
117 117
118 118 # apply color to output and display it
119 119 for i in xrange(len(lines)):
120 120 status = abbreviations[lines_with_status[i][0]]
121 121 effects = effectdefs[status]
122 122 if effects:
123 123 lines[i] = render_effects(lines[i], effects)
124 124 ui.write(lines[i] + delimiter)
125 125 return retval
126 126
127 127
128 128 _status_abbreviations = { 'M': 'modified',
129 129 'A': 'added',
130 130 'R': 'removed',
131 131 '!': 'deleted',
132 132 '?': 'unknown',
133 133 'I': 'ignored',
134 134 'C': 'clean',
135 135 ' ': 'copied', }
136 136
137 137 _status_effects = { 'modified': ['blue', 'bold'],
138 138 'added': ['green', 'bold'],
139 139 'removed': ['red', 'bold'],
140 140 'deleted': ['cyan', 'bold', 'underline'],
141 141 'unknown': ['magenta', 'bold', 'underline'],
142 142 'ignored': ['black', 'bold'],
143 143 'clean': ['none'],
144 144 'copied': ['none'], }
145 145
146 146 def colorstatus(orig, ui, repo, *pats, **opts):
147 147 '''run the status command with colored output'''
148 148 return _colorstatuslike(_status_abbreviations, _status_effects,
149 149 orig, ui, repo, *pats, **opts)
150 150
151 151
152 152 _resolve_abbreviations = { 'U': 'unresolved',
153 153 'R': 'resolved', }
154 154
155 155 _resolve_effects = { 'unresolved': ['red', 'bold'],
156 156 'resolved': ['green', 'bold'], }
157 157
158 158 def colorresolve(orig, ui, repo, *pats, **opts):
159 159 '''run the resolve command with colored output'''
160 160 if not opts.get('list'):
161 161 # only colorize for resolve -l
162 162 return orig(ui, repo, *pats, **opts)
163 163 return _colorstatuslike(_resolve_abbreviations, _resolve_effects,
164 164 orig, ui, repo, *pats, **opts)
165 165
166 166
167 167 _bookmark_effects = { 'current': ['green'] }
168 168
169 169 def colorbookmarks(orig, ui, repo, *pats, **opts):
170 170 def colorize(orig, s):
171 171 lines = s.split('\n')
172 172 for i, line in enumerate(lines):
173 173 if line.startswith(" *"):
174 174 lines[i] = render_effects(line, _bookmark_effects['current'])
175 175 orig('\n'.join(lines))
176 176 oldwrite = extensions.wrapfunction(ui, 'write', colorize)
177 177 try:
178 178 orig(ui, repo, *pats, **opts)
179 179 finally:
180 180 ui.write = oldwrite
181 181
182 182 def colorqseries(orig, ui, repo, *dummy, **opts):
183 183 '''run the qseries command with colored output'''
184 184 ui.pushbuffer()
185 185 retval = orig(ui, repo, **opts)
186 186 patchlines = ui.popbuffer().splitlines()
187 187 patchnames = repo.mq.series
188 188
189 189 for patch, patchname in zip(patchlines, patchnames):
190 190 if opts['missing']:
191 191 effects = _patch_effects['missing']
192 192 # Determine if patch is applied.
193 193 elif [applied for applied in repo.mq.applied
194 194 if patchname == applied.name]:
195 195 effects = _patch_effects['applied']
196 196 else:
197 197 effects = _patch_effects['unapplied']
198 198
199 199 patch = patch.replace(patchname, render_effects(patchname, effects), 1)
200 200 ui.write(patch + '\n')
201 201 return retval
202 202
203 203 _patch_effects = { 'applied': ['blue', 'bold', 'underline'],
204 204 'missing': ['red', 'bold'],
205 205 'unapplied': ['black', 'bold'], }
206 206 def colorwrap(orig, *args):
207 207 '''wrap ui.write for colored diff output'''
208 208 def _colorize(s):
209 209 lines = s.split('\n')
210 210 for i, line in enumerate(lines):
211 211 stripline = line
212 212 if line and line[0] in '+-':
213 213 # highlight trailing whitespace, but only in changed lines
214 214 stripline = line.rstrip()
215 215 for prefix, style in _diff_prefixes:
216 216 if stripline.startswith(prefix):
217 217 lines[i] = render_effects(stripline, _diff_effects[style])
218 218 break
219 219 if line != stripline:
220 220 lines[i] += render_effects(
221 221 line[len(stripline):], _diff_effects['trailingwhitespace'])
222 222 return '\n'.join(lines)
223 223 orig(*[_colorize(s) for s in args])
224 224
225 225 def colorshowpatch(orig, self, node):
226 226 '''wrap cmdutil.changeset_printer.showpatch with colored output'''
227 227 oldwrite = extensions.wrapfunction(self.ui, 'write', colorwrap)
228 228 try:
229 229 orig(self, node)
230 230 finally:
231 231 self.ui.write = oldwrite
232 232
233 233 def colordiffstat(orig, s):
234 234 lines = s.split('\n')
235 235 for i, line in enumerate(lines):
236 236 if line and line[-1] in '+-':
237 237 name, graph = line.rsplit(' ', 1)
238 238 graph = graph.replace('-',
239 239 render_effects('-', _diff_effects['deleted']))
240 240 graph = graph.replace('+',
241 241 render_effects('+', _diff_effects['inserted']))
242 242 lines[i] = ' '.join([name, graph])
243 243 orig('\n'.join(lines))
244 244
245 245 def colordiff(orig, ui, repo, *pats, **opts):
246 246 '''run the diff command with colored output'''
247 247 if opts.get('stat'):
248 248 wrapper = colordiffstat
249 249 else:
250 250 wrapper = colorwrap
251 251 oldwrite = extensions.wrapfunction(ui, 'write', wrapper)
252 252 try:
253 253 orig(ui, repo, *pats, **opts)
254 254 finally:
255 255 ui.write = oldwrite
256 256
257 257 def colorchurn(orig, ui, repo, *pats, **opts):
258 258 '''run the churn command with colored output'''
259 259 if not opts.get('diffstat'):
260 260 return orig(ui, repo, *pats, **opts)
261 261 oldwrite = extensions.wrapfunction(ui, 'write', colordiffstat)
262 262 try:
263 263 orig(ui, repo, *pats, **opts)
264 264 finally:
265 265 ui.write = oldwrite
266 266
267 267 _diff_prefixes = [('diff', 'diffline'),
268 268 ('copy', 'extended'),
269 269 ('rename', 'extended'),
270 270 ('old', 'extended'),
271 271 ('new', 'extended'),
272 272 ('deleted', 'extended'),
273 273 ('---', 'file_a'),
274 274 ('+++', 'file_b'),
275 275 ('@', 'hunk'),
276 276 ('-', 'deleted'),
277 277 ('+', 'inserted')]
278 278
279 279 _diff_effects = {'diffline': ['bold'],
280 280 'extended': ['cyan', 'bold'],
281 281 'file_a': ['red', 'bold'],
282 282 'file_b': ['green', 'bold'],
283 283 'hunk': ['magenta'],
284 284 'deleted': ['red'],
285 285 'inserted': ['green'],
286 286 'changed': ['white'],
287 287 'trailingwhitespace': ['bold', 'red_background']}
288 288
289 289 def extsetup(ui):
290 290 '''Initialize the extension.'''
291 291 _setupcmd(ui, 'diff', commands.table, colordiff, _diff_effects)
292 292 _setupcmd(ui, 'incoming', commands.table, None, _diff_effects)
293 293 _setupcmd(ui, 'log', commands.table, None, _diff_effects)
294 294 _setupcmd(ui, 'outgoing', commands.table, None, _diff_effects)
295 295 _setupcmd(ui, 'tip', commands.table, None, _diff_effects)
296 296 _setupcmd(ui, 'status', commands.table, colorstatus, _status_effects)
297 297 _setupcmd(ui, 'resolve', commands.table, colorresolve, _resolve_effects)
298 298
299 299 try:
300 300 mq = extensions.find('mq')
301 301 _setupcmd(ui, 'qdiff', mq.cmdtable, colordiff, _diff_effects)
302 302 _setupcmd(ui, 'qseries', mq.cmdtable, colorqseries, _patch_effects)
303 303 except KeyError:
304 304 mq = None
305 305
306 306 try:
307 307 rec = extensions.find('record')
308 308 _setupcmd(ui, 'record', rec.cmdtable, colordiff, _diff_effects)
309 309 except KeyError:
310 310 rec = None
311 311
312 312 if mq and rec:
313 313 _setupcmd(ui, 'qrecord', rec.cmdtable, colordiff, _diff_effects)
314 314 try:
315 315 churn = extensions.find('churn')
316 316 _setupcmd(ui, 'churn', churn.cmdtable, colorchurn, _diff_effects)
317 317 except KeyError:
318 318 churn = None
319 319
320 320 try:
321 321 bookmarks = extensions.find('bookmarks')
322 322 _setupcmd(ui, 'bookmarks', bookmarks.cmdtable, colorbookmarks,
323 323 _bookmark_effects)
324 324 except KeyError:
325 325 # The bookmarks extension is not enabled
326 326 pass
327 327
328 328 def _setupcmd(ui, cmd, table, func, effectsmap):
329 329 '''patch in command to command table and load effect map'''
330 330 def nocolor(orig, *args, **opts):
331 331
332 332 if (opts['no_color'] or opts['color'] == 'never' or
333 333 (opts['color'] == 'auto' and (os.environ.get('TERM') == 'dumb'
334 334 or not sys.__stdout__.isatty()))):
335 335 del opts['no_color']
336 336 del opts['color']
337 337 return orig(*args, **opts)
338 338
339 339 oldshowpatch = extensions.wrapfunction(cmdutil.changeset_printer,
340 340 'showpatch', colorshowpatch)
341 341 del opts['no_color']
342 342 del opts['color']
343 343 try:
344 344 if func is not None:
345 345 return func(orig, *args, **opts)
346 346 return orig(*args, **opts)
347 347 finally:
348 348 cmdutil.changeset_printer.showpatch = oldshowpatch
349 349
350 350 entry = extensions.wrapcommand(table, cmd, nocolor)
351 351 entry[1].extend([
352 352 ('', 'color', 'auto', _("when to colorize (always, auto, or never)")),
353 353 ('', 'no-color', None, _("don't colorize output (DEPRECATED)")),
354 354 ])
355 355
356 356 for status in effectsmap:
357 357 configkey = cmd + '.' + status
358 358 effects = ui.configlist('color', configkey)
359 359 if effects:
360 360 good = []
361 361 for e in effects:
362 362 if e in _effect_params:
363 363 good.append(e)
364 364 else:
365 365 ui.warn(_("ignoring unknown color/effect %r "
366 366 "(configured in color.%s)\n")
367 367 % (e, configkey))
368 368 effectsmap[status] = good
@@ -1,87 +1,86
1 1 # __init__.py - inotify-based status acceleration for Linux
2 2 #
3 3 # Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com>
4 4 # Copyright 2007, 2008 Brendan Cully <brendan@kublai.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''accelerate status report using Linux's inotify service'''
10 10
11 11 # todo: socket permissions
12 12
13 13 from mercurial.i18n import _
14 from mercurial import cmdutil, util
15 14 import server
16 15 from client import client, QueryFailed
17 16
18 17 def serve(ui, repo, **opts):
19 18 '''start an inotify server for this repository'''
20 19 server.start(ui, repo.dirstate, repo.root, opts)
21 20
22 21 def debuginotify(ui, repo, **opts):
23 22 '''debugging information for inotify extension
24 23
25 24 Prints the list of directories being watched by the inotify server.
26 25 '''
27 26 cli = client(ui, repo)
28 27 response = cli.debugquery()
29 28
30 29 ui.write(_('directories being watched:\n'))
31 30 for path in response:
32 31 ui.write((' %s/\n') % path)
33 32
34 33 def reposetup(ui, repo):
35 34 if not hasattr(repo, 'dirstate'):
36 35 return
37 36
38 37 class inotifydirstate(repo.dirstate.__class__):
39 38
40 39 # We'll set this to false after an unsuccessful attempt so that
41 40 # next calls of status() within the same instance don't try again
42 41 # to start an inotify server if it won't start.
43 42 _inotifyon = True
44 43
45 44 def status(self, match, subrepos, ignored, clean, unknown=True):
46 45 files = match.files()
47 46 if '.' in files:
48 47 files = []
49 48 if self._inotifyon and not ignored and not subrepos and not self._dirty:
50 49 cli = client(ui, repo)
51 50 try:
52 51 result = cli.statusquery(files, match, False,
53 52 clean, unknown)
54 53 except QueryFailed, instr:
55 54 ui.debug(str(instr))
56 55 # don't retry within the same hg instance
57 56 inotifydirstate._inotifyon = False
58 57 pass
59 58 else:
60 59 if ui.config('inotify', 'debug'):
61 60 r2 = super(inotifydirstate, self).status(
62 61 match, False, clean, unknown)
63 62 for c, a, b in zip('LMARDUIC', result, r2):
64 63 for f in a:
65 64 if f not in b:
66 65 ui.warn('*** inotify: %s +%s\n' % (c, f))
67 66 for f in b:
68 67 if f not in a:
69 68 ui.warn('*** inotify: %s -%s\n' % (c, f))
70 69 result = r2
71 70 return result
72 71 return super(inotifydirstate, self).status(
73 72 match, subrepos, ignored, clean, unknown)
74 73
75 74 repo.dirstate.__class__ = inotifydirstate
76 75
77 76 cmdtable = {
78 77 'debuginotify':
79 78 (debuginotify, [], ('hg debuginotify')),
80 79 '^inserve':
81 80 (serve,
82 81 [('d', 'daemon', None, _('run server in background')),
83 82 ('', 'daemon-pipefds', '', _('used internally by daemon mode')),
84 83 ('t', 'idle-timeout', '', _('minutes to sit idle before exiting')),
85 84 ('', 'pid-file', '', _('name of file to write process ID to'))],
86 85 _('hg inserve [OPTION]...')),
87 86 }
@@ -1,442 +1,441
1 1 # linuxserver.py - inotify status server for linux
2 2 #
3 3 # Copyright 2006, 2007, 2008 Bryan O'Sullivan <bos@serpentine.com>
4 4 # Copyright 2007, 2008 Brendan Cully <brendan@kublai.com>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 from mercurial.i18n import _
10 10 from mercurial import osutil, util
11 import common
12 11 import server
13 12 import errno, os, select, stat, sys, time
14 13
15 14 try:
16 15 import linux as inotify
17 16 from linux import watcher
18 17 except ImportError:
19 18 raise
20 19
21 20 def walkrepodirs(dirstate, absroot):
22 21 '''Iterate over all subdirectories of this repo.
23 22 Exclude the .hg directory, any nested repos, and ignored dirs.'''
24 23 def walkit(dirname, top):
25 24 fullpath = server.join(absroot, dirname)
26 25 try:
27 26 for name, kind in osutil.listdir(fullpath):
28 27 if kind == stat.S_IFDIR:
29 28 if name == '.hg':
30 29 if not top:
31 30 return
32 31 else:
33 32 d = server.join(dirname, name)
34 33 if dirstate._ignore(d):
35 34 continue
36 35 for subdir in walkit(d, False):
37 36 yield subdir
38 37 except OSError, err:
39 38 if err.errno not in server.walk_ignored_errors:
40 39 raise
41 40 yield fullpath
42 41
43 42 return walkit('', True)
44 43
45 44 def _explain_watch_limit(ui, dirstate, rootabs):
46 45 path = '/proc/sys/fs/inotify/max_user_watches'
47 46 try:
48 47 limit = int(file(path).read())
49 48 except IOError, err:
50 49 if err.errno != errno.ENOENT:
51 50 raise
52 51 raise util.Abort(_('this system does not seem to '
53 52 'support inotify'))
54 53 ui.warn(_('*** the current per-user limit on the number '
55 54 'of inotify watches is %s\n') % limit)
56 55 ui.warn(_('*** this limit is too low to watch every '
57 56 'directory in this repository\n'))
58 57 ui.warn(_('*** counting directories: '))
59 58 ndirs = len(list(walkrepodirs(dirstate, rootabs)))
60 59 ui.warn(_('found %d\n') % ndirs)
61 60 newlimit = min(limit, 1024)
62 61 while newlimit < ((limit + ndirs) * 1.1):
63 62 newlimit *= 2
64 63 ui.warn(_('*** to raise the limit from %d to %d (run as root):\n') %
65 64 (limit, newlimit))
66 65 ui.warn(_('*** echo %d > %s\n') % (newlimit, path))
67 66 raise util.Abort(_('cannot watch %s until inotify watch limit is raised')
68 67 % rootabs)
69 68
70 69 class pollable(object):
71 70 """
72 71 Interface to support polling.
73 72 The file descriptor returned by fileno() is registered to a polling
74 73 object.
75 74 Usage:
76 75 Every tick, check if an event has happened since the last tick:
77 76 * If yes, call handle_events
78 77 * If no, call handle_timeout
79 78 """
80 79 poll_events = select.POLLIN
81 80 instances = {}
82 81 poll = select.poll()
83 82
84 83 def fileno(self):
85 84 raise NotImplementedError
86 85
87 86 def handle_events(self, events):
88 87 raise NotImplementedError
89 88
90 89 def handle_timeout(self):
91 90 raise NotImplementedError
92 91
93 92 def shutdown(self):
94 93 raise NotImplementedError
95 94
96 95 def register(self, timeout):
97 96 fd = self.fileno()
98 97
99 98 pollable.poll.register(fd, pollable.poll_events)
100 99 pollable.instances[fd] = self
101 100
102 101 self.registered = True
103 102 self.timeout = timeout
104 103
105 104 def unregister(self):
106 105 pollable.poll.unregister(self)
107 106 self.registered = False
108 107
109 108 @classmethod
110 109 def run(cls):
111 110 while True:
112 111 timeout = None
113 112 timeobj = None
114 113 for obj in cls.instances.itervalues():
115 114 if obj.timeout is not None and (timeout is None
116 115 or obj.timeout < timeout):
117 116 timeout, timeobj = obj.timeout, obj
118 117 try:
119 118 events = cls.poll.poll(timeout)
120 119 except select.error, err:
121 120 if err[0] == errno.EINTR:
122 121 continue
123 122 raise
124 123 if events:
125 124 by_fd = {}
126 125 for fd, event in events:
127 126 by_fd.setdefault(fd, []).append(event)
128 127
129 128 for fd, events in by_fd.iteritems():
130 129 cls.instances[fd].handle_pollevents(events)
131 130
132 131 elif timeobj:
133 132 timeobj.handle_timeout()
134 133
135 134 def eventaction(code):
136 135 """
137 136 Decorator to help handle events in repowatcher
138 137 """
139 138 def decorator(f):
140 139 def wrapper(self, wpath):
141 140 if code == 'm' and wpath in self.lastevent and \
142 141 self.lastevent[wpath] in 'cm':
143 142 return
144 143 self.lastevent[wpath] = code
145 144 self.timeout = 250
146 145
147 146 f(self, wpath)
148 147
149 148 wrapper.func_name = f.func_name
150 149 return wrapper
151 150 return decorator
152 151
153 152 class repowatcher(server.repowatcher, pollable):
154 153 """
155 154 Watches inotify events
156 155 """
157 156 mask = (
158 157 inotify.IN_ATTRIB |
159 158 inotify.IN_CREATE |
160 159 inotify.IN_DELETE |
161 160 inotify.IN_DELETE_SELF |
162 161 inotify.IN_MODIFY |
163 162 inotify.IN_MOVED_FROM |
164 163 inotify.IN_MOVED_TO |
165 164 inotify.IN_MOVE_SELF |
166 165 inotify.IN_ONLYDIR |
167 166 inotify.IN_UNMOUNT |
168 167 0)
169 168
170 169 def __init__(self, ui, dirstate, root):
171 170 server.repowatcher.__init__(self, ui, dirstate, root)
172 171
173 172 self.lastevent = {}
174 173 self.dirty = False
175 174 try:
176 175 self.watcher = watcher.watcher()
177 176 except OSError, err:
178 177 raise util.Abort(_('inotify service not available: %s') %
179 178 err.strerror)
180 179 self.threshold = watcher.threshold(self.watcher)
181 180 self.fileno = self.watcher.fileno
182 181 self.register(timeout=None)
183 182
184 183 self.handle_timeout()
185 184 self.scan()
186 185
187 186 def event_time(self):
188 187 last = self.last_event
189 188 now = time.time()
190 189 self.last_event = now
191 190
192 191 if last is None:
193 192 return 'start'
194 193 delta = now - last
195 194 if delta < 5:
196 195 return '+%.3f' % delta
197 196 if delta < 50:
198 197 return '+%.2f' % delta
199 198 return '+%.1f' % delta
200 199
201 200 def add_watch(self, path, mask):
202 201 if not path:
203 202 return
204 203 if self.watcher.path(path) is None:
205 204 if self.ui.debugflag:
206 205 self.ui.note(_('watching %r\n') % path[self.prefixlen:])
207 206 try:
208 207 self.watcher.add(path, mask)
209 208 except OSError, err:
210 209 if err.errno in (errno.ENOENT, errno.ENOTDIR):
211 210 return
212 211 if err.errno != errno.ENOSPC:
213 212 raise
214 213 _explain_watch_limit(self.ui, self.dirstate, self.wprefix)
215 214
216 215 def setup(self):
217 216 self.ui.note(_('watching directories under %r\n') % self.wprefix)
218 217 self.add_watch(self.wprefix + '.hg', inotify.IN_DELETE)
219 218
220 219 def scan(self, topdir=''):
221 220 ds = self.dirstate._map.copy()
222 221 self.add_watch(server.join(self.wprefix, topdir), self.mask)
223 222 for root, dirs, files in server.walk(self.dirstate, self.wprefix,
224 223 topdir):
225 224 for d in dirs:
226 225 self.add_watch(server.join(root, d), self.mask)
227 226 wroot = root[self.prefixlen:]
228 227 for fn in files:
229 228 wfn = server.join(wroot, fn)
230 229 self.updatefile(wfn, self.getstat(wfn))
231 230 ds.pop(wfn, None)
232 231 wtopdir = topdir
233 232 if wtopdir and wtopdir[-1] != '/':
234 233 wtopdir += '/'
235 234 for wfn, state in ds.iteritems():
236 235 if not wfn.startswith(wtopdir):
237 236 continue
238 237 try:
239 238 st = self.stat(wfn)
240 239 except OSError:
241 240 status = state[0]
242 241 self.deletefile(wfn, status)
243 242 else:
244 243 self.updatefile(wfn, st)
245 244 self.check_deleted('!')
246 245 self.check_deleted('r')
247 246
248 247 @eventaction('c')
249 248 def created(self, wpath):
250 249 if wpath == '.hgignore':
251 250 self.update_hgignore()
252 251 try:
253 252 st = self.stat(wpath)
254 253 if stat.S_ISREG(st[0]) or stat.S_ISLNK(st[0]):
255 254 self.updatefile(wpath, st)
256 255 except OSError:
257 256 pass
258 257
259 258 @eventaction('m')
260 259 def modified(self, wpath):
261 260 if wpath == '.hgignore':
262 261 self.update_hgignore()
263 262 try:
264 263 st = self.stat(wpath)
265 264 if stat.S_ISREG(st[0]):
266 265 if self.dirstate[wpath] in 'lmn':
267 266 self.updatefile(wpath, st)
268 267 except OSError:
269 268 pass
270 269
271 270 @eventaction('d')
272 271 def deleted(self, wpath):
273 272 if wpath == '.hgignore':
274 273 self.update_hgignore()
275 274 elif wpath.startswith('.hg/'):
276 275 return
277 276
278 277 self.deletefile(wpath, self.dirstate[wpath])
279 278
280 279 def process_create(self, wpath, evt):
281 280 if self.ui.debugflag:
282 281 self.ui.note(_('%s event: created %s\n') %
283 282 (self.event_time(), wpath))
284 283
285 284 if evt.mask & inotify.IN_ISDIR:
286 285 self.scan(wpath)
287 286 else:
288 287 self.created(wpath)
289 288
290 289 def process_delete(self, wpath, evt):
291 290 if self.ui.debugflag:
292 291 self.ui.note(_('%s event: deleted %s\n') %
293 292 (self.event_time(), wpath))
294 293
295 294 if evt.mask & inotify.IN_ISDIR:
296 295 tree = self.tree.dir(wpath)
297 296 todelete = [wfn for wfn, ignore in tree.walk('?')]
298 297 for fn in todelete:
299 298 self.deletefile(fn, '?')
300 299 self.scan(wpath)
301 300 else:
302 301 self.deleted(wpath)
303 302
304 303 def process_modify(self, wpath, evt):
305 304 if self.ui.debugflag:
306 305 self.ui.note(_('%s event: modified %s\n') %
307 306 (self.event_time(), wpath))
308 307
309 308 if not (evt.mask & inotify.IN_ISDIR):
310 309 self.modified(wpath)
311 310
312 311 def process_unmount(self, evt):
313 312 self.ui.warn(_('filesystem containing %s was unmounted\n') %
314 313 evt.fullpath)
315 314 sys.exit(0)
316 315
317 316 def handle_pollevents(self, events):
318 317 if self.ui.debugflag:
319 318 self.ui.note(_('%s readable: %d bytes\n') %
320 319 (self.event_time(), self.threshold.readable()))
321 320 if not self.threshold():
322 321 if self.registered:
323 322 if self.ui.debugflag:
324 323 self.ui.note(_('%s below threshold - unhooking\n') %
325 324 (self.event_time()))
326 325 self.unregister()
327 326 self.timeout = 250
328 327 else:
329 328 self.read_events()
330 329
331 330 def read_events(self, bufsize=None):
332 331 events = self.watcher.read(bufsize)
333 332 if self.ui.debugflag:
334 333 self.ui.note(_('%s reading %d events\n') %
335 334 (self.event_time(), len(events)))
336 335 for evt in events:
337 336 if evt.fullpath == self.wprefix[:-1]:
338 337 # events on the root of the repository
339 338 # itself, e.g. permission changes or repository move
340 339 continue
341 340 assert evt.fullpath.startswith(self.wprefix)
342 341 wpath = evt.fullpath[self.prefixlen:]
343 342
344 343 # paths have been normalized, wpath never ends with a '/'
345 344
346 345 if wpath.startswith('.hg/') and evt.mask & inotify.IN_ISDIR:
347 346 # ignore subdirectories of .hg/ (merge, patches...)
348 347 continue
349 348 if wpath == ".hg/wlock":
350 349 if evt.mask & inotify.IN_DELETE:
351 350 self.dirstate.invalidate()
352 351 self.dirty = False
353 352 self.scan()
354 353 elif evt.mask & inotify.IN_CREATE:
355 354 self.dirty = True
356 355 else:
357 356 if self.dirty:
358 357 continue
359 358
360 359 if evt.mask & inotify.IN_UNMOUNT:
361 360 self.process_unmount(wpath, evt)
362 361 elif evt.mask & (inotify.IN_MODIFY | inotify.IN_ATTRIB):
363 362 self.process_modify(wpath, evt)
364 363 elif evt.mask & (inotify.IN_DELETE | inotify.IN_DELETE_SELF |
365 364 inotify.IN_MOVED_FROM):
366 365 self.process_delete(wpath, evt)
367 366 elif evt.mask & (inotify.IN_CREATE | inotify.IN_MOVED_TO):
368 367 self.process_create(wpath, evt)
369 368
370 369 self.lastevent.clear()
371 370
372 371 def handle_timeout(self):
373 372 if not self.registered:
374 373 if self.ui.debugflag:
375 374 self.ui.note(_('%s hooking back up with %d bytes readable\n') %
376 375 (self.event_time(), self.threshold.readable()))
377 376 self.read_events(0)
378 377 self.register(timeout=None)
379 378
380 379 self.timeout = None
381 380
382 381 def shutdown(self):
383 382 self.watcher.close()
384 383
385 384 def debug(self):
386 385 """
387 386 Returns a sorted list of relatives paths currently watched,
388 387 for debugging purposes.
389 388 """
390 389 return sorted(tuple[0][self.prefixlen:] for tuple in self.watcher)
391 390
392 391 class socketlistener(server.socketlistener, pollable):
393 392 """
394 393 Listens for client queries on unix socket inotify.sock
395 394 """
396 395 def __init__(self, ui, root, repowatcher, timeout):
397 396 server.socketlistener.__init__(self, ui, root, repowatcher, timeout)
398 397 self.register(timeout=timeout)
399 398
400 399 def handle_timeout(self):
401 400 pass
402 401
403 402 def handle_pollevents(self, events):
404 403 for e in events:
405 404 self.accept_connection()
406 405
407 406 def shutdown(self):
408 407 self.sock.close()
409 408 try:
410 409 os.unlink(self.sockpath)
411 410 if self.realsockpath:
412 411 os.unlink(self.realsockpath)
413 412 os.rmdir(os.path.dirname(self.realsockpath))
414 413 except OSError, err:
415 414 if err.errno != errno.ENOENT:
416 415 raise
417 416
418 417 def answer_stat_query(self, cs):
419 418 if self.repowatcher.timeout:
420 419 # We got a query while a rescan is pending. Make sure we
421 420 # rescan before responding, or we could give back a wrong
422 421 # answer.
423 422 self.repowatcher.handle_timeout()
424 423 return server.socketlistener.answer_stat_query(self, cs)
425 424
426 425 class master(object):
427 426 def __init__(self, ui, dirstate, root, timeout=None):
428 427 self.ui = ui
429 428 self.repowatcher = repowatcher(ui, dirstate, root)
430 429 self.socketlistener = socketlistener(ui, root, self.repowatcher,
431 430 timeout)
432 431
433 432 def shutdown(self):
434 433 for obj in pollable.instances.itervalues():
435 434 obj.shutdown()
436 435
437 436 def run(self):
438 437 self.repowatcher.setup()
439 438 self.ui.note(_('finished setup\n'))
440 439 if os.getenv('TIME_STARTUP'):
441 440 sys.exit(0)
442 441 pollable.run()
@@ -1,183 +1,182
1 1 # progress.py show progress bars for some actions
2 2 #
3 3 # Copyright (C) 2010 Augie Fackler <durin42@gmail.com>
4 4 #
5 5 # This program is free software; you can redistribute it and/or modify it
6 6 # under the terms of the GNU General Public License as published by the
7 7 # Free Software Foundation; either version 2 of the License, or (at your
8 8 # option) any later version.
9 9 #
10 10 # This program is distributed in the hope that it will be useful, but
11 11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
13 13 # Public License for more details.
14 14 #
15 15 # You should have received a copy of the GNU General Public License along
16 16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 18
19 19 """show progress bars for some actions
20 20
21 21 This extension uses the progress information logged by hg commands
22 22 to draw progress bars that are as informative as possible. Some progress
23 23 bars only offer indeterminate information, while others have a definite
24 24 end point.
25 25
26 26 The following settings are available::
27 27
28 28 [progress]
29 29 delay = 3 # number of seconds (float) before showing the progress bar
30 30 refresh = 0.1 # time in seconds between refreshes of the progress bar
31 31 format = topic bar number # format of the progress bar
32 32 width = <none> # if set, the maximum width of the progress information
33 33 # (that is, min(width, term width) will be used)
34 34 clear-complete = True # clear the progress bar after it's done
35 35
36 36 Valid entries for the format field are topic, bar, number, unit, and item.
37 37 item defaults to the last 20 characters of the item, but this can be
38 38 changed by adding either -<num> which would take the last num characters,
39 39 or +<num> for the first num characters.
40 40 """
41 41
42 import math
43 42 import sys
44 43 import time
45 44
46 45 from mercurial import extensions
47 46 from mercurial import util
48 47
49 48 def spacejoin(*args):
50 49 return ' '.join(s for s in args if s)
51 50
52 51 class progbar(object):
53 52 def __init__(self, ui):
54 53 self.ui = ui
55 54 self.resetstate()
56 55
57 56 def resetstate(self):
58 57 self.topics = []
59 58 self.printed = False
60 59 self.lastprint = time.time() + float(self.ui.config(
61 60 'progress', 'delay', default=3))
62 61 self.indetcount = 0
63 62 self.refresh = float(self.ui.config(
64 63 'progress', 'refresh', default=0.1))
65 64 self.order = self.ui.configlist(
66 65 'progress', 'format',
67 66 default=['topic', 'bar', 'number'])
68 67
69 68 def show(self, topic, pos, item, unit, total):
70 69 termwidth = self.width()
71 70 self.printed = True
72 71 head = ''
73 72 needprogress = False
74 73 tail = ''
75 74 for indicator in self.order:
76 75 add = ''
77 76 if indicator == 'topic':
78 77 add = topic
79 78 elif indicator == 'number':
80 79 if total:
81 80 add = ('% ' + str(len(str(total))) +
82 81 's/%s') % (pos, total)
83 82 else:
84 83 add = str(pos)
85 84 elif indicator.startswith('item') and item:
86 85 slice = 'end'
87 86 if '-' in indicator:
88 87 wid = int(indicator.split('-')[1])
89 88 elif '+' in indicator:
90 89 slice = 'beginning'
91 90 wid = int(indicator.split('+')[1])
92 91 else:
93 92 wid = 20
94 93 if slice == 'end':
95 94 add = item[-wid:]
96 95 else:
97 96 add = item[:wid]
98 97 add += (wid - len(add)) * ' '
99 98 elif indicator == 'bar':
100 99 add = ''
101 100 needprogress = True
102 101 elif indicator == 'unit' and unit:
103 102 add = unit
104 103 if not needprogress:
105 104 head = spacejoin(head, add)
106 105 else:
107 106 tail = spacejoin(add, tail)
108 107 if needprogress:
109 108 used = 0
110 109 if head:
111 110 used += len(head) + 1
112 111 if tail:
113 112 used += len(tail) + 1
114 113 progwidth = termwidth - used - 3
115 114 if total:
116 115 amt = pos * progwidth // total
117 116 bar = '=' * (amt - 1)
118 117 if amt > 0:
119 118 bar += '>'
120 119 bar += ' ' * (progwidth - amt)
121 120 else:
122 121 progwidth -= 3
123 122 self.indetcount += 1
124 123 # mod the count by twice the width so we can make the
125 124 # cursor bounce between the right and left sides
126 125 amt = self.indetcount % (2 * progwidth)
127 126 amt -= progwidth
128 127 bar = (' ' * int(progwidth - abs(amt)) + '<=>' +
129 128 ' ' * int(abs(amt)))
130 129 prog = ''.join(('[', bar , ']'))
131 130 out = spacejoin(head, prog, tail)
132 131 else:
133 132 out = spacejoin(head, tail)
134 133 sys.stdout.write('\r' + out[:termwidth])
135 134 sys.stdout.flush()
136 135
137 136 def clear(self):
138 137 sys.stdout.write('\r%s\r' % (' ' * self.width()))
139 138
140 139 def complete(self):
141 140 if self.ui.configbool('progress', 'clear-complete', default=True):
142 141 self.clear()
143 142 else:
144 143 sys.stdout.write('\n')
145 144 sys.stdout.flush()
146 145
147 146 def width(self):
148 147 tw = util.termwidth()
149 148 return min(int(self.ui.config('progress', 'width', default=tw)), tw)
150 149
151 150 def progress(self, orig, topic, pos, item='', unit='', total=None):
152 151 if pos is None:
153 152 if self.topics and self.topics[-1] == topic and self.printed:
154 153 self.complete()
155 154 self.resetstate()
156 155 else:
157 156 if topic not in self.topics:
158 157 self.topics.append(topic)
159 158 now = time.time()
160 159 if now - self.lastprint > self.refresh and topic == self.topics[-1]:
161 160 self.lastprint = now
162 161 self.show(topic, pos, item, unit, total)
163 162 return orig(topic, pos, item=item, unit=unit, total=total)
164 163
165 164 def write(self, orig, *args):
166 165 if self.printed:
167 166 self.clear()
168 167 return orig(*args)
169 168
170 169 sharedprog = None
171 170
172 171 def uisetup(ui):
173 172 if ui.interactive() and not ui.debugflag:
174 173 # we instantiate one globally shared progress bar to avoid
175 174 # competing progress bars when multiple UI objects get created
176 175 global sharedprog
177 176 if not sharedprog:
178 177 sharedprog = progbar(ui)
179 178 extensions.wrapfunction(ui, 'progress', sharedprog.progress)
180 179 extensions.wrapfunction(ui, 'write', sharedprog.write)
181 180
182 181 def reposetup(ui, repo):
183 182 uisetup(repo.ui)
@@ -1,1191 +1,1191
1 1 # cmdutil.py - help for command processing in mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from node import hex, nullid, nullrev, short
9 9 from i18n import _
10 import os, sys, errno, re, glob, tempfile, time
10 import os, sys, errno, re, glob, tempfile
11 11 import mdiff, bdiff, util, templater, patch, error, encoding, templatekw
12 12 import match as _match
13 13
14 14 revrangesep = ':'
15 15
16 16 def parsealiases(cmd):
17 17 return cmd.lstrip("^").split("|")
18 18
19 19 def findpossible(cmd, table, strict=False):
20 20 """
21 21 Return cmd -> (aliases, command table entry)
22 22 for each matching command.
23 23 Return debug commands (or their aliases) only if no normal command matches.
24 24 """
25 25 choice = {}
26 26 debugchoice = {}
27 27 for e in table.keys():
28 28 aliases = parsealiases(e)
29 29 found = None
30 30 if cmd in aliases:
31 31 found = cmd
32 32 elif not strict:
33 33 for a in aliases:
34 34 if a.startswith(cmd):
35 35 found = a
36 36 break
37 37 if found is not None:
38 38 if aliases[0].startswith("debug") or found.startswith("debug"):
39 39 debugchoice[found] = (aliases, table[e])
40 40 else:
41 41 choice[found] = (aliases, table[e])
42 42
43 43 if not choice and debugchoice:
44 44 choice = debugchoice
45 45
46 46 return choice
47 47
48 48 def findcmd(cmd, table, strict=True):
49 49 """Return (aliases, command table entry) for command string."""
50 50 choice = findpossible(cmd, table, strict)
51 51
52 52 if cmd in choice:
53 53 return choice[cmd]
54 54
55 55 if len(choice) > 1:
56 56 clist = choice.keys()
57 57 clist.sort()
58 58 raise error.AmbiguousCommand(cmd, clist)
59 59
60 60 if choice:
61 61 return choice.values()[0]
62 62
63 63 raise error.UnknownCommand(cmd)
64 64
65 65 def findrepo(p):
66 66 while not os.path.isdir(os.path.join(p, ".hg")):
67 67 oldp, p = p, os.path.dirname(p)
68 68 if p == oldp:
69 69 return None
70 70
71 71 return p
72 72
73 73 def bail_if_changed(repo):
74 74 if repo.dirstate.parents()[1] != nullid:
75 75 raise util.Abort(_('outstanding uncommitted merge'))
76 76 modified, added, removed, deleted = repo.status()[:4]
77 77 if modified or added or removed or deleted:
78 78 raise util.Abort(_("outstanding uncommitted changes"))
79 79
80 80 def logmessage(opts):
81 81 """ get the log message according to -m and -l option """
82 82 message = opts.get('message')
83 83 logfile = opts.get('logfile')
84 84
85 85 if message and logfile:
86 86 raise util.Abort(_('options --message and --logfile are mutually '
87 87 'exclusive'))
88 88 if not message and logfile:
89 89 try:
90 90 if logfile == '-':
91 91 message = sys.stdin.read()
92 92 else:
93 93 message = open(logfile).read()
94 94 except IOError, inst:
95 95 raise util.Abort(_("can't read commit message '%s': %s") %
96 96 (logfile, inst.strerror))
97 97 return message
98 98
99 99 def loglimit(opts):
100 100 """get the log limit according to option -l/--limit"""
101 101 limit = opts.get('limit')
102 102 if limit:
103 103 try:
104 104 limit = int(limit)
105 105 except ValueError:
106 106 raise util.Abort(_('limit must be a positive integer'))
107 107 if limit <= 0:
108 108 raise util.Abort(_('limit must be positive'))
109 109 else:
110 110 limit = None
111 111 return limit
112 112
113 113 def remoteui(src, opts):
114 114 'build a remote ui from ui or repo and opts'
115 115 if hasattr(src, 'baseui'): # looks like a repository
116 116 dst = src.baseui.copy() # drop repo-specific config
117 117 src = src.ui # copy target options from repo
118 118 else: # assume it's a global ui object
119 119 dst = src.copy() # keep all global options
120 120
121 121 # copy ssh-specific options
122 122 for o in 'ssh', 'remotecmd':
123 123 v = opts.get(o) or src.config('ui', o)
124 124 if v:
125 125 dst.setconfig("ui", o, v)
126 126
127 127 # copy bundle-specific options
128 128 r = src.config('bundle', 'mainreporoot')
129 129 if r:
130 130 dst.setconfig('bundle', 'mainreporoot', r)
131 131
132 132 # copy auth section settings
133 133 for key, val in src.configitems('auth'):
134 134 dst.setconfig('auth', key, val)
135 135
136 136 return dst
137 137
138 138 def revpair(repo, revs):
139 139 '''return pair of nodes, given list of revisions. second item can
140 140 be None, meaning use working dir.'''
141 141
142 142 def revfix(repo, val, defval):
143 143 if not val and val != 0 and defval is not None:
144 144 val = defval
145 145 return repo.lookup(val)
146 146
147 147 if not revs:
148 148 return repo.dirstate.parents()[0], None
149 149 end = None
150 150 if len(revs) == 1:
151 151 if revrangesep in revs[0]:
152 152 start, end = revs[0].split(revrangesep, 1)
153 153 start = revfix(repo, start, 0)
154 154 end = revfix(repo, end, len(repo) - 1)
155 155 else:
156 156 start = revfix(repo, revs[0], None)
157 157 elif len(revs) == 2:
158 158 if revrangesep in revs[0] or revrangesep in revs[1]:
159 159 raise util.Abort(_('too many revisions specified'))
160 160 start = revfix(repo, revs[0], None)
161 161 end = revfix(repo, revs[1], None)
162 162 else:
163 163 raise util.Abort(_('too many revisions specified'))
164 164 return start, end
165 165
166 166 def revrange(repo, revs):
167 167 """Yield revision as strings from a list of revision specifications."""
168 168
169 169 def revfix(repo, val, defval):
170 170 if not val and val != 0 and defval is not None:
171 171 return defval
172 172 return repo.changelog.rev(repo.lookup(val))
173 173
174 174 seen, l = set(), []
175 175 for spec in revs:
176 176 if revrangesep in spec:
177 177 start, end = spec.split(revrangesep, 1)
178 178 start = revfix(repo, start, 0)
179 179 end = revfix(repo, end, len(repo) - 1)
180 180 step = start > end and -1 or 1
181 181 for rev in xrange(start, end + step, step):
182 182 if rev in seen:
183 183 continue
184 184 seen.add(rev)
185 185 l.append(rev)
186 186 else:
187 187 rev = revfix(repo, spec, None)
188 188 if rev in seen:
189 189 continue
190 190 seen.add(rev)
191 191 l.append(rev)
192 192
193 193 return l
194 194
195 195 def make_filename(repo, pat, node,
196 196 total=None, seqno=None, revwidth=None, pathname=None):
197 197 node_expander = {
198 198 'H': lambda: hex(node),
199 199 'R': lambda: str(repo.changelog.rev(node)),
200 200 'h': lambda: short(node),
201 201 }
202 202 expander = {
203 203 '%': lambda: '%',
204 204 'b': lambda: os.path.basename(repo.root),
205 205 }
206 206
207 207 try:
208 208 if node:
209 209 expander.update(node_expander)
210 210 if node:
211 211 expander['r'] = (lambda:
212 212 str(repo.changelog.rev(node)).zfill(revwidth or 0))
213 213 if total is not None:
214 214 expander['N'] = lambda: str(total)
215 215 if seqno is not None:
216 216 expander['n'] = lambda: str(seqno)
217 217 if total is not None and seqno is not None:
218 218 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
219 219 if pathname is not None:
220 220 expander['s'] = lambda: os.path.basename(pathname)
221 221 expander['d'] = lambda: os.path.dirname(pathname) or '.'
222 222 expander['p'] = lambda: pathname
223 223
224 224 newname = []
225 225 patlen = len(pat)
226 226 i = 0
227 227 while i < patlen:
228 228 c = pat[i]
229 229 if c == '%':
230 230 i += 1
231 231 c = pat[i]
232 232 c = expander[c]()
233 233 newname.append(c)
234 234 i += 1
235 235 return ''.join(newname)
236 236 except KeyError, inst:
237 237 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
238 238 inst.args[0])
239 239
240 240 def make_file(repo, pat, node=None,
241 241 total=None, seqno=None, revwidth=None, mode='wb', pathname=None):
242 242
243 243 writable = 'w' in mode or 'a' in mode
244 244
245 245 if not pat or pat == '-':
246 246 return writable and sys.stdout or sys.stdin
247 247 if hasattr(pat, 'write') and writable:
248 248 return pat
249 249 if hasattr(pat, 'read') and 'r' in mode:
250 250 return pat
251 251 return open(make_filename(repo, pat, node, total, seqno, revwidth,
252 252 pathname),
253 253 mode)
254 254
255 255 def expandpats(pats):
256 256 if not util.expandglobs:
257 257 return list(pats)
258 258 ret = []
259 259 for p in pats:
260 260 kind, name = _match._patsplit(p, None)
261 261 if kind is None:
262 262 try:
263 263 globbed = glob.glob(name)
264 264 except re.error:
265 265 globbed = [name]
266 266 if globbed:
267 267 ret.extend(globbed)
268 268 continue
269 269 ret.append(p)
270 270 return ret
271 271
272 272 def match(repo, pats=[], opts={}, globbed=False, default='relpath'):
273 273 if not globbed and default == 'relpath':
274 274 pats = expandpats(pats or [])
275 275 m = _match.match(repo.root, repo.getcwd(), pats,
276 276 opts.get('include'), opts.get('exclude'), default)
277 277 def badfn(f, msg):
278 278 repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
279 279 m.bad = badfn
280 280 return m
281 281
282 282 def matchall(repo):
283 283 return _match.always(repo.root, repo.getcwd())
284 284
285 285 def matchfiles(repo, files):
286 286 return _match.exact(repo.root, repo.getcwd(), files)
287 287
288 288 def findrenames(repo, added, removed, threshold):
289 289 '''find renamed files -- yields (before, after, score) tuples'''
290 290 copies = {}
291 291 ctx = repo['.']
292 292 for r in removed:
293 293 if r not in ctx:
294 294 continue
295 295 fctx = ctx.filectx(r)
296 296
297 297 def score(text):
298 298 if not len(text):
299 299 return 0.0
300 300 if not fctx.cmp(text):
301 301 return 1.0
302 302 if threshold == 1.0:
303 303 return 0.0
304 304 orig = fctx.data()
305 305 # bdiff.blocks() returns blocks of matching lines
306 306 # count the number of bytes in each
307 307 equal = 0
308 308 alines = mdiff.splitnewlines(text)
309 309 matches = bdiff.blocks(text, orig)
310 310 for x1, x2, y1, y2 in matches:
311 311 for line in alines[x1:x2]:
312 312 equal += len(line)
313 313
314 314 lengths = len(text) + len(orig)
315 315 return equal * 2.0 / lengths
316 316
317 317 for a in added:
318 318 bestscore = copies.get(a, (None, threshold))[1]
319 319 myscore = score(repo.wread(a))
320 320 if myscore >= bestscore:
321 321 copies[a] = (r, myscore)
322 322
323 323 for dest, v in copies.iteritems():
324 324 source, score = v
325 325 yield source, dest, score
326 326
327 327 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
328 328 if dry_run is None:
329 329 dry_run = opts.get('dry_run')
330 330 if similarity is None:
331 331 similarity = float(opts.get('similarity') or 0)
332 332 # we'd use status here, except handling of symlinks and ignore is tricky
333 333 added, unknown, deleted, removed = [], [], [], []
334 334 audit_path = util.path_auditor(repo.root)
335 335 m = match(repo, pats, opts)
336 336 for abs in repo.walk(m):
337 337 target = repo.wjoin(abs)
338 338 good = True
339 339 try:
340 340 audit_path(abs)
341 341 except:
342 342 good = False
343 343 rel = m.rel(abs)
344 344 exact = m.exact(abs)
345 345 if good and abs not in repo.dirstate:
346 346 unknown.append(abs)
347 347 if repo.ui.verbose or not exact:
348 348 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
349 349 elif repo.dirstate[abs] != 'r' and (not good or not util.lexists(target)
350 350 or (os.path.isdir(target) and not os.path.islink(target))):
351 351 deleted.append(abs)
352 352 if repo.ui.verbose or not exact:
353 353 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
354 354 # for finding renames
355 355 elif repo.dirstate[abs] == 'r':
356 356 removed.append(abs)
357 357 elif repo.dirstate[abs] == 'a':
358 358 added.append(abs)
359 359 if not dry_run:
360 360 repo.remove(deleted)
361 361 repo.add(unknown)
362 362 if similarity > 0:
363 363 for old, new, score in findrenames(repo, added + unknown,
364 364 removed + deleted, similarity):
365 365 if repo.ui.verbose or not m.exact(old) or not m.exact(new):
366 366 repo.ui.status(_('recording removal of %s as rename to %s '
367 367 '(%d%% similar)\n') %
368 368 (m.rel(old), m.rel(new), score * 100))
369 369 if not dry_run:
370 370 repo.copy(old, new)
371 371
372 372 def copy(ui, repo, pats, opts, rename=False):
373 373 # called with the repo lock held
374 374 #
375 375 # hgsep => pathname that uses "/" to separate directories
376 376 # ossep => pathname that uses os.sep to separate directories
377 377 cwd = repo.getcwd()
378 378 targets = {}
379 379 after = opts.get("after")
380 380 dryrun = opts.get("dry_run")
381 381
382 382 def walkpat(pat):
383 383 srcs = []
384 384 m = match(repo, [pat], opts, globbed=True)
385 385 for abs in repo.walk(m):
386 386 state = repo.dirstate[abs]
387 387 rel = m.rel(abs)
388 388 exact = m.exact(abs)
389 389 if state in '?r':
390 390 if exact and state == '?':
391 391 ui.warn(_('%s: not copying - file is not managed\n') % rel)
392 392 if exact and state == 'r':
393 393 ui.warn(_('%s: not copying - file has been marked for'
394 394 ' remove\n') % rel)
395 395 continue
396 396 # abs: hgsep
397 397 # rel: ossep
398 398 srcs.append((abs, rel, exact))
399 399 return srcs
400 400
401 401 # abssrc: hgsep
402 402 # relsrc: ossep
403 403 # otarget: ossep
404 404 def copyfile(abssrc, relsrc, otarget, exact):
405 405 abstarget = util.canonpath(repo.root, cwd, otarget)
406 406 reltarget = repo.pathto(abstarget, cwd)
407 407 target = repo.wjoin(abstarget)
408 408 src = repo.wjoin(abssrc)
409 409 state = repo.dirstate[abstarget]
410 410
411 411 # check for collisions
412 412 prevsrc = targets.get(abstarget)
413 413 if prevsrc is not None:
414 414 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
415 415 (reltarget, repo.pathto(abssrc, cwd),
416 416 repo.pathto(prevsrc, cwd)))
417 417 return
418 418
419 419 # check for overwrites
420 420 exists = os.path.exists(target)
421 421 if not after and exists or after and state in 'mn':
422 422 if not opts['force']:
423 423 ui.warn(_('%s: not overwriting - file exists\n') %
424 424 reltarget)
425 425 return
426 426
427 427 if after:
428 428 if not exists:
429 429 return
430 430 elif not dryrun:
431 431 try:
432 432 if exists:
433 433 os.unlink(target)
434 434 targetdir = os.path.dirname(target) or '.'
435 435 if not os.path.isdir(targetdir):
436 436 os.makedirs(targetdir)
437 437 util.copyfile(src, target)
438 438 except IOError, inst:
439 439 if inst.errno == errno.ENOENT:
440 440 ui.warn(_('%s: deleted in working copy\n') % relsrc)
441 441 else:
442 442 ui.warn(_('%s: cannot copy - %s\n') %
443 443 (relsrc, inst.strerror))
444 444 return True # report a failure
445 445
446 446 if ui.verbose or not exact:
447 447 if rename:
448 448 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
449 449 else:
450 450 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
451 451
452 452 targets[abstarget] = abssrc
453 453
454 454 # fix up dirstate
455 455 origsrc = repo.dirstate.copied(abssrc) or abssrc
456 456 if abstarget == origsrc: # copying back a copy?
457 457 if state not in 'mn' and not dryrun:
458 458 repo.dirstate.normallookup(abstarget)
459 459 else:
460 460 if repo.dirstate[origsrc] == 'a' and origsrc == abssrc:
461 461 if not ui.quiet:
462 462 ui.warn(_("%s has not been committed yet, so no copy "
463 463 "data will be stored for %s.\n")
464 464 % (repo.pathto(origsrc, cwd), reltarget))
465 465 if repo.dirstate[abstarget] in '?r' and not dryrun:
466 466 repo.add([abstarget])
467 467 elif not dryrun:
468 468 repo.copy(origsrc, abstarget)
469 469
470 470 if rename and not dryrun:
471 471 repo.remove([abssrc], not after)
472 472
473 473 # pat: ossep
474 474 # dest ossep
475 475 # srcs: list of (hgsep, hgsep, ossep, bool)
476 476 # return: function that takes hgsep and returns ossep
477 477 def targetpathfn(pat, dest, srcs):
478 478 if os.path.isdir(pat):
479 479 abspfx = util.canonpath(repo.root, cwd, pat)
480 480 abspfx = util.localpath(abspfx)
481 481 if destdirexists:
482 482 striplen = len(os.path.split(abspfx)[0])
483 483 else:
484 484 striplen = len(abspfx)
485 485 if striplen:
486 486 striplen += len(os.sep)
487 487 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
488 488 elif destdirexists:
489 489 res = lambda p: os.path.join(dest,
490 490 os.path.basename(util.localpath(p)))
491 491 else:
492 492 res = lambda p: dest
493 493 return res
494 494
495 495 # pat: ossep
496 496 # dest ossep
497 497 # srcs: list of (hgsep, hgsep, ossep, bool)
498 498 # return: function that takes hgsep and returns ossep
499 499 def targetpathafterfn(pat, dest, srcs):
500 500 if _match.patkind(pat):
501 501 # a mercurial pattern
502 502 res = lambda p: os.path.join(dest,
503 503 os.path.basename(util.localpath(p)))
504 504 else:
505 505 abspfx = util.canonpath(repo.root, cwd, pat)
506 506 if len(abspfx) < len(srcs[0][0]):
507 507 # A directory. Either the target path contains the last
508 508 # component of the source path or it does not.
509 509 def evalpath(striplen):
510 510 score = 0
511 511 for s in srcs:
512 512 t = os.path.join(dest, util.localpath(s[0])[striplen:])
513 513 if os.path.exists(t):
514 514 score += 1
515 515 return score
516 516
517 517 abspfx = util.localpath(abspfx)
518 518 striplen = len(abspfx)
519 519 if striplen:
520 520 striplen += len(os.sep)
521 521 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
522 522 score = evalpath(striplen)
523 523 striplen1 = len(os.path.split(abspfx)[0])
524 524 if striplen1:
525 525 striplen1 += len(os.sep)
526 526 if evalpath(striplen1) > score:
527 527 striplen = striplen1
528 528 res = lambda p: os.path.join(dest,
529 529 util.localpath(p)[striplen:])
530 530 else:
531 531 # a file
532 532 if destdirexists:
533 533 res = lambda p: os.path.join(dest,
534 534 os.path.basename(util.localpath(p)))
535 535 else:
536 536 res = lambda p: dest
537 537 return res
538 538
539 539
540 540 pats = expandpats(pats)
541 541 if not pats:
542 542 raise util.Abort(_('no source or destination specified'))
543 543 if len(pats) == 1:
544 544 raise util.Abort(_('no destination specified'))
545 545 dest = pats.pop()
546 546 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
547 547 if not destdirexists:
548 548 if len(pats) > 1 or _match.patkind(pats[0]):
549 549 raise util.Abort(_('with multiple sources, destination must be an '
550 550 'existing directory'))
551 551 if util.endswithsep(dest):
552 552 raise util.Abort(_('destination %s is not a directory') % dest)
553 553
554 554 tfn = targetpathfn
555 555 if after:
556 556 tfn = targetpathafterfn
557 557 copylist = []
558 558 for pat in pats:
559 559 srcs = walkpat(pat)
560 560 if not srcs:
561 561 continue
562 562 copylist.append((tfn(pat, dest, srcs), srcs))
563 563 if not copylist:
564 564 raise util.Abort(_('no files to copy'))
565 565
566 566 errors = 0
567 567 for targetpath, srcs in copylist:
568 568 for abssrc, relsrc, exact in srcs:
569 569 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
570 570 errors += 1
571 571
572 572 if errors:
573 573 ui.warn(_('(consider using --after)\n'))
574 574
575 575 return errors
576 576
577 577 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
578 578 runargs=None, appendpid=False):
579 579 '''Run a command as a service.'''
580 580
581 581 if opts['daemon'] and not opts['daemon_pipefds']:
582 582 # Signal child process startup with file removal
583 583 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
584 584 os.close(lockfd)
585 585 try:
586 586 if not runargs:
587 587 runargs = util.hgcmd() + sys.argv[1:]
588 588 runargs.append('--daemon-pipefds=%s' % lockpath)
589 589 # Don't pass --cwd to the child process, because we've already
590 590 # changed directory.
591 591 for i in xrange(1, len(runargs)):
592 592 if runargs[i].startswith('--cwd='):
593 593 del runargs[i]
594 594 break
595 595 elif runargs[i].startswith('--cwd'):
596 596 del runargs[i:i + 2]
597 597 break
598 598 def condfn():
599 599 return not os.path.exists(lockpath)
600 600 pid = util.rundetached(runargs, condfn)
601 601 if pid < 0:
602 602 raise util.Abort(_('child process failed to start'))
603 603 finally:
604 604 try:
605 605 os.unlink(lockpath)
606 606 except OSError, e:
607 607 if e.errno != errno.ENOENT:
608 608 raise
609 609 if parentfn:
610 610 return parentfn(pid)
611 611 else:
612 612 return
613 613
614 614 if initfn:
615 615 initfn()
616 616
617 617 if opts['pid_file']:
618 618 mode = appendpid and 'a' or 'w'
619 619 fp = open(opts['pid_file'], mode)
620 620 fp.write(str(os.getpid()) + '\n')
621 621 fp.close()
622 622
623 623 if opts['daemon_pipefds']:
624 624 lockpath = opts['daemon_pipefds']
625 625 try:
626 626 os.setsid()
627 627 except AttributeError:
628 628 pass
629 629 os.unlink(lockpath)
630 630 util.hidewindow()
631 631 sys.stdout.flush()
632 632 sys.stderr.flush()
633 633
634 634 nullfd = os.open(util.nulldev, os.O_RDWR)
635 635 logfilefd = nullfd
636 636 if logfile:
637 637 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
638 638 os.dup2(nullfd, 0)
639 639 os.dup2(logfilefd, 1)
640 640 os.dup2(logfilefd, 2)
641 641 if nullfd not in (0, 1, 2):
642 642 os.close(nullfd)
643 643 if logfile and logfilefd not in (0, 1, 2):
644 644 os.close(logfilefd)
645 645
646 646 if runfn:
647 647 return runfn()
648 648
649 649 class changeset_printer(object):
650 650 '''show changeset information when templating not requested.'''
651 651
652 652 def __init__(self, ui, repo, patch, diffopts, buffered):
653 653 self.ui = ui
654 654 self.repo = repo
655 655 self.buffered = buffered
656 656 self.patch = patch
657 657 self.diffopts = diffopts
658 658 self.header = {}
659 659 self.hunk = {}
660 660 self.lastheader = None
661 661 self.footer = None
662 662
663 663 def flush(self, rev):
664 664 if rev in self.header:
665 665 h = self.header[rev]
666 666 if h != self.lastheader:
667 667 self.lastheader = h
668 668 self.ui.write(h)
669 669 del self.header[rev]
670 670 if rev in self.hunk:
671 671 self.ui.write(self.hunk[rev])
672 672 del self.hunk[rev]
673 673 return 1
674 674 return 0
675 675
676 676 def close(self):
677 677 if self.footer:
678 678 self.ui.write(self.footer)
679 679
680 680 def show(self, ctx, copies=None, **props):
681 681 if self.buffered:
682 682 self.ui.pushbuffer()
683 683 self._show(ctx, copies, props)
684 684 self.hunk[ctx.rev()] = self.ui.popbuffer()
685 685 else:
686 686 self._show(ctx, copies, props)
687 687
688 688 def _show(self, ctx, copies, props):
689 689 '''show a single changeset or file revision'''
690 690 changenode = ctx.node()
691 691 rev = ctx.rev()
692 692
693 693 if self.ui.quiet:
694 694 self.ui.write("%d:%s\n" % (rev, short(changenode)))
695 695 return
696 696
697 697 log = self.repo.changelog
698 698 date = util.datestr(ctx.date())
699 699
700 700 hexfunc = self.ui.debugflag and hex or short
701 701
702 702 parents = [(p, hexfunc(log.node(p)))
703 703 for p in self._meaningful_parentrevs(log, rev)]
704 704
705 705 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)))
706 706
707 707 branch = ctx.branch()
708 708 # don't show the default branch name
709 709 if branch != 'default':
710 710 branch = encoding.tolocal(branch)
711 711 self.ui.write(_("branch: %s\n") % branch)
712 712 for tag in self.repo.nodetags(changenode):
713 713 self.ui.write(_("tag: %s\n") % tag)
714 714 for parent in parents:
715 715 self.ui.write(_("parent: %d:%s\n") % parent)
716 716
717 717 if self.ui.debugflag:
718 718 mnode = ctx.manifestnode()
719 719 self.ui.write(_("manifest: %d:%s\n") %
720 720 (self.repo.manifest.rev(mnode), hex(mnode)))
721 721 self.ui.write(_("user: %s\n") % ctx.user())
722 722 self.ui.write(_("date: %s\n") % date)
723 723
724 724 if self.ui.debugflag:
725 725 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
726 726 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
727 727 files):
728 728 if value:
729 729 self.ui.write("%-12s %s\n" % (key, " ".join(value)))
730 730 elif ctx.files() and self.ui.verbose:
731 731 self.ui.write(_("files: %s\n") % " ".join(ctx.files()))
732 732 if copies and self.ui.verbose:
733 733 copies = ['%s (%s)' % c for c in copies]
734 734 self.ui.write(_("copies: %s\n") % ' '.join(copies))
735 735
736 736 extra = ctx.extra()
737 737 if extra and self.ui.debugflag:
738 738 for key, value in sorted(extra.items()):
739 739 self.ui.write(_("extra: %s=%s\n")
740 740 % (key, value.encode('string_escape')))
741 741
742 742 description = ctx.description().strip()
743 743 if description:
744 744 if self.ui.verbose:
745 745 self.ui.write(_("description:\n"))
746 746 self.ui.write(description)
747 747 self.ui.write("\n\n")
748 748 else:
749 749 self.ui.write(_("summary: %s\n") %
750 750 description.splitlines()[0])
751 751 self.ui.write("\n")
752 752
753 753 self.showpatch(changenode)
754 754
755 755 def showpatch(self, node):
756 756 if self.patch:
757 757 prev = self.repo.changelog.parents(node)[0]
758 758 chunks = patch.diff(self.repo, prev, node, match=self.patch,
759 759 opts=patch.diffopts(self.ui, self.diffopts))
760 760 for chunk in chunks:
761 761 self.ui.write(chunk)
762 762 self.ui.write("\n")
763 763
764 764 def _meaningful_parentrevs(self, log, rev):
765 765 """Return list of meaningful (or all if debug) parentrevs for rev.
766 766
767 767 For merges (two non-nullrev revisions) both parents are meaningful.
768 768 Otherwise the first parent revision is considered meaningful if it
769 769 is not the preceding revision.
770 770 """
771 771 parents = log.parentrevs(rev)
772 772 if not self.ui.debugflag and parents[1] == nullrev:
773 773 if parents[0] >= rev - 1:
774 774 parents = []
775 775 else:
776 776 parents = [parents[0]]
777 777 return parents
778 778
779 779
780 780 class changeset_templater(changeset_printer):
781 781 '''format changeset information.'''
782 782
783 783 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
784 784 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
785 785 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
786 786 defaulttempl = {
787 787 'parent': '{rev}:{node|formatnode} ',
788 788 'manifest': '{rev}:{node|formatnode}',
789 789 'file_copy': '{name} ({source})',
790 790 'extra': '{key}={value|stringescape}'
791 791 }
792 792 # filecopy is preserved for compatibility reasons
793 793 defaulttempl['filecopy'] = defaulttempl['file_copy']
794 794 self.t = templater.templater(mapfile, {'formatnode': formatnode},
795 795 cache=defaulttempl)
796 796 self.cache = {}
797 797
798 798 def use_template(self, t):
799 799 '''set template string to use'''
800 800 self.t.cache['changeset'] = t
801 801
802 802 def _meaningful_parentrevs(self, ctx):
803 803 """Return list of meaningful (or all if debug) parentrevs for rev.
804 804 """
805 805 parents = ctx.parents()
806 806 if len(parents) > 1:
807 807 return parents
808 808 if self.ui.debugflag:
809 809 return [parents[0], self.repo['null']]
810 810 if parents[0].rev() >= ctx.rev() - 1:
811 811 return []
812 812 return parents
813 813
814 814 def _show(self, ctx, copies, props):
815 815 '''show a single changeset or file revision'''
816 816
817 817 showlist = templatekw.showlist
818 818
819 819 # showparents() behaviour depends on ui trace level which
820 820 # causes unexpected behaviours at templating level and makes
821 821 # it harder to extract it in a standalone function. Its
822 822 # behaviour cannot be changed so leave it here for now.
823 823 def showparents(**args):
824 824 ctx = args['ctx']
825 825 parents = [[('rev', p.rev()), ('node', p.hex())]
826 826 for p in self._meaningful_parentrevs(ctx)]
827 827 return showlist('parent', parents, **args)
828 828
829 829 props = props.copy()
830 830 props.update(templatekw.keywords)
831 831 props['parents'] = showparents
832 832 props['templ'] = self.t
833 833 props['ctx'] = ctx
834 834 props['repo'] = self.repo
835 835 props['revcache'] = {'copies': copies}
836 836 props['cache'] = self.cache
837 837
838 838 # find correct templates for current mode
839 839
840 840 tmplmodes = [
841 841 (True, None),
842 842 (self.ui.verbose, 'verbose'),
843 843 (self.ui.quiet, 'quiet'),
844 844 (self.ui.debugflag, 'debug'),
845 845 ]
846 846
847 847 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
848 848 for mode, postfix in tmplmodes:
849 849 for type in types:
850 850 cur = postfix and ('%s_%s' % (type, postfix)) or type
851 851 if mode and cur in self.t:
852 852 types[type] = cur
853 853
854 854 try:
855 855
856 856 # write header
857 857 if types['header']:
858 858 h = templater.stringify(self.t(types['header'], **props))
859 859 if self.buffered:
860 860 self.header[ctx.rev()] = h
861 861 else:
862 862 self.ui.write(h)
863 863
864 864 # write changeset metadata, then patch if requested
865 865 key = types['changeset']
866 866 self.ui.write(templater.stringify(self.t(key, **props)))
867 867 self.showpatch(ctx.node())
868 868
869 869 if types['footer']:
870 870 if not self.footer:
871 871 self.footer = templater.stringify(self.t(types['footer'],
872 872 **props))
873 873
874 874 except KeyError, inst:
875 875 msg = _("%s: no key named '%s'")
876 876 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
877 877 except SyntaxError, inst:
878 878 raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0]))
879 879
880 880 def show_changeset(ui, repo, opts, buffered=False, matchfn=False):
881 881 """show one changeset using template or regular display.
882 882
883 883 Display format will be the first non-empty hit of:
884 884 1. option 'template'
885 885 2. option 'style'
886 886 3. [ui] setting 'logtemplate'
887 887 4. [ui] setting 'style'
888 888 If all of these values are either the unset or the empty string,
889 889 regular display via changeset_printer() is done.
890 890 """
891 891 # options
892 892 patch = False
893 893 if opts.get('patch'):
894 894 patch = matchfn or matchall(repo)
895 895
896 896 tmpl = opts.get('template')
897 897 style = None
898 898 if tmpl:
899 899 tmpl = templater.parsestring(tmpl, quoted=False)
900 900 else:
901 901 style = opts.get('style')
902 902
903 903 # ui settings
904 904 if not (tmpl or style):
905 905 tmpl = ui.config('ui', 'logtemplate')
906 906 if tmpl:
907 907 tmpl = templater.parsestring(tmpl)
908 908 else:
909 909 style = util.expandpath(ui.config('ui', 'style', ''))
910 910
911 911 if not (tmpl or style):
912 912 return changeset_printer(ui, repo, patch, opts, buffered)
913 913
914 914 mapfile = None
915 915 if style and not tmpl:
916 916 mapfile = style
917 917 if not os.path.split(mapfile)[0]:
918 918 mapname = (templater.templatepath('map-cmdline.' + mapfile)
919 919 or templater.templatepath(mapfile))
920 920 if mapname:
921 921 mapfile = mapname
922 922
923 923 try:
924 924 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
925 925 except SyntaxError, inst:
926 926 raise util.Abort(inst.args[0])
927 927 if tmpl:
928 928 t.use_template(tmpl)
929 929 return t
930 930
931 931 def finddate(ui, repo, date):
932 932 """Find the tipmost changeset that matches the given date spec"""
933 933
934 934 df = util.matchdate(date)
935 935 m = matchall(repo)
936 936 results = {}
937 937
938 938 def prep(ctx, fns):
939 939 d = ctx.date()
940 940 if df(d[0]):
941 941 results[ctx.rev()] = d
942 942
943 943 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
944 944 rev = ctx.rev()
945 945 if rev in results:
946 946 ui.status(_("Found revision %s from %s\n") %
947 947 (rev, util.datestr(results[rev])))
948 948 return str(rev)
949 949
950 950 raise util.Abort(_("revision matching date not found"))
951 951
952 952 def walkchangerevs(repo, match, opts, prepare):
953 953 '''Iterate over files and the revs in which they changed.
954 954
955 955 Callers most commonly need to iterate backwards over the history
956 956 in which they are interested. Doing so has awful (quadratic-looking)
957 957 performance, so we use iterators in a "windowed" way.
958 958
959 959 We walk a window of revisions in the desired order. Within the
960 960 window, we first walk forwards to gather data, then in the desired
961 961 order (usually backwards) to display it.
962 962
963 963 This function returns an iterator yielding contexts. Before
964 964 yielding each context, the iterator will first call the prepare
965 965 function on each context in the window in forward order.'''
966 966
967 967 def increasing_windows(start, end, windowsize=8, sizelimit=512):
968 968 if start < end:
969 969 while start < end:
970 970 yield start, min(windowsize, end - start)
971 971 start += windowsize
972 972 if windowsize < sizelimit:
973 973 windowsize *= 2
974 974 else:
975 975 while start > end:
976 976 yield start, min(windowsize, start - end - 1)
977 977 start -= windowsize
978 978 if windowsize < sizelimit:
979 979 windowsize *= 2
980 980
981 981 follow = opts.get('follow') or opts.get('follow_first')
982 982
983 983 if not len(repo):
984 984 return []
985 985
986 986 if follow:
987 987 defrange = '%s:0' % repo['.'].rev()
988 988 else:
989 989 defrange = '-1:0'
990 990 revs = revrange(repo, opts['rev'] or [defrange])
991 991 wanted = set()
992 992 slowpath = match.anypats() or (match.files() and opts.get('removed'))
993 993 fncache = {}
994 994 change = util.cachefunc(repo.changectx)
995 995
996 996 if not slowpath and not match.files():
997 997 # No files, no patterns. Display all revs.
998 998 wanted = set(revs)
999 999 copies = []
1000 1000
1001 1001 if not slowpath:
1002 1002 # Only files, no patterns. Check the history of each file.
1003 1003 def filerevgen(filelog, node):
1004 1004 cl_count = len(repo)
1005 1005 if node is None:
1006 1006 last = len(filelog) - 1
1007 1007 else:
1008 1008 last = filelog.rev(node)
1009 1009 for i, window in increasing_windows(last, nullrev):
1010 1010 revs = []
1011 1011 for j in xrange(i - window, i + 1):
1012 1012 n = filelog.node(j)
1013 1013 revs.append((filelog.linkrev(j),
1014 1014 follow and filelog.renamed(n)))
1015 1015 for rev in reversed(revs):
1016 1016 # only yield rev for which we have the changelog, it can
1017 1017 # happen while doing "hg log" during a pull or commit
1018 1018 if rev[0] < cl_count:
1019 1019 yield rev
1020 1020 def iterfiles():
1021 1021 for filename in match.files():
1022 1022 yield filename, None
1023 1023 for filename_node in copies:
1024 1024 yield filename_node
1025 1025 minrev, maxrev = min(revs), max(revs)
1026 1026 for file_, node in iterfiles():
1027 1027 filelog = repo.file(file_)
1028 1028 if not len(filelog):
1029 1029 if node is None:
1030 1030 # A zero count may be a directory or deleted file, so
1031 1031 # try to find matching entries on the slow path.
1032 1032 if follow:
1033 1033 raise util.Abort(
1034 1034 _('cannot follow nonexistent file: "%s"') % file_)
1035 1035 slowpath = True
1036 1036 break
1037 1037 else:
1038 1038 continue
1039 1039 for rev, copied in filerevgen(filelog, node):
1040 1040 if rev <= maxrev:
1041 1041 if rev < minrev:
1042 1042 break
1043 1043 fncache.setdefault(rev, [])
1044 1044 fncache[rev].append(file_)
1045 1045 wanted.add(rev)
1046 1046 if follow and copied:
1047 1047 copies.append(copied)
1048 1048 if slowpath:
1049 1049 if follow:
1050 1050 raise util.Abort(_('can only follow copies/renames for explicit '
1051 1051 'filenames'))
1052 1052
1053 1053 # The slow path checks files modified in every changeset.
1054 1054 def changerevgen():
1055 1055 for i, window in increasing_windows(len(repo) - 1, nullrev):
1056 1056 for j in xrange(i - window, i + 1):
1057 1057 yield change(j)
1058 1058
1059 1059 for ctx in changerevgen():
1060 1060 matches = filter(match, ctx.files())
1061 1061 if matches:
1062 1062 fncache[ctx.rev()] = matches
1063 1063 wanted.add(ctx.rev())
1064 1064
1065 1065 class followfilter(object):
1066 1066 def __init__(self, onlyfirst=False):
1067 1067 self.startrev = nullrev
1068 1068 self.roots = set()
1069 1069 self.onlyfirst = onlyfirst
1070 1070
1071 1071 def match(self, rev):
1072 1072 def realparents(rev):
1073 1073 if self.onlyfirst:
1074 1074 return repo.changelog.parentrevs(rev)[0:1]
1075 1075 else:
1076 1076 return filter(lambda x: x != nullrev,
1077 1077 repo.changelog.parentrevs(rev))
1078 1078
1079 1079 if self.startrev == nullrev:
1080 1080 self.startrev = rev
1081 1081 return True
1082 1082
1083 1083 if rev > self.startrev:
1084 1084 # forward: all descendants
1085 1085 if not self.roots:
1086 1086 self.roots.add(self.startrev)
1087 1087 for parent in realparents(rev):
1088 1088 if parent in self.roots:
1089 1089 self.roots.add(rev)
1090 1090 return True
1091 1091 else:
1092 1092 # backwards: all parents
1093 1093 if not self.roots:
1094 1094 self.roots.update(realparents(self.startrev))
1095 1095 if rev in self.roots:
1096 1096 self.roots.remove(rev)
1097 1097 self.roots.update(realparents(rev))
1098 1098 return True
1099 1099
1100 1100 return False
1101 1101
1102 1102 # it might be worthwhile to do this in the iterator if the rev range
1103 1103 # is descending and the prune args are all within that range
1104 1104 for rev in opts.get('prune', ()):
1105 1105 rev = repo.changelog.rev(repo.lookup(rev))
1106 1106 ff = followfilter()
1107 1107 stop = min(revs[0], revs[-1])
1108 1108 for x in xrange(rev, stop - 1, -1):
1109 1109 if ff.match(x):
1110 1110 wanted.discard(x)
1111 1111
1112 1112 def iterate():
1113 1113 if follow and not match.files():
1114 1114 ff = followfilter(onlyfirst=opts.get('follow_first'))
1115 1115 def want(rev):
1116 1116 return ff.match(rev) and rev in wanted
1117 1117 else:
1118 1118 def want(rev):
1119 1119 return rev in wanted
1120 1120
1121 1121 for i, window in increasing_windows(0, len(revs)):
1122 1122 change = util.cachefunc(repo.changectx)
1123 1123 nrevs = [rev for rev in revs[i:i + window] if want(rev)]
1124 1124 for rev in sorted(nrevs):
1125 1125 fns = fncache.get(rev)
1126 1126 ctx = change(rev)
1127 1127 if not fns:
1128 1128 def fns_generator():
1129 1129 for f in ctx.files():
1130 1130 if match(f):
1131 1131 yield f
1132 1132 fns = fns_generator()
1133 1133 prepare(ctx, fns)
1134 1134 for rev in nrevs:
1135 1135 yield change(rev)
1136 1136 return iterate()
1137 1137
1138 1138 def commit(ui, repo, commitfunc, pats, opts):
1139 1139 '''commit the specified files or all outstanding changes'''
1140 1140 date = opts.get('date')
1141 1141 if date:
1142 1142 opts['date'] = util.parsedate(date)
1143 1143 message = logmessage(opts)
1144 1144
1145 1145 # extract addremove carefully -- this function can be called from a command
1146 1146 # that doesn't support addremove
1147 1147 if opts.get('addremove'):
1148 1148 addremove(repo, pats, opts)
1149 1149
1150 1150 return commitfunc(ui, repo, message, match(repo, pats, opts), opts)
1151 1151
1152 1152 def commiteditor(repo, ctx, subs):
1153 1153 if ctx.description():
1154 1154 return ctx.description()
1155 1155 return commitforceeditor(repo, ctx, subs)
1156 1156
1157 1157 def commitforceeditor(repo, ctx, subs):
1158 1158 edittext = []
1159 1159 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1160 1160 if ctx.description():
1161 1161 edittext.append(ctx.description())
1162 1162 edittext.append("")
1163 1163 edittext.append("") # Empty line between message and comments.
1164 1164 edittext.append(_("HG: Enter commit message."
1165 1165 " Lines beginning with 'HG:' are removed."))
1166 1166 edittext.append(_("HG: Leave message empty to abort commit."))
1167 1167 edittext.append("HG: --")
1168 1168 edittext.append(_("HG: user: %s") % ctx.user())
1169 1169 if ctx.p2():
1170 1170 edittext.append(_("HG: branch merge"))
1171 1171 if ctx.branch():
1172 1172 edittext.append(_("HG: branch '%s'")
1173 1173 % encoding.tolocal(ctx.branch()))
1174 1174 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1175 1175 edittext.extend([_("HG: added %s") % f for f in added])
1176 1176 edittext.extend([_("HG: changed %s") % f for f in modified])
1177 1177 edittext.extend([_("HG: removed %s") % f for f in removed])
1178 1178 if not added and not modified and not removed:
1179 1179 edittext.append(_("HG: no files changed"))
1180 1180 edittext.append("")
1181 1181 # run editor in the repository root
1182 1182 olddir = os.getcwd()
1183 1183 os.chdir(repo.root)
1184 1184 text = repo.ui.edit("\n".join(edittext), ctx.user())
1185 1185 text = re.sub("(?m)^HG:.*\n", "", text)
1186 1186 os.chdir(olddir)
1187 1187
1188 1188 if not text.strip():
1189 1189 raise util.Abort(_("empty commit message"))
1190 1190
1191 1191 return text
@@ -1,253 +1,252
1 1 # copies.py - copy detection for Mercurial
2 2 #
3 3 # Copyright 2008 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 from i18n import _
9 8 import util
10 9 import heapq
11 10
12 11 def _nonoverlap(d1, d2, d3):
13 12 "Return list of elements in d1 not in d2 or d3"
14 13 return sorted([d for d in d1 if d not in d3 and d not in d2])
15 14
16 15 def _dirname(f):
17 16 s = f.rfind("/")
18 17 if s == -1:
19 18 return ""
20 19 return f[:s]
21 20
22 21 def _dirs(files):
23 22 d = set()
24 23 for f in files:
25 24 f = _dirname(f)
26 25 while f not in d:
27 26 d.add(f)
28 27 f = _dirname(f)
29 28 return d
30 29
31 30 def _findlimit(repo, a, b):
32 31 """Find the earliest revision that's an ancestor of a or b but not both,
33 32 None if no such revision exists.
34 33 """
35 34 # basic idea:
36 35 # - mark a and b with different sides
37 36 # - if a parent's children are all on the same side, the parent is
38 37 # on that side, otherwise it is on no side
39 38 # - walk the graph in topological order with the help of a heap;
40 39 # - add unseen parents to side map
41 40 # - clear side of any parent that has children on different sides
42 41 # - track number of interesting revs that might still be on a side
43 42 # - track the lowest interesting rev seen
44 43 # - quit when interesting revs is zero
45 44
46 45 cl = repo.changelog
47 46 working = len(cl) # pseudo rev for the working directory
48 47 if a is None:
49 48 a = working
50 49 if b is None:
51 50 b = working
52 51
53 52 side = {a: -1, b: 1}
54 53 visit = [-a, -b]
55 54 heapq.heapify(visit)
56 55 interesting = len(visit)
57 56 hascommonancestor = False
58 57 limit = working
59 58
60 59 while interesting:
61 60 r = -heapq.heappop(visit)
62 61 if r == working:
63 62 parents = [cl.rev(p) for p in repo.dirstate.parents()]
64 63 else:
65 64 parents = cl.parentrevs(r)
66 65 for p in parents:
67 66 if p < 0:
68 67 continue
69 68 if p not in side:
70 69 # first time we see p; add it to visit
71 70 side[p] = side[r]
72 71 if side[p]:
73 72 interesting += 1
74 73 heapq.heappush(visit, -p)
75 74 elif side[p] and side[p] != side[r]:
76 75 # p was interesting but now we know better
77 76 side[p] = 0
78 77 interesting -= 1
79 78 hascommonancestor = True
80 79 if side[r]:
81 80 limit = r # lowest rev visited
82 81 interesting -= 1
83 82
84 83 if not hascommonancestor:
85 84 return None
86 85 return limit
87 86
88 87 def copies(repo, c1, c2, ca, checkdirs=False):
89 88 """
90 89 Find moves and copies between context c1 and c2
91 90 """
92 91 # avoid silly behavior for update from empty dir
93 92 if not c1 or not c2 or c1 == c2:
94 93 return {}, {}
95 94
96 95 # avoid silly behavior for parent -> working dir
97 96 if c2.node() is None and c1.node() == repo.dirstate.parents()[0]:
98 97 return repo.dirstate.copies(), {}
99 98
100 99 limit = _findlimit(repo, c1.rev(), c2.rev())
101 100 if limit is None:
102 101 # no common ancestor, no copies
103 102 return {}, {}
104 103 m1 = c1.manifest()
105 104 m2 = c2.manifest()
106 105 ma = ca.manifest()
107 106
108 107 def makectx(f, n):
109 108 if len(n) != 20: # in a working context?
110 109 if c1.rev() is None:
111 110 return c1.filectx(f)
112 111 return c2.filectx(f)
113 112 return repo.filectx(f, fileid=n)
114 113
115 114 ctx = util.lrucachefunc(makectx)
116 115 copy = {}
117 116 fullcopy = {}
118 117 diverge = {}
119 118
120 119 def related(f1, f2, limit):
121 120 g1, g2 = f1.ancestors(), f2.ancestors()
122 121 try:
123 122 while 1:
124 123 f1r, f2r = f1.rev(), f2.rev()
125 124 if f1r > f2r:
126 125 f1 = g1.next()
127 126 elif f2r > f1r:
128 127 f2 = g2.next()
129 128 elif f1 == f2:
130 129 return f1 # a match
131 130 elif f1r == f2r or f1r < limit or f2r < limit:
132 131 return False # copy no longer relevant
133 132 except StopIteration:
134 133 return False
135 134
136 135 def checkcopies(f, m1, m2):
137 136 '''check possible copies of f from m1 to m2'''
138 137 of = None
139 138 seen = set([f])
140 139 for oc in ctx(f, m1[f]).ancestors():
141 140 ocr = oc.rev()
142 141 of = oc.path()
143 142 if of in seen:
144 143 # check limit late - grab last rename before
145 144 if ocr < limit:
146 145 break
147 146 continue
148 147 seen.add(of)
149 148
150 149 fullcopy[f] = of # remember for dir rename detection
151 150 if of not in m2:
152 151 continue # no match, keep looking
153 152 if m2[of] == ma.get(of):
154 153 break # no merge needed, quit early
155 154 c2 = ctx(of, m2[of])
156 155 cr = related(oc, c2, ca.rev())
157 156 if cr and (of == f or of == c2.path()): # non-divergent
158 157 copy[f] = of
159 158 of = None
160 159 break
161 160
162 161 if of in ma:
163 162 diverge.setdefault(of, []).append(f)
164 163
165 164 repo.ui.debug(" searching for copies back to rev %d\n" % limit)
166 165
167 166 u1 = _nonoverlap(m1, m2, ma)
168 167 u2 = _nonoverlap(m2, m1, ma)
169 168
170 169 if u1:
171 170 repo.ui.debug(" unmatched files in local:\n %s\n"
172 171 % "\n ".join(u1))
173 172 if u2:
174 173 repo.ui.debug(" unmatched files in other:\n %s\n"
175 174 % "\n ".join(u2))
176 175
177 176 for f in u1:
178 177 checkcopies(f, m1, m2)
179 178 for f in u2:
180 179 checkcopies(f, m2, m1)
181 180
182 181 diverge2 = set()
183 182 for of, fl in diverge.items():
184 183 if len(fl) == 1:
185 184 del diverge[of] # not actually divergent
186 185 else:
187 186 diverge2.update(fl) # reverse map for below
188 187
189 188 if fullcopy:
190 189 repo.ui.debug(" all copies found (* = to merge, ! = divergent):\n")
191 190 for f in fullcopy:
192 191 note = ""
193 192 if f in copy:
194 193 note += "*"
195 194 if f in diverge2:
196 195 note += "!"
197 196 repo.ui.debug(" %s -> %s %s\n" % (f, fullcopy[f], note))
198 197 del diverge2
199 198
200 199 if not fullcopy or not checkdirs:
201 200 return copy, diverge
202 201
203 202 repo.ui.debug(" checking for directory renames\n")
204 203
205 204 # generate a directory move map
206 205 d1, d2 = _dirs(m1), _dirs(m2)
207 206 invalid = set()
208 207 dirmove = {}
209 208
210 209 # examine each file copy for a potential directory move, which is
211 210 # when all the files in a directory are moved to a new directory
212 211 for dst, src in fullcopy.iteritems():
213 212 dsrc, ddst = _dirname(src), _dirname(dst)
214 213 if dsrc in invalid:
215 214 # already seen to be uninteresting
216 215 continue
217 216 elif dsrc in d1 and ddst in d1:
218 217 # directory wasn't entirely moved locally
219 218 invalid.add(dsrc)
220 219 elif dsrc in d2 and ddst in d2:
221 220 # directory wasn't entirely moved remotely
222 221 invalid.add(dsrc)
223 222 elif dsrc in dirmove and dirmove[dsrc] != ddst:
224 223 # files from the same directory moved to two different places
225 224 invalid.add(dsrc)
226 225 else:
227 226 # looks good so far
228 227 dirmove[dsrc + "/"] = ddst + "/"
229 228
230 229 for i in invalid:
231 230 if i in dirmove:
232 231 del dirmove[i]
233 232 del d1, d2, invalid
234 233
235 234 if not dirmove:
236 235 return copy, diverge
237 236
238 237 for d in dirmove:
239 238 repo.ui.debug(" dir %s -> %s\n" % (d, dirmove[d]))
240 239
241 240 # check unaccounted nonoverlapping files against directory moves
242 241 for f in u1 + u2:
243 242 if f not in fullcopy:
244 243 for d in dirmove:
245 244 if f.startswith(d):
246 245 # new file added in a directory that was moved, move it
247 246 df = dirmove[d] + f[len(d):]
248 247 if df not in copy:
249 248 copy[f] = df
250 249 repo.ui.debug(" file %s -> %s\n" % (f, copy[f]))
251 250 break
252 251
253 252 return copy, diverge
General Comments 0
You need to be logged in to leave comments. Login now