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