##// END OF EJS Templates
patch: turn patch.diff() into a generator...
Dirkjan Ochtman -
r7308:b6f5490e default
parent child Browse files
Show More
@@ -1,184 +1,182
1 1 # churn.py - create a graph of revisions count grouped by template
2 2 #
3 3 # Copyright 2006 Josef "Jeff" Sipek <jeffpc@josefsipek.net>
4 4 # Copyright 2008 Alexander Solovyov <piranha@piranha.org.ua>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8 '''command to show certain statistics about revision history'''
9 9
10 10 from mercurial.i18n import _
11 11 from mercurial import patch, cmdutil, util, templater
12 12 import os, sys
13 13 import time, datetime
14 14
15 15 def get_tty_width():
16 16 if 'COLUMNS' in os.environ:
17 17 try:
18 18 return int(os.environ['COLUMNS'])
19 19 except ValueError:
20 20 pass
21 21 try:
22 22 import termios, array, fcntl
23 23 for dev in (sys.stdout, sys.stdin):
24 24 try:
25 25 fd = dev.fileno()
26 26 if not os.isatty(fd):
27 27 continue
28 28 arri = fcntl.ioctl(fd, termios.TIOCGWINSZ, '\0' * 8)
29 29 return array.array('h', arri)[1]
30 30 except ValueError:
31 31 pass
32 32 except ImportError:
33 33 pass
34 34 return 80
35 35
36 36 def maketemplater(ui, repo, tmpl):
37 37 tmpl = templater.parsestring(tmpl, quoted=False)
38 38 try:
39 39 t = cmdutil.changeset_templater(ui, repo, False, None, False)
40 40 except SyntaxError, inst:
41 41 raise util.Abort(inst.args[0])
42 42 t.use_template(tmpl)
43 43 return t
44 44
45 45 def changedlines(ui, repo, ctx1, ctx2):
46 46 lines = 0
47 ui.pushbuffer()
48 patch.diff(repo, ctx1.node(), ctx2.node())
49 diff = ui.popbuffer()
47 diff = ''.join(patch.diff(repo, ctx1.node(), ctx2.node()))
50 48 for l in diff.split('\n'):
51 49 if (l.startswith("+") and not l.startswith("+++ ") or
52 50 l.startswith("-") and not l.startswith("--- ")):
53 51 lines += 1
54 52 return lines
55 53
56 54 def countrate(ui, repo, amap, *pats, **opts):
57 55 """Calculate stats"""
58 56 if opts.get('dateformat'):
59 57 def getkey(ctx):
60 58 t, tz = ctx.date()
61 59 date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
62 60 return date.strftime(opts['dateformat'])
63 61 else:
64 62 tmpl = opts.get('template', '{author|email}')
65 63 tmpl = maketemplater(ui, repo, tmpl)
66 64 def getkey(ctx):
67 65 ui.pushbuffer()
68 66 tmpl.show(changenode=ctx.node())
69 67 return ui.popbuffer()
70 68
71 69 count = pct = 0
72 70 rate = {}
73 71 df = False
74 72 if opts.get('date'):
75 73 df = util.matchdate(opts['date'])
76 74
77 75 get = util.cachefunc(lambda r: repo[r].changeset())
78 76 changeiter, matchfn = cmdutil.walkchangerevs(ui, repo, pats, get, opts)
79 77 for st, rev, fns in changeiter:
80 78 if not st == 'add':
81 79 continue
82 80 if df and not df(get(rev)[2][0]): # doesn't match date format
83 81 continue
84 82
85 83 ctx = repo[rev]
86 84 key = getkey(ctx)
87 85 key = amap.get(key, key) # alias remap
88 86 if opts.get('changesets'):
89 87 rate[key] = rate.get(key, 0) + 1
90 88 else:
91 89 parents = ctx.parents()
92 90 if len(parents) > 1:
93 91 ui.note(_('Revision %d is a merge, ignoring...\n') % (rev,))
94 92 continue
95 93
96 94 ctx1 = parents[0]
97 95 lines = changedlines(ui, repo, ctx1, ctx)
98 96 rate[key] = rate.get(key, 0) + lines
99 97
100 98 if opts.get('progress'):
101 99 count += 1
102 100 newpct = int(100.0 * count / max(len(repo), 1))
103 101 if pct < newpct:
104 102 pct = newpct
105 103 ui.write(_("\rGenerating stats: %d%%") % pct)
106 104 sys.stdout.flush()
107 105
108 106 if opts.get('progress'):
109 107 ui.write("\r")
110 108 sys.stdout.flush()
111 109
112 110 return rate
113 111
114 112
115 113 def churn(ui, repo, *pats, **opts):
116 114 '''Graph count of revisions grouped by template
117 115
118 116 Will graph count of changed lines or revisions grouped by template or
119 117 alternatively by date, if dateformat is used. In this case it will override
120 118 template.
121 119
122 120 By default statistics are counted for number of changed lines.
123 121
124 122 Examples:
125 123
126 124 # display count of changed lines for every committer
127 125 hg churn -t '{author|email}'
128 126
129 127 # display daily activity graph
130 128 hg churn -f '%H' -s -c
131 129
132 130 # display activity of developers by month
133 131 hg churn -f '%Y-%m' -s -c
134 132
135 133 # display count of lines changed in every year
136 134 hg churn -f '%Y' -s
137 135
138 136 The map file format used to specify aliases is fairly simple:
139 137
140 138 <alias email> <actual email>'''
141 139 def pad(s, l):
142 140 return (s + " " * l)[:l]
143 141
144 142 amap = {}
145 143 aliases = opts.get('aliases')
146 144 if aliases:
147 145 for l in open(aliases, "r"):
148 146 l = l.strip()
149 147 alias, actual = l.split()
150 148 amap[alias] = actual
151 149
152 150 rate = countrate(ui, repo, amap, *pats, **opts).items()
153 151 if not rate:
154 152 return
155 153
156 154 sortfn = ((not opts.get('sort')) and (lambda a, b: cmp(b[1], a[1])) or None)
157 155 rate.sort(sortfn)
158 156
159 157 maxcount = float(max([v for k, v in rate]))
160 158 maxname = max([len(k) for k, v in rate])
161 159
162 160 ttywidth = get_tty_width()
163 161 ui.debug(_("assuming %i character terminal\n") % ttywidth)
164 162 width = ttywidth - maxname - 2 - 6 - 2 - 2
165 163
166 164 for date, count in rate:
167 165 print "%s %6d %s" % (pad(date, maxname), count,
168 166 "*" * int(count * width / maxcount))
169 167
170 168
171 169 cmdtable = {
172 170 "churn":
173 171 (churn,
174 172 [('r', 'rev', [], _('count rate for the specified revision or range')),
175 173 ('d', 'date', '', _('count rate for revs matching date spec')),
176 174 ('t', 'template', '{author|email}', _('template to group changesets')),
177 175 ('f', 'dateformat', '',
178 176 _('strftime-compatible format for grouping by date')),
179 177 ('c', 'changesets', False, _('count rate by number of changesets')),
180 178 ('s', 'sort', False, _('sort by key (default: sort by count)')),
181 179 ('', 'aliases', '', _('file with email aliases')),
182 180 ('', 'progress', None, _('show progress'))],
183 181 _("hg churn [-d DATE] [-r REV] [--aliases FILE] [--progress] [FILE]")),
184 182 }
@@ -1,356 +1,358
1 1 # Minimal support for git commands on an hg repository
2 2 #
3 3 # Copyright 2005, 2006 Chris Mason <mason@suse.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7 '''browsing the repository in a graphical way
8 8
9 9 The hgk extension allows browsing the history of a repository in a
10 10 graphical way. It requires Tcl/Tk version 8.4 or later. (Tcl/Tk is
11 11 not distributed with Mercurial.)
12 12
13 13 hgk consists of two parts: a Tcl script that does the displaying and
14 14 querying of information, and an extension to mercurial named hgk.py,
15 15 which provides hooks for hgk to get information. hgk can be found in
16 16 the contrib directory, and hgk.py can be found in the hgext directory.
17 17
18 18 To load the hgext.py extension, add it to your .hgrc file (you have
19 19 to use your global $HOME/.hgrc file, not one in a repository). You
20 20 can specify an absolute path:
21 21
22 22 [extensions]
23 23 hgk=/usr/local/lib/hgk.py
24 24
25 25 Mercurial can also scan the default python library path for a file
26 26 named 'hgk.py' if you set hgk empty:
27 27
28 28 [extensions]
29 29 hgk=
30 30
31 31 The hg view command will launch the hgk Tcl script. For this command
32 32 to work, hgk must be in your search path. Alternately, you can
33 33 specify the path to hgk in your .hgrc file:
34 34
35 35 [hgk]
36 36 path=/location/of/hgk
37 37
38 38 hgk can make use of the extdiff extension to visualize revisions.
39 39 Assuming you had already configured extdiff vdiff command, just add:
40 40
41 41 [hgk]
42 42 vdiff=vdiff
43 43
44 44 Revisions context menu will now display additional entries to fire
45 45 vdiff on hovered and selected revisions.'''
46 46
47 47 import os
48 48 from mercurial import commands, util, patch, revlog, cmdutil
49 49 from mercurial.node import nullid, nullrev, short
50 50 from mercurial.i18n import _
51 51
52 52 def difftree(ui, repo, node1=None, node2=None, *files, **opts):
53 53 """diff trees from two commits"""
54 54 def __difftree(repo, node1, node2, files=[]):
55 55 assert node2 is not None
56 56 mmap = repo[node1].manifest()
57 57 mmap2 = repo[node2].manifest()
58 58 m = cmdutil.match(repo, files)
59 59 modified, added, removed = repo.status(node1, node2, m)[:3]
60 60 empty = short(nullid)
61 61
62 62 for f in modified:
63 63 # TODO get file permissions
64 64 ui.write(":100664 100664 %s %s M\t%s\t%s\n" %
65 65 (short(mmap[f]), short(mmap2[f]), f, f))
66 66 for f in added:
67 67 ui.write(":000000 100664 %s %s N\t%s\t%s\n" %
68 68 (empty, short(mmap2[f]), f, f))
69 69 for f in removed:
70 70 ui.write(":100664 000000 %s %s D\t%s\t%s\n" %
71 71 (short(mmap[f]), empty, f, f))
72 72 ##
73 73
74 74 while True:
75 75 if opts['stdin']:
76 76 try:
77 77 line = raw_input().split(' ')
78 78 node1 = line[0]
79 79 if len(line) > 1:
80 80 node2 = line[1]
81 81 else:
82 82 node2 = None
83 83 except EOFError:
84 84 break
85 85 node1 = repo.lookup(node1)
86 86 if node2:
87 87 node2 = repo.lookup(node2)
88 88 else:
89 89 node2 = node1
90 90 node1 = repo.changelog.parents(node1)[0]
91 91 if opts['patch']:
92 92 if opts['pretty']:
93 93 catcommit(ui, repo, node2, "")
94 94 m = cmdutil.match(repo, files)
95 patch.diff(repo, node1, node2, match=m,
96 opts=patch.diffopts(ui, {'git': True}))
95 chunks = patch.diff(repo, node1, node2, match=m,
96 opts=patch.diffopts(ui, {'git': True}))
97 for chunk in chunks:
98 repo.ui.write(chunk)
97 99 else:
98 100 __difftree(repo, node1, node2, files=files)
99 101 if not opts['stdin']:
100 102 break
101 103
102 104 def catcommit(ui, repo, n, prefix, ctx=None):
103 105 nlprefix = '\n' + prefix;
104 106 if ctx is None:
105 107 ctx = repo[n]
106 108 ui.write("tree %s\n" % short(ctx.changeset()[0])) # use ctx.node() instead ??
107 109 for p in ctx.parents():
108 110 ui.write("parent %s\n" % p)
109 111
110 112 date = ctx.date()
111 113 description = ctx.description().replace("\0", "")
112 114 lines = description.splitlines()
113 115 if lines and lines[-1].startswith('committer:'):
114 116 committer = lines[-1].split(': ')[1].rstrip()
115 117 else:
116 118 committer = ctx.user()
117 119
118 120 ui.write("author %s %s %s\n" % (ctx.user(), int(date[0]), date[1]))
119 121 ui.write("committer %s %s %s\n" % (committer, int(date[0]), date[1]))
120 122 ui.write("revision %d\n" % ctx.rev())
121 123 ui.write("branch %s\n\n" % ctx.branch())
122 124
123 125 if prefix != "":
124 126 ui.write("%s%s\n" % (prefix, description.replace('\n', nlprefix).strip()))
125 127 else:
126 128 ui.write(description + "\n")
127 129 if prefix:
128 130 ui.write('\0')
129 131
130 132 def base(ui, repo, node1, node2):
131 133 """Output common ancestor information"""
132 134 node1 = repo.lookup(node1)
133 135 node2 = repo.lookup(node2)
134 136 n = repo.changelog.ancestor(node1, node2)
135 137 ui.write(short(n) + "\n")
136 138
137 139 def catfile(ui, repo, type=None, r=None, **opts):
138 140 """cat a specific revision"""
139 141 # in stdin mode, every line except the commit is prefixed with two
140 142 # spaces. This way the our caller can find the commit without magic
141 143 # strings
142 144 #
143 145 prefix = ""
144 146 if opts['stdin']:
145 147 try:
146 148 (type, r) = raw_input().split(' ');
147 149 prefix = " "
148 150 except EOFError:
149 151 return
150 152
151 153 else:
152 154 if not type or not r:
153 155 ui.warn(_("cat-file: type or revision not supplied\n"))
154 156 commands.help_(ui, 'cat-file')
155 157
156 158 while r:
157 159 if type != "commit":
158 160 ui.warn(_("aborting hg cat-file only understands commits\n"))
159 161 return 1;
160 162 n = repo.lookup(r)
161 163 catcommit(ui, repo, n, prefix)
162 164 if opts['stdin']:
163 165 try:
164 166 (type, r) = raw_input().split(' ');
165 167 except EOFError:
166 168 break
167 169 else:
168 170 break
169 171
170 172 # git rev-tree is a confusing thing. You can supply a number of
171 173 # commit sha1s on the command line, and it walks the commit history
172 174 # telling you which commits are reachable from the supplied ones via
173 175 # a bitmask based on arg position.
174 176 # you can specify a commit to stop at by starting the sha1 with ^
175 177 def revtree(ui, args, repo, full="tree", maxnr=0, parents=False):
176 178 def chlogwalk():
177 179 count = len(repo)
178 180 i = count
179 181 l = [0] * 100
180 182 chunk = 100
181 183 while True:
182 184 if chunk > i:
183 185 chunk = i
184 186 i = 0
185 187 else:
186 188 i -= chunk
187 189
188 190 for x in xrange(0, chunk):
189 191 if i + x >= count:
190 192 l[chunk - x:] = [0] * (chunk - x)
191 193 break
192 194 if full != None:
193 195 l[x] = repo[i + x]
194 196 l[x].changeset() # force reading
195 197 else:
196 198 l[x] = 1
197 199 for x in xrange(chunk-1, -1, -1):
198 200 if l[x] != 0:
199 201 yield (i + x, full != None and l[x] or None)
200 202 if i == 0:
201 203 break
202 204
203 205 # calculate and return the reachability bitmask for sha
204 206 def is_reachable(ar, reachable, sha):
205 207 if len(ar) == 0:
206 208 return 1
207 209 mask = 0
208 210 for i in xrange(len(ar)):
209 211 if sha in reachable[i]:
210 212 mask |= 1 << i
211 213
212 214 return mask
213 215
214 216 reachable = []
215 217 stop_sha1 = []
216 218 want_sha1 = []
217 219 count = 0
218 220
219 221 # figure out which commits they are asking for and which ones they
220 222 # want us to stop on
221 223 for i in xrange(len(args)):
222 224 if args[i].startswith('^'):
223 225 s = repo.lookup(args[i][1:])
224 226 stop_sha1.append(s)
225 227 want_sha1.append(s)
226 228 elif args[i] != 'HEAD':
227 229 want_sha1.append(repo.lookup(args[i]))
228 230
229 231 # calculate the graph for the supplied commits
230 232 for i in xrange(len(want_sha1)):
231 233 reachable.append({});
232 234 n = want_sha1[i];
233 235 visit = [n];
234 236 reachable[i][n] = 1
235 237 while visit:
236 238 n = visit.pop(0)
237 239 if n in stop_sha1:
238 240 continue
239 241 for p in repo.changelog.parents(n):
240 242 if p not in reachable[i]:
241 243 reachable[i][p] = 1
242 244 visit.append(p)
243 245 if p in stop_sha1:
244 246 continue
245 247
246 248 # walk the repository looking for commits that are in our
247 249 # reachability graph
248 250 for i, ctx in chlogwalk():
249 251 n = repo.changelog.node(i)
250 252 mask = is_reachable(want_sha1, reachable, n)
251 253 if mask:
252 254 parentstr = ""
253 255 if parents:
254 256 pp = repo.changelog.parents(n)
255 257 if pp[0] != nullid:
256 258 parentstr += " " + short(pp[0])
257 259 if pp[1] != nullid:
258 260 parentstr += " " + short(pp[1])
259 261 if not full:
260 262 ui.write("%s%s\n" % (short(n), parentstr))
261 263 elif full == "commit":
262 264 ui.write("%s%s\n" % (short(n), parentstr))
263 265 catcommit(ui, repo, n, ' ', ctx)
264 266 else:
265 267 (p1, p2) = repo.changelog.parents(n)
266 268 (h, h1, h2) = map(short, (n, p1, p2))
267 269 (i1, i2) = map(repo.changelog.rev, (p1, p2))
268 270
269 271 date = ctx.date()[0]
270 272 ui.write("%s %s:%s" % (date, h, mask))
271 273 mask = is_reachable(want_sha1, reachable, p1)
272 274 if i1 != nullrev and mask > 0:
273 275 ui.write("%s:%s " % (h1, mask)),
274 276 mask = is_reachable(want_sha1, reachable, p2)
275 277 if i2 != nullrev and mask > 0:
276 278 ui.write("%s:%s " % (h2, mask))
277 279 ui.write("\n")
278 280 if maxnr and count >= maxnr:
279 281 break
280 282 count += 1
281 283
282 284 def revparse(ui, repo, *revs, **opts):
283 285 """Parse given revisions"""
284 286 def revstr(rev):
285 287 if rev == 'HEAD':
286 288 rev = 'tip'
287 289 return revlog.hex(repo.lookup(rev))
288 290
289 291 for r in revs:
290 292 revrange = r.split(':', 1)
291 293 ui.write('%s\n' % revstr(revrange[0]))
292 294 if len(revrange) == 2:
293 295 ui.write('^%s\n' % revstr(revrange[1]))
294 296
295 297 # git rev-list tries to order things by date, and has the ability to stop
296 298 # at a given commit without walking the whole repo. TODO add the stop
297 299 # parameter
298 300 def revlist(ui, repo, *revs, **opts):
299 301 """print revisions"""
300 302 if opts['header']:
301 303 full = "commit"
302 304 else:
303 305 full = None
304 306 copy = [x for x in revs]
305 307 revtree(ui, copy, repo, full, opts['max_count'], opts['parents'])
306 308
307 309 def config(ui, repo, **opts):
308 310 """print extension options"""
309 311 def writeopt(name, value):
310 312 ui.write('k=%s\nv=%s\n' % (name, value))
311 313
312 314 writeopt('vdiff', ui.config('hgk', 'vdiff', ''))
313 315
314 316
315 317 def view(ui, repo, *etc, **opts):
316 318 "start interactive history viewer"
317 319 os.chdir(repo.root)
318 320 optstr = ' '.join(['--%s %s' % (k, v) for k, v in opts.iteritems() if v])
319 321 cmd = ui.config("hgk", "path", "hgk") + " %s %s" % (optstr, " ".join(etc))
320 322 ui.debug(_("running %s\n") % cmd)
321 323 util.system(cmd)
322 324
323 325 cmdtable = {
324 326 "^view":
325 327 (view,
326 328 [('l', 'limit', '', _('limit number of changes displayed'))],
327 329 _('hg view [-l LIMIT] [REVRANGE]')),
328 330 "debug-diff-tree":
329 331 (difftree,
330 332 [('p', 'patch', None, _('generate patch')),
331 333 ('r', 'recursive', None, _('recursive')),
332 334 ('P', 'pretty', None, _('pretty')),
333 335 ('s', 'stdin', None, _('stdin')),
334 336 ('C', 'copy', None, _('detect copies')),
335 337 ('S', 'search', "", _('search'))],
336 338 _('hg git-diff-tree [OPTION]... NODE1 NODE2 [FILE]...')),
337 339 "debug-cat-file":
338 340 (catfile,
339 341 [('s', 'stdin', None, _('stdin'))],
340 342 _('hg debug-cat-file [OPTION]... TYPE FILE')),
341 343 "debug-config":
342 344 (config, [], _('hg debug-config')),
343 345 "debug-merge-base":
344 346 (base, [], _('hg debug-merge-base node node')),
345 347 "debug-rev-parse":
346 348 (revparse,
347 349 [('', 'default', '', _('ignored'))],
348 350 _('hg debug-rev-parse REV')),
349 351 "debug-rev-list":
350 352 (revlist,
351 353 [('H', 'header', None, _('header')),
352 354 ('t', 'topo-order', None, _('topo-order')),
353 355 ('p', 'parents', None, _('parents')),
354 356 ('n', 'max-count', 0, _('max-count'))],
355 357 _('hg debug-rev-list [options] revs')),
356 358 }
@@ -1,543 +1,543
1 1 # keyword.py - $Keyword$ expansion for Mercurial
2 2 #
3 3 # Copyright 2007, 2008 Christian Ebert <blacktrash@gmx.net>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7 #
8 8 # $Id$
9 9 #
10 10 # Keyword expansion hack against the grain of a DSCM
11 11 #
12 12 # There are many good reasons why this is not needed in a distributed
13 13 # SCM, still it may be useful in very small projects based on single
14 14 # files (like LaTeX packages), that are mostly addressed to an audience
15 15 # not running a version control system.
16 16 #
17 17 # For in-depth discussion refer to
18 18 # <http://www.selenic.com/mercurial/wiki/index.cgi/KeywordPlan>.
19 19 #
20 20 # Keyword expansion is based on Mercurial's changeset template mappings.
21 21 #
22 22 # Binary files are not touched.
23 23 #
24 24 # Setup in hgrc:
25 25 #
26 26 # [extensions]
27 27 # # enable extension
28 28 # hgext.keyword =
29 29 #
30 30 # Files to act upon/ignore are specified in the [keyword] section.
31 31 # Customized keyword template mappings in the [keywordmaps] section.
32 32 #
33 33 # Run "hg help keyword" and "hg kwdemo" to get info on configuration.
34 34
35 35 '''keyword expansion in local repositories
36 36
37 37 This extension expands RCS/CVS-like or self-customized $Keywords$
38 38 in tracked text files selected by your configuration.
39 39
40 40 Keywords are only expanded in local repositories and not stored in
41 41 the change history. The mechanism can be regarded as a convenience
42 42 for the current user or for archive distribution.
43 43
44 44 Configuration is done in the [keyword] and [keywordmaps] sections
45 45 of hgrc files.
46 46
47 47 Example:
48 48
49 49 [keyword]
50 50 # expand keywords in every python file except those matching "x*"
51 51 **.py =
52 52 x* = ignore
53 53
54 54 Note: the more specific you are in your filename patterns
55 55 the less you lose speed in huge repos.
56 56
57 57 For [keywordmaps] template mapping and expansion demonstration and
58 58 control run "hg kwdemo".
59 59
60 60 An additional date template filter {date|utcdate} is provided.
61 61
62 62 The default template mappings (view with "hg kwdemo -d") can be replaced
63 63 with customized keywords and templates.
64 64 Again, run "hg kwdemo" to control the results of your config changes.
65 65
66 66 Before changing/disabling active keywords, run "hg kwshrink" to avoid
67 67 the risk of inadvertedly storing expanded keywords in the change history.
68 68
69 69 To force expansion after enabling it, or a configuration change, run
70 70 "hg kwexpand".
71 71
72 72 Also, when committing with the record extension or using mq's qrecord, be aware
73 73 that keywords cannot be updated. Again, run "hg kwexpand" on the files in
74 74 question to update keyword expansions after all changes have been checked in.
75 75
76 76 Expansions spanning more than one line and incremental expansions,
77 77 like CVS' $Log$, are not supported. A keyword template map
78 78 "Log = {desc}" expands to the first line of the changeset description.
79 79 '''
80 80
81 81 from mercurial import commands, cmdutil, dispatch, filelog, revlog, extensions
82 82 from mercurial import patch, localrepo, templater, templatefilters, util
83 83 from mercurial.hgweb import webcommands
84 84 from mercurial.node import nullid, hex
85 85 from mercurial.i18n import _
86 86 import re, shutil, tempfile, time
87 87
88 88 commands.optionalrepo += ' kwdemo'
89 89
90 90 # hg commands that do not act on keywords
91 91 nokwcommands = ('add addremove annotate bundle copy export grep incoming init'
92 92 ' log outgoing push rename rollback tip verify'
93 93 ' convert email glog')
94 94
95 95 # hg commands that trigger expansion only when writing to working dir,
96 96 # not when reading filelog, and unexpand when reading from working dir
97 97 restricted = 'merge record resolve qfold qimport qnew qpush qrefresh qrecord'
98 98
99 99 def utcdate(date):
100 100 '''Returns hgdate in cvs-like UTC format.'''
101 101 return time.strftime('%Y/%m/%d %H:%M:%S', time.gmtime(date[0]))
102 102
103 103 # make keyword tools accessible
104 104 kwtools = {'templater': None, 'hgcmd': '', 'inc': [], 'exc': ['.hg*']}
105 105
106 106
107 107 class kwtemplater(object):
108 108 '''
109 109 Sets up keyword templates, corresponding keyword regex, and
110 110 provides keyword substitution functions.
111 111 '''
112 112 templates = {
113 113 'Revision': '{node|short}',
114 114 'Author': '{author|user}',
115 115 'Date': '{date|utcdate}',
116 116 'RCSFile': '{file|basename},v',
117 117 'Source': '{root}/{file},v',
118 118 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
119 119 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
120 120 }
121 121
122 122 def __init__(self, ui, repo):
123 123 self.ui = ui
124 124 self.repo = repo
125 125 self.matcher = util.matcher(repo.root,
126 126 inc=kwtools['inc'], exc=kwtools['exc'])[1]
127 127 self.restrict = kwtools['hgcmd'] in restricted.split()
128 128
129 129 kwmaps = self.ui.configitems('keywordmaps')
130 130 if kwmaps: # override default templates
131 131 kwmaps = [(k, templater.parsestring(v, False))
132 132 for (k, v) in kwmaps]
133 133 self.templates = dict(kwmaps)
134 134 escaped = map(re.escape, self.templates.keys())
135 135 kwpat = r'\$(%s)(: [^$\n\r]*? )??\$' % '|'.join(escaped)
136 136 self.re_kw = re.compile(kwpat)
137 137
138 138 templatefilters.filters['utcdate'] = utcdate
139 139 self.ct = cmdutil.changeset_templater(self.ui, self.repo,
140 140 False, '', False)
141 141
142 142 def getnode(self, path, fnode):
143 143 '''Derives changenode from file path and filenode.'''
144 144 # used by kwfilelog.read and kwexpand
145 145 c = self.repo.filectx(path, fileid=fnode)
146 146 return c.node()
147 147
148 148 def substitute(self, data, path, node, subfunc):
149 149 '''Replaces keywords in data with expanded template.'''
150 150 def kwsub(mobj):
151 151 kw = mobj.group(1)
152 152 self.ct.use_template(self.templates[kw])
153 153 self.ui.pushbuffer()
154 154 self.ct.show(changenode=node, root=self.repo.root, file=path)
155 155 ekw = templatefilters.firstline(self.ui.popbuffer())
156 156 return '$%s: %s $' % (kw, ekw)
157 157 return subfunc(kwsub, data)
158 158
159 159 def expand(self, path, node, data):
160 160 '''Returns data with keywords expanded.'''
161 161 if not self.restrict and self.matcher(path) and not util.binary(data):
162 162 changenode = self.getnode(path, node)
163 163 return self.substitute(data, path, changenode, self.re_kw.sub)
164 164 return data
165 165
166 166 def iskwfile(self, path, flagfunc):
167 167 '''Returns true if path matches [keyword] pattern
168 168 and is not a symbolic link.
169 169 Caveat: localrepository._link fails on Windows.'''
170 170 return self.matcher(path) and not 'l' in flagfunc(path)
171 171
172 172 def overwrite(self, node, expand, files):
173 173 '''Overwrites selected files expanding/shrinking keywords.'''
174 174 if node is not None: # commit
175 175 ctx = self.repo[node]
176 176 mf = ctx.manifest()
177 177 files = [f for f in ctx.files() if f in mf]
178 178 notify = self.ui.debug
179 179 else: # kwexpand/kwshrink
180 180 ctx = self.repo['.']
181 181 mf = ctx.manifest()
182 182 notify = self.ui.note
183 183 candidates = [f for f in files if self.iskwfile(f, ctx.flags)]
184 184 if candidates:
185 185 self.restrict = True # do not expand when reading
186 186 action = expand and 'expanding' or 'shrinking'
187 187 for f in candidates:
188 188 fp = self.repo.file(f)
189 189 data = fp.read(mf[f])
190 190 if util.binary(data):
191 191 continue
192 192 if expand:
193 193 changenode = node or self.getnode(f, mf[f])
194 194 data, found = self.substitute(data, f, changenode,
195 195 self.re_kw.subn)
196 196 else:
197 197 found = self.re_kw.search(data)
198 198 if found:
199 199 notify(_('overwriting %s %s keywords\n') % (f, action))
200 200 self.repo.wwrite(f, data, mf.flags(f))
201 201 self.repo.dirstate.normal(f)
202 202 self.restrict = False
203 203
204 204 def shrinktext(self, text):
205 205 '''Unconditionally removes all keyword substitutions from text.'''
206 206 return self.re_kw.sub(r'$\1$', text)
207 207
208 208 def shrink(self, fname, text):
209 209 '''Returns text with all keyword substitutions removed.'''
210 210 if self.matcher(fname) and not util.binary(text):
211 211 return self.shrinktext(text)
212 212 return text
213 213
214 214 def shrinklines(self, fname, lines):
215 215 '''Returns lines with keyword substitutions removed.'''
216 216 if self.matcher(fname):
217 217 text = ''.join(lines)
218 218 if not util.binary(text):
219 219 return self.shrinktext(text).splitlines(True)
220 220 return lines
221 221
222 222 def wread(self, fname, data):
223 223 '''If in restricted mode returns data read from wdir with
224 224 keyword substitutions removed.'''
225 225 return self.restrict and self.shrink(fname, data) or data
226 226
227 227 class kwfilelog(filelog.filelog):
228 228 '''
229 229 Subclass of filelog to hook into its read, add, cmp methods.
230 230 Keywords are "stored" unexpanded, and processed on reading.
231 231 '''
232 232 def __init__(self, opener, kwt, path):
233 233 super(kwfilelog, self).__init__(opener, path)
234 234 self.kwt = kwt
235 235 self.path = path
236 236
237 237 def read(self, node):
238 238 '''Expands keywords when reading filelog.'''
239 239 data = super(kwfilelog, self).read(node)
240 240 return self.kwt.expand(self.path, node, data)
241 241
242 242 def add(self, text, meta, tr, link, p1=None, p2=None):
243 243 '''Removes keyword substitutions when adding to filelog.'''
244 244 text = self.kwt.shrink(self.path, text)
245 245 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
246 246
247 247 def cmp(self, node, text):
248 248 '''Removes keyword substitutions for comparison.'''
249 249 text = self.kwt.shrink(self.path, text)
250 250 if self.renamed(node):
251 251 t2 = super(kwfilelog, self).read(node)
252 252 return t2 != text
253 253 return revlog.revlog.cmp(self, node, text)
254 254
255 255 def _status(ui, repo, kwt, unknown, *pats, **opts):
256 256 '''Bails out if [keyword] configuration is not active.
257 257 Returns status of working directory.'''
258 258 if kwt:
259 259 matcher = cmdutil.match(repo, pats, opts)
260 260 return repo.status(match=matcher, unknown=unknown, clean=True)
261 261 if ui.configitems('keyword'):
262 262 raise util.Abort(_('[keyword] patterns cannot match'))
263 263 raise util.Abort(_('no [keyword] patterns configured'))
264 264
265 265 def _kwfwrite(ui, repo, expand, *pats, **opts):
266 266 '''Selects files and passes them to kwtemplater.overwrite.'''
267 267 if repo.dirstate.parents()[1] != nullid:
268 268 raise util.Abort(_('outstanding uncommitted merge'))
269 269 kwt = kwtools['templater']
270 270 status = _status(ui, repo, kwt, False, *pats, **opts)
271 271 modified, added, removed, deleted = status[:4]
272 272 if modified or added or removed or deleted:
273 273 raise util.Abort(_('outstanding uncommitted changes'))
274 274 wlock = lock = None
275 275 try:
276 276 wlock = repo.wlock()
277 277 lock = repo.lock()
278 278 kwt.overwrite(None, expand, status[6])
279 279 finally:
280 280 del wlock, lock
281 281
282 282
283 283 def demo(ui, repo, *args, **opts):
284 284 '''print [keywordmaps] configuration and an expansion example
285 285
286 286 Show current, custom, or default keyword template maps
287 287 and their expansion.
288 288
289 289 Extend current configuration by specifying maps as arguments
290 290 and optionally by reading from an additional hgrc file.
291 291
292 292 Override current keyword template maps with "default" option.
293 293 '''
294 294 def demostatus(stat):
295 295 ui.status(_('\n\t%s\n') % stat)
296 296
297 297 def demoitems(section, items):
298 298 ui.write('[%s]\n' % section)
299 299 for k, v in items:
300 300 ui.write('%s = %s\n' % (k, v))
301 301
302 302 msg = 'hg keyword config and expansion example'
303 303 kwstatus = 'current'
304 304 fn = 'demo.txt'
305 305 branchname = 'demobranch'
306 306 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
307 307 ui.note(_('creating temporary repo at %s\n') % tmpdir)
308 308 repo = localrepo.localrepository(ui, tmpdir, True)
309 309 ui.setconfig('keyword', fn, '')
310 310 if args or opts.get('rcfile'):
311 311 kwstatus = 'custom'
312 312 if opts.get('rcfile'):
313 313 ui.readconfig(opts.get('rcfile'))
314 314 if opts.get('default'):
315 315 kwstatus = 'default'
316 316 kwmaps = kwtemplater.templates
317 317 if ui.configitems('keywordmaps'):
318 318 # override maps from optional rcfile
319 319 for k, v in kwmaps.iteritems():
320 320 ui.setconfig('keywordmaps', k, v)
321 321 elif args:
322 322 # simulate hgrc parsing
323 323 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
324 324 fp = repo.opener('hgrc', 'w')
325 325 fp.writelines(rcmaps)
326 326 fp.close()
327 327 ui.readconfig(repo.join('hgrc'))
328 328 if not opts.get('default'):
329 329 kwmaps = dict(ui.configitems('keywordmaps')) or kwtemplater.templates
330 330 uisetup(ui)
331 331 reposetup(ui, repo)
332 332 for k, v in ui.configitems('extensions'):
333 333 if k.endswith('keyword'):
334 334 extension = '%s = %s' % (k, v)
335 335 break
336 336 demostatus('config using %s keyword template maps' % kwstatus)
337 337 ui.write('[extensions]\n%s\n' % extension)
338 338 demoitems('keyword', ui.configitems('keyword'))
339 339 demoitems('keywordmaps', kwmaps.iteritems())
340 340 keywords = '$' + '$\n$'.join(kwmaps.keys()) + '$\n'
341 341 repo.wopener(fn, 'w').write(keywords)
342 342 repo.add([fn])
343 343 path = repo.wjoin(fn)
344 344 ui.note(_('\n%s keywords written to %s:\n') % (kwstatus, path))
345 345 ui.note(keywords)
346 346 ui.note('\nhg -R "%s" branch "%s"\n' % (tmpdir, branchname))
347 347 # silence branch command if not verbose
348 348 quiet = ui.quiet
349 349 ui.quiet = not ui.verbose
350 350 commands.branch(ui, repo, branchname)
351 351 ui.quiet = quiet
352 352 for name, cmd in ui.configitems('hooks'):
353 353 if name.split('.', 1)[0].find('commit') > -1:
354 354 repo.ui.setconfig('hooks', name, '')
355 355 ui.note(_('unhooked all commit hooks\n'))
356 356 ui.note('hg -R "%s" ci -m "%s"\n' % (tmpdir, msg))
357 357 repo.commit(text=msg)
358 358 format = ui.verbose and ' in %s' % path or ''
359 359 demostatus('%s keywords expanded%s' % (kwstatus, format))
360 360 ui.write(repo.wread(fn))
361 361 ui.debug(_('\nremoving temporary repo %s\n') % tmpdir)
362 362 shutil.rmtree(tmpdir, ignore_errors=True)
363 363
364 364 def expand(ui, repo, *pats, **opts):
365 365 '''expand keywords in working directory
366 366
367 367 Run after (re)enabling keyword expansion.
368 368
369 369 kwexpand refuses to run if given files contain local changes.
370 370 '''
371 371 # 3rd argument sets expansion to True
372 372 _kwfwrite(ui, repo, True, *pats, **opts)
373 373
374 374 def files(ui, repo, *pats, **opts):
375 375 '''print files currently configured for keyword expansion
376 376
377 377 Crosscheck which files in working directory are potential targets for
378 378 keyword expansion.
379 379 That is, files matched by [keyword] config patterns but not symlinks.
380 380 '''
381 381 kwt = kwtools['templater']
382 382 status = _status(ui, repo, kwt, opts.get('untracked'), *pats, **opts)
383 383 modified, added, removed, deleted, unknown, ignored, clean = status
384 384 files = util.sort(modified + added + clean + unknown)
385 385 wctx = repo[None]
386 386 kwfiles = [f for f in files if kwt.iskwfile(f, wctx.flags)]
387 387 cwd = pats and repo.getcwd() or ''
388 388 kwfstats = not opts.get('ignore') and (('K', kwfiles),) or ()
389 389 if opts.get('all') or opts.get('ignore'):
390 390 kwfstats += (('I', [f for f in files if f not in kwfiles]),)
391 391 for char, filenames in kwfstats:
392 392 format = (opts.get('all') or ui.verbose) and '%s %%s\n' % char or '%s\n'
393 393 for f in filenames:
394 394 ui.write(format % repo.pathto(f, cwd))
395 395
396 396 def shrink(ui, repo, *pats, **opts):
397 397 '''revert expanded keywords in working directory
398 398
399 399 Run before changing/disabling active keywords
400 400 or if you experience problems with "hg import" or "hg merge".
401 401
402 402 kwshrink refuses to run if given files contain local changes.
403 403 '''
404 404 # 3rd argument sets expansion to False
405 405 _kwfwrite(ui, repo, False, *pats, **opts)
406 406
407 407
408 408 def uisetup(ui):
409 409 '''Collects [keyword] config in kwtools.
410 410 Monkeypatches dispatch._parse if needed.'''
411 411
412 412 for pat, opt in ui.configitems('keyword'):
413 413 if opt != 'ignore':
414 414 kwtools['inc'].append(pat)
415 415 else:
416 416 kwtools['exc'].append(pat)
417 417
418 418 if kwtools['inc']:
419 419 def kwdispatch_parse(orig, ui, args):
420 420 '''Monkeypatch dispatch._parse to obtain running hg command.'''
421 421 cmd, func, args, options, cmdoptions = orig(ui, args)
422 422 kwtools['hgcmd'] = cmd
423 423 return cmd, func, args, options, cmdoptions
424 424
425 425 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
426 426
427 427 def reposetup(ui, repo):
428 428 '''Sets up repo as kwrepo for keyword substitution.
429 429 Overrides file method to return kwfilelog instead of filelog
430 430 if file matches user configuration.
431 431 Wraps commit to overwrite configured files with updated
432 432 keyword substitutions.
433 433 Monkeypatches patch and webcommands.'''
434 434
435 435 try:
436 436 if (not repo.local() or not kwtools['inc']
437 437 or kwtools['hgcmd'] in nokwcommands.split()
438 438 or '.hg' in util.splitpath(repo.root)
439 439 or repo._url.startswith('bundle:')):
440 440 return
441 441 except AttributeError:
442 442 pass
443 443
444 444 kwtools['templater'] = kwt = kwtemplater(ui, repo)
445 445
446 446 class kwrepo(repo.__class__):
447 447 def file(self, f):
448 448 if f[0] == '/':
449 449 f = f[1:]
450 450 return kwfilelog(self.sopener, kwt, f)
451 451
452 452 def wread(self, filename):
453 453 data = super(kwrepo, self).wread(filename)
454 454 return kwt.wread(filename, data)
455 455
456 456 def commit(self, files=None, text='', user=None, date=None,
457 457 match=None, force=False, force_editor=False,
458 458 p1=None, p2=None, extra={}, empty_ok=False):
459 459 wlock = lock = None
460 460 _p1 = _p2 = None
461 461 try:
462 462 wlock = self.wlock()
463 463 lock = self.lock()
464 464 # store and postpone commit hooks
465 465 commithooks = {}
466 466 for name, cmd in ui.configitems('hooks'):
467 467 if name.split('.', 1)[0] == 'commit':
468 468 commithooks[name] = cmd
469 469 ui.setconfig('hooks', name, None)
470 470 if commithooks:
471 471 # store parents for commit hook environment
472 472 if p1 is None:
473 473 _p1, _p2 = repo.dirstate.parents()
474 474 else:
475 475 _p1, _p2 = p1, p2 or nullid
476 476 _p1 = hex(_p1)
477 477 if _p2 == nullid:
478 478 _p2 = ''
479 479 else:
480 480 _p2 = hex(_p2)
481 481
482 482 n = super(kwrepo, self).commit(files, text, user, date, match,
483 483 force, force_editor, p1, p2,
484 484 extra, empty_ok)
485 485
486 486 # restore commit hooks
487 487 for name, cmd in commithooks.iteritems():
488 488 ui.setconfig('hooks', name, cmd)
489 489 if n is not None:
490 490 kwt.overwrite(n, True, None)
491 491 repo.hook('commit', node=n, parent1=_p1, parent2=_p2)
492 492 return n
493 493 finally:
494 494 del wlock, lock
495 495
496 496 # monkeypatches
497 497 def kwpatchfile_init(orig, self, ui, fname, missing=False):
498 498 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
499 499 rejects or conflicts due to expanded keywords in working dir.'''
500 500 orig(self, ui, fname, missing)
501 501 # shrink keywords read from working dir
502 502 self.lines = kwt.shrinklines(self.fname, self.lines)
503 503
504 def kw_diff(orig, repo, node1=None, node2=None, match=None,
505 fp=None, changes=None, opts=None):
504 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
505 opts=None):
506 506 '''Monkeypatch patch.diff to avoid expansion except when
507 507 comparing against working dir.'''
508 508 if node2 is not None:
509 509 kwt.matcher = util.never
510 510 elif node1 is not None and node1 != repo['.'].node():
511 511 kwt.restrict = True
512 orig(repo, node1, node2, match, fp, changes, opts)
512 return orig(repo, node1, node2, match, changes, opts)
513 513
514 514 def kwweb_skip(orig, web, req, tmpl):
515 515 '''Wraps webcommands.x turning off keyword expansion.'''
516 516 kwt.matcher = util.never
517 517 return orig(web, req, tmpl)
518 518
519 519 repo.__class__ = kwrepo
520 520
521 521 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
522 522 extensions.wrapfunction(patch, 'diff', kw_diff)
523 523 for c in 'annotate changeset rev filediff diff'.split():
524 524 extensions.wrapfunction(webcommands, c, kwweb_skip)
525 525
526 526 cmdtable = {
527 527 'kwdemo':
528 528 (demo,
529 529 [('d', 'default', None, _('show default keyword template maps')),
530 530 ('f', 'rcfile', [], _('read maps from rcfile'))],
531 531 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...')),
532 532 'kwexpand': (expand, commands.walkopts,
533 533 _('hg kwexpand [OPTION]... [FILE]...')),
534 534 'kwfiles':
535 535 (files,
536 536 [('a', 'all', None, _('show keyword status flags of all files')),
537 537 ('i', 'ignore', None, _('show files excluded from expansion')),
538 538 ('u', 'untracked', None, _('additionally show untracked files')),
539 539 ] + commands.walkopts,
540 540 _('hg kwfiles [OPTION]... [FILE]...')),
541 541 'kwshrink': (shrink, commands.walkopts,
542 542 _('hg kwshrink [OPTION]... [FILE]...')),
543 543 }
@@ -1,2513 +1,2520
1 1 # mq.py - patch queues for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Chris Mason <mason@suse.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 '''patch management and development
9 9
10 10 This extension lets you work with a stack of patches in a Mercurial
11 11 repository. It manages two stacks of patches - all known patches, and
12 12 applied patches (subset of known patches).
13 13
14 14 Known patches are represented as patch files in the .hg/patches
15 15 directory. Applied patches are both patch files and changesets.
16 16
17 17 Common tasks (use "hg help command" for more details):
18 18
19 19 prepare repository to work with patches qinit
20 20 create new patch qnew
21 21 import existing patch qimport
22 22
23 23 print patch series qseries
24 24 print applied patches qapplied
25 25 print name of top applied patch qtop
26 26
27 27 add known patch to applied stack qpush
28 28 remove patch from applied stack qpop
29 29 refresh contents of top applied patch qrefresh
30 30 '''
31 31
32 32 from mercurial.i18n import _
33 33 from mercurial.node import bin, hex, short
34 34 from mercurial.repo import RepoError
35 35 from mercurial import commands, cmdutil, hg, patch, revlog, util
36 36 from mercurial import repair, extensions, url
37 37 import os, sys, re, errno
38 38
39 39 commands.norepo += " qclone"
40 40
41 41 # Patch names looks like unix-file names.
42 42 # They must be joinable with queue directory and result in the patch path.
43 43 normname = util.normpath
44 44
45 45 class statusentry:
46 46 def __init__(self, rev, name=None):
47 47 if not name:
48 48 fields = rev.split(':', 1)
49 49 if len(fields) == 2:
50 50 self.rev, self.name = fields
51 51 else:
52 52 self.rev, self.name = None, None
53 53 else:
54 54 self.rev, self.name = rev, name
55 55
56 56 def __str__(self):
57 57 return self.rev + ':' + self.name
58 58
59 59 class queue:
60 60 def __init__(self, ui, path, patchdir=None):
61 61 self.basepath = path
62 62 self.path = patchdir or os.path.join(path, "patches")
63 63 self.opener = util.opener(self.path)
64 64 self.ui = ui
65 65 self.applied = []
66 66 self.full_series = []
67 67 self.applied_dirty = 0
68 68 self.series_dirty = 0
69 69 self.series_path = "series"
70 70 self.status_path = "status"
71 71 self.guards_path = "guards"
72 72 self.active_guards = None
73 73 self.guards_dirty = False
74 74 self._diffopts = None
75 75
76 76 if os.path.exists(self.join(self.series_path)):
77 77 self.full_series = self.opener(self.series_path).read().splitlines()
78 78 self.parse_series()
79 79
80 80 if os.path.exists(self.join(self.status_path)):
81 81 lines = self.opener(self.status_path).read().splitlines()
82 82 self.applied = [statusentry(l) for l in lines]
83 83
84 84 def diffopts(self):
85 85 if self._diffopts is None:
86 86 self._diffopts = patch.diffopts(self.ui)
87 87 return self._diffopts
88 88
89 89 def join(self, *p):
90 90 return os.path.join(self.path, *p)
91 91
92 92 def find_series(self, patch):
93 93 pre = re.compile("(\s*)([^#]+)")
94 94 index = 0
95 95 for l in self.full_series:
96 96 m = pre.match(l)
97 97 if m:
98 98 s = m.group(2)
99 99 s = s.rstrip()
100 100 if s == patch:
101 101 return index
102 102 index += 1
103 103 return None
104 104
105 105 guard_re = re.compile(r'\s?#([-+][^-+# \t\r\n\f][^# \t\r\n\f]*)')
106 106
107 107 def parse_series(self):
108 108 self.series = []
109 109 self.series_guards = []
110 110 for l in self.full_series:
111 111 h = l.find('#')
112 112 if h == -1:
113 113 patch = l
114 114 comment = ''
115 115 elif h == 0:
116 116 continue
117 117 else:
118 118 patch = l[:h]
119 119 comment = l[h:]
120 120 patch = patch.strip()
121 121 if patch:
122 122 if patch in self.series:
123 123 raise util.Abort(_('%s appears more than once in %s') %
124 124 (patch, self.join(self.series_path)))
125 125 self.series.append(patch)
126 126 self.series_guards.append(self.guard_re.findall(comment))
127 127
128 128 def check_guard(self, guard):
129 129 if not guard:
130 130 return _('guard cannot be an empty string')
131 131 bad_chars = '# \t\r\n\f'
132 132 first = guard[0]
133 133 for c in '-+':
134 134 if first == c:
135 135 return (_('guard %r starts with invalid character: %r') %
136 136 (guard, c))
137 137 for c in bad_chars:
138 138 if c in guard:
139 139 return _('invalid character in guard %r: %r') % (guard, c)
140 140
141 141 def set_active(self, guards):
142 142 for guard in guards:
143 143 bad = self.check_guard(guard)
144 144 if bad:
145 145 raise util.Abort(bad)
146 146 guards = util.sort(util.unique(guards))
147 147 self.ui.debug(_('active guards: %s\n') % ' '.join(guards))
148 148 self.active_guards = guards
149 149 self.guards_dirty = True
150 150
151 151 def active(self):
152 152 if self.active_guards is None:
153 153 self.active_guards = []
154 154 try:
155 155 guards = self.opener(self.guards_path).read().split()
156 156 except IOError, err:
157 157 if err.errno != errno.ENOENT: raise
158 158 guards = []
159 159 for i, guard in enumerate(guards):
160 160 bad = self.check_guard(guard)
161 161 if bad:
162 162 self.ui.warn('%s:%d: %s\n' %
163 163 (self.join(self.guards_path), i + 1, bad))
164 164 else:
165 165 self.active_guards.append(guard)
166 166 return self.active_guards
167 167
168 168 def set_guards(self, idx, guards):
169 169 for g in guards:
170 170 if len(g) < 2:
171 171 raise util.Abort(_('guard %r too short') % g)
172 172 if g[0] not in '-+':
173 173 raise util.Abort(_('guard %r starts with invalid char') % g)
174 174 bad = self.check_guard(g[1:])
175 175 if bad:
176 176 raise util.Abort(bad)
177 177 drop = self.guard_re.sub('', self.full_series[idx])
178 178 self.full_series[idx] = drop + ''.join([' #' + g for g in guards])
179 179 self.parse_series()
180 180 self.series_dirty = True
181 181
182 182 def pushable(self, idx):
183 183 if isinstance(idx, str):
184 184 idx = self.series.index(idx)
185 185 patchguards = self.series_guards[idx]
186 186 if not patchguards:
187 187 return True, None
188 188 guards = self.active()
189 189 exactneg = [g for g in patchguards if g[0] == '-' and g[1:] in guards]
190 190 if exactneg:
191 191 return False, exactneg[0]
192 192 pos = [g for g in patchguards if g[0] == '+']
193 193 exactpos = [g for g in pos if g[1:] in guards]
194 194 if pos:
195 195 if exactpos:
196 196 return True, exactpos[0]
197 197 return False, pos
198 198 return True, ''
199 199
200 200 def explain_pushable(self, idx, all_patches=False):
201 201 write = all_patches and self.ui.write or self.ui.warn
202 202 if all_patches or self.ui.verbose:
203 203 if isinstance(idx, str):
204 204 idx = self.series.index(idx)
205 205 pushable, why = self.pushable(idx)
206 206 if all_patches and pushable:
207 207 if why is None:
208 208 write(_('allowing %s - no guards in effect\n') %
209 209 self.series[idx])
210 210 else:
211 211 if not why:
212 212 write(_('allowing %s - no matching negative guards\n') %
213 213 self.series[idx])
214 214 else:
215 215 write(_('allowing %s - guarded by %r\n') %
216 216 (self.series[idx], why))
217 217 if not pushable:
218 218 if why:
219 219 write(_('skipping %s - guarded by %r\n') %
220 220 (self.series[idx], why))
221 221 else:
222 222 write(_('skipping %s - no matching guards\n') %
223 223 self.series[idx])
224 224
225 225 def save_dirty(self):
226 226 def write_list(items, path):
227 227 fp = self.opener(path, 'w')
228 228 for i in items:
229 229 fp.write("%s\n" % i)
230 230 fp.close()
231 231 if self.applied_dirty: write_list(map(str, self.applied), self.status_path)
232 232 if self.series_dirty: write_list(self.full_series, self.series_path)
233 233 if self.guards_dirty: write_list(self.active_guards, self.guards_path)
234 234
235 235 def readheaders(self, patch):
236 236 def eatdiff(lines):
237 237 while lines:
238 238 l = lines[-1]
239 239 if (l.startswith("diff -") or
240 240 l.startswith("Index:") or
241 241 l.startswith("===========")):
242 242 del lines[-1]
243 243 else:
244 244 break
245 245 def eatempty(lines):
246 246 while lines:
247 247 l = lines[-1]
248 248 if re.match('\s*$', l):
249 249 del lines[-1]
250 250 else:
251 251 break
252 252
253 253 pf = self.join(patch)
254 254 message = []
255 255 comments = []
256 256 user = None
257 257 date = None
258 258 format = None
259 259 subject = None
260 260 diffstart = 0
261 261
262 262 for line in file(pf):
263 263 line = line.rstrip()
264 264 if line.startswith('diff --git'):
265 265 diffstart = 2
266 266 break
267 267 if diffstart:
268 268 if line.startswith('+++ '):
269 269 diffstart = 2
270 270 break
271 271 if line.startswith("--- "):
272 272 diffstart = 1
273 273 continue
274 274 elif format == "hgpatch":
275 275 # parse values when importing the result of an hg export
276 276 if line.startswith("# User "):
277 277 user = line[7:]
278 278 elif line.startswith("# Date "):
279 279 date = line[7:]
280 280 elif not line.startswith("# ") and line:
281 281 message.append(line)
282 282 format = None
283 283 elif line == '# HG changeset patch':
284 284 format = "hgpatch"
285 285 elif (format != "tagdone" and (line.startswith("Subject: ") or
286 286 line.startswith("subject: "))):
287 287 subject = line[9:]
288 288 format = "tag"
289 289 elif (format != "tagdone" and (line.startswith("From: ") or
290 290 line.startswith("from: "))):
291 291 user = line[6:]
292 292 format = "tag"
293 293 elif format == "tag" and line == "":
294 294 # when looking for tags (subject: from: etc) they
295 295 # end once you find a blank line in the source
296 296 format = "tagdone"
297 297 elif message or line:
298 298 message.append(line)
299 299 comments.append(line)
300 300
301 301 eatdiff(message)
302 302 eatdiff(comments)
303 303 eatempty(message)
304 304 eatempty(comments)
305 305
306 306 # make sure message isn't empty
307 307 if format and format.startswith("tag") and subject:
308 308 message.insert(0, "")
309 309 message.insert(0, subject)
310 310 return (message, comments, user, date, diffstart > 1)
311 311
312 312 def removeundo(self, repo):
313 313 undo = repo.sjoin('undo')
314 314 if not os.path.exists(undo):
315 315 return
316 316 try:
317 317 os.unlink(undo)
318 318 except OSError, inst:
319 319 self.ui.warn(_('error removing undo: %s\n') % str(inst))
320 320
321 321 def printdiff(self, repo, node1, node2=None, files=None,
322 322 fp=None, changes=None, opts={}):
323 323 m = cmdutil.match(repo, files, opts)
324 patch.diff(repo, node1, node2, m, fp, changes, self.diffopts())
324 chunks = patch.diff(repo, node1, node2, m, changes, self.diffopts())
325 write = fp is None and repo.ui.write or fp.write
326 for chunk in chunks:
327 write(chunk)
325 328
326 329 def mergeone(self, repo, mergeq, head, patch, rev):
327 330 # first try just applying the patch
328 331 (err, n) = self.apply(repo, [ patch ], update_status=False,
329 332 strict=True, merge=rev)
330 333
331 334 if err == 0:
332 335 return (err, n)
333 336
334 337 if n is None:
335 338 raise util.Abort(_("apply failed for patch %s") % patch)
336 339
337 340 self.ui.warn(_("patch didn't work out, merging %s\n") % patch)
338 341
339 342 # apply failed, strip away that rev and merge.
340 343 hg.clean(repo, head)
341 344 self.strip(repo, n, update=False, backup='strip')
342 345
343 346 ctx = repo[rev]
344 347 ret = hg.merge(repo, rev)
345 348 if ret:
346 349 raise util.Abort(_("update returned %d") % ret)
347 350 n = repo.commit(None, ctx.description(), ctx.user(), force=1)
348 351 if n == None:
349 352 raise util.Abort(_("repo commit failed"))
350 353 try:
351 354 message, comments, user, date, patchfound = mergeq.readheaders(patch)
352 355 except:
353 356 raise util.Abort(_("unable to read %s") % patch)
354 357
355 358 patchf = self.opener(patch, "w")
356 359 if comments:
357 360 comments = "\n".join(comments) + '\n\n'
358 361 patchf.write(comments)
359 362 self.printdiff(repo, head, n, fp=patchf)
360 363 patchf.close()
361 364 self.removeundo(repo)
362 365 return (0, n)
363 366
364 367 def qparents(self, repo, rev=None):
365 368 if rev is None:
366 369 (p1, p2) = repo.dirstate.parents()
367 370 if p2 == revlog.nullid:
368 371 return p1
369 372 if len(self.applied) == 0:
370 373 return None
371 374 return revlog.bin(self.applied[-1].rev)
372 375 pp = repo.changelog.parents(rev)
373 376 if pp[1] != revlog.nullid:
374 377 arevs = [ x.rev for x in self.applied ]
375 378 p0 = revlog.hex(pp[0])
376 379 p1 = revlog.hex(pp[1])
377 380 if p0 in arevs:
378 381 return pp[0]
379 382 if p1 in arevs:
380 383 return pp[1]
381 384 return pp[0]
382 385
383 386 def mergepatch(self, repo, mergeq, series):
384 387 if len(self.applied) == 0:
385 388 # each of the patches merged in will have two parents. This
386 389 # can confuse the qrefresh, qdiff, and strip code because it
387 390 # needs to know which parent is actually in the patch queue.
388 391 # so, we insert a merge marker with only one parent. This way
389 392 # the first patch in the queue is never a merge patch
390 393 #
391 394 pname = ".hg.patches.merge.marker"
392 395 n = repo.commit(None, '[mq]: merge marker', user=None, force=1)
393 396 self.removeundo(repo)
394 397 self.applied.append(statusentry(revlog.hex(n), pname))
395 398 self.applied_dirty = 1
396 399
397 400 head = self.qparents(repo)
398 401
399 402 for patch in series:
400 403 patch = mergeq.lookup(patch, strict=True)
401 404 if not patch:
402 405 self.ui.warn(_("patch %s does not exist\n") % patch)
403 406 return (1, None)
404 407 pushable, reason = self.pushable(patch)
405 408 if not pushable:
406 409 self.explain_pushable(patch, all_patches=True)
407 410 continue
408 411 info = mergeq.isapplied(patch)
409 412 if not info:
410 413 self.ui.warn(_("patch %s is not applied\n") % patch)
411 414 return (1, None)
412 415 rev = revlog.bin(info[1])
413 416 (err, head) = self.mergeone(repo, mergeq, head, patch, rev)
414 417 if head:
415 418 self.applied.append(statusentry(revlog.hex(head), patch))
416 419 self.applied_dirty = 1
417 420 if err:
418 421 return (err, head)
419 422 self.save_dirty()
420 423 return (0, head)
421 424
422 425 def patch(self, repo, patchfile):
423 426 '''Apply patchfile to the working directory.
424 427 patchfile: file name of patch'''
425 428 files = {}
426 429 try:
427 430 fuzz = patch.patch(patchfile, self.ui, strip=1, cwd=repo.root,
428 431 files=files)
429 432 except Exception, inst:
430 433 self.ui.note(str(inst) + '\n')
431 434 if not self.ui.verbose:
432 435 self.ui.warn(_("patch failed, unable to continue (try -v)\n"))
433 436 return (False, files, False)
434 437
435 438 return (True, files, fuzz)
436 439
437 440 def apply(self, repo, series, list=False, update_status=True,
438 441 strict=False, patchdir=None, merge=None, all_files={}):
439 442 wlock = lock = tr = None
440 443 try:
441 444 wlock = repo.wlock()
442 445 lock = repo.lock()
443 446 tr = repo.transaction()
444 447 try:
445 448 ret = self._apply(repo, series, list, update_status,
446 449 strict, patchdir, merge, all_files=all_files)
447 450 tr.close()
448 451 self.save_dirty()
449 452 return ret
450 453 except:
451 454 try:
452 455 tr.abort()
453 456 finally:
454 457 repo.invalidate()
455 458 repo.dirstate.invalidate()
456 459 raise
457 460 finally:
458 461 del tr, lock, wlock
459 462 self.removeundo(repo)
460 463
461 464 def _apply(self, repo, series, list=False, update_status=True,
462 465 strict=False, patchdir=None, merge=None, all_files={}):
463 466 # TODO unify with commands.py
464 467 if not patchdir:
465 468 patchdir = self.path
466 469 err = 0
467 470 n = None
468 471 for patchname in series:
469 472 pushable, reason = self.pushable(patchname)
470 473 if not pushable:
471 474 self.explain_pushable(patchname, all_patches=True)
472 475 continue
473 476 self.ui.warn(_("applying %s\n") % patchname)
474 477 pf = os.path.join(patchdir, patchname)
475 478
476 479 try:
477 480 message, comments, user, date, patchfound = self.readheaders(patchname)
478 481 except:
479 482 self.ui.warn(_("Unable to read %s\n") % patchname)
480 483 err = 1
481 484 break
482 485
483 486 if not message:
484 487 message = _("imported patch %s\n") % patchname
485 488 else:
486 489 if list:
487 490 message.append(_("\nimported patch %s") % patchname)
488 491 message = '\n'.join(message)
489 492
490 493 (patcherr, files, fuzz) = self.patch(repo, pf)
491 494 all_files.update(files)
492 495 patcherr = not patcherr
493 496
494 497 if merge and files:
495 498 # Mark as removed/merged and update dirstate parent info
496 499 removed = []
497 500 merged = []
498 501 for f in files:
499 502 if os.path.exists(repo.wjoin(f)):
500 503 merged.append(f)
501 504 else:
502 505 removed.append(f)
503 506 for f in removed:
504 507 repo.dirstate.remove(f)
505 508 for f in merged:
506 509 repo.dirstate.merge(f)
507 510 p1, p2 = repo.dirstate.parents()
508 511 repo.dirstate.setparents(p1, merge)
509 512
510 513 files = patch.updatedir(self.ui, repo, files)
511 514 match = cmdutil.matchfiles(repo, files or [])
512 515 n = repo.commit(files, message, user, date, match=match,
513 516 force=True)
514 517
515 518 if n == None:
516 519 raise util.Abort(_("repo commit failed"))
517 520
518 521 if update_status:
519 522 self.applied.append(statusentry(revlog.hex(n), patchname))
520 523
521 524 if patcherr:
522 525 if not patchfound:
523 526 self.ui.warn(_("patch %s is empty\n") % patchname)
524 527 err = 0
525 528 else:
526 529 self.ui.warn(_("patch failed, rejects left in working dir\n"))
527 530 err = 1
528 531 break
529 532
530 533 if fuzz and strict:
531 534 self.ui.warn(_("fuzz found when applying patch, stopping\n"))
532 535 err = 1
533 536 break
534 537 return (err, n)
535 538
536 539 def _clean_series(self, patches):
537 540 indices = util.sort([self.find_series(p) for p in patches])
538 541 for i in indices[-1::-1]:
539 542 del self.full_series[i]
540 543 self.parse_series()
541 544 self.series_dirty = 1
542 545
543 546 def finish(self, repo, revs):
544 547 revs.sort()
545 548 firstrev = repo[self.applied[0].rev].rev()
546 549 appliedbase = 0
547 550 patches = []
548 551 for rev in util.sort(revs):
549 552 if rev < firstrev:
550 553 raise util.Abort(_('revision %d is not managed') % rev)
551 554 base = revlog.bin(self.applied[appliedbase].rev)
552 555 node = repo.changelog.node(rev)
553 556 if node != base:
554 557 raise util.Abort(_('cannot delete revision %d above '
555 558 'applied patches') % rev)
556 559 patches.append(self.applied[appliedbase].name)
557 560 appliedbase += 1
558 561
559 562 r = self.qrepo()
560 563 if r:
561 564 r.remove(patches, True)
562 565 else:
563 566 for p in patches:
564 567 os.unlink(self.join(p))
565 568
566 569 del self.applied[:appliedbase]
567 570 self.applied_dirty = 1
568 571 self._clean_series(patches)
569 572
570 573 def delete(self, repo, patches, opts):
571 574 if not patches and not opts.get('rev'):
572 575 raise util.Abort(_('qdelete requires at least one revision or '
573 576 'patch name'))
574 577
575 578 realpatches = []
576 579 for patch in patches:
577 580 patch = self.lookup(patch, strict=True)
578 581 info = self.isapplied(patch)
579 582 if info:
580 583 raise util.Abort(_("cannot delete applied patch %s") % patch)
581 584 if patch not in self.series:
582 585 raise util.Abort(_("patch %s not in series file") % patch)
583 586 realpatches.append(patch)
584 587
585 588 appliedbase = 0
586 589 if opts.get('rev'):
587 590 if not self.applied:
588 591 raise util.Abort(_('no patches applied'))
589 592 revs = cmdutil.revrange(repo, opts['rev'])
590 593 if len(revs) > 1 and revs[0] > revs[1]:
591 594 revs.reverse()
592 595 for rev in revs:
593 596 if appliedbase >= len(self.applied):
594 597 raise util.Abort(_("revision %d is not managed") % rev)
595 598
596 599 base = revlog.bin(self.applied[appliedbase].rev)
597 600 node = repo.changelog.node(rev)
598 601 if node != base:
599 602 raise util.Abort(_("cannot delete revision %d above "
600 603 "applied patches") % rev)
601 604 realpatches.append(self.applied[appliedbase].name)
602 605 appliedbase += 1
603 606
604 607 if not opts.get('keep'):
605 608 r = self.qrepo()
606 609 if r:
607 610 r.remove(realpatches, True)
608 611 else:
609 612 for p in realpatches:
610 613 os.unlink(self.join(p))
611 614
612 615 if appliedbase:
613 616 del self.applied[:appliedbase]
614 617 self.applied_dirty = 1
615 618 self._clean_series(realpatches)
616 619
617 620 def check_toppatch(self, repo):
618 621 if len(self.applied) > 0:
619 622 top = revlog.bin(self.applied[-1].rev)
620 623 pp = repo.dirstate.parents()
621 624 if top not in pp:
622 625 raise util.Abort(_("working directory revision is not qtip"))
623 626 return top
624 627 return None
625 628 def check_localchanges(self, repo, force=False, refresh=True):
626 629 m, a, r, d = repo.status()[:4]
627 630 if m or a or r or d:
628 631 if not force:
629 632 if refresh:
630 633 raise util.Abort(_("local changes found, refresh first"))
631 634 else:
632 635 raise util.Abort(_("local changes found"))
633 636 return m, a, r, d
634 637
635 638 _reserved = ('series', 'status', 'guards')
636 639 def check_reserved_name(self, name):
637 640 if (name in self._reserved or name.startswith('.hg')
638 641 or name.startswith('.mq')):
639 642 raise util.Abort(_('"%s" cannot be used as the name of a patch')
640 643 % name)
641 644
642 645 def new(self, repo, patchfn, *pats, **opts):
643 646 """options:
644 647 msg: a string or a no-argument function returning a string
645 648 """
646 649 msg = opts.get('msg')
647 650 force = opts.get('force')
648 651 user = opts.get('user')
649 652 date = opts.get('date')
650 653 if date:
651 654 date = util.parsedate(date)
652 655 self.check_reserved_name(patchfn)
653 656 if os.path.exists(self.join(patchfn)):
654 657 raise util.Abort(_('patch "%s" already exists') % patchfn)
655 658 if opts.get('include') or opts.get('exclude') or pats:
656 659 match = cmdutil.match(repo, pats, opts)
657 660 # detect missing files in pats
658 661 def badfn(f, msg):
659 662 raise util.Abort('%s: %s' % (f, msg))
660 663 match.bad = badfn
661 664 m, a, r, d = repo.status(match=match)[:4]
662 665 else:
663 666 m, a, r, d = self.check_localchanges(repo, force)
664 667 match = cmdutil.matchfiles(repo, m + a + r)
665 668 commitfiles = m + a + r
666 669 self.check_toppatch(repo)
667 670 insert = self.full_series_end()
668 671 wlock = repo.wlock()
669 672 try:
670 673 # if patch file write fails, abort early
671 674 p = self.opener(patchfn, "w")
672 675 try:
673 676 if date:
674 677 p.write("# HG changeset patch\n")
675 678 if user:
676 679 p.write("# User " + user + "\n")
677 680 p.write("# Date %d %d\n\n" % date)
678 681 elif user:
679 682 p.write("From: " + user + "\n\n")
680 683
681 684 if callable(msg):
682 685 msg = msg()
683 686 commitmsg = msg and msg or ("[mq]: %s" % patchfn)
684 687 n = repo.commit(commitfiles, commitmsg, user, date, match=match, force=True)
685 688 if n == None:
686 689 raise util.Abort(_("repo commit failed"))
687 690 try:
688 691 self.full_series[insert:insert] = [patchfn]
689 692 self.applied.append(statusentry(revlog.hex(n), patchfn))
690 693 self.parse_series()
691 694 self.series_dirty = 1
692 695 self.applied_dirty = 1
693 696 if msg:
694 697 msg = msg + "\n"
695 698 p.write(msg)
696 699 if commitfiles:
697 700 diffopts = self.diffopts()
698 701 if opts.get('git'): diffopts.git = True
699 702 parent = self.qparents(repo, n)
700 patch.diff(repo, node1=parent, node2=n, fp=p,
701 match=match, opts=diffopts)
703 chunks = patch.diff(repo, node1=parent, node2=n,
704 match=match, opts=diffopts)
705 for chunk in chunks:
706 p.write(chunk)
702 707 p.close()
703 708 wlock = None
704 709 r = self.qrepo()
705 710 if r: r.add([patchfn])
706 711 except:
707 712 repo.rollback()
708 713 raise
709 714 except Exception:
710 715 patchpath = self.join(patchfn)
711 716 try:
712 717 os.unlink(patchpath)
713 718 except:
714 719 self.ui.warn(_('error unlinking %s\n') % patchpath)
715 720 raise
716 721 self.removeundo(repo)
717 722 finally:
718 723 del wlock
719 724
720 725 def strip(self, repo, rev, update=True, backup="all", force=None):
721 726 wlock = lock = None
722 727 try:
723 728 wlock = repo.wlock()
724 729 lock = repo.lock()
725 730
726 731 if update:
727 732 self.check_localchanges(repo, force=force, refresh=False)
728 733 urev = self.qparents(repo, rev)
729 734 hg.clean(repo, urev)
730 735 repo.dirstate.write()
731 736
732 737 self.removeundo(repo)
733 738 repair.strip(self.ui, repo, rev, backup)
734 739 # strip may have unbundled a set of backed up revisions after
735 740 # the actual strip
736 741 self.removeundo(repo)
737 742 finally:
738 743 del lock, wlock
739 744
740 745 def isapplied(self, patch):
741 746 """returns (index, rev, patch)"""
742 747 for i in xrange(len(self.applied)):
743 748 a = self.applied[i]
744 749 if a.name == patch:
745 750 return (i, a.rev, a.name)
746 751 return None
747 752
748 753 # if the exact patch name does not exist, we try a few
749 754 # variations. If strict is passed, we try only #1
750 755 #
751 756 # 1) a number to indicate an offset in the series file
752 757 # 2) a unique substring of the patch name was given
753 758 # 3) patchname[-+]num to indicate an offset in the series file
754 759 def lookup(self, patch, strict=False):
755 760 patch = patch and str(patch)
756 761
757 762 def partial_name(s):
758 763 if s in self.series:
759 764 return s
760 765 matches = [x for x in self.series if s in x]
761 766 if len(matches) > 1:
762 767 self.ui.warn(_('patch name "%s" is ambiguous:\n') % s)
763 768 for m in matches:
764 769 self.ui.warn(' %s\n' % m)
765 770 return None
766 771 if matches:
767 772 return matches[0]
768 773 if len(self.series) > 0 and len(self.applied) > 0:
769 774 if s == 'qtip':
770 775 return self.series[self.series_end(True)-1]
771 776 if s == 'qbase':
772 777 return self.series[0]
773 778 return None
774 779 if patch == None:
775 780 return None
776 781
777 782 # we don't want to return a partial match until we make
778 783 # sure the file name passed in does not exist (checked below)
779 784 res = partial_name(patch)
780 785 if res and res == patch:
781 786 return res
782 787
783 788 if not os.path.isfile(self.join(patch)):
784 789 try:
785 790 sno = int(patch)
786 791 except(ValueError, OverflowError):
787 792 pass
788 793 else:
789 794 if sno < len(self.series):
790 795 return self.series[sno]
791 796 if not strict:
792 797 # return any partial match made above
793 798 if res:
794 799 return res
795 800 minus = patch.rfind('-')
796 801 if minus >= 0:
797 802 res = partial_name(patch[:minus])
798 803 if res:
799 804 i = self.series.index(res)
800 805 try:
801 806 off = int(patch[minus+1:] or 1)
802 807 except(ValueError, OverflowError):
803 808 pass
804 809 else:
805 810 if i - off >= 0:
806 811 return self.series[i - off]
807 812 plus = patch.rfind('+')
808 813 if plus >= 0:
809 814 res = partial_name(patch[:plus])
810 815 if res:
811 816 i = self.series.index(res)
812 817 try:
813 818 off = int(patch[plus+1:] or 1)
814 819 except(ValueError, OverflowError):
815 820 pass
816 821 else:
817 822 if i + off < len(self.series):
818 823 return self.series[i + off]
819 824 raise util.Abort(_("patch %s not in series") % patch)
820 825
821 826 def push(self, repo, patch=None, force=False, list=False,
822 827 mergeq=None):
823 828 wlock = repo.wlock()
824 829 if repo.dirstate.parents()[0] != repo.changelog.tip():
825 830 self.ui.status(_("(working directory not at tip)\n"))
826 831
827 832 try:
828 833 patch = self.lookup(patch)
829 834 # Suppose our series file is: A B C and the current 'top'
830 835 # patch is B. qpush C should be performed (moving forward)
831 836 # qpush B is a NOP (no change) qpush A is an error (can't
832 837 # go backwards with qpush)
833 838 if patch:
834 839 info = self.isapplied(patch)
835 840 if info:
836 841 if info[0] < len(self.applied) - 1:
837 842 raise util.Abort(
838 843 _("cannot push to a previous patch: %s") % patch)
839 844 if info[0] < len(self.series) - 1:
840 845 self.ui.warn(
841 846 _('qpush: %s is already at the top\n') % patch)
842 847 else:
843 848 self.ui.warn(_('all patches are currently applied\n'))
844 849 return
845 850
846 851 # Following the above example, starting at 'top' of B:
847 852 # qpush should be performed (pushes C), but a subsequent
848 853 # qpush without an argument is an error (nothing to
849 854 # apply). This allows a loop of "...while hg qpush..." to
850 855 # work as it detects an error when done
851 856 if self.series_end() == len(self.series):
852 857 self.ui.warn(_('patch series already fully applied\n'))
853 858 return 1
854 859 if not force:
855 860 self.check_localchanges(repo)
856 861
857 862 self.applied_dirty = 1;
858 863 start = self.series_end()
859 864 if start > 0:
860 865 self.check_toppatch(repo)
861 866 if not patch:
862 867 patch = self.series[start]
863 868 end = start + 1
864 869 else:
865 870 end = self.series.index(patch, start) + 1
866 871 s = self.series[start:end]
867 872 all_files = {}
868 873 try:
869 874 if mergeq:
870 875 ret = self.mergepatch(repo, mergeq, s)
871 876 else:
872 877 ret = self.apply(repo, s, list, all_files=all_files)
873 878 except:
874 879 self.ui.warn(_('cleaning up working directory...'))
875 880 node = repo.dirstate.parents()[0]
876 881 hg.revert(repo, node, None)
877 882 unknown = repo.status(unknown=True)[4]
878 883 # only remove unknown files that we know we touched or
879 884 # created while patching
880 885 for f in unknown:
881 886 if f in all_files:
882 887 util.unlink(repo.wjoin(f))
883 888 self.ui.warn(_('done\n'))
884 889 raise
885 890 top = self.applied[-1].name
886 891 if ret[0]:
887 892 self.ui.write(
888 893 "Errors during apply, please fix and refresh %s\n" % top)
889 894 else:
890 895 self.ui.write("Now at: %s\n" % top)
891 896 return ret[0]
892 897 finally:
893 898 del wlock
894 899
895 900 def pop(self, repo, patch=None, force=False, update=True, all=False):
896 901 def getfile(f, rev, flags):
897 902 t = repo.file(f).read(rev)
898 903 repo.wwrite(f, t, flags)
899 904
900 905 wlock = repo.wlock()
901 906 try:
902 907 if patch:
903 908 # index, rev, patch
904 909 info = self.isapplied(patch)
905 910 if not info:
906 911 patch = self.lookup(patch)
907 912 info = self.isapplied(patch)
908 913 if not info:
909 914 raise util.Abort(_("patch %s is not applied") % patch)
910 915
911 916 if len(self.applied) == 0:
912 917 # Allow qpop -a to work repeatedly,
913 918 # but not qpop without an argument
914 919 self.ui.warn(_("no patches applied\n"))
915 920 return not all
916 921
917 922 if not update:
918 923 parents = repo.dirstate.parents()
919 924 rr = [ revlog.bin(x.rev) for x in self.applied ]
920 925 for p in parents:
921 926 if p in rr:
922 927 self.ui.warn(_("qpop: forcing dirstate update\n"))
923 928 update = True
924 929
925 930 if not force and update:
926 931 self.check_localchanges(repo)
927 932
928 933 self.applied_dirty = 1;
929 934 end = len(self.applied)
930 935 if not patch:
931 936 if all:
932 937 popi = 0
933 938 else:
934 939 popi = len(self.applied) - 1
935 940 else:
936 941 popi = info[0] + 1
937 942 if popi >= end:
938 943 self.ui.warn(_("qpop: %s is already at the top\n") % patch)
939 944 return
940 945 info = [ popi ] + [self.applied[popi].rev, self.applied[popi].name]
941 946
942 947 start = info[0]
943 948 rev = revlog.bin(info[1])
944 949
945 950 if update:
946 951 top = self.check_toppatch(repo)
947 952
948 953 if repo.changelog.heads(rev) != [revlog.bin(self.applied[-1].rev)]:
949 954 raise util.Abort(_("popping would remove a revision not "
950 955 "managed by this patch queue"))
951 956
952 957 # we know there are no local changes, so we can make a simplified
953 958 # form of hg.update.
954 959 if update:
955 960 qp = self.qparents(repo, rev)
956 961 changes = repo.changelog.read(qp)
957 962 mmap = repo.manifest.read(changes[0])
958 963 m, a, r, d = repo.status(qp, top)[:4]
959 964 if d:
960 965 raise util.Abort(_("deletions found between repo revs"))
961 966 for f in m:
962 967 getfile(f, mmap[f], mmap.flags(f))
963 968 for f in r:
964 969 getfile(f, mmap[f], mmap.flags(f))
965 970 for f in m + r:
966 971 repo.dirstate.normal(f)
967 972 for f in a:
968 973 try:
969 974 os.unlink(repo.wjoin(f))
970 975 except OSError, e:
971 976 if e.errno != errno.ENOENT:
972 977 raise
973 978 try: os.removedirs(os.path.dirname(repo.wjoin(f)))
974 979 except: pass
975 980 repo.dirstate.forget(f)
976 981 repo.dirstate.setparents(qp, revlog.nullid)
977 982 del self.applied[start:end]
978 983 self.strip(repo, rev, update=False, backup='strip')
979 984 if len(self.applied):
980 985 self.ui.write(_("Now at: %s\n") % self.applied[-1].name)
981 986 else:
982 987 self.ui.write(_("Patch queue now empty\n"))
983 988 finally:
984 989 del wlock
985 990
986 991 def diff(self, repo, pats, opts):
987 992 top = self.check_toppatch(repo)
988 993 if not top:
989 994 self.ui.write(_("No patches applied\n"))
990 995 return
991 996 qp = self.qparents(repo, top)
992 997 self._diffopts = patch.diffopts(self.ui, opts)
993 998 self.printdiff(repo, qp, files=pats, opts=opts)
994 999
995 1000 def refresh(self, repo, pats=None, **opts):
996 1001 if len(self.applied) == 0:
997 1002 self.ui.write(_("No patches applied\n"))
998 1003 return 1
999 1004 newdate = opts.get('date')
1000 1005 if newdate:
1001 1006 newdate = '%d %d' % util.parsedate(newdate)
1002 1007 wlock = repo.wlock()
1003 1008 try:
1004 1009 self.check_toppatch(repo)
1005 1010 (top, patchfn) = (self.applied[-1].rev, self.applied[-1].name)
1006 1011 top = revlog.bin(top)
1007 1012 if repo.changelog.heads(top) != [top]:
1008 1013 raise util.Abort(_("cannot refresh a revision with children"))
1009 1014 cparents = repo.changelog.parents(top)
1010 1015 patchparent = self.qparents(repo, top)
1011 1016 message, comments, user, date, patchfound = self.readheaders(patchfn)
1012 1017
1013 1018 patchf = self.opener(patchfn, 'r+')
1014 1019
1015 1020 # if the patch was a git patch, refresh it as a git patch
1016 1021 for line in patchf:
1017 1022 if line.startswith('diff --git'):
1018 1023 self.diffopts().git = True
1019 1024 break
1020 1025
1021 1026 msg = opts.get('msg', '').rstrip()
1022 1027 if msg and comments:
1023 1028 # Remove existing message, keeping the rest of the comments
1024 1029 # fields.
1025 1030 # If comments contains 'subject: ', message will prepend
1026 1031 # the field and a blank line.
1027 1032 if message:
1028 1033 subj = 'subject: ' + message[0].lower()
1029 1034 for i in xrange(len(comments)):
1030 1035 if subj == comments[i].lower():
1031 1036 del comments[i]
1032 1037 message = message[2:]
1033 1038 break
1034 1039 ci = 0
1035 1040 for mi in xrange(len(message)):
1036 1041 while message[mi] != comments[ci]:
1037 1042 ci += 1
1038 1043 del comments[ci]
1039 1044
1040 1045 def setheaderfield(comments, prefixes, new):
1041 1046 # Update all references to a field in the patch header.
1042 1047 # If none found, add it email style.
1043 1048 res = False
1044 1049 for prefix in prefixes:
1045 1050 for i in xrange(len(comments)):
1046 1051 if comments[i].startswith(prefix):
1047 1052 comments[i] = prefix + new
1048 1053 res = True
1049 1054 break
1050 1055 return res
1051 1056
1052 1057 newuser = opts.get('user')
1053 1058 if newuser:
1054 1059 if not setheaderfield(comments, ['From: ', '# User '], newuser):
1055 1060 try:
1056 1061 patchheaderat = comments.index('# HG changeset patch')
1057 1062 comments.insert(patchheaderat + 1,'# User ' + newuser)
1058 1063 except ValueError:
1059 1064 comments = ['From: ' + newuser, ''] + comments
1060 1065 user = newuser
1061 1066
1062 1067 if newdate:
1063 1068 if setheaderfield(comments, ['# Date '], newdate):
1064 1069 date = newdate
1065 1070
1066 1071 if msg:
1067 1072 comments.append(msg)
1068 1073
1069 1074 patchf.seek(0)
1070 1075 patchf.truncate()
1071 1076
1072 1077 if comments:
1073 1078 comments = "\n".join(comments) + '\n\n'
1074 1079 patchf.write(comments)
1075 1080
1076 1081 if opts.get('git'):
1077 1082 self.diffopts().git = True
1078 1083 tip = repo.changelog.tip()
1079 1084 if top == tip:
1080 1085 # if the top of our patch queue is also the tip, there is an
1081 1086 # optimization here. We update the dirstate in place and strip
1082 1087 # off the tip commit. Then just commit the current directory
1083 1088 # tree. We can also send repo.commit the list of files
1084 1089 # changed to speed up the diff
1085 1090 #
1086 1091 # in short mode, we only diff the files included in the
1087 1092 # patch already plus specified files
1088 1093 #
1089 1094 # this should really read:
1090 1095 # mm, dd, aa, aa2 = repo.status(tip, patchparent)[:4]
1091 1096 # but we do it backwards to take advantage of manifest/chlog
1092 1097 # caching against the next repo.status call
1093 1098 #
1094 1099 mm, aa, dd, aa2 = repo.status(patchparent, tip)[:4]
1095 1100 changes = repo.changelog.read(tip)
1096 1101 man = repo.manifest.read(changes[0])
1097 1102 aaa = aa[:]
1098 1103 matchfn = cmdutil.match(repo, pats, opts)
1099 1104 if opts.get('short'):
1100 1105 # if amending a patch, we start with existing
1101 1106 # files plus specified files - unfiltered
1102 1107 match = cmdutil.matchfiles(repo, mm + aa + dd + matchfn.files())
1103 1108 # filter with inc/exl options
1104 1109 matchfn = cmdutil.match(repo, opts=opts)
1105 1110 else:
1106 1111 match = cmdutil.matchall(repo)
1107 1112 m, a, r, d = repo.status(match=match)[:4]
1108 1113
1109 1114 # we might end up with files that were added between
1110 1115 # tip and the dirstate parent, but then changed in the
1111 1116 # local dirstate. in this case, we want them to only
1112 1117 # show up in the added section
1113 1118 for x in m:
1114 1119 if x not in aa:
1115 1120 mm.append(x)
1116 1121 # we might end up with files added by the local dirstate that
1117 1122 # were deleted by the patch. In this case, they should only
1118 1123 # show up in the changed section.
1119 1124 for x in a:
1120 1125 if x in dd:
1121 1126 del dd[dd.index(x)]
1122 1127 mm.append(x)
1123 1128 else:
1124 1129 aa.append(x)
1125 1130 # make sure any files deleted in the local dirstate
1126 1131 # are not in the add or change column of the patch
1127 1132 forget = []
1128 1133 for x in d + r:
1129 1134 if x in aa:
1130 1135 del aa[aa.index(x)]
1131 1136 forget.append(x)
1132 1137 continue
1133 1138 elif x in mm:
1134 1139 del mm[mm.index(x)]
1135 1140 dd.append(x)
1136 1141
1137 1142 m = util.unique(mm)
1138 1143 r = util.unique(dd)
1139 1144 a = util.unique(aa)
1140 1145 c = [filter(matchfn, l) for l in (m, a, r)]
1141 1146 match = cmdutil.matchfiles(repo, util.unique(c[0] + c[1] + c[2]))
1142 patch.diff(repo, patchparent, match=match,
1143 fp=patchf, changes=c, opts=self.diffopts())
1147 chunks = patch.diff(repo, patchparent, match=match,
1148 changes=c, opts=self.diffopts())
1149 for chunk in chunks:
1150 patchf.write(chunk)
1144 1151 patchf.close()
1145 1152
1146 1153 repo.dirstate.setparents(*cparents)
1147 1154 copies = {}
1148 1155 for dst in a:
1149 1156 src = repo.dirstate.copied(dst)
1150 1157 if src is not None:
1151 1158 copies.setdefault(src, []).append(dst)
1152 1159 repo.dirstate.add(dst)
1153 1160 # remember the copies between patchparent and tip
1154 1161 # this may be slow, so don't do it if we're not tracking copies
1155 1162 if self.diffopts().git:
1156 1163 for dst in aaa:
1157 1164 f = repo.file(dst)
1158 1165 src = f.renamed(man[dst])
1159 1166 if src:
1160 1167 copies.setdefault(src[0], []).extend(copies.get(dst, []))
1161 1168 if dst in a:
1162 1169 copies[src[0]].append(dst)
1163 1170 # we can't copy a file created by the patch itself
1164 1171 if dst in copies:
1165 1172 del copies[dst]
1166 1173 for src, dsts in copies.iteritems():
1167 1174 for dst in dsts:
1168 1175 repo.dirstate.copy(src, dst)
1169 1176 for f in r:
1170 1177 repo.dirstate.remove(f)
1171 1178 # if the patch excludes a modified file, mark that
1172 1179 # file with mtime=0 so status can see it.
1173 1180 mm = []
1174 1181 for i in xrange(len(m)-1, -1, -1):
1175 1182 if not matchfn(m[i]):
1176 1183 mm.append(m[i])
1177 1184 del m[i]
1178 1185 for f in m:
1179 1186 repo.dirstate.normal(f)
1180 1187 for f in mm:
1181 1188 repo.dirstate.normallookup(f)
1182 1189 for f in forget:
1183 1190 repo.dirstate.forget(f)
1184 1191
1185 1192 if not msg:
1186 1193 if not message:
1187 1194 message = "[mq]: %s\n" % patchfn
1188 1195 else:
1189 1196 message = "\n".join(message)
1190 1197 else:
1191 1198 message = msg
1192 1199
1193 1200 if not user:
1194 1201 user = changes[1]
1195 1202
1196 1203 self.applied.pop()
1197 1204 self.applied_dirty = 1
1198 1205 self.strip(repo, top, update=False,
1199 1206 backup='strip')
1200 1207 n = repo.commit(match.files(), message, user, date, match=match,
1201 1208 force=1)
1202 1209 self.applied.append(statusentry(revlog.hex(n), patchfn))
1203 1210 self.removeundo(repo)
1204 1211 else:
1205 1212 self.printdiff(repo, patchparent, fp=patchf)
1206 1213 patchf.close()
1207 1214 added = repo.status()[1]
1208 1215 for a in added:
1209 1216 f = repo.wjoin(a)
1210 1217 try:
1211 1218 os.unlink(f)
1212 1219 except OSError, e:
1213 1220 if e.errno != errno.ENOENT:
1214 1221 raise
1215 1222 try: os.removedirs(os.path.dirname(f))
1216 1223 except: pass
1217 1224 # forget the file copies in the dirstate
1218 1225 # push should readd the files later on
1219 1226 repo.dirstate.forget(a)
1220 1227 self.pop(repo, force=True)
1221 1228 self.push(repo, force=True)
1222 1229 finally:
1223 1230 del wlock
1224 1231
1225 1232 def init(self, repo, create=False):
1226 1233 if not create and os.path.isdir(self.path):
1227 1234 raise util.Abort(_("patch queue directory already exists"))
1228 1235 try:
1229 1236 os.mkdir(self.path)
1230 1237 except OSError, inst:
1231 1238 if inst.errno != errno.EEXIST or not create:
1232 1239 raise
1233 1240 if create:
1234 1241 return self.qrepo(create=True)
1235 1242
1236 1243 def unapplied(self, repo, patch=None):
1237 1244 if patch and patch not in self.series:
1238 1245 raise util.Abort(_("patch %s is not in series file") % patch)
1239 1246 if not patch:
1240 1247 start = self.series_end()
1241 1248 else:
1242 1249 start = self.series.index(patch) + 1
1243 1250 unapplied = []
1244 1251 for i in xrange(start, len(self.series)):
1245 1252 pushable, reason = self.pushable(i)
1246 1253 if pushable:
1247 1254 unapplied.append((i, self.series[i]))
1248 1255 self.explain_pushable(i)
1249 1256 return unapplied
1250 1257
1251 1258 def qseries(self, repo, missing=None, start=0, length=None, status=None,
1252 1259 summary=False):
1253 1260 def displayname(patchname):
1254 1261 if summary:
1255 1262 msg = self.readheaders(patchname)[0]
1256 1263 msg = msg and ': ' + msg[0] or ': '
1257 1264 else:
1258 1265 msg = ''
1259 1266 return '%s%s' % (patchname, msg)
1260 1267
1261 1268 applied = dict.fromkeys([p.name for p in self.applied])
1262 1269 if length is None:
1263 1270 length = len(self.series) - start
1264 1271 if not missing:
1265 1272 for i in xrange(start, start+length):
1266 1273 patch = self.series[i]
1267 1274 if patch in applied:
1268 1275 stat = 'A'
1269 1276 elif self.pushable(i)[0]:
1270 1277 stat = 'U'
1271 1278 else:
1272 1279 stat = 'G'
1273 1280 pfx = ''
1274 1281 if self.ui.verbose:
1275 1282 pfx = '%d %s ' % (i, stat)
1276 1283 elif status and status != stat:
1277 1284 continue
1278 1285 self.ui.write('%s%s\n' % (pfx, displayname(patch)))
1279 1286 else:
1280 1287 msng_list = []
1281 1288 for root, dirs, files in os.walk(self.path):
1282 1289 d = root[len(self.path) + 1:]
1283 1290 for f in files:
1284 1291 fl = os.path.join(d, f)
1285 1292 if (fl not in self.series and
1286 1293 fl not in (self.status_path, self.series_path,
1287 1294 self.guards_path)
1288 1295 and not fl.startswith('.')):
1289 1296 msng_list.append(fl)
1290 1297 for x in util.sort(msng_list):
1291 1298 pfx = self.ui.verbose and ('D ') or ''
1292 1299 self.ui.write("%s%s\n" % (pfx, displayname(x)))
1293 1300
1294 1301 def issaveline(self, l):
1295 1302 if l.name == '.hg.patches.save.line':
1296 1303 return True
1297 1304
1298 1305 def qrepo(self, create=False):
1299 1306 if create or os.path.isdir(self.join(".hg")):
1300 1307 return hg.repository(self.ui, path=self.path, create=create)
1301 1308
1302 1309 def restore(self, repo, rev, delete=None, qupdate=None):
1303 1310 c = repo.changelog.read(rev)
1304 1311 desc = c[4].strip()
1305 1312 lines = desc.splitlines()
1306 1313 i = 0
1307 1314 datastart = None
1308 1315 series = []
1309 1316 applied = []
1310 1317 qpp = None
1311 1318 for i in xrange(0, len(lines)):
1312 1319 if lines[i] == 'Patch Data:':
1313 1320 datastart = i + 1
1314 1321 elif lines[i].startswith('Dirstate:'):
1315 1322 l = lines[i].rstrip()
1316 1323 l = l[10:].split(' ')
1317 1324 qpp = [ bin(x) for x in l ]
1318 1325 elif datastart != None:
1319 1326 l = lines[i].rstrip()
1320 1327 se = statusentry(l)
1321 1328 file_ = se.name
1322 1329 if se.rev:
1323 1330 applied.append(se)
1324 1331 else:
1325 1332 series.append(file_)
1326 1333 if datastart == None:
1327 1334 self.ui.warn(_("No saved patch data found\n"))
1328 1335 return 1
1329 1336 self.ui.warn(_("restoring status: %s\n") % lines[0])
1330 1337 self.full_series = series
1331 1338 self.applied = applied
1332 1339 self.parse_series()
1333 1340 self.series_dirty = 1
1334 1341 self.applied_dirty = 1
1335 1342 heads = repo.changelog.heads()
1336 1343 if delete:
1337 1344 if rev not in heads:
1338 1345 self.ui.warn(_("save entry has children, leaving it alone\n"))
1339 1346 else:
1340 1347 self.ui.warn(_("removing save entry %s\n") % short(rev))
1341 1348 pp = repo.dirstate.parents()
1342 1349 if rev in pp:
1343 1350 update = True
1344 1351 else:
1345 1352 update = False
1346 1353 self.strip(repo, rev, update=update, backup='strip')
1347 1354 if qpp:
1348 1355 self.ui.warn(_("saved queue repository parents: %s %s\n") %
1349 1356 (short(qpp[0]), short(qpp[1])))
1350 1357 if qupdate:
1351 1358 self.ui.status(_("queue directory updating\n"))
1352 1359 r = self.qrepo()
1353 1360 if not r:
1354 1361 self.ui.warn(_("Unable to load queue repository\n"))
1355 1362 return 1
1356 1363 hg.clean(r, qpp[0])
1357 1364
1358 1365 def save(self, repo, msg=None):
1359 1366 if len(self.applied) == 0:
1360 1367 self.ui.warn(_("save: no patches applied, exiting\n"))
1361 1368 return 1
1362 1369 if self.issaveline(self.applied[-1]):
1363 1370 self.ui.warn(_("status is already saved\n"))
1364 1371 return 1
1365 1372
1366 1373 ar = [ ':' + x for x in self.full_series ]
1367 1374 if not msg:
1368 1375 msg = _("hg patches saved state")
1369 1376 else:
1370 1377 msg = "hg patches: " + msg.rstrip('\r\n')
1371 1378 r = self.qrepo()
1372 1379 if r:
1373 1380 pp = r.dirstate.parents()
1374 1381 msg += "\nDirstate: %s %s" % (hex(pp[0]), hex(pp[1]))
1375 1382 msg += "\n\nPatch Data:\n"
1376 1383 text = msg + "\n".join([str(x) for x in self.applied]) + '\n' + (ar and
1377 1384 "\n".join(ar) + '\n' or "")
1378 1385 n = repo.commit(None, text, user=None, force=1)
1379 1386 if not n:
1380 1387 self.ui.warn(_("repo commit failed\n"))
1381 1388 return 1
1382 1389 self.applied.append(statusentry(revlog.hex(n),'.hg.patches.save.line'))
1383 1390 self.applied_dirty = 1
1384 1391 self.removeundo(repo)
1385 1392
1386 1393 def full_series_end(self):
1387 1394 if len(self.applied) > 0:
1388 1395 p = self.applied[-1].name
1389 1396 end = self.find_series(p)
1390 1397 if end == None:
1391 1398 return len(self.full_series)
1392 1399 return end + 1
1393 1400 return 0
1394 1401
1395 1402 def series_end(self, all_patches=False):
1396 1403 """If all_patches is False, return the index of the next pushable patch
1397 1404 in the series, or the series length. If all_patches is True, return the
1398 1405 index of the first patch past the last applied one.
1399 1406 """
1400 1407 end = 0
1401 1408 def next(start):
1402 1409 if all_patches:
1403 1410 return start
1404 1411 i = start
1405 1412 while i < len(self.series):
1406 1413 p, reason = self.pushable(i)
1407 1414 if p:
1408 1415 break
1409 1416 self.explain_pushable(i)
1410 1417 i += 1
1411 1418 return i
1412 1419 if len(self.applied) > 0:
1413 1420 p = self.applied[-1].name
1414 1421 try:
1415 1422 end = self.series.index(p)
1416 1423 except ValueError:
1417 1424 return 0
1418 1425 return next(end + 1)
1419 1426 return next(end)
1420 1427
1421 1428 def appliedname(self, index):
1422 1429 pname = self.applied[index].name
1423 1430 if not self.ui.verbose:
1424 1431 p = pname
1425 1432 else:
1426 1433 p = str(self.series.index(pname)) + " " + pname
1427 1434 return p
1428 1435
1429 1436 def qimport(self, repo, files, patchname=None, rev=None, existing=None,
1430 1437 force=None, git=False):
1431 1438 def checkseries(patchname):
1432 1439 if patchname in self.series:
1433 1440 raise util.Abort(_('patch %s is already in the series file')
1434 1441 % patchname)
1435 1442 def checkfile(patchname):
1436 1443 if not force and os.path.exists(self.join(patchname)):
1437 1444 raise util.Abort(_('patch "%s" already exists')
1438 1445 % patchname)
1439 1446
1440 1447 if rev:
1441 1448 if files:
1442 1449 raise util.Abort(_('option "-r" not valid when importing '
1443 1450 'files'))
1444 1451 rev = cmdutil.revrange(repo, rev)
1445 1452 rev.sort(lambda x, y: cmp(y, x))
1446 1453 if (len(files) > 1 or len(rev) > 1) and patchname:
1447 1454 raise util.Abort(_('option "-n" not valid when importing multiple '
1448 1455 'patches'))
1449 1456 i = 0
1450 1457 added = []
1451 1458 if rev:
1452 1459 # If mq patches are applied, we can only import revisions
1453 1460 # that form a linear path to qbase.
1454 1461 # Otherwise, they should form a linear path to a head.
1455 1462 heads = repo.changelog.heads(repo.changelog.node(rev[-1]))
1456 1463 if len(heads) > 1:
1457 1464 raise util.Abort(_('revision %d is the root of more than one '
1458 1465 'branch') % rev[-1])
1459 1466 if self.applied:
1460 1467 base = revlog.hex(repo.changelog.node(rev[0]))
1461 1468 if base in [n.rev for n in self.applied]:
1462 1469 raise util.Abort(_('revision %d is already managed')
1463 1470 % rev[0])
1464 1471 if heads != [revlog.bin(self.applied[-1].rev)]:
1465 1472 raise util.Abort(_('revision %d is not the parent of '
1466 1473 'the queue') % rev[0])
1467 1474 base = repo.changelog.rev(revlog.bin(self.applied[0].rev))
1468 1475 lastparent = repo.changelog.parentrevs(base)[0]
1469 1476 else:
1470 1477 if heads != [repo.changelog.node(rev[0])]:
1471 1478 raise util.Abort(_('revision %d has unmanaged children')
1472 1479 % rev[0])
1473 1480 lastparent = None
1474 1481
1475 1482 if git:
1476 1483 self.diffopts().git = True
1477 1484
1478 1485 for r in rev:
1479 1486 p1, p2 = repo.changelog.parentrevs(r)
1480 1487 n = repo.changelog.node(r)
1481 1488 if p2 != revlog.nullrev:
1482 1489 raise util.Abort(_('cannot import merge revision %d') % r)
1483 1490 if lastparent and lastparent != r:
1484 1491 raise util.Abort(_('revision %d is not the parent of %d')
1485 1492 % (r, lastparent))
1486 1493 lastparent = p1
1487 1494
1488 1495 if not patchname:
1489 1496 patchname = normname('%d.diff' % r)
1490 1497 self.check_reserved_name(patchname)
1491 1498 checkseries(patchname)
1492 1499 checkfile(patchname)
1493 1500 self.full_series.insert(0, patchname)
1494 1501
1495 1502 patchf = self.opener(patchname, "w")
1496 1503 patch.export(repo, [n], fp=patchf, opts=self.diffopts())
1497 1504 patchf.close()
1498 1505
1499 1506 se = statusentry(revlog.hex(n), patchname)
1500 1507 self.applied.insert(0, se)
1501 1508
1502 1509 added.append(patchname)
1503 1510 patchname = None
1504 1511 self.parse_series()
1505 1512 self.applied_dirty = 1
1506 1513
1507 1514 for filename in files:
1508 1515 if existing:
1509 1516 if filename == '-':
1510 1517 raise util.Abort(_('-e is incompatible with import from -'))
1511 1518 if not patchname:
1512 1519 patchname = normname(filename)
1513 1520 self.check_reserved_name(patchname)
1514 1521 if not os.path.isfile(self.join(patchname)):
1515 1522 raise util.Abort(_("patch %s does not exist") % patchname)
1516 1523 else:
1517 1524 try:
1518 1525 if filename == '-':
1519 1526 if not patchname:
1520 1527 raise util.Abort(_('need --name to import a patch from -'))
1521 1528 text = sys.stdin.read()
1522 1529 else:
1523 1530 text = url.open(self.ui, filename).read()
1524 1531 except IOError:
1525 1532 raise util.Abort(_("unable to read %s") % filename)
1526 1533 if not patchname:
1527 1534 patchname = normname(os.path.basename(filename))
1528 1535 self.check_reserved_name(patchname)
1529 1536 checkfile(patchname)
1530 1537 patchf = self.opener(patchname, "w")
1531 1538 patchf.write(text)
1532 1539 if not force:
1533 1540 checkseries(patchname)
1534 1541 if patchname not in self.series:
1535 1542 index = self.full_series_end() + i
1536 1543 self.full_series[index:index] = [patchname]
1537 1544 self.parse_series()
1538 1545 self.ui.warn("adding %s to series file\n" % patchname)
1539 1546 i += 1
1540 1547 added.append(patchname)
1541 1548 patchname = None
1542 1549 self.series_dirty = 1
1543 1550 qrepo = self.qrepo()
1544 1551 if qrepo:
1545 1552 qrepo.add(added)
1546 1553
1547 1554 def delete(ui, repo, *patches, **opts):
1548 1555 """remove patches from queue
1549 1556
1550 1557 The patches must not be applied, unless they are arguments to
1551 1558 the --rev parameter. At least one patch or revision is required.
1552 1559
1553 1560 With --rev, mq will stop managing the named revisions (converting
1554 1561 them to regular mercurial changesets). The qfinish command should be
1555 1562 used as an alternative for qdel -r, as the latter option is deprecated.
1556 1563
1557 1564 With --keep, the patch files are preserved in the patch directory."""
1558 1565 q = repo.mq
1559 1566 q.delete(repo, patches, opts)
1560 1567 q.save_dirty()
1561 1568 return 0
1562 1569
1563 1570 def applied(ui, repo, patch=None, **opts):
1564 1571 """print the patches already applied"""
1565 1572 q = repo.mq
1566 1573 if patch:
1567 1574 if patch not in q.series:
1568 1575 raise util.Abort(_("patch %s is not in series file") % patch)
1569 1576 end = q.series.index(patch) + 1
1570 1577 else:
1571 1578 end = q.series_end(True)
1572 1579 return q.qseries(repo, length=end, status='A', summary=opts.get('summary'))
1573 1580
1574 1581 def unapplied(ui, repo, patch=None, **opts):
1575 1582 """print the patches not yet applied"""
1576 1583 q = repo.mq
1577 1584 if patch:
1578 1585 if patch not in q.series:
1579 1586 raise util.Abort(_("patch %s is not in series file") % patch)
1580 1587 start = q.series.index(patch) + 1
1581 1588 else:
1582 1589 start = q.series_end(True)
1583 1590 q.qseries(repo, start=start, status='U', summary=opts.get('summary'))
1584 1591
1585 1592 def qimport(ui, repo, *filename, **opts):
1586 1593 """import a patch
1587 1594
1588 1595 The patch is inserted into the series after the last applied patch.
1589 1596 If no patches have been applied, qimport prepends the patch
1590 1597 to the series.
1591 1598
1592 1599 The patch will have the same name as its source file unless you
1593 1600 give it a new one with --name.
1594 1601
1595 1602 You can register an existing patch inside the patch directory
1596 1603 with the --existing flag.
1597 1604
1598 1605 With --force, an existing patch of the same name will be overwritten.
1599 1606
1600 1607 An existing changeset may be placed under mq control with --rev
1601 1608 (e.g. qimport --rev tip -n patch will place tip under mq control).
1602 1609 With --git, patches imported with --rev will use the git diff
1603 1610 format. See the gitdiffs help topic for information on why this is
1604 1611 important for preserving rename/copy information and permission changes.
1605 1612 """
1606 1613 q = repo.mq
1607 1614 q.qimport(repo, filename, patchname=opts['name'],
1608 1615 existing=opts['existing'], force=opts['force'], rev=opts['rev'],
1609 1616 git=opts['git'])
1610 1617 q.save_dirty()
1611 1618 return 0
1612 1619
1613 1620 def init(ui, repo, **opts):
1614 1621 """init a new queue repository
1615 1622
1616 1623 The queue repository is unversioned by default. If -c is
1617 1624 specified, qinit will create a separate nested repository
1618 1625 for patches (qinit -c may also be run later to convert
1619 1626 an unversioned patch repository into a versioned one).
1620 1627 You can use qcommit to commit changes to this queue repository."""
1621 1628 q = repo.mq
1622 1629 r = q.init(repo, create=opts['create_repo'])
1623 1630 q.save_dirty()
1624 1631 if r:
1625 1632 if not os.path.exists(r.wjoin('.hgignore')):
1626 1633 fp = r.wopener('.hgignore', 'w')
1627 1634 fp.write('^\\.hg\n')
1628 1635 fp.write('^\\.mq\n')
1629 1636 fp.write('syntax: glob\n')
1630 1637 fp.write('status\n')
1631 1638 fp.write('guards\n')
1632 1639 fp.close()
1633 1640 if not os.path.exists(r.wjoin('series')):
1634 1641 r.wopener('series', 'w').close()
1635 1642 r.add(['.hgignore', 'series'])
1636 1643 commands.add(ui, r)
1637 1644 return 0
1638 1645
1639 1646 def clone(ui, source, dest=None, **opts):
1640 1647 '''clone main and patch repository at same time
1641 1648
1642 1649 If source is local, destination will have no patches applied. If
1643 1650 source is remote, this command can not check if patches are
1644 1651 applied in source, so cannot guarantee that patches are not
1645 1652 applied in destination. If you clone remote repository, be sure
1646 1653 before that it has no patches applied.
1647 1654
1648 1655 Source patch repository is looked for in <src>/.hg/patches by
1649 1656 default. Use -p <url> to change.
1650 1657
1651 1658 The patch directory must be a nested mercurial repository, as
1652 1659 would be created by qinit -c.
1653 1660 '''
1654 1661 def patchdir(repo):
1655 1662 url = repo.url()
1656 1663 if url.endswith('/'):
1657 1664 url = url[:-1]
1658 1665 return url + '/.hg/patches'
1659 1666 cmdutil.setremoteconfig(ui, opts)
1660 1667 if dest is None:
1661 1668 dest = hg.defaultdest(source)
1662 1669 sr = hg.repository(ui, ui.expandpath(source))
1663 1670 patchespath = opts['patches'] or patchdir(sr)
1664 1671 try:
1665 1672 pr = hg.repository(ui, patchespath)
1666 1673 except RepoError:
1667 1674 raise util.Abort(_('versioned patch repository not found'
1668 1675 ' (see qinit -c)'))
1669 1676 qbase, destrev = None, None
1670 1677 if sr.local():
1671 1678 if sr.mq.applied:
1672 1679 qbase = revlog.bin(sr.mq.applied[0].rev)
1673 1680 if not hg.islocal(dest):
1674 1681 heads = dict.fromkeys(sr.heads())
1675 1682 for h in sr.heads(qbase):
1676 1683 del heads[h]
1677 1684 destrev = heads.keys()
1678 1685 destrev.append(sr.changelog.parents(qbase)[0])
1679 1686 elif sr.capable('lookup'):
1680 1687 try:
1681 1688 qbase = sr.lookup('qbase')
1682 1689 except RepoError:
1683 1690 pass
1684 1691 ui.note(_('cloning main repo\n'))
1685 1692 sr, dr = hg.clone(ui, sr.url(), dest,
1686 1693 pull=opts['pull'],
1687 1694 rev=destrev,
1688 1695 update=False,
1689 1696 stream=opts['uncompressed'])
1690 1697 ui.note(_('cloning patch repo\n'))
1691 1698 spr, dpr = hg.clone(ui, opts['patches'] or patchdir(sr), patchdir(dr),
1692 1699 pull=opts['pull'], update=not opts['noupdate'],
1693 1700 stream=opts['uncompressed'])
1694 1701 if dr.local():
1695 1702 if qbase:
1696 1703 ui.note(_('stripping applied patches from destination repo\n'))
1697 1704 dr.mq.strip(dr, qbase, update=False, backup=None)
1698 1705 if not opts['noupdate']:
1699 1706 ui.note(_('updating destination repo\n'))
1700 1707 hg.update(dr, dr.changelog.tip())
1701 1708
1702 1709 def commit(ui, repo, *pats, **opts):
1703 1710 """commit changes in the queue repository"""
1704 1711 q = repo.mq
1705 1712 r = q.qrepo()
1706 1713 if not r: raise util.Abort('no queue repository')
1707 1714 commands.commit(r.ui, r, *pats, **opts)
1708 1715
1709 1716 def series(ui, repo, **opts):
1710 1717 """print the entire series file"""
1711 1718 repo.mq.qseries(repo, missing=opts['missing'], summary=opts['summary'])
1712 1719 return 0
1713 1720
1714 1721 def top(ui, repo, **opts):
1715 1722 """print the name of the current patch"""
1716 1723 q = repo.mq
1717 1724 t = q.applied and q.series_end(True) or 0
1718 1725 if t:
1719 1726 return q.qseries(repo, start=t-1, length=1, status='A',
1720 1727 summary=opts.get('summary'))
1721 1728 else:
1722 1729 ui.write("No patches applied\n")
1723 1730 return 1
1724 1731
1725 1732 def next(ui, repo, **opts):
1726 1733 """print the name of the next patch"""
1727 1734 q = repo.mq
1728 1735 end = q.series_end()
1729 1736 if end == len(q.series):
1730 1737 ui.write("All patches applied\n")
1731 1738 return 1
1732 1739 return q.qseries(repo, start=end, length=1, summary=opts.get('summary'))
1733 1740
1734 1741 def prev(ui, repo, **opts):
1735 1742 """print the name of the previous patch"""
1736 1743 q = repo.mq
1737 1744 l = len(q.applied)
1738 1745 if l == 1:
1739 1746 ui.write("Only one patch applied\n")
1740 1747 return 1
1741 1748 if not l:
1742 1749 ui.write("No patches applied\n")
1743 1750 return 1
1744 1751 return q.qseries(repo, start=l-2, length=1, status='A',
1745 1752 summary=opts.get('summary'))
1746 1753
1747 1754 def setupheaderopts(ui, opts):
1748 1755 def do(opt,val):
1749 1756 if not opts[opt] and opts['current' + opt]:
1750 1757 opts[opt] = val
1751 1758 do('user', ui.username())
1752 1759 do('date', "%d %d" % util.makedate())
1753 1760
1754 1761 def new(ui, repo, patch, *args, **opts):
1755 1762 """create a new patch
1756 1763
1757 1764 qnew creates a new patch on top of the currently-applied patch (if any).
1758 1765 It will refuse to run if there are any outstanding changes unless -f is
1759 1766 specified, in which case the patch will be initialized with them. You
1760 1767 may also use -I, -X, and/or a list of files after the patch name to add
1761 1768 only changes to matching files to the new patch, leaving the rest as
1762 1769 uncommitted modifications.
1763 1770
1764 1771 -u and -d can be used to set the (given) user and date, respectively.
1765 1772 -U and -D set user to current user and date to current date.
1766 1773
1767 1774 -e, -m or -l set the patch header as well as the commit message. If none
1768 1775 is specified, the header is empty and the commit message is '[mq]: PATCH'.
1769 1776
1770 1777 Use the --git option to keep the patch in the git extended diff format.
1771 1778 Read the gitdiffs help topic for more information on why this is
1772 1779 important for preserving permission changes and copy/rename information.
1773 1780 """
1774 1781 msg = cmdutil.logmessage(opts)
1775 1782 def getmsg(): return ui.edit(msg, ui.username())
1776 1783 q = repo.mq
1777 1784 opts['msg'] = msg
1778 1785 if opts.get('edit'):
1779 1786 opts['msg'] = getmsg
1780 1787 else:
1781 1788 opts['msg'] = msg
1782 1789 setupheaderopts(ui, opts)
1783 1790 q.new(repo, patch, *args, **opts)
1784 1791 q.save_dirty()
1785 1792 return 0
1786 1793
1787 1794 def refresh(ui, repo, *pats, **opts):
1788 1795 """update the current patch
1789 1796
1790 1797 If any file patterns are provided, the refreshed patch will contain only
1791 1798 the modifications that match those patterns; the remaining modifications
1792 1799 will remain in the working directory.
1793 1800
1794 1801 If --short is specified, files currently included in the patch will
1795 1802 be refreshed just like matched files and remain in the patch.
1796 1803
1797 1804 hg add/remove/copy/rename work as usual, though you might want to use
1798 1805 git-style patches (--git or [diff] git=1) to track copies and renames.
1799 1806 See the gitdiffs help topic for more information on the git diff format.
1800 1807 """
1801 1808 q = repo.mq
1802 1809 message = cmdutil.logmessage(opts)
1803 1810 if opts['edit']:
1804 1811 if not q.applied:
1805 1812 ui.write(_("No patches applied\n"))
1806 1813 return 1
1807 1814 if message:
1808 1815 raise util.Abort(_('option "-e" incompatible with "-m" or "-l"'))
1809 1816 patch = q.applied[-1].name
1810 1817 (message, comment, user, date, hasdiff) = q.readheaders(patch)
1811 1818 message = ui.edit('\n'.join(message), user or ui.username())
1812 1819 setupheaderopts(ui, opts)
1813 1820 ret = q.refresh(repo, pats, msg=message, **opts)
1814 1821 q.save_dirty()
1815 1822 return ret
1816 1823
1817 1824 def diff(ui, repo, *pats, **opts):
1818 1825 """diff of the current patch and subsequent modifications
1819 1826
1820 1827 Shows a diff which includes the current patch as well as any changes which
1821 1828 have been made in the working directory since the last refresh (thus
1822 1829 showing what the current patch would become after a qrefresh).
1823 1830
1824 1831 Use 'hg diff' if you only want to see the changes made since the last
1825 1832 qrefresh, or 'hg export qtip' if you want to see changes made by the
1826 1833 current patch without including changes made since the qrefresh.
1827 1834 """
1828 1835 repo.mq.diff(repo, pats, opts)
1829 1836 return 0
1830 1837
1831 1838 def fold(ui, repo, *files, **opts):
1832 1839 """fold the named patches into the current patch
1833 1840
1834 1841 Patches must not yet be applied. Each patch will be successively
1835 1842 applied to the current patch in the order given. If all the
1836 1843 patches apply successfully, the current patch will be refreshed
1837 1844 with the new cumulative patch, and the folded patches will
1838 1845 be deleted. With -k/--keep, the folded patch files will not
1839 1846 be removed afterwards.
1840 1847
1841 1848 The header for each folded patch will be concatenated with
1842 1849 the current patch header, separated by a line of '* * *'."""
1843 1850
1844 1851 q = repo.mq
1845 1852
1846 1853 if not files:
1847 1854 raise util.Abort(_('qfold requires at least one patch name'))
1848 1855 if not q.check_toppatch(repo):
1849 1856 raise util.Abort(_('No patches applied'))
1850 1857
1851 1858 message = cmdutil.logmessage(opts)
1852 1859 if opts['edit']:
1853 1860 if message:
1854 1861 raise util.Abort(_('option "-e" incompatible with "-m" or "-l"'))
1855 1862
1856 1863 parent = q.lookup('qtip')
1857 1864 patches = []
1858 1865 messages = []
1859 1866 for f in files:
1860 1867 p = q.lookup(f)
1861 1868 if p in patches or p == parent:
1862 1869 ui.warn(_('Skipping already folded patch %s') % p)
1863 1870 if q.isapplied(p):
1864 1871 raise util.Abort(_('qfold cannot fold already applied patch %s') % p)
1865 1872 patches.append(p)
1866 1873
1867 1874 for p in patches:
1868 1875 if not message:
1869 1876 messages.append(q.readheaders(p)[0])
1870 1877 pf = q.join(p)
1871 1878 (patchsuccess, files, fuzz) = q.patch(repo, pf)
1872 1879 if not patchsuccess:
1873 1880 raise util.Abort(_('Error folding patch %s') % p)
1874 1881 patch.updatedir(ui, repo, files)
1875 1882
1876 1883 if not message:
1877 1884 message, comments, user = q.readheaders(parent)[0:3]
1878 1885 for msg in messages:
1879 1886 message.append('* * *')
1880 1887 message.extend(msg)
1881 1888 message = '\n'.join(message)
1882 1889
1883 1890 if opts['edit']:
1884 1891 message = ui.edit(message, user or ui.username())
1885 1892
1886 1893 q.refresh(repo, msg=message)
1887 1894 q.delete(repo, patches, opts)
1888 1895 q.save_dirty()
1889 1896
1890 1897 def goto(ui, repo, patch, **opts):
1891 1898 '''push or pop patches until named patch is at top of stack'''
1892 1899 q = repo.mq
1893 1900 patch = q.lookup(patch)
1894 1901 if q.isapplied(patch):
1895 1902 ret = q.pop(repo, patch, force=opts['force'])
1896 1903 else:
1897 1904 ret = q.push(repo, patch, force=opts['force'])
1898 1905 q.save_dirty()
1899 1906 return ret
1900 1907
1901 1908 def guard(ui, repo, *args, **opts):
1902 1909 '''set or print guards for a patch
1903 1910
1904 1911 Guards control whether a patch can be pushed. A patch with no
1905 1912 guards is always pushed. A patch with a positive guard ("+foo") is
1906 1913 pushed only if the qselect command has activated it. A patch with
1907 1914 a negative guard ("-foo") is never pushed if the qselect command
1908 1915 has activated it.
1909 1916
1910 1917 With no arguments, print the currently active guards.
1911 1918 With arguments, set guards for the named patch.
1912 1919
1913 1920 To set a negative guard "-foo" on topmost patch ("--" is needed so
1914 1921 hg will not interpret "-foo" as an option):
1915 1922 hg qguard -- -foo
1916 1923
1917 1924 To set guards on another patch:
1918 1925 hg qguard other.patch +2.6.17 -stable
1919 1926 '''
1920 1927 def status(idx):
1921 1928 guards = q.series_guards[idx] or ['unguarded']
1922 1929 ui.write('%s: %s\n' % (q.series[idx], ' '.join(guards)))
1923 1930 q = repo.mq
1924 1931 patch = None
1925 1932 args = list(args)
1926 1933 if opts['list']:
1927 1934 if args or opts['none']:
1928 1935 raise util.Abort(_('cannot mix -l/--list with options or arguments'))
1929 1936 for i in xrange(len(q.series)):
1930 1937 status(i)
1931 1938 return
1932 1939 if not args or args[0][0:1] in '-+':
1933 1940 if not q.applied:
1934 1941 raise util.Abort(_('no patches applied'))
1935 1942 patch = q.applied[-1].name
1936 1943 if patch is None and args[0][0:1] not in '-+':
1937 1944 patch = args.pop(0)
1938 1945 if patch is None:
1939 1946 raise util.Abort(_('no patch to work with'))
1940 1947 if args or opts['none']:
1941 1948 idx = q.find_series(patch)
1942 1949 if idx is None:
1943 1950 raise util.Abort(_('no patch named %s') % patch)
1944 1951 q.set_guards(idx, args)
1945 1952 q.save_dirty()
1946 1953 else:
1947 1954 status(q.series.index(q.lookup(patch)))
1948 1955
1949 1956 def header(ui, repo, patch=None):
1950 1957 """Print the header of the topmost or specified patch"""
1951 1958 q = repo.mq
1952 1959
1953 1960 if patch:
1954 1961 patch = q.lookup(patch)
1955 1962 else:
1956 1963 if not q.applied:
1957 1964 ui.write('No patches applied\n')
1958 1965 return 1
1959 1966 patch = q.lookup('qtip')
1960 1967 message = repo.mq.readheaders(patch)[0]
1961 1968
1962 1969 ui.write('\n'.join(message) + '\n')
1963 1970
1964 1971 def lastsavename(path):
1965 1972 (directory, base) = os.path.split(path)
1966 1973 names = os.listdir(directory)
1967 1974 namere = re.compile("%s.([0-9]+)" % base)
1968 1975 maxindex = None
1969 1976 maxname = None
1970 1977 for f in names:
1971 1978 m = namere.match(f)
1972 1979 if m:
1973 1980 index = int(m.group(1))
1974 1981 if maxindex == None or index > maxindex:
1975 1982 maxindex = index
1976 1983 maxname = f
1977 1984 if maxname:
1978 1985 return (os.path.join(directory, maxname), maxindex)
1979 1986 return (None, None)
1980 1987
1981 1988 def savename(path):
1982 1989 (last, index) = lastsavename(path)
1983 1990 if last is None:
1984 1991 index = 0
1985 1992 newpath = path + ".%d" % (index + 1)
1986 1993 return newpath
1987 1994
1988 1995 def push(ui, repo, patch=None, **opts):
1989 1996 """push the next patch onto the stack
1990 1997
1991 1998 When --force is applied, all local changes in patched files will be lost.
1992 1999 """
1993 2000 q = repo.mq
1994 2001 mergeq = None
1995 2002
1996 2003 if opts['all']:
1997 2004 if not q.series:
1998 2005 ui.warn(_('no patches in series\n'))
1999 2006 return 0
2000 2007 patch = q.series[-1]
2001 2008 if opts['merge']:
2002 2009 if opts['name']:
2003 2010 newpath = repo.join(opts['name'])
2004 2011 else:
2005 2012 newpath, i = lastsavename(q.path)
2006 2013 if not newpath:
2007 2014 ui.warn(_("no saved queues found, please use -n\n"))
2008 2015 return 1
2009 2016 mergeq = queue(ui, repo.join(""), newpath)
2010 2017 ui.warn(_("merging with queue at: %s\n") % mergeq.path)
2011 2018 ret = q.push(repo, patch, force=opts['force'], list=opts['list'],
2012 2019 mergeq=mergeq)
2013 2020 return ret
2014 2021
2015 2022 def pop(ui, repo, patch=None, **opts):
2016 2023 """pop the current patch off the stack
2017 2024
2018 2025 By default, pops off the top of the patch stack. If given a patch name,
2019 2026 keeps popping off patches until the named patch is at the top of the stack.
2020 2027 """
2021 2028 localupdate = True
2022 2029 if opts['name']:
2023 2030 q = queue(ui, repo.join(""), repo.join(opts['name']))
2024 2031 ui.warn(_('using patch queue: %s\n') % q.path)
2025 2032 localupdate = False
2026 2033 else:
2027 2034 q = repo.mq
2028 2035 ret = q.pop(repo, patch, force=opts['force'], update=localupdate,
2029 2036 all=opts['all'])
2030 2037 q.save_dirty()
2031 2038 return ret
2032 2039
2033 2040 def rename(ui, repo, patch, name=None, **opts):
2034 2041 """rename a patch
2035 2042
2036 2043 With one argument, renames the current patch to PATCH1.
2037 2044 With two arguments, renames PATCH1 to PATCH2."""
2038 2045
2039 2046 q = repo.mq
2040 2047
2041 2048 if not name:
2042 2049 name = patch
2043 2050 patch = None
2044 2051
2045 2052 if patch:
2046 2053 patch = q.lookup(patch)
2047 2054 else:
2048 2055 if not q.applied:
2049 2056 ui.write(_('No patches applied\n'))
2050 2057 return
2051 2058 patch = q.lookup('qtip')
2052 2059 absdest = q.join(name)
2053 2060 if os.path.isdir(absdest):
2054 2061 name = normname(os.path.join(name, os.path.basename(patch)))
2055 2062 absdest = q.join(name)
2056 2063 if os.path.exists(absdest):
2057 2064 raise util.Abort(_('%s already exists') % absdest)
2058 2065
2059 2066 if name in q.series:
2060 2067 raise util.Abort(_('A patch named %s already exists in the series file') % name)
2061 2068
2062 2069 if ui.verbose:
2063 2070 ui.write('Renaming %s to %s\n' % (patch, name))
2064 2071 i = q.find_series(patch)
2065 2072 guards = q.guard_re.findall(q.full_series[i])
2066 2073 q.full_series[i] = name + ''.join([' #' + g for g in guards])
2067 2074 q.parse_series()
2068 2075 q.series_dirty = 1
2069 2076
2070 2077 info = q.isapplied(patch)
2071 2078 if info:
2072 2079 q.applied[info[0]] = statusentry(info[1], name)
2073 2080 q.applied_dirty = 1
2074 2081
2075 2082 util.rename(q.join(patch), absdest)
2076 2083 r = q.qrepo()
2077 2084 if r:
2078 2085 wlock = r.wlock()
2079 2086 try:
2080 2087 if r.dirstate[patch] == 'a':
2081 2088 r.dirstate.forget(patch)
2082 2089 r.dirstate.add(name)
2083 2090 else:
2084 2091 if r.dirstate[name] == 'r':
2085 2092 r.undelete([name])
2086 2093 r.copy(patch, name)
2087 2094 r.remove([patch], False)
2088 2095 finally:
2089 2096 del wlock
2090 2097
2091 2098 q.save_dirty()
2092 2099
2093 2100 def restore(ui, repo, rev, **opts):
2094 2101 """restore the queue state saved by a rev"""
2095 2102 rev = repo.lookup(rev)
2096 2103 q = repo.mq
2097 2104 q.restore(repo, rev, delete=opts['delete'],
2098 2105 qupdate=opts['update'])
2099 2106 q.save_dirty()
2100 2107 return 0
2101 2108
2102 2109 def save(ui, repo, **opts):
2103 2110 """save current queue state"""
2104 2111 q = repo.mq
2105 2112 message = cmdutil.logmessage(opts)
2106 2113 ret = q.save(repo, msg=message)
2107 2114 if ret:
2108 2115 return ret
2109 2116 q.save_dirty()
2110 2117 if opts['copy']:
2111 2118 path = q.path
2112 2119 if opts['name']:
2113 2120 newpath = os.path.join(q.basepath, opts['name'])
2114 2121 if os.path.exists(newpath):
2115 2122 if not os.path.isdir(newpath):
2116 2123 raise util.Abort(_('destination %s exists and is not '
2117 2124 'a directory') % newpath)
2118 2125 if not opts['force']:
2119 2126 raise util.Abort(_('destination %s exists, '
2120 2127 'use -f to force') % newpath)
2121 2128 else:
2122 2129 newpath = savename(path)
2123 2130 ui.warn(_("copy %s to %s\n") % (path, newpath))
2124 2131 util.copyfiles(path, newpath)
2125 2132 if opts['empty']:
2126 2133 try:
2127 2134 os.unlink(q.join(q.status_path))
2128 2135 except:
2129 2136 pass
2130 2137 return 0
2131 2138
2132 2139 def strip(ui, repo, rev, **opts):
2133 2140 """strip a revision and all its descendants from the repository
2134 2141
2135 2142 If one of the working dir's parent revisions is stripped, the working
2136 2143 directory will be updated to the parent of the stripped revision.
2137 2144 """
2138 2145 backup = 'all'
2139 2146 if opts['backup']:
2140 2147 backup = 'strip'
2141 2148 elif opts['nobackup']:
2142 2149 backup = 'none'
2143 2150
2144 2151 rev = repo.lookup(rev)
2145 2152 p = repo.dirstate.parents()
2146 2153 cl = repo.changelog
2147 2154 update = True
2148 2155 if p[0] == revlog.nullid:
2149 2156 update = False
2150 2157 elif p[1] == revlog.nullid and rev != cl.ancestor(p[0], rev):
2151 2158 update = False
2152 2159 elif rev not in (cl.ancestor(p[0], rev), cl.ancestor(p[1], rev)):
2153 2160 update = False
2154 2161
2155 2162 repo.mq.strip(repo, rev, backup=backup, update=update, force=opts['force'])
2156 2163 return 0
2157 2164
2158 2165 def select(ui, repo, *args, **opts):
2159 2166 '''set or print guarded patches to push
2160 2167
2161 2168 Use the qguard command to set or print guards on patch, then use
2162 2169 qselect to tell mq which guards to use. A patch will be pushed if it
2163 2170 has no guards or any positive guards match the currently selected guard,
2164 2171 but will not be pushed if any negative guards match the current guard.
2165 2172 For example:
2166 2173
2167 2174 qguard foo.patch -stable (negative guard)
2168 2175 qguard bar.patch +stable (positive guard)
2169 2176 qselect stable
2170 2177
2171 2178 This activates the "stable" guard. mq will skip foo.patch (because
2172 2179 it has a negative match) but push bar.patch (because it
2173 2180 has a positive match).
2174 2181
2175 2182 With no arguments, prints the currently active guards.
2176 2183 With one argument, sets the active guard.
2177 2184
2178 2185 Use -n/--none to deactivate guards (no other arguments needed).
2179 2186 When no guards are active, patches with positive guards are skipped
2180 2187 and patches with negative guards are pushed.
2181 2188
2182 2189 qselect can change the guards on applied patches. It does not pop
2183 2190 guarded patches by default. Use --pop to pop back to the last applied
2184 2191 patch that is not guarded. Use --reapply (which implies --pop) to push
2185 2192 back to the current patch afterwards, but skip guarded patches.
2186 2193
2187 2194 Use -s/--series to print a list of all guards in the series file (no
2188 2195 other arguments needed). Use -v for more information.'''
2189 2196
2190 2197 q = repo.mq
2191 2198 guards = q.active()
2192 2199 if args or opts['none']:
2193 2200 old_unapplied = q.unapplied(repo)
2194 2201 old_guarded = [i for i in xrange(len(q.applied)) if
2195 2202 not q.pushable(i)[0]]
2196 2203 q.set_active(args)
2197 2204 q.save_dirty()
2198 2205 if not args:
2199 2206 ui.status(_('guards deactivated\n'))
2200 2207 if not opts['pop'] and not opts['reapply']:
2201 2208 unapplied = q.unapplied(repo)
2202 2209 guarded = [i for i in xrange(len(q.applied))
2203 2210 if not q.pushable(i)[0]]
2204 2211 if len(unapplied) != len(old_unapplied):
2205 2212 ui.status(_('number of unguarded, unapplied patches has '
2206 2213 'changed from %d to %d\n') %
2207 2214 (len(old_unapplied), len(unapplied)))
2208 2215 if len(guarded) != len(old_guarded):
2209 2216 ui.status(_('number of guarded, applied patches has changed '
2210 2217 'from %d to %d\n') %
2211 2218 (len(old_guarded), len(guarded)))
2212 2219 elif opts['series']:
2213 2220 guards = {}
2214 2221 noguards = 0
2215 2222 for gs in q.series_guards:
2216 2223 if not gs:
2217 2224 noguards += 1
2218 2225 for g in gs:
2219 2226 guards.setdefault(g, 0)
2220 2227 guards[g] += 1
2221 2228 if ui.verbose:
2222 2229 guards['NONE'] = noguards
2223 2230 guards = guards.items()
2224 2231 guards.sort(lambda a, b: cmp(a[0][1:], b[0][1:]))
2225 2232 if guards:
2226 2233 ui.note(_('guards in series file:\n'))
2227 2234 for guard, count in guards:
2228 2235 ui.note('%2d ' % count)
2229 2236 ui.write(guard, '\n')
2230 2237 else:
2231 2238 ui.note(_('no guards in series file\n'))
2232 2239 else:
2233 2240 if guards:
2234 2241 ui.note(_('active guards:\n'))
2235 2242 for g in guards:
2236 2243 ui.write(g, '\n')
2237 2244 else:
2238 2245 ui.write(_('no active guards\n'))
2239 2246 reapply = opts['reapply'] and q.applied and q.appliedname(-1)
2240 2247 popped = False
2241 2248 if opts['pop'] or opts['reapply']:
2242 2249 for i in xrange(len(q.applied)):
2243 2250 pushable, reason = q.pushable(i)
2244 2251 if not pushable:
2245 2252 ui.status(_('popping guarded patches\n'))
2246 2253 popped = True
2247 2254 if i == 0:
2248 2255 q.pop(repo, all=True)
2249 2256 else:
2250 2257 q.pop(repo, i-1)
2251 2258 break
2252 2259 if popped:
2253 2260 try:
2254 2261 if reapply:
2255 2262 ui.status(_('reapplying unguarded patches\n'))
2256 2263 q.push(repo, reapply)
2257 2264 finally:
2258 2265 q.save_dirty()
2259 2266
2260 2267 def finish(ui, repo, *revrange, **opts):
2261 2268 """move applied patches into repository history
2262 2269
2263 2270 Finishes the specified revisions (corresponding to applied patches) by
2264 2271 moving them out of mq control into regular repository history.
2265 2272
2266 2273 Accepts a revision range or the --applied option. If --applied is
2267 2274 specified, all applied mq revisions are removed from mq control.
2268 2275 Otherwise, the given revisions must be at the base of the stack of
2269 2276 applied patches.
2270 2277
2271 2278 This can be especially useful if your changes have been applied to an
2272 2279 upstream repository, or if you are about to push your changes to upstream.
2273 2280 """
2274 2281 if not opts['applied'] and not revrange:
2275 2282 raise util.Abort(_('no revisions specified'))
2276 2283 elif opts['applied']:
2277 2284 revrange = ('qbase:qtip',) + revrange
2278 2285
2279 2286 q = repo.mq
2280 2287 if not q.applied:
2281 2288 ui.status(_('no patches applied\n'))
2282 2289 return 0
2283 2290
2284 2291 revs = cmdutil.revrange(repo, revrange)
2285 2292 q.finish(repo, revs)
2286 2293 q.save_dirty()
2287 2294 return 0
2288 2295
2289 2296 def reposetup(ui, repo):
2290 2297 class mqrepo(repo.__class__):
2291 2298 def abort_if_wdir_patched(self, errmsg, force=False):
2292 2299 if self.mq.applied and not force:
2293 2300 parent = revlog.hex(self.dirstate.parents()[0])
2294 2301 if parent in [s.rev for s in self.mq.applied]:
2295 2302 raise util.Abort(errmsg)
2296 2303
2297 2304 def commit(self, *args, **opts):
2298 2305 if len(args) >= 6:
2299 2306 force = args[5]
2300 2307 else:
2301 2308 force = opts.get('force')
2302 2309 self.abort_if_wdir_patched(
2303 2310 _('cannot commit over an applied mq patch'),
2304 2311 force)
2305 2312
2306 2313 return super(mqrepo, self).commit(*args, **opts)
2307 2314
2308 2315 def push(self, remote, force=False, revs=None):
2309 2316 if self.mq.applied and not force and not revs:
2310 2317 raise util.Abort(_('source has mq patches applied'))
2311 2318 return super(mqrepo, self).push(remote, force, revs)
2312 2319
2313 2320 def tags(self):
2314 2321 if self.tagscache:
2315 2322 return self.tagscache
2316 2323
2317 2324 tagscache = super(mqrepo, self).tags()
2318 2325
2319 2326 q = self.mq
2320 2327 if not q.applied:
2321 2328 return tagscache
2322 2329
2323 2330 mqtags = [(revlog.bin(patch.rev), patch.name) for patch in q.applied]
2324 2331
2325 2332 if mqtags[-1][0] not in self.changelog.nodemap:
2326 2333 self.ui.warn(_('mq status file refers to unknown node %s\n')
2327 2334 % revlog.short(mqtags[-1][0]))
2328 2335 return tagscache
2329 2336
2330 2337 mqtags.append((mqtags[-1][0], 'qtip'))
2331 2338 mqtags.append((mqtags[0][0], 'qbase'))
2332 2339 mqtags.append((self.changelog.parents(mqtags[0][0])[0], 'qparent'))
2333 2340 for patch in mqtags:
2334 2341 if patch[1] in tagscache:
2335 2342 self.ui.warn(_('Tag %s overrides mq patch of the same name\n')
2336 2343 % patch[1])
2337 2344 else:
2338 2345 tagscache[patch[1]] = patch[0]
2339 2346
2340 2347 return tagscache
2341 2348
2342 2349 def _branchtags(self, partial, lrev):
2343 2350 q = self.mq
2344 2351 if not q.applied:
2345 2352 return super(mqrepo, self)._branchtags(partial, lrev)
2346 2353
2347 2354 cl = self.changelog
2348 2355 qbasenode = revlog.bin(q.applied[0].rev)
2349 2356 if qbasenode not in cl.nodemap:
2350 2357 self.ui.warn(_('mq status file refers to unknown node %s\n')
2351 2358 % revlog.short(qbasenode))
2352 2359 return super(mqrepo, self)._branchtags(partial, lrev)
2353 2360
2354 2361 qbase = cl.rev(qbasenode)
2355 2362 start = lrev + 1
2356 2363 if start < qbase:
2357 2364 # update the cache (excluding the patches) and save it
2358 2365 self._updatebranchcache(partial, lrev+1, qbase)
2359 2366 self._writebranchcache(partial, cl.node(qbase-1), qbase-1)
2360 2367 start = qbase
2361 2368 # if start = qbase, the cache is as updated as it should be.
2362 2369 # if start > qbase, the cache includes (part of) the patches.
2363 2370 # we might as well use it, but we won't save it.
2364 2371
2365 2372 # update the cache up to the tip
2366 2373 self._updatebranchcache(partial, start, len(cl))
2367 2374
2368 2375 return partial
2369 2376
2370 2377 if repo.local():
2371 2378 repo.__class__ = mqrepo
2372 2379 repo.mq = queue(ui, repo.join(""))
2373 2380
2374 2381 def mqimport(orig, ui, repo, *args, **kwargs):
2375 2382 if hasattr(repo, 'abort_if_wdir_patched'):
2376 2383 repo.abort_if_wdir_patched(_('cannot import over an applied patch'),
2377 2384 kwargs.get('force'))
2378 2385 return orig(ui, repo, *args, **kwargs)
2379 2386
2380 2387 def uisetup(ui):
2381 2388 extensions.wrapcommand(commands.table, 'import', mqimport)
2382 2389
2383 2390 seriesopts = [('s', 'summary', None, _('print first line of patch header'))]
2384 2391
2385 2392 cmdtable = {
2386 2393 "qapplied": (applied, [] + seriesopts, _('hg qapplied [-s] [PATCH]')),
2387 2394 "qclone":
2388 2395 (clone,
2389 2396 [('', 'pull', None, _('use pull protocol to copy metadata')),
2390 2397 ('U', 'noupdate', None, _('do not update the new working directories')),
2391 2398 ('', 'uncompressed', None,
2392 2399 _('use uncompressed transfer (fast over LAN)')),
2393 2400 ('p', 'patches', '', _('location of source patch repo')),
2394 2401 ] + commands.remoteopts,
2395 2402 _('hg qclone [OPTION]... SOURCE [DEST]')),
2396 2403 "qcommit|qci":
2397 2404 (commit,
2398 2405 commands.table["^commit|ci"][1],
2399 2406 _('hg qcommit [OPTION]... [FILE]...')),
2400 2407 "^qdiff":
2401 2408 (diff,
2402 2409 commands.diffopts + commands.diffopts2 + commands.walkopts,
2403 2410 _('hg qdiff [OPTION]... [FILE]...')),
2404 2411 "qdelete|qremove|qrm":
2405 2412 (delete,
2406 2413 [('k', 'keep', None, _('keep patch file')),
2407 2414 ('r', 'rev', [], _('stop managing a revision'))],
2408 2415 _('hg qdelete [-k] [-r REV]... [PATCH]...')),
2409 2416 'qfold':
2410 2417 (fold,
2411 2418 [('e', 'edit', None, _('edit patch header')),
2412 2419 ('k', 'keep', None, _('keep folded patch files')),
2413 2420 ] + commands.commitopts,
2414 2421 _('hg qfold [-e] [-k] [-m TEXT] [-l FILE] PATCH...')),
2415 2422 'qgoto':
2416 2423 (goto,
2417 2424 [('f', 'force', None, _('overwrite any local changes'))],
2418 2425 _('hg qgoto [OPTION]... PATCH')),
2419 2426 'qguard':
2420 2427 (guard,
2421 2428 [('l', 'list', None, _('list all patches and guards')),
2422 2429 ('n', 'none', None, _('drop all guards'))],
2423 2430 _('hg qguard [-l] [-n] [PATCH] [+GUARD]... [-GUARD]...')),
2424 2431 'qheader': (header, [], _('hg qheader [PATCH]')),
2425 2432 "^qimport":
2426 2433 (qimport,
2427 2434 [('e', 'existing', None, _('import file in patch dir')),
2428 2435 ('n', 'name', '', _('patch file name')),
2429 2436 ('f', 'force', None, _('overwrite existing files')),
2430 2437 ('r', 'rev', [], _('place existing revisions under mq control')),
2431 2438 ('g', 'git', None, _('use git extended diff format'))],
2432 2439 _('hg qimport [-e] [-n NAME] [-f] [-g] [-r REV]... FILE...')),
2433 2440 "^qinit":
2434 2441 (init,
2435 2442 [('c', 'create-repo', None, _('create queue repository'))],
2436 2443 _('hg qinit [-c]')),
2437 2444 "qnew":
2438 2445 (new,
2439 2446 [('e', 'edit', None, _('edit commit message')),
2440 2447 ('f', 'force', None, _('import uncommitted changes into patch')),
2441 2448 ('g', 'git', None, _('use git extended diff format')),
2442 2449 ('U', 'currentuser', None, _('add "From: <current user>" to patch')),
2443 2450 ('u', 'user', '', _('add "From: <given user>" to patch')),
2444 2451 ('D', 'currentdate', None, _('add "Date: <current date>" to patch')),
2445 2452 ('d', 'date', '', _('add "Date: <given date>" to patch'))
2446 2453 ] + commands.walkopts + commands.commitopts,
2447 2454 _('hg qnew [-e] [-m TEXT] [-l FILE] [-f] PATCH [FILE]...')),
2448 2455 "qnext": (next, [] + seriesopts, _('hg qnext [-s]')),
2449 2456 "qprev": (prev, [] + seriesopts, _('hg qprev [-s]')),
2450 2457 "^qpop":
2451 2458 (pop,
2452 2459 [('a', 'all', None, _('pop all patches')),
2453 2460 ('n', 'name', '', _('queue name to pop')),
2454 2461 ('f', 'force', None, _('forget any local changes'))],
2455 2462 _('hg qpop [-a] [-n NAME] [-f] [PATCH | INDEX]')),
2456 2463 "^qpush":
2457 2464 (push,
2458 2465 [('f', 'force', None, _('apply if the patch has rejects')),
2459 2466 ('l', 'list', None, _('list patch name in commit text')),
2460 2467 ('a', 'all', None, _('apply all patches')),
2461 2468 ('m', 'merge', None, _('merge from another queue')),
2462 2469 ('n', 'name', '', _('merge queue name'))],
2463 2470 _('hg qpush [-f] [-l] [-a] [-m] [-n NAME] [PATCH | INDEX]')),
2464 2471 "^qrefresh":
2465 2472 (refresh,
2466 2473 [('e', 'edit', None, _('edit commit message')),
2467 2474 ('g', 'git', None, _('use git extended diff format')),
2468 2475 ('s', 'short', None, _('refresh only files already in the patch and specified files')),
2469 2476 ('U', 'currentuser', None, _('add/update "From: <current user>" in patch')),
2470 2477 ('u', 'user', '', _('add/update "From: <given user>" in patch')),
2471 2478 ('D', 'currentdate', None, _('update "Date: <current date>" in patch (if present)')),
2472 2479 ('d', 'date', '', _('update "Date: <given date>" in patch (if present)'))
2473 2480 ] + commands.walkopts + commands.commitopts,
2474 2481 _('hg qrefresh [-I] [-X] [-e] [-m TEXT] [-l FILE] [-s] [FILE]...')),
2475 2482 'qrename|qmv':
2476 2483 (rename, [], _('hg qrename PATCH1 [PATCH2]')),
2477 2484 "qrestore":
2478 2485 (restore,
2479 2486 [('d', 'delete', None, _('delete save entry')),
2480 2487 ('u', 'update', None, _('update queue working dir'))],
2481 2488 _('hg qrestore [-d] [-u] REV')),
2482 2489 "qsave":
2483 2490 (save,
2484 2491 [('c', 'copy', None, _('copy patch directory')),
2485 2492 ('n', 'name', '', _('copy directory name')),
2486 2493 ('e', 'empty', None, _('clear queue status file')),
2487 2494 ('f', 'force', None, _('force copy'))] + commands.commitopts,
2488 2495 _('hg qsave [-m TEXT] [-l FILE] [-c] [-n NAME] [-e] [-f]')),
2489 2496 "qselect":
2490 2497 (select,
2491 2498 [('n', 'none', None, _('disable all guards')),
2492 2499 ('s', 'series', None, _('list all guards in series file')),
2493 2500 ('', 'pop', None, _('pop to before first guarded applied patch')),
2494 2501 ('', 'reapply', None, _('pop, then reapply patches'))],
2495 2502 _('hg qselect [OPTION]... [GUARD]...')),
2496 2503 "qseries":
2497 2504 (series,
2498 2505 [('m', 'missing', None, _('print patches not in series')),
2499 2506 ] + seriesopts,
2500 2507 _('hg qseries [-ms]')),
2501 2508 "^strip":
2502 2509 (strip,
2503 2510 [('f', 'force', None, _('force removal with local changes')),
2504 2511 ('b', 'backup', None, _('bundle unrelated changesets')),
2505 2512 ('n', 'nobackup', None, _('no backups'))],
2506 2513 _('hg strip [-f] [-b] [-n] REV')),
2507 2514 "qtop": (top, [] + seriesopts, _('hg qtop [-s]')),
2508 2515 "qunapplied": (unapplied, [] + seriesopts, _('hg qunapplied [-s] [PATCH]')),
2509 2516 "qfinish":
2510 2517 (finish,
2511 2518 [('a', 'applied', None, _('finish all applied changesets'))],
2512 2519 _('hg qfinish [-a] [REV...]')),
2513 2520 }
@@ -1,287 +1,286
1 1 # notify.py - email notifications for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 '''hook extension to email notifications on commits/pushes
9 9
10 10 Subscriptions can be managed through hgrc. Default mode is to print
11 11 messages to stdout, for testing and configuring.
12 12
13 13 To use, configure notify extension and enable in hgrc like this:
14 14
15 15 [extensions]
16 16 hgext.notify =
17 17
18 18 [hooks]
19 19 # one email for each incoming changeset
20 20 incoming.notify = python:hgext.notify.hook
21 21 # batch emails when many changesets incoming at one time
22 22 changegroup.notify = python:hgext.notify.hook
23 23
24 24 [notify]
25 25 # config items go in here
26 26
27 27 config items:
28 28
29 29 REQUIRED:
30 30 config = /path/to/file # file containing subscriptions
31 31
32 32 OPTIONAL:
33 33 test = True # print messages to stdout for testing
34 34 strip = 3 # number of slashes to strip for url paths
35 35 domain = example.com # domain to use if committer missing domain
36 36 style = ... # style file to use when formatting email
37 37 template = ... # template to use when formatting email
38 38 incoming = ... # template to use when run as incoming hook
39 39 changegroup = ... # template when run as changegroup hook
40 40 maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
41 41 maxsubject = 67 # truncate subject line longer than this
42 42 diffstat = True # add a diffstat before the diff content
43 43 sources = serve # notify if source of incoming changes in this list
44 44 # (serve == ssh or http, push, pull, bundle)
45 45 [email]
46 46 from = user@host.com # email address to send as if none given
47 47 [web]
48 48 baseurl = http://hgserver/... # root of hg web site for browsing commits
49 49
50 50 notify config file has same format as regular hgrc. it has two
51 51 sections so you can express subscriptions in whatever way is handier
52 52 for you.
53 53
54 54 [usersubs]
55 55 # key is subscriber email, value is ","-separated list of glob patterns
56 56 user@host = pattern
57 57
58 58 [reposubs]
59 59 # key is glob pattern, value is ","-separated list of subscriber emails
60 60 pattern = user@host
61 61
62 62 glob patterns are matched against path to repo root.
63 63
64 64 if you like, you can put notify config file in repo that users can
65 65 push changes to, they can manage their own subscriptions.'''
66 66
67 67 from mercurial.i18n import _
68 68 from mercurial.node import bin, short
69 69 from mercurial import patch, cmdutil, templater, util, mail
70 70 import email.Parser, fnmatch, socket, time
71 71
72 72 # template for single changeset can include email headers.
73 73 single_template = '''
74 74 Subject: changeset in {webroot}: {desc|firstline|strip}
75 75 From: {author}
76 76
77 77 changeset {node|short} in {root}
78 78 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
79 79 description:
80 80 \t{desc|tabindent|strip}
81 81 '''.lstrip()
82 82
83 83 # template for multiple changesets should not contain email headers,
84 84 # because only first set of headers will be used and result will look
85 85 # strange.
86 86 multiple_template = '''
87 87 changeset {node|short} in {root}
88 88 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
89 89 summary: {desc|firstline}
90 90 '''
91 91
92 92 deftemplates = {
93 93 'changegroup': multiple_template,
94 94 }
95 95
96 96 class notifier(object):
97 97 '''email notification class.'''
98 98
99 99 def __init__(self, ui, repo, hooktype):
100 100 self.ui = ui
101 101 cfg = self.ui.config('notify', 'config')
102 102 if cfg:
103 103 self.ui.readsections(cfg, 'usersubs', 'reposubs')
104 104 self.repo = repo
105 105 self.stripcount = int(self.ui.config('notify', 'strip', 0))
106 106 self.root = self.strip(self.repo.root)
107 107 self.domain = self.ui.config('notify', 'domain')
108 108 self.charsets = mail._charsets(self.ui)
109 109 self.subs = self.subscribers()
110 110
111 111 mapfile = self.ui.config('notify', 'style')
112 112 template = (self.ui.config('notify', hooktype) or
113 113 self.ui.config('notify', 'template'))
114 114 self.t = cmdutil.changeset_templater(self.ui, self.repo,
115 115 False, mapfile, False)
116 116 if not mapfile and not template:
117 117 template = deftemplates.get(hooktype) or single_template
118 118 if template:
119 119 template = templater.parsestring(template, quoted=False)
120 120 self.t.use_template(template)
121 121
122 122 def strip(self, path):
123 123 '''strip leading slashes from local path, turn into web-safe path.'''
124 124
125 125 path = util.pconvert(path)
126 126 count = self.stripcount
127 127 while count > 0:
128 128 c = path.find('/')
129 129 if c == -1:
130 130 break
131 131 path = path[c+1:]
132 132 count -= 1
133 133 return path
134 134
135 135 def fixmail(self, addr):
136 136 '''try to clean up email addresses.'''
137 137
138 138 addr = util.email(addr.strip())
139 139 if self.domain:
140 140 a = addr.find('@localhost')
141 141 if a != -1:
142 142 addr = addr[:a]
143 143 if '@' not in addr:
144 144 return addr + '@' + self.domain
145 145 return addr
146 146
147 147 def subscribers(self):
148 148 '''return list of email addresses of subscribers to this repo.'''
149 149
150 150 subs = {}
151 151 for user, pats in self.ui.configitems('usersubs'):
152 152 for pat in pats.split(','):
153 153 if fnmatch.fnmatch(self.repo.root, pat.strip()):
154 154 subs[self.fixmail(user)] = 1
155 155 for pat, users in self.ui.configitems('reposubs'):
156 156 if fnmatch.fnmatch(self.repo.root, pat):
157 157 for user in users.split(','):
158 158 subs[self.fixmail(user)] = 1
159 159 subs = util.sort(subs)
160 160 return [mail.addressencode(self.ui, s, self.charsets) for s in subs]
161 161
162 162 def url(self, path=None):
163 163 return self.ui.config('web', 'baseurl') + (path or self.root)
164 164
165 165 def node(self, node):
166 166 '''format one changeset.'''
167 167
168 168 self.t.show(changenode=node, changes=self.repo.changelog.read(node),
169 169 baseurl=self.ui.config('web', 'baseurl'),
170 170 root=self.repo.root,
171 171 webroot=self.root)
172 172
173 173 def skipsource(self, source):
174 174 '''true if incoming changes from this source should be skipped.'''
175 175 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
176 176 return source not in ok_sources
177 177
178 178 def send(self, node, count, data):
179 179 '''send message.'''
180 180
181 181 p = email.Parser.Parser()
182 182 msg = p.parsestr(data)
183 183
184 184 # store sender and subject
185 185 sender, subject = msg['From'], msg['Subject']
186 186 # create fresh mime message from msg body
187 187 text = msg.get_payload()
188 188 # for notification prefer readability over data precision
189 189 msg = mail.mimeencode(self.ui, text, self.charsets)
190 190
191 191 def fix_subject(subject):
192 192 '''try to make subject line exist and be useful.'''
193 193
194 194 if not subject:
195 195 if count > 1:
196 196 subject = _('%s: %d new changesets') % (self.root, count)
197 197 else:
198 198 changes = self.repo.changelog.read(node)
199 199 s = changes[4].lstrip().split('\n', 1)[0].rstrip()
200 200 subject = '%s: %s' % (self.root, s)
201 201 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
202 202 if maxsubject and len(subject) > maxsubject:
203 203 subject = subject[:maxsubject-3] + '...'
204 204 msg['Subject'] = mail.headencode(self.ui, subject, self.charsets)
205 205
206 206 def fix_sender(sender):
207 207 '''try to make message have proper sender.'''
208 208
209 209 if not sender:
210 210 sender = self.ui.config('email', 'from') or self.ui.username()
211 211 if '@' not in sender or '@localhost' in sender:
212 212 sender = self.fixmail(sender)
213 213 msg['From'] = mail.addressencode(self.ui, sender, self.charsets)
214 214
215 215 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
216 216 fix_subject(subject)
217 217 fix_sender(sender)
218 218
219 219 msg['X-Hg-Notification'] = 'changeset ' + short(node)
220 220 if not msg['Message-Id']:
221 221 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
222 222 (short(node), int(time.time()),
223 223 hash(self.repo.root), socket.getfqdn()))
224 224 msg['To'] = ', '.join(self.subs)
225 225
226 226 msgtext = msg.as_string(0)
227 227 if self.ui.configbool('notify', 'test', True):
228 228 self.ui.write(msgtext)
229 229 if not msgtext.endswith('\n'):
230 230 self.ui.write('\n')
231 231 else:
232 232 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
233 233 (len(self.subs), count))
234 234 mail.sendmail(self.ui, util.email(msg['From']),
235 235 self.subs, msgtext)
236 236
237 237 def diff(self, node, ref):
238 238 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
239 239 prev = self.repo.changelog.parents(node)[0]
240 240
241 self.ui.pushbuffer()
242 patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui))
243 difflines = self.ui.popbuffer().splitlines()
241 chunks = patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui))
242 difflines = ''.join(chunks).splitlines()
244 243
245 244 if self.ui.configbool('notify', 'diffstat', True):
246 245 s = patch.diffstat(difflines)
247 246 # s may be nil, don't include the header if it is
248 247 if s:
249 248 self.ui.write('\ndiffstat:\n\n%s' % s)
250 249 if maxdiff == 0:
251 250 return
252 251 if maxdiff > 0 and len(difflines) > maxdiff:
253 252 self.ui.write(_('\ndiffs (truncated from %d to %d lines):\n\n') %
254 253 (len(difflines), maxdiff))
255 254 difflines = difflines[:maxdiff]
256 255 elif difflines:
257 256 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
258 257 self.ui.write("\n".join(difflines))
259 258
260 259 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
261 260 '''send email notifications to interested subscribers.
262 261
263 262 if used as changegroup hook, send one email for all changesets in
264 263 changegroup. else send one email per changeset.'''
265 264 n = notifier(ui, repo, hooktype)
266 265 if not n.subs:
267 266 ui.debug(_('notify: no subscribers to repo %s\n') % n.root)
268 267 return
269 268 if n.skipsource(source):
270 269 ui.debug(_('notify: changes have source "%s" - skipping\n') %
271 270 source)
272 271 return
273 272 node = bin(node)
274 273 ui.pushbuffer()
275 274 if hooktype == 'changegroup':
276 275 start = repo[node].rev()
277 276 end = len(repo)
278 277 count = end - start
279 278 for rev in xrange(start, end):
280 279 n.node(repo[rev].node())
281 280 n.diff(node, repo.changelog.tip())
282 281 else:
283 282 count = 1
284 283 n.node(node)
285 284 n.diff(node, node)
286 285 data = ui.popbuffer()
287 286 n.send(node, count, data)
@@ -1,539 +1,540
1 1 # record.py
2 2 #
3 3 # Copyright 2007 Bryan O'Sullivan <bos@serpentine.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of
6 6 # the GNU General Public License, incorporated herein by reference.
7 7
8 8 '''interactive change selection during commit or qrefresh'''
9 9
10 10 from mercurial.i18n import gettext, _
11 11 from mercurial import cmdutil, commands, extensions, hg, mdiff, patch
12 12 from mercurial import util
13 13 import copy, cStringIO, errno, operator, os, re, tempfile
14 14
15 15 lines_re = re.compile(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
16 16
17 17 def scanpatch(fp):
18 18 """like patch.iterhunks, but yield different events
19 19
20 20 - ('file', [header_lines + fromfile + tofile])
21 21 - ('context', [context_lines])
22 22 - ('hunk', [hunk_lines])
23 23 - ('range', (-start,len, +start,len, diffp))
24 24 """
25 25 lr = patch.linereader(fp)
26 26
27 27 def scanwhile(first, p):
28 28 """scan lr while predicate holds"""
29 29 lines = [first]
30 30 while True:
31 31 line = lr.readline()
32 32 if not line:
33 33 break
34 34 if p(line):
35 35 lines.append(line)
36 36 else:
37 37 lr.push(line)
38 38 break
39 39 return lines
40 40
41 41 while True:
42 42 line = lr.readline()
43 43 if not line:
44 44 break
45 45 if line.startswith('diff --git a/'):
46 46 def notheader(line):
47 47 s = line.split(None, 1)
48 48 return not s or s[0] not in ('---', 'diff')
49 49 header = scanwhile(line, notheader)
50 50 fromfile = lr.readline()
51 51 if fromfile.startswith('---'):
52 52 tofile = lr.readline()
53 53 header += [fromfile, tofile]
54 54 else:
55 55 lr.push(fromfile)
56 56 yield 'file', header
57 57 elif line[0] == ' ':
58 58 yield 'context', scanwhile(line, lambda l: l[0] in ' \\')
59 59 elif line[0] in '-+':
60 60 yield 'hunk', scanwhile(line, lambda l: l[0] in '-+\\')
61 61 else:
62 62 m = lines_re.match(line)
63 63 if m:
64 64 yield 'range', m.groups()
65 65 else:
66 66 raise patch.PatchError('unknown patch content: %r' % line)
67 67
68 68 class header(object):
69 69 """patch header
70 70
71 71 XXX shoudn't we move this to mercurial/patch.py ?
72 72 """
73 73 diff_re = re.compile('diff --git a/(.*) b/(.*)$')
74 74 allhunks_re = re.compile('(?:index|new file|deleted file) ')
75 75 pretty_re = re.compile('(?:new file|deleted file) ')
76 76 special_re = re.compile('(?:index|new|deleted|copy|rename) ')
77 77
78 78 def __init__(self, header):
79 79 self.header = header
80 80 self.hunks = []
81 81
82 82 def binary(self):
83 83 for h in self.header:
84 84 if h.startswith('index '):
85 85 return True
86 86
87 87 def pretty(self, fp):
88 88 for h in self.header:
89 89 if h.startswith('index '):
90 90 fp.write(_('this modifies a binary file (all or nothing)\n'))
91 91 break
92 92 if self.pretty_re.match(h):
93 93 fp.write(h)
94 94 if self.binary():
95 95 fp.write(_('this is a binary file\n'))
96 96 break
97 97 if h.startswith('---'):
98 98 fp.write(_('%d hunks, %d lines changed\n') %
99 99 (len(self.hunks),
100 100 sum([h.added + h.removed for h in self.hunks])))
101 101 break
102 102 fp.write(h)
103 103
104 104 def write(self, fp):
105 105 fp.write(''.join(self.header))
106 106
107 107 def allhunks(self):
108 108 for h in self.header:
109 109 if self.allhunks_re.match(h):
110 110 return True
111 111
112 112 def files(self):
113 113 fromfile, tofile = self.diff_re.match(self.header[0]).groups()
114 114 if fromfile == tofile:
115 115 return [fromfile]
116 116 return [fromfile, tofile]
117 117
118 118 def filename(self):
119 119 return self.files()[-1]
120 120
121 121 def __repr__(self):
122 122 return '<header %s>' % (' '.join(map(repr, self.files())))
123 123
124 124 def special(self):
125 125 for h in self.header:
126 126 if self.special_re.match(h):
127 127 return True
128 128
129 129 def countchanges(hunk):
130 130 """hunk -> (n+,n-)"""
131 131 add = len([h for h in hunk if h[0] == '+'])
132 132 rem = len([h for h in hunk if h[0] == '-'])
133 133 return add, rem
134 134
135 135 class hunk(object):
136 136 """patch hunk
137 137
138 138 XXX shouldn't we merge this with patch.hunk ?
139 139 """
140 140 maxcontext = 3
141 141
142 142 def __init__(self, header, fromline, toline, proc, before, hunk, after):
143 143 def trimcontext(number, lines):
144 144 delta = len(lines) - self.maxcontext
145 145 if False and delta > 0:
146 146 return number + delta, lines[:self.maxcontext]
147 147 return number, lines
148 148
149 149 self.header = header
150 150 self.fromline, self.before = trimcontext(fromline, before)
151 151 self.toline, self.after = trimcontext(toline, after)
152 152 self.proc = proc
153 153 self.hunk = hunk
154 154 self.added, self.removed = countchanges(self.hunk)
155 155
156 156 def write(self, fp):
157 157 delta = len(self.before) + len(self.after)
158 158 if self.after and self.after[-1] == '\\ No newline at end of file\n':
159 159 delta -= 1
160 160 fromlen = delta + self.removed
161 161 tolen = delta + self.added
162 162 fp.write('@@ -%d,%d +%d,%d @@%s\n' %
163 163 (self.fromline, fromlen, self.toline, tolen,
164 164 self.proc and (' ' + self.proc)))
165 165 fp.write(''.join(self.before + self.hunk + self.after))
166 166
167 167 pretty = write
168 168
169 169 def filename(self):
170 170 return self.header.filename()
171 171
172 172 def __repr__(self):
173 173 return '<hunk %r@%d>' % (self.filename(), self.fromline)
174 174
175 175 def parsepatch(fp):
176 176 """patch -> [] of hunks """
177 177 class parser(object):
178 178 """patch parsing state machine"""
179 179 def __init__(self):
180 180 self.fromline = 0
181 181 self.toline = 0
182 182 self.proc = ''
183 183 self.header = None
184 184 self.context = []
185 185 self.before = []
186 186 self.hunk = []
187 187 self.stream = []
188 188
189 189 def addrange(self, (fromstart, fromend, tostart, toend, proc)):
190 190 self.fromline = int(fromstart)
191 191 self.toline = int(tostart)
192 192 self.proc = proc
193 193
194 194 def addcontext(self, context):
195 195 if self.hunk:
196 196 h = hunk(self.header, self.fromline, self.toline, self.proc,
197 197 self.before, self.hunk, context)
198 198 self.header.hunks.append(h)
199 199 self.stream.append(h)
200 200 self.fromline += len(self.before) + h.removed
201 201 self.toline += len(self.before) + h.added
202 202 self.before = []
203 203 self.hunk = []
204 204 self.proc = ''
205 205 self.context = context
206 206
207 207 def addhunk(self, hunk):
208 208 if self.context:
209 209 self.before = self.context
210 210 self.context = []
211 211 self.hunk = hunk
212 212
213 213 def newfile(self, hdr):
214 214 self.addcontext([])
215 215 h = header(hdr)
216 216 self.stream.append(h)
217 217 self.header = h
218 218
219 219 def finished(self):
220 220 self.addcontext([])
221 221 return self.stream
222 222
223 223 transitions = {
224 224 'file': {'context': addcontext,
225 225 'file': newfile,
226 226 'hunk': addhunk,
227 227 'range': addrange},
228 228 'context': {'file': newfile,
229 229 'hunk': addhunk,
230 230 'range': addrange},
231 231 'hunk': {'context': addcontext,
232 232 'file': newfile,
233 233 'range': addrange},
234 234 'range': {'context': addcontext,
235 235 'hunk': addhunk},
236 236 }
237 237
238 238 p = parser()
239 239
240 240 state = 'context'
241 241 for newstate, data in scanpatch(fp):
242 242 try:
243 243 p.transitions[state][newstate](p, data)
244 244 except KeyError:
245 245 raise patch.PatchError('unhandled transition: %s -> %s' %
246 246 (state, newstate))
247 247 state = newstate
248 248 return p.finished()
249 249
250 250 def filterpatch(ui, chunks):
251 251 """Interactively filter patch chunks into applied-only chunks"""
252 252 chunks = list(chunks)
253 253 chunks.reverse()
254 254 seen = {}
255 255 def consumefile():
256 256 """fetch next portion from chunks until a 'header' is seen
257 257 NB: header == new-file mark
258 258 """
259 259 consumed = []
260 260 while chunks:
261 261 if isinstance(chunks[-1], header):
262 262 break
263 263 else:
264 264 consumed.append(chunks.pop())
265 265 return consumed
266 266
267 267 resp_all = [None] # this two are changed from inside prompt,
268 268 resp_file = [None] # so can't be usual variables
269 269 applied = {} # 'filename' -> [] of chunks
270 270 def prompt(query):
271 271 """prompt query, and process base inputs
272 272
273 273 - y/n for the rest of file
274 274 - y/n for the rest
275 275 - ? (help)
276 276 - q (quit)
277 277
278 278 else, input is returned to the caller.
279 279 """
280 280 if resp_all[0] is not None:
281 281 return resp_all[0]
282 282 if resp_file[0] is not None:
283 283 return resp_file[0]
284 284 while True:
285 285 choices = _('[Ynsfdaq?]')
286 286 r = (ui.prompt("%s %s " % (query, choices), '(?i)%s?$' % choices)
287 287 or _('y')).lower()
288 288 if r == _('?'):
289 289 doc = gettext(record.__doc__)
290 290 c = doc.find(_('y - record this change'))
291 291 for l in doc[c:].splitlines():
292 292 if l: ui.write(l.strip(), '\n')
293 293 continue
294 294 elif r == _('s'):
295 295 r = resp_file[0] = 'n'
296 296 elif r == _('f'):
297 297 r = resp_file[0] = 'y'
298 298 elif r == _('d'):
299 299 r = resp_all[0] = 'n'
300 300 elif r == _('a'):
301 301 r = resp_all[0] = 'y'
302 302 elif r == _('q'):
303 303 raise util.Abort(_('user quit'))
304 304 return r
305 305 while chunks:
306 306 chunk = chunks.pop()
307 307 if isinstance(chunk, header):
308 308 # new-file mark
309 309 resp_file = [None]
310 310 fixoffset = 0
311 311 hdr = ''.join(chunk.header)
312 312 if hdr in seen:
313 313 consumefile()
314 314 continue
315 315 seen[hdr] = True
316 316 if resp_all[0] is None:
317 317 chunk.pretty(ui)
318 318 r = prompt(_('examine changes to %s?') %
319 319 _(' and ').join(map(repr, chunk.files())))
320 320 if r == _('y'):
321 321 applied[chunk.filename()] = [chunk]
322 322 if chunk.allhunks():
323 323 applied[chunk.filename()] += consumefile()
324 324 else:
325 325 consumefile()
326 326 else:
327 327 # new hunk
328 328 if resp_file[0] is None and resp_all[0] is None:
329 329 chunk.pretty(ui)
330 330 r = prompt(_('record this change to %r?') %
331 331 chunk.filename())
332 332 if r == _('y'):
333 333 if fixoffset:
334 334 chunk = copy.copy(chunk)
335 335 chunk.toline += fixoffset
336 336 applied[chunk.filename()].append(chunk)
337 337 else:
338 338 fixoffset += chunk.removed - chunk.added
339 339 return reduce(operator.add, [h for h in applied.itervalues()
340 340 if h[0].special() or len(h) > 1], [])
341 341
342 342 def record(ui, repo, *pats, **opts):
343 343 '''interactively select changes to commit
344 344
345 345 If a list of files is omitted, all changes reported by "hg status"
346 346 will be candidates for recording.
347 347
348 348 See 'hg help dates' for a list of formats valid for -d/--date.
349 349
350 350 You will be prompted for whether to record changes to each
351 351 modified file, and for files with multiple changes, for each
352 352 change to use. For each query, the following responses are
353 353 possible:
354 354
355 355 y - record this change
356 356 n - skip this change
357 357
358 358 s - skip remaining changes to this file
359 359 f - record remaining changes to this file
360 360
361 361 d - done, skip remaining changes and files
362 362 a - record all changes to all remaining files
363 363 q - quit, recording no changes
364 364
365 365 ? - display help'''
366 366
367 367 def record_committer(ui, repo, pats, opts):
368 368 commands.commit(ui, repo, *pats, **opts)
369 369
370 370 dorecord(ui, repo, record_committer, *pats, **opts)
371 371
372 372
373 373 def qrecord(ui, repo, patch, *pats, **opts):
374 374 '''interactively record a new patch
375 375
376 376 see 'hg help qnew' & 'hg help record' for more information and usage
377 377 '''
378 378
379 379 try:
380 380 mq = extensions.find('mq')
381 381 except KeyError:
382 382 raise util.Abort(_("'mq' extension not loaded"))
383 383
384 384 def qrecord_committer(ui, repo, pats, opts):
385 385 mq.new(ui, repo, patch, *pats, **opts)
386 386
387 387 opts = opts.copy()
388 388 opts['force'] = True # always 'qnew -f'
389 389 dorecord(ui, repo, qrecord_committer, *pats, **opts)
390 390
391 391
392 392 def dorecord(ui, repo, committer, *pats, **opts):
393 393 if not ui.interactive:
394 394 raise util.Abort(_('running non-interactively, use commit instead'))
395 395
396 396 def recordfunc(ui, repo, message, match, opts):
397 397 """This is generic record driver.
398 398
399 399 It's job is to interactively filter local changes, and accordingly
400 400 prepare working dir into a state, where the job can be delegated to
401 401 non-interactive commit command such as 'commit' or 'qrefresh'.
402 402
403 403 After the actual job is done by non-interactive command, working dir
404 404 state is restored to original.
405 405
406 406 In the end we'll record intresting changes, and everything else will be
407 407 left in place, so the user can continue his work.
408 408 """
409 409 if match.files():
410 410 changes = None
411 411 else:
412 412 changes = repo.status(match=match)[:3]
413 413 modified, added, removed = changes
414 414 match = cmdutil.matchfiles(repo, modified + added + removed)
415 415 diffopts = mdiff.diffopts(git=True, nodates=True)
416 chunks = patch.diff(repo, repo.dirstate.parents()[0], match=match,
417 changes=changes, opts=diffopts)
416 418 fp = cStringIO.StringIO()
417 patch.diff(repo, repo.dirstate.parents()[0], match=match,
418 changes=changes, opts=diffopts, fp=fp)
419 fp.write(''.join(chunks))
419 420 fp.seek(0)
420 421
421 422 # 1. filter patch, so we have intending-to apply subset of it
422 423 chunks = filterpatch(ui, parsepatch(fp))
423 424 del fp
424 425
425 426 contenders = {}
426 427 for h in chunks:
427 428 try: contenders.update(dict.fromkeys(h.files()))
428 429 except AttributeError: pass
429 430
430 431 newfiles = [f for f in match.files() if f in contenders]
431 432
432 433 if not newfiles:
433 434 ui.status(_('no changes to record\n'))
434 435 return 0
435 436
436 437 if changes is None:
437 438 match = cmdutil.matchfiles(repo, newfiles)
438 439 changes = repo.status(match=match)
439 440 modified = dict.fromkeys(changes[0])
440 441
441 442 # 2. backup changed files, so we can restore them in the end
442 443 backups = {}
443 444 backupdir = repo.join('record-backups')
444 445 try:
445 446 os.mkdir(backupdir)
446 447 except OSError, err:
447 448 if err.errno != errno.EEXIST:
448 449 raise
449 450 try:
450 451 # backup continues
451 452 for f in newfiles:
452 453 if f not in modified:
453 454 continue
454 455 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
455 456 dir=backupdir)
456 457 os.close(fd)
457 458 ui.debug(_('backup %r as %r\n') % (f, tmpname))
458 459 util.copyfile(repo.wjoin(f), tmpname)
459 460 backups[f] = tmpname
460 461
461 462 fp = cStringIO.StringIO()
462 463 for c in chunks:
463 464 if c.filename() in backups:
464 465 c.write(fp)
465 466 dopatch = fp.tell()
466 467 fp.seek(0)
467 468
468 469 # 3a. apply filtered patch to clean repo (clean)
469 470 if backups:
470 471 hg.revert(repo, repo.dirstate.parents()[0], backups.has_key)
471 472
472 473 # 3b. (apply)
473 474 if dopatch:
474 475 try:
475 476 ui.debug(_('applying patch\n'))
476 477 ui.debug(fp.getvalue())
477 478 patch.internalpatch(fp, ui, 1, repo.root)
478 479 except patch.PatchError, err:
479 480 s = str(err)
480 481 if s:
481 482 raise util.Abort(s)
482 483 else:
483 484 raise util.Abort(_('patch failed to apply'))
484 485 del fp
485 486
486 487 # 4. We prepared working directory according to filtered patch.
487 488 # Now is the time to delegate the job to commit/qrefresh or the like!
488 489
489 490 # it is important to first chdir to repo root -- we'll call a
490 491 # highlevel command with list of pathnames relative to repo root
491 492 cwd = os.getcwd()
492 493 os.chdir(repo.root)
493 494 try:
494 495 committer(ui, repo, newfiles, opts)
495 496 finally:
496 497 os.chdir(cwd)
497 498
498 499 return 0
499 500 finally:
500 501 # 5. finally restore backed-up files
501 502 try:
502 503 for realname, tmpname in backups.iteritems():
503 504 ui.debug(_('restoring %r to %r\n') % (tmpname, realname))
504 505 util.copyfile(tmpname, repo.wjoin(realname))
505 506 os.unlink(tmpname)
506 507 os.rmdir(backupdir)
507 508 except OSError:
508 509 pass
509 510 return cmdutil.commit(ui, repo, recordfunc, pats, opts)
510 511
511 512 cmdtable = {
512 513 "record":
513 514 (record,
514 515
515 516 # add commit options
516 517 commands.table['^commit|ci'][1],
517 518
518 519 _('hg record [OPTION]... [FILE]...')),
519 520 }
520 521
521 522
522 523 def extsetup():
523 524 try:
524 525 mq = extensions.find('mq')
525 526 except KeyError:
526 527 return
527 528
528 529 qcmdtable = {
529 530 "qrecord":
530 531 (qrecord,
531 532
532 533 # add qnew options, except '--force'
533 534 [opt for opt in mq.cmdtable['qnew'][1] if opt[1] != 'force'],
534 535
535 536 _('hg qrecord [OPTION]... PATCH [FILE]...')),
536 537 }
537 538
538 539 cmdtable.update(qcmdtable)
539 540
@@ -1,581 +1,584
1 1 # Patch transplanting extension for Mercurial
2 2 #
3 3 # Copyright 2006, 2007 Brendan Cully <brendan@kublai.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from mercurial.i18n import _
9 9 import os, tempfile
10 10 from mercurial import bundlerepo, changegroup, cmdutil, hg, merge
11 11 from mercurial import patch, revlog, util
12 12
13 13 '''patch transplanting tool
14 14
15 15 This extension allows you to transplant patches from another branch.
16 16
17 17 Transplanted patches are recorded in .hg/transplant/transplants, as a map
18 18 from a changeset hash to its hash in the source repository.
19 19 '''
20 20
21 21 class transplantentry:
22 22 def __init__(self, lnode, rnode):
23 23 self.lnode = lnode
24 24 self.rnode = rnode
25 25
26 26 class transplants:
27 27 def __init__(self, path=None, transplantfile=None, opener=None):
28 28 self.path = path
29 29 self.transplantfile = transplantfile
30 30 self.opener = opener
31 31
32 32 if not opener:
33 33 self.opener = util.opener(self.path)
34 34 self.transplants = []
35 35 self.dirty = False
36 36 self.read()
37 37
38 38 def read(self):
39 39 abspath = os.path.join(self.path, self.transplantfile)
40 40 if self.transplantfile and os.path.exists(abspath):
41 41 for line in self.opener(self.transplantfile).read().splitlines():
42 42 lnode, rnode = map(revlog.bin, line.split(':'))
43 43 self.transplants.append(transplantentry(lnode, rnode))
44 44
45 45 def write(self):
46 46 if self.dirty and self.transplantfile:
47 47 if not os.path.isdir(self.path):
48 48 os.mkdir(self.path)
49 49 fp = self.opener(self.transplantfile, 'w')
50 50 for c in self.transplants:
51 51 l, r = map(revlog.hex, (c.lnode, c.rnode))
52 52 fp.write(l + ':' + r + '\n')
53 53 fp.close()
54 54 self.dirty = False
55 55
56 56 def get(self, rnode):
57 57 return [t for t in self.transplants if t.rnode == rnode]
58 58
59 59 def set(self, lnode, rnode):
60 60 self.transplants.append(transplantentry(lnode, rnode))
61 61 self.dirty = True
62 62
63 63 def remove(self, transplant):
64 64 del self.transplants[self.transplants.index(transplant)]
65 65 self.dirty = True
66 66
67 67 class transplanter:
68 68 def __init__(self, ui, repo):
69 69 self.ui = ui
70 70 self.path = repo.join('transplant')
71 71 self.opener = util.opener(self.path)
72 72 self.transplants = transplants(self.path, 'transplants', opener=self.opener)
73 73
74 74 def applied(self, repo, node, parent):
75 75 '''returns True if a node is already an ancestor of parent
76 76 or has already been transplanted'''
77 77 if hasnode(repo, node):
78 78 if node in repo.changelog.reachable(parent, stop=node):
79 79 return True
80 80 for t in self.transplants.get(node):
81 81 # it might have been stripped
82 82 if not hasnode(repo, t.lnode):
83 83 self.transplants.remove(t)
84 84 return False
85 85 if t.lnode in repo.changelog.reachable(parent, stop=t.lnode):
86 86 return True
87 87 return False
88 88
89 89 def apply(self, repo, source, revmap, merges, opts={}):
90 90 '''apply the revisions in revmap one by one in revision order'''
91 91 revs = util.sort(revmap)
92 92 p1, p2 = repo.dirstate.parents()
93 93 pulls = []
94 94 diffopts = patch.diffopts(self.ui, opts)
95 95 diffopts.git = True
96 96
97 97 lock = wlock = None
98 98 try:
99 99 wlock = repo.wlock()
100 100 lock = repo.lock()
101 101 for rev in revs:
102 102 node = revmap[rev]
103 103 revstr = '%s:%s' % (rev, revlog.short(node))
104 104
105 105 if self.applied(repo, node, p1):
106 106 self.ui.warn(_('skipping already applied revision %s\n') %
107 107 revstr)
108 108 continue
109 109
110 110 parents = source.changelog.parents(node)
111 111 if not opts.get('filter'):
112 112 # If the changeset parent is the same as the wdir's parent,
113 113 # just pull it.
114 114 if parents[0] == p1:
115 115 pulls.append(node)
116 116 p1 = node
117 117 continue
118 118 if pulls:
119 119 if source != repo:
120 120 repo.pull(source, heads=pulls)
121 121 merge.update(repo, pulls[-1], False, False, None)
122 122 p1, p2 = repo.dirstate.parents()
123 123 pulls = []
124 124
125 125 domerge = False
126 126 if node in merges:
127 127 # pulling all the merge revs at once would mean we couldn't
128 128 # transplant after the latest even if transplants before them
129 129 # fail.
130 130 domerge = True
131 131 if not hasnode(repo, node):
132 132 repo.pull(source, heads=[node])
133 133
134 134 if parents[1] != revlog.nullid:
135 135 self.ui.note(_('skipping merge changeset %s:%s\n')
136 136 % (rev, revlog.short(node)))
137 137 patchfile = None
138 138 else:
139 139 fd, patchfile = tempfile.mkstemp(prefix='hg-transplant-')
140 140 fp = os.fdopen(fd, 'w')
141 patch.diff(source, parents[0], node, fp=fp, opts=diffopts)
141 gen = patch.diff(source, parents[0], node, opts=diffopts)
142 for chunk in gen:
143 fp.write(chunk)
142 144 fp.close()
143 145
144 146 del revmap[rev]
145 147 if patchfile or domerge:
146 148 try:
147 149 n = self.applyone(repo, node,
148 150 source.changelog.read(node),
149 151 patchfile, merge=domerge,
150 152 log=opts.get('log'),
151 153 filter=opts.get('filter'))
152 154 if n and domerge:
153 155 self.ui.status(_('%s merged at %s\n') % (revstr,
154 156 revlog.short(n)))
155 157 elif n:
156 158 self.ui.status(_('%s transplanted to %s\n') % (revlog.short(node),
157 159 revlog.short(n)))
158 160 finally:
159 161 if patchfile:
160 162 os.unlink(patchfile)
161 163 if pulls:
162 164 repo.pull(source, heads=pulls)
163 165 merge.update(repo, pulls[-1], False, False, None)
164 166 finally:
165 167 self.saveseries(revmap, merges)
166 168 self.transplants.write()
167 169 del lock, wlock
168 170
169 171 def filter(self, filter, changelog, patchfile):
170 172 '''arbitrarily rewrite changeset before applying it'''
171 173
172 174 self.ui.status(_('filtering %s\n') % patchfile)
173 175 user, date, msg = (changelog[1], changelog[2], changelog[4])
174 176
175 177 fd, headerfile = tempfile.mkstemp(prefix='hg-transplant-')
176 178 fp = os.fdopen(fd, 'w')
177 179 fp.write("# HG changeset patch\n")
178 180 fp.write("# User %s\n" % user)
179 181 fp.write("# Date %d %d\n" % date)
180 182 fp.write(changelog[4])
181 183 fp.close()
182 184
183 185 try:
184 186 util.system('%s %s %s' % (filter, util.shellquote(headerfile),
185 187 util.shellquote(patchfile)),
186 188 environ={'HGUSER': changelog[1]},
187 189 onerr=util.Abort, errprefix=_('filter failed'))
188 190 user, date, msg = self.parselog(file(headerfile))[1:4]
189 191 finally:
190 192 os.unlink(headerfile)
191 193
192 194 return (user, date, msg)
193 195
194 196 def applyone(self, repo, node, cl, patchfile, merge=False, log=False,
195 197 filter=None):
196 198 '''apply the patch in patchfile to the repository as a transplant'''
197 199 (manifest, user, (time, timezone), files, message) = cl[:5]
198 200 date = "%d %d" % (time, timezone)
199 201 extra = {'transplant_source': node}
200 202 if filter:
201 203 (user, date, message) = self.filter(filter, cl, patchfile)
202 204
203 205 if log:
204 206 message += '\n(transplanted from %s)' % revlog.hex(node)
205 207
206 208 self.ui.status(_('applying %s\n') % revlog.short(node))
207 209 self.ui.note('%s %s\n%s\n' % (user, date, message))
208 210
209 211 if not patchfile and not merge:
210 212 raise util.Abort(_('can only omit patchfile if merging'))
211 213 if patchfile:
212 214 try:
213 215 files = {}
214 216 try:
215 217 fuzz = patch.patch(patchfile, self.ui, cwd=repo.root,
216 218 files=files)
217 219 if not files:
218 220 self.ui.warn(_('%s: empty changeset') % revlog.hex(node))
219 221 return None
220 222 finally:
221 223 files = patch.updatedir(self.ui, repo, files)
222 224 except Exception, inst:
223 225 if filter:
224 226 os.unlink(patchfile)
225 227 seriespath = os.path.join(self.path, 'series')
226 228 if os.path.exists(seriespath):
227 229 os.unlink(seriespath)
228 230 p1 = repo.dirstate.parents()[0]
229 231 p2 = node
230 232 self.log(user, date, message, p1, p2, merge=merge)
231 233 self.ui.write(str(inst) + '\n')
232 234 raise util.Abort(_('Fix up the merge and run hg transplant --continue'))
233 235 else:
234 236 files = None
235 237 if merge:
236 238 p1, p2 = repo.dirstate.parents()
237 239 repo.dirstate.setparents(p1, node)
238 240
239 241 n = repo.commit(files, message, user, date, extra=extra)
240 242 if not merge:
241 243 self.transplants.set(n, node)
242 244
243 245 return n
244 246
245 247 def resume(self, repo, source, opts=None):
246 248 '''recover last transaction and apply remaining changesets'''
247 249 if os.path.exists(os.path.join(self.path, 'journal')):
248 250 n, node = self.recover(repo)
249 251 self.ui.status(_('%s transplanted as %s\n') % (revlog.short(node),
250 252 revlog.short(n)))
251 253 seriespath = os.path.join(self.path, 'series')
252 254 if not os.path.exists(seriespath):
253 255 self.transplants.write()
254 256 return
255 257 nodes, merges = self.readseries()
256 258 revmap = {}
257 259 for n in nodes:
258 260 revmap[source.changelog.rev(n)] = n
259 261 os.unlink(seriespath)
260 262
261 263 self.apply(repo, source, revmap, merges, opts)
262 264
263 265 def recover(self, repo):
264 266 '''commit working directory using journal metadata'''
265 267 node, user, date, message, parents = self.readlog()
266 268 merge = len(parents) == 2
267 269
268 270 if not user or not date or not message or not parents[0]:
269 271 raise util.Abort(_('transplant log file is corrupt'))
270 272
271 273 extra = {'transplant_source': node}
272 274 wlock = repo.wlock()
273 275 try:
274 276 p1, p2 = repo.dirstate.parents()
275 277 if p1 != parents[0]:
276 278 raise util.Abort(
277 279 _('working dir not at transplant parent %s') %
278 280 revlog.hex(parents[0]))
279 281 if merge:
280 282 repo.dirstate.setparents(p1, parents[1])
281 283 n = repo.commit(None, message, user, date, extra=extra)
282 284 if not n:
283 285 raise util.Abort(_('commit failed'))
284 286 if not merge:
285 287 self.transplants.set(n, node)
286 288 self.unlog()
287 289
288 290 return n, node
289 291 finally:
290 292 del wlock
291 293
292 294 def readseries(self):
293 295 nodes = []
294 296 merges = []
295 297 cur = nodes
296 298 for line in self.opener('series').read().splitlines():
297 299 if line.startswith('# Merges'):
298 300 cur = merges
299 301 continue
300 302 cur.append(revlog.bin(line))
301 303
302 304 return (nodes, merges)
303 305
304 306 def saveseries(self, revmap, merges):
305 307 if not revmap:
306 308 return
307 309
308 310 if not os.path.isdir(self.path):
309 311 os.mkdir(self.path)
310 312 series = self.opener('series', 'w')
311 313 for rev in util.sort(revmap):
312 314 series.write(revlog.hex(revmap[rev]) + '\n')
313 315 if merges:
314 316 series.write('# Merges\n')
315 317 for m in merges:
316 318 series.write(revlog.hex(m) + '\n')
317 319 series.close()
318 320
319 321 def parselog(self, fp):
320 322 parents = []
321 323 message = []
322 324 node = revlog.nullid
323 325 inmsg = False
324 326 for line in fp.read().splitlines():
325 327 if inmsg:
326 328 message.append(line)
327 329 elif line.startswith('# User '):
328 330 user = line[7:]
329 331 elif line.startswith('# Date '):
330 332 date = line[7:]
331 333 elif line.startswith('# Node ID '):
332 334 node = revlog.bin(line[10:])
333 335 elif line.startswith('# Parent '):
334 336 parents.append(revlog.bin(line[9:]))
335 337 elif not line.startswith('#'):
336 338 inmsg = True
337 339 message.append(line)
338 340 return (node, user, date, '\n'.join(message), parents)
339 341
340 342 def log(self, user, date, message, p1, p2, merge=False):
341 343 '''journal changelog metadata for later recover'''
342 344
343 345 if not os.path.isdir(self.path):
344 346 os.mkdir(self.path)
345 347 fp = self.opener('journal', 'w')
346 348 fp.write('# User %s\n' % user)
347 349 fp.write('# Date %s\n' % date)
348 350 fp.write('# Node ID %s\n' % revlog.hex(p2))
349 351 fp.write('# Parent ' + revlog.hex(p1) + '\n')
350 352 if merge:
351 353 fp.write('# Parent ' + revlog.hex(p2) + '\n')
352 354 fp.write(message.rstrip() + '\n')
353 355 fp.close()
354 356
355 357 def readlog(self):
356 358 return self.parselog(self.opener('journal'))
357 359
358 360 def unlog(self):
359 361 '''remove changelog journal'''
360 362 absdst = os.path.join(self.path, 'journal')
361 363 if os.path.exists(absdst):
362 364 os.unlink(absdst)
363 365
364 366 def transplantfilter(self, repo, source, root):
365 367 def matchfn(node):
366 368 if self.applied(repo, node, root):
367 369 return False
368 370 if source.changelog.parents(node)[1] != revlog.nullid:
369 371 return False
370 372 extra = source.changelog.read(node)[5]
371 373 cnode = extra.get('transplant_source')
372 374 if cnode and self.applied(repo, cnode, root):
373 375 return False
374 376 return True
375 377
376 378 return matchfn
377 379
378 380 def hasnode(repo, node):
379 381 try:
380 382 return repo.changelog.rev(node) != None
381 383 except revlog.RevlogError:
382 384 return False
383 385
384 386 def browserevs(ui, repo, nodes, opts):
385 387 '''interactively transplant changesets'''
386 388 def browsehelp(ui):
387 389 ui.write('y: transplant this changeset\n'
388 390 'n: skip this changeset\n'
389 391 'm: merge at this changeset\n'
390 392 'p: show patch\n'
391 393 'c: commit selected changesets\n'
392 394 'q: cancel transplant\n'
393 395 '?: show this help\n')
394 396
395 397 displayer = cmdutil.show_changeset(ui, repo, opts)
396 398 transplants = []
397 399 merges = []
398 400 for node in nodes:
399 401 displayer.show(changenode=node)
400 402 action = None
401 403 while not action:
402 404 action = ui.prompt(_('apply changeset? [ynmpcq?]:'))
403 405 if action == '?':
404 406 browsehelp(ui)
405 407 action = None
406 408 elif action == 'p':
407 409 parent = repo.changelog.parents(node)[0]
408 patch.diff(repo, parent, node)
410 for chunk in patch.diff(repo, parent, node):
411 repo.ui.write(chunk)
409 412 action = None
410 413 elif action not in ('y', 'n', 'm', 'c', 'q'):
411 414 ui.write('no such option\n')
412 415 action = None
413 416 if action == 'y':
414 417 transplants.append(node)
415 418 elif action == 'm':
416 419 merges.append(node)
417 420 elif action == 'c':
418 421 break
419 422 elif action == 'q':
420 423 transplants = ()
421 424 merges = ()
422 425 break
423 426 return (transplants, merges)
424 427
425 428 def transplant(ui, repo, *revs, **opts):
426 429 '''transplant changesets from another branch
427 430
428 431 Selected changesets will be applied on top of the current working
429 432 directory with the log of the original changeset. If --log is
430 433 specified, log messages will have a comment appended of the form:
431 434
432 435 (transplanted from CHANGESETHASH)
433 436
434 437 You can rewrite the changelog message with the --filter option.
435 438 Its argument will be invoked with the current changelog message
436 439 as $1 and the patch as $2.
437 440
438 441 If --source is specified, selects changesets from the named
439 442 repository. If --branch is specified, selects changesets from the
440 443 branch holding the named revision, up to that revision. If --all
441 444 is specified, all changesets on the branch will be transplanted,
442 445 otherwise you will be prompted to select the changesets you want.
443 446
444 447 hg transplant --branch REVISION --all will rebase the selected branch
445 448 (up to the named revision) onto your current working directory.
446 449
447 450 You can optionally mark selected transplanted changesets as
448 451 merge changesets. You will not be prompted to transplant any
449 452 ancestors of a merged transplant, and you can merge descendants
450 453 of them normally instead of transplanting them.
451 454
452 455 If no merges or revisions are provided, hg transplant will start
453 456 an interactive changeset browser.
454 457
455 458 If a changeset application fails, you can fix the merge by hand and
456 459 then resume where you left off by calling hg transplant --continue.
457 460 '''
458 461 def getremotechanges(repo, url):
459 462 sourcerepo = ui.expandpath(url)
460 463 source = hg.repository(ui, sourcerepo)
461 464 incoming = repo.findincoming(source, force=True)
462 465 if not incoming:
463 466 return (source, None, None)
464 467
465 468 bundle = None
466 469 if not source.local():
467 470 cg = source.changegroup(incoming, 'incoming')
468 471 bundle = changegroup.writebundle(cg, None, 'HG10UN')
469 472 source = bundlerepo.bundlerepository(ui, repo.root, bundle)
470 473
471 474 return (source, incoming, bundle)
472 475
473 476 def incwalk(repo, incoming, branches, match=util.always):
474 477 if not branches:
475 478 branches=None
476 479 for node in repo.changelog.nodesbetween(incoming, branches)[0]:
477 480 if match(node):
478 481 yield node
479 482
480 483 def transplantwalk(repo, root, branches, match=util.always):
481 484 if not branches:
482 485 branches = repo.heads()
483 486 ancestors = []
484 487 for branch in branches:
485 488 ancestors.append(repo.changelog.ancestor(root, branch))
486 489 for node in repo.changelog.nodesbetween(ancestors, branches)[0]:
487 490 if match(node):
488 491 yield node
489 492
490 493 def checkopts(opts, revs):
491 494 if opts.get('continue'):
492 495 if filter(lambda opt: opts.get(opt), ('branch', 'all', 'merge')):
493 496 raise util.Abort(_('--continue is incompatible with branch, all or merge'))
494 497 return
495 498 if not (opts.get('source') or revs or
496 499 opts.get('merge') or opts.get('branch')):
497 500 raise util.Abort(_('no source URL, branch tag or revision list provided'))
498 501 if opts.get('all'):
499 502 if not opts.get('branch'):
500 503 raise util.Abort(_('--all requires a branch revision'))
501 504 if revs:
502 505 raise util.Abort(_('--all is incompatible with a revision list'))
503 506
504 507 checkopts(opts, revs)
505 508
506 509 if not opts.get('log'):
507 510 opts['log'] = ui.config('transplant', 'log')
508 511 if not opts.get('filter'):
509 512 opts['filter'] = ui.config('transplant', 'filter')
510 513
511 514 tp = transplanter(ui, repo)
512 515
513 516 p1, p2 = repo.dirstate.parents()
514 517 if p1 == revlog.nullid:
515 518 raise util.Abort(_('no revision checked out'))
516 519 if not opts.get('continue'):
517 520 if p2 != revlog.nullid:
518 521 raise util.Abort(_('outstanding uncommitted merges'))
519 522 m, a, r, d = repo.status()[:4]
520 523 if m or a or r or d:
521 524 raise util.Abort(_('outstanding local changes'))
522 525
523 526 bundle = None
524 527 source = opts.get('source')
525 528 if source:
526 529 (source, incoming, bundle) = getremotechanges(repo, source)
527 530 else:
528 531 source = repo
529 532
530 533 try:
531 534 if opts.get('continue'):
532 535 tp.resume(repo, source, opts)
533 536 return
534 537
535 538 tf=tp.transplantfilter(repo, source, p1)
536 539 if opts.get('prune'):
537 540 prune = [source.lookup(r)
538 541 for r in cmdutil.revrange(source, opts.get('prune'))]
539 542 matchfn = lambda x: tf(x) and x not in prune
540 543 else:
541 544 matchfn = tf
542 545 branches = map(source.lookup, opts.get('branch', ()))
543 546 merges = map(source.lookup, opts.get('merge', ()))
544 547 revmap = {}
545 548 if revs:
546 549 for r in cmdutil.revrange(source, revs):
547 550 revmap[int(r)] = source.lookup(r)
548 551 elif opts.get('all') or not merges:
549 552 if source != repo:
550 553 alltransplants = incwalk(source, incoming, branches, match=matchfn)
551 554 else:
552 555 alltransplants = transplantwalk(source, p1, branches, match=matchfn)
553 556 if opts.get('all'):
554 557 revs = alltransplants
555 558 else:
556 559 revs, newmerges = browserevs(ui, source, alltransplants, opts)
557 560 merges.extend(newmerges)
558 561 for r in revs:
559 562 revmap[source.changelog.rev(r)] = r
560 563 for r in merges:
561 564 revmap[source.changelog.rev(r)] = r
562 565
563 566 tp.apply(repo, source, revmap, merges, opts)
564 567 finally:
565 568 if bundle:
566 569 source.close()
567 570 os.unlink(bundle)
568 571
569 572 cmdtable = {
570 573 "transplant":
571 574 (transplant,
572 575 [('s', 'source', '', _('pull patches from REPOSITORY')),
573 576 ('b', 'branch', [], _('pull patches from branch BRANCH')),
574 577 ('a', 'all', None, _('pull all changesets up to BRANCH')),
575 578 ('p', 'prune', [], _('skip over REV')),
576 579 ('m', 'merge', [], _('merge at REV')),
577 580 ('', 'log', None, _('append transplant info to log message')),
578 581 ('c', 'continue', None, _('continue last transplant session after repair')),
579 582 ('', 'filter', '', _('filter changesets through FILTER'))],
580 583 _('hg transplant [-s REPOSITORY] [-b BRANCH [-a]] [-p REV] [-m REV] [REV]...'))
581 584 }
@@ -1,1188 +1,1190
1 1 # cmdutil.py - help for command processing in mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from node import hex, nullid, nullrev, short
9 9 from i18n import _
10 10 import os, sys, bisect, stat
11 11 import mdiff, bdiff, util, templater, templatefilters, patch, errno
12 12 import match as _match
13 13
14 14 revrangesep = ':'
15 15
16 16 class UnknownCommand(Exception):
17 17 """Exception raised if command is not in the command table."""
18 18 class AmbiguousCommand(Exception):
19 19 """Exception raised if command shortcut matches more than one command."""
20 20
21 21 def findpossible(cmd, table, strict=False):
22 22 """
23 23 Return cmd -> (aliases, command table entry)
24 24 for each matching command.
25 25 Return debug commands (or their aliases) only if no normal command matches.
26 26 """
27 27 choice = {}
28 28 debugchoice = {}
29 29 for e in table.keys():
30 30 aliases = e.lstrip("^").split("|")
31 31 found = None
32 32 if cmd in aliases:
33 33 found = cmd
34 34 elif not strict:
35 35 for a in aliases:
36 36 if a.startswith(cmd):
37 37 found = a
38 38 break
39 39 if found is not None:
40 40 if aliases[0].startswith("debug") or found.startswith("debug"):
41 41 debugchoice[found] = (aliases, table[e])
42 42 else:
43 43 choice[found] = (aliases, table[e])
44 44
45 45 if not choice and debugchoice:
46 46 choice = debugchoice
47 47
48 48 return choice
49 49
50 50 def findcmd(cmd, table, strict=True):
51 51 """Return (aliases, command table entry) for command string."""
52 52 choice = findpossible(cmd, table, strict)
53 53
54 54 if cmd in choice:
55 55 return choice[cmd]
56 56
57 57 if len(choice) > 1:
58 58 clist = choice.keys()
59 59 clist.sort()
60 60 raise AmbiguousCommand(cmd, clist)
61 61
62 62 if choice:
63 63 return choice.values()[0]
64 64
65 65 raise UnknownCommand(cmd)
66 66
67 67 def bail_if_changed(repo):
68 68 if repo.dirstate.parents()[1] != nullid:
69 69 raise util.Abort(_('outstanding uncommitted merge'))
70 70 modified, added, removed, deleted = repo.status()[:4]
71 71 if modified or added or removed or deleted:
72 72 raise util.Abort(_("outstanding uncommitted changes"))
73 73
74 74 def logmessage(opts):
75 75 """ get the log message according to -m and -l option """
76 76 message = opts['message']
77 77 logfile = opts['logfile']
78 78
79 79 if message and logfile:
80 80 raise util.Abort(_('options --message and --logfile are mutually '
81 81 'exclusive'))
82 82 if not message and logfile:
83 83 try:
84 84 if logfile == '-':
85 85 message = sys.stdin.read()
86 86 else:
87 87 message = open(logfile).read()
88 88 except IOError, inst:
89 89 raise util.Abort(_("can't read commit message '%s': %s") %
90 90 (logfile, inst.strerror))
91 91 return message
92 92
93 93 def loglimit(opts):
94 94 """get the log limit according to option -l/--limit"""
95 95 limit = opts.get('limit')
96 96 if limit:
97 97 try:
98 98 limit = int(limit)
99 99 except ValueError:
100 100 raise util.Abort(_('limit must be a positive integer'))
101 101 if limit <= 0: raise util.Abort(_('limit must be positive'))
102 102 else:
103 103 limit = sys.maxint
104 104 return limit
105 105
106 106 def setremoteconfig(ui, opts):
107 107 "copy remote options to ui tree"
108 108 if opts.get('ssh'):
109 109 ui.setconfig("ui", "ssh", opts['ssh'])
110 110 if opts.get('remotecmd'):
111 111 ui.setconfig("ui", "remotecmd", opts['remotecmd'])
112 112
113 113 def revpair(repo, revs):
114 114 '''return pair of nodes, given list of revisions. second item can
115 115 be None, meaning use working dir.'''
116 116
117 117 def revfix(repo, val, defval):
118 118 if not val and val != 0 and defval is not None:
119 119 val = defval
120 120 return repo.lookup(val)
121 121
122 122 if not revs:
123 123 return repo.dirstate.parents()[0], None
124 124 end = None
125 125 if len(revs) == 1:
126 126 if revrangesep in revs[0]:
127 127 start, end = revs[0].split(revrangesep, 1)
128 128 start = revfix(repo, start, 0)
129 129 end = revfix(repo, end, len(repo) - 1)
130 130 else:
131 131 start = revfix(repo, revs[0], None)
132 132 elif len(revs) == 2:
133 133 if revrangesep in revs[0] or revrangesep in revs[1]:
134 134 raise util.Abort(_('too many revisions specified'))
135 135 start = revfix(repo, revs[0], None)
136 136 end = revfix(repo, revs[1], None)
137 137 else:
138 138 raise util.Abort(_('too many revisions specified'))
139 139 return start, end
140 140
141 141 def revrange(repo, revs):
142 142 """Yield revision as strings from a list of revision specifications."""
143 143
144 144 def revfix(repo, val, defval):
145 145 if not val and val != 0 and defval is not None:
146 146 return defval
147 147 return repo.changelog.rev(repo.lookup(val))
148 148
149 149 seen, l = {}, []
150 150 for spec in revs:
151 151 if revrangesep in spec:
152 152 start, end = spec.split(revrangesep, 1)
153 153 start = revfix(repo, start, 0)
154 154 end = revfix(repo, end, len(repo) - 1)
155 155 step = start > end and -1 or 1
156 156 for rev in xrange(start, end+step, step):
157 157 if rev in seen:
158 158 continue
159 159 seen[rev] = 1
160 160 l.append(rev)
161 161 else:
162 162 rev = revfix(repo, spec, None)
163 163 if rev in seen:
164 164 continue
165 165 seen[rev] = 1
166 166 l.append(rev)
167 167
168 168 return l
169 169
170 170 def make_filename(repo, pat, node,
171 171 total=None, seqno=None, revwidth=None, pathname=None):
172 172 node_expander = {
173 173 'H': lambda: hex(node),
174 174 'R': lambda: str(repo.changelog.rev(node)),
175 175 'h': lambda: short(node),
176 176 }
177 177 expander = {
178 178 '%': lambda: '%',
179 179 'b': lambda: os.path.basename(repo.root),
180 180 }
181 181
182 182 try:
183 183 if node:
184 184 expander.update(node_expander)
185 185 if node:
186 186 expander['r'] = (lambda:
187 187 str(repo.changelog.rev(node)).zfill(revwidth or 0))
188 188 if total is not None:
189 189 expander['N'] = lambda: str(total)
190 190 if seqno is not None:
191 191 expander['n'] = lambda: str(seqno)
192 192 if total is not None and seqno is not None:
193 193 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
194 194 if pathname is not None:
195 195 expander['s'] = lambda: os.path.basename(pathname)
196 196 expander['d'] = lambda: os.path.dirname(pathname) or '.'
197 197 expander['p'] = lambda: pathname
198 198
199 199 newname = []
200 200 patlen = len(pat)
201 201 i = 0
202 202 while i < patlen:
203 203 c = pat[i]
204 204 if c == '%':
205 205 i += 1
206 206 c = pat[i]
207 207 c = expander[c]()
208 208 newname.append(c)
209 209 i += 1
210 210 return ''.join(newname)
211 211 except KeyError, inst:
212 212 raise util.Abort(_("invalid format spec '%%%s' in output file name") %
213 213 inst.args[0])
214 214
215 215 def make_file(repo, pat, node=None,
216 216 total=None, seqno=None, revwidth=None, mode='wb', pathname=None):
217 217 if not pat or pat == '-':
218 218 return 'w' in mode and sys.stdout or sys.stdin
219 219 if hasattr(pat, 'write') and 'w' in mode:
220 220 return pat
221 221 if hasattr(pat, 'read') and 'r' in mode:
222 222 return pat
223 223 return open(make_filename(repo, pat, node, total, seqno, revwidth,
224 224 pathname),
225 225 mode)
226 226
227 227 def match(repo, pats=[], opts={}, globbed=False, default='relpath'):
228 228 if not globbed and default == 'relpath':
229 229 pats = util.expand_glob(pats or [])
230 230 m = _match.match(repo.root, repo.getcwd(), pats,
231 231 opts.get('include'), opts.get('exclude'), default)
232 232 def badfn(f, msg):
233 233 repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
234 234 return False
235 235 m.bad = badfn
236 236 return m
237 237
238 238 def matchall(repo):
239 239 return _match.always(repo.root, repo.getcwd())
240 240
241 241 def matchfiles(repo, files):
242 242 return _match.exact(repo.root, repo.getcwd(), files)
243 243
244 244 def findrenames(repo, added=None, removed=None, threshold=0.5):
245 245 '''find renamed files -- yields (before, after, score) tuples'''
246 246 if added is None or removed is None:
247 247 added, removed = repo.status()[1:3]
248 248 ctx = repo['.']
249 249 for a in added:
250 250 aa = repo.wread(a)
251 251 bestname, bestscore = None, threshold
252 252 for r in removed:
253 253 rr = ctx.filectx(r).data()
254 254
255 255 # bdiff.blocks() returns blocks of matching lines
256 256 # count the number of bytes in each
257 257 equal = 0
258 258 alines = mdiff.splitnewlines(aa)
259 259 matches = bdiff.blocks(aa, rr)
260 260 for x1,x2,y1,y2 in matches:
261 261 for line in alines[x1:x2]:
262 262 equal += len(line)
263 263
264 264 lengths = len(aa) + len(rr)
265 265 if lengths:
266 266 myscore = equal*2.0 / lengths
267 267 if myscore >= bestscore:
268 268 bestname, bestscore = r, myscore
269 269 if bestname:
270 270 yield bestname, a, bestscore
271 271
272 272 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
273 273 if dry_run is None:
274 274 dry_run = opts.get('dry_run')
275 275 if similarity is None:
276 276 similarity = float(opts.get('similarity') or 0)
277 277 add, remove = [], []
278 278 mapping = {}
279 279 audit_path = util.path_auditor(repo.root)
280 280 m = match(repo, pats, opts)
281 281 for abs in repo.walk(m):
282 282 target = repo.wjoin(abs)
283 283 good = True
284 284 try:
285 285 audit_path(abs)
286 286 except:
287 287 good = False
288 288 rel = m.rel(abs)
289 289 exact = m.exact(abs)
290 290 if good and abs not in repo.dirstate:
291 291 add.append(abs)
292 292 mapping[abs] = rel, m.exact(abs)
293 293 if repo.ui.verbose or not exact:
294 294 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
295 295 if repo.dirstate[abs] != 'r' and (not good or not util.lexists(target)
296 296 or (os.path.isdir(target) and not os.path.islink(target))):
297 297 remove.append(abs)
298 298 mapping[abs] = rel, exact
299 299 if repo.ui.verbose or not exact:
300 300 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
301 301 if not dry_run:
302 302 repo.remove(remove)
303 303 repo.add(add)
304 304 if similarity > 0:
305 305 for old, new, score in findrenames(repo, add, remove, similarity):
306 306 oldrel, oldexact = mapping[old]
307 307 newrel, newexact = mapping[new]
308 308 if repo.ui.verbose or not oldexact or not newexact:
309 309 repo.ui.status(_('recording removal of %s as rename to %s '
310 310 '(%d%% similar)\n') %
311 311 (oldrel, newrel, score * 100))
312 312 if not dry_run:
313 313 repo.copy(old, new)
314 314
315 315 def copy(ui, repo, pats, opts, rename=False):
316 316 # called with the repo lock held
317 317 #
318 318 # hgsep => pathname that uses "/" to separate directories
319 319 # ossep => pathname that uses os.sep to separate directories
320 320 cwd = repo.getcwd()
321 321 targets = {}
322 322 after = opts.get("after")
323 323 dryrun = opts.get("dry_run")
324 324
325 325 def walkpat(pat):
326 326 srcs = []
327 327 m = match(repo, [pat], opts, globbed=True)
328 328 for abs in repo.walk(m):
329 329 state = repo.dirstate[abs]
330 330 rel = m.rel(abs)
331 331 exact = m.exact(abs)
332 332 if state in '?r':
333 333 if exact and state == '?':
334 334 ui.warn(_('%s: not copying - file is not managed\n') % rel)
335 335 if exact and state == 'r':
336 336 ui.warn(_('%s: not copying - file has been marked for'
337 337 ' remove\n') % rel)
338 338 continue
339 339 # abs: hgsep
340 340 # rel: ossep
341 341 srcs.append((abs, rel, exact))
342 342 return srcs
343 343
344 344 # abssrc: hgsep
345 345 # relsrc: ossep
346 346 # otarget: ossep
347 347 def copyfile(abssrc, relsrc, otarget, exact):
348 348 abstarget = util.canonpath(repo.root, cwd, otarget)
349 349 reltarget = repo.pathto(abstarget, cwd)
350 350 target = repo.wjoin(abstarget)
351 351 src = repo.wjoin(abssrc)
352 352 state = repo.dirstate[abstarget]
353 353
354 354 # check for collisions
355 355 prevsrc = targets.get(abstarget)
356 356 if prevsrc is not None:
357 357 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
358 358 (reltarget, repo.pathto(abssrc, cwd),
359 359 repo.pathto(prevsrc, cwd)))
360 360 return
361 361
362 362 # check for overwrites
363 363 exists = os.path.exists(target)
364 364 if (not after and exists or after and state in 'mn'):
365 365 if not opts['force']:
366 366 ui.warn(_('%s: not overwriting - file exists\n') %
367 367 reltarget)
368 368 return
369 369
370 370 if after:
371 371 if not exists:
372 372 return
373 373 elif not dryrun:
374 374 try:
375 375 if exists:
376 376 os.unlink(target)
377 377 targetdir = os.path.dirname(target) or '.'
378 378 if not os.path.isdir(targetdir):
379 379 os.makedirs(targetdir)
380 380 util.copyfile(src, target)
381 381 except IOError, inst:
382 382 if inst.errno == errno.ENOENT:
383 383 ui.warn(_('%s: deleted in working copy\n') % relsrc)
384 384 else:
385 385 ui.warn(_('%s: cannot copy - %s\n') %
386 386 (relsrc, inst.strerror))
387 387 return True # report a failure
388 388
389 389 if ui.verbose or not exact:
390 390 action = rename and "moving" or "copying"
391 391 ui.status(_('%s %s to %s\n') % (action, relsrc, reltarget))
392 392
393 393 targets[abstarget] = abssrc
394 394
395 395 # fix up dirstate
396 396 origsrc = repo.dirstate.copied(abssrc) or abssrc
397 397 if abstarget == origsrc: # copying back a copy?
398 398 if state not in 'mn' and not dryrun:
399 399 repo.dirstate.normallookup(abstarget)
400 400 else:
401 401 if repo.dirstate[origsrc] == 'a' and origsrc == abssrc:
402 402 if not ui.quiet:
403 403 ui.warn(_("%s has not been committed yet, so no copy "
404 404 "data will be stored for %s.\n")
405 405 % (repo.pathto(origsrc, cwd), reltarget))
406 406 if repo.dirstate[abstarget] in '?r' and not dryrun:
407 407 repo.add([abstarget])
408 408 elif not dryrun:
409 409 repo.copy(origsrc, abstarget)
410 410
411 411 if rename and not dryrun:
412 412 repo.remove([abssrc], not after)
413 413
414 414 # pat: ossep
415 415 # dest ossep
416 416 # srcs: list of (hgsep, hgsep, ossep, bool)
417 417 # return: function that takes hgsep and returns ossep
418 418 def targetpathfn(pat, dest, srcs):
419 419 if os.path.isdir(pat):
420 420 abspfx = util.canonpath(repo.root, cwd, pat)
421 421 abspfx = util.localpath(abspfx)
422 422 if destdirexists:
423 423 striplen = len(os.path.split(abspfx)[0])
424 424 else:
425 425 striplen = len(abspfx)
426 426 if striplen:
427 427 striplen += len(os.sep)
428 428 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
429 429 elif destdirexists:
430 430 res = lambda p: os.path.join(dest,
431 431 os.path.basename(util.localpath(p)))
432 432 else:
433 433 res = lambda p: dest
434 434 return res
435 435
436 436 # pat: ossep
437 437 # dest ossep
438 438 # srcs: list of (hgsep, hgsep, ossep, bool)
439 439 # return: function that takes hgsep and returns ossep
440 440 def targetpathafterfn(pat, dest, srcs):
441 441 if util.patkind(pat, None)[0]:
442 442 # a mercurial pattern
443 443 res = lambda p: os.path.join(dest,
444 444 os.path.basename(util.localpath(p)))
445 445 else:
446 446 abspfx = util.canonpath(repo.root, cwd, pat)
447 447 if len(abspfx) < len(srcs[0][0]):
448 448 # A directory. Either the target path contains the last
449 449 # component of the source path or it does not.
450 450 def evalpath(striplen):
451 451 score = 0
452 452 for s in srcs:
453 453 t = os.path.join(dest, util.localpath(s[0])[striplen:])
454 454 if os.path.exists(t):
455 455 score += 1
456 456 return score
457 457
458 458 abspfx = util.localpath(abspfx)
459 459 striplen = len(abspfx)
460 460 if striplen:
461 461 striplen += len(os.sep)
462 462 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
463 463 score = evalpath(striplen)
464 464 striplen1 = len(os.path.split(abspfx)[0])
465 465 if striplen1:
466 466 striplen1 += len(os.sep)
467 467 if evalpath(striplen1) > score:
468 468 striplen = striplen1
469 469 res = lambda p: os.path.join(dest,
470 470 util.localpath(p)[striplen:])
471 471 else:
472 472 # a file
473 473 if destdirexists:
474 474 res = lambda p: os.path.join(dest,
475 475 os.path.basename(util.localpath(p)))
476 476 else:
477 477 res = lambda p: dest
478 478 return res
479 479
480 480
481 481 pats = util.expand_glob(pats)
482 482 if not pats:
483 483 raise util.Abort(_('no source or destination specified'))
484 484 if len(pats) == 1:
485 485 raise util.Abort(_('no destination specified'))
486 486 dest = pats.pop()
487 487 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
488 488 if not destdirexists:
489 489 if len(pats) > 1 or util.patkind(pats[0], None)[0]:
490 490 raise util.Abort(_('with multiple sources, destination must be an '
491 491 'existing directory'))
492 492 if util.endswithsep(dest):
493 493 raise util.Abort(_('destination %s is not a directory') % dest)
494 494
495 495 tfn = targetpathfn
496 496 if after:
497 497 tfn = targetpathafterfn
498 498 copylist = []
499 499 for pat in pats:
500 500 srcs = walkpat(pat)
501 501 if not srcs:
502 502 continue
503 503 copylist.append((tfn(pat, dest, srcs), srcs))
504 504 if not copylist:
505 505 raise util.Abort(_('no files to copy'))
506 506
507 507 errors = 0
508 508 for targetpath, srcs in copylist:
509 509 for abssrc, relsrc, exact in srcs:
510 510 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
511 511 errors += 1
512 512
513 513 if errors:
514 514 ui.warn(_('(consider using --after)\n'))
515 515
516 516 return errors
517 517
518 518 def service(opts, parentfn=None, initfn=None, runfn=None):
519 519 '''Run a command as a service.'''
520 520
521 521 if opts['daemon'] and not opts['daemon_pipefds']:
522 522 rfd, wfd = os.pipe()
523 523 args = sys.argv[:]
524 524 args.append('--daemon-pipefds=%d,%d' % (rfd, wfd))
525 525 # Don't pass --cwd to the child process, because we've already
526 526 # changed directory.
527 527 for i in xrange(1,len(args)):
528 528 if args[i].startswith('--cwd='):
529 529 del args[i]
530 530 break
531 531 elif args[i].startswith('--cwd'):
532 532 del args[i:i+2]
533 533 break
534 534 pid = os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0),
535 535 args[0], args)
536 536 os.close(wfd)
537 537 os.read(rfd, 1)
538 538 if parentfn:
539 539 return parentfn(pid)
540 540 else:
541 541 os._exit(0)
542 542
543 543 if initfn:
544 544 initfn()
545 545
546 546 if opts['pid_file']:
547 547 fp = open(opts['pid_file'], 'w')
548 548 fp.write(str(os.getpid()) + '\n')
549 549 fp.close()
550 550
551 551 if opts['daemon_pipefds']:
552 552 rfd, wfd = [int(x) for x in opts['daemon_pipefds'].split(',')]
553 553 os.close(rfd)
554 554 try:
555 555 os.setsid()
556 556 except AttributeError:
557 557 pass
558 558 os.write(wfd, 'y')
559 559 os.close(wfd)
560 560 sys.stdout.flush()
561 561 sys.stderr.flush()
562 562 fd = os.open(util.nulldev, os.O_RDWR)
563 563 if fd != 0: os.dup2(fd, 0)
564 564 if fd != 1: os.dup2(fd, 1)
565 565 if fd != 2: os.dup2(fd, 2)
566 566 if fd not in (0, 1, 2): os.close(fd)
567 567
568 568 if runfn:
569 569 return runfn()
570 570
571 571 class changeset_printer(object):
572 572 '''show changeset information when templating not requested.'''
573 573
574 574 def __init__(self, ui, repo, patch, buffered):
575 575 self.ui = ui
576 576 self.repo = repo
577 577 self.buffered = buffered
578 578 self.patch = patch
579 579 self.header = {}
580 580 self.hunk = {}
581 581 self.lastheader = None
582 582
583 583 def flush(self, rev):
584 584 if rev in self.header:
585 585 h = self.header[rev]
586 586 if h != self.lastheader:
587 587 self.lastheader = h
588 588 self.ui.write(h)
589 589 del self.header[rev]
590 590 if rev in self.hunk:
591 591 self.ui.write(self.hunk[rev])
592 592 del self.hunk[rev]
593 593 return 1
594 594 return 0
595 595
596 596 def show(self, rev=0, changenode=None, copies=(), **props):
597 597 if self.buffered:
598 598 self.ui.pushbuffer()
599 599 self._show(rev, changenode, copies, props)
600 600 self.hunk[rev] = self.ui.popbuffer()
601 601 else:
602 602 self._show(rev, changenode, copies, props)
603 603
604 604 def _show(self, rev, changenode, copies, props):
605 605 '''show a single changeset or file revision'''
606 606 log = self.repo.changelog
607 607 if changenode is None:
608 608 changenode = log.node(rev)
609 609 elif not rev:
610 610 rev = log.rev(changenode)
611 611
612 612 if self.ui.quiet:
613 613 self.ui.write("%d:%s\n" % (rev, short(changenode)))
614 614 return
615 615
616 616 changes = log.read(changenode)
617 617 date = util.datestr(changes[2])
618 618 extra = changes[5]
619 619 branch = extra.get("branch")
620 620
621 621 hexfunc = self.ui.debugflag and hex or short
622 622
623 623 parents = [(p, hexfunc(log.node(p)))
624 624 for p in self._meaningful_parentrevs(log, rev)]
625 625
626 626 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)))
627 627
628 628 # don't show the default branch name
629 629 if branch != 'default':
630 630 branch = util.tolocal(branch)
631 631 self.ui.write(_("branch: %s\n") % branch)
632 632 for tag in self.repo.nodetags(changenode):
633 633 self.ui.write(_("tag: %s\n") % tag)
634 634 for parent in parents:
635 635 self.ui.write(_("parent: %d:%s\n") % parent)
636 636
637 637 if self.ui.debugflag:
638 638 self.ui.write(_("manifest: %d:%s\n") %
639 639 (self.repo.manifest.rev(changes[0]), hex(changes[0])))
640 640 self.ui.write(_("user: %s\n") % changes[1])
641 641 self.ui.write(_("date: %s\n") % date)
642 642
643 643 if self.ui.debugflag:
644 644 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
645 645 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
646 646 files):
647 647 if value:
648 648 self.ui.write("%-12s %s\n" % (key, " ".join(value)))
649 649 elif changes[3] and self.ui.verbose:
650 650 self.ui.write(_("files: %s\n") % " ".join(changes[3]))
651 651 if copies and self.ui.verbose:
652 652 copies = ['%s (%s)' % c for c in copies]
653 653 self.ui.write(_("copies: %s\n") % ' '.join(copies))
654 654
655 655 if extra and self.ui.debugflag:
656 656 for key, value in util.sort(extra.items()):
657 657 self.ui.write(_("extra: %s=%s\n")
658 658 % (key, value.encode('string_escape')))
659 659
660 660 description = changes[4].strip()
661 661 if description:
662 662 if self.ui.verbose:
663 663 self.ui.write(_("description:\n"))
664 664 self.ui.write(description)
665 665 self.ui.write("\n\n")
666 666 else:
667 667 self.ui.write(_("summary: %s\n") %
668 668 description.splitlines()[0])
669 669 self.ui.write("\n")
670 670
671 671 self.showpatch(changenode)
672 672
673 673 def showpatch(self, node):
674 674 if self.patch:
675 675 prev = self.repo.changelog.parents(node)[0]
676 patch.diff(self.repo, prev, node, match=self.patch, fp=self.ui,
677 opts=patch.diffopts(self.ui))
676 chunks = patch.diff(self.repo, prev, node, match=self.patch,
677 opts=patch.diffopts(self.ui))
678 for chunk in chunks:
679 self.ui.write(chunk)
678 680 self.ui.write("\n")
679 681
680 682 def _meaningful_parentrevs(self, log, rev):
681 683 """Return list of meaningful (or all if debug) parentrevs for rev.
682 684
683 685 For merges (two non-nullrev revisions) both parents are meaningful.
684 686 Otherwise the first parent revision is considered meaningful if it
685 687 is not the preceding revision.
686 688 """
687 689 parents = log.parentrevs(rev)
688 690 if not self.ui.debugflag and parents[1] == nullrev:
689 691 if parents[0] >= rev - 1:
690 692 parents = []
691 693 else:
692 694 parents = [parents[0]]
693 695 return parents
694 696
695 697
696 698 class changeset_templater(changeset_printer):
697 699 '''format changeset information.'''
698 700
699 701 def __init__(self, ui, repo, patch, mapfile, buffered):
700 702 changeset_printer.__init__(self, ui, repo, patch, buffered)
701 703 filters = templatefilters.filters.copy()
702 704 filters['formatnode'] = (ui.debugflag and (lambda x: x)
703 705 or (lambda x: x[:12]))
704 706 self.t = templater.templater(mapfile, filters,
705 707 cache={
706 708 'parent': '{rev}:{node|formatnode} ',
707 709 'manifest': '{rev}:{node|formatnode}',
708 710 'filecopy': '{name} ({source})'})
709 711
710 712 def use_template(self, t):
711 713 '''set template string to use'''
712 714 self.t.cache['changeset'] = t
713 715
714 716 def _show(self, rev, changenode, copies, props):
715 717 '''show a single changeset or file revision'''
716 718 log = self.repo.changelog
717 719 if changenode is None:
718 720 changenode = log.node(rev)
719 721 elif not rev:
720 722 rev = log.rev(changenode)
721 723
722 724 changes = log.read(changenode)
723 725
724 726 def showlist(name, values, plural=None, **args):
725 727 '''expand set of values.
726 728 name is name of key in template map.
727 729 values is list of strings or dicts.
728 730 plural is plural of name, if not simply name + 's'.
729 731
730 732 expansion works like this, given name 'foo'.
731 733
732 734 if values is empty, expand 'no_foos'.
733 735
734 736 if 'foo' not in template map, return values as a string,
735 737 joined by space.
736 738
737 739 expand 'start_foos'.
738 740
739 741 for each value, expand 'foo'. if 'last_foo' in template
740 742 map, expand it instead of 'foo' for last key.
741 743
742 744 expand 'end_foos'.
743 745 '''
744 746 if plural: names = plural
745 747 else: names = name + 's'
746 748 if not values:
747 749 noname = 'no_' + names
748 750 if noname in self.t:
749 751 yield self.t(noname, **args)
750 752 return
751 753 if name not in self.t:
752 754 if isinstance(values[0], str):
753 755 yield ' '.join(values)
754 756 else:
755 757 for v in values:
756 758 yield dict(v, **args)
757 759 return
758 760 startname = 'start_' + names
759 761 if startname in self.t:
760 762 yield self.t(startname, **args)
761 763 vargs = args.copy()
762 764 def one(v, tag=name):
763 765 try:
764 766 vargs.update(v)
765 767 except (AttributeError, ValueError):
766 768 try:
767 769 for a, b in v:
768 770 vargs[a] = b
769 771 except ValueError:
770 772 vargs[name] = v
771 773 return self.t(tag, **vargs)
772 774 lastname = 'last_' + name
773 775 if lastname in self.t:
774 776 last = values.pop()
775 777 else:
776 778 last = None
777 779 for v in values:
778 780 yield one(v)
779 781 if last is not None:
780 782 yield one(last, tag=lastname)
781 783 endname = 'end_' + names
782 784 if endname in self.t:
783 785 yield self.t(endname, **args)
784 786
785 787 def showbranches(**args):
786 788 branch = changes[5].get("branch")
787 789 if branch != 'default':
788 790 branch = util.tolocal(branch)
789 791 return showlist('branch', [branch], plural='branches', **args)
790 792
791 793 def showparents(**args):
792 794 parents = [[('rev', p), ('node', hex(log.node(p)))]
793 795 for p in self._meaningful_parentrevs(log, rev)]
794 796 return showlist('parent', parents, **args)
795 797
796 798 def showtags(**args):
797 799 return showlist('tag', self.repo.nodetags(changenode), **args)
798 800
799 801 def showextras(**args):
800 802 for key, value in util.sort(changes[5].items()):
801 803 args = args.copy()
802 804 args.update(dict(key=key, value=value))
803 805 yield self.t('extra', **args)
804 806
805 807 def showcopies(**args):
806 808 c = [{'name': x[0], 'source': x[1]} for x in copies]
807 809 return showlist('file_copy', c, plural='file_copies', **args)
808 810
809 811 files = []
810 812 def getfiles():
811 813 if not files:
812 814 files[:] = self.repo.status(
813 815 log.parents(changenode)[0], changenode)[:3]
814 816 return files
815 817 def showfiles(**args):
816 818 return showlist('file', changes[3], **args)
817 819 def showmods(**args):
818 820 return showlist('file_mod', getfiles()[0], **args)
819 821 def showadds(**args):
820 822 return showlist('file_add', getfiles()[1], **args)
821 823 def showdels(**args):
822 824 return showlist('file_del', getfiles()[2], **args)
823 825 def showmanifest(**args):
824 826 args = args.copy()
825 827 args.update(dict(rev=self.repo.manifest.rev(changes[0]),
826 828 node=hex(changes[0])))
827 829 return self.t('manifest', **args)
828 830
829 831 defprops = {
830 832 'author': changes[1],
831 833 'branches': showbranches,
832 834 'date': changes[2],
833 835 'desc': changes[4].strip(),
834 836 'file_adds': showadds,
835 837 'file_dels': showdels,
836 838 'file_mods': showmods,
837 839 'files': showfiles,
838 840 'file_copies': showcopies,
839 841 'manifest': showmanifest,
840 842 'node': hex(changenode),
841 843 'parents': showparents,
842 844 'rev': rev,
843 845 'tags': showtags,
844 846 'extras': showextras,
845 847 }
846 848 props = props.copy()
847 849 props.update(defprops)
848 850
849 851 try:
850 852 if self.ui.debugflag and 'header_debug' in self.t:
851 853 key = 'header_debug'
852 854 elif self.ui.quiet and 'header_quiet' in self.t:
853 855 key = 'header_quiet'
854 856 elif self.ui.verbose and 'header_verbose' in self.t:
855 857 key = 'header_verbose'
856 858 elif 'header' in self.t:
857 859 key = 'header'
858 860 else:
859 861 key = ''
860 862 if key:
861 863 h = templater.stringify(self.t(key, **props))
862 864 if self.buffered:
863 865 self.header[rev] = h
864 866 else:
865 867 self.ui.write(h)
866 868 if self.ui.debugflag and 'changeset_debug' in self.t:
867 869 key = 'changeset_debug'
868 870 elif self.ui.quiet and 'changeset_quiet' in self.t:
869 871 key = 'changeset_quiet'
870 872 elif self.ui.verbose and 'changeset_verbose' in self.t:
871 873 key = 'changeset_verbose'
872 874 else:
873 875 key = 'changeset'
874 876 self.ui.write(templater.stringify(self.t(key, **props)))
875 877 self.showpatch(changenode)
876 878 except KeyError, inst:
877 879 raise util.Abort(_("%s: no key named '%s'") % (self.t.mapfile,
878 880 inst.args[0]))
879 881 except SyntaxError, inst:
880 882 raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0]))
881 883
882 884 def show_changeset(ui, repo, opts, buffered=False, matchfn=False):
883 885 """show one changeset using template or regular display.
884 886
885 887 Display format will be the first non-empty hit of:
886 888 1. option 'template'
887 889 2. option 'style'
888 890 3. [ui] setting 'logtemplate'
889 891 4. [ui] setting 'style'
890 892 If all of these values are either the unset or the empty string,
891 893 regular display via changeset_printer() is done.
892 894 """
893 895 # options
894 896 patch = False
895 897 if opts.get('patch'):
896 898 patch = matchfn or matchall(repo)
897 899
898 900 tmpl = opts.get('template')
899 901 mapfile = None
900 902 if tmpl:
901 903 tmpl = templater.parsestring(tmpl, quoted=False)
902 904 else:
903 905 mapfile = opts.get('style')
904 906 # ui settings
905 907 if not mapfile:
906 908 tmpl = ui.config('ui', 'logtemplate')
907 909 if tmpl:
908 910 tmpl = templater.parsestring(tmpl)
909 911 else:
910 912 mapfile = ui.config('ui', 'style')
911 913
912 914 if tmpl or mapfile:
913 915 if mapfile:
914 916 if not os.path.split(mapfile)[0]:
915 917 mapname = (templater.templatepath('map-cmdline.' + mapfile)
916 918 or templater.templatepath(mapfile))
917 919 if mapname: mapfile = mapname
918 920 try:
919 921 t = changeset_templater(ui, repo, patch, mapfile, buffered)
920 922 except SyntaxError, inst:
921 923 raise util.Abort(inst.args[0])
922 924 if tmpl: t.use_template(tmpl)
923 925 return t
924 926 return changeset_printer(ui, repo, patch, buffered)
925 927
926 928 def finddate(ui, repo, date):
927 929 """Find the tipmost changeset that matches the given date spec"""
928 930 df = util.matchdate(date)
929 931 get = util.cachefunc(lambda r: repo[r].changeset())
930 932 changeiter, matchfn = walkchangerevs(ui, repo, [], get, {'rev':None})
931 933 results = {}
932 934 for st, rev, fns in changeiter:
933 935 if st == 'add':
934 936 d = get(rev)[2]
935 937 if df(d[0]):
936 938 results[rev] = d
937 939 elif st == 'iter':
938 940 if rev in results:
939 941 ui.status(_("Found revision %s from %s\n") %
940 942 (rev, util.datestr(results[rev])))
941 943 return str(rev)
942 944
943 945 raise util.Abort(_("revision matching date not found"))
944 946
945 947 def walkchangerevs(ui, repo, pats, change, opts):
946 948 '''Iterate over files and the revs they changed in.
947 949
948 950 Callers most commonly need to iterate backwards over the history
949 951 it is interested in. Doing so has awful (quadratic-looking)
950 952 performance, so we use iterators in a "windowed" way.
951 953
952 954 We walk a window of revisions in the desired order. Within the
953 955 window, we first walk forwards to gather data, then in the desired
954 956 order (usually backwards) to display it.
955 957
956 958 This function returns an (iterator, matchfn) tuple. The iterator
957 959 yields 3-tuples. They will be of one of the following forms:
958 960
959 961 "window", incrementing, lastrev: stepping through a window,
960 962 positive if walking forwards through revs, last rev in the
961 963 sequence iterated over - use to reset state for the current window
962 964
963 965 "add", rev, fns: out-of-order traversal of the given file names
964 966 fns, which changed during revision rev - use to gather data for
965 967 possible display
966 968
967 969 "iter", rev, None: in-order traversal of the revs earlier iterated
968 970 over with "add" - use to display data'''
969 971
970 972 def increasing_windows(start, end, windowsize=8, sizelimit=512):
971 973 if start < end:
972 974 while start < end:
973 975 yield start, min(windowsize, end-start)
974 976 start += windowsize
975 977 if windowsize < sizelimit:
976 978 windowsize *= 2
977 979 else:
978 980 while start > end:
979 981 yield start, min(windowsize, start-end-1)
980 982 start -= windowsize
981 983 if windowsize < sizelimit:
982 984 windowsize *= 2
983 985
984 986 m = match(repo, pats, opts)
985 987 follow = opts.get('follow') or opts.get('follow_first')
986 988
987 989 if not len(repo):
988 990 return [], m
989 991
990 992 if follow:
991 993 defrange = '%s:0' % repo['.'].rev()
992 994 else:
993 995 defrange = '-1:0'
994 996 revs = revrange(repo, opts['rev'] or [defrange])
995 997 wanted = {}
996 998 slowpath = m.anypats() or opts.get('removed')
997 999 fncache = {}
998 1000
999 1001 if not slowpath and not m.files():
1000 1002 # No files, no patterns. Display all revs.
1001 1003 wanted = dict.fromkeys(revs)
1002 1004 copies = []
1003 1005 if not slowpath:
1004 1006 # Only files, no patterns. Check the history of each file.
1005 1007 def filerevgen(filelog, node):
1006 1008 cl_count = len(repo)
1007 1009 if node is None:
1008 1010 last = len(filelog) - 1
1009 1011 else:
1010 1012 last = filelog.rev(node)
1011 1013 for i, window in increasing_windows(last, nullrev):
1012 1014 revs = []
1013 1015 for j in xrange(i - window, i + 1):
1014 1016 n = filelog.node(j)
1015 1017 revs.append((filelog.linkrev(n),
1016 1018 follow and filelog.renamed(n)))
1017 1019 revs.reverse()
1018 1020 for rev in revs:
1019 1021 # only yield rev for which we have the changelog, it can
1020 1022 # happen while doing "hg log" during a pull or commit
1021 1023 if rev[0] < cl_count:
1022 1024 yield rev
1023 1025 def iterfiles():
1024 1026 for filename in m.files():
1025 1027 yield filename, None
1026 1028 for filename_node in copies:
1027 1029 yield filename_node
1028 1030 minrev, maxrev = min(revs), max(revs)
1029 1031 for file_, node in iterfiles():
1030 1032 filelog = repo.file(file_)
1031 1033 if not len(filelog):
1032 1034 if node is None:
1033 1035 # A zero count may be a directory or deleted file, so
1034 1036 # try to find matching entries on the slow path.
1035 1037 slowpath = True
1036 1038 break
1037 1039 else:
1038 1040 ui.warn(_('%s:%s copy source revision cannot be found!\n')
1039 1041 % (file_, short(node)))
1040 1042 continue
1041 1043 for rev, copied in filerevgen(filelog, node):
1042 1044 if rev <= maxrev:
1043 1045 if rev < minrev:
1044 1046 break
1045 1047 fncache.setdefault(rev, [])
1046 1048 fncache[rev].append(file_)
1047 1049 wanted[rev] = 1
1048 1050 if follow and copied:
1049 1051 copies.append(copied)
1050 1052 if slowpath:
1051 1053 if follow:
1052 1054 raise util.Abort(_('can only follow copies/renames for explicit '
1053 1055 'file names'))
1054 1056
1055 1057 # The slow path checks files modified in every changeset.
1056 1058 def changerevgen():
1057 1059 for i, window in increasing_windows(len(repo) - 1, nullrev):
1058 1060 for j in xrange(i - window, i + 1):
1059 1061 yield j, change(j)[3]
1060 1062
1061 1063 for rev, changefiles in changerevgen():
1062 1064 matches = filter(m, changefiles)
1063 1065 if matches:
1064 1066 fncache[rev] = matches
1065 1067 wanted[rev] = 1
1066 1068
1067 1069 class followfilter:
1068 1070 def __init__(self, onlyfirst=False):
1069 1071 self.startrev = nullrev
1070 1072 self.roots = []
1071 1073 self.onlyfirst = onlyfirst
1072 1074
1073 1075 def match(self, rev):
1074 1076 def realparents(rev):
1075 1077 if self.onlyfirst:
1076 1078 return repo.changelog.parentrevs(rev)[0:1]
1077 1079 else:
1078 1080 return filter(lambda x: x != nullrev,
1079 1081 repo.changelog.parentrevs(rev))
1080 1082
1081 1083 if self.startrev == nullrev:
1082 1084 self.startrev = rev
1083 1085 return True
1084 1086
1085 1087 if rev > self.startrev:
1086 1088 # forward: all descendants
1087 1089 if not self.roots:
1088 1090 self.roots.append(self.startrev)
1089 1091 for parent in realparents(rev):
1090 1092 if parent in self.roots:
1091 1093 self.roots.append(rev)
1092 1094 return True
1093 1095 else:
1094 1096 # backwards: all parents
1095 1097 if not self.roots:
1096 1098 self.roots.extend(realparents(self.startrev))
1097 1099 if rev in self.roots:
1098 1100 self.roots.remove(rev)
1099 1101 self.roots.extend(realparents(rev))
1100 1102 return True
1101 1103
1102 1104 return False
1103 1105
1104 1106 # it might be worthwhile to do this in the iterator if the rev range
1105 1107 # is descending and the prune args are all within that range
1106 1108 for rev in opts.get('prune', ()):
1107 1109 rev = repo.changelog.rev(repo.lookup(rev))
1108 1110 ff = followfilter()
1109 1111 stop = min(revs[0], revs[-1])
1110 1112 for x in xrange(rev, stop-1, -1):
1111 1113 if ff.match(x) and x in wanted:
1112 1114 del wanted[x]
1113 1115
1114 1116 def iterate():
1115 1117 if follow and not m.files():
1116 1118 ff = followfilter(onlyfirst=opts.get('follow_first'))
1117 1119 def want(rev):
1118 1120 if ff.match(rev) and rev in wanted:
1119 1121 return True
1120 1122 return False
1121 1123 else:
1122 1124 def want(rev):
1123 1125 return rev in wanted
1124 1126
1125 1127 for i, window in increasing_windows(0, len(revs)):
1126 1128 yield 'window', revs[0] < revs[-1], revs[-1]
1127 1129 nrevs = [rev for rev in revs[i:i+window] if want(rev)]
1128 1130 for rev in util.sort(list(nrevs)):
1129 1131 fns = fncache.get(rev)
1130 1132 if not fns:
1131 1133 def fns_generator():
1132 1134 for f in change(rev)[3]:
1133 1135 if m(f):
1134 1136 yield f
1135 1137 fns = fns_generator()
1136 1138 yield 'add', rev, fns
1137 1139 for rev in nrevs:
1138 1140 yield 'iter', rev, None
1139 1141 return iterate(), m
1140 1142
1141 1143 def commit(ui, repo, commitfunc, pats, opts):
1142 1144 '''commit the specified files or all outstanding changes'''
1143 1145 date = opts.get('date')
1144 1146 if date:
1145 1147 opts['date'] = util.parsedate(date)
1146 1148 message = logmessage(opts)
1147 1149
1148 1150 # extract addremove carefully -- this function can be called from a command
1149 1151 # that doesn't support addremove
1150 1152 if opts.get('addremove'):
1151 1153 addremove(repo, pats, opts)
1152 1154
1153 1155 m = match(repo, pats, opts)
1154 1156 if pats:
1155 1157 modified, added, removed = repo.status(match=m)[:3]
1156 1158 files = util.sort(modified + added + removed)
1157 1159
1158 1160 def is_dir(f):
1159 1161 name = f + '/'
1160 1162 i = bisect.bisect(files, name)
1161 1163 return i < len(files) and files[i].startswith(name)
1162 1164
1163 1165 for f in m.files():
1164 1166 if f == '.':
1165 1167 continue
1166 1168 if f not in files:
1167 1169 rf = repo.wjoin(f)
1168 1170 rel = repo.pathto(f)
1169 1171 try:
1170 1172 mode = os.lstat(rf)[stat.ST_MODE]
1171 1173 except OSError:
1172 1174 if is_dir(f): # deleted directory ?
1173 1175 continue
1174 1176 raise util.Abort(_("file %s not found!") % rel)
1175 1177 if stat.S_ISDIR(mode):
1176 1178 if not is_dir(f):
1177 1179 raise util.Abort(_("no match under directory %s!")
1178 1180 % rel)
1179 1181 elif not (stat.S_ISREG(mode) or stat.S_ISLNK(mode)):
1180 1182 raise util.Abort(_("can't commit %s: "
1181 1183 "unsupported file type!") % rel)
1182 1184 elif f not in repo.dirstate:
1183 1185 raise util.Abort(_("file %s not tracked!") % rel)
1184 1186 m = matchfiles(repo, files)
1185 1187 try:
1186 1188 return commitfunc(ui, repo, message, m, opts)
1187 1189 except ValueError, inst:
1188 1190 raise util.Abort(str(inst))
@@ -1,3382 +1,3384
1 1 # commands.py - command processing for mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from node import hex, nullid, nullrev, short
9 9 from repo import RepoError, NoCapability
10 10 from i18n import _, gettext
11 11 import os, re, sys
12 12 import hg, util, revlog, bundlerepo, extensions, copies
13 13 import difflib, patch, time, help, mdiff, tempfile, url
14 14 import version
15 15 import archival, changegroup, cmdutil, hgweb.server, sshserver, hbisect
16 16 import merge as merge_
17 17
18 18 # Commands start here, listed alphabetically
19 19
20 20 def add(ui, repo, *pats, **opts):
21 21 """add the specified files on the next commit
22 22
23 23 Schedule files to be version controlled and added to the repository.
24 24
25 25 The files will be added to the repository at the next commit. To
26 26 undo an add before that, see hg revert.
27 27
28 28 If no names are given, add all files in the repository.
29 29 """
30 30
31 31 rejected = None
32 32 exacts = {}
33 33 names = []
34 34 m = cmdutil.match(repo, pats, opts)
35 35 m.bad = lambda x,y: True
36 36 for abs in repo.walk(m):
37 37 if m.exact(abs):
38 38 if ui.verbose:
39 39 ui.status(_('adding %s\n') % m.rel(abs))
40 40 names.append(abs)
41 41 exacts[abs] = 1
42 42 elif abs not in repo.dirstate:
43 43 ui.status(_('adding %s\n') % m.rel(abs))
44 44 names.append(abs)
45 45 if not opts.get('dry_run'):
46 46 rejected = repo.add(names)
47 47 rejected = [p for p in rejected if p in exacts]
48 48 return rejected and 1 or 0
49 49
50 50 def addremove(ui, repo, *pats, **opts):
51 51 """add all new files, delete all missing files
52 52
53 53 Add all new files and remove all missing files from the repository.
54 54
55 55 New files are ignored if they match any of the patterns in .hgignore. As
56 56 with add, these changes take effect at the next commit.
57 57
58 58 Use the -s option to detect renamed files. With a parameter > 0,
59 59 this compares every removed file with every added file and records
60 60 those similar enough as renames. This option takes a percentage
61 61 between 0 (disabled) and 100 (files must be identical) as its
62 62 parameter. Detecting renamed files this way can be expensive.
63 63 """
64 64 try:
65 65 sim = float(opts.get('similarity') or 0)
66 66 except ValueError:
67 67 raise util.Abort(_('similarity must be a number'))
68 68 if sim < 0 or sim > 100:
69 69 raise util.Abort(_('similarity must be between 0 and 100'))
70 70 return cmdutil.addremove(repo, pats, opts, similarity=sim/100.)
71 71
72 72 def annotate(ui, repo, *pats, **opts):
73 73 """show changeset information per file line
74 74
75 75 List changes in files, showing the revision id responsible for each line
76 76
77 77 This command is useful to discover who did a change or when a change took
78 78 place.
79 79
80 80 Without the -a option, annotate will avoid processing files it
81 81 detects as binary. With -a, annotate will generate an annotation
82 82 anyway, probably with undesirable results.
83 83 """
84 84 datefunc = ui.quiet and util.shortdate or util.datestr
85 85 getdate = util.cachefunc(lambda x: datefunc(x[0].date()))
86 86
87 87 if not pats:
88 88 raise util.Abort(_('at least one file name or pattern required'))
89 89
90 90 opmap = [('user', lambda x: ui.shortuser(x[0].user())),
91 91 ('number', lambda x: str(x[0].rev())),
92 92 ('changeset', lambda x: short(x[0].node())),
93 93 ('date', getdate),
94 94 ('follow', lambda x: x[0].path()),
95 95 ]
96 96
97 97 if (not opts.get('user') and not opts.get('changeset') and not opts.get('date')
98 98 and not opts.get('follow')):
99 99 opts['number'] = 1
100 100
101 101 linenumber = opts.get('line_number') is not None
102 102 if (linenumber and (not opts.get('changeset')) and (not opts.get('number'))):
103 103 raise util.Abort(_('at least one of -n/-c is required for -l'))
104 104
105 105 funcmap = [func for op, func in opmap if opts.get(op)]
106 106 if linenumber:
107 107 lastfunc = funcmap[-1]
108 108 funcmap[-1] = lambda x: "%s:%s" % (lastfunc(x), x[1])
109 109
110 110 ctx = repo[opts.get('rev')]
111 111
112 112 m = cmdutil.match(repo, pats, opts)
113 113 for abs in ctx.walk(m):
114 114 fctx = ctx[abs]
115 115 if not opts.get('text') and util.binary(fctx.data()):
116 116 ui.write(_("%s: binary file\n") % ((pats and m.rel(abs)) or abs))
117 117 continue
118 118
119 119 lines = fctx.annotate(follow=opts.get('follow'),
120 120 linenumber=linenumber)
121 121 pieces = []
122 122
123 123 for f in funcmap:
124 124 l = [f(n) for n, dummy in lines]
125 125 if l:
126 126 ml = max(map(len, l))
127 127 pieces.append(["%*s" % (ml, x) for x in l])
128 128
129 129 if pieces:
130 130 for p, l in zip(zip(*pieces), lines):
131 131 ui.write("%s: %s" % (" ".join(p), l[1]))
132 132
133 133 def archive(ui, repo, dest, **opts):
134 134 '''create unversioned archive of a repository revision
135 135
136 136 By default, the revision used is the parent of the working
137 137 directory; use "-r" to specify a different revision.
138 138
139 139 To specify the type of archive to create, use "-t". Valid
140 140 types are:
141 141
142 142 "files" (default): a directory full of files
143 143 "tar": tar archive, uncompressed
144 144 "tbz2": tar archive, compressed using bzip2
145 145 "tgz": tar archive, compressed using gzip
146 146 "uzip": zip archive, uncompressed
147 147 "zip": zip archive, compressed using deflate
148 148
149 149 The exact name of the destination archive or directory is given
150 150 using a format string; see "hg help export" for details.
151 151
152 152 Each member added to an archive file has a directory prefix
153 153 prepended. Use "-p" to specify a format string for the prefix.
154 154 The default is the basename of the archive, with suffixes removed.
155 155 '''
156 156
157 157 ctx = repo[opts.get('rev')]
158 158 if not ctx:
159 159 raise util.Abort(_('repository has no revisions'))
160 160 node = ctx.node()
161 161 dest = cmdutil.make_filename(repo, dest, node)
162 162 if os.path.realpath(dest) == repo.root:
163 163 raise util.Abort(_('repository root cannot be destination'))
164 164 matchfn = cmdutil.match(repo, [], opts)
165 165 kind = opts.get('type') or 'files'
166 166 prefix = opts.get('prefix')
167 167 if dest == '-':
168 168 if kind == 'files':
169 169 raise util.Abort(_('cannot archive plain files to stdout'))
170 170 dest = sys.stdout
171 171 if not prefix: prefix = os.path.basename(repo.root) + '-%h'
172 172 prefix = cmdutil.make_filename(repo, prefix, node)
173 173 archival.archive(repo, dest, node, kind, not opts.get('no_decode'),
174 174 matchfn, prefix)
175 175
176 176 def backout(ui, repo, node=None, rev=None, **opts):
177 177 '''reverse effect of earlier changeset
178 178
179 179 Commit the backed out changes as a new changeset. The new
180 180 changeset is a child of the backed out changeset.
181 181
182 182 If you back out a changeset other than the tip, a new head is
183 183 created. This head will be the new tip and you should merge this
184 184 backout changeset with another head (current one by default).
185 185
186 186 The --merge option remembers the parent of the working directory
187 187 before starting the backout, then merges the new head with that
188 188 changeset afterwards. This saves you from doing the merge by
189 189 hand. The result of this merge is not committed, as for a normal
190 190 merge.
191 191
192 192 See \'hg help dates\' for a list of formats valid for -d/--date.
193 193 '''
194 194 if rev and node:
195 195 raise util.Abort(_("please specify just one revision"))
196 196
197 197 if not rev:
198 198 rev = node
199 199
200 200 if not rev:
201 201 raise util.Abort(_("please specify a revision to backout"))
202 202
203 203 date = opts.get('date')
204 204 if date:
205 205 opts['date'] = util.parsedate(date)
206 206
207 207 cmdutil.bail_if_changed(repo)
208 208 node = repo.lookup(rev)
209 209
210 210 op1, op2 = repo.dirstate.parents()
211 211 a = repo.changelog.ancestor(op1, node)
212 212 if a != node:
213 213 raise util.Abort(_('cannot back out change on a different branch'))
214 214
215 215 p1, p2 = repo.changelog.parents(node)
216 216 if p1 == nullid:
217 217 raise util.Abort(_('cannot back out a change with no parents'))
218 218 if p2 != nullid:
219 219 if not opts.get('parent'):
220 220 raise util.Abort(_('cannot back out a merge changeset without '
221 221 '--parent'))
222 222 p = repo.lookup(opts['parent'])
223 223 if p not in (p1, p2):
224 224 raise util.Abort(_('%s is not a parent of %s') %
225 225 (short(p), short(node)))
226 226 parent = p
227 227 else:
228 228 if opts.get('parent'):
229 229 raise util.Abort(_('cannot use --parent on non-merge changeset'))
230 230 parent = p1
231 231
232 232 # the backout should appear on the same branch
233 233 branch = repo.dirstate.branch()
234 234 hg.clean(repo, node, show_stats=False)
235 235 repo.dirstate.setbranch(branch)
236 236 revert_opts = opts.copy()
237 237 revert_opts['date'] = None
238 238 revert_opts['all'] = True
239 239 revert_opts['rev'] = hex(parent)
240 240 revert_opts['no_backup'] = None
241 241 revert(ui, repo, **revert_opts)
242 242 commit_opts = opts.copy()
243 243 commit_opts['addremove'] = False
244 244 if not commit_opts['message'] and not commit_opts['logfile']:
245 245 commit_opts['message'] = _("Backed out changeset %s") % (short(node))
246 246 commit_opts['force_editor'] = True
247 247 commit(ui, repo, **commit_opts)
248 248 def nice(node):
249 249 return '%d:%s' % (repo.changelog.rev(node), short(node))
250 250 ui.status(_('changeset %s backs out changeset %s\n') %
251 251 (nice(repo.changelog.tip()), nice(node)))
252 252 if op1 != node:
253 253 hg.clean(repo, op1, show_stats=False)
254 254 if opts.get('merge'):
255 255 ui.status(_('merging with changeset %s\n') % nice(repo.changelog.tip()))
256 256 hg.merge(repo, hex(repo.changelog.tip()))
257 257 else:
258 258 ui.status(_('the backout changeset is a new head - '
259 259 'do not forget to merge\n'))
260 260 ui.status(_('(use "backout --merge" '
261 261 'if you want to auto-merge)\n'))
262 262
263 263 def bisect(ui, repo, rev=None, extra=None, command=None,
264 264 reset=None, good=None, bad=None, skip=None, noupdate=None):
265 265 """subdivision search of changesets
266 266
267 267 This command helps to find changesets which introduce problems.
268 268 To use, mark the earliest changeset you know exhibits the problem
269 269 as bad, then mark the latest changeset which is free from the
270 270 problem as good. Bisect will update your working directory to a
271 271 revision for testing (unless the --noupdate option is specified).
272 272 Once you have performed tests, mark the working directory as bad
273 273 or good and bisect will either update to another candidate changeset
274 274 or announce that it has found the bad revision.
275 275
276 276 As a shortcut, you can also use the revision argument to mark a
277 277 revision as good or bad without checking it out first.
278 278
279 279 If you supply a command it will be used for automatic bisection. Its exit
280 280 status will be used as flag to mark revision as bad or good. In case exit
281 281 status is 0 the revision is marked as good, 125 - skipped, 127 (command not
282 282 found) - bisection will be aborted and any other status bigger than 0 will
283 283 mark revision as bad.
284 284 """
285 285 def print_result(nodes, good):
286 286 displayer = cmdutil.show_changeset(ui, repo, {})
287 287 transition = (good and "good" or "bad")
288 288 if len(nodes) == 1:
289 289 # narrowed it down to a single revision
290 290 ui.write(_("The first %s revision is:\n") % transition)
291 291 displayer.show(changenode=nodes[0])
292 292 else:
293 293 # multiple possible revisions
294 294 ui.write(_("Due to skipped revisions, the first "
295 295 "%s revision could be any of:\n") % transition)
296 296 for n in nodes:
297 297 displayer.show(changenode=n)
298 298
299 299 def check_state(state, interactive=True):
300 300 if not state['good'] or not state['bad']:
301 301 if (good or bad or skip or reset) and interactive:
302 302 return
303 303 if not state['good']:
304 304 raise util.Abort(_('cannot bisect (no known good revisions)'))
305 305 else:
306 306 raise util.Abort(_('cannot bisect (no known bad revisions)'))
307 307 return True
308 308
309 309 # backward compatibility
310 310 if rev in "good bad reset init".split():
311 311 ui.warn(_("(use of 'hg bisect <cmd>' is deprecated)\n"))
312 312 cmd, rev, extra = rev, extra, None
313 313 if cmd == "good":
314 314 good = True
315 315 elif cmd == "bad":
316 316 bad = True
317 317 else:
318 318 reset = True
319 319 elif extra or good + bad + skip + reset + bool(command) > 1:
320 320 raise util.Abort(_('incompatible arguments'))
321 321
322 322 if reset:
323 323 p = repo.join("bisect.state")
324 324 if os.path.exists(p):
325 325 os.unlink(p)
326 326 return
327 327
328 328 state = hbisect.load_state(repo)
329 329
330 330 if command:
331 331 changesets = 1
332 332 while changesets:
333 333 # update state
334 334 status = os.spawnlp(os.P_WAIT, command)
335 335 node = repo.lookup(rev or '.')
336 336 if status == 125:
337 337 transition = "skip"
338 338 elif status == 0:
339 339 transition = "good"
340 340 # status < 0 means process was killed
341 341 elif status == 127 or status < 0:
342 342 break
343 343 else:
344 344 transition = "bad"
345 345 state[transition].append(node)
346 346 ui.note(_('Changeset %s: %s\n') % (short(node), transition))
347 347 check_state(state, interactive=False)
348 348 # bisect
349 349 nodes, changesets, good = hbisect.bisect(repo.changelog, state)
350 350 # update to next check
351 351 cmdutil.bail_if_changed(repo)
352 352 hg.clean(repo, nodes[0], show_stats=False)
353 353 hbisect.save_state(repo, state)
354 354 return print_result(nodes, not status)
355 355
356 356 # update state
357 357 node = repo.lookup(rev or '.')
358 358 if good:
359 359 state['good'].append(node)
360 360 elif bad:
361 361 state['bad'].append(node)
362 362 elif skip:
363 363 state['skip'].append(node)
364 364
365 365 hbisect.save_state(repo, state)
366 366
367 367 if not check_state(state):
368 368 return
369 369
370 370 # actually bisect
371 371 nodes, changesets, good = hbisect.bisect(repo.changelog, state)
372 372 if changesets == 0:
373 373 print_result(nodes, good)
374 374 else:
375 375 assert len(nodes) == 1 # only a single node can be tested next
376 376 node = nodes[0]
377 377 # compute the approximate number of remaining tests
378 378 tests, size = 0, 2
379 379 while size <= changesets:
380 380 tests, size = tests + 1, size * 2
381 381 rev = repo.changelog.rev(node)
382 382 ui.write(_("Testing changeset %s:%s "
383 383 "(%s changesets remaining, ~%s tests)\n")
384 384 % (rev, short(node), changesets, tests))
385 385 if not noupdate:
386 386 cmdutil.bail_if_changed(repo)
387 387 return hg.clean(repo, node)
388 388
389 389 def branch(ui, repo, label=None, **opts):
390 390 """set or show the current branch name
391 391
392 392 With no argument, show the current branch name. With one argument,
393 393 set the working directory branch name (the branch does not exist in
394 394 the repository until the next commit).
395 395
396 396 Unless --force is specified, branch will not let you set a
397 397 branch name that shadows an existing branch.
398 398
399 399 Use --clean to reset the working directory branch to that of the
400 400 parent of the working directory, negating a previous branch change.
401 401
402 402 Use the command 'hg update' to switch to an existing branch.
403 403 """
404 404
405 405 if opts.get('clean'):
406 406 label = repo[None].parents()[0].branch()
407 407 repo.dirstate.setbranch(label)
408 408 ui.status(_('reset working directory to branch %s\n') % label)
409 409 elif label:
410 410 if not opts.get('force') and label in repo.branchtags():
411 411 if label not in [p.branch() for p in repo.parents()]:
412 412 raise util.Abort(_('a branch of the same name already exists'
413 413 ' (use --force to override)'))
414 414 repo.dirstate.setbranch(util.fromlocal(label))
415 415 ui.status(_('marked working directory as branch %s\n') % label)
416 416 else:
417 417 ui.write("%s\n" % util.tolocal(repo.dirstate.branch()))
418 418
419 419 def branches(ui, repo, active=False):
420 420 """list repository named branches
421 421
422 422 List the repository's named branches, indicating which ones are
423 423 inactive. If active is specified, only show active branches.
424 424
425 425 A branch is considered active if it contains repository heads.
426 426
427 427 Use the command 'hg update' to switch to an existing branch.
428 428 """
429 429 hexfunc = ui.debugflag and hex or short
430 430 activebranches = [util.tolocal(repo[n].branch())
431 431 for n in repo.heads()]
432 432 branches = util.sort([(tag in activebranches, repo.changelog.rev(node), tag)
433 433 for tag, node in repo.branchtags().items()])
434 434 branches.reverse()
435 435
436 436 for isactive, node, tag in branches:
437 437 if (not active) or isactive:
438 438 if ui.quiet:
439 439 ui.write("%s\n" % tag)
440 440 else:
441 441 rev = str(node).rjust(31 - util.locallen(tag))
442 442 isinactive = ((not isactive) and " (inactive)") or ''
443 443 data = tag, rev, hexfunc(repo.lookup(node)), isinactive
444 444 ui.write("%s %s:%s%s\n" % data)
445 445
446 446 def bundle(ui, repo, fname, dest=None, **opts):
447 447 """create a changegroup file
448 448
449 449 Generate a compressed changegroup file collecting changesets not
450 450 found in the other repository.
451 451
452 452 If no destination repository is specified the destination is
453 453 assumed to have all the nodes specified by one or more --base
454 454 parameters. To create a bundle containing all changesets, use
455 455 --all (or --base null). To change the compression method applied,
456 456 use the -t option (by default, bundles are compressed using bz2).
457 457
458 458 The bundle file can then be transferred using conventional means and
459 459 applied to another repository with the unbundle or pull command.
460 460 This is useful when direct push and pull are not available or when
461 461 exporting an entire repository is undesirable.
462 462
463 463 Applying bundles preserves all changeset contents including
464 464 permissions, copy/rename information, and revision history.
465 465 """
466 466 revs = opts.get('rev') or None
467 467 if revs:
468 468 revs = [repo.lookup(rev) for rev in revs]
469 469 if opts.get('all'):
470 470 base = ['null']
471 471 else:
472 472 base = opts.get('base')
473 473 if base:
474 474 if dest:
475 475 raise util.Abort(_("--base is incompatible with specifiying "
476 476 "a destination"))
477 477 base = [repo.lookup(rev) for rev in base]
478 478 # create the right base
479 479 # XXX: nodesbetween / changegroup* should be "fixed" instead
480 480 o = []
481 481 has = {nullid: None}
482 482 for n in base:
483 483 has.update(repo.changelog.reachable(n))
484 484 if revs:
485 485 visit = list(revs)
486 486 else:
487 487 visit = repo.changelog.heads()
488 488 seen = {}
489 489 while visit:
490 490 n = visit.pop(0)
491 491 parents = [p for p in repo.changelog.parents(n) if p not in has]
492 492 if len(parents) == 0:
493 493 o.insert(0, n)
494 494 else:
495 495 for p in parents:
496 496 if p not in seen:
497 497 seen[p] = 1
498 498 visit.append(p)
499 499 else:
500 500 cmdutil.setremoteconfig(ui, opts)
501 501 dest, revs, checkout = hg.parseurl(
502 502 ui.expandpath(dest or 'default-push', dest or 'default'), revs)
503 503 other = hg.repository(ui, dest)
504 504 o = repo.findoutgoing(other, force=opts.get('force'))
505 505
506 506 if revs:
507 507 cg = repo.changegroupsubset(o, revs, 'bundle')
508 508 else:
509 509 cg = repo.changegroup(o, 'bundle')
510 510
511 511 bundletype = opts.get('type', 'bzip2').lower()
512 512 btypes = {'none': 'HG10UN', 'bzip2': 'HG10BZ', 'gzip': 'HG10GZ'}
513 513 bundletype = btypes.get(bundletype)
514 514 if bundletype not in changegroup.bundletypes:
515 515 raise util.Abort(_('unknown bundle type specified with --type'))
516 516
517 517 changegroup.writebundle(cg, fname, bundletype)
518 518
519 519 def cat(ui, repo, file1, *pats, **opts):
520 520 """output the current or given revision of files
521 521
522 522 Print the specified files as they were at the given revision.
523 523 If no revision is given, the parent of the working directory is used,
524 524 or tip if no revision is checked out.
525 525
526 526 Output may be to a file, in which case the name of the file is
527 527 given using a format string. The formatting rules are the same as
528 528 for the export command, with the following additions:
529 529
530 530 %s basename of file being printed
531 531 %d dirname of file being printed, or '.' if in repo root
532 532 %p root-relative path name of file being printed
533 533 """
534 534 ctx = repo[opts.get('rev')]
535 535 err = 1
536 536 m = cmdutil.match(repo, (file1,) + pats, opts)
537 537 for abs in ctx.walk(m):
538 538 fp = cmdutil.make_file(repo, opts.get('output'), ctx.node(), pathname=abs)
539 539 data = ctx[abs].data()
540 540 if opts.get('decode'):
541 541 data = repo.wwritedata(abs, data)
542 542 fp.write(data)
543 543 err = 0
544 544 return err
545 545
546 546 def clone(ui, source, dest=None, **opts):
547 547 """make a copy of an existing repository
548 548
549 549 Create a copy of an existing repository in a new directory.
550 550
551 551 If no destination directory name is specified, it defaults to the
552 552 basename of the source.
553 553
554 554 The location of the source is added to the new repository's
555 555 .hg/hgrc file, as the default to be used for future pulls.
556 556
557 557 For efficiency, hardlinks are used for cloning whenever the source
558 558 and destination are on the same filesystem (note this applies only
559 559 to the repository data, not to the checked out files). Some
560 560 filesystems, such as AFS, implement hardlinking incorrectly, but
561 561 do not report errors. In these cases, use the --pull option to
562 562 avoid hardlinking.
563 563
564 564 In some cases, you can clone repositories and checked out files
565 565 using full hardlinks with
566 566
567 567 $ cp -al REPO REPOCLONE
568 568
569 569 This is the fastest way to clone, but it is not always safe. The
570 570 operation is not atomic (making sure REPO is not modified during
571 571 the operation is up to you) and you have to make sure your editor
572 572 breaks hardlinks (Emacs and most Linux Kernel tools do so). Also,
573 573 this is not compatible with certain extensions that place their
574 574 metadata under the .hg directory, such as mq.
575 575
576 576 If you use the -r option to clone up to a specific revision, no
577 577 subsequent revisions will be present in the cloned repository.
578 578 This option implies --pull, even on local repositories.
579 579
580 580 If the -U option is used, the new clone will contain only a repository
581 581 (.hg) and no working copy (the working copy parent is the null revision).
582 582
583 583 See pull for valid source format details.
584 584
585 585 It is possible to specify an ssh:// URL as the destination, but no
586 586 .hg/hgrc and working directory will be created on the remote side.
587 587 Look at the help text for the pull command for important details
588 588 about ssh:// URLs.
589 589 """
590 590 cmdutil.setremoteconfig(ui, opts)
591 591 hg.clone(ui, source, dest,
592 592 pull=opts.get('pull'),
593 593 stream=opts.get('uncompressed'),
594 594 rev=opts.get('rev'),
595 595 update=not opts.get('noupdate'))
596 596
597 597 def commit(ui, repo, *pats, **opts):
598 598 """commit the specified files or all outstanding changes
599 599
600 600 Commit changes to the given files into the repository.
601 601
602 602 If a list of files is omitted, all changes reported by "hg status"
603 603 will be committed.
604 604
605 605 If you are committing the result of a merge, do not provide any
606 606 file names or -I/-X filters.
607 607
608 608 If no commit message is specified, the configured editor is started to
609 609 enter a message.
610 610
611 611 See 'hg help dates' for a list of formats valid for -d/--date.
612 612 """
613 613 def commitfunc(ui, repo, message, match, opts):
614 614 return repo.commit(match.files(), message, opts.get('user'), opts.get('date'),
615 615 match, force_editor=opts.get('force_editor'))
616 616
617 617 node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
618 618 if not node:
619 619 return
620 620 cl = repo.changelog
621 621 rev = cl.rev(node)
622 622 parents = cl.parentrevs(rev)
623 623 if rev - 1 in parents:
624 624 # one of the parents was the old tip
625 625 pass
626 626 elif (parents == (nullrev, nullrev) or
627 627 len(cl.heads(cl.node(parents[0]))) > 1 and
628 628 (parents[1] == nullrev or len(cl.heads(cl.node(parents[1]))) > 1)):
629 629 ui.status(_('created new head\n'))
630 630
631 631 if ui.debugflag:
632 632 ui.write(_('committed changeset %d:%s\n') % (rev,hex(node)))
633 633 elif ui.verbose:
634 634 ui.write(_('committed changeset %d:%s\n') % (rev,short(node)))
635 635
636 636 def copy(ui, repo, *pats, **opts):
637 637 """mark files as copied for the next commit
638 638
639 639 Mark dest as having copies of source files. If dest is a
640 640 directory, copies are put in that directory. If dest is a file,
641 641 there can only be one source.
642 642
643 643 By default, this command copies the contents of files as they
644 644 stand in the working directory. If invoked with --after, the
645 645 operation is recorded, but no copying is performed.
646 646
647 647 This command takes effect in the next commit. To undo a copy
648 648 before that, see hg revert.
649 649 """
650 650 wlock = repo.wlock(False)
651 651 try:
652 652 return cmdutil.copy(ui, repo, pats, opts)
653 653 finally:
654 654 del wlock
655 655
656 656 def debugancestor(ui, repo, *args):
657 657 """find the ancestor revision of two revisions in a given index"""
658 658 if len(args) == 3:
659 659 index, rev1, rev2 = args
660 660 r = revlog.revlog(util.opener(os.getcwd(), audit=False), index)
661 661 lookup = r.lookup
662 662 elif len(args) == 2:
663 663 if not repo:
664 664 raise util.Abort(_("There is no Mercurial repository here "
665 665 "(.hg not found)"))
666 666 rev1, rev2 = args
667 667 r = repo.changelog
668 668 lookup = repo.lookup
669 669 else:
670 670 raise util.Abort(_('either two or three arguments required'))
671 671 a = r.ancestor(lookup(rev1), lookup(rev2))
672 672 ui.write("%d:%s\n" % (r.rev(a), hex(a)))
673 673
674 674 def debugcomplete(ui, cmd='', **opts):
675 675 """returns the completion list associated with the given command"""
676 676
677 677 if opts.get('options'):
678 678 options = []
679 679 otables = [globalopts]
680 680 if cmd:
681 681 aliases, entry = cmdutil.findcmd(cmd, table, False)
682 682 otables.append(entry[1])
683 683 for t in otables:
684 684 for o in t:
685 685 if o[0]:
686 686 options.append('-%s' % o[0])
687 687 options.append('--%s' % o[1])
688 688 ui.write("%s\n" % "\n".join(options))
689 689 return
690 690
691 691 ui.write("%s\n" % "\n".join(util.sort(cmdutil.findpossible(cmd, table))))
692 692
693 693 def debugfsinfo(ui, path = "."):
694 694 file('.debugfsinfo', 'w').write('')
695 695 ui.write('exec: %s\n' % (util.checkexec(path) and 'yes' or 'no'))
696 696 ui.write('symlink: %s\n' % (util.checklink(path) and 'yes' or 'no'))
697 697 ui.write('case-sensitive: %s\n' % (util.checkcase('.debugfsinfo')
698 698 and 'yes' or 'no'))
699 699 os.unlink('.debugfsinfo')
700 700
701 701 def debugrebuildstate(ui, repo, rev="tip"):
702 702 """rebuild the dirstate as it would look like for the given revision"""
703 703 ctx = repo[rev]
704 704 wlock = repo.wlock()
705 705 try:
706 706 repo.dirstate.rebuild(ctx.node(), ctx.manifest())
707 707 finally:
708 708 del wlock
709 709
710 710 def debugcheckstate(ui, repo):
711 711 """validate the correctness of the current dirstate"""
712 712 parent1, parent2 = repo.dirstate.parents()
713 713 m1 = repo[parent1].manifest()
714 714 m2 = repo[parent2].manifest()
715 715 errors = 0
716 716 for f in repo.dirstate:
717 717 state = repo.dirstate[f]
718 718 if state in "nr" and f not in m1:
719 719 ui.warn(_("%s in state %s, but not in manifest1\n") % (f, state))
720 720 errors += 1
721 721 if state in "a" and f in m1:
722 722 ui.warn(_("%s in state %s, but also in manifest1\n") % (f, state))
723 723 errors += 1
724 724 if state in "m" and f not in m1 and f not in m2:
725 725 ui.warn(_("%s in state %s, but not in either manifest\n") %
726 726 (f, state))
727 727 errors += 1
728 728 for f in m1:
729 729 state = repo.dirstate[f]
730 730 if state not in "nrm":
731 731 ui.warn(_("%s in manifest1, but listed as state %s") % (f, state))
732 732 errors += 1
733 733 if errors:
734 734 error = _(".hg/dirstate inconsistent with current parent's manifest")
735 735 raise util.Abort(error)
736 736
737 737 def showconfig(ui, repo, *values, **opts):
738 738 """show combined config settings from all hgrc files
739 739
740 740 With no args, print names and values of all config items.
741 741
742 742 With one arg of the form section.name, print just the value of
743 743 that config item.
744 744
745 745 With multiple args, print names and values of all config items
746 746 with matching section names."""
747 747
748 748 untrusted = bool(opts.get('untrusted'))
749 749 if values:
750 750 if len([v for v in values if '.' in v]) > 1:
751 751 raise util.Abort(_('only one config item permitted'))
752 752 for section, name, value in ui.walkconfig(untrusted=untrusted):
753 753 sectname = section + '.' + name
754 754 if values:
755 755 for v in values:
756 756 if v == section:
757 757 ui.write('%s=%s\n' % (sectname, value))
758 758 elif v == sectname:
759 759 ui.write(value, '\n')
760 760 else:
761 761 ui.write('%s=%s\n' % (sectname, value))
762 762
763 763 def debugsetparents(ui, repo, rev1, rev2=None):
764 764 """manually set the parents of the current working directory
765 765
766 766 This is useful for writing repository conversion tools, but should
767 767 be used with care.
768 768 """
769 769
770 770 if not rev2:
771 771 rev2 = hex(nullid)
772 772
773 773 wlock = repo.wlock()
774 774 try:
775 775 repo.dirstate.setparents(repo.lookup(rev1), repo.lookup(rev2))
776 776 finally:
777 777 del wlock
778 778
779 779 def debugstate(ui, repo, nodates=None):
780 780 """show the contents of the current dirstate"""
781 781 timestr = ""
782 782 showdate = not nodates
783 783 for file_, ent in util.sort(repo.dirstate._map.items()):
784 784 if showdate:
785 785 if ent[3] == -1:
786 786 # Pad or slice to locale representation
787 787 locale_len = len(time.strftime("%Y-%m-%d %H:%M:%S ", time.localtime(0)))
788 788 timestr = 'unset'
789 789 timestr = timestr[:locale_len] + ' '*(locale_len - len(timestr))
790 790 else:
791 791 timestr = time.strftime("%Y-%m-%d %H:%M:%S ", time.localtime(ent[3]))
792 792 if ent[1] & 020000:
793 793 mode = 'lnk'
794 794 else:
795 795 mode = '%3o' % (ent[1] & 0777)
796 796 ui.write("%c %s %10d %s%s\n" % (ent[0], mode, ent[2], timestr, file_))
797 797 for f in repo.dirstate.copies():
798 798 ui.write(_("copy: %s -> %s\n") % (repo.dirstate.copied(f), f))
799 799
800 800 def debugdata(ui, file_, rev):
801 801 """dump the contents of a data file revision"""
802 802 r = revlog.revlog(util.opener(os.getcwd(), audit=False), file_[:-2] + ".i")
803 803 try:
804 804 ui.write(r.revision(r.lookup(rev)))
805 805 except KeyError:
806 806 raise util.Abort(_('invalid revision identifier %s') % rev)
807 807
808 808 def debugdate(ui, date, range=None, **opts):
809 809 """parse and display a date"""
810 810 if opts["extended"]:
811 811 d = util.parsedate(date, util.extendeddateformats)
812 812 else:
813 813 d = util.parsedate(date)
814 814 ui.write("internal: %s %s\n" % d)
815 815 ui.write("standard: %s\n" % util.datestr(d))
816 816 if range:
817 817 m = util.matchdate(range)
818 818 ui.write("match: %s\n" % m(d[0]))
819 819
820 820 def debugindex(ui, file_):
821 821 """dump the contents of an index file"""
822 822 r = revlog.revlog(util.opener(os.getcwd(), audit=False), file_)
823 823 ui.write(" rev offset length base linkrev" +
824 824 " nodeid p1 p2\n")
825 825 for i in r:
826 826 node = r.node(i)
827 827 try:
828 828 pp = r.parents(node)
829 829 except:
830 830 pp = [nullid, nullid]
831 831 ui.write("% 6d % 9d % 7d % 6d % 7d %s %s %s\n" % (
832 832 i, r.start(i), r.length(i), r.base(i), r.linkrev(node),
833 833 short(node), short(pp[0]), short(pp[1])))
834 834
835 835 def debugindexdot(ui, file_):
836 836 """dump an index DAG as a .dot file"""
837 837 r = revlog.revlog(util.opener(os.getcwd(), audit=False), file_)
838 838 ui.write("digraph G {\n")
839 839 for i in r:
840 840 node = r.node(i)
841 841 pp = r.parents(node)
842 842 ui.write("\t%d -> %d\n" % (r.rev(pp[0]), i))
843 843 if pp[1] != nullid:
844 844 ui.write("\t%d -> %d\n" % (r.rev(pp[1]), i))
845 845 ui.write("}\n")
846 846
847 847 def debuginstall(ui):
848 848 '''test Mercurial installation'''
849 849
850 850 def writetemp(contents):
851 851 (fd, name) = tempfile.mkstemp(prefix="hg-debuginstall-")
852 852 f = os.fdopen(fd, "wb")
853 853 f.write(contents)
854 854 f.close()
855 855 return name
856 856
857 857 problems = 0
858 858
859 859 # encoding
860 860 ui.status(_("Checking encoding (%s)...\n") % util._encoding)
861 861 try:
862 862 util.fromlocal("test")
863 863 except util.Abort, inst:
864 864 ui.write(" %s\n" % inst)
865 865 ui.write(_(" (check that your locale is properly set)\n"))
866 866 problems += 1
867 867
868 868 # compiled modules
869 869 ui.status(_("Checking extensions...\n"))
870 870 try:
871 871 import bdiff, mpatch, base85
872 872 except Exception, inst:
873 873 ui.write(" %s\n" % inst)
874 874 ui.write(_(" One or more extensions could not be found"))
875 875 ui.write(_(" (check that you compiled the extensions)\n"))
876 876 problems += 1
877 877
878 878 # templates
879 879 ui.status(_("Checking templates...\n"))
880 880 try:
881 881 import templater
882 882 t = templater.templater(templater.templatepath("map-cmdline.default"))
883 883 except Exception, inst:
884 884 ui.write(" %s\n" % inst)
885 885 ui.write(_(" (templates seem to have been installed incorrectly)\n"))
886 886 problems += 1
887 887
888 888 # patch
889 889 ui.status(_("Checking patch...\n"))
890 890 patchproblems = 0
891 891 a = "1\n2\n3\n4\n"
892 892 b = "1\n2\n3\ninsert\n4\n"
893 893 fa = writetemp(a)
894 894 d = mdiff.unidiff(a, None, b, None, os.path.basename(fa),
895 895 os.path.basename(fa))
896 896 fd = writetemp(d)
897 897
898 898 files = {}
899 899 try:
900 900 patch.patch(fd, ui, cwd=os.path.dirname(fa), files=files)
901 901 except util.Abort, e:
902 902 ui.write(_(" patch call failed:\n"))
903 903 ui.write(" " + str(e) + "\n")
904 904 patchproblems += 1
905 905 else:
906 906 if list(files) != [os.path.basename(fa)]:
907 907 ui.write(_(" unexpected patch output!\n"))
908 908 patchproblems += 1
909 909 a = file(fa).read()
910 910 if a != b:
911 911 ui.write(_(" patch test failed!\n"))
912 912 patchproblems += 1
913 913
914 914 if patchproblems:
915 915 if ui.config('ui', 'patch'):
916 916 ui.write(_(" (Current patch tool may be incompatible with patch,"
917 917 " or misconfigured. Please check your .hgrc file)\n"))
918 918 else:
919 919 ui.write(_(" Internal patcher failure, please report this error"
920 920 " to http://www.selenic.com/mercurial/bts\n"))
921 921 problems += patchproblems
922 922
923 923 os.unlink(fa)
924 924 os.unlink(fd)
925 925
926 926 # editor
927 927 ui.status(_("Checking commit editor...\n"))
928 928 editor = ui.geteditor()
929 929 cmdpath = util.find_exe(editor) or util.find_exe(editor.split()[0])
930 930 if not cmdpath:
931 931 if editor == 'vi':
932 932 ui.write(_(" No commit editor set and can't find vi in PATH\n"))
933 933 ui.write(_(" (specify a commit editor in your .hgrc file)\n"))
934 934 else:
935 935 ui.write(_(" Can't find editor '%s' in PATH\n") % editor)
936 936 ui.write(_(" (specify a commit editor in your .hgrc file)\n"))
937 937 problems += 1
938 938
939 939 # check username
940 940 ui.status(_("Checking username...\n"))
941 941 user = os.environ.get("HGUSER")
942 942 if user is None:
943 943 user = ui.config("ui", "username")
944 944 if user is None:
945 945 user = os.environ.get("EMAIL")
946 946 if not user:
947 947 ui.warn(" ")
948 948 ui.username()
949 949 ui.write(_(" (specify a username in your .hgrc file)\n"))
950 950
951 951 if not problems:
952 952 ui.status(_("No problems detected\n"))
953 953 else:
954 954 ui.write(_("%s problems detected,"
955 955 " please check your install!\n") % problems)
956 956
957 957 return problems
958 958
959 959 def debugrename(ui, repo, file1, *pats, **opts):
960 960 """dump rename information"""
961 961
962 962 ctx = repo[opts.get('rev')]
963 963 m = cmdutil.match(repo, (file1,) + pats, opts)
964 964 for abs in ctx.walk(m):
965 965 fctx = ctx[abs]
966 966 o = fctx.filelog().renamed(fctx.filenode())
967 967 rel = m.rel(abs)
968 968 if o:
969 969 ui.write(_("%s renamed from %s:%s\n") % (rel, o[0], hex(o[1])))
970 970 else:
971 971 ui.write(_("%s not renamed\n") % rel)
972 972
973 973 def debugwalk(ui, repo, *pats, **opts):
974 974 """show how files match on given patterns"""
975 975 m = cmdutil.match(repo, pats, opts)
976 976 items = list(repo.walk(m))
977 977 if not items:
978 978 return
979 979 fmt = 'f %%-%ds %%-%ds %%s' % (
980 980 max([len(abs) for abs in items]),
981 981 max([len(m.rel(abs)) for abs in items]))
982 982 for abs in items:
983 983 line = fmt % (abs, m.rel(abs), m.exact(abs) and 'exact' or '')
984 984 ui.write("%s\n" % line.rstrip())
985 985
986 986 def diff(ui, repo, *pats, **opts):
987 987 """diff repository (or selected files)
988 988
989 989 Show differences between revisions for the specified files.
990 990
991 991 Differences between files are shown using the unified diff format.
992 992
993 993 NOTE: diff may generate unexpected results for merges, as it will
994 994 default to comparing against the working directory's first parent
995 995 changeset if no revisions are specified.
996 996
997 997 When two revision arguments are given, then changes are shown
998 998 between those revisions. If only one revision is specified then
999 999 that revision is compared to the working directory, and, when no
1000 1000 revisions are specified, the working directory files are compared
1001 1001 to its parent.
1002 1002
1003 1003 Without the -a option, diff will avoid generating diffs of files
1004 1004 it detects as binary. With -a, diff will generate a diff anyway,
1005 1005 probably with undesirable results.
1006 1006
1007 1007 Use the --git option to generate diffs in the git extended diff
1008 1008 format. Read the gitdiffs help topic for more information.
1009 1009 """
1010 1010 node1, node2 = cmdutil.revpair(repo, opts.get('rev'))
1011 1011
1012 1012 m = cmdutil.match(repo, pats, opts)
1013 patch.diff(repo, node1, node2, match=m, opts=patch.diffopts(ui, opts))
1013 it = patch.diff(repo, node1, node2, match=m, opts=patch.diffopts(ui, opts))
1014 for chunk in it:
1015 repo.ui.write(chunk)
1014 1016
1015 1017 def export(ui, repo, *changesets, **opts):
1016 1018 """dump the header and diffs for one or more changesets
1017 1019
1018 1020 Print the changeset header and diffs for one or more revisions.
1019 1021
1020 1022 The information shown in the changeset header is: author,
1021 1023 changeset hash, parent(s) and commit comment.
1022 1024
1023 1025 NOTE: export may generate unexpected diff output for merge changesets,
1024 1026 as it will compare the merge changeset against its first parent only.
1025 1027
1026 1028 Output may be to a file, in which case the name of the file is
1027 1029 given using a format string. The formatting rules are as follows:
1028 1030
1029 1031 %% literal "%" character
1030 1032 %H changeset hash (40 bytes of hexadecimal)
1031 1033 %N number of patches being generated
1032 1034 %R changeset revision number
1033 1035 %b basename of the exporting repository
1034 1036 %h short-form changeset hash (12 bytes of hexadecimal)
1035 1037 %n zero-padded sequence number, starting at 1
1036 1038 %r zero-padded changeset revision number
1037 1039
1038 1040 Without the -a option, export will avoid generating diffs of files
1039 1041 it detects as binary. With -a, export will generate a diff anyway,
1040 1042 probably with undesirable results.
1041 1043
1042 1044 Use the --git option to generate diffs in the git extended diff
1043 1045 format. Read the gitdiffs help topic for more information.
1044 1046
1045 1047 With the --switch-parent option, the diff will be against the second
1046 1048 parent. It can be useful to review a merge.
1047 1049 """
1048 1050 if not changesets:
1049 1051 raise util.Abort(_("export requires at least one changeset"))
1050 1052 revs = cmdutil.revrange(repo, changesets)
1051 1053 if len(revs) > 1:
1052 1054 ui.note(_('exporting patches:\n'))
1053 1055 else:
1054 1056 ui.note(_('exporting patch:\n'))
1055 1057 patch.export(repo, revs, template=opts.get('output'),
1056 1058 switch_parent=opts.get('switch_parent'),
1057 1059 opts=patch.diffopts(ui, opts))
1058 1060
1059 1061 def grep(ui, repo, pattern, *pats, **opts):
1060 1062 """search for a pattern in specified files and revisions
1061 1063
1062 1064 Search revisions of files for a regular expression.
1063 1065
1064 1066 This command behaves differently than Unix grep. It only accepts
1065 1067 Python/Perl regexps. It searches repository history, not the
1066 1068 working directory. It always prints the revision number in which
1067 1069 a match appears.
1068 1070
1069 1071 By default, grep only prints output for the first revision of a
1070 1072 file in which it finds a match. To get it to print every revision
1071 1073 that contains a change in match status ("-" for a match that
1072 1074 becomes a non-match, or "+" for a non-match that becomes a match),
1073 1075 use the --all flag.
1074 1076 """
1075 1077 reflags = 0
1076 1078 if opts.get('ignore_case'):
1077 1079 reflags |= re.I
1078 1080 try:
1079 1081 regexp = re.compile(pattern, reflags)
1080 1082 except Exception, inst:
1081 1083 ui.warn(_("grep: invalid match pattern: %s\n") % inst)
1082 1084 return None
1083 1085 sep, eol = ':', '\n'
1084 1086 if opts.get('print0'):
1085 1087 sep = eol = '\0'
1086 1088
1087 1089 fcache = {}
1088 1090 def getfile(fn):
1089 1091 if fn not in fcache:
1090 1092 fcache[fn] = repo.file(fn)
1091 1093 return fcache[fn]
1092 1094
1093 1095 def matchlines(body):
1094 1096 begin = 0
1095 1097 linenum = 0
1096 1098 while True:
1097 1099 match = regexp.search(body, begin)
1098 1100 if not match:
1099 1101 break
1100 1102 mstart, mend = match.span()
1101 1103 linenum += body.count('\n', begin, mstart) + 1
1102 1104 lstart = body.rfind('\n', begin, mstart) + 1 or begin
1103 1105 begin = body.find('\n', mend) + 1 or len(body)
1104 1106 lend = begin - 1
1105 1107 yield linenum, mstart - lstart, mend - lstart, body[lstart:lend]
1106 1108
1107 1109 class linestate(object):
1108 1110 def __init__(self, line, linenum, colstart, colend):
1109 1111 self.line = line
1110 1112 self.linenum = linenum
1111 1113 self.colstart = colstart
1112 1114 self.colend = colend
1113 1115
1114 1116 def __hash__(self):
1115 1117 return hash((self.linenum, self.line))
1116 1118
1117 1119 def __eq__(self, other):
1118 1120 return self.line == other.line
1119 1121
1120 1122 matches = {}
1121 1123 copies = {}
1122 1124 def grepbody(fn, rev, body):
1123 1125 matches[rev].setdefault(fn, [])
1124 1126 m = matches[rev][fn]
1125 1127 for lnum, cstart, cend, line in matchlines(body):
1126 1128 s = linestate(line, lnum, cstart, cend)
1127 1129 m.append(s)
1128 1130
1129 1131 def difflinestates(a, b):
1130 1132 sm = difflib.SequenceMatcher(None, a, b)
1131 1133 for tag, alo, ahi, blo, bhi in sm.get_opcodes():
1132 1134 if tag == 'insert':
1133 1135 for i in xrange(blo, bhi):
1134 1136 yield ('+', b[i])
1135 1137 elif tag == 'delete':
1136 1138 for i in xrange(alo, ahi):
1137 1139 yield ('-', a[i])
1138 1140 elif tag == 'replace':
1139 1141 for i in xrange(alo, ahi):
1140 1142 yield ('-', a[i])
1141 1143 for i in xrange(blo, bhi):
1142 1144 yield ('+', b[i])
1143 1145
1144 1146 prev = {}
1145 1147 def display(fn, rev, states, prevstates):
1146 1148 datefunc = ui.quiet and util.shortdate or util.datestr
1147 1149 found = False
1148 1150 filerevmatches = {}
1149 1151 r = prev.get(fn, -1)
1150 1152 if opts.get('all'):
1151 1153 iter = difflinestates(states, prevstates)
1152 1154 else:
1153 1155 iter = [('', l) for l in prevstates]
1154 1156 for change, l in iter:
1155 1157 cols = [fn, str(r)]
1156 1158 if opts.get('line_number'):
1157 1159 cols.append(str(l.linenum))
1158 1160 if opts.get('all'):
1159 1161 cols.append(change)
1160 1162 if opts.get('user'):
1161 1163 cols.append(ui.shortuser(get(r)[1]))
1162 1164 if opts.get('date'):
1163 1165 cols.append(datefunc(get(r)[2]))
1164 1166 if opts.get('files_with_matches'):
1165 1167 c = (fn, r)
1166 1168 if c in filerevmatches:
1167 1169 continue
1168 1170 filerevmatches[c] = 1
1169 1171 else:
1170 1172 cols.append(l.line)
1171 1173 ui.write(sep.join(cols), eol)
1172 1174 found = True
1173 1175 return found
1174 1176
1175 1177 fstate = {}
1176 1178 skip = {}
1177 1179 get = util.cachefunc(lambda r: repo[r].changeset())
1178 1180 changeiter, matchfn = cmdutil.walkchangerevs(ui, repo, pats, get, opts)
1179 1181 found = False
1180 1182 follow = opts.get('follow')
1181 1183 for st, rev, fns in changeiter:
1182 1184 if st == 'window':
1183 1185 matches.clear()
1184 1186 elif st == 'add':
1185 1187 ctx = repo[rev]
1186 1188 matches[rev] = {}
1187 1189 for fn in fns:
1188 1190 if fn in skip:
1189 1191 continue
1190 1192 try:
1191 1193 grepbody(fn, rev, getfile(fn).read(ctx.filenode(fn)))
1192 1194 fstate.setdefault(fn, [])
1193 1195 if follow:
1194 1196 copied = getfile(fn).renamed(ctx.filenode(fn))
1195 1197 if copied:
1196 1198 copies.setdefault(rev, {})[fn] = copied[0]
1197 1199 except revlog.LookupError:
1198 1200 pass
1199 1201 elif st == 'iter':
1200 1202 for fn, m in util.sort(matches[rev].items()):
1201 1203 copy = copies.get(rev, {}).get(fn)
1202 1204 if fn in skip:
1203 1205 if copy:
1204 1206 skip[copy] = True
1205 1207 continue
1206 1208 if fn in prev or fstate[fn]:
1207 1209 r = display(fn, rev, m, fstate[fn])
1208 1210 found = found or r
1209 1211 if r and not opts.get('all'):
1210 1212 skip[fn] = True
1211 1213 if copy:
1212 1214 skip[copy] = True
1213 1215 fstate[fn] = m
1214 1216 if copy:
1215 1217 fstate[copy] = m
1216 1218 prev[fn] = rev
1217 1219
1218 1220 for fn, state in util.sort(fstate.items()):
1219 1221 if fn in skip:
1220 1222 continue
1221 1223 if fn not in copies.get(prev[fn], {}):
1222 1224 found = display(fn, rev, {}, state) or found
1223 1225 return (not found and 1) or 0
1224 1226
1225 1227 def heads(ui, repo, *branchrevs, **opts):
1226 1228 """show current repository heads or show branch heads
1227 1229
1228 1230 With no arguments, show all repository head changesets.
1229 1231
1230 1232 If branch or revisions names are given this will show the heads of
1231 1233 the specified branches or the branches those revisions are tagged
1232 1234 with.
1233 1235
1234 1236 Repository "heads" are changesets that don't have child
1235 1237 changesets. They are where development generally takes place and
1236 1238 are the usual targets for update and merge operations.
1237 1239
1238 1240 Branch heads are changesets that have a given branch tag, but have
1239 1241 no child changesets with that tag. They are usually where
1240 1242 development on the given branch takes place.
1241 1243 """
1242 1244 if opts.get('rev'):
1243 1245 start = repo.lookup(opts['rev'])
1244 1246 else:
1245 1247 start = None
1246 1248 if not branchrevs:
1247 1249 # Assume we're looking repo-wide heads if no revs were specified.
1248 1250 heads = repo.heads(start)
1249 1251 else:
1250 1252 heads = []
1251 1253 visitedset = util.set()
1252 1254 for branchrev in branchrevs:
1253 1255 branch = repo[branchrev].branch()
1254 1256 if branch in visitedset:
1255 1257 continue
1256 1258 visitedset.add(branch)
1257 1259 bheads = repo.branchheads(branch, start)
1258 1260 if not bheads:
1259 1261 if branch != branchrev:
1260 1262 ui.warn(_("no changes on branch %s containing %s are "
1261 1263 "reachable from %s\n")
1262 1264 % (branch, branchrev, opts.get('rev')))
1263 1265 else:
1264 1266 ui.warn(_("no changes on branch %s are reachable from %s\n")
1265 1267 % (branch, opts.get('rev')))
1266 1268 heads.extend(bheads)
1267 1269 if not heads:
1268 1270 return 1
1269 1271 displayer = cmdutil.show_changeset(ui, repo, opts)
1270 1272 for n in heads:
1271 1273 displayer.show(changenode=n)
1272 1274
1273 1275 def help_(ui, name=None, with_version=False):
1274 1276 """show help for a given topic or a help overview
1275 1277
1276 1278 With no arguments, print a list of commands and short help.
1277 1279
1278 1280 Given a topic, extension, or command name, print help for that topic."""
1279 1281 option_lists = []
1280 1282
1281 1283 def addglobalopts(aliases):
1282 1284 if ui.verbose:
1283 1285 option_lists.append((_("global options:"), globalopts))
1284 1286 if name == 'shortlist':
1285 1287 option_lists.append((_('use "hg help" for the full list '
1286 1288 'of commands'), ()))
1287 1289 else:
1288 1290 if name == 'shortlist':
1289 1291 msg = _('use "hg help" for the full list of commands '
1290 1292 'or "hg -v" for details')
1291 1293 elif aliases:
1292 1294 msg = _('use "hg -v help%s" to show aliases and '
1293 1295 'global options') % (name and " " + name or "")
1294 1296 else:
1295 1297 msg = _('use "hg -v help %s" to show global options') % name
1296 1298 option_lists.append((msg, ()))
1297 1299
1298 1300 def helpcmd(name):
1299 1301 if with_version:
1300 1302 version_(ui)
1301 1303 ui.write('\n')
1302 1304
1303 1305 try:
1304 1306 aliases, i = cmdutil.findcmd(name, table, False)
1305 1307 except cmdutil.AmbiguousCommand, inst:
1306 1308 select = lambda c: c.lstrip('^').startswith(inst.args[0])
1307 1309 helplist(_('list of commands:\n\n'), select)
1308 1310 return
1309 1311
1310 1312 # synopsis
1311 1313 ui.write("%s\n" % i[2])
1312 1314
1313 1315 # aliases
1314 1316 if not ui.quiet and len(aliases) > 1:
1315 1317 ui.write(_("\naliases: %s\n") % ', '.join(aliases[1:]))
1316 1318
1317 1319 # description
1318 1320 doc = gettext(i[0].__doc__)
1319 1321 if not doc:
1320 1322 doc = _("(No help text available)")
1321 1323 if ui.quiet:
1322 1324 doc = doc.splitlines(0)[0]
1323 1325 ui.write("\n%s\n" % doc.rstrip())
1324 1326
1325 1327 if not ui.quiet:
1326 1328 # options
1327 1329 if i[1]:
1328 1330 option_lists.append((_("options:\n"), i[1]))
1329 1331
1330 1332 addglobalopts(False)
1331 1333
1332 1334 def helplist(header, select=None):
1333 1335 h = {}
1334 1336 cmds = {}
1335 1337 for c, e in table.items():
1336 1338 f = c.split("|", 1)[0]
1337 1339 if select and not select(f):
1338 1340 continue
1339 1341 if (not select and name != 'shortlist' and
1340 1342 e[0].__module__ != __name__):
1341 1343 continue
1342 1344 if name == "shortlist" and not f.startswith("^"):
1343 1345 continue
1344 1346 f = f.lstrip("^")
1345 1347 if not ui.debugflag and f.startswith("debug"):
1346 1348 continue
1347 1349 doc = gettext(e[0].__doc__)
1348 1350 if not doc:
1349 1351 doc = _("(No help text available)")
1350 1352 h[f] = doc.splitlines(0)[0].rstrip()
1351 1353 cmds[f] = c.lstrip("^")
1352 1354
1353 1355 if not h:
1354 1356 ui.status(_('no commands defined\n'))
1355 1357 return
1356 1358
1357 1359 ui.status(header)
1358 1360 fns = util.sort(h)
1359 1361 m = max(map(len, fns))
1360 1362 for f in fns:
1361 1363 if ui.verbose:
1362 1364 commands = cmds[f].replace("|",", ")
1363 1365 ui.write(" %s:\n %s\n"%(commands, h[f]))
1364 1366 else:
1365 1367 ui.write(' %-*s %s\n' % (m, f, h[f]))
1366 1368
1367 1369 exts = list(extensions.extensions())
1368 1370 if exts and name != 'shortlist':
1369 1371 ui.write(_('\nenabled extensions:\n\n'))
1370 1372 maxlength = 0
1371 1373 exthelps = []
1372 1374 for ename, ext in exts:
1373 1375 doc = (ext.__doc__ or _('(no help text available)'))
1374 1376 ename = ename.split('.')[-1]
1375 1377 maxlength = max(len(ename), maxlength)
1376 1378 exthelps.append((ename, doc.splitlines(0)[0].strip()))
1377 1379 for ename, text in exthelps:
1378 1380 ui.write(_(' %s %s\n') % (ename.ljust(maxlength), text))
1379 1381
1380 1382 if not ui.quiet:
1381 1383 addglobalopts(True)
1382 1384
1383 1385 def helptopic(name):
1384 1386 for names, header, doc in help.helptable:
1385 1387 if name in names:
1386 1388 break
1387 1389 else:
1388 1390 raise cmdutil.UnknownCommand(name)
1389 1391
1390 1392 # description
1391 1393 if not doc:
1392 1394 doc = _("(No help text available)")
1393 1395 if callable(doc):
1394 1396 doc = doc()
1395 1397
1396 1398 ui.write("%s\n" % header)
1397 1399 ui.write("%s\n" % doc.rstrip())
1398 1400
1399 1401 def helpext(name):
1400 1402 try:
1401 1403 mod = extensions.find(name)
1402 1404 except KeyError:
1403 1405 raise cmdutil.UnknownCommand(name)
1404 1406
1405 1407 doc = gettext(mod.__doc__) or _('No help text available')
1406 1408 doc = doc.splitlines(0)
1407 1409 ui.write(_('%s extension - %s\n') % (name.split('.')[-1], doc[0]))
1408 1410 for d in doc[1:]:
1409 1411 ui.write(d, '\n')
1410 1412
1411 1413 ui.status('\n')
1412 1414
1413 1415 try:
1414 1416 ct = mod.cmdtable
1415 1417 except AttributeError:
1416 1418 ct = {}
1417 1419
1418 1420 modcmds = dict.fromkeys([c.split('|', 1)[0] for c in ct])
1419 1421 helplist(_('list of commands:\n\n'), modcmds.has_key)
1420 1422
1421 1423 if name and name != 'shortlist':
1422 1424 i = None
1423 1425 for f in (helpcmd, helptopic, helpext):
1424 1426 try:
1425 1427 f(name)
1426 1428 i = None
1427 1429 break
1428 1430 except cmdutil.UnknownCommand, inst:
1429 1431 i = inst
1430 1432 if i:
1431 1433 raise i
1432 1434
1433 1435 else:
1434 1436 # program name
1435 1437 if ui.verbose or with_version:
1436 1438 version_(ui)
1437 1439 else:
1438 1440 ui.status(_("Mercurial Distributed SCM\n"))
1439 1441 ui.status('\n')
1440 1442
1441 1443 # list of commands
1442 1444 if name == "shortlist":
1443 1445 header = _('basic commands:\n\n')
1444 1446 else:
1445 1447 header = _('list of commands:\n\n')
1446 1448
1447 1449 helplist(header)
1448 1450
1449 1451 # list all option lists
1450 1452 opt_output = []
1451 1453 for title, options in option_lists:
1452 1454 opt_output.append(("\n%s" % title, None))
1453 1455 for shortopt, longopt, default, desc in options:
1454 1456 if "DEPRECATED" in desc and not ui.verbose: continue
1455 1457 opt_output.append(("%2s%s" % (shortopt and "-%s" % shortopt,
1456 1458 longopt and " --%s" % longopt),
1457 1459 "%s%s" % (desc,
1458 1460 default
1459 1461 and _(" (default: %s)") % default
1460 1462 or "")))
1461 1463
1462 1464 if not name:
1463 1465 ui.write(_("\nadditional help topics:\n\n"))
1464 1466 topics = []
1465 1467 for names, header, doc in help.helptable:
1466 1468 names = [(-len(name), name) for name in names]
1467 1469 names.sort()
1468 1470 topics.append((names[0][1], header))
1469 1471 topics_len = max([len(s[0]) for s in topics])
1470 1472 for t, desc in topics:
1471 1473 ui.write(" %-*s %s\n" % (topics_len, t, desc))
1472 1474
1473 1475 if opt_output:
1474 1476 opts_len = max([len(line[0]) for line in opt_output if line[1]] or [0])
1475 1477 for first, second in opt_output:
1476 1478 if second:
1477 1479 ui.write(" %-*s %s\n" % (opts_len, first, second))
1478 1480 else:
1479 1481 ui.write("%s\n" % first)
1480 1482
1481 1483 def identify(ui, repo, source=None,
1482 1484 rev=None, num=None, id=None, branch=None, tags=None):
1483 1485 """identify the working copy or specified revision
1484 1486
1485 1487 With no revision, print a summary of the current state of the repo.
1486 1488
1487 1489 With a path, do a lookup in another repository.
1488 1490
1489 1491 This summary identifies the repository state using one or two parent
1490 1492 hash identifiers, followed by a "+" if there are uncommitted changes
1491 1493 in the working directory, a list of tags for this revision and a branch
1492 1494 name for non-default branches.
1493 1495 """
1494 1496
1495 1497 if not repo and not source:
1496 1498 raise util.Abort(_("There is no Mercurial repository here "
1497 1499 "(.hg not found)"))
1498 1500
1499 1501 hexfunc = ui.debugflag and hex or short
1500 1502 default = not (num or id or branch or tags)
1501 1503 output = []
1502 1504
1503 1505 if source:
1504 1506 source, revs, checkout = hg.parseurl(ui.expandpath(source), [])
1505 1507 srepo = hg.repository(ui, source)
1506 1508 if not rev and revs:
1507 1509 rev = revs[0]
1508 1510 if not rev:
1509 1511 rev = "tip"
1510 1512 if num or branch or tags:
1511 1513 raise util.Abort(
1512 1514 "can't query remote revision number, branch, or tags")
1513 1515 output = [hexfunc(srepo.lookup(rev))]
1514 1516 elif not rev:
1515 1517 ctx = repo[None]
1516 1518 parents = ctx.parents()
1517 1519 changed = False
1518 1520 if default or id or num:
1519 1521 changed = ctx.files() + ctx.deleted()
1520 1522 if default or id:
1521 1523 output = ["%s%s" % ('+'.join([hexfunc(p.node()) for p in parents]),
1522 1524 (changed) and "+" or "")]
1523 1525 if num:
1524 1526 output.append("%s%s" % ('+'.join([str(p.rev()) for p in parents]),
1525 1527 (changed) and "+" or ""))
1526 1528 else:
1527 1529 ctx = repo[rev]
1528 1530 if default or id:
1529 1531 output = [hexfunc(ctx.node())]
1530 1532 if num:
1531 1533 output.append(str(ctx.rev()))
1532 1534
1533 1535 if not source and default and not ui.quiet:
1534 1536 b = util.tolocal(ctx.branch())
1535 1537 if b != 'default':
1536 1538 output.append("(%s)" % b)
1537 1539
1538 1540 # multiple tags for a single parent separated by '/'
1539 1541 t = "/".join(ctx.tags())
1540 1542 if t:
1541 1543 output.append(t)
1542 1544
1543 1545 if branch:
1544 1546 output.append(util.tolocal(ctx.branch()))
1545 1547
1546 1548 if tags:
1547 1549 output.extend(ctx.tags())
1548 1550
1549 1551 ui.write("%s\n" % ' '.join(output))
1550 1552
1551 1553 def import_(ui, repo, patch1, *patches, **opts):
1552 1554 """import an ordered set of patches
1553 1555
1554 1556 Import a list of patches and commit them individually.
1555 1557
1556 1558 If there are outstanding changes in the working directory, import
1557 1559 will abort unless given the -f flag.
1558 1560
1559 1561 You can import a patch straight from a mail message. Even patches
1560 1562 as attachments work (body part must be type text/plain or
1561 1563 text/x-patch to be used). From and Subject headers of email
1562 1564 message are used as default committer and commit message. All
1563 1565 text/plain body parts before first diff are added to commit
1564 1566 message.
1565 1567
1566 1568 If the imported patch was generated by hg export, user and description
1567 1569 from patch override values from message headers and body. Values
1568 1570 given on command line with -m and -u override these.
1569 1571
1570 1572 If --exact is specified, import will set the working directory
1571 1573 to the parent of each patch before applying it, and will abort
1572 1574 if the resulting changeset has a different ID than the one
1573 1575 recorded in the patch. This may happen due to character set
1574 1576 problems or other deficiencies in the text patch format.
1575 1577
1576 1578 To read a patch from standard input, use patch name "-".
1577 1579 See 'hg help dates' for a list of formats valid for -d/--date.
1578 1580 """
1579 1581 patches = (patch1,) + patches
1580 1582
1581 1583 date = opts.get('date')
1582 1584 if date:
1583 1585 opts['date'] = util.parsedate(date)
1584 1586
1585 1587 if opts.get('exact') or not opts.get('force'):
1586 1588 cmdutil.bail_if_changed(repo)
1587 1589
1588 1590 d = opts["base"]
1589 1591 strip = opts["strip"]
1590 1592 wlock = lock = None
1591 1593 try:
1592 1594 wlock = repo.wlock()
1593 1595 lock = repo.lock()
1594 1596 for p in patches:
1595 1597 pf = os.path.join(d, p)
1596 1598
1597 1599 if pf == '-':
1598 1600 ui.status(_("applying patch from stdin\n"))
1599 1601 pf = sys.stdin
1600 1602 else:
1601 1603 ui.status(_("applying %s\n") % p)
1602 1604 pf = url.open(ui, pf)
1603 1605 data = patch.extract(ui, pf)
1604 1606 tmpname, message, user, date, branch, nodeid, p1, p2 = data
1605 1607
1606 1608 if tmpname is None:
1607 1609 raise util.Abort(_('no diffs found'))
1608 1610
1609 1611 try:
1610 1612 cmdline_message = cmdutil.logmessage(opts)
1611 1613 if cmdline_message:
1612 1614 # pickup the cmdline msg
1613 1615 message = cmdline_message
1614 1616 elif message:
1615 1617 # pickup the patch msg
1616 1618 message = message.strip()
1617 1619 else:
1618 1620 # launch the editor
1619 1621 message = None
1620 1622 ui.debug(_('message:\n%s\n') % message)
1621 1623
1622 1624 wp = repo.parents()
1623 1625 if opts.get('exact'):
1624 1626 if not nodeid or not p1:
1625 1627 raise util.Abort(_('not a mercurial patch'))
1626 1628 p1 = repo.lookup(p1)
1627 1629 p2 = repo.lookup(p2 or hex(nullid))
1628 1630
1629 1631 if p1 != wp[0].node():
1630 1632 hg.clean(repo, p1)
1631 1633 repo.dirstate.setparents(p1, p2)
1632 1634 elif p2:
1633 1635 try:
1634 1636 p1 = repo.lookup(p1)
1635 1637 p2 = repo.lookup(p2)
1636 1638 if p1 == wp[0].node():
1637 1639 repo.dirstate.setparents(p1, p2)
1638 1640 except RepoError:
1639 1641 pass
1640 1642 if opts.get('exact') or opts.get('import_branch'):
1641 1643 repo.dirstate.setbranch(branch or 'default')
1642 1644
1643 1645 files = {}
1644 1646 try:
1645 1647 fuzz = patch.patch(tmpname, ui, strip=strip, cwd=repo.root,
1646 1648 files=files)
1647 1649 finally:
1648 1650 files = patch.updatedir(ui, repo, files)
1649 1651 if not opts.get('no_commit'):
1650 1652 n = repo.commit(files, message, opts.get('user') or user,
1651 1653 opts.get('date') or date)
1652 1654 if opts.get('exact'):
1653 1655 if hex(n) != nodeid:
1654 1656 repo.rollback()
1655 1657 raise util.Abort(_('patch is damaged'
1656 1658 ' or loses information'))
1657 1659 # Force a dirstate write so that the next transaction
1658 1660 # backups an up-do-date file.
1659 1661 repo.dirstate.write()
1660 1662 finally:
1661 1663 os.unlink(tmpname)
1662 1664 finally:
1663 1665 del lock, wlock
1664 1666
1665 1667 def incoming(ui, repo, source="default", **opts):
1666 1668 """show new changesets found in source
1667 1669
1668 1670 Show new changesets found in the specified path/URL or the default
1669 1671 pull location. These are the changesets that would be pulled if a pull
1670 1672 was requested.
1671 1673
1672 1674 For remote repository, using --bundle avoids downloading the changesets
1673 1675 twice if the incoming is followed by a pull.
1674 1676
1675 1677 See pull for valid source format details.
1676 1678 """
1677 1679 limit = cmdutil.loglimit(opts)
1678 1680 source, revs, checkout = hg.parseurl(ui.expandpath(source), opts.get('rev'))
1679 1681 cmdutil.setremoteconfig(ui, opts)
1680 1682
1681 1683 other = hg.repository(ui, source)
1682 1684 ui.status(_('comparing with %s\n') % url.hidepassword(source))
1683 1685 if revs:
1684 1686 revs = [other.lookup(rev) for rev in revs]
1685 1687 incoming = repo.findincoming(other, heads=revs, force=opts["force"])
1686 1688 if not incoming:
1687 1689 try:
1688 1690 os.unlink(opts["bundle"])
1689 1691 except:
1690 1692 pass
1691 1693 ui.status(_("no changes found\n"))
1692 1694 return 1
1693 1695
1694 1696 cleanup = None
1695 1697 try:
1696 1698 fname = opts["bundle"]
1697 1699 if fname or not other.local():
1698 1700 # create a bundle (uncompressed if other repo is not local)
1699 1701 if revs is None:
1700 1702 cg = other.changegroup(incoming, "incoming")
1701 1703 else:
1702 1704 cg = other.changegroupsubset(incoming, revs, 'incoming')
1703 1705 bundletype = other.local() and "HG10BZ" or "HG10UN"
1704 1706 fname = cleanup = changegroup.writebundle(cg, fname, bundletype)
1705 1707 # keep written bundle?
1706 1708 if opts["bundle"]:
1707 1709 cleanup = None
1708 1710 if not other.local():
1709 1711 # use the created uncompressed bundlerepo
1710 1712 other = bundlerepo.bundlerepository(ui, repo.root, fname)
1711 1713
1712 1714 o = other.changelog.nodesbetween(incoming, revs)[0]
1713 1715 if opts.get('newest_first'):
1714 1716 o.reverse()
1715 1717 displayer = cmdutil.show_changeset(ui, other, opts)
1716 1718 count = 0
1717 1719 for n in o:
1718 1720 if count >= limit:
1719 1721 break
1720 1722 parents = [p for p in other.changelog.parents(n) if p != nullid]
1721 1723 if opts.get('no_merges') and len(parents) == 2:
1722 1724 continue
1723 1725 count += 1
1724 1726 displayer.show(changenode=n)
1725 1727 finally:
1726 1728 if hasattr(other, 'close'):
1727 1729 other.close()
1728 1730 if cleanup:
1729 1731 os.unlink(cleanup)
1730 1732
1731 1733 def init(ui, dest=".", **opts):
1732 1734 """create a new repository in the given directory
1733 1735
1734 1736 Initialize a new repository in the given directory. If the given
1735 1737 directory does not exist, it is created.
1736 1738
1737 1739 If no directory is given, the current directory is used.
1738 1740
1739 1741 It is possible to specify an ssh:// URL as the destination.
1740 1742 Look at the help text for the pull command for important details
1741 1743 about ssh:// URLs.
1742 1744 """
1743 1745 cmdutil.setremoteconfig(ui, opts)
1744 1746 hg.repository(ui, dest, create=1)
1745 1747
1746 1748 def locate(ui, repo, *pats, **opts):
1747 1749 """locate files matching specific patterns
1748 1750
1749 1751 Print all files under Mercurial control whose names match the
1750 1752 given patterns.
1751 1753
1752 1754 This command searches the entire repository by default. To search
1753 1755 just the current directory and its subdirectories, use
1754 1756 "--include .".
1755 1757
1756 1758 If no patterns are given to match, this command prints all file
1757 1759 names.
1758 1760
1759 1761 If you want to feed the output of this command into the "xargs"
1760 1762 command, use the "-0" option to both this command and "xargs".
1761 1763 This will avoid the problem of "xargs" treating single filenames
1762 1764 that contain white space as multiple filenames.
1763 1765 """
1764 1766 end = opts.get('print0') and '\0' or '\n'
1765 1767 rev = opts.get('rev') or None
1766 1768
1767 1769 ret = 1
1768 1770 m = cmdutil.match(repo, pats, opts, default='relglob')
1769 1771 m.bad = lambda x,y: False
1770 1772 for abs in repo[rev].walk(m):
1771 1773 if not rev and abs not in repo.dirstate:
1772 1774 continue
1773 1775 if opts.get('fullpath'):
1774 1776 ui.write(os.path.join(repo.root, abs), end)
1775 1777 else:
1776 1778 ui.write(((pats and m.rel(abs)) or abs), end)
1777 1779 ret = 0
1778 1780
1779 1781 return ret
1780 1782
1781 1783 def log(ui, repo, *pats, **opts):
1782 1784 """show revision history of entire repository or files
1783 1785
1784 1786 Print the revision history of the specified files or the entire
1785 1787 project.
1786 1788
1787 1789 File history is shown without following rename or copy history of
1788 1790 files. Use -f/--follow with a file name to follow history across
1789 1791 renames and copies. --follow without a file name will only show
1790 1792 ancestors or descendants of the starting revision. --follow-first
1791 1793 only follows the first parent of merge revisions.
1792 1794
1793 1795 If no revision range is specified, the default is tip:0 unless
1794 1796 --follow is set, in which case the working directory parent is
1795 1797 used as the starting revision.
1796 1798
1797 1799 See 'hg help dates' for a list of formats valid for -d/--date.
1798 1800
1799 1801 By default this command outputs: changeset id and hash, tags,
1800 1802 non-trivial parents, user, date and time, and a summary for each
1801 1803 commit. When the -v/--verbose switch is used, the list of changed
1802 1804 files and full commit message is shown.
1803 1805
1804 1806 NOTE: log -p may generate unexpected diff output for merge
1805 1807 changesets, as it will compare the merge changeset against its
1806 1808 first parent only. Also, the files: list will only reflect files
1807 1809 that are different from BOTH parents.
1808 1810
1809 1811 """
1810 1812
1811 1813 get = util.cachefunc(lambda r: repo[r].changeset())
1812 1814 changeiter, matchfn = cmdutil.walkchangerevs(ui, repo, pats, get, opts)
1813 1815
1814 1816 limit = cmdutil.loglimit(opts)
1815 1817 count = 0
1816 1818
1817 1819 if opts.get('copies') and opts.get('rev'):
1818 1820 endrev = max(cmdutil.revrange(repo, opts.get('rev'))) + 1
1819 1821 else:
1820 1822 endrev = len(repo)
1821 1823 rcache = {}
1822 1824 ncache = {}
1823 1825 def getrenamed(fn, rev):
1824 1826 '''looks up all renames for a file (up to endrev) the first
1825 1827 time the file is given. It indexes on the changerev and only
1826 1828 parses the manifest if linkrev != changerev.
1827 1829 Returns rename info for fn at changerev rev.'''
1828 1830 if fn not in rcache:
1829 1831 rcache[fn] = {}
1830 1832 ncache[fn] = {}
1831 1833 fl = repo.file(fn)
1832 1834 for i in fl:
1833 1835 node = fl.node(i)
1834 1836 lr = fl.linkrev(node)
1835 1837 renamed = fl.renamed(node)
1836 1838 rcache[fn][lr] = renamed
1837 1839 if renamed:
1838 1840 ncache[fn][node] = renamed
1839 1841 if lr >= endrev:
1840 1842 break
1841 1843 if rev in rcache[fn]:
1842 1844 return rcache[fn][rev]
1843 1845
1844 1846 # If linkrev != rev (i.e. rev not found in rcache) fallback to
1845 1847 # filectx logic.
1846 1848
1847 1849 try:
1848 1850 return repo[rev][fn].renamed()
1849 1851 except revlog.LookupError:
1850 1852 pass
1851 1853 return None
1852 1854
1853 1855 df = False
1854 1856 if opts["date"]:
1855 1857 df = util.matchdate(opts["date"])
1856 1858
1857 1859 only_branches = opts.get('only_branch')
1858 1860
1859 1861 displayer = cmdutil.show_changeset(ui, repo, opts, True, matchfn)
1860 1862 for st, rev, fns in changeiter:
1861 1863 if st == 'add':
1862 1864 changenode = repo.changelog.node(rev)
1863 1865 parents = [p for p in repo.changelog.parentrevs(rev)
1864 1866 if p != nullrev]
1865 1867 if opts.get('no_merges') and len(parents) == 2:
1866 1868 continue
1867 1869 if opts.get('only_merges') and len(parents) != 2:
1868 1870 continue
1869 1871
1870 1872 if only_branches:
1871 1873 revbranch = get(rev)[5]['branch']
1872 1874 if revbranch not in only_branches:
1873 1875 continue
1874 1876
1875 1877 if df:
1876 1878 changes = get(rev)
1877 1879 if not df(changes[2][0]):
1878 1880 continue
1879 1881
1880 1882 if opts.get('keyword'):
1881 1883 changes = get(rev)
1882 1884 miss = 0
1883 1885 for k in [kw.lower() for kw in opts['keyword']]:
1884 1886 if not (k in changes[1].lower() or
1885 1887 k in changes[4].lower() or
1886 1888 k in " ".join(changes[3]).lower()):
1887 1889 miss = 1
1888 1890 break
1889 1891 if miss:
1890 1892 continue
1891 1893
1892 1894 if opts['user']:
1893 1895 changes = get(rev)
1894 1896 miss = 0
1895 1897 for k in opts['user']:
1896 1898 if k != changes[1]:
1897 1899 miss = 1
1898 1900 break
1899 1901 if miss:
1900 1902 continue
1901 1903
1902 1904 copies = []
1903 1905 if opts.get('copies') and rev:
1904 1906 for fn in get(rev)[3]:
1905 1907 rename = getrenamed(fn, rev)
1906 1908 if rename:
1907 1909 copies.append((fn, rename[0]))
1908 1910 displayer.show(rev, changenode, copies=copies)
1909 1911 elif st == 'iter':
1910 1912 if count == limit: break
1911 1913 if displayer.flush(rev):
1912 1914 count += 1
1913 1915
1914 1916 def manifest(ui, repo, node=None, rev=None):
1915 1917 """output the current or given revision of the project manifest
1916 1918
1917 1919 Print a list of version controlled files for the given revision.
1918 1920 If no revision is given, the parent of the working directory is used,
1919 1921 or tip if no revision is checked out.
1920 1922
1921 1923 The manifest is the list of files being version controlled. If no revision
1922 1924 is given then the first parent of the working directory is used.
1923 1925
1924 1926 With -v flag, print file permissions, symlink and executable bits. With
1925 1927 --debug flag, print file revision hashes.
1926 1928 """
1927 1929
1928 1930 if rev and node:
1929 1931 raise util.Abort(_("please specify just one revision"))
1930 1932
1931 1933 if not node:
1932 1934 node = rev
1933 1935
1934 1936 decor = {'l':'644 @ ', 'x':'755 * ', '':'644 '}
1935 1937 ctx = repo[node]
1936 1938 for f in ctx:
1937 1939 if ui.debugflag:
1938 1940 ui.write("%40s " % hex(ctx.manifest()[f]))
1939 1941 if ui.verbose:
1940 1942 ui.write(decor[ctx.flags(f)])
1941 1943 ui.write("%s\n" % f)
1942 1944
1943 1945 def merge(ui, repo, node=None, force=None, rev=None):
1944 1946 """merge working directory with another revision
1945 1947
1946 1948 Merge the contents of the current working directory and the
1947 1949 requested revision. Files that changed between either parent are
1948 1950 marked as changed for the next commit and a commit must be
1949 1951 performed before any further updates are allowed.
1950 1952
1951 1953 If no revision is specified, the working directory's parent is a
1952 1954 head revision, and the current branch contains exactly one other head,
1953 1955 the other head is merged with by default. Otherwise, an explicit
1954 1956 revision to merge with must be provided.
1955 1957 """
1956 1958
1957 1959 if rev and node:
1958 1960 raise util.Abort(_("please specify just one revision"))
1959 1961 if not node:
1960 1962 node = rev
1961 1963
1962 1964 if not node:
1963 1965 branch = repo.changectx(None).branch()
1964 1966 bheads = repo.branchheads(branch)
1965 1967 if len(bheads) > 2:
1966 1968 raise util.Abort(_("branch '%s' has %d heads - "
1967 1969 "please merge with an explicit rev") %
1968 1970 (branch, len(bheads)))
1969 1971
1970 1972 parent = repo.dirstate.parents()[0]
1971 1973 if len(bheads) == 1:
1972 1974 if len(repo.heads()) > 1:
1973 1975 raise util.Abort(_("branch '%s' has one head - "
1974 1976 "please merge with an explicit rev") %
1975 1977 branch)
1976 1978 msg = _('there is nothing to merge')
1977 1979 if parent != repo.lookup(repo[None].branch()):
1978 1980 msg = _('%s - use "hg update" instead') % msg
1979 1981 raise util.Abort(msg)
1980 1982
1981 1983 if parent not in bheads:
1982 1984 raise util.Abort(_('working dir not at a head rev - '
1983 1985 'use "hg update" or merge with an explicit rev'))
1984 1986 node = parent == bheads[0] and bheads[-1] or bheads[0]
1985 1987 return hg.merge(repo, node, force=force)
1986 1988
1987 1989 def outgoing(ui, repo, dest=None, **opts):
1988 1990 """show changesets not found in destination
1989 1991
1990 1992 Show changesets not found in the specified destination repository or
1991 1993 the default push location. These are the changesets that would be pushed
1992 1994 if a push was requested.
1993 1995
1994 1996 See pull for valid destination format details.
1995 1997 """
1996 1998 limit = cmdutil.loglimit(opts)
1997 1999 dest, revs, checkout = hg.parseurl(
1998 2000 ui.expandpath(dest or 'default-push', dest or 'default'), opts.get('rev'))
1999 2001 cmdutil.setremoteconfig(ui, opts)
2000 2002 if revs:
2001 2003 revs = [repo.lookup(rev) for rev in revs]
2002 2004
2003 2005 other = hg.repository(ui, dest)
2004 2006 ui.status(_('comparing with %s\n') % url.hidepassword(dest))
2005 2007 o = repo.findoutgoing(other, force=opts.get('force'))
2006 2008 if not o:
2007 2009 ui.status(_("no changes found\n"))
2008 2010 return 1
2009 2011 o = repo.changelog.nodesbetween(o, revs)[0]
2010 2012 if opts.get('newest_first'):
2011 2013 o.reverse()
2012 2014 displayer = cmdutil.show_changeset(ui, repo, opts)
2013 2015 count = 0
2014 2016 for n in o:
2015 2017 if count >= limit:
2016 2018 break
2017 2019 parents = [p for p in repo.changelog.parents(n) if p != nullid]
2018 2020 if opts.get('no_merges') and len(parents) == 2:
2019 2021 continue
2020 2022 count += 1
2021 2023 displayer.show(changenode=n)
2022 2024
2023 2025 def parents(ui, repo, file_=None, **opts):
2024 2026 """show the parents of the working dir or revision
2025 2027
2026 2028 Print the working directory's parent revisions. If a
2027 2029 revision is given via --rev, the parent of that revision
2028 2030 will be printed. If a file argument is given, revision in
2029 2031 which the file was last changed (before the working directory
2030 2032 revision or the argument to --rev if given) is printed.
2031 2033 """
2032 2034 rev = opts.get('rev')
2033 2035 if rev:
2034 2036 ctx = repo[rev]
2035 2037 else:
2036 2038 ctx = repo[None]
2037 2039
2038 2040 if file_:
2039 2041 m = cmdutil.match(repo, (file_,), opts)
2040 2042 if m.anypats() or len(m.files()) != 1:
2041 2043 raise util.Abort(_('can only specify an explicit file name'))
2042 2044 file_ = m.files()[0]
2043 2045 filenodes = []
2044 2046 for cp in ctx.parents():
2045 2047 if not cp:
2046 2048 continue
2047 2049 try:
2048 2050 filenodes.append(cp.filenode(file_))
2049 2051 except revlog.LookupError:
2050 2052 pass
2051 2053 if not filenodes:
2052 2054 raise util.Abort(_("'%s' not found in manifest!") % file_)
2053 2055 fl = repo.file(file_)
2054 2056 p = [repo.lookup(fl.linkrev(fn)) for fn in filenodes]
2055 2057 else:
2056 2058 p = [cp.node() for cp in ctx.parents()]
2057 2059
2058 2060 displayer = cmdutil.show_changeset(ui, repo, opts)
2059 2061 for n in p:
2060 2062 if n != nullid:
2061 2063 displayer.show(changenode=n)
2062 2064
2063 2065 def paths(ui, repo, search=None):
2064 2066 """show definition of symbolic path names
2065 2067
2066 2068 Show definition of symbolic path name NAME. If no name is given, show
2067 2069 definition of available names.
2068 2070
2069 2071 Path names are defined in the [paths] section of /etc/mercurial/hgrc
2070 2072 and $HOME/.hgrc. If run inside a repository, .hg/hgrc is used, too.
2071 2073 """
2072 2074 if search:
2073 2075 for name, path in ui.configitems("paths"):
2074 2076 if name == search:
2075 2077 ui.write("%s\n" % url.hidepassword(path))
2076 2078 return
2077 2079 ui.warn(_("not found!\n"))
2078 2080 return 1
2079 2081 else:
2080 2082 for name, path in ui.configitems("paths"):
2081 2083 ui.write("%s = %s\n" % (name, url.hidepassword(path)))
2082 2084
2083 2085 def postincoming(ui, repo, modheads, optupdate, checkout):
2084 2086 if modheads == 0:
2085 2087 return
2086 2088 if optupdate:
2087 2089 if modheads <= 1 or checkout:
2088 2090 return hg.update(repo, checkout)
2089 2091 else:
2090 2092 ui.status(_("not updating, since new heads added\n"))
2091 2093 if modheads > 1:
2092 2094 ui.status(_("(run 'hg heads' to see heads, 'hg merge' to merge)\n"))
2093 2095 else:
2094 2096 ui.status(_("(run 'hg update' to get a working copy)\n"))
2095 2097
2096 2098 def pull(ui, repo, source="default", **opts):
2097 2099 """pull changes from the specified source
2098 2100
2099 2101 Pull changes from a remote repository to a local one.
2100 2102
2101 2103 This finds all changes from the repository at the specified path
2102 2104 or URL and adds them to the local repository. By default, this
2103 2105 does not update the copy of the project in the working directory.
2104 2106
2105 2107 Valid URLs are of the form:
2106 2108
2107 2109 local/filesystem/path (or file://local/filesystem/path)
2108 2110 http://[user[:pass]@]host[:port]/[path]
2109 2111 https://[user[:pass]@]host[:port]/[path]
2110 2112 ssh://[user[:pass]@]host[:port]/[path]
2111 2113
2112 2114 Paths in the local filesystem can either point to Mercurial
2113 2115 repositories or to bundle files (as created by 'hg bundle' or
2114 2116 'hg incoming --bundle').
2115 2117
2116 2118 An optional identifier after # indicates a particular branch, tag,
2117 2119 or changeset to pull.
2118 2120
2119 2121 Some notes about using SSH with Mercurial:
2120 2122 - SSH requires an accessible shell account on the destination machine
2121 2123 and a copy of hg in the remote path or specified with as remotecmd.
2122 2124 - path is relative to the remote user's home directory by default.
2123 2125 Use an extra slash at the start of a path to specify an absolute path:
2124 2126 ssh://example.com//tmp/repository
2125 2127 - Mercurial doesn't use its own compression via SSH; the right thing
2126 2128 to do is to configure it in your ~/.ssh/config, e.g.:
2127 2129 Host *.mylocalnetwork.example.com
2128 2130 Compression no
2129 2131 Host *
2130 2132 Compression yes
2131 2133 Alternatively specify "ssh -C" as your ssh command in your hgrc or
2132 2134 with the --ssh command line option.
2133 2135 """
2134 2136 source, revs, checkout = hg.parseurl(ui.expandpath(source), opts.get('rev'))
2135 2137 cmdutil.setremoteconfig(ui, opts)
2136 2138
2137 2139 other = hg.repository(ui, source)
2138 2140 ui.status(_('pulling from %s\n') % url.hidepassword(source))
2139 2141 if revs:
2140 2142 try:
2141 2143 revs = [other.lookup(rev) for rev in revs]
2142 2144 except NoCapability:
2143 2145 error = _("Other repository doesn't support revision lookup, "
2144 2146 "so a rev cannot be specified.")
2145 2147 raise util.Abort(error)
2146 2148
2147 2149 modheads = repo.pull(other, heads=revs, force=opts.get('force'))
2148 2150 return postincoming(ui, repo, modheads, opts.get('update'), checkout)
2149 2151
2150 2152 def push(ui, repo, dest=None, **opts):
2151 2153 """push changes to the specified destination
2152 2154
2153 2155 Push changes from the local repository to the given destination.
2154 2156
2155 2157 This is the symmetrical operation for pull. It helps to move
2156 2158 changes from the current repository to a different one. If the
2157 2159 destination is local this is identical to a pull in that directory
2158 2160 from the current one.
2159 2161
2160 2162 By default, push will refuse to run if it detects the result would
2161 2163 increase the number of remote heads. This generally indicates the
2162 2164 the client has forgotten to pull and merge before pushing.
2163 2165
2164 2166 Valid URLs are of the form:
2165 2167
2166 2168 local/filesystem/path (or file://local/filesystem/path)
2167 2169 ssh://[user[:pass]@]host[:port]/[path]
2168 2170 http://[user[:pass]@]host[:port]/[path]
2169 2171 https://[user[:pass]@]host[:port]/[path]
2170 2172
2171 2173 An optional identifier after # indicates a particular branch, tag,
2172 2174 or changeset to push. If -r is used, the named changeset and all its
2173 2175 ancestors will be pushed to the remote repository.
2174 2176
2175 2177 Look at the help text for the pull command for important details
2176 2178 about ssh:// URLs.
2177 2179
2178 2180 Pushing to http:// and https:// URLs is only possible, if this
2179 2181 feature is explicitly enabled on the remote Mercurial server.
2180 2182 """
2181 2183 dest, revs, checkout = hg.parseurl(
2182 2184 ui.expandpath(dest or 'default-push', dest or 'default'), opts.get('rev'))
2183 2185 cmdutil.setremoteconfig(ui, opts)
2184 2186
2185 2187 other = hg.repository(ui, dest)
2186 2188 ui.status(_('pushing to %s\n') % url.hidepassword(dest))
2187 2189 if revs:
2188 2190 revs = [repo.lookup(rev) for rev in revs]
2189 2191 r = repo.push(other, opts.get('force'), revs=revs)
2190 2192 return r == 0
2191 2193
2192 2194 def rawcommit(ui, repo, *pats, **opts):
2193 2195 """raw commit interface (DEPRECATED)
2194 2196
2195 2197 (DEPRECATED)
2196 2198 Lowlevel commit, for use in helper scripts.
2197 2199
2198 2200 This command is not intended to be used by normal users, as it is
2199 2201 primarily useful for importing from other SCMs.
2200 2202
2201 2203 This command is now deprecated and will be removed in a future
2202 2204 release, please use debugsetparents and commit instead.
2203 2205 """
2204 2206
2205 2207 ui.warn(_("(the rawcommit command is deprecated)\n"))
2206 2208
2207 2209 message = cmdutil.logmessage(opts)
2208 2210
2209 2211 files = cmdutil.match(repo, pats, opts).files()
2210 2212 if opts.get('files'):
2211 2213 files += open(opts['files']).read().splitlines()
2212 2214
2213 2215 parents = [repo.lookup(p) for p in opts['parent']]
2214 2216
2215 2217 try:
2216 2218 repo.rawcommit(files, message, opts['user'], opts['date'], *parents)
2217 2219 except ValueError, inst:
2218 2220 raise util.Abort(str(inst))
2219 2221
2220 2222 def recover(ui, repo):
2221 2223 """roll back an interrupted transaction
2222 2224
2223 2225 Recover from an interrupted commit or pull.
2224 2226
2225 2227 This command tries to fix the repository status after an interrupted
2226 2228 operation. It should only be necessary when Mercurial suggests it.
2227 2229 """
2228 2230 if repo.recover():
2229 2231 return hg.verify(repo)
2230 2232 return 1
2231 2233
2232 2234 def remove(ui, repo, *pats, **opts):
2233 2235 """remove the specified files on the next commit
2234 2236
2235 2237 Schedule the indicated files for removal from the repository.
2236 2238
2237 2239 This only removes files from the current branch, not from the entire
2238 2240 project history. -A can be used to remove only files that have already
2239 2241 been deleted, -f can be used to force deletion, and -Af can be used
2240 2242 to remove files from the next revision without deleting them.
2241 2243
2242 2244 The following table details the behavior of remove for different file
2243 2245 states (columns) and option combinations (rows). The file states are
2244 2246 Added, Clean, Modified and Missing (as reported by hg status). The
2245 2247 actions are Warn, Remove (from branch) and Delete (from disk).
2246 2248
2247 2249 A C M !
2248 2250 none W RD W R
2249 2251 -f R RD RD R
2250 2252 -A W W W R
2251 2253 -Af R R R R
2252 2254
2253 2255 This command schedules the files to be removed at the next commit.
2254 2256 To undo a remove before that, see hg revert.
2255 2257 """
2256 2258
2257 2259 after, force = opts.get('after'), opts.get('force')
2258 2260 if not pats and not after:
2259 2261 raise util.Abort(_('no files specified'))
2260 2262
2261 2263 m = cmdutil.match(repo, pats, opts)
2262 2264 s = repo.status(match=m, clean=True)
2263 2265 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2264 2266
2265 2267 def warn(files, reason):
2266 2268 for f in files:
2267 2269 ui.warn(_('not removing %s: file %s (use -f to force removal)\n')
2268 2270 % (m.rel(f), reason))
2269 2271
2270 2272 if force:
2271 2273 remove, forget = modified + deleted + clean, added
2272 2274 elif after:
2273 2275 remove, forget = deleted, []
2274 2276 warn(modified + added + clean, _('still exists'))
2275 2277 else:
2276 2278 remove, forget = deleted + clean, []
2277 2279 warn(modified, _('is modified'))
2278 2280 warn(added, _('has been marked for add'))
2279 2281
2280 2282 for f in util.sort(remove + forget):
2281 2283 if ui.verbose or not m.exact(f):
2282 2284 ui.status(_('removing %s\n') % m.rel(f))
2283 2285
2284 2286 repo.forget(forget)
2285 2287 repo.remove(remove, unlink=not after)
2286 2288
2287 2289 def rename(ui, repo, *pats, **opts):
2288 2290 """rename files; equivalent of copy + remove
2289 2291
2290 2292 Mark dest as copies of sources; mark sources for deletion. If
2291 2293 dest is a directory, copies are put in that directory. If dest is
2292 2294 a file, there can only be one source.
2293 2295
2294 2296 By default, this command copies the contents of files as they
2295 2297 stand in the working directory. If invoked with --after, the
2296 2298 operation is recorded, but no copying is performed.
2297 2299
2298 2300 This command takes effect in the next commit. To undo a rename
2299 2301 before that, see hg revert.
2300 2302 """
2301 2303 wlock = repo.wlock(False)
2302 2304 try:
2303 2305 return cmdutil.copy(ui, repo, pats, opts, rename=True)
2304 2306 finally:
2305 2307 del wlock
2306 2308
2307 2309 def resolve(ui, repo, *pats, **opts):
2308 2310 """resolve file merges from a branch merge or update
2309 2311
2310 2312 This command will attempt to resolve unresolved merges from the
2311 2313 last update or merge command. This will use the local file
2312 2314 revision preserved at the last update or merge to cleanly retry
2313 2315 the file merge attempt. With no file or options specified, this
2314 2316 command will attempt to resolve all unresolved files.
2315 2317
2316 2318 The codes used to show the status of files are:
2317 2319 U = unresolved
2318 2320 R = resolved
2319 2321 """
2320 2322
2321 2323 if len([x for x in opts if opts[x]]) > 1:
2322 2324 raise util.Abort(_("too many options specified"))
2323 2325
2324 2326 ms = merge_.mergestate(repo)
2325 2327 m = cmdutil.match(repo, pats, opts)
2326 2328
2327 2329 for f in ms:
2328 2330 if m(f):
2329 2331 if opts.get("list"):
2330 2332 ui.write("%s %s\n" % (ms[f].upper(), f))
2331 2333 elif opts.get("mark"):
2332 2334 ms.mark(f, "r")
2333 2335 elif opts.get("unmark"):
2334 2336 ms.mark(f, "u")
2335 2337 else:
2336 2338 wctx = repo[None]
2337 2339 mctx = wctx.parents()[-1]
2338 2340 ms.resolve(f, wctx, mctx)
2339 2341
2340 2342 def revert(ui, repo, *pats, **opts):
2341 2343 """restore individual files or dirs to an earlier state
2342 2344
2343 2345 (use update -r to check out earlier revisions, revert does not
2344 2346 change the working dir parents)
2345 2347
2346 2348 With no revision specified, revert the named files or directories
2347 2349 to the contents they had in the parent of the working directory.
2348 2350 This restores the contents of the affected files to an unmodified
2349 2351 state and unschedules adds, removes, copies, and renames. If the
2350 2352 working directory has two parents, you must explicitly specify the
2351 2353 revision to revert to.
2352 2354
2353 2355 Using the -r option, revert the given files or directories to their
2354 2356 contents as of a specific revision. This can be helpful to "roll
2355 2357 back" some or all of an earlier change.
2356 2358 See 'hg help dates' for a list of formats valid for -d/--date.
2357 2359
2358 2360 Revert modifies the working directory. It does not commit any
2359 2361 changes, or change the parent of the working directory. If you
2360 2362 revert to a revision other than the parent of the working
2361 2363 directory, the reverted files will thus appear modified
2362 2364 afterwards.
2363 2365
2364 2366 If a file has been deleted, it is restored. If the executable
2365 2367 mode of a file was changed, it is reset.
2366 2368
2367 2369 If names are given, all files matching the names are reverted.
2368 2370 If no arguments are given, no files are reverted.
2369 2371
2370 2372 Modified files are saved with a .orig suffix before reverting.
2371 2373 To disable these backups, use --no-backup.
2372 2374 """
2373 2375
2374 2376 if opts["date"]:
2375 2377 if opts["rev"]:
2376 2378 raise util.Abort(_("you can't specify a revision and a date"))
2377 2379 opts["rev"] = cmdutil.finddate(ui, repo, opts["date"])
2378 2380
2379 2381 if not pats and not opts.get('all'):
2380 2382 raise util.Abort(_('no files or directories specified; '
2381 2383 'use --all to revert the whole repo'))
2382 2384
2383 2385 parent, p2 = repo.dirstate.parents()
2384 2386 if not opts.get('rev') and p2 != nullid:
2385 2387 raise util.Abort(_('uncommitted merge - please provide a '
2386 2388 'specific revision'))
2387 2389 ctx = repo[opts.get('rev')]
2388 2390 node = ctx.node()
2389 2391 mf = ctx.manifest()
2390 2392 if node == parent:
2391 2393 pmf = mf
2392 2394 else:
2393 2395 pmf = None
2394 2396
2395 2397 # need all matching names in dirstate and manifest of target rev,
2396 2398 # so have to walk both. do not print errors if files exist in one
2397 2399 # but not other.
2398 2400
2399 2401 names = {}
2400 2402
2401 2403 wlock = repo.wlock()
2402 2404 try:
2403 2405 # walk dirstate.
2404 2406 files = []
2405 2407
2406 2408 m = cmdutil.match(repo, pats, opts)
2407 2409 m.bad = lambda x,y: False
2408 2410 for abs in repo.walk(m):
2409 2411 names[abs] = m.rel(abs), m.exact(abs)
2410 2412
2411 2413 # walk target manifest.
2412 2414
2413 2415 def badfn(path, msg):
2414 2416 if path in names:
2415 2417 return False
2416 2418 path_ = path + '/'
2417 2419 for f in names:
2418 2420 if f.startswith(path_):
2419 2421 return False
2420 2422 repo.ui.warn("%s: %s\n" % (m.rel(path), msg))
2421 2423 return False
2422 2424
2423 2425 m = cmdutil.match(repo, pats, opts)
2424 2426 m.bad = badfn
2425 2427 for abs in repo[node].walk(m):
2426 2428 if abs not in names:
2427 2429 names[abs] = m.rel(abs), m.exact(abs)
2428 2430
2429 2431 m = cmdutil.matchfiles(repo, names)
2430 2432 changes = repo.status(match=m)[:4]
2431 2433 modified, added, removed, deleted = map(dict.fromkeys, changes)
2432 2434
2433 2435 # if f is a rename, also revert the source
2434 2436 cwd = repo.getcwd()
2435 2437 for f in added:
2436 2438 src = repo.dirstate.copied(f)
2437 2439 if src and src not in names and repo.dirstate[src] == 'r':
2438 2440 removed[src] = None
2439 2441 names[src] = (repo.pathto(src, cwd), True)
2440 2442
2441 2443 def removeforget(abs):
2442 2444 if repo.dirstate[abs] == 'a':
2443 2445 return _('forgetting %s\n')
2444 2446 return _('removing %s\n')
2445 2447
2446 2448 revert = ([], _('reverting %s\n'))
2447 2449 add = ([], _('adding %s\n'))
2448 2450 remove = ([], removeforget)
2449 2451 undelete = ([], _('undeleting %s\n'))
2450 2452
2451 2453 disptable = (
2452 2454 # dispatch table:
2453 2455 # file state
2454 2456 # action if in target manifest
2455 2457 # action if not in target manifest
2456 2458 # make backup if in target manifest
2457 2459 # make backup if not in target manifest
2458 2460 (modified, revert, remove, True, True),
2459 2461 (added, revert, remove, True, False),
2460 2462 (removed, undelete, None, False, False),
2461 2463 (deleted, revert, remove, False, False),
2462 2464 )
2463 2465
2464 2466 for abs, (rel, exact) in util.sort(names.items()):
2465 2467 mfentry = mf.get(abs)
2466 2468 target = repo.wjoin(abs)
2467 2469 def handle(xlist, dobackup):
2468 2470 xlist[0].append(abs)
2469 2471 if dobackup and not opts.get('no_backup') and util.lexists(target):
2470 2472 bakname = "%s.orig" % rel
2471 2473 ui.note(_('saving current version of %s as %s\n') %
2472 2474 (rel, bakname))
2473 2475 if not opts.get('dry_run'):
2474 2476 util.copyfile(target, bakname)
2475 2477 if ui.verbose or not exact:
2476 2478 msg = xlist[1]
2477 2479 if not isinstance(msg, basestring):
2478 2480 msg = msg(abs)
2479 2481 ui.status(msg % rel)
2480 2482 for table, hitlist, misslist, backuphit, backupmiss in disptable:
2481 2483 if abs not in table: continue
2482 2484 # file has changed in dirstate
2483 2485 if mfentry:
2484 2486 handle(hitlist, backuphit)
2485 2487 elif misslist is not None:
2486 2488 handle(misslist, backupmiss)
2487 2489 break
2488 2490 else:
2489 2491 if abs not in repo.dirstate:
2490 2492 if mfentry:
2491 2493 handle(add, True)
2492 2494 elif exact:
2493 2495 ui.warn(_('file not managed: %s\n') % rel)
2494 2496 continue
2495 2497 # file has not changed in dirstate
2496 2498 if node == parent:
2497 2499 if exact: ui.warn(_('no changes needed to %s\n') % rel)
2498 2500 continue
2499 2501 if pmf is None:
2500 2502 # only need parent manifest in this unlikely case,
2501 2503 # so do not read by default
2502 2504 pmf = repo[parent].manifest()
2503 2505 if abs in pmf:
2504 2506 if mfentry:
2505 2507 # if version of file is same in parent and target
2506 2508 # manifests, do nothing
2507 2509 if (pmf[abs] != mfentry or
2508 2510 pmf.flags(abs) != mf.flags(abs)):
2509 2511 handle(revert, False)
2510 2512 else:
2511 2513 handle(remove, False)
2512 2514
2513 2515 if not opts.get('dry_run'):
2514 2516 def checkout(f):
2515 2517 fc = ctx[f]
2516 2518 repo.wwrite(f, fc.data(), fc.flags())
2517 2519
2518 2520 audit_path = util.path_auditor(repo.root)
2519 2521 for f in remove[0]:
2520 2522 if repo.dirstate[f] == 'a':
2521 2523 repo.dirstate.forget(f)
2522 2524 continue
2523 2525 audit_path(f)
2524 2526 try:
2525 2527 util.unlink(repo.wjoin(f))
2526 2528 except OSError:
2527 2529 pass
2528 2530 repo.dirstate.remove(f)
2529 2531
2530 2532 normal = None
2531 2533 if node == parent:
2532 2534 # We're reverting to our parent. If possible, we'd like status
2533 2535 # to report the file as clean. We have to use normallookup for
2534 2536 # merges to avoid losing information about merged/dirty files.
2535 2537 if p2 != nullid:
2536 2538 normal = repo.dirstate.normallookup
2537 2539 else:
2538 2540 normal = repo.dirstate.normal
2539 2541 for f in revert[0]:
2540 2542 checkout(f)
2541 2543 if normal:
2542 2544 normal(f)
2543 2545
2544 2546 for f in add[0]:
2545 2547 checkout(f)
2546 2548 repo.dirstate.add(f)
2547 2549
2548 2550 normal = repo.dirstate.normallookup
2549 2551 if node == parent and p2 == nullid:
2550 2552 normal = repo.dirstate.normal
2551 2553 for f in undelete[0]:
2552 2554 checkout(f)
2553 2555 normal(f)
2554 2556
2555 2557 finally:
2556 2558 del wlock
2557 2559
2558 2560 def rollback(ui, repo):
2559 2561 """roll back the last transaction
2560 2562
2561 2563 This command should be used with care. There is only one level of
2562 2564 rollback, and there is no way to undo a rollback. It will also
2563 2565 restore the dirstate at the time of the last transaction, losing
2564 2566 any dirstate changes since that time.
2565 2567
2566 2568 Transactions are used to encapsulate the effects of all commands
2567 2569 that create new changesets or propagate existing changesets into a
2568 2570 repository. For example, the following commands are transactional,
2569 2571 and their effects can be rolled back:
2570 2572
2571 2573 commit
2572 2574 import
2573 2575 pull
2574 2576 push (with this repository as destination)
2575 2577 unbundle
2576 2578
2577 2579 This command is not intended for use on public repositories. Once
2578 2580 changes are visible for pull by other users, rolling a transaction
2579 2581 back locally is ineffective (someone else may already have pulled
2580 2582 the changes). Furthermore, a race is possible with readers of the
2581 2583 repository; for example an in-progress pull from the repository
2582 2584 may fail if a rollback is performed.
2583 2585 """
2584 2586 repo.rollback()
2585 2587
2586 2588 def root(ui, repo):
2587 2589 """print the root (top) of the current working dir
2588 2590
2589 2591 Print the root directory of the current repository.
2590 2592 """
2591 2593 ui.write(repo.root + "\n")
2592 2594
2593 2595 def serve(ui, repo, **opts):
2594 2596 """export the repository via HTTP
2595 2597
2596 2598 Start a local HTTP repository browser and pull server.
2597 2599
2598 2600 By default, the server logs accesses to stdout and errors to
2599 2601 stderr. Use the "-A" and "-E" options to log to files.
2600 2602 """
2601 2603
2602 2604 if opts["stdio"]:
2603 2605 if repo is None:
2604 2606 raise RepoError(_("There is no Mercurial repository here"
2605 2607 " (.hg not found)"))
2606 2608 s = sshserver.sshserver(ui, repo)
2607 2609 s.serve_forever()
2608 2610
2609 2611 parentui = ui.parentui or ui
2610 2612 optlist = ("name templates style address port prefix ipv6"
2611 2613 " accesslog errorlog webdir_conf certificate")
2612 2614 for o in optlist.split():
2613 2615 if opts[o]:
2614 2616 parentui.setconfig("web", o, str(opts[o]))
2615 2617 if (repo is not None) and (repo.ui != parentui):
2616 2618 repo.ui.setconfig("web", o, str(opts[o]))
2617 2619
2618 2620 if repo is None and not ui.config("web", "webdir_conf"):
2619 2621 raise RepoError(_("There is no Mercurial repository here"
2620 2622 " (.hg not found)"))
2621 2623
2622 2624 class service:
2623 2625 def init(self):
2624 2626 util.set_signal_handler()
2625 2627 self.httpd = hgweb.server.create_server(parentui, repo)
2626 2628
2627 2629 if not ui.verbose: return
2628 2630
2629 2631 if self.httpd.prefix:
2630 2632 prefix = self.httpd.prefix.strip('/') + '/'
2631 2633 else:
2632 2634 prefix = ''
2633 2635
2634 2636 port = ':%d' % self.httpd.port
2635 2637 if port == ':80':
2636 2638 port = ''
2637 2639
2638 2640 bindaddr = self.httpd.addr
2639 2641 if bindaddr == '0.0.0.0':
2640 2642 bindaddr = '*'
2641 2643 elif ':' in bindaddr: # IPv6
2642 2644 bindaddr = '[%s]' % bindaddr
2643 2645
2644 2646 fqaddr = self.httpd.fqaddr
2645 2647 if ':' in fqaddr:
2646 2648 fqaddr = '[%s]' % fqaddr
2647 2649 ui.status(_('listening at http://%s%s/%s (bound to %s:%d)\n') %
2648 2650 (fqaddr, port, prefix, bindaddr, self.httpd.port))
2649 2651
2650 2652 def run(self):
2651 2653 self.httpd.serve_forever()
2652 2654
2653 2655 service = service()
2654 2656
2655 2657 cmdutil.service(opts, initfn=service.init, runfn=service.run)
2656 2658
2657 2659 def status(ui, repo, *pats, **opts):
2658 2660 """show changed files in the working directory
2659 2661
2660 2662 Show status of files in the repository. If names are given, only
2661 2663 files that match are shown. Files that are clean or ignored or
2662 2664 source of a copy/move operation, are not listed unless -c (clean),
2663 2665 -i (ignored), -C (copies) or -A is given. Unless options described
2664 2666 with "show only ..." are given, the options -mardu are used.
2665 2667
2666 2668 Option -q/--quiet hides untracked (unknown and ignored) files
2667 2669 unless explicitly requested with -u/--unknown or -i/-ignored.
2668 2670
2669 2671 NOTE: status may appear to disagree with diff if permissions have
2670 2672 changed or a merge has occurred. The standard diff format does not
2671 2673 report permission changes and diff only reports changes relative
2672 2674 to one merge parent.
2673 2675
2674 2676 If one revision is given, it is used as the base revision.
2675 2677 If two revisions are given, the difference between them is shown.
2676 2678
2677 2679 The codes used to show the status of files are:
2678 2680 M = modified
2679 2681 A = added
2680 2682 R = removed
2681 2683 C = clean
2682 2684 ! = deleted, but still tracked
2683 2685 ? = not tracked
2684 2686 I = ignored
2685 2687 = the previous added file was copied from here
2686 2688 """
2687 2689
2688 2690 node1, node2 = cmdutil.revpair(repo, opts.get('rev'))
2689 2691 cwd = (pats and repo.getcwd()) or ''
2690 2692 end = opts.get('print0') and '\0' or '\n'
2691 2693 copy = {}
2692 2694 states = 'modified added removed deleted unknown ignored clean'.split()
2693 2695 show = [k for k in states if opts[k]]
2694 2696 if opts.get('all'):
2695 2697 show += ui.quiet and (states[:4] + ['clean']) or states
2696 2698 if not show:
2697 2699 show = ui.quiet and states[:4] or states[:5]
2698 2700
2699 2701 stat = repo.status(node1, node2, cmdutil.match(repo, pats, opts),
2700 2702 'ignored' in show, 'clean' in show, 'unknown' in show)
2701 2703 changestates = zip(states, 'MAR!?IC', stat)
2702 2704
2703 2705 if (opts.get('all') or opts.get('copies')) and not opts.get('no_status'):
2704 2706 ctxn = repo[nullid]
2705 2707 ctx1 = repo[node1]
2706 2708 ctx2 = repo[node2]
2707 2709 added = stat[1]
2708 2710 if node2 is None:
2709 2711 added = stat[0] + stat[1] # merged?
2710 2712
2711 2713 for k, v in copies.copies(repo, ctx1, ctx2, ctxn)[0].items():
2712 2714 if k in added:
2713 2715 copy[k] = v
2714 2716 elif v in added:
2715 2717 copy[v] = k
2716 2718
2717 2719 for state, char, files in changestates:
2718 2720 if state in show:
2719 2721 format = "%s %%s%s" % (char, end)
2720 2722 if opts.get('no_status'):
2721 2723 format = "%%s%s" % end
2722 2724
2723 2725 for f in files:
2724 2726 ui.write(format % repo.pathto(f, cwd))
2725 2727 if f in copy:
2726 2728 ui.write(' %s%s' % (repo.pathto(copy[f], cwd), end))
2727 2729
2728 2730 def tag(ui, repo, name1, *names, **opts):
2729 2731 """add one or more tags for the current or given revision
2730 2732
2731 2733 Name a particular revision using <name>.
2732 2734
2733 2735 Tags are used to name particular revisions of the repository and are
2734 2736 very useful to compare different revisions, to go back to significant
2735 2737 earlier versions or to mark branch points as releases, etc.
2736 2738
2737 2739 If no revision is given, the parent of the working directory is used,
2738 2740 or tip if no revision is checked out.
2739 2741
2740 2742 To facilitate version control, distribution, and merging of tags,
2741 2743 they are stored as a file named ".hgtags" which is managed
2742 2744 similarly to other project files and can be hand-edited if
2743 2745 necessary. The file '.hg/localtags' is used for local tags (not
2744 2746 shared among repositories).
2745 2747
2746 2748 See 'hg help dates' for a list of formats valid for -d/--date.
2747 2749 """
2748 2750
2749 2751 rev_ = "."
2750 2752 names = (name1,) + names
2751 2753 if len(names) != len(dict.fromkeys(names)):
2752 2754 raise util.Abort(_('tag names must be unique'))
2753 2755 for n in names:
2754 2756 if n in ['tip', '.', 'null']:
2755 2757 raise util.Abort(_('the name \'%s\' is reserved') % n)
2756 2758 if opts.get('rev') and opts.get('remove'):
2757 2759 raise util.Abort(_("--rev and --remove are incompatible"))
2758 2760 if opts.get('rev'):
2759 2761 rev_ = opts['rev']
2760 2762 message = opts.get('message')
2761 2763 if opts.get('remove'):
2762 2764 expectedtype = opts.get('local') and 'local' or 'global'
2763 2765 for n in names:
2764 2766 if not repo.tagtype(n):
2765 2767 raise util.Abort(_('tag \'%s\' does not exist') % n)
2766 2768 if repo.tagtype(n) != expectedtype:
2767 2769 raise util.Abort(_('tag \'%s\' is not a %s tag') %
2768 2770 (n, expectedtype))
2769 2771 rev_ = nullid
2770 2772 if not message:
2771 2773 message = _('Removed tag %s') % ', '.join(names)
2772 2774 elif not opts.get('force'):
2773 2775 for n in names:
2774 2776 if n in repo.tags():
2775 2777 raise util.Abort(_('tag \'%s\' already exists '
2776 2778 '(use -f to force)') % n)
2777 2779 if not rev_ and repo.dirstate.parents()[1] != nullid:
2778 2780 raise util.Abort(_('uncommitted merge - please provide a '
2779 2781 'specific revision'))
2780 2782 r = repo[rev_].node()
2781 2783
2782 2784 if not message:
2783 2785 message = (_('Added tag %s for changeset %s') %
2784 2786 (', '.join(names), short(r)))
2785 2787
2786 2788 date = opts.get('date')
2787 2789 if date:
2788 2790 date = util.parsedate(date)
2789 2791
2790 2792 repo.tag(names, r, message, opts.get('local'), opts.get('user'), date)
2791 2793
2792 2794 def tags(ui, repo):
2793 2795 """list repository tags
2794 2796
2795 2797 List the repository tags.
2796 2798
2797 2799 This lists both regular and local tags. When the -v/--verbose switch
2798 2800 is used, a third column "local" is printed for local tags.
2799 2801 """
2800 2802
2801 2803 l = repo.tagslist()
2802 2804 l.reverse()
2803 2805 hexfunc = ui.debugflag and hex or short
2804 2806 tagtype = ""
2805 2807
2806 2808 for t, n in l:
2807 2809 if ui.quiet:
2808 2810 ui.write("%s\n" % t)
2809 2811 continue
2810 2812
2811 2813 try:
2812 2814 hn = hexfunc(n)
2813 2815 r = "%5d:%s" % (repo.changelog.rev(n), hn)
2814 2816 except revlog.LookupError:
2815 2817 r = " ?:%s" % hn
2816 2818 else:
2817 2819 spaces = " " * (30 - util.locallen(t))
2818 2820 if ui.verbose:
2819 2821 if repo.tagtype(t) == 'local':
2820 2822 tagtype = " local"
2821 2823 else:
2822 2824 tagtype = ""
2823 2825 ui.write("%s%s %s%s\n" % (t, spaces, r, tagtype))
2824 2826
2825 2827 def tip(ui, repo, **opts):
2826 2828 """show the tip revision
2827 2829
2828 2830 The tip revision (usually just called the tip) is the most
2829 2831 recently added changeset in the repository, the most recently
2830 2832 changed head.
2831 2833
2832 2834 If you have just made a commit, that commit will be the tip. If
2833 2835 you have just pulled changes from another repository, the tip of
2834 2836 that repository becomes the current tip. The "tip" tag is special
2835 2837 and cannot be renamed or assigned to a different changeset.
2836 2838 """
2837 2839 cmdutil.show_changeset(ui, repo, opts).show(len(repo) - 1)
2838 2840
2839 2841 def unbundle(ui, repo, fname1, *fnames, **opts):
2840 2842 """apply one or more changegroup files
2841 2843
2842 2844 Apply one or more compressed changegroup files generated by the
2843 2845 bundle command.
2844 2846 """
2845 2847 fnames = (fname1,) + fnames
2846 2848
2847 2849 lock = None
2848 2850 try:
2849 2851 lock = repo.lock()
2850 2852 for fname in fnames:
2851 2853 f = url.open(ui, fname)
2852 2854 gen = changegroup.readbundle(f, fname)
2853 2855 modheads = repo.addchangegroup(gen, 'unbundle', 'bundle:' + fname)
2854 2856 finally:
2855 2857 del lock
2856 2858
2857 2859 return postincoming(ui, repo, modheads, opts.get('update'), None)
2858 2860
2859 2861 def update(ui, repo, node=None, rev=None, clean=False, date=None):
2860 2862 """update working directory
2861 2863
2862 2864 Update the repository's working directory to the specified revision,
2863 2865 or the tip of the current branch if none is specified. Use null as
2864 2866 the revision to remove the working copy (like 'hg clone -U').
2865 2867
2866 2868 When the working dir contains no uncommitted changes, it will be
2867 2869 replaced by the state of the requested revision from the repo. When
2868 2870 the requested revision is on a different branch, the working dir
2869 2871 will additionally be switched to that branch.
2870 2872
2871 2873 When there are uncommitted changes, use option -C to discard them,
2872 2874 forcibly replacing the state of the working dir with the requested
2873 2875 revision.
2874 2876
2875 2877 When there are uncommitted changes and option -C is not used, and
2876 2878 the parent revision and requested revision are on the same branch,
2877 2879 and one of them is an ancestor of the other, then the new working
2878 2880 directory will contain the requested revision merged with the
2879 2881 uncommitted changes. Otherwise, the update will fail with a
2880 2882 suggestion to use 'merge' or 'update -C' instead.
2881 2883
2882 2884 If you want to update just one file to an older revision, use revert.
2883 2885
2884 2886 See 'hg help dates' for a list of formats valid for --date.
2885 2887 """
2886 2888 if rev and node:
2887 2889 raise util.Abort(_("please specify just one revision"))
2888 2890
2889 2891 if not rev:
2890 2892 rev = node
2891 2893
2892 2894 if date:
2893 2895 if rev:
2894 2896 raise util.Abort(_("you can't specify a revision and a date"))
2895 2897 rev = cmdutil.finddate(ui, repo, date)
2896 2898
2897 2899 if clean:
2898 2900 return hg.clean(repo, rev)
2899 2901 else:
2900 2902 return hg.update(repo, rev)
2901 2903
2902 2904 def verify(ui, repo):
2903 2905 """verify the integrity of the repository
2904 2906
2905 2907 Verify the integrity of the current repository.
2906 2908
2907 2909 This will perform an extensive check of the repository's
2908 2910 integrity, validating the hashes and checksums of each entry in
2909 2911 the changelog, manifest, and tracked files, as well as the
2910 2912 integrity of their crosslinks and indices.
2911 2913 """
2912 2914 return hg.verify(repo)
2913 2915
2914 2916 def version_(ui):
2915 2917 """output version and copyright information"""
2916 2918 ui.write(_("Mercurial Distributed SCM (version %s)\n")
2917 2919 % version.get_version())
2918 2920 ui.status(_(
2919 2921 "\nCopyright (C) 2005-2008 Matt Mackall <mpm@selenic.com> and others\n"
2920 2922 "This is free software; see the source for copying conditions. "
2921 2923 "There is NO\nwarranty; "
2922 2924 "not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"
2923 2925 ))
2924 2926
2925 2927 # Command options and aliases are listed here, alphabetically
2926 2928
2927 2929 globalopts = [
2928 2930 ('R', 'repository', '',
2929 2931 _('repository root directory or symbolic path name')),
2930 2932 ('', 'cwd', '', _('change working directory')),
2931 2933 ('y', 'noninteractive', None,
2932 2934 _('do not prompt, assume \'yes\' for any required answers')),
2933 2935 ('q', 'quiet', None, _('suppress output')),
2934 2936 ('v', 'verbose', None, _('enable additional output')),
2935 2937 ('', 'config', [], _('set/override config option')),
2936 2938 ('', 'debug', None, _('enable debugging output')),
2937 2939 ('', 'debugger', None, _('start debugger')),
2938 2940 ('', 'encoding', util._encoding, _('set the charset encoding')),
2939 2941 ('', 'encodingmode', util._encodingmode, _('set the charset encoding mode')),
2940 2942 ('', 'lsprof', None, _('print improved command execution profile')),
2941 2943 ('', 'traceback', None, _('print traceback on exception')),
2942 2944 ('', 'time', None, _('time how long the command takes')),
2943 2945 ('', 'profile', None, _('print command execution profile')),
2944 2946 ('', 'version', None, _('output version information and exit')),
2945 2947 ('h', 'help', None, _('display help and exit')),
2946 2948 ]
2947 2949
2948 2950 dryrunopts = [('n', 'dry-run', None,
2949 2951 _('do not perform actions, just print output'))]
2950 2952
2951 2953 remoteopts = [
2952 2954 ('e', 'ssh', '', _('specify ssh command to use')),
2953 2955 ('', 'remotecmd', '', _('specify hg command to run on the remote side')),
2954 2956 ]
2955 2957
2956 2958 walkopts = [
2957 2959 ('I', 'include', [], _('include names matching the given patterns')),
2958 2960 ('X', 'exclude', [], _('exclude names matching the given patterns')),
2959 2961 ]
2960 2962
2961 2963 commitopts = [
2962 2964 ('m', 'message', '', _('use <text> as commit message')),
2963 2965 ('l', 'logfile', '', _('read commit message from <file>')),
2964 2966 ]
2965 2967
2966 2968 commitopts2 = [
2967 2969 ('d', 'date', '', _('record datecode as commit date')),
2968 2970 ('u', 'user', '', _('record user as committer')),
2969 2971 ]
2970 2972
2971 2973 templateopts = [
2972 2974 ('', 'style', '', _('display using template map file')),
2973 2975 ('', 'template', '', _('display with template')),
2974 2976 ]
2975 2977
2976 2978 logopts = [
2977 2979 ('p', 'patch', None, _('show patch')),
2978 2980 ('l', 'limit', '', _('limit number of changes displayed')),
2979 2981 ('M', 'no-merges', None, _('do not show merges')),
2980 2982 ] + templateopts
2981 2983
2982 2984 diffopts = [
2983 2985 ('a', 'text', None, _('treat all files as text')),
2984 2986 ('g', 'git', None, _('use git extended diff format')),
2985 2987 ('', 'nodates', None, _("don't include dates in diff headers"))
2986 2988 ]
2987 2989
2988 2990 diffopts2 = [
2989 2991 ('p', 'show-function', None, _('show which function each change is in')),
2990 2992 ('w', 'ignore-all-space', None,
2991 2993 _('ignore white space when comparing lines')),
2992 2994 ('b', 'ignore-space-change', None,
2993 2995 _('ignore changes in the amount of white space')),
2994 2996 ('B', 'ignore-blank-lines', None,
2995 2997 _('ignore changes whose lines are all blank')),
2996 2998 ('U', 'unified', '', _('number of lines of context to show'))
2997 2999 ]
2998 3000
2999 3001 table = {
3000 3002 "^add": (add, walkopts + dryrunopts, _('hg add [OPTION]... [FILE]...')),
3001 3003 "addremove":
3002 3004 (addremove,
3003 3005 [('s', 'similarity', '',
3004 3006 _('guess renamed files by similarity (0<=s<=100)')),
3005 3007 ] + walkopts + dryrunopts,
3006 3008 _('hg addremove [OPTION]... [FILE]...')),
3007 3009 "^annotate|blame":
3008 3010 (annotate,
3009 3011 [('r', 'rev', '', _('annotate the specified revision')),
3010 3012 ('f', 'follow', None, _('follow file copies and renames')),
3011 3013 ('a', 'text', None, _('treat all files as text')),
3012 3014 ('u', 'user', None, _('list the author (long with -v)')),
3013 3015 ('d', 'date', None, _('list the date (short with -q)')),
3014 3016 ('n', 'number', None, _('list the revision number (default)')),
3015 3017 ('c', 'changeset', None, _('list the changeset')),
3016 3018 ('l', 'line-number', None,
3017 3019 _('show line number at the first appearance'))
3018 3020 ] + walkopts,
3019 3021 _('hg annotate [-r REV] [-f] [-a] [-u] [-d] [-n] [-c] [-l] FILE...')),
3020 3022 "archive":
3021 3023 (archive,
3022 3024 [('', 'no-decode', None, _('do not pass files through decoders')),
3023 3025 ('p', 'prefix', '', _('directory prefix for files in archive')),
3024 3026 ('r', 'rev', '', _('revision to distribute')),
3025 3027 ('t', 'type', '', _('type of distribution to create')),
3026 3028 ] + walkopts,
3027 3029 _('hg archive [OPTION]... DEST')),
3028 3030 "backout":
3029 3031 (backout,
3030 3032 [('', 'merge', None,
3031 3033 _('merge with old dirstate parent after backout')),
3032 3034 ('', 'parent', '', _('parent to choose when backing out merge')),
3033 3035 ('r', 'rev', '', _('revision to backout')),
3034 3036 ] + walkopts + commitopts + commitopts2,
3035 3037 _('hg backout [OPTION]... [-r] REV')),
3036 3038 "bisect":
3037 3039 (bisect,
3038 3040 [('r', 'reset', False, _('reset bisect state')),
3039 3041 ('g', 'good', False, _('mark changeset good')),
3040 3042 ('b', 'bad', False, _('mark changeset bad')),
3041 3043 ('s', 'skip', False, _('skip testing changeset')),
3042 3044 ('c', 'command', '', _('Use command to check changeset state')),
3043 3045 ('U', 'noupdate', False, _('do not update to target'))],
3044 3046 _("hg bisect [-gbsr] [-c CMD] [REV]")),
3045 3047 "branch":
3046 3048 (branch,
3047 3049 [('f', 'force', None,
3048 3050 _('set branch name even if it shadows an existing branch')),
3049 3051 ('C', 'clean', None, _('reset branch name to parent branch name'))],
3050 3052 _('hg branch [-fC] [NAME]')),
3051 3053 "branches":
3052 3054 (branches,
3053 3055 [('a', 'active', False,
3054 3056 _('show only branches that have unmerged heads'))],
3055 3057 _('hg branches [-a]')),
3056 3058 "bundle":
3057 3059 (bundle,
3058 3060 [('f', 'force', None,
3059 3061 _('run even when remote repository is unrelated')),
3060 3062 ('r', 'rev', [],
3061 3063 _('a changeset up to which you would like to bundle')),
3062 3064 ('', 'base', [],
3063 3065 _('a base changeset to specify instead of a destination')),
3064 3066 ('a', 'all', None, _('bundle all changesets in the repository')),
3065 3067 ('t', 'type', 'bzip2', _('bundle compression type to use')),
3066 3068 ] + remoteopts,
3067 3069 _('hg bundle [-f] [-a] [-r REV]... [--base REV]... FILE [DEST]')),
3068 3070 "cat":
3069 3071 (cat,
3070 3072 [('o', 'output', '', _('print output to file with formatted name')),
3071 3073 ('r', 'rev', '', _('print the given revision')),
3072 3074 ('', 'decode', None, _('apply any matching decode filter')),
3073 3075 ] + walkopts,
3074 3076 _('hg cat [OPTION]... FILE...')),
3075 3077 "^clone":
3076 3078 (clone,
3077 3079 [('U', 'noupdate', None,
3078 3080 _('the clone will only contain a repository (no working copy)')),
3079 3081 ('r', 'rev', [],
3080 3082 _('a changeset you would like to have after cloning')),
3081 3083 ('', 'pull', None, _('use pull protocol to copy metadata')),
3082 3084 ('', 'uncompressed', None,
3083 3085 _('use uncompressed transfer (fast over LAN)')),
3084 3086 ] + remoteopts,
3085 3087 _('hg clone [OPTION]... SOURCE [DEST]')),
3086 3088 "^commit|ci":
3087 3089 (commit,
3088 3090 [('A', 'addremove', None,
3089 3091 _('mark new/missing files as added/removed before committing')),
3090 3092 ] + walkopts + commitopts + commitopts2,
3091 3093 _('hg commit [OPTION]... [FILE]...')),
3092 3094 "copy|cp":
3093 3095 (copy,
3094 3096 [('A', 'after', None, _('record a copy that has already occurred')),
3095 3097 ('f', 'force', None,
3096 3098 _('forcibly copy over an existing managed file')),
3097 3099 ] + walkopts + dryrunopts,
3098 3100 _('hg copy [OPTION]... [SOURCE]... DEST')),
3099 3101 "debugancestor": (debugancestor, [],
3100 3102 _('hg debugancestor [INDEX] REV1 REV2')),
3101 3103 "debugcheckstate": (debugcheckstate, [], _('hg debugcheckstate')),
3102 3104 "debugcomplete":
3103 3105 (debugcomplete,
3104 3106 [('o', 'options', None, _('show the command options'))],
3105 3107 _('hg debugcomplete [-o] CMD')),
3106 3108 "debugdate":
3107 3109 (debugdate,
3108 3110 [('e', 'extended', None, _('try extended date formats'))],
3109 3111 _('hg debugdate [-e] DATE [RANGE]')),
3110 3112 "debugdata": (debugdata, [], _('hg debugdata FILE REV')),
3111 3113 "debugfsinfo": (debugfsinfo, [], _('hg debugfsinfo [PATH]')),
3112 3114 "debugindex": (debugindex, [], _('hg debugindex FILE')),
3113 3115 "debugindexdot": (debugindexdot, [], _('hg debugindexdot FILE')),
3114 3116 "debuginstall": (debuginstall, [], _('hg debuginstall')),
3115 3117 "debugrawcommit|rawcommit":
3116 3118 (rawcommit,
3117 3119 [('p', 'parent', [], _('parent')),
3118 3120 ('F', 'files', '', _('file list'))
3119 3121 ] + commitopts + commitopts2,
3120 3122 _('hg debugrawcommit [OPTION]... [FILE]...')),
3121 3123 "debugrebuildstate":
3122 3124 (debugrebuildstate,
3123 3125 [('r', 'rev', '', _('revision to rebuild to'))],
3124 3126 _('hg debugrebuildstate [-r REV] [REV]')),
3125 3127 "debugrename":
3126 3128 (debugrename,
3127 3129 [('r', 'rev', '', _('revision to debug'))],
3128 3130 _('hg debugrename [-r REV] FILE')),
3129 3131 "debugsetparents":
3130 3132 (debugsetparents,
3131 3133 [],
3132 3134 _('hg debugsetparents REV1 [REV2]')),
3133 3135 "debugstate":
3134 3136 (debugstate,
3135 3137 [('', 'nodates', None, _('do not display the saved mtime'))],
3136 3138 _('hg debugstate [OPTION]...')),
3137 3139 "debugwalk": (debugwalk, walkopts, _('hg debugwalk [OPTION]... [FILE]...')),
3138 3140 "^diff":
3139 3141 (diff,
3140 3142 [('r', 'rev', [], _('revision'))
3141 3143 ] + diffopts + diffopts2 + walkopts,
3142 3144 _('hg diff [OPTION]... [-r REV1 [-r REV2]] [FILE]...')),
3143 3145 "^export":
3144 3146 (export,
3145 3147 [('o', 'output', '', _('print output to file with formatted name')),
3146 3148 ('', 'switch-parent', None, _('diff against the second parent'))
3147 3149 ] + diffopts,
3148 3150 _('hg export [OPTION]... [-o OUTFILESPEC] REV...')),
3149 3151 "grep":
3150 3152 (grep,
3151 3153 [('0', 'print0', None, _('end fields with NUL')),
3152 3154 ('', 'all', None, _('print all revisions that match')),
3153 3155 ('f', 'follow', None,
3154 3156 _('follow changeset history, or file history across copies and renames')),
3155 3157 ('i', 'ignore-case', None, _('ignore case when matching')),
3156 3158 ('l', 'files-with-matches', None,
3157 3159 _('print only filenames and revs that match')),
3158 3160 ('n', 'line-number', None, _('print matching line numbers')),
3159 3161 ('r', 'rev', [], _('search in given revision range')),
3160 3162 ('u', 'user', None, _('list the author (long with -v)')),
3161 3163 ('d', 'date', None, _('list the date (short with -q)')),
3162 3164 ] + walkopts,
3163 3165 _('hg grep [OPTION]... PATTERN [FILE]...')),
3164 3166 "heads":
3165 3167 (heads,
3166 3168 [('r', 'rev', '', _('show only heads which are descendants of rev')),
3167 3169 ] + templateopts,
3168 3170 _('hg heads [-r REV] [REV]...')),
3169 3171 "help": (help_, [], _('hg help [TOPIC]')),
3170 3172 "identify|id":
3171 3173 (identify,
3172 3174 [('r', 'rev', '', _('identify the specified rev')),
3173 3175 ('n', 'num', None, _('show local revision number')),
3174 3176 ('i', 'id', None, _('show global revision id')),
3175 3177 ('b', 'branch', None, _('show branch')),
3176 3178 ('t', 'tags', None, _('show tags'))],
3177 3179 _('hg identify [-nibt] [-r REV] [SOURCE]')),
3178 3180 "import|patch":
3179 3181 (import_,
3180 3182 [('p', 'strip', 1,
3181 3183 _('directory strip option for patch. This has the same\n'
3182 3184 'meaning as the corresponding patch option')),
3183 3185 ('b', 'base', '', _('base path')),
3184 3186 ('f', 'force', None,
3185 3187 _('skip check for outstanding uncommitted changes')),
3186 3188 ('', 'no-commit', None, _("don't commit, just update the working directory")),
3187 3189 ('', 'exact', None,
3188 3190 _('apply patch to the nodes from which it was generated')),
3189 3191 ('', 'import-branch', None,
3190 3192 _('Use any branch information in patch (implied by --exact)'))] +
3191 3193 commitopts + commitopts2,
3192 3194 _('hg import [OPTION]... PATCH...')),
3193 3195 "incoming|in":
3194 3196 (incoming,
3195 3197 [('f', 'force', None,
3196 3198 _('run even when remote repository is unrelated')),
3197 3199 ('n', 'newest-first', None, _('show newest record first')),
3198 3200 ('', 'bundle', '', _('file to store the bundles into')),
3199 3201 ('r', 'rev', [],
3200 3202 _('a specific revision up to which you would like to pull')),
3201 3203 ] + logopts + remoteopts,
3202 3204 _('hg incoming [-p] [-n] [-M] [-f] [-r REV]...'
3203 3205 ' [--bundle FILENAME] [SOURCE]')),
3204 3206 "^init":
3205 3207 (init,
3206 3208 remoteopts,
3207 3209 _('hg init [-e CMD] [--remotecmd CMD] [DEST]')),
3208 3210 "locate":
3209 3211 (locate,
3210 3212 [('r', 'rev', '', _('search the repository as it stood at rev')),
3211 3213 ('0', 'print0', None,
3212 3214 _('end filenames with NUL, for use with xargs')),
3213 3215 ('f', 'fullpath', None,
3214 3216 _('print complete paths from the filesystem root')),
3215 3217 ] + walkopts,
3216 3218 _('hg locate [OPTION]... [PATTERN]...')),
3217 3219 "^log|history":
3218 3220 (log,
3219 3221 [('f', 'follow', None,
3220 3222 _('follow changeset history, or file history across copies and renames')),
3221 3223 ('', 'follow-first', None,
3222 3224 _('only follow the first parent of merge changesets')),
3223 3225 ('d', 'date', '', _('show revs matching date spec')),
3224 3226 ('C', 'copies', None, _('show copied files')),
3225 3227 ('k', 'keyword', [], _('do case-insensitive search for a keyword')),
3226 3228 ('r', 'rev', [], _('show the specified revision or range')),
3227 3229 ('', 'removed', None, _('include revs where files were removed')),
3228 3230 ('m', 'only-merges', None, _('show only merges')),
3229 3231 ('u', 'user', [], _('revs committed by user')),
3230 3232 ('b', 'only-branch', [],
3231 3233 _('show only changesets within the given named branch')),
3232 3234 ('P', 'prune', [], _('do not display revision or any of its ancestors')),
3233 3235 ] + logopts + walkopts,
3234 3236 _('hg log [OPTION]... [FILE]')),
3235 3237 "manifest":
3236 3238 (manifest,
3237 3239 [('r', 'rev', '', _('revision to display'))],
3238 3240 _('hg manifest [-r REV]')),
3239 3241 "^merge":
3240 3242 (merge,
3241 3243 [('f', 'force', None, _('force a merge with outstanding changes')),
3242 3244 ('r', 'rev', '', _('revision to merge')),
3243 3245 ],
3244 3246 _('hg merge [-f] [[-r] REV]')),
3245 3247 "outgoing|out":
3246 3248 (outgoing,
3247 3249 [('f', 'force', None,
3248 3250 _('run even when remote repository is unrelated')),
3249 3251 ('r', 'rev', [],
3250 3252 _('a specific revision up to which you would like to push')),
3251 3253 ('n', 'newest-first', None, _('show newest record first')),
3252 3254 ] + logopts + remoteopts,
3253 3255 _('hg outgoing [-M] [-p] [-n] [-f] [-r REV]... [DEST]')),
3254 3256 "^parents":
3255 3257 (parents,
3256 3258 [('r', 'rev', '', _('show parents from the specified rev')),
3257 3259 ] + templateopts,
3258 3260 _('hg parents [-r REV] [FILE]')),
3259 3261 "paths": (paths, [], _('hg paths [NAME]')),
3260 3262 "^pull":
3261 3263 (pull,
3262 3264 [('u', 'update', None,
3263 3265 _('update to new tip if changesets were pulled')),
3264 3266 ('f', 'force', None,
3265 3267 _('run even when remote repository is unrelated')),
3266 3268 ('r', 'rev', [],
3267 3269 _('a specific revision up to which you would like to pull')),
3268 3270 ] + remoteopts,
3269 3271 _('hg pull [-u] [-f] [-r REV]... [-e CMD] [--remotecmd CMD] [SOURCE]')),
3270 3272 "^push":
3271 3273 (push,
3272 3274 [('f', 'force', None, _('force push')),
3273 3275 ('r', 'rev', [],
3274 3276 _('a specific revision up to which you would like to push')),
3275 3277 ] + remoteopts,
3276 3278 _('hg push [-f] [-r REV]... [-e CMD] [--remotecmd CMD] [DEST]')),
3277 3279 "recover": (recover, [], _('hg recover')),
3278 3280 "^remove|rm":
3279 3281 (remove,
3280 3282 [('A', 'after', None, _('record delete for missing files')),
3281 3283 ('f', 'force', None,
3282 3284 _('remove (and delete) file even if added or modified')),
3283 3285 ] + walkopts,
3284 3286 _('hg remove [OPTION]... FILE...')),
3285 3287 "rename|mv":
3286 3288 (rename,
3287 3289 [('A', 'after', None, _('record a rename that has already occurred')),
3288 3290 ('f', 'force', None,
3289 3291 _('forcibly copy over an existing managed file')),
3290 3292 ] + walkopts + dryrunopts,
3291 3293 _('hg rename [OPTION]... SOURCE... DEST')),
3292 3294 "resolve":
3293 3295 (resolve,
3294 3296 [('l', 'list', None, _('list state of files needing merge')),
3295 3297 ('m', 'mark', None, _('mark files as resolved')),
3296 3298 ('u', 'unmark', None, _('unmark files as resolved'))],
3297 3299 _('hg resolve [OPTION]... [FILE]...')),
3298 3300 "revert":
3299 3301 (revert,
3300 3302 [('a', 'all', None, _('revert all changes when no arguments given')),
3301 3303 ('d', 'date', '', _('tipmost revision matching date')),
3302 3304 ('r', 'rev', '', _('revision to revert to')),
3303 3305 ('', 'no-backup', None, _('do not save backup copies of files')),
3304 3306 ] + walkopts + dryrunopts,
3305 3307 _('hg revert [OPTION]... [-r REV] [NAME]...')),
3306 3308 "rollback": (rollback, [], _('hg rollback')),
3307 3309 "root": (root, [], _('hg root')),
3308 3310 "^serve":
3309 3311 (serve,
3310 3312 [('A', 'accesslog', '', _('name of access log file to write to')),
3311 3313 ('d', 'daemon', None, _('run server in background')),
3312 3314 ('', 'daemon-pipefds', '', _('used internally by daemon mode')),
3313 3315 ('E', 'errorlog', '', _('name of error log file to write to')),
3314 3316 ('p', 'port', 0, _('port to listen on (default: 8000)')),
3315 3317 ('a', 'address', '', _('address to listen on (default: all interfaces)')),
3316 3318 ('', 'prefix', '', _('prefix path to serve from (default: server root)')),
3317 3319 ('n', 'name', '',
3318 3320 _('name to show in web pages (default: working dir)')),
3319 3321 ('', 'webdir-conf', '', _('name of the webdir config file'
3320 3322 ' (serve more than one repo)')),
3321 3323 ('', 'pid-file', '', _('name of file to write process ID to')),
3322 3324 ('', 'stdio', None, _('for remote clients')),
3323 3325 ('t', 'templates', '', _('web templates to use')),
3324 3326 ('', 'style', '', _('template style to use')),
3325 3327 ('6', 'ipv6', None, _('use IPv6 in addition to IPv4')),
3326 3328 ('', 'certificate', '', _('SSL certificate file'))],
3327 3329 _('hg serve [OPTION]...')),
3328 3330 "showconfig|debugconfig":
3329 3331 (showconfig,
3330 3332 [('u', 'untrusted', None, _('show untrusted configuration options'))],
3331 3333 _('hg showconfig [-u] [NAME]...')),
3332 3334 "^status|st":
3333 3335 (status,
3334 3336 [('A', 'all', None, _('show status of all files')),
3335 3337 ('m', 'modified', None, _('show only modified files')),
3336 3338 ('a', 'added', None, _('show only added files')),
3337 3339 ('r', 'removed', None, _('show only removed files')),
3338 3340 ('d', 'deleted', None, _('show only deleted (but tracked) files')),
3339 3341 ('c', 'clean', None, _('show only files without changes')),
3340 3342 ('u', 'unknown', None, _('show only unknown (not tracked) files')),
3341 3343 ('i', 'ignored', None, _('show only ignored files')),
3342 3344 ('n', 'no-status', None, _('hide status prefix')),
3343 3345 ('C', 'copies', None, _('show source of copied files')),
3344 3346 ('0', 'print0', None,
3345 3347 _('end filenames with NUL, for use with xargs')),
3346 3348 ('', 'rev', [], _('show difference from revision')),
3347 3349 ] + walkopts,
3348 3350 _('hg status [OPTION]... [FILE]...')),
3349 3351 "tag":
3350 3352 (tag,
3351 3353 [('f', 'force', None, _('replace existing tag')),
3352 3354 ('l', 'local', None, _('make the tag local')),
3353 3355 ('r', 'rev', '', _('revision to tag')),
3354 3356 ('', 'remove', None, _('remove a tag')),
3355 3357 # -l/--local is already there, commitopts cannot be used
3356 3358 ('m', 'message', '', _('use <text> as commit message')),
3357 3359 ] + commitopts2,
3358 3360 _('hg tag [-l] [-m TEXT] [-d DATE] [-u USER] [-r REV] NAME...')),
3359 3361 "tags": (tags, [], _('hg tags')),
3360 3362 "tip":
3361 3363 (tip,
3362 3364 [('p', 'patch', None, _('show patch')),
3363 3365 ] + templateopts,
3364 3366 _('hg tip [-p]')),
3365 3367 "unbundle":
3366 3368 (unbundle,
3367 3369 [('u', 'update', None,
3368 3370 _('update to new tip if changesets were unbundled'))],
3369 3371 _('hg unbundle [-u] FILE...')),
3370 3372 "^update|up|checkout|co":
3371 3373 (update,
3372 3374 [('C', 'clean', None, _('overwrite locally modified files (no backup)')),
3373 3375 ('d', 'date', '', _('tipmost revision matching date')),
3374 3376 ('r', 'rev', '', _('revision'))],
3375 3377 _('hg update [-C] [-d DATE] [[-r] REV]')),
3376 3378 "verify": (verify, [], _('hg verify')),
3377 3379 "version": (version_, [], _('hg version')),
3378 3380 }
3379 3381
3380 3382 norepo = ("clone init version help debugcomplete debugdata"
3381 3383 " debugindex debugindexdot debugdate debuginstall debugfsinfo")
3382 3384 optionalrepo = ("identify paths serve showconfig debugancestor")
@@ -1,1331 +1,1330
1 1 # patch.py - patch file parsing routines
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 from i18n import _
10 10 from node import hex, nullid, short
11 11 import base85, cmdutil, mdiff, util, revlog, diffhelpers, copies
12 12 import cStringIO, email.Parser, os, re, errno
13 13 import sys, tempfile, zlib
14 14
15 15 gitre = re.compile('diff --git a/(.*) b/(.*)')
16 16
17 17 class PatchError(Exception):
18 18 pass
19 19
20 20 class NoHunks(PatchError):
21 21 pass
22 22
23 23 # helper functions
24 24
25 25 def copyfile(src, dst, basedir=None):
26 26 if not basedir:
27 27 basedir = os.getcwd()
28 28
29 29 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
30 30 if os.path.exists(absdst):
31 31 raise util.Abort(_("cannot create %s: destination already exists") %
32 32 dst)
33 33
34 34 if not os.path.isdir(basedir):
35 35 os.makedirs(basedir)
36 36
37 37 util.copyfile(abssrc, absdst)
38 38
39 39 # public functions
40 40
41 41 def extract(ui, fileobj):
42 42 '''extract patch from data read from fileobj.
43 43
44 44 patch can be a normal patch or contained in an email message.
45 45
46 46 return tuple (filename, message, user, date, node, p1, p2).
47 47 Any item in the returned tuple can be None. If filename is None,
48 48 fileobj did not contain a patch. Caller must unlink filename when done.'''
49 49
50 50 # attempt to detect the start of a patch
51 51 # (this heuristic is borrowed from quilt)
52 52 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
53 53 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
54 54 '(---|\*\*\*)[ \t])', re.MULTILINE)
55 55
56 56 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
57 57 tmpfp = os.fdopen(fd, 'w')
58 58 try:
59 59 msg = email.Parser.Parser().parse(fileobj)
60 60
61 61 subject = msg['Subject']
62 62 user = msg['From']
63 63 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
64 64 # should try to parse msg['Date']
65 65 date = None
66 66 nodeid = None
67 67 branch = None
68 68 parents = []
69 69
70 70 if subject:
71 71 if subject.startswith('[PATCH'):
72 72 pend = subject.find(']')
73 73 if pend >= 0:
74 74 subject = subject[pend+1:].lstrip()
75 75 subject = subject.replace('\n\t', ' ')
76 76 ui.debug('Subject: %s\n' % subject)
77 77 if user:
78 78 ui.debug('From: %s\n' % user)
79 79 diffs_seen = 0
80 80 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
81 81 message = ''
82 82 for part in msg.walk():
83 83 content_type = part.get_content_type()
84 84 ui.debug('Content-Type: %s\n' % content_type)
85 85 if content_type not in ok_types:
86 86 continue
87 87 payload = part.get_payload(decode=True)
88 88 m = diffre.search(payload)
89 89 if m:
90 90 hgpatch = False
91 91 ignoretext = False
92 92
93 93 ui.debug(_('found patch at byte %d\n') % m.start(0))
94 94 diffs_seen += 1
95 95 cfp = cStringIO.StringIO()
96 96 for line in payload[:m.start(0)].splitlines():
97 97 if line.startswith('# HG changeset patch'):
98 98 ui.debug(_('patch generated by hg export\n'))
99 99 hgpatch = True
100 100 # drop earlier commit message content
101 101 cfp.seek(0)
102 102 cfp.truncate()
103 103 subject = None
104 104 elif hgpatch:
105 105 if line.startswith('# User '):
106 106 user = line[7:]
107 107 ui.debug('From: %s\n' % user)
108 108 elif line.startswith("# Date "):
109 109 date = line[7:]
110 110 elif line.startswith("# Branch "):
111 111 branch = line[9:]
112 112 elif line.startswith("# Node ID "):
113 113 nodeid = line[10:]
114 114 elif line.startswith("# Parent "):
115 115 parents.append(line[10:])
116 116 elif line == '---' and gitsendmail:
117 117 ignoretext = True
118 118 if not line.startswith('# ') and not ignoretext:
119 119 cfp.write(line)
120 120 cfp.write('\n')
121 121 message = cfp.getvalue()
122 122 if tmpfp:
123 123 tmpfp.write(payload)
124 124 if not payload.endswith('\n'):
125 125 tmpfp.write('\n')
126 126 elif not diffs_seen and message and content_type == 'text/plain':
127 127 message += '\n' + payload
128 128 except:
129 129 tmpfp.close()
130 130 os.unlink(tmpname)
131 131 raise
132 132
133 133 if subject and not message.startswith(subject):
134 134 message = '%s\n%s' % (subject, message)
135 135 tmpfp.close()
136 136 if not diffs_seen:
137 137 os.unlink(tmpname)
138 138 return None, message, user, date, branch, None, None, None
139 139 p1 = parents and parents.pop(0) or None
140 140 p2 = parents and parents.pop(0) or None
141 141 return tmpname, message, user, date, branch, nodeid, p1, p2
142 142
143 143 GP_PATCH = 1 << 0 # we have to run patch
144 144 GP_FILTER = 1 << 1 # there's some copy/rename operation
145 145 GP_BINARY = 1 << 2 # there's a binary patch
146 146
147 147 class patchmeta:
148 148 """Patched file metadata
149 149
150 150 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
151 151 or COPY. 'path' is patched file path. 'oldpath' is set to the
152 152 origin file when 'op' is either COPY or RENAME, None otherwise. If
153 153 file mode is changed, 'mode' is a tuple (islink, isexec) where
154 154 'islink' is True if the file is a symlink and 'isexec' is True if
155 155 the file is executable. Otherwise, 'mode' is None.
156 156 """
157 157 def __init__(self, path):
158 158 self.path = path
159 159 self.oldpath = None
160 160 self.mode = None
161 161 self.op = 'MODIFY'
162 162 self.lineno = 0
163 163 self.binary = False
164 164
165 165 def setmode(self, mode):
166 166 islink = mode & 020000
167 167 isexec = mode & 0100
168 168 self.mode = (islink, isexec)
169 169
170 170 def readgitpatch(lr):
171 171 """extract git-style metadata about patches from <patchname>"""
172 172
173 173 # Filter patch for git information
174 174 gp = None
175 175 gitpatches = []
176 176 # Can have a git patch with only metadata, causing patch to complain
177 177 dopatch = 0
178 178
179 179 lineno = 0
180 180 for line in lr:
181 181 lineno += 1
182 182 if line.startswith('diff --git'):
183 183 m = gitre.match(line)
184 184 if m:
185 185 if gp:
186 186 gitpatches.append(gp)
187 187 src, dst = m.group(1, 2)
188 188 gp = patchmeta(dst)
189 189 gp.lineno = lineno
190 190 elif gp:
191 191 if line.startswith('--- '):
192 192 if gp.op in ('COPY', 'RENAME'):
193 193 dopatch |= GP_FILTER
194 194 gitpatches.append(gp)
195 195 gp = None
196 196 dopatch |= GP_PATCH
197 197 continue
198 198 if line.startswith('rename from '):
199 199 gp.op = 'RENAME'
200 200 gp.oldpath = line[12:].rstrip()
201 201 elif line.startswith('rename to '):
202 202 gp.path = line[10:].rstrip()
203 203 elif line.startswith('copy from '):
204 204 gp.op = 'COPY'
205 205 gp.oldpath = line[10:].rstrip()
206 206 elif line.startswith('copy to '):
207 207 gp.path = line[8:].rstrip()
208 208 elif line.startswith('deleted file'):
209 209 gp.op = 'DELETE'
210 210 elif line.startswith('new file mode '):
211 211 gp.op = 'ADD'
212 212 gp.setmode(int(line.rstrip()[-6:], 8))
213 213 elif line.startswith('new mode '):
214 214 gp.setmode(int(line.rstrip()[-6:], 8))
215 215 elif line.startswith('GIT binary patch'):
216 216 dopatch |= GP_BINARY
217 217 gp.binary = True
218 218 if gp:
219 219 gitpatches.append(gp)
220 220
221 221 if not gitpatches:
222 222 dopatch = GP_PATCH
223 223
224 224 return (dopatch, gitpatches)
225 225
226 226 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
227 227 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
228 228 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
229 229
230 230 class patchfile:
231 231 def __init__(self, ui, fname, missing=False):
232 232 self.fname = fname
233 233 self.ui = ui
234 234 self.lines = []
235 235 self.exists = False
236 236 self.missing = missing
237 237 if not missing:
238 238 try:
239 239 fp = file(fname, 'rb')
240 240 self.lines = fp.readlines()
241 241 self.exists = True
242 242 except IOError:
243 243 pass
244 244 else:
245 245 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
246 246
247 247 if not self.exists:
248 248 dirname = os.path.dirname(fname)
249 249 if dirname and not os.path.isdir(dirname):
250 250 os.makedirs(dirname)
251 251
252 252 self.hash = {}
253 253 self.dirty = 0
254 254 self.offset = 0
255 255 self.rej = []
256 256 self.fileprinted = False
257 257 self.printfile(False)
258 258 self.hunks = 0
259 259
260 260 def printfile(self, warn):
261 261 if self.fileprinted:
262 262 return
263 263 if warn or self.ui.verbose:
264 264 self.fileprinted = True
265 265 s = _("patching file %s\n") % self.fname
266 266 if warn:
267 267 self.ui.warn(s)
268 268 else:
269 269 self.ui.note(s)
270 270
271 271
272 272 def findlines(self, l, linenum):
273 273 # looks through the hash and finds candidate lines. The
274 274 # result is a list of line numbers sorted based on distance
275 275 # from linenum
276 276 def sorter(a, b):
277 277 vala = abs(a - linenum)
278 278 valb = abs(b - linenum)
279 279 return cmp(vala, valb)
280 280
281 281 try:
282 282 cand = self.hash[l]
283 283 except:
284 284 return []
285 285
286 286 if len(cand) > 1:
287 287 # resort our list of potentials forward then back.
288 288 cand.sort(sorter)
289 289 return cand
290 290
291 291 def hashlines(self):
292 292 self.hash = {}
293 293 for x in xrange(len(self.lines)):
294 294 s = self.lines[x]
295 295 self.hash.setdefault(s, []).append(x)
296 296
297 297 def write_rej(self):
298 298 # our rejects are a little different from patch(1). This always
299 299 # creates rejects in the same form as the original patch. A file
300 300 # header is inserted so that you can run the reject through patch again
301 301 # without having to type the filename.
302 302
303 303 if not self.rej:
304 304 return
305 305
306 306 fname = self.fname + ".rej"
307 307 self.ui.warn(
308 308 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
309 309 (len(self.rej), self.hunks, fname))
310 310 try: os.unlink(fname)
311 311 except:
312 312 pass
313 313 fp = file(fname, 'wb')
314 314 base = os.path.basename(self.fname)
315 315 fp.write("--- %s\n+++ %s\n" % (base, base))
316 316 for x in self.rej:
317 317 for l in x.hunk:
318 318 fp.write(l)
319 319 if l[-1] != '\n':
320 320 fp.write("\n\ No newline at end of file\n")
321 321
322 322 def write(self, dest=None):
323 323 if self.dirty:
324 324 if not dest:
325 325 dest = self.fname
326 326 st = None
327 327 try:
328 328 st = os.lstat(dest)
329 329 except OSError, inst:
330 330 if inst.errno != errno.ENOENT:
331 331 raise
332 332 if st and st.st_nlink > 1:
333 333 os.unlink(dest)
334 334 fp = file(dest, 'wb')
335 335 if st and st.st_nlink > 1:
336 336 os.chmod(dest, st.st_mode)
337 337 fp.writelines(self.lines)
338 338 fp.close()
339 339
340 340 def close(self):
341 341 self.write()
342 342 self.write_rej()
343 343
344 344 def apply(self, h, reverse):
345 345 if not h.complete():
346 346 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
347 347 (h.number, h.desc, len(h.a), h.lena, len(h.b),
348 348 h.lenb))
349 349
350 350 self.hunks += 1
351 351 if reverse:
352 352 h.reverse()
353 353
354 354 if self.missing:
355 355 self.rej.append(h)
356 356 return -1
357 357
358 358 if self.exists and h.createfile():
359 359 self.ui.warn(_("file %s already exists\n") % self.fname)
360 360 self.rej.append(h)
361 361 return -1
362 362
363 363 if isinstance(h, binhunk):
364 364 if h.rmfile():
365 365 os.unlink(self.fname)
366 366 else:
367 367 self.lines[:] = h.new()
368 368 self.offset += len(h.new())
369 369 self.dirty = 1
370 370 return 0
371 371
372 372 # fast case first, no offsets, no fuzz
373 373 old = h.old()
374 374 # patch starts counting at 1 unless we are adding the file
375 375 if h.starta == 0:
376 376 start = 0
377 377 else:
378 378 start = h.starta + self.offset - 1
379 379 orig_start = start
380 380 if diffhelpers.testhunk(old, self.lines, start) == 0:
381 381 if h.rmfile():
382 382 os.unlink(self.fname)
383 383 else:
384 384 self.lines[start : start + h.lena] = h.new()
385 385 self.offset += h.lenb - h.lena
386 386 self.dirty = 1
387 387 return 0
388 388
389 389 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
390 390 self.hashlines()
391 391 if h.hunk[-1][0] != ' ':
392 392 # if the hunk tried to put something at the bottom of the file
393 393 # override the start line and use eof here
394 394 search_start = len(self.lines)
395 395 else:
396 396 search_start = orig_start
397 397
398 398 for fuzzlen in xrange(3):
399 399 for toponly in [ True, False ]:
400 400 old = h.old(fuzzlen, toponly)
401 401
402 402 cand = self.findlines(old[0][1:], search_start)
403 403 for l in cand:
404 404 if diffhelpers.testhunk(old, self.lines, l) == 0:
405 405 newlines = h.new(fuzzlen, toponly)
406 406 self.lines[l : l + len(old)] = newlines
407 407 self.offset += len(newlines) - len(old)
408 408 self.dirty = 1
409 409 if fuzzlen:
410 410 fuzzstr = "with fuzz %d " % fuzzlen
411 411 f = self.ui.warn
412 412 self.printfile(True)
413 413 else:
414 414 fuzzstr = ""
415 415 f = self.ui.note
416 416 offset = l - orig_start - fuzzlen
417 417 if offset == 1:
418 418 linestr = "line"
419 419 else:
420 420 linestr = "lines"
421 421 f(_("Hunk #%d succeeded at %d %s(offset %d %s).\n") %
422 422 (h.number, l+1, fuzzstr, offset, linestr))
423 423 return fuzzlen
424 424 self.printfile(True)
425 425 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
426 426 self.rej.append(h)
427 427 return -1
428 428
429 429 class hunk:
430 430 def __init__(self, desc, num, lr, context, create=False, remove=False):
431 431 self.number = num
432 432 self.desc = desc
433 433 self.hunk = [ desc ]
434 434 self.a = []
435 435 self.b = []
436 436 if context:
437 437 self.read_context_hunk(lr)
438 438 else:
439 439 self.read_unified_hunk(lr)
440 440 self.create = create
441 441 self.remove = remove and not create
442 442
443 443 def read_unified_hunk(self, lr):
444 444 m = unidesc.match(self.desc)
445 445 if not m:
446 446 raise PatchError(_("bad hunk #%d") % self.number)
447 447 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
448 448 if self.lena == None:
449 449 self.lena = 1
450 450 else:
451 451 self.lena = int(self.lena)
452 452 if self.lenb == None:
453 453 self.lenb = 1
454 454 else:
455 455 self.lenb = int(self.lenb)
456 456 self.starta = int(self.starta)
457 457 self.startb = int(self.startb)
458 458 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
459 459 # if we hit eof before finishing out the hunk, the last line will
460 460 # be zero length. Lets try to fix it up.
461 461 while len(self.hunk[-1]) == 0:
462 462 del self.hunk[-1]
463 463 del self.a[-1]
464 464 del self.b[-1]
465 465 self.lena -= 1
466 466 self.lenb -= 1
467 467
468 468 def read_context_hunk(self, lr):
469 469 self.desc = lr.readline()
470 470 m = contextdesc.match(self.desc)
471 471 if not m:
472 472 raise PatchError(_("bad hunk #%d") % self.number)
473 473 foo, self.starta, foo2, aend, foo3 = m.groups()
474 474 self.starta = int(self.starta)
475 475 if aend == None:
476 476 aend = self.starta
477 477 self.lena = int(aend) - self.starta
478 478 if self.starta:
479 479 self.lena += 1
480 480 for x in xrange(self.lena):
481 481 l = lr.readline()
482 482 if l.startswith('---'):
483 483 lr.push(l)
484 484 break
485 485 s = l[2:]
486 486 if l.startswith('- ') or l.startswith('! '):
487 487 u = '-' + s
488 488 elif l.startswith(' '):
489 489 u = ' ' + s
490 490 else:
491 491 raise PatchError(_("bad hunk #%d old text line %d") %
492 492 (self.number, x))
493 493 self.a.append(u)
494 494 self.hunk.append(u)
495 495
496 496 l = lr.readline()
497 497 if l.startswith('\ '):
498 498 s = self.a[-1][:-1]
499 499 self.a[-1] = s
500 500 self.hunk[-1] = s
501 501 l = lr.readline()
502 502 m = contextdesc.match(l)
503 503 if not m:
504 504 raise PatchError(_("bad hunk #%d") % self.number)
505 505 foo, self.startb, foo2, bend, foo3 = m.groups()
506 506 self.startb = int(self.startb)
507 507 if bend == None:
508 508 bend = self.startb
509 509 self.lenb = int(bend) - self.startb
510 510 if self.startb:
511 511 self.lenb += 1
512 512 hunki = 1
513 513 for x in xrange(self.lenb):
514 514 l = lr.readline()
515 515 if l.startswith('\ '):
516 516 s = self.b[-1][:-1]
517 517 self.b[-1] = s
518 518 self.hunk[hunki-1] = s
519 519 continue
520 520 if not l:
521 521 lr.push(l)
522 522 break
523 523 s = l[2:]
524 524 if l.startswith('+ ') or l.startswith('! '):
525 525 u = '+' + s
526 526 elif l.startswith(' '):
527 527 u = ' ' + s
528 528 elif len(self.b) == 0:
529 529 # this can happen when the hunk does not add any lines
530 530 lr.push(l)
531 531 break
532 532 else:
533 533 raise PatchError(_("bad hunk #%d old text line %d") %
534 534 (self.number, x))
535 535 self.b.append(s)
536 536 while True:
537 537 if hunki >= len(self.hunk):
538 538 h = ""
539 539 else:
540 540 h = self.hunk[hunki]
541 541 hunki += 1
542 542 if h == u:
543 543 break
544 544 elif h.startswith('-'):
545 545 continue
546 546 else:
547 547 self.hunk.insert(hunki-1, u)
548 548 break
549 549
550 550 if not self.a:
551 551 # this happens when lines were only added to the hunk
552 552 for x in self.hunk:
553 553 if x.startswith('-') or x.startswith(' '):
554 554 self.a.append(x)
555 555 if not self.b:
556 556 # this happens when lines were only deleted from the hunk
557 557 for x in self.hunk:
558 558 if x.startswith('+') or x.startswith(' '):
559 559 self.b.append(x[1:])
560 560 # @@ -start,len +start,len @@
561 561 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
562 562 self.startb, self.lenb)
563 563 self.hunk[0] = self.desc
564 564
565 565 def reverse(self):
566 566 self.create, self.remove = self.remove, self.create
567 567 origlena = self.lena
568 568 origstarta = self.starta
569 569 self.lena = self.lenb
570 570 self.starta = self.startb
571 571 self.lenb = origlena
572 572 self.startb = origstarta
573 573 self.a = []
574 574 self.b = []
575 575 # self.hunk[0] is the @@ description
576 576 for x in xrange(1, len(self.hunk)):
577 577 o = self.hunk[x]
578 578 if o.startswith('-'):
579 579 n = '+' + o[1:]
580 580 self.b.append(o[1:])
581 581 elif o.startswith('+'):
582 582 n = '-' + o[1:]
583 583 self.a.append(n)
584 584 else:
585 585 n = o
586 586 self.b.append(o[1:])
587 587 self.a.append(o)
588 588 self.hunk[x] = o
589 589
590 590 def fix_newline(self):
591 591 diffhelpers.fix_newline(self.hunk, self.a, self.b)
592 592
593 593 def complete(self):
594 594 return len(self.a) == self.lena and len(self.b) == self.lenb
595 595
596 596 def createfile(self):
597 597 return self.starta == 0 and self.lena == 0 and self.create
598 598
599 599 def rmfile(self):
600 600 return self.startb == 0 and self.lenb == 0 and self.remove
601 601
602 602 def fuzzit(self, l, fuzz, toponly):
603 603 # this removes context lines from the top and bottom of list 'l'. It
604 604 # checks the hunk to make sure only context lines are removed, and then
605 605 # returns a new shortened list of lines.
606 606 fuzz = min(fuzz, len(l)-1)
607 607 if fuzz:
608 608 top = 0
609 609 bot = 0
610 610 hlen = len(self.hunk)
611 611 for x in xrange(hlen-1):
612 612 # the hunk starts with the @@ line, so use x+1
613 613 if self.hunk[x+1][0] == ' ':
614 614 top += 1
615 615 else:
616 616 break
617 617 if not toponly:
618 618 for x in xrange(hlen-1):
619 619 if self.hunk[hlen-bot-1][0] == ' ':
620 620 bot += 1
621 621 else:
622 622 break
623 623
624 624 # top and bot now count context in the hunk
625 625 # adjust them if either one is short
626 626 context = max(top, bot, 3)
627 627 if bot < context:
628 628 bot = max(0, fuzz - (context - bot))
629 629 else:
630 630 bot = min(fuzz, bot)
631 631 if top < context:
632 632 top = max(0, fuzz - (context - top))
633 633 else:
634 634 top = min(fuzz, top)
635 635
636 636 return l[top:len(l)-bot]
637 637 return l
638 638
639 639 def old(self, fuzz=0, toponly=False):
640 640 return self.fuzzit(self.a, fuzz, toponly)
641 641
642 642 def newctrl(self):
643 643 res = []
644 644 for x in self.hunk:
645 645 c = x[0]
646 646 if c == ' ' or c == '+':
647 647 res.append(x)
648 648 return res
649 649
650 650 def new(self, fuzz=0, toponly=False):
651 651 return self.fuzzit(self.b, fuzz, toponly)
652 652
653 653 class binhunk:
654 654 'A binary patch file. Only understands literals so far.'
655 655 def __init__(self, gitpatch):
656 656 self.gitpatch = gitpatch
657 657 self.text = None
658 658 self.hunk = ['GIT binary patch\n']
659 659
660 660 def createfile(self):
661 661 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
662 662
663 663 def rmfile(self):
664 664 return self.gitpatch.op == 'DELETE'
665 665
666 666 def complete(self):
667 667 return self.text is not None
668 668
669 669 def new(self):
670 670 return [self.text]
671 671
672 672 def extract(self, lr):
673 673 line = lr.readline()
674 674 self.hunk.append(line)
675 675 while line and not line.startswith('literal '):
676 676 line = lr.readline()
677 677 self.hunk.append(line)
678 678 if not line:
679 679 raise PatchError(_('could not extract binary patch'))
680 680 size = int(line[8:].rstrip())
681 681 dec = []
682 682 line = lr.readline()
683 683 self.hunk.append(line)
684 684 while len(line) > 1:
685 685 l = line[0]
686 686 if l <= 'Z' and l >= 'A':
687 687 l = ord(l) - ord('A') + 1
688 688 else:
689 689 l = ord(l) - ord('a') + 27
690 690 dec.append(base85.b85decode(line[1:-1])[:l])
691 691 line = lr.readline()
692 692 self.hunk.append(line)
693 693 text = zlib.decompress(''.join(dec))
694 694 if len(text) != size:
695 695 raise PatchError(_('binary patch is %d bytes, not %d') %
696 696 len(text), size)
697 697 self.text = text
698 698
699 699 def parsefilename(str):
700 700 # --- filename \t|space stuff
701 701 s = str[4:].rstrip('\r\n')
702 702 i = s.find('\t')
703 703 if i < 0:
704 704 i = s.find(' ')
705 705 if i < 0:
706 706 return s
707 707 return s[:i]
708 708
709 709 def selectfile(afile_orig, bfile_orig, hunk, strip, reverse):
710 710 def pathstrip(path, count=1):
711 711 pathlen = len(path)
712 712 i = 0
713 713 if count == 0:
714 714 return '', path.rstrip()
715 715 while count > 0:
716 716 i = path.find('/', i)
717 717 if i == -1:
718 718 raise PatchError(_("unable to strip away %d dirs from %s") %
719 719 (count, path))
720 720 i += 1
721 721 # consume '//' in the path
722 722 while i < pathlen - 1 and path[i] == '/':
723 723 i += 1
724 724 count -= 1
725 725 return path[:i].lstrip(), path[i:].rstrip()
726 726
727 727 nulla = afile_orig == "/dev/null"
728 728 nullb = bfile_orig == "/dev/null"
729 729 abase, afile = pathstrip(afile_orig, strip)
730 730 gooda = not nulla and os.path.exists(afile)
731 731 bbase, bfile = pathstrip(bfile_orig, strip)
732 732 if afile == bfile:
733 733 goodb = gooda
734 734 else:
735 735 goodb = not nullb and os.path.exists(bfile)
736 736 createfunc = hunk.createfile
737 737 if reverse:
738 738 createfunc = hunk.rmfile
739 739 missing = not goodb and not gooda and not createfunc()
740 740 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
741 741 # diff is between a file and its backup. In this case, the original
742 742 # file should be patched (see original mpatch code).
743 743 isbackup = (abase == bbase and bfile.startswith(afile))
744 744 fname = None
745 745 if not missing:
746 746 if gooda and goodb:
747 747 fname = isbackup and afile or bfile
748 748 elif gooda:
749 749 fname = afile
750 750
751 751 if not fname:
752 752 if not nullb:
753 753 fname = isbackup and afile or bfile
754 754 elif not nulla:
755 755 fname = afile
756 756 else:
757 757 raise PatchError(_("undefined source and destination files"))
758 758
759 759 return fname, missing
760 760
761 761 class linereader:
762 762 # simple class to allow pushing lines back into the input stream
763 763 def __init__(self, fp):
764 764 self.fp = fp
765 765 self.buf = []
766 766
767 767 def push(self, line):
768 768 if line is not None:
769 769 self.buf.append(line)
770 770
771 771 def readline(self):
772 772 if self.buf:
773 773 l = self.buf[0]
774 774 del self.buf[0]
775 775 return l
776 776 return self.fp.readline()
777 777
778 778 def __iter__(self):
779 779 while 1:
780 780 l = self.readline()
781 781 if not l:
782 782 break
783 783 yield l
784 784
785 785 def scangitpatch(lr, firstline):
786 786 """
787 787 Git patches can emit:
788 788 - rename a to b
789 789 - change b
790 790 - copy a to c
791 791 - change c
792 792
793 793 We cannot apply this sequence as-is, the renamed 'a' could not be
794 794 found for it would have been renamed already. And we cannot copy
795 795 from 'b' instead because 'b' would have been changed already. So
796 796 we scan the git patch for copy and rename commands so we can
797 797 perform the copies ahead of time.
798 798 """
799 799 pos = 0
800 800 try:
801 801 pos = lr.fp.tell()
802 802 fp = lr.fp
803 803 except IOError:
804 804 fp = cStringIO.StringIO(lr.fp.read())
805 805 gitlr = linereader(fp)
806 806 gitlr.push(firstline)
807 807 (dopatch, gitpatches) = readgitpatch(gitlr)
808 808 fp.seek(pos)
809 809 return dopatch, gitpatches
810 810
811 811 def iterhunks(ui, fp, sourcefile=None):
812 812 """Read a patch and yield the following events:
813 813 - ("file", afile, bfile, firsthunk): select a new target file.
814 814 - ("hunk", hunk): a new hunk is ready to be applied, follows a
815 815 "file" event.
816 816 - ("git", gitchanges): current diff is in git format, gitchanges
817 817 maps filenames to gitpatch records. Unique event.
818 818 """
819 819 changed = {}
820 820 current_hunk = None
821 821 afile = ""
822 822 bfile = ""
823 823 state = None
824 824 hunknum = 0
825 825 emitfile = False
826 826 git = False
827 827
828 828 # our states
829 829 BFILE = 1
830 830 context = None
831 831 lr = linereader(fp)
832 832 dopatch = True
833 833 # gitworkdone is True if a git operation (copy, rename, ...) was
834 834 # performed already for the current file. Useful when the file
835 835 # section may have no hunk.
836 836 gitworkdone = False
837 837
838 838 while True:
839 839 newfile = False
840 840 x = lr.readline()
841 841 if not x:
842 842 break
843 843 if current_hunk:
844 844 if x.startswith('\ '):
845 845 current_hunk.fix_newline()
846 846 yield 'hunk', current_hunk
847 847 current_hunk = None
848 848 gitworkdone = False
849 849 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
850 850 ((context or context == None) and x.startswith('***************')))):
851 851 try:
852 852 if context == None and x.startswith('***************'):
853 853 context = True
854 854 gpatch = changed.get(bfile)
855 855 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
856 856 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
857 857 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
858 858 except PatchError, err:
859 859 ui.debug(err)
860 860 current_hunk = None
861 861 continue
862 862 hunknum += 1
863 863 if emitfile:
864 864 emitfile = False
865 865 yield 'file', (afile, bfile, current_hunk)
866 866 elif state == BFILE and x.startswith('GIT binary patch'):
867 867 current_hunk = binhunk(changed[bfile])
868 868 hunknum += 1
869 869 if emitfile:
870 870 emitfile = False
871 871 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
872 872 current_hunk.extract(lr)
873 873 elif x.startswith('diff --git'):
874 874 # check for git diff, scanning the whole patch file if needed
875 875 m = gitre.match(x)
876 876 if m:
877 877 afile, bfile = m.group(1, 2)
878 878 if not git:
879 879 git = True
880 880 dopatch, gitpatches = scangitpatch(lr, x)
881 881 yield 'git', gitpatches
882 882 for gp in gitpatches:
883 883 changed[gp.path] = gp
884 884 # else error?
885 885 # copy/rename + modify should modify target, not source
886 886 gp = changed.get(bfile)
887 887 if gp and gp.op in ('COPY', 'DELETE', 'RENAME'):
888 888 afile = bfile
889 889 gitworkdone = True
890 890 newfile = True
891 891 elif x.startswith('---'):
892 892 # check for a unified diff
893 893 l2 = lr.readline()
894 894 if not l2.startswith('+++'):
895 895 lr.push(l2)
896 896 continue
897 897 newfile = True
898 898 context = False
899 899 afile = parsefilename(x)
900 900 bfile = parsefilename(l2)
901 901 elif x.startswith('***'):
902 902 # check for a context diff
903 903 l2 = lr.readline()
904 904 if not l2.startswith('---'):
905 905 lr.push(l2)
906 906 continue
907 907 l3 = lr.readline()
908 908 lr.push(l3)
909 909 if not l3.startswith("***************"):
910 910 lr.push(l2)
911 911 continue
912 912 newfile = True
913 913 context = True
914 914 afile = parsefilename(x)
915 915 bfile = parsefilename(l2)
916 916
917 917 if newfile:
918 918 emitfile = True
919 919 state = BFILE
920 920 hunknum = 0
921 921 if current_hunk:
922 922 if current_hunk.complete():
923 923 yield 'hunk', current_hunk
924 924 else:
925 925 raise PatchError(_("malformed patch %s %s") % (afile,
926 926 current_hunk.desc))
927 927
928 928 if hunknum == 0 and dopatch and not gitworkdone:
929 929 raise NoHunks
930 930
931 931 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False):
932 932 """reads a patch from fp and tries to apply it. The dict 'changed' is
933 933 filled in with all of the filenames changed by the patch. Returns 0
934 934 for a clean patch, -1 if any rejects were found and 1 if there was
935 935 any fuzz."""
936 936
937 937 rejects = 0
938 938 err = 0
939 939 current_file = None
940 940 gitpatches = None
941 941
942 942 def closefile():
943 943 if not current_file:
944 944 return 0
945 945 current_file.close()
946 946 return len(current_file.rej)
947 947
948 948 for state, values in iterhunks(ui, fp, sourcefile):
949 949 if state == 'hunk':
950 950 if not current_file:
951 951 continue
952 952 current_hunk = values
953 953 ret = current_file.apply(current_hunk, reverse)
954 954 if ret >= 0:
955 955 changed.setdefault(current_file.fname, None)
956 956 if ret > 0:
957 957 err = 1
958 958 elif state == 'file':
959 959 rejects += closefile()
960 960 afile, bfile, first_hunk = values
961 961 try:
962 962 if sourcefile:
963 963 current_file = patchfile(ui, sourcefile)
964 964 else:
965 965 current_file, missing = selectfile(afile, bfile, first_hunk,
966 966 strip, reverse)
967 967 current_file = patchfile(ui, current_file, missing)
968 968 except PatchError, err:
969 969 ui.warn(str(err) + '\n')
970 970 current_file, current_hunk = None, None
971 971 rejects += 1
972 972 continue
973 973 elif state == 'git':
974 974 gitpatches = values
975 975 cwd = os.getcwd()
976 976 for gp in gitpatches:
977 977 if gp.op in ('COPY', 'RENAME'):
978 978 src, dst = [util.canonpath(cwd, cwd, x)
979 979 for x in [gp.oldpath, gp.path]]
980 980 copyfile(src, dst)
981 981 changed[gp.path] = gp
982 982 else:
983 983 raise util.Abort(_('unsupported parser state: %s') % state)
984 984
985 985 rejects += closefile()
986 986
987 987 if rejects:
988 988 return -1
989 989 return err
990 990
991 991 def diffopts(ui, opts={}, untrusted=False):
992 992 def get(key, name=None, getter=ui.configbool):
993 993 return (opts.get(key) or
994 994 getter('diff', name or key, None, untrusted=untrusted))
995 995 return mdiff.diffopts(
996 996 text=opts.get('text'),
997 997 git=get('git'),
998 998 nodates=get('nodates'),
999 999 showfunc=get('show_function', 'showfunc'),
1000 1000 ignorews=get('ignore_all_space', 'ignorews'),
1001 1001 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1002 1002 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1003 1003 context=get('unified', getter=ui.config))
1004 1004
1005 1005 def updatedir(ui, repo, patches):
1006 1006 '''Update dirstate after patch application according to metadata'''
1007 1007 if not patches:
1008 1008 return
1009 1009 copies = []
1010 1010 removes = {}
1011 1011 cfiles = patches.keys()
1012 1012 cwd = repo.getcwd()
1013 1013 if cwd:
1014 1014 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1015 1015 for f in patches:
1016 1016 gp = patches[f]
1017 1017 if not gp:
1018 1018 continue
1019 1019 if gp.op == 'RENAME':
1020 1020 copies.append((gp.oldpath, gp.path))
1021 1021 removes[gp.oldpath] = 1
1022 1022 elif gp.op == 'COPY':
1023 1023 copies.append((gp.oldpath, gp.path))
1024 1024 elif gp.op == 'DELETE':
1025 1025 removes[gp.path] = 1
1026 1026 for src, dst in copies:
1027 1027 repo.copy(src, dst)
1028 1028 removes = removes.keys()
1029 1029 if removes:
1030 1030 repo.remove(util.sort(removes), True)
1031 1031 for f in patches:
1032 1032 gp = patches[f]
1033 1033 if gp and gp.mode:
1034 1034 islink, isexec = gp.mode
1035 1035 dst = os.path.join(repo.root, gp.path)
1036 1036 # patch won't create empty files
1037 1037 if gp.op == 'ADD' and not os.path.exists(dst):
1038 1038 flags = (isexec and 'x' or '') + (islink and 'l' or '')
1039 1039 repo.wwrite(gp.path, '', flags)
1040 1040 else:
1041 1041 util.set_flags(dst, islink, isexec)
1042 1042 cmdutil.addremove(repo, cfiles)
1043 1043 files = patches.keys()
1044 1044 files.extend([r for r in removes if r not in files])
1045 1045 return util.sort(files)
1046 1046
1047 1047 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
1048 1048 """use <patcher> to apply <patchname> to the working directory.
1049 1049 returns whether patch was applied with fuzz factor."""
1050 1050
1051 1051 fuzz = False
1052 1052 if cwd:
1053 1053 args.append('-d %s' % util.shellquote(cwd))
1054 1054 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1055 1055 util.shellquote(patchname)))
1056 1056
1057 1057 for line in fp:
1058 1058 line = line.rstrip()
1059 1059 ui.note(line + '\n')
1060 1060 if line.startswith('patching file '):
1061 1061 pf = util.parse_patch_output(line)
1062 1062 printed_file = False
1063 1063 files.setdefault(pf, None)
1064 1064 elif line.find('with fuzz') >= 0:
1065 1065 fuzz = True
1066 1066 if not printed_file:
1067 1067 ui.warn(pf + '\n')
1068 1068 printed_file = True
1069 1069 ui.warn(line + '\n')
1070 1070 elif line.find('saving rejects to file') >= 0:
1071 1071 ui.warn(line + '\n')
1072 1072 elif line.find('FAILED') >= 0:
1073 1073 if not printed_file:
1074 1074 ui.warn(pf + '\n')
1075 1075 printed_file = True
1076 1076 ui.warn(line + '\n')
1077 1077 code = fp.close()
1078 1078 if code:
1079 1079 raise PatchError(_("patch command failed: %s") %
1080 1080 util.explain_exit(code)[0])
1081 1081 return fuzz
1082 1082
1083 1083 def internalpatch(patchobj, ui, strip, cwd, files={}):
1084 1084 """use builtin patch to apply <patchobj> to the working directory.
1085 1085 returns whether patch was applied with fuzz factor."""
1086 1086 try:
1087 1087 fp = file(patchobj, 'rb')
1088 1088 except TypeError:
1089 1089 fp = patchobj
1090 1090 if cwd:
1091 1091 curdir = os.getcwd()
1092 1092 os.chdir(cwd)
1093 1093 try:
1094 1094 ret = applydiff(ui, fp, files, strip=strip)
1095 1095 finally:
1096 1096 if cwd:
1097 1097 os.chdir(curdir)
1098 1098 if ret < 0:
1099 1099 raise PatchError
1100 1100 return ret > 0
1101 1101
1102 1102 def patch(patchname, ui, strip=1, cwd=None, files={}):
1103 1103 """apply <patchname> to the working directory.
1104 1104 returns whether patch was applied with fuzz factor."""
1105 1105 patcher = ui.config('ui', 'patch')
1106 1106 args = []
1107 1107 try:
1108 1108 if patcher:
1109 1109 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1110 1110 files)
1111 1111 else:
1112 1112 try:
1113 1113 return internalpatch(patchname, ui, strip, cwd, files)
1114 1114 except NoHunks:
1115 1115 patcher = util.find_exe('gpatch') or util.find_exe('patch')
1116 1116 ui.debug(_('no valid hunks found; trying with %r instead\n') %
1117 1117 patcher)
1118 1118 if util.needbinarypatch():
1119 1119 args.append('--binary')
1120 1120 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1121 1121 files)
1122 1122 except PatchError, err:
1123 1123 s = str(err)
1124 1124 if s:
1125 1125 raise util.Abort(s)
1126 1126 else:
1127 1127 raise util.Abort(_('patch failed to apply'))
1128 1128
1129 1129 def b85diff(to, tn):
1130 1130 '''print base85-encoded binary diff'''
1131 1131 def gitindex(text):
1132 1132 if not text:
1133 1133 return '0' * 40
1134 1134 l = len(text)
1135 1135 s = util.sha1('blob %d\0' % l)
1136 1136 s.update(text)
1137 1137 return s.hexdigest()
1138 1138
1139 1139 def fmtline(line):
1140 1140 l = len(line)
1141 1141 if l <= 26:
1142 1142 l = chr(ord('A') + l - 1)
1143 1143 else:
1144 1144 l = chr(l - 26 + ord('a') - 1)
1145 1145 return '%c%s\n' % (l, base85.b85encode(line, True))
1146 1146
1147 1147 def chunk(text, csize=52):
1148 1148 l = len(text)
1149 1149 i = 0
1150 1150 while i < l:
1151 1151 yield text[i:i+csize]
1152 1152 i += csize
1153 1153
1154 1154 tohash = gitindex(to)
1155 1155 tnhash = gitindex(tn)
1156 1156 if tohash == tnhash:
1157 1157 return ""
1158 1158
1159 1159 # TODO: deltas
1160 1160 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1161 1161 (tohash, tnhash, len(tn))]
1162 1162 for l in chunk(zlib.compress(tn)):
1163 1163 ret.append(fmtline(l))
1164 1164 ret.append('\n')
1165 1165 return ''.join(ret)
1166 1166
1167 1167 def _addmodehdr(header, omode, nmode):
1168 1168 if omode != nmode:
1169 1169 header.append('old mode %s\n' % omode)
1170 1170 header.append('new mode %s\n' % nmode)
1171 1171
1172 def diff(repo, node1=None, node2=None, match=None,
1173 fp=None, changes=None, opts=None):
1174 '''print diff of changes to files between two nodes, or node and
1172 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None):
1173 '''yields diff of changes to files between two nodes, or node and
1175 1174 working directory.
1176 1175
1177 1176 if node1 is None, use first dirstate parent instead.
1178 1177 if node2 is None, compare node1 with working directory.'''
1179 1178
1180 1179 if not match:
1181 1180 match = cmdutil.matchall(repo)
1182 1181
1183 1182 if opts is None:
1184 1183 opts = mdiff.defaultopts
1185 if fp is None:
1186 fp = repo.ui
1187 1184
1188 1185 if not node1:
1189 1186 node1 = repo.dirstate.parents()[0]
1190 1187
1191 1188 flcache = {}
1192 1189 def getfilectx(f, ctx):
1193 1190 flctx = ctx.filectx(f, filelog=flcache.get(f))
1194 1191 if f not in flcache:
1195 1192 flcache[f] = flctx._filelog
1196 1193 return flctx
1197 1194
1198 1195 ctx1 = repo[node1]
1199 1196 ctx2 = repo[node2]
1200 1197
1201 1198 if not changes:
1202 1199 changes = repo.status(ctx1, ctx2, match=match)
1203 1200 modified, added, removed = changes[:3]
1204 1201
1205 1202 if not modified and not added and not removed:
1206 1203 return
1207 1204
1208 1205 date1 = util.datestr(ctx1.date())
1209 1206 man1 = ctx1.manifest()
1210 1207
1211 1208 if repo.ui.quiet:
1212 1209 r = None
1213 1210 else:
1214 1211 hexfunc = repo.ui.debugflag and hex or short
1215 1212 r = [hexfunc(node) for node in [node1, node2] if node]
1216 1213
1217 1214 if opts.git:
1218 1215 copy, diverge = copies.copies(repo, ctx1, ctx2, repo[nullid])
1219 1216 for k, v in copy.items():
1220 1217 copy[v] = k
1221 1218
1222 1219 gone = {}
1223 1220 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1224 1221
1225 1222 for f in util.sort(modified + added + removed):
1226 1223 to = None
1227 1224 tn = None
1228 1225 dodiff = True
1229 1226 header = []
1230 1227 if f in man1:
1231 1228 to = getfilectx(f, ctx1).data()
1232 1229 if f not in removed:
1233 1230 tn = getfilectx(f, ctx2).data()
1234 1231 a, b = f, f
1235 1232 if opts.git:
1236 1233 if f in added:
1237 1234 mode = gitmode[ctx2.flags(f)]
1238 1235 if f in copy:
1239 1236 a = copy[f]
1240 1237 omode = gitmode[man1.flags(a)]
1241 1238 _addmodehdr(header, omode, mode)
1242 1239 if a in removed and a not in gone:
1243 1240 op = 'rename'
1244 1241 gone[a] = 1
1245 1242 else:
1246 1243 op = 'copy'
1247 1244 header.append('%s from %s\n' % (op, a))
1248 1245 header.append('%s to %s\n' % (op, f))
1249 1246 to = getfilectx(a, ctx1).data()
1250 1247 else:
1251 1248 header.append('new file mode %s\n' % mode)
1252 1249 if util.binary(tn):
1253 1250 dodiff = 'binary'
1254 1251 elif f in removed:
1255 1252 # have we already reported a copy above?
1256 1253 if f in copy and copy[f] in added and copy[copy[f]] == f:
1257 1254 dodiff = False
1258 1255 else:
1259 1256 header.append('deleted file mode %s\n' %
1260 1257 gitmode[man1.flags(f)])
1261 1258 else:
1262 1259 omode = gitmode[man1.flags(f)]
1263 1260 nmode = gitmode[ctx2.flags(f)]
1264 1261 _addmodehdr(header, omode, nmode)
1265 1262 if util.binary(to) or util.binary(tn):
1266 1263 dodiff = 'binary'
1267 1264 r = None
1268 1265 header.insert(0, mdiff.diffline(r, a, b, opts))
1269 1266 if dodiff:
1270 1267 if dodiff == 'binary':
1271 1268 text = b85diff(to, tn)
1272 1269 else:
1273 1270 text = mdiff.unidiff(to, date1,
1274 1271 # ctx2 date may be dynamic
1275 1272 tn, util.datestr(ctx2.date()),
1276 1273 a, b, r, opts=opts)
1277 if text or len(header) > 1:
1278 fp.write(''.join(header))
1279 fp.write(text)
1274 if header and (text or len(header) > 1):
1275 yield ''.join(header)
1276 if text:
1277 yield text
1280 1278
1281 1279 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1282 1280 opts=None):
1283 1281 '''export changesets as hg patches.'''
1284 1282
1285 1283 total = len(revs)
1286 1284 revwidth = max([len(str(rev)) for rev in revs])
1287 1285
1288 1286 def single(rev, seqno, fp):
1289 1287 ctx = repo[rev]
1290 1288 node = ctx.node()
1291 1289 parents = [p.node() for p in ctx.parents() if p]
1292 1290 branch = ctx.branch()
1293 1291 if switch_parent:
1294 1292 parents.reverse()
1295 1293 prev = (parents and parents[0]) or nullid
1296 1294
1297 1295 if not fp:
1298 1296 fp = cmdutil.make_file(repo, template, node, total=total,
1299 1297 seqno=seqno, revwidth=revwidth)
1300 1298 if fp != sys.stdout and hasattr(fp, 'name'):
1301 1299 repo.ui.note("%s\n" % fp.name)
1302 1300
1303 1301 fp.write("# HG changeset patch\n")
1304 1302 fp.write("# User %s\n" % ctx.user())
1305 1303 fp.write("# Date %d %d\n" % ctx.date())
1306 1304 if branch and (branch != 'default'):
1307 1305 fp.write("# Branch %s\n" % branch)
1308 1306 fp.write("# Node ID %s\n" % hex(node))
1309 1307 fp.write("# Parent %s\n" % hex(prev))
1310 1308 if len(parents) > 1:
1311 1309 fp.write("# Parent %s\n" % hex(parents[1]))
1312 1310 fp.write(ctx.description().rstrip())
1313 1311 fp.write("\n\n")
1314 1312
1315 diff(repo, prev, node, fp=fp, opts=opts)
1313 for chunk in diff(repo, prev, node, opts=opts):
1314 fp.write(chunk)
1316 1315 if fp not in (sys.stdout, repo.ui):
1317 1316 fp.close()
1318 1317
1319 1318 for seqno, rev in enumerate(revs):
1320 1319 single(rev, seqno+1, fp)
1321 1320
1322 1321 def diffstat(patchlines):
1323 1322 if not util.find_exe('diffstat'):
1324 1323 return
1325 1324 output = util.filter('\n'.join(patchlines),
1326 1325 'diffstat -p1 -w79 2>%s' % util.nulldev)
1327 1326 stat = [l.lstrip() for l in output.splitlines(True)]
1328 1327 last = stat.pop()
1329 1328 stat.insert(0, last)
1330 1329 stat = ''.join(stat)
1331 1330 return stat
General Comments 0
You need to be logged in to leave comments. Login now