##// END OF EJS Templates
export: fixed silent output file overwriting...
Ronny Pfannschmidt -
r7319:eae1767c default
parent child Browse files
Show More
@@ -1,1190 +1,1193 b''
1 # cmdutil.py - help for command processing in mercurial
1 # cmdutil.py - help for command processing in mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 from node import hex, nullid, nullrev, short
8 from node import hex, nullid, nullrev, short
9 from i18n import _
9 from i18n import _
10 import os, sys, bisect, stat
10 import os, sys, bisect, stat
11 import mdiff, bdiff, util, templater, templatefilters, patch, errno
11 import mdiff, bdiff, util, templater, templatefilters, patch, errno
12 import match as _match
12 import match as _match
13
13
14 revrangesep = ':'
14 revrangesep = ':'
15
15
16 class UnknownCommand(Exception):
16 class UnknownCommand(Exception):
17 """Exception raised if command is not in the command table."""
17 """Exception raised if command is not in the command table."""
18 class AmbiguousCommand(Exception):
18 class AmbiguousCommand(Exception):
19 """Exception raised if command shortcut matches more than one command."""
19 """Exception raised if command shortcut matches more than one command."""
20
20
21 def findpossible(cmd, table, strict=False):
21 def findpossible(cmd, table, strict=False):
22 """
22 """
23 Return cmd -> (aliases, command table entry)
23 Return cmd -> (aliases, command table entry)
24 for each matching command.
24 for each matching command.
25 Return debug commands (or their aliases) only if no normal command matches.
25 Return debug commands (or their aliases) only if no normal command matches.
26 """
26 """
27 choice = {}
27 choice = {}
28 debugchoice = {}
28 debugchoice = {}
29 for e in table.keys():
29 for e in table.keys():
30 aliases = e.lstrip("^").split("|")
30 aliases = e.lstrip("^").split("|")
31 found = None
31 found = None
32 if cmd in aliases:
32 if cmd in aliases:
33 found = cmd
33 found = cmd
34 elif not strict:
34 elif not strict:
35 for a in aliases:
35 for a in aliases:
36 if a.startswith(cmd):
36 if a.startswith(cmd):
37 found = a
37 found = a
38 break
38 break
39 if found is not None:
39 if found is not None:
40 if aliases[0].startswith("debug") or found.startswith("debug"):
40 if aliases[0].startswith("debug") or found.startswith("debug"):
41 debugchoice[found] = (aliases, table[e])
41 debugchoice[found] = (aliases, table[e])
42 else:
42 else:
43 choice[found] = (aliases, table[e])
43 choice[found] = (aliases, table[e])
44
44
45 if not choice and debugchoice:
45 if not choice and debugchoice:
46 choice = debugchoice
46 choice = debugchoice
47
47
48 return choice
48 return choice
49
49
50 def findcmd(cmd, table, strict=True):
50 def findcmd(cmd, table, strict=True):
51 """Return (aliases, command table entry) for command string."""
51 """Return (aliases, command table entry) for command string."""
52 choice = findpossible(cmd, table, strict)
52 choice = findpossible(cmd, table, strict)
53
53
54 if cmd in choice:
54 if cmd in choice:
55 return choice[cmd]
55 return choice[cmd]
56
56
57 if len(choice) > 1:
57 if len(choice) > 1:
58 clist = choice.keys()
58 clist = choice.keys()
59 clist.sort()
59 clist.sort()
60 raise AmbiguousCommand(cmd, clist)
60 raise AmbiguousCommand(cmd, clist)
61
61
62 if choice:
62 if choice:
63 return choice.values()[0]
63 return choice.values()[0]
64
64
65 raise UnknownCommand(cmd)
65 raise UnknownCommand(cmd)
66
66
67 def bail_if_changed(repo):
67 def bail_if_changed(repo):
68 if repo.dirstate.parents()[1] != nullid:
68 if repo.dirstate.parents()[1] != nullid:
69 raise util.Abort(_('outstanding uncommitted merge'))
69 raise util.Abort(_('outstanding uncommitted merge'))
70 modified, added, removed, deleted = repo.status()[:4]
70 modified, added, removed, deleted = repo.status()[:4]
71 if modified or added or removed or deleted:
71 if modified or added or removed or deleted:
72 raise util.Abort(_("outstanding uncommitted changes"))
72 raise util.Abort(_("outstanding uncommitted changes"))
73
73
74 def logmessage(opts):
74 def logmessage(opts):
75 """ get the log message according to -m and -l option """
75 """ get the log message according to -m and -l option """
76 message = opts['message']
76 message = opts['message']
77 logfile = opts['logfile']
77 logfile = opts['logfile']
78
78
79 if message and logfile:
79 if message and logfile:
80 raise util.Abort(_('options --message and --logfile are mutually '
80 raise util.Abort(_('options --message and --logfile are mutually '
81 'exclusive'))
81 'exclusive'))
82 if not message and logfile:
82 if not message and logfile:
83 try:
83 try:
84 if logfile == '-':
84 if logfile == '-':
85 message = sys.stdin.read()
85 message = sys.stdin.read()
86 else:
86 else:
87 message = open(logfile).read()
87 message = open(logfile).read()
88 except IOError, inst:
88 except IOError, inst:
89 raise util.Abort(_("can't read commit message '%s': %s") %
89 raise util.Abort(_("can't read commit message '%s': %s") %
90 (logfile, inst.strerror))
90 (logfile, inst.strerror))
91 return message
91 return message
92
92
93 def loglimit(opts):
93 def loglimit(opts):
94 """get the log limit according to option -l/--limit"""
94 """get the log limit according to option -l/--limit"""
95 limit = opts.get('limit')
95 limit = opts.get('limit')
96 if limit:
96 if limit:
97 try:
97 try:
98 limit = int(limit)
98 limit = int(limit)
99 except ValueError:
99 except ValueError:
100 raise util.Abort(_('limit must be a positive integer'))
100 raise util.Abort(_('limit must be a positive integer'))
101 if limit <= 0: raise util.Abort(_('limit must be positive'))
101 if limit <= 0: raise util.Abort(_('limit must be positive'))
102 else:
102 else:
103 limit = sys.maxint
103 limit = sys.maxint
104 return limit
104 return limit
105
105
106 def setremoteconfig(ui, opts):
106 def setremoteconfig(ui, opts):
107 "copy remote options to ui tree"
107 "copy remote options to ui tree"
108 if opts.get('ssh'):
108 if opts.get('ssh'):
109 ui.setconfig("ui", "ssh", opts['ssh'])
109 ui.setconfig("ui", "ssh", opts['ssh'])
110 if opts.get('remotecmd'):
110 if opts.get('remotecmd'):
111 ui.setconfig("ui", "remotecmd", opts['remotecmd'])
111 ui.setconfig("ui", "remotecmd", opts['remotecmd'])
112
112
113 def revpair(repo, revs):
113 def revpair(repo, revs):
114 '''return pair of nodes, given list of revisions. second item can
114 '''return pair of nodes, given list of revisions. second item can
115 be None, meaning use working dir.'''
115 be None, meaning use working dir.'''
116
116
117 def revfix(repo, val, defval):
117 def revfix(repo, val, defval):
118 if not val and val != 0 and defval is not None:
118 if not val and val != 0 and defval is not None:
119 val = defval
119 val = defval
120 return repo.lookup(val)
120 return repo.lookup(val)
121
121
122 if not revs:
122 if not revs:
123 return repo.dirstate.parents()[0], None
123 return repo.dirstate.parents()[0], None
124 end = None
124 end = None
125 if len(revs) == 1:
125 if len(revs) == 1:
126 if revrangesep in revs[0]:
126 if revrangesep in revs[0]:
127 start, end = revs[0].split(revrangesep, 1)
127 start, end = revs[0].split(revrangesep, 1)
128 start = revfix(repo, start, 0)
128 start = revfix(repo, start, 0)
129 end = revfix(repo, end, len(repo) - 1)
129 end = revfix(repo, end, len(repo) - 1)
130 else:
130 else:
131 start = revfix(repo, revs[0], None)
131 start = revfix(repo, revs[0], None)
132 elif len(revs) == 2:
132 elif len(revs) == 2:
133 if revrangesep in revs[0] or revrangesep in revs[1]:
133 if revrangesep in revs[0] or revrangesep in revs[1]:
134 raise util.Abort(_('too many revisions specified'))
134 raise util.Abort(_('too many revisions specified'))
135 start = revfix(repo, revs[0], None)
135 start = revfix(repo, revs[0], None)
136 end = revfix(repo, revs[1], None)
136 end = revfix(repo, revs[1], None)
137 else:
137 else:
138 raise util.Abort(_('too many revisions specified'))
138 raise util.Abort(_('too many revisions specified'))
139 return start, end
139 return start, end
140
140
141 def revrange(repo, revs):
141 def revrange(repo, revs):
142 """Yield revision as strings from a list of revision specifications."""
142 """Yield revision as strings from a list of revision specifications."""
143
143
144 def revfix(repo, val, defval):
144 def revfix(repo, val, defval):
145 if not val and val != 0 and defval is not None:
145 if not val and val != 0 and defval is not None:
146 return defval
146 return defval
147 return repo.changelog.rev(repo.lookup(val))
147 return repo.changelog.rev(repo.lookup(val))
148
148
149 seen, l = {}, []
149 seen, l = {}, []
150 for spec in revs:
150 for spec in revs:
151 if revrangesep in spec:
151 if revrangesep in spec:
152 start, end = spec.split(revrangesep, 1)
152 start, end = spec.split(revrangesep, 1)
153 start = revfix(repo, start, 0)
153 start = revfix(repo, start, 0)
154 end = revfix(repo, end, len(repo) - 1)
154 end = revfix(repo, end, len(repo) - 1)
155 step = start > end and -1 or 1
155 step = start > end and -1 or 1
156 for rev in xrange(start, end+step, step):
156 for rev in xrange(start, end+step, step):
157 if rev in seen:
157 if rev in seen:
158 continue
158 continue
159 seen[rev] = 1
159 seen[rev] = 1
160 l.append(rev)
160 l.append(rev)
161 else:
161 else:
162 rev = revfix(repo, spec, None)
162 rev = revfix(repo, spec, None)
163 if rev in seen:
163 if rev in seen:
164 continue
164 continue
165 seen[rev] = 1
165 seen[rev] = 1
166 l.append(rev)
166 l.append(rev)
167
167
168 return l
168 return l
169
169
170 def make_filename(repo, pat, node,
170 def make_filename(repo, pat, node,
171 total=None, seqno=None, revwidth=None, pathname=None):
171 total=None, seqno=None, revwidth=None, pathname=None):
172 node_expander = {
172 node_expander = {
173 'H': lambda: hex(node),
173 'H': lambda: hex(node),
174 'R': lambda: str(repo.changelog.rev(node)),
174 'R': lambda: str(repo.changelog.rev(node)),
175 'h': lambda: short(node),
175 'h': lambda: short(node),
176 }
176 }
177 expander = {
177 expander = {
178 '%': lambda: '%',
178 '%': lambda: '%',
179 'b': lambda: os.path.basename(repo.root),
179 'b': lambda: os.path.basename(repo.root),
180 }
180 }
181
181
182 try:
182 try:
183 if node:
183 if node:
184 expander.update(node_expander)
184 expander.update(node_expander)
185 if node:
185 if node:
186 expander['r'] = (lambda:
186 expander['r'] = (lambda:
187 str(repo.changelog.rev(node)).zfill(revwidth or 0))
187 str(repo.changelog.rev(node)).zfill(revwidth or 0))
188 if total is not None:
188 if total is not None:
189 expander['N'] = lambda: str(total)
189 expander['N'] = lambda: str(total)
190 if seqno is not None:
190 if seqno is not None:
191 expander['n'] = lambda: str(seqno)
191 expander['n'] = lambda: str(seqno)
192 if total is not None and seqno is not None:
192 if total is not None and seqno is not None:
193 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
193 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
194 if pathname is not None:
194 if pathname is not None:
195 expander['s'] = lambda: os.path.basename(pathname)
195 expander['s'] = lambda: os.path.basename(pathname)
196 expander['d'] = lambda: os.path.dirname(pathname) or '.'
196 expander['d'] = lambda: os.path.dirname(pathname) or '.'
197 expander['p'] = lambda: pathname
197 expander['p'] = lambda: pathname
198
198
199 newname = []
199 newname = []
200 patlen = len(pat)
200 patlen = len(pat)
201 i = 0
201 i = 0
202 while i < patlen:
202 while i < patlen:
203 c = pat[i]
203 c = pat[i]
204 if c == '%':
204 if c == '%':
205 i += 1
205 i += 1
206 c = pat[i]
206 c = pat[i]
207 c = expander[c]()
207 c = expander[c]()
208 newname.append(c)
208 newname.append(c)
209 i += 1
209 i += 1
210 return ''.join(newname)
210 return ''.join(newname)
211 except KeyError, inst:
211 except KeyError, inst:
212 raise util.Abort(_("invalid format spec '%%%s' in output file name") %
212 raise util.Abort(_("invalid format spec '%%%s' in output file name") %
213 inst.args[0])
213 inst.args[0])
214
214
215 def make_file(repo, pat, node=None,
215 def make_file(repo, pat, node=None,
216 total=None, seqno=None, revwidth=None, mode='wb', pathname=None):
216 total=None, seqno=None, revwidth=None, mode='wb', pathname=None):
217
218 writable = 'w' in mode or 'a' in mode
219
217 if not pat or pat == '-':
220 if not pat or pat == '-':
218 return 'w' in mode and sys.stdout or sys.stdin
221 return writable and sys.stdout or sys.stdin
219 if hasattr(pat, 'write') and 'w' in mode:
222 if hasattr(pat, 'write') and writable:
220 return pat
223 return pat
221 if hasattr(pat, 'read') and 'r' in mode:
224 if hasattr(pat, 'read') and 'r' in mode:
222 return pat
225 return pat
223 return open(make_filename(repo, pat, node, total, seqno, revwidth,
226 return open(make_filename(repo, pat, node, total, seqno, revwidth,
224 pathname),
227 pathname),
225 mode)
228 mode)
226
229
227 def match(repo, pats=[], opts={}, globbed=False, default='relpath'):
230 def match(repo, pats=[], opts={}, globbed=False, default='relpath'):
228 if not globbed and default == 'relpath':
231 if not globbed and default == 'relpath':
229 pats = util.expand_glob(pats or [])
232 pats = util.expand_glob(pats or [])
230 m = _match.match(repo.root, repo.getcwd(), pats,
233 m = _match.match(repo.root, repo.getcwd(), pats,
231 opts.get('include'), opts.get('exclude'), default)
234 opts.get('include'), opts.get('exclude'), default)
232 def badfn(f, msg):
235 def badfn(f, msg):
233 repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
236 repo.ui.warn("%s: %s\n" % (m.rel(f), msg))
234 return False
237 return False
235 m.bad = badfn
238 m.bad = badfn
236 return m
239 return m
237
240
238 def matchall(repo):
241 def matchall(repo):
239 return _match.always(repo.root, repo.getcwd())
242 return _match.always(repo.root, repo.getcwd())
240
243
241 def matchfiles(repo, files):
244 def matchfiles(repo, files):
242 return _match.exact(repo.root, repo.getcwd(), files)
245 return _match.exact(repo.root, repo.getcwd(), files)
243
246
244 def findrenames(repo, added=None, removed=None, threshold=0.5):
247 def findrenames(repo, added=None, removed=None, threshold=0.5):
245 '''find renamed files -- yields (before, after, score) tuples'''
248 '''find renamed files -- yields (before, after, score) tuples'''
246 if added is None or removed is None:
249 if added is None or removed is None:
247 added, removed = repo.status()[1:3]
250 added, removed = repo.status()[1:3]
248 ctx = repo['.']
251 ctx = repo['.']
249 for a in added:
252 for a in added:
250 aa = repo.wread(a)
253 aa = repo.wread(a)
251 bestname, bestscore = None, threshold
254 bestname, bestscore = None, threshold
252 for r in removed:
255 for r in removed:
253 rr = ctx.filectx(r).data()
256 rr = ctx.filectx(r).data()
254
257
255 # bdiff.blocks() returns blocks of matching lines
258 # bdiff.blocks() returns blocks of matching lines
256 # count the number of bytes in each
259 # count the number of bytes in each
257 equal = 0
260 equal = 0
258 alines = mdiff.splitnewlines(aa)
261 alines = mdiff.splitnewlines(aa)
259 matches = bdiff.blocks(aa, rr)
262 matches = bdiff.blocks(aa, rr)
260 for x1,x2,y1,y2 in matches:
263 for x1,x2,y1,y2 in matches:
261 for line in alines[x1:x2]:
264 for line in alines[x1:x2]:
262 equal += len(line)
265 equal += len(line)
263
266
264 lengths = len(aa) + len(rr)
267 lengths = len(aa) + len(rr)
265 if lengths:
268 if lengths:
266 myscore = equal*2.0 / lengths
269 myscore = equal*2.0 / lengths
267 if myscore >= bestscore:
270 if myscore >= bestscore:
268 bestname, bestscore = r, myscore
271 bestname, bestscore = r, myscore
269 if bestname:
272 if bestname:
270 yield bestname, a, bestscore
273 yield bestname, a, bestscore
271
274
272 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
275 def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None):
273 if dry_run is None:
276 if dry_run is None:
274 dry_run = opts.get('dry_run')
277 dry_run = opts.get('dry_run')
275 if similarity is None:
278 if similarity is None:
276 similarity = float(opts.get('similarity') or 0)
279 similarity = float(opts.get('similarity') or 0)
277 add, remove = [], []
280 add, remove = [], []
278 mapping = {}
281 mapping = {}
279 audit_path = util.path_auditor(repo.root)
282 audit_path = util.path_auditor(repo.root)
280 m = match(repo, pats, opts)
283 m = match(repo, pats, opts)
281 for abs in repo.walk(m):
284 for abs in repo.walk(m):
282 target = repo.wjoin(abs)
285 target = repo.wjoin(abs)
283 good = True
286 good = True
284 try:
287 try:
285 audit_path(abs)
288 audit_path(abs)
286 except:
289 except:
287 good = False
290 good = False
288 rel = m.rel(abs)
291 rel = m.rel(abs)
289 exact = m.exact(abs)
292 exact = m.exact(abs)
290 if good and abs not in repo.dirstate:
293 if good and abs not in repo.dirstate:
291 add.append(abs)
294 add.append(abs)
292 mapping[abs] = rel, m.exact(abs)
295 mapping[abs] = rel, m.exact(abs)
293 if repo.ui.verbose or not exact:
296 if repo.ui.verbose or not exact:
294 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
297 repo.ui.status(_('adding %s\n') % ((pats and rel) or abs))
295 if repo.dirstate[abs] != 'r' and (not good or not util.lexists(target)
298 if repo.dirstate[abs] != 'r' and (not good or not util.lexists(target)
296 or (os.path.isdir(target) and not os.path.islink(target))):
299 or (os.path.isdir(target) and not os.path.islink(target))):
297 remove.append(abs)
300 remove.append(abs)
298 mapping[abs] = rel, exact
301 mapping[abs] = rel, exact
299 if repo.ui.verbose or not exact:
302 if repo.ui.verbose or not exact:
300 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
303 repo.ui.status(_('removing %s\n') % ((pats and rel) or abs))
301 if not dry_run:
304 if not dry_run:
302 repo.remove(remove)
305 repo.remove(remove)
303 repo.add(add)
306 repo.add(add)
304 if similarity > 0:
307 if similarity > 0:
305 for old, new, score in findrenames(repo, add, remove, similarity):
308 for old, new, score in findrenames(repo, add, remove, similarity):
306 oldrel, oldexact = mapping[old]
309 oldrel, oldexact = mapping[old]
307 newrel, newexact = mapping[new]
310 newrel, newexact = mapping[new]
308 if repo.ui.verbose or not oldexact or not newexact:
311 if repo.ui.verbose or not oldexact or not newexact:
309 repo.ui.status(_('recording removal of %s as rename to %s '
312 repo.ui.status(_('recording removal of %s as rename to %s '
310 '(%d%% similar)\n') %
313 '(%d%% similar)\n') %
311 (oldrel, newrel, score * 100))
314 (oldrel, newrel, score * 100))
312 if not dry_run:
315 if not dry_run:
313 repo.copy(old, new)
316 repo.copy(old, new)
314
317
315 def copy(ui, repo, pats, opts, rename=False):
318 def copy(ui, repo, pats, opts, rename=False):
316 # called with the repo lock held
319 # called with the repo lock held
317 #
320 #
318 # hgsep => pathname that uses "/" to separate directories
321 # hgsep => pathname that uses "/" to separate directories
319 # ossep => pathname that uses os.sep to separate directories
322 # ossep => pathname that uses os.sep to separate directories
320 cwd = repo.getcwd()
323 cwd = repo.getcwd()
321 targets = {}
324 targets = {}
322 after = opts.get("after")
325 after = opts.get("after")
323 dryrun = opts.get("dry_run")
326 dryrun = opts.get("dry_run")
324
327
325 def walkpat(pat):
328 def walkpat(pat):
326 srcs = []
329 srcs = []
327 m = match(repo, [pat], opts, globbed=True)
330 m = match(repo, [pat], opts, globbed=True)
328 for abs in repo.walk(m):
331 for abs in repo.walk(m):
329 state = repo.dirstate[abs]
332 state = repo.dirstate[abs]
330 rel = m.rel(abs)
333 rel = m.rel(abs)
331 exact = m.exact(abs)
334 exact = m.exact(abs)
332 if state in '?r':
335 if state in '?r':
333 if exact and state == '?':
336 if exact and state == '?':
334 ui.warn(_('%s: not copying - file is not managed\n') % rel)
337 ui.warn(_('%s: not copying - file is not managed\n') % rel)
335 if exact and state == 'r':
338 if exact and state == 'r':
336 ui.warn(_('%s: not copying - file has been marked for'
339 ui.warn(_('%s: not copying - file has been marked for'
337 ' remove\n') % rel)
340 ' remove\n') % rel)
338 continue
341 continue
339 # abs: hgsep
342 # abs: hgsep
340 # rel: ossep
343 # rel: ossep
341 srcs.append((abs, rel, exact))
344 srcs.append((abs, rel, exact))
342 return srcs
345 return srcs
343
346
344 # abssrc: hgsep
347 # abssrc: hgsep
345 # relsrc: ossep
348 # relsrc: ossep
346 # otarget: ossep
349 # otarget: ossep
347 def copyfile(abssrc, relsrc, otarget, exact):
350 def copyfile(abssrc, relsrc, otarget, exact):
348 abstarget = util.canonpath(repo.root, cwd, otarget)
351 abstarget = util.canonpath(repo.root, cwd, otarget)
349 reltarget = repo.pathto(abstarget, cwd)
352 reltarget = repo.pathto(abstarget, cwd)
350 target = repo.wjoin(abstarget)
353 target = repo.wjoin(abstarget)
351 src = repo.wjoin(abssrc)
354 src = repo.wjoin(abssrc)
352 state = repo.dirstate[abstarget]
355 state = repo.dirstate[abstarget]
353
356
354 # check for collisions
357 # check for collisions
355 prevsrc = targets.get(abstarget)
358 prevsrc = targets.get(abstarget)
356 if prevsrc is not None:
359 if prevsrc is not None:
357 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
360 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
358 (reltarget, repo.pathto(abssrc, cwd),
361 (reltarget, repo.pathto(abssrc, cwd),
359 repo.pathto(prevsrc, cwd)))
362 repo.pathto(prevsrc, cwd)))
360 return
363 return
361
364
362 # check for overwrites
365 # check for overwrites
363 exists = os.path.exists(target)
366 exists = os.path.exists(target)
364 if (not after and exists or after and state in 'mn'):
367 if (not after and exists or after and state in 'mn'):
365 if not opts['force']:
368 if not opts['force']:
366 ui.warn(_('%s: not overwriting - file exists\n') %
369 ui.warn(_('%s: not overwriting - file exists\n') %
367 reltarget)
370 reltarget)
368 return
371 return
369
372
370 if after:
373 if after:
371 if not exists:
374 if not exists:
372 return
375 return
373 elif not dryrun:
376 elif not dryrun:
374 try:
377 try:
375 if exists:
378 if exists:
376 os.unlink(target)
379 os.unlink(target)
377 targetdir = os.path.dirname(target) or '.'
380 targetdir = os.path.dirname(target) or '.'
378 if not os.path.isdir(targetdir):
381 if not os.path.isdir(targetdir):
379 os.makedirs(targetdir)
382 os.makedirs(targetdir)
380 util.copyfile(src, target)
383 util.copyfile(src, target)
381 except IOError, inst:
384 except IOError, inst:
382 if inst.errno == errno.ENOENT:
385 if inst.errno == errno.ENOENT:
383 ui.warn(_('%s: deleted in working copy\n') % relsrc)
386 ui.warn(_('%s: deleted in working copy\n') % relsrc)
384 else:
387 else:
385 ui.warn(_('%s: cannot copy - %s\n') %
388 ui.warn(_('%s: cannot copy - %s\n') %
386 (relsrc, inst.strerror))
389 (relsrc, inst.strerror))
387 return True # report a failure
390 return True # report a failure
388
391
389 if ui.verbose or not exact:
392 if ui.verbose or not exact:
390 action = rename and "moving" or "copying"
393 action = rename and "moving" or "copying"
391 ui.status(_('%s %s to %s\n') % (action, relsrc, reltarget))
394 ui.status(_('%s %s to %s\n') % (action, relsrc, reltarget))
392
395
393 targets[abstarget] = abssrc
396 targets[abstarget] = abssrc
394
397
395 # fix up dirstate
398 # fix up dirstate
396 origsrc = repo.dirstate.copied(abssrc) or abssrc
399 origsrc = repo.dirstate.copied(abssrc) or abssrc
397 if abstarget == origsrc: # copying back a copy?
400 if abstarget == origsrc: # copying back a copy?
398 if state not in 'mn' and not dryrun:
401 if state not in 'mn' and not dryrun:
399 repo.dirstate.normallookup(abstarget)
402 repo.dirstate.normallookup(abstarget)
400 else:
403 else:
401 if repo.dirstate[origsrc] == 'a' and origsrc == abssrc:
404 if repo.dirstate[origsrc] == 'a' and origsrc == abssrc:
402 if not ui.quiet:
405 if not ui.quiet:
403 ui.warn(_("%s has not been committed yet, so no copy "
406 ui.warn(_("%s has not been committed yet, so no copy "
404 "data will be stored for %s.\n")
407 "data will be stored for %s.\n")
405 % (repo.pathto(origsrc, cwd), reltarget))
408 % (repo.pathto(origsrc, cwd), reltarget))
406 if repo.dirstate[abstarget] in '?r' and not dryrun:
409 if repo.dirstate[abstarget] in '?r' and not dryrun:
407 repo.add([abstarget])
410 repo.add([abstarget])
408 elif not dryrun:
411 elif not dryrun:
409 repo.copy(origsrc, abstarget)
412 repo.copy(origsrc, abstarget)
410
413
411 if rename and not dryrun:
414 if rename and not dryrun:
412 repo.remove([abssrc], not after)
415 repo.remove([abssrc], not after)
413
416
414 # pat: ossep
417 # pat: ossep
415 # dest ossep
418 # dest ossep
416 # srcs: list of (hgsep, hgsep, ossep, bool)
419 # srcs: list of (hgsep, hgsep, ossep, bool)
417 # return: function that takes hgsep and returns ossep
420 # return: function that takes hgsep and returns ossep
418 def targetpathfn(pat, dest, srcs):
421 def targetpathfn(pat, dest, srcs):
419 if os.path.isdir(pat):
422 if os.path.isdir(pat):
420 abspfx = util.canonpath(repo.root, cwd, pat)
423 abspfx = util.canonpath(repo.root, cwd, pat)
421 abspfx = util.localpath(abspfx)
424 abspfx = util.localpath(abspfx)
422 if destdirexists:
425 if destdirexists:
423 striplen = len(os.path.split(abspfx)[0])
426 striplen = len(os.path.split(abspfx)[0])
424 else:
427 else:
425 striplen = len(abspfx)
428 striplen = len(abspfx)
426 if striplen:
429 if striplen:
427 striplen += len(os.sep)
430 striplen += len(os.sep)
428 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
431 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
429 elif destdirexists:
432 elif destdirexists:
430 res = lambda p: os.path.join(dest,
433 res = lambda p: os.path.join(dest,
431 os.path.basename(util.localpath(p)))
434 os.path.basename(util.localpath(p)))
432 else:
435 else:
433 res = lambda p: dest
436 res = lambda p: dest
434 return res
437 return res
435
438
436 # pat: ossep
439 # pat: ossep
437 # dest ossep
440 # dest ossep
438 # srcs: list of (hgsep, hgsep, ossep, bool)
441 # srcs: list of (hgsep, hgsep, ossep, bool)
439 # return: function that takes hgsep and returns ossep
442 # return: function that takes hgsep and returns ossep
440 def targetpathafterfn(pat, dest, srcs):
443 def targetpathafterfn(pat, dest, srcs):
441 if util.patkind(pat, None)[0]:
444 if util.patkind(pat, None)[0]:
442 # a mercurial pattern
445 # a mercurial pattern
443 res = lambda p: os.path.join(dest,
446 res = lambda p: os.path.join(dest,
444 os.path.basename(util.localpath(p)))
447 os.path.basename(util.localpath(p)))
445 else:
448 else:
446 abspfx = util.canonpath(repo.root, cwd, pat)
449 abspfx = util.canonpath(repo.root, cwd, pat)
447 if len(abspfx) < len(srcs[0][0]):
450 if len(abspfx) < len(srcs[0][0]):
448 # A directory. Either the target path contains the last
451 # A directory. Either the target path contains the last
449 # component of the source path or it does not.
452 # component of the source path or it does not.
450 def evalpath(striplen):
453 def evalpath(striplen):
451 score = 0
454 score = 0
452 for s in srcs:
455 for s in srcs:
453 t = os.path.join(dest, util.localpath(s[0])[striplen:])
456 t = os.path.join(dest, util.localpath(s[0])[striplen:])
454 if os.path.exists(t):
457 if os.path.exists(t):
455 score += 1
458 score += 1
456 return score
459 return score
457
460
458 abspfx = util.localpath(abspfx)
461 abspfx = util.localpath(abspfx)
459 striplen = len(abspfx)
462 striplen = len(abspfx)
460 if striplen:
463 if striplen:
461 striplen += len(os.sep)
464 striplen += len(os.sep)
462 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
465 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
463 score = evalpath(striplen)
466 score = evalpath(striplen)
464 striplen1 = len(os.path.split(abspfx)[0])
467 striplen1 = len(os.path.split(abspfx)[0])
465 if striplen1:
468 if striplen1:
466 striplen1 += len(os.sep)
469 striplen1 += len(os.sep)
467 if evalpath(striplen1) > score:
470 if evalpath(striplen1) > score:
468 striplen = striplen1
471 striplen = striplen1
469 res = lambda p: os.path.join(dest,
472 res = lambda p: os.path.join(dest,
470 util.localpath(p)[striplen:])
473 util.localpath(p)[striplen:])
471 else:
474 else:
472 # a file
475 # a file
473 if destdirexists:
476 if destdirexists:
474 res = lambda p: os.path.join(dest,
477 res = lambda p: os.path.join(dest,
475 os.path.basename(util.localpath(p)))
478 os.path.basename(util.localpath(p)))
476 else:
479 else:
477 res = lambda p: dest
480 res = lambda p: dest
478 return res
481 return res
479
482
480
483
481 pats = util.expand_glob(pats)
484 pats = util.expand_glob(pats)
482 if not pats:
485 if not pats:
483 raise util.Abort(_('no source or destination specified'))
486 raise util.Abort(_('no source or destination specified'))
484 if len(pats) == 1:
487 if len(pats) == 1:
485 raise util.Abort(_('no destination specified'))
488 raise util.Abort(_('no destination specified'))
486 dest = pats.pop()
489 dest = pats.pop()
487 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
490 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
488 if not destdirexists:
491 if not destdirexists:
489 if len(pats) > 1 or util.patkind(pats[0], None)[0]:
492 if len(pats) > 1 or util.patkind(pats[0], None)[0]:
490 raise util.Abort(_('with multiple sources, destination must be an '
493 raise util.Abort(_('with multiple sources, destination must be an '
491 'existing directory'))
494 'existing directory'))
492 if util.endswithsep(dest):
495 if util.endswithsep(dest):
493 raise util.Abort(_('destination %s is not a directory') % dest)
496 raise util.Abort(_('destination %s is not a directory') % dest)
494
497
495 tfn = targetpathfn
498 tfn = targetpathfn
496 if after:
499 if after:
497 tfn = targetpathafterfn
500 tfn = targetpathafterfn
498 copylist = []
501 copylist = []
499 for pat in pats:
502 for pat in pats:
500 srcs = walkpat(pat)
503 srcs = walkpat(pat)
501 if not srcs:
504 if not srcs:
502 continue
505 continue
503 copylist.append((tfn(pat, dest, srcs), srcs))
506 copylist.append((tfn(pat, dest, srcs), srcs))
504 if not copylist:
507 if not copylist:
505 raise util.Abort(_('no files to copy'))
508 raise util.Abort(_('no files to copy'))
506
509
507 errors = 0
510 errors = 0
508 for targetpath, srcs in copylist:
511 for targetpath, srcs in copylist:
509 for abssrc, relsrc, exact in srcs:
512 for abssrc, relsrc, exact in srcs:
510 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
513 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
511 errors += 1
514 errors += 1
512
515
513 if errors:
516 if errors:
514 ui.warn(_('(consider using --after)\n'))
517 ui.warn(_('(consider using --after)\n'))
515
518
516 return errors
519 return errors
517
520
518 def service(opts, parentfn=None, initfn=None, runfn=None):
521 def service(opts, parentfn=None, initfn=None, runfn=None):
519 '''Run a command as a service.'''
522 '''Run a command as a service.'''
520
523
521 if opts['daemon'] and not opts['daemon_pipefds']:
524 if opts['daemon'] and not opts['daemon_pipefds']:
522 rfd, wfd = os.pipe()
525 rfd, wfd = os.pipe()
523 args = sys.argv[:]
526 args = sys.argv[:]
524 args.append('--daemon-pipefds=%d,%d' % (rfd, wfd))
527 args.append('--daemon-pipefds=%d,%d' % (rfd, wfd))
525 # Don't pass --cwd to the child process, because we've already
528 # Don't pass --cwd to the child process, because we've already
526 # changed directory.
529 # changed directory.
527 for i in xrange(1,len(args)):
530 for i in xrange(1,len(args)):
528 if args[i].startswith('--cwd='):
531 if args[i].startswith('--cwd='):
529 del args[i]
532 del args[i]
530 break
533 break
531 elif args[i].startswith('--cwd'):
534 elif args[i].startswith('--cwd'):
532 del args[i:i+2]
535 del args[i:i+2]
533 break
536 break
534 pid = os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0),
537 pid = os.spawnvp(os.P_NOWAIT | getattr(os, 'P_DETACH', 0),
535 args[0], args)
538 args[0], args)
536 os.close(wfd)
539 os.close(wfd)
537 os.read(rfd, 1)
540 os.read(rfd, 1)
538 if parentfn:
541 if parentfn:
539 return parentfn(pid)
542 return parentfn(pid)
540 else:
543 else:
541 os._exit(0)
544 os._exit(0)
542
545
543 if initfn:
546 if initfn:
544 initfn()
547 initfn()
545
548
546 if opts['pid_file']:
549 if opts['pid_file']:
547 fp = open(opts['pid_file'], 'w')
550 fp = open(opts['pid_file'], 'w')
548 fp.write(str(os.getpid()) + '\n')
551 fp.write(str(os.getpid()) + '\n')
549 fp.close()
552 fp.close()
550
553
551 if opts['daemon_pipefds']:
554 if opts['daemon_pipefds']:
552 rfd, wfd = [int(x) for x in opts['daemon_pipefds'].split(',')]
555 rfd, wfd = [int(x) for x in opts['daemon_pipefds'].split(',')]
553 os.close(rfd)
556 os.close(rfd)
554 try:
557 try:
555 os.setsid()
558 os.setsid()
556 except AttributeError:
559 except AttributeError:
557 pass
560 pass
558 os.write(wfd, 'y')
561 os.write(wfd, 'y')
559 os.close(wfd)
562 os.close(wfd)
560 sys.stdout.flush()
563 sys.stdout.flush()
561 sys.stderr.flush()
564 sys.stderr.flush()
562 fd = os.open(util.nulldev, os.O_RDWR)
565 fd = os.open(util.nulldev, os.O_RDWR)
563 if fd != 0: os.dup2(fd, 0)
566 if fd != 0: os.dup2(fd, 0)
564 if fd != 1: os.dup2(fd, 1)
567 if fd != 1: os.dup2(fd, 1)
565 if fd != 2: os.dup2(fd, 2)
568 if fd != 2: os.dup2(fd, 2)
566 if fd not in (0, 1, 2): os.close(fd)
569 if fd not in (0, 1, 2): os.close(fd)
567
570
568 if runfn:
571 if runfn:
569 return runfn()
572 return runfn()
570
573
571 class changeset_printer(object):
574 class changeset_printer(object):
572 '''show changeset information when templating not requested.'''
575 '''show changeset information when templating not requested.'''
573
576
574 def __init__(self, ui, repo, patch, buffered):
577 def __init__(self, ui, repo, patch, buffered):
575 self.ui = ui
578 self.ui = ui
576 self.repo = repo
579 self.repo = repo
577 self.buffered = buffered
580 self.buffered = buffered
578 self.patch = patch
581 self.patch = patch
579 self.header = {}
582 self.header = {}
580 self.hunk = {}
583 self.hunk = {}
581 self.lastheader = None
584 self.lastheader = None
582
585
583 def flush(self, rev):
586 def flush(self, rev):
584 if rev in self.header:
587 if rev in self.header:
585 h = self.header[rev]
588 h = self.header[rev]
586 if h != self.lastheader:
589 if h != self.lastheader:
587 self.lastheader = h
590 self.lastheader = h
588 self.ui.write(h)
591 self.ui.write(h)
589 del self.header[rev]
592 del self.header[rev]
590 if rev in self.hunk:
593 if rev in self.hunk:
591 self.ui.write(self.hunk[rev])
594 self.ui.write(self.hunk[rev])
592 del self.hunk[rev]
595 del self.hunk[rev]
593 return 1
596 return 1
594 return 0
597 return 0
595
598
596 def show(self, rev=0, changenode=None, copies=(), **props):
599 def show(self, rev=0, changenode=None, copies=(), **props):
597 if self.buffered:
600 if self.buffered:
598 self.ui.pushbuffer()
601 self.ui.pushbuffer()
599 self._show(rev, changenode, copies, props)
602 self._show(rev, changenode, copies, props)
600 self.hunk[rev] = self.ui.popbuffer()
603 self.hunk[rev] = self.ui.popbuffer()
601 else:
604 else:
602 self._show(rev, changenode, copies, props)
605 self._show(rev, changenode, copies, props)
603
606
604 def _show(self, rev, changenode, copies, props):
607 def _show(self, rev, changenode, copies, props):
605 '''show a single changeset or file revision'''
608 '''show a single changeset or file revision'''
606 log = self.repo.changelog
609 log = self.repo.changelog
607 if changenode is None:
610 if changenode is None:
608 changenode = log.node(rev)
611 changenode = log.node(rev)
609 elif not rev:
612 elif not rev:
610 rev = log.rev(changenode)
613 rev = log.rev(changenode)
611
614
612 if self.ui.quiet:
615 if self.ui.quiet:
613 self.ui.write("%d:%s\n" % (rev, short(changenode)))
616 self.ui.write("%d:%s\n" % (rev, short(changenode)))
614 return
617 return
615
618
616 changes = log.read(changenode)
619 changes = log.read(changenode)
617 date = util.datestr(changes[2])
620 date = util.datestr(changes[2])
618 extra = changes[5]
621 extra = changes[5]
619 branch = extra.get("branch")
622 branch = extra.get("branch")
620
623
621 hexfunc = self.ui.debugflag and hex or short
624 hexfunc = self.ui.debugflag and hex or short
622
625
623 parents = [(p, hexfunc(log.node(p)))
626 parents = [(p, hexfunc(log.node(p)))
624 for p in self._meaningful_parentrevs(log, rev)]
627 for p in self._meaningful_parentrevs(log, rev)]
625
628
626 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)))
629 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)))
627
630
628 # don't show the default branch name
631 # don't show the default branch name
629 if branch != 'default':
632 if branch != 'default':
630 branch = util.tolocal(branch)
633 branch = util.tolocal(branch)
631 self.ui.write(_("branch: %s\n") % branch)
634 self.ui.write(_("branch: %s\n") % branch)
632 for tag in self.repo.nodetags(changenode):
635 for tag in self.repo.nodetags(changenode):
633 self.ui.write(_("tag: %s\n") % tag)
636 self.ui.write(_("tag: %s\n") % tag)
634 for parent in parents:
637 for parent in parents:
635 self.ui.write(_("parent: %d:%s\n") % parent)
638 self.ui.write(_("parent: %d:%s\n") % parent)
636
639
637 if self.ui.debugflag:
640 if self.ui.debugflag:
638 self.ui.write(_("manifest: %d:%s\n") %
641 self.ui.write(_("manifest: %d:%s\n") %
639 (self.repo.manifest.rev(changes[0]), hex(changes[0])))
642 (self.repo.manifest.rev(changes[0]), hex(changes[0])))
640 self.ui.write(_("user: %s\n") % changes[1])
643 self.ui.write(_("user: %s\n") % changes[1])
641 self.ui.write(_("date: %s\n") % date)
644 self.ui.write(_("date: %s\n") % date)
642
645
643 if self.ui.debugflag:
646 if self.ui.debugflag:
644 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
647 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
645 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
648 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
646 files):
649 files):
647 if value:
650 if value:
648 self.ui.write("%-12s %s\n" % (key, " ".join(value)))
651 self.ui.write("%-12s %s\n" % (key, " ".join(value)))
649 elif changes[3] and self.ui.verbose:
652 elif changes[3] and self.ui.verbose:
650 self.ui.write(_("files: %s\n") % " ".join(changes[3]))
653 self.ui.write(_("files: %s\n") % " ".join(changes[3]))
651 if copies and self.ui.verbose:
654 if copies and self.ui.verbose:
652 copies = ['%s (%s)' % c for c in copies]
655 copies = ['%s (%s)' % c for c in copies]
653 self.ui.write(_("copies: %s\n") % ' '.join(copies))
656 self.ui.write(_("copies: %s\n") % ' '.join(copies))
654
657
655 if extra and self.ui.debugflag:
658 if extra and self.ui.debugflag:
656 for key, value in util.sort(extra.items()):
659 for key, value in util.sort(extra.items()):
657 self.ui.write(_("extra: %s=%s\n")
660 self.ui.write(_("extra: %s=%s\n")
658 % (key, value.encode('string_escape')))
661 % (key, value.encode('string_escape')))
659
662
660 description = changes[4].strip()
663 description = changes[4].strip()
661 if description:
664 if description:
662 if self.ui.verbose:
665 if self.ui.verbose:
663 self.ui.write(_("description:\n"))
666 self.ui.write(_("description:\n"))
664 self.ui.write(description)
667 self.ui.write(description)
665 self.ui.write("\n\n")
668 self.ui.write("\n\n")
666 else:
669 else:
667 self.ui.write(_("summary: %s\n") %
670 self.ui.write(_("summary: %s\n") %
668 description.splitlines()[0])
671 description.splitlines()[0])
669 self.ui.write("\n")
672 self.ui.write("\n")
670
673
671 self.showpatch(changenode)
674 self.showpatch(changenode)
672
675
673 def showpatch(self, node):
676 def showpatch(self, node):
674 if self.patch:
677 if self.patch:
675 prev = self.repo.changelog.parents(node)[0]
678 prev = self.repo.changelog.parents(node)[0]
676 chunks = patch.diff(self.repo, prev, node, match=self.patch,
679 chunks = patch.diff(self.repo, prev, node, match=self.patch,
677 opts=patch.diffopts(self.ui))
680 opts=patch.diffopts(self.ui))
678 for chunk in chunks:
681 for chunk in chunks:
679 self.ui.write(chunk)
682 self.ui.write(chunk)
680 self.ui.write("\n")
683 self.ui.write("\n")
681
684
682 def _meaningful_parentrevs(self, log, rev):
685 def _meaningful_parentrevs(self, log, rev):
683 """Return list of meaningful (or all if debug) parentrevs for rev.
686 """Return list of meaningful (or all if debug) parentrevs for rev.
684
687
685 For merges (two non-nullrev revisions) both parents are meaningful.
688 For merges (two non-nullrev revisions) both parents are meaningful.
686 Otherwise the first parent revision is considered meaningful if it
689 Otherwise the first parent revision is considered meaningful if it
687 is not the preceding revision.
690 is not the preceding revision.
688 """
691 """
689 parents = log.parentrevs(rev)
692 parents = log.parentrevs(rev)
690 if not self.ui.debugflag and parents[1] == nullrev:
693 if not self.ui.debugflag and parents[1] == nullrev:
691 if parents[0] >= rev - 1:
694 if parents[0] >= rev - 1:
692 parents = []
695 parents = []
693 else:
696 else:
694 parents = [parents[0]]
697 parents = [parents[0]]
695 return parents
698 return parents
696
699
697
700
698 class changeset_templater(changeset_printer):
701 class changeset_templater(changeset_printer):
699 '''format changeset information.'''
702 '''format changeset information.'''
700
703
701 def __init__(self, ui, repo, patch, mapfile, buffered):
704 def __init__(self, ui, repo, patch, mapfile, buffered):
702 changeset_printer.__init__(self, ui, repo, patch, buffered)
705 changeset_printer.__init__(self, ui, repo, patch, buffered)
703 filters = templatefilters.filters.copy()
706 filters = templatefilters.filters.copy()
704 filters['formatnode'] = (ui.debugflag and (lambda x: x)
707 filters['formatnode'] = (ui.debugflag and (lambda x: x)
705 or (lambda x: x[:12]))
708 or (lambda x: x[:12]))
706 self.t = templater.templater(mapfile, filters,
709 self.t = templater.templater(mapfile, filters,
707 cache={
710 cache={
708 'parent': '{rev}:{node|formatnode} ',
711 'parent': '{rev}:{node|formatnode} ',
709 'manifest': '{rev}:{node|formatnode}',
712 'manifest': '{rev}:{node|formatnode}',
710 'filecopy': '{name} ({source})'})
713 'filecopy': '{name} ({source})'})
711
714
712 def use_template(self, t):
715 def use_template(self, t):
713 '''set template string to use'''
716 '''set template string to use'''
714 self.t.cache['changeset'] = t
717 self.t.cache['changeset'] = t
715
718
716 def _show(self, rev, changenode, copies, props):
719 def _show(self, rev, changenode, copies, props):
717 '''show a single changeset or file revision'''
720 '''show a single changeset or file revision'''
718 log = self.repo.changelog
721 log = self.repo.changelog
719 if changenode is None:
722 if changenode is None:
720 changenode = log.node(rev)
723 changenode = log.node(rev)
721 elif not rev:
724 elif not rev:
722 rev = log.rev(changenode)
725 rev = log.rev(changenode)
723
726
724 changes = log.read(changenode)
727 changes = log.read(changenode)
725
728
726 def showlist(name, values, plural=None, **args):
729 def showlist(name, values, plural=None, **args):
727 '''expand set of values.
730 '''expand set of values.
728 name is name of key in template map.
731 name is name of key in template map.
729 values is list of strings or dicts.
732 values is list of strings or dicts.
730 plural is plural of name, if not simply name + 's'.
733 plural is plural of name, if not simply name + 's'.
731
734
732 expansion works like this, given name 'foo'.
735 expansion works like this, given name 'foo'.
733
736
734 if values is empty, expand 'no_foos'.
737 if values is empty, expand 'no_foos'.
735
738
736 if 'foo' not in template map, return values as a string,
739 if 'foo' not in template map, return values as a string,
737 joined by space.
740 joined by space.
738
741
739 expand 'start_foos'.
742 expand 'start_foos'.
740
743
741 for each value, expand 'foo'. if 'last_foo' in template
744 for each value, expand 'foo'. if 'last_foo' in template
742 map, expand it instead of 'foo' for last key.
745 map, expand it instead of 'foo' for last key.
743
746
744 expand 'end_foos'.
747 expand 'end_foos'.
745 '''
748 '''
746 if plural: names = plural
749 if plural: names = plural
747 else: names = name + 's'
750 else: names = name + 's'
748 if not values:
751 if not values:
749 noname = 'no_' + names
752 noname = 'no_' + names
750 if noname in self.t:
753 if noname in self.t:
751 yield self.t(noname, **args)
754 yield self.t(noname, **args)
752 return
755 return
753 if name not in self.t:
756 if name not in self.t:
754 if isinstance(values[0], str):
757 if isinstance(values[0], str):
755 yield ' '.join(values)
758 yield ' '.join(values)
756 else:
759 else:
757 for v in values:
760 for v in values:
758 yield dict(v, **args)
761 yield dict(v, **args)
759 return
762 return
760 startname = 'start_' + names
763 startname = 'start_' + names
761 if startname in self.t:
764 if startname in self.t:
762 yield self.t(startname, **args)
765 yield self.t(startname, **args)
763 vargs = args.copy()
766 vargs = args.copy()
764 def one(v, tag=name):
767 def one(v, tag=name):
765 try:
768 try:
766 vargs.update(v)
769 vargs.update(v)
767 except (AttributeError, ValueError):
770 except (AttributeError, ValueError):
768 try:
771 try:
769 for a, b in v:
772 for a, b in v:
770 vargs[a] = b
773 vargs[a] = b
771 except ValueError:
774 except ValueError:
772 vargs[name] = v
775 vargs[name] = v
773 return self.t(tag, **vargs)
776 return self.t(tag, **vargs)
774 lastname = 'last_' + name
777 lastname = 'last_' + name
775 if lastname in self.t:
778 if lastname in self.t:
776 last = values.pop()
779 last = values.pop()
777 else:
780 else:
778 last = None
781 last = None
779 for v in values:
782 for v in values:
780 yield one(v)
783 yield one(v)
781 if last is not None:
784 if last is not None:
782 yield one(last, tag=lastname)
785 yield one(last, tag=lastname)
783 endname = 'end_' + names
786 endname = 'end_' + names
784 if endname in self.t:
787 if endname in self.t:
785 yield self.t(endname, **args)
788 yield self.t(endname, **args)
786
789
787 def showbranches(**args):
790 def showbranches(**args):
788 branch = changes[5].get("branch")
791 branch = changes[5].get("branch")
789 if branch != 'default':
792 if branch != 'default':
790 branch = util.tolocal(branch)
793 branch = util.tolocal(branch)
791 return showlist('branch', [branch], plural='branches', **args)
794 return showlist('branch', [branch], plural='branches', **args)
792
795
793 def showparents(**args):
796 def showparents(**args):
794 parents = [[('rev', p), ('node', hex(log.node(p)))]
797 parents = [[('rev', p), ('node', hex(log.node(p)))]
795 for p in self._meaningful_parentrevs(log, rev)]
798 for p in self._meaningful_parentrevs(log, rev)]
796 return showlist('parent', parents, **args)
799 return showlist('parent', parents, **args)
797
800
798 def showtags(**args):
801 def showtags(**args):
799 return showlist('tag', self.repo.nodetags(changenode), **args)
802 return showlist('tag', self.repo.nodetags(changenode), **args)
800
803
801 def showextras(**args):
804 def showextras(**args):
802 for key, value in util.sort(changes[5].items()):
805 for key, value in util.sort(changes[5].items()):
803 args = args.copy()
806 args = args.copy()
804 args.update(dict(key=key, value=value))
807 args.update(dict(key=key, value=value))
805 yield self.t('extra', **args)
808 yield self.t('extra', **args)
806
809
807 def showcopies(**args):
810 def showcopies(**args):
808 c = [{'name': x[0], 'source': x[1]} for x in copies]
811 c = [{'name': x[0], 'source': x[1]} for x in copies]
809 return showlist('file_copy', c, plural='file_copies', **args)
812 return showlist('file_copy', c, plural='file_copies', **args)
810
813
811 files = []
814 files = []
812 def getfiles():
815 def getfiles():
813 if not files:
816 if not files:
814 files[:] = self.repo.status(
817 files[:] = self.repo.status(
815 log.parents(changenode)[0], changenode)[:3]
818 log.parents(changenode)[0], changenode)[:3]
816 return files
819 return files
817 def showfiles(**args):
820 def showfiles(**args):
818 return showlist('file', changes[3], **args)
821 return showlist('file', changes[3], **args)
819 def showmods(**args):
822 def showmods(**args):
820 return showlist('file_mod', getfiles()[0], **args)
823 return showlist('file_mod', getfiles()[0], **args)
821 def showadds(**args):
824 def showadds(**args):
822 return showlist('file_add', getfiles()[1], **args)
825 return showlist('file_add', getfiles()[1], **args)
823 def showdels(**args):
826 def showdels(**args):
824 return showlist('file_del', getfiles()[2], **args)
827 return showlist('file_del', getfiles()[2], **args)
825 def showmanifest(**args):
828 def showmanifest(**args):
826 args = args.copy()
829 args = args.copy()
827 args.update(dict(rev=self.repo.manifest.rev(changes[0]),
830 args.update(dict(rev=self.repo.manifest.rev(changes[0]),
828 node=hex(changes[0])))
831 node=hex(changes[0])))
829 return self.t('manifest', **args)
832 return self.t('manifest', **args)
830
833
831 defprops = {
834 defprops = {
832 'author': changes[1],
835 'author': changes[1],
833 'branches': showbranches,
836 'branches': showbranches,
834 'date': changes[2],
837 'date': changes[2],
835 'desc': changes[4].strip(),
838 'desc': changes[4].strip(),
836 'file_adds': showadds,
839 'file_adds': showadds,
837 'file_dels': showdels,
840 'file_dels': showdels,
838 'file_mods': showmods,
841 'file_mods': showmods,
839 'files': showfiles,
842 'files': showfiles,
840 'file_copies': showcopies,
843 'file_copies': showcopies,
841 'manifest': showmanifest,
844 'manifest': showmanifest,
842 'node': hex(changenode),
845 'node': hex(changenode),
843 'parents': showparents,
846 'parents': showparents,
844 'rev': rev,
847 'rev': rev,
845 'tags': showtags,
848 'tags': showtags,
846 'extras': showextras,
849 'extras': showextras,
847 }
850 }
848 props = props.copy()
851 props = props.copy()
849 props.update(defprops)
852 props.update(defprops)
850
853
851 try:
854 try:
852 if self.ui.debugflag and 'header_debug' in self.t:
855 if self.ui.debugflag and 'header_debug' in self.t:
853 key = 'header_debug'
856 key = 'header_debug'
854 elif self.ui.quiet and 'header_quiet' in self.t:
857 elif self.ui.quiet and 'header_quiet' in self.t:
855 key = 'header_quiet'
858 key = 'header_quiet'
856 elif self.ui.verbose and 'header_verbose' in self.t:
859 elif self.ui.verbose and 'header_verbose' in self.t:
857 key = 'header_verbose'
860 key = 'header_verbose'
858 elif 'header' in self.t:
861 elif 'header' in self.t:
859 key = 'header'
862 key = 'header'
860 else:
863 else:
861 key = ''
864 key = ''
862 if key:
865 if key:
863 h = templater.stringify(self.t(key, **props))
866 h = templater.stringify(self.t(key, **props))
864 if self.buffered:
867 if self.buffered:
865 self.header[rev] = h
868 self.header[rev] = h
866 else:
869 else:
867 self.ui.write(h)
870 self.ui.write(h)
868 if self.ui.debugflag and 'changeset_debug' in self.t:
871 if self.ui.debugflag and 'changeset_debug' in self.t:
869 key = 'changeset_debug'
872 key = 'changeset_debug'
870 elif self.ui.quiet and 'changeset_quiet' in self.t:
873 elif self.ui.quiet and 'changeset_quiet' in self.t:
871 key = 'changeset_quiet'
874 key = 'changeset_quiet'
872 elif self.ui.verbose and 'changeset_verbose' in self.t:
875 elif self.ui.verbose and 'changeset_verbose' in self.t:
873 key = 'changeset_verbose'
876 key = 'changeset_verbose'
874 else:
877 else:
875 key = 'changeset'
878 key = 'changeset'
876 self.ui.write(templater.stringify(self.t(key, **props)))
879 self.ui.write(templater.stringify(self.t(key, **props)))
877 self.showpatch(changenode)
880 self.showpatch(changenode)
878 except KeyError, inst:
881 except KeyError, inst:
879 raise util.Abort(_("%s: no key named '%s'") % (self.t.mapfile,
882 raise util.Abort(_("%s: no key named '%s'") % (self.t.mapfile,
880 inst.args[0]))
883 inst.args[0]))
881 except SyntaxError, inst:
884 except SyntaxError, inst:
882 raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0]))
885 raise util.Abort(_('%s: %s') % (self.t.mapfile, inst.args[0]))
883
886
884 def show_changeset(ui, repo, opts, buffered=False, matchfn=False):
887 def show_changeset(ui, repo, opts, buffered=False, matchfn=False):
885 """show one changeset using template or regular display.
888 """show one changeset using template or regular display.
886
889
887 Display format will be the first non-empty hit of:
890 Display format will be the first non-empty hit of:
888 1. option 'template'
891 1. option 'template'
889 2. option 'style'
892 2. option 'style'
890 3. [ui] setting 'logtemplate'
893 3. [ui] setting 'logtemplate'
891 4. [ui] setting 'style'
894 4. [ui] setting 'style'
892 If all of these values are either the unset or the empty string,
895 If all of these values are either the unset or the empty string,
893 regular display via changeset_printer() is done.
896 regular display via changeset_printer() is done.
894 """
897 """
895 # options
898 # options
896 patch = False
899 patch = False
897 if opts.get('patch'):
900 if opts.get('patch'):
898 patch = matchfn or matchall(repo)
901 patch = matchfn or matchall(repo)
899
902
900 tmpl = opts.get('template')
903 tmpl = opts.get('template')
901 mapfile = None
904 mapfile = None
902 if tmpl:
905 if tmpl:
903 tmpl = templater.parsestring(tmpl, quoted=False)
906 tmpl = templater.parsestring(tmpl, quoted=False)
904 else:
907 else:
905 mapfile = opts.get('style')
908 mapfile = opts.get('style')
906 # ui settings
909 # ui settings
907 if not mapfile:
910 if not mapfile:
908 tmpl = ui.config('ui', 'logtemplate')
911 tmpl = ui.config('ui', 'logtemplate')
909 if tmpl:
912 if tmpl:
910 tmpl = templater.parsestring(tmpl)
913 tmpl = templater.parsestring(tmpl)
911 else:
914 else:
912 mapfile = ui.config('ui', 'style')
915 mapfile = ui.config('ui', 'style')
913
916
914 if tmpl or mapfile:
917 if tmpl or mapfile:
915 if mapfile:
918 if mapfile:
916 if not os.path.split(mapfile)[0]:
919 if not os.path.split(mapfile)[0]:
917 mapname = (templater.templatepath('map-cmdline.' + mapfile)
920 mapname = (templater.templatepath('map-cmdline.' + mapfile)
918 or templater.templatepath(mapfile))
921 or templater.templatepath(mapfile))
919 if mapname: mapfile = mapname
922 if mapname: mapfile = mapname
920 try:
923 try:
921 t = changeset_templater(ui, repo, patch, mapfile, buffered)
924 t = changeset_templater(ui, repo, patch, mapfile, buffered)
922 except SyntaxError, inst:
925 except SyntaxError, inst:
923 raise util.Abort(inst.args[0])
926 raise util.Abort(inst.args[0])
924 if tmpl: t.use_template(tmpl)
927 if tmpl: t.use_template(tmpl)
925 return t
928 return t
926 return changeset_printer(ui, repo, patch, buffered)
929 return changeset_printer(ui, repo, patch, buffered)
927
930
928 def finddate(ui, repo, date):
931 def finddate(ui, repo, date):
929 """Find the tipmost changeset that matches the given date spec"""
932 """Find the tipmost changeset that matches the given date spec"""
930 df = util.matchdate(date)
933 df = util.matchdate(date)
931 get = util.cachefunc(lambda r: repo[r].changeset())
934 get = util.cachefunc(lambda r: repo[r].changeset())
932 changeiter, matchfn = walkchangerevs(ui, repo, [], get, {'rev':None})
935 changeiter, matchfn = walkchangerevs(ui, repo, [], get, {'rev':None})
933 results = {}
936 results = {}
934 for st, rev, fns in changeiter:
937 for st, rev, fns in changeiter:
935 if st == 'add':
938 if st == 'add':
936 d = get(rev)[2]
939 d = get(rev)[2]
937 if df(d[0]):
940 if df(d[0]):
938 results[rev] = d
941 results[rev] = d
939 elif st == 'iter':
942 elif st == 'iter':
940 if rev in results:
943 if rev in results:
941 ui.status(_("Found revision %s from %s\n") %
944 ui.status(_("Found revision %s from %s\n") %
942 (rev, util.datestr(results[rev])))
945 (rev, util.datestr(results[rev])))
943 return str(rev)
946 return str(rev)
944
947
945 raise util.Abort(_("revision matching date not found"))
948 raise util.Abort(_("revision matching date not found"))
946
949
947 def walkchangerevs(ui, repo, pats, change, opts):
950 def walkchangerevs(ui, repo, pats, change, opts):
948 '''Iterate over files and the revs they changed in.
951 '''Iterate over files and the revs they changed in.
949
952
950 Callers most commonly need to iterate backwards over the history
953 Callers most commonly need to iterate backwards over the history
951 it is interested in. Doing so has awful (quadratic-looking)
954 it is interested in. Doing so has awful (quadratic-looking)
952 performance, so we use iterators in a "windowed" way.
955 performance, so we use iterators in a "windowed" way.
953
956
954 We walk a window of revisions in the desired order. Within the
957 We walk a window of revisions in the desired order. Within the
955 window, we first walk forwards to gather data, then in the desired
958 window, we first walk forwards to gather data, then in the desired
956 order (usually backwards) to display it.
959 order (usually backwards) to display it.
957
960
958 This function returns an (iterator, matchfn) tuple. The iterator
961 This function returns an (iterator, matchfn) tuple. The iterator
959 yields 3-tuples. They will be of one of the following forms:
962 yields 3-tuples. They will be of one of the following forms:
960
963
961 "window", incrementing, lastrev: stepping through a window,
964 "window", incrementing, lastrev: stepping through a window,
962 positive if walking forwards through revs, last rev in the
965 positive if walking forwards through revs, last rev in the
963 sequence iterated over - use to reset state for the current window
966 sequence iterated over - use to reset state for the current window
964
967
965 "add", rev, fns: out-of-order traversal of the given file names
968 "add", rev, fns: out-of-order traversal of the given file names
966 fns, which changed during revision rev - use to gather data for
969 fns, which changed during revision rev - use to gather data for
967 possible display
970 possible display
968
971
969 "iter", rev, None: in-order traversal of the revs earlier iterated
972 "iter", rev, None: in-order traversal of the revs earlier iterated
970 over with "add" - use to display data'''
973 over with "add" - use to display data'''
971
974
972 def increasing_windows(start, end, windowsize=8, sizelimit=512):
975 def increasing_windows(start, end, windowsize=8, sizelimit=512):
973 if start < end:
976 if start < end:
974 while start < end:
977 while start < end:
975 yield start, min(windowsize, end-start)
978 yield start, min(windowsize, end-start)
976 start += windowsize
979 start += windowsize
977 if windowsize < sizelimit:
980 if windowsize < sizelimit:
978 windowsize *= 2
981 windowsize *= 2
979 else:
982 else:
980 while start > end:
983 while start > end:
981 yield start, min(windowsize, start-end-1)
984 yield start, min(windowsize, start-end-1)
982 start -= windowsize
985 start -= windowsize
983 if windowsize < sizelimit:
986 if windowsize < sizelimit:
984 windowsize *= 2
987 windowsize *= 2
985
988
986 m = match(repo, pats, opts)
989 m = match(repo, pats, opts)
987 follow = opts.get('follow') or opts.get('follow_first')
990 follow = opts.get('follow') or opts.get('follow_first')
988
991
989 if not len(repo):
992 if not len(repo):
990 return [], m
993 return [], m
991
994
992 if follow:
995 if follow:
993 defrange = '%s:0' % repo['.'].rev()
996 defrange = '%s:0' % repo['.'].rev()
994 else:
997 else:
995 defrange = '-1:0'
998 defrange = '-1:0'
996 revs = revrange(repo, opts['rev'] or [defrange])
999 revs = revrange(repo, opts['rev'] or [defrange])
997 wanted = {}
1000 wanted = {}
998 slowpath = m.anypats() or opts.get('removed')
1001 slowpath = m.anypats() or opts.get('removed')
999 fncache = {}
1002 fncache = {}
1000
1003
1001 if not slowpath and not m.files():
1004 if not slowpath and not m.files():
1002 # No files, no patterns. Display all revs.
1005 # No files, no patterns. Display all revs.
1003 wanted = dict.fromkeys(revs)
1006 wanted = dict.fromkeys(revs)
1004 copies = []
1007 copies = []
1005 if not slowpath:
1008 if not slowpath:
1006 # Only files, no patterns. Check the history of each file.
1009 # Only files, no patterns. Check the history of each file.
1007 def filerevgen(filelog, node):
1010 def filerevgen(filelog, node):
1008 cl_count = len(repo)
1011 cl_count = len(repo)
1009 if node is None:
1012 if node is None:
1010 last = len(filelog) - 1
1013 last = len(filelog) - 1
1011 else:
1014 else:
1012 last = filelog.rev(node)
1015 last = filelog.rev(node)
1013 for i, window in increasing_windows(last, nullrev):
1016 for i, window in increasing_windows(last, nullrev):
1014 revs = []
1017 revs = []
1015 for j in xrange(i - window, i + 1):
1018 for j in xrange(i - window, i + 1):
1016 n = filelog.node(j)
1019 n = filelog.node(j)
1017 revs.append((filelog.linkrev(n),
1020 revs.append((filelog.linkrev(n),
1018 follow and filelog.renamed(n)))
1021 follow and filelog.renamed(n)))
1019 revs.reverse()
1022 revs.reverse()
1020 for rev in revs:
1023 for rev in revs:
1021 # only yield rev for which we have the changelog, it can
1024 # only yield rev for which we have the changelog, it can
1022 # happen while doing "hg log" during a pull or commit
1025 # happen while doing "hg log" during a pull or commit
1023 if rev[0] < cl_count:
1026 if rev[0] < cl_count:
1024 yield rev
1027 yield rev
1025 def iterfiles():
1028 def iterfiles():
1026 for filename in m.files():
1029 for filename in m.files():
1027 yield filename, None
1030 yield filename, None
1028 for filename_node in copies:
1031 for filename_node in copies:
1029 yield filename_node
1032 yield filename_node
1030 minrev, maxrev = min(revs), max(revs)
1033 minrev, maxrev = min(revs), max(revs)
1031 for file_, node in iterfiles():
1034 for file_, node in iterfiles():
1032 filelog = repo.file(file_)
1035 filelog = repo.file(file_)
1033 if not len(filelog):
1036 if not len(filelog):
1034 if node is None:
1037 if node is None:
1035 # A zero count may be a directory or deleted file, so
1038 # A zero count may be a directory or deleted file, so
1036 # try to find matching entries on the slow path.
1039 # try to find matching entries on the slow path.
1037 slowpath = True
1040 slowpath = True
1038 break
1041 break
1039 else:
1042 else:
1040 ui.warn(_('%s:%s copy source revision cannot be found!\n')
1043 ui.warn(_('%s:%s copy source revision cannot be found!\n')
1041 % (file_, short(node)))
1044 % (file_, short(node)))
1042 continue
1045 continue
1043 for rev, copied in filerevgen(filelog, node):
1046 for rev, copied in filerevgen(filelog, node):
1044 if rev <= maxrev:
1047 if rev <= maxrev:
1045 if rev < minrev:
1048 if rev < minrev:
1046 break
1049 break
1047 fncache.setdefault(rev, [])
1050 fncache.setdefault(rev, [])
1048 fncache[rev].append(file_)
1051 fncache[rev].append(file_)
1049 wanted[rev] = 1
1052 wanted[rev] = 1
1050 if follow and copied:
1053 if follow and copied:
1051 copies.append(copied)
1054 copies.append(copied)
1052 if slowpath:
1055 if slowpath:
1053 if follow:
1056 if follow:
1054 raise util.Abort(_('can only follow copies/renames for explicit '
1057 raise util.Abort(_('can only follow copies/renames for explicit '
1055 'file names'))
1058 'file names'))
1056
1059
1057 # The slow path checks files modified in every changeset.
1060 # The slow path checks files modified in every changeset.
1058 def changerevgen():
1061 def changerevgen():
1059 for i, window in increasing_windows(len(repo) - 1, nullrev):
1062 for i, window in increasing_windows(len(repo) - 1, nullrev):
1060 for j in xrange(i - window, i + 1):
1063 for j in xrange(i - window, i + 1):
1061 yield j, change(j)[3]
1064 yield j, change(j)[3]
1062
1065
1063 for rev, changefiles in changerevgen():
1066 for rev, changefiles in changerevgen():
1064 matches = filter(m, changefiles)
1067 matches = filter(m, changefiles)
1065 if matches:
1068 if matches:
1066 fncache[rev] = matches
1069 fncache[rev] = matches
1067 wanted[rev] = 1
1070 wanted[rev] = 1
1068
1071
1069 class followfilter:
1072 class followfilter:
1070 def __init__(self, onlyfirst=False):
1073 def __init__(self, onlyfirst=False):
1071 self.startrev = nullrev
1074 self.startrev = nullrev
1072 self.roots = []
1075 self.roots = []
1073 self.onlyfirst = onlyfirst
1076 self.onlyfirst = onlyfirst
1074
1077
1075 def match(self, rev):
1078 def match(self, rev):
1076 def realparents(rev):
1079 def realparents(rev):
1077 if self.onlyfirst:
1080 if self.onlyfirst:
1078 return repo.changelog.parentrevs(rev)[0:1]
1081 return repo.changelog.parentrevs(rev)[0:1]
1079 else:
1082 else:
1080 return filter(lambda x: x != nullrev,
1083 return filter(lambda x: x != nullrev,
1081 repo.changelog.parentrevs(rev))
1084 repo.changelog.parentrevs(rev))
1082
1085
1083 if self.startrev == nullrev:
1086 if self.startrev == nullrev:
1084 self.startrev = rev
1087 self.startrev = rev
1085 return True
1088 return True
1086
1089
1087 if rev > self.startrev:
1090 if rev > self.startrev:
1088 # forward: all descendants
1091 # forward: all descendants
1089 if not self.roots:
1092 if not self.roots:
1090 self.roots.append(self.startrev)
1093 self.roots.append(self.startrev)
1091 for parent in realparents(rev):
1094 for parent in realparents(rev):
1092 if parent in self.roots:
1095 if parent in self.roots:
1093 self.roots.append(rev)
1096 self.roots.append(rev)
1094 return True
1097 return True
1095 else:
1098 else:
1096 # backwards: all parents
1099 # backwards: all parents
1097 if not self.roots:
1100 if not self.roots:
1098 self.roots.extend(realparents(self.startrev))
1101 self.roots.extend(realparents(self.startrev))
1099 if rev in self.roots:
1102 if rev in self.roots:
1100 self.roots.remove(rev)
1103 self.roots.remove(rev)
1101 self.roots.extend(realparents(rev))
1104 self.roots.extend(realparents(rev))
1102 return True
1105 return True
1103
1106
1104 return False
1107 return False
1105
1108
1106 # it might be worthwhile to do this in the iterator if the rev range
1109 # it might be worthwhile to do this in the iterator if the rev range
1107 # is descending and the prune args are all within that range
1110 # is descending and the prune args are all within that range
1108 for rev in opts.get('prune', ()):
1111 for rev in opts.get('prune', ()):
1109 rev = repo.changelog.rev(repo.lookup(rev))
1112 rev = repo.changelog.rev(repo.lookup(rev))
1110 ff = followfilter()
1113 ff = followfilter()
1111 stop = min(revs[0], revs[-1])
1114 stop = min(revs[0], revs[-1])
1112 for x in xrange(rev, stop-1, -1):
1115 for x in xrange(rev, stop-1, -1):
1113 if ff.match(x) and x in wanted:
1116 if ff.match(x) and x in wanted:
1114 del wanted[x]
1117 del wanted[x]
1115
1118
1116 def iterate():
1119 def iterate():
1117 if follow and not m.files():
1120 if follow and not m.files():
1118 ff = followfilter(onlyfirst=opts.get('follow_first'))
1121 ff = followfilter(onlyfirst=opts.get('follow_first'))
1119 def want(rev):
1122 def want(rev):
1120 if ff.match(rev) and rev in wanted:
1123 if ff.match(rev) and rev in wanted:
1121 return True
1124 return True
1122 return False
1125 return False
1123 else:
1126 else:
1124 def want(rev):
1127 def want(rev):
1125 return rev in wanted
1128 return rev in wanted
1126
1129
1127 for i, window in increasing_windows(0, len(revs)):
1130 for i, window in increasing_windows(0, len(revs)):
1128 yield 'window', revs[0] < revs[-1], revs[-1]
1131 yield 'window', revs[0] < revs[-1], revs[-1]
1129 nrevs = [rev for rev in revs[i:i+window] if want(rev)]
1132 nrevs = [rev for rev in revs[i:i+window] if want(rev)]
1130 for rev in util.sort(list(nrevs)):
1133 for rev in util.sort(list(nrevs)):
1131 fns = fncache.get(rev)
1134 fns = fncache.get(rev)
1132 if not fns:
1135 if not fns:
1133 def fns_generator():
1136 def fns_generator():
1134 for f in change(rev)[3]:
1137 for f in change(rev)[3]:
1135 if m(f):
1138 if m(f):
1136 yield f
1139 yield f
1137 fns = fns_generator()
1140 fns = fns_generator()
1138 yield 'add', rev, fns
1141 yield 'add', rev, fns
1139 for rev in nrevs:
1142 for rev in nrevs:
1140 yield 'iter', rev, None
1143 yield 'iter', rev, None
1141 return iterate(), m
1144 return iterate(), m
1142
1145
1143 def commit(ui, repo, commitfunc, pats, opts):
1146 def commit(ui, repo, commitfunc, pats, opts):
1144 '''commit the specified files or all outstanding changes'''
1147 '''commit the specified files or all outstanding changes'''
1145 date = opts.get('date')
1148 date = opts.get('date')
1146 if date:
1149 if date:
1147 opts['date'] = util.parsedate(date)
1150 opts['date'] = util.parsedate(date)
1148 message = logmessage(opts)
1151 message = logmessage(opts)
1149
1152
1150 # extract addremove carefully -- this function can be called from a command
1153 # extract addremove carefully -- this function can be called from a command
1151 # that doesn't support addremove
1154 # that doesn't support addremove
1152 if opts.get('addremove'):
1155 if opts.get('addremove'):
1153 addremove(repo, pats, opts)
1156 addremove(repo, pats, opts)
1154
1157
1155 m = match(repo, pats, opts)
1158 m = match(repo, pats, opts)
1156 if pats:
1159 if pats:
1157 modified, added, removed = repo.status(match=m)[:3]
1160 modified, added, removed = repo.status(match=m)[:3]
1158 files = util.sort(modified + added + removed)
1161 files = util.sort(modified + added + removed)
1159
1162
1160 def is_dir(f):
1163 def is_dir(f):
1161 name = f + '/'
1164 name = f + '/'
1162 i = bisect.bisect(files, name)
1165 i = bisect.bisect(files, name)
1163 return i < len(files) and files[i].startswith(name)
1166 return i < len(files) and files[i].startswith(name)
1164
1167
1165 for f in m.files():
1168 for f in m.files():
1166 if f == '.':
1169 if f == '.':
1167 continue
1170 continue
1168 if f not in files:
1171 if f not in files:
1169 rf = repo.wjoin(f)
1172 rf = repo.wjoin(f)
1170 rel = repo.pathto(f)
1173 rel = repo.pathto(f)
1171 try:
1174 try:
1172 mode = os.lstat(rf)[stat.ST_MODE]
1175 mode = os.lstat(rf)[stat.ST_MODE]
1173 except OSError:
1176 except OSError:
1174 if is_dir(f): # deleted directory ?
1177 if is_dir(f): # deleted directory ?
1175 continue
1178 continue
1176 raise util.Abort(_("file %s not found!") % rel)
1179 raise util.Abort(_("file %s not found!") % rel)
1177 if stat.S_ISDIR(mode):
1180 if stat.S_ISDIR(mode):
1178 if not is_dir(f):
1181 if not is_dir(f):
1179 raise util.Abort(_("no match under directory %s!")
1182 raise util.Abort(_("no match under directory %s!")
1180 % rel)
1183 % rel)
1181 elif not (stat.S_ISREG(mode) or stat.S_ISLNK(mode)):
1184 elif not (stat.S_ISREG(mode) or stat.S_ISLNK(mode)):
1182 raise util.Abort(_("can't commit %s: "
1185 raise util.Abort(_("can't commit %s: "
1183 "unsupported file type!") % rel)
1186 "unsupported file type!") % rel)
1184 elif f not in repo.dirstate:
1187 elif f not in repo.dirstate:
1185 raise util.Abort(_("file %s not tracked!") % rel)
1188 raise util.Abort(_("file %s not tracked!") % rel)
1186 m = matchfiles(repo, files)
1189 m = matchfiles(repo, files)
1187 try:
1190 try:
1188 return commitfunc(ui, repo, message, m, opts)
1191 return commitfunc(ui, repo, message, m, opts)
1189 except ValueError, inst:
1192 except ValueError, inst:
1190 raise util.Abort(str(inst))
1193 raise util.Abort(str(inst))
@@ -1,1330 +1,1331 b''
1 # patch.py - patch file parsing routines
1 # patch.py - patch file parsing routines
2 #
2 #
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 #
5 #
6 # This software may be used and distributed according to the terms
6 # This software may be used and distributed according to the terms
7 # of the GNU General Public License, incorporated herein by reference.
7 # of the GNU General Public License, incorporated herein by reference.
8
8
9 from i18n import _
9 from i18n import _
10 from node import hex, nullid, short
10 from node import hex, nullid, short
11 import base85, cmdutil, mdiff, util, revlog, diffhelpers, copies
11 import base85, cmdutil, mdiff, util, revlog, diffhelpers, copies
12 import cStringIO, email.Parser, os, re, errno
12 import cStringIO, email.Parser, os, re, errno
13 import sys, tempfile, zlib
13 import sys, tempfile, zlib
14
14
15 gitre = re.compile('diff --git a/(.*) b/(.*)')
15 gitre = re.compile('diff --git a/(.*) b/(.*)')
16
16
17 class PatchError(Exception):
17 class PatchError(Exception):
18 pass
18 pass
19
19
20 class NoHunks(PatchError):
20 class NoHunks(PatchError):
21 pass
21 pass
22
22
23 # helper functions
23 # helper functions
24
24
25 def copyfile(src, dst, basedir=None):
25 def copyfile(src, dst, basedir=None):
26 if not basedir:
26 if not basedir:
27 basedir = os.getcwd()
27 basedir = os.getcwd()
28
28
29 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
29 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
30 if os.path.exists(absdst):
30 if os.path.exists(absdst):
31 raise util.Abort(_("cannot create %s: destination already exists") %
31 raise util.Abort(_("cannot create %s: destination already exists") %
32 dst)
32 dst)
33
33
34 if not os.path.isdir(basedir):
34 if not os.path.isdir(basedir):
35 os.makedirs(basedir)
35 os.makedirs(basedir)
36
36
37 util.copyfile(abssrc, absdst)
37 util.copyfile(abssrc, absdst)
38
38
39 # public functions
39 # public functions
40
40
41 def extract(ui, fileobj):
41 def extract(ui, fileobj):
42 '''extract patch from data read from fileobj.
42 '''extract patch from data read from fileobj.
43
43
44 patch can be a normal patch or contained in an email message.
44 patch can be a normal patch or contained in an email message.
45
45
46 return tuple (filename, message, user, date, node, p1, p2).
46 return tuple (filename, message, user, date, node, p1, p2).
47 Any item in the returned tuple can be None. If filename is None,
47 Any item in the returned tuple can be None. If filename is None,
48 fileobj did not contain a patch. Caller must unlink filename when done.'''
48 fileobj did not contain a patch. Caller must unlink filename when done.'''
49
49
50 # attempt to detect the start of a patch
50 # attempt to detect the start of a patch
51 # (this heuristic is borrowed from quilt)
51 # (this heuristic is borrowed from quilt)
52 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
52 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
53 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
53 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
54 '(---|\*\*\*)[ \t])', re.MULTILINE)
54 '(---|\*\*\*)[ \t])', re.MULTILINE)
55
55
56 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
56 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
57 tmpfp = os.fdopen(fd, 'w')
57 tmpfp = os.fdopen(fd, 'w')
58 try:
58 try:
59 msg = email.Parser.Parser().parse(fileobj)
59 msg = email.Parser.Parser().parse(fileobj)
60
60
61 subject = msg['Subject']
61 subject = msg['Subject']
62 user = msg['From']
62 user = msg['From']
63 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
63 gitsendmail = 'git-send-email' in msg.get('X-Mailer', '')
64 # should try to parse msg['Date']
64 # should try to parse msg['Date']
65 date = None
65 date = None
66 nodeid = None
66 nodeid = None
67 branch = None
67 branch = None
68 parents = []
68 parents = []
69
69
70 if subject:
70 if subject:
71 if subject.startswith('[PATCH'):
71 if subject.startswith('[PATCH'):
72 pend = subject.find(']')
72 pend = subject.find(']')
73 if pend >= 0:
73 if pend >= 0:
74 subject = subject[pend+1:].lstrip()
74 subject = subject[pend+1:].lstrip()
75 subject = subject.replace('\n\t', ' ')
75 subject = subject.replace('\n\t', ' ')
76 ui.debug('Subject: %s\n' % subject)
76 ui.debug('Subject: %s\n' % subject)
77 if user:
77 if user:
78 ui.debug('From: %s\n' % user)
78 ui.debug('From: %s\n' % user)
79 diffs_seen = 0
79 diffs_seen = 0
80 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
80 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
81 message = ''
81 message = ''
82 for part in msg.walk():
82 for part in msg.walk():
83 content_type = part.get_content_type()
83 content_type = part.get_content_type()
84 ui.debug('Content-Type: %s\n' % content_type)
84 ui.debug('Content-Type: %s\n' % content_type)
85 if content_type not in ok_types:
85 if content_type not in ok_types:
86 continue
86 continue
87 payload = part.get_payload(decode=True)
87 payload = part.get_payload(decode=True)
88 m = diffre.search(payload)
88 m = diffre.search(payload)
89 if m:
89 if m:
90 hgpatch = False
90 hgpatch = False
91 ignoretext = False
91 ignoretext = False
92
92
93 ui.debug(_('found patch at byte %d\n') % m.start(0))
93 ui.debug(_('found patch at byte %d\n') % m.start(0))
94 diffs_seen += 1
94 diffs_seen += 1
95 cfp = cStringIO.StringIO()
95 cfp = cStringIO.StringIO()
96 for line in payload[:m.start(0)].splitlines():
96 for line in payload[:m.start(0)].splitlines():
97 if line.startswith('# HG changeset patch'):
97 if line.startswith('# HG changeset patch'):
98 ui.debug(_('patch generated by hg export\n'))
98 ui.debug(_('patch generated by hg export\n'))
99 hgpatch = True
99 hgpatch = True
100 # drop earlier commit message content
100 # drop earlier commit message content
101 cfp.seek(0)
101 cfp.seek(0)
102 cfp.truncate()
102 cfp.truncate()
103 subject = None
103 subject = None
104 elif hgpatch:
104 elif hgpatch:
105 if line.startswith('# User '):
105 if line.startswith('# User '):
106 user = line[7:]
106 user = line[7:]
107 ui.debug('From: %s\n' % user)
107 ui.debug('From: %s\n' % user)
108 elif line.startswith("# Date "):
108 elif line.startswith("# Date "):
109 date = line[7:]
109 date = line[7:]
110 elif line.startswith("# Branch "):
110 elif line.startswith("# Branch "):
111 branch = line[9:]
111 branch = line[9:]
112 elif line.startswith("# Node ID "):
112 elif line.startswith("# Node ID "):
113 nodeid = line[10:]
113 nodeid = line[10:]
114 elif line.startswith("# Parent "):
114 elif line.startswith("# Parent "):
115 parents.append(line[10:])
115 parents.append(line[10:])
116 elif line == '---' and gitsendmail:
116 elif line == '---' and gitsendmail:
117 ignoretext = True
117 ignoretext = True
118 if not line.startswith('# ') and not ignoretext:
118 if not line.startswith('# ') and not ignoretext:
119 cfp.write(line)
119 cfp.write(line)
120 cfp.write('\n')
120 cfp.write('\n')
121 message = cfp.getvalue()
121 message = cfp.getvalue()
122 if tmpfp:
122 if tmpfp:
123 tmpfp.write(payload)
123 tmpfp.write(payload)
124 if not payload.endswith('\n'):
124 if not payload.endswith('\n'):
125 tmpfp.write('\n')
125 tmpfp.write('\n')
126 elif not diffs_seen and message and content_type == 'text/plain':
126 elif not diffs_seen and message and content_type == 'text/plain':
127 message += '\n' + payload
127 message += '\n' + payload
128 except:
128 except:
129 tmpfp.close()
129 tmpfp.close()
130 os.unlink(tmpname)
130 os.unlink(tmpname)
131 raise
131 raise
132
132
133 if subject and not message.startswith(subject):
133 if subject and not message.startswith(subject):
134 message = '%s\n%s' % (subject, message)
134 message = '%s\n%s' % (subject, message)
135 tmpfp.close()
135 tmpfp.close()
136 if not diffs_seen:
136 if not diffs_seen:
137 os.unlink(tmpname)
137 os.unlink(tmpname)
138 return None, message, user, date, branch, None, None, None
138 return None, message, user, date, branch, None, None, None
139 p1 = parents and parents.pop(0) or None
139 p1 = parents and parents.pop(0) or None
140 p2 = parents and parents.pop(0) or None
140 p2 = parents and parents.pop(0) or None
141 return tmpname, message, user, date, branch, nodeid, p1, p2
141 return tmpname, message, user, date, branch, nodeid, p1, p2
142
142
143 GP_PATCH = 1 << 0 # we have to run patch
143 GP_PATCH = 1 << 0 # we have to run patch
144 GP_FILTER = 1 << 1 # there's some copy/rename operation
144 GP_FILTER = 1 << 1 # there's some copy/rename operation
145 GP_BINARY = 1 << 2 # there's a binary patch
145 GP_BINARY = 1 << 2 # there's a binary patch
146
146
147 class patchmeta:
147 class patchmeta:
148 """Patched file metadata
148 """Patched file metadata
149
149
150 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
150 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
151 or COPY. 'path' is patched file path. 'oldpath' is set to the
151 or COPY. 'path' is patched file path. 'oldpath' is set to the
152 origin file when 'op' is either COPY or RENAME, None otherwise. If
152 origin file when 'op' is either COPY or RENAME, None otherwise. If
153 file mode is changed, 'mode' is a tuple (islink, isexec) where
153 file mode is changed, 'mode' is a tuple (islink, isexec) where
154 'islink' is True if the file is a symlink and 'isexec' is True if
154 'islink' is True if the file is a symlink and 'isexec' is True if
155 the file is executable. Otherwise, 'mode' is None.
155 the file is executable. Otherwise, 'mode' is None.
156 """
156 """
157 def __init__(self, path):
157 def __init__(self, path):
158 self.path = path
158 self.path = path
159 self.oldpath = None
159 self.oldpath = None
160 self.mode = None
160 self.mode = None
161 self.op = 'MODIFY'
161 self.op = 'MODIFY'
162 self.lineno = 0
162 self.lineno = 0
163 self.binary = False
163 self.binary = False
164
164
165 def setmode(self, mode):
165 def setmode(self, mode):
166 islink = mode & 020000
166 islink = mode & 020000
167 isexec = mode & 0100
167 isexec = mode & 0100
168 self.mode = (islink, isexec)
168 self.mode = (islink, isexec)
169
169
170 def readgitpatch(lr):
170 def readgitpatch(lr):
171 """extract git-style metadata about patches from <patchname>"""
171 """extract git-style metadata about patches from <patchname>"""
172
172
173 # Filter patch for git information
173 # Filter patch for git information
174 gp = None
174 gp = None
175 gitpatches = []
175 gitpatches = []
176 # Can have a git patch with only metadata, causing patch to complain
176 # Can have a git patch with only metadata, causing patch to complain
177 dopatch = 0
177 dopatch = 0
178
178
179 lineno = 0
179 lineno = 0
180 for line in lr:
180 for line in lr:
181 lineno += 1
181 lineno += 1
182 if line.startswith('diff --git'):
182 if line.startswith('diff --git'):
183 m = gitre.match(line)
183 m = gitre.match(line)
184 if m:
184 if m:
185 if gp:
185 if gp:
186 gitpatches.append(gp)
186 gitpatches.append(gp)
187 src, dst = m.group(1, 2)
187 src, dst = m.group(1, 2)
188 gp = patchmeta(dst)
188 gp = patchmeta(dst)
189 gp.lineno = lineno
189 gp.lineno = lineno
190 elif gp:
190 elif gp:
191 if line.startswith('--- '):
191 if line.startswith('--- '):
192 if gp.op in ('COPY', 'RENAME'):
192 if gp.op in ('COPY', 'RENAME'):
193 dopatch |= GP_FILTER
193 dopatch |= GP_FILTER
194 gitpatches.append(gp)
194 gitpatches.append(gp)
195 gp = None
195 gp = None
196 dopatch |= GP_PATCH
196 dopatch |= GP_PATCH
197 continue
197 continue
198 if line.startswith('rename from '):
198 if line.startswith('rename from '):
199 gp.op = 'RENAME'
199 gp.op = 'RENAME'
200 gp.oldpath = line[12:].rstrip()
200 gp.oldpath = line[12:].rstrip()
201 elif line.startswith('rename to '):
201 elif line.startswith('rename to '):
202 gp.path = line[10:].rstrip()
202 gp.path = line[10:].rstrip()
203 elif line.startswith('copy from '):
203 elif line.startswith('copy from '):
204 gp.op = 'COPY'
204 gp.op = 'COPY'
205 gp.oldpath = line[10:].rstrip()
205 gp.oldpath = line[10:].rstrip()
206 elif line.startswith('copy to '):
206 elif line.startswith('copy to '):
207 gp.path = line[8:].rstrip()
207 gp.path = line[8:].rstrip()
208 elif line.startswith('deleted file'):
208 elif line.startswith('deleted file'):
209 gp.op = 'DELETE'
209 gp.op = 'DELETE'
210 elif line.startswith('new file mode '):
210 elif line.startswith('new file mode '):
211 gp.op = 'ADD'
211 gp.op = 'ADD'
212 gp.setmode(int(line.rstrip()[-6:], 8))
212 gp.setmode(int(line.rstrip()[-6:], 8))
213 elif line.startswith('new mode '):
213 elif line.startswith('new mode '):
214 gp.setmode(int(line.rstrip()[-6:], 8))
214 gp.setmode(int(line.rstrip()[-6:], 8))
215 elif line.startswith('GIT binary patch'):
215 elif line.startswith('GIT binary patch'):
216 dopatch |= GP_BINARY
216 dopatch |= GP_BINARY
217 gp.binary = True
217 gp.binary = True
218 if gp:
218 if gp:
219 gitpatches.append(gp)
219 gitpatches.append(gp)
220
220
221 if not gitpatches:
221 if not gitpatches:
222 dopatch = GP_PATCH
222 dopatch = GP_PATCH
223
223
224 return (dopatch, gitpatches)
224 return (dopatch, gitpatches)
225
225
226 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
226 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
227 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
227 unidesc = re.compile('@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@')
228 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
228 contextdesc = re.compile('(---|\*\*\*) (\d+)(,(\d+))? (---|\*\*\*)')
229
229
230 class patchfile:
230 class patchfile:
231 def __init__(self, ui, fname, missing=False):
231 def __init__(self, ui, fname, missing=False):
232 self.fname = fname
232 self.fname = fname
233 self.ui = ui
233 self.ui = ui
234 self.lines = []
234 self.lines = []
235 self.exists = False
235 self.exists = False
236 self.missing = missing
236 self.missing = missing
237 if not missing:
237 if not missing:
238 try:
238 try:
239 fp = file(fname, 'rb')
239 fp = file(fname, 'rb')
240 self.lines = fp.readlines()
240 self.lines = fp.readlines()
241 self.exists = True
241 self.exists = True
242 except IOError:
242 except IOError:
243 pass
243 pass
244 else:
244 else:
245 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
245 self.ui.warn(_("unable to find '%s' for patching\n") % self.fname)
246
246
247 if not self.exists:
247 if not self.exists:
248 dirname = os.path.dirname(fname)
248 dirname = os.path.dirname(fname)
249 if dirname and not os.path.isdir(dirname):
249 if dirname and not os.path.isdir(dirname):
250 os.makedirs(dirname)
250 os.makedirs(dirname)
251
251
252 self.hash = {}
252 self.hash = {}
253 self.dirty = 0
253 self.dirty = 0
254 self.offset = 0
254 self.offset = 0
255 self.rej = []
255 self.rej = []
256 self.fileprinted = False
256 self.fileprinted = False
257 self.printfile(False)
257 self.printfile(False)
258 self.hunks = 0
258 self.hunks = 0
259
259
260 def printfile(self, warn):
260 def printfile(self, warn):
261 if self.fileprinted:
261 if self.fileprinted:
262 return
262 return
263 if warn or self.ui.verbose:
263 if warn or self.ui.verbose:
264 self.fileprinted = True
264 self.fileprinted = True
265 s = _("patching file %s\n") % self.fname
265 s = _("patching file %s\n") % self.fname
266 if warn:
266 if warn:
267 self.ui.warn(s)
267 self.ui.warn(s)
268 else:
268 else:
269 self.ui.note(s)
269 self.ui.note(s)
270
270
271
271
272 def findlines(self, l, linenum):
272 def findlines(self, l, linenum):
273 # looks through the hash and finds candidate lines. The
273 # looks through the hash and finds candidate lines. The
274 # result is a list of line numbers sorted based on distance
274 # result is a list of line numbers sorted based on distance
275 # from linenum
275 # from linenum
276 def sorter(a, b):
276 def sorter(a, b):
277 vala = abs(a - linenum)
277 vala = abs(a - linenum)
278 valb = abs(b - linenum)
278 valb = abs(b - linenum)
279 return cmp(vala, valb)
279 return cmp(vala, valb)
280
280
281 try:
281 try:
282 cand = self.hash[l]
282 cand = self.hash[l]
283 except:
283 except:
284 return []
284 return []
285
285
286 if len(cand) > 1:
286 if len(cand) > 1:
287 # resort our list of potentials forward then back.
287 # resort our list of potentials forward then back.
288 cand.sort(sorter)
288 cand.sort(sorter)
289 return cand
289 return cand
290
290
291 def hashlines(self):
291 def hashlines(self):
292 self.hash = {}
292 self.hash = {}
293 for x in xrange(len(self.lines)):
293 for x in xrange(len(self.lines)):
294 s = self.lines[x]
294 s = self.lines[x]
295 self.hash.setdefault(s, []).append(x)
295 self.hash.setdefault(s, []).append(x)
296
296
297 def write_rej(self):
297 def write_rej(self):
298 # our rejects are a little different from patch(1). This always
298 # our rejects are a little different from patch(1). This always
299 # creates rejects in the same form as the original patch. A file
299 # creates rejects in the same form as the original patch. A file
300 # header is inserted so that you can run the reject through patch again
300 # header is inserted so that you can run the reject through patch again
301 # without having to type the filename.
301 # without having to type the filename.
302
302
303 if not self.rej:
303 if not self.rej:
304 return
304 return
305
305
306 fname = self.fname + ".rej"
306 fname = self.fname + ".rej"
307 self.ui.warn(
307 self.ui.warn(
308 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
308 _("%d out of %d hunks FAILED -- saving rejects to file %s\n") %
309 (len(self.rej), self.hunks, fname))
309 (len(self.rej), self.hunks, fname))
310 try: os.unlink(fname)
310 try: os.unlink(fname)
311 except:
311 except:
312 pass
312 pass
313 fp = file(fname, 'wb')
313 fp = file(fname, 'wb')
314 base = os.path.basename(self.fname)
314 base = os.path.basename(self.fname)
315 fp.write("--- %s\n+++ %s\n" % (base, base))
315 fp.write("--- %s\n+++ %s\n" % (base, base))
316 for x in self.rej:
316 for x in self.rej:
317 for l in x.hunk:
317 for l in x.hunk:
318 fp.write(l)
318 fp.write(l)
319 if l[-1] != '\n':
319 if l[-1] != '\n':
320 fp.write("\n\ No newline at end of file\n")
320 fp.write("\n\ No newline at end of file\n")
321
321
322 def write(self, dest=None):
322 def write(self, dest=None):
323 if self.dirty:
323 if self.dirty:
324 if not dest:
324 if not dest:
325 dest = self.fname
325 dest = self.fname
326 st = None
326 st = None
327 try:
327 try:
328 st = os.lstat(dest)
328 st = os.lstat(dest)
329 except OSError, inst:
329 except OSError, inst:
330 if inst.errno != errno.ENOENT:
330 if inst.errno != errno.ENOENT:
331 raise
331 raise
332 if st and st.st_nlink > 1:
332 if st and st.st_nlink > 1:
333 os.unlink(dest)
333 os.unlink(dest)
334 fp = file(dest, 'wb')
334 fp = file(dest, 'wb')
335 if st and st.st_nlink > 1:
335 if st and st.st_nlink > 1:
336 os.chmod(dest, st.st_mode)
336 os.chmod(dest, st.st_mode)
337 fp.writelines(self.lines)
337 fp.writelines(self.lines)
338 fp.close()
338 fp.close()
339
339
340 def close(self):
340 def close(self):
341 self.write()
341 self.write()
342 self.write_rej()
342 self.write_rej()
343
343
344 def apply(self, h, reverse):
344 def apply(self, h, reverse):
345 if not h.complete():
345 if not h.complete():
346 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
346 raise PatchError(_("bad hunk #%d %s (%d %d %d %d)") %
347 (h.number, h.desc, len(h.a), h.lena, len(h.b),
347 (h.number, h.desc, len(h.a), h.lena, len(h.b),
348 h.lenb))
348 h.lenb))
349
349
350 self.hunks += 1
350 self.hunks += 1
351 if reverse:
351 if reverse:
352 h.reverse()
352 h.reverse()
353
353
354 if self.missing:
354 if self.missing:
355 self.rej.append(h)
355 self.rej.append(h)
356 return -1
356 return -1
357
357
358 if self.exists and h.createfile():
358 if self.exists and h.createfile():
359 self.ui.warn(_("file %s already exists\n") % self.fname)
359 self.ui.warn(_("file %s already exists\n") % self.fname)
360 self.rej.append(h)
360 self.rej.append(h)
361 return -1
361 return -1
362
362
363 if isinstance(h, binhunk):
363 if isinstance(h, binhunk):
364 if h.rmfile():
364 if h.rmfile():
365 os.unlink(self.fname)
365 os.unlink(self.fname)
366 else:
366 else:
367 self.lines[:] = h.new()
367 self.lines[:] = h.new()
368 self.offset += len(h.new())
368 self.offset += len(h.new())
369 self.dirty = 1
369 self.dirty = 1
370 return 0
370 return 0
371
371
372 # fast case first, no offsets, no fuzz
372 # fast case first, no offsets, no fuzz
373 old = h.old()
373 old = h.old()
374 # patch starts counting at 1 unless we are adding the file
374 # patch starts counting at 1 unless we are adding the file
375 if h.starta == 0:
375 if h.starta == 0:
376 start = 0
376 start = 0
377 else:
377 else:
378 start = h.starta + self.offset - 1
378 start = h.starta + self.offset - 1
379 orig_start = start
379 orig_start = start
380 if diffhelpers.testhunk(old, self.lines, start) == 0:
380 if diffhelpers.testhunk(old, self.lines, start) == 0:
381 if h.rmfile():
381 if h.rmfile():
382 os.unlink(self.fname)
382 os.unlink(self.fname)
383 else:
383 else:
384 self.lines[start : start + h.lena] = h.new()
384 self.lines[start : start + h.lena] = h.new()
385 self.offset += h.lenb - h.lena
385 self.offset += h.lenb - h.lena
386 self.dirty = 1
386 self.dirty = 1
387 return 0
387 return 0
388
388
389 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
389 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
390 self.hashlines()
390 self.hashlines()
391 if h.hunk[-1][0] != ' ':
391 if h.hunk[-1][0] != ' ':
392 # if the hunk tried to put something at the bottom of the file
392 # if the hunk tried to put something at the bottom of the file
393 # override the start line and use eof here
393 # override the start line and use eof here
394 search_start = len(self.lines)
394 search_start = len(self.lines)
395 else:
395 else:
396 search_start = orig_start
396 search_start = orig_start
397
397
398 for fuzzlen in xrange(3):
398 for fuzzlen in xrange(3):
399 for toponly in [ True, False ]:
399 for toponly in [ True, False ]:
400 old = h.old(fuzzlen, toponly)
400 old = h.old(fuzzlen, toponly)
401
401
402 cand = self.findlines(old[0][1:], search_start)
402 cand = self.findlines(old[0][1:], search_start)
403 for l in cand:
403 for l in cand:
404 if diffhelpers.testhunk(old, self.lines, l) == 0:
404 if diffhelpers.testhunk(old, self.lines, l) == 0:
405 newlines = h.new(fuzzlen, toponly)
405 newlines = h.new(fuzzlen, toponly)
406 self.lines[l : l + len(old)] = newlines
406 self.lines[l : l + len(old)] = newlines
407 self.offset += len(newlines) - len(old)
407 self.offset += len(newlines) - len(old)
408 self.dirty = 1
408 self.dirty = 1
409 if fuzzlen:
409 if fuzzlen:
410 fuzzstr = "with fuzz %d " % fuzzlen
410 fuzzstr = "with fuzz %d " % fuzzlen
411 f = self.ui.warn
411 f = self.ui.warn
412 self.printfile(True)
412 self.printfile(True)
413 else:
413 else:
414 fuzzstr = ""
414 fuzzstr = ""
415 f = self.ui.note
415 f = self.ui.note
416 offset = l - orig_start - fuzzlen
416 offset = l - orig_start - fuzzlen
417 if offset == 1:
417 if offset == 1:
418 linestr = "line"
418 linestr = "line"
419 else:
419 else:
420 linestr = "lines"
420 linestr = "lines"
421 f(_("Hunk #%d succeeded at %d %s(offset %d %s).\n") %
421 f(_("Hunk #%d succeeded at %d %s(offset %d %s).\n") %
422 (h.number, l+1, fuzzstr, offset, linestr))
422 (h.number, l+1, fuzzstr, offset, linestr))
423 return fuzzlen
423 return fuzzlen
424 self.printfile(True)
424 self.printfile(True)
425 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
425 self.ui.warn(_("Hunk #%d FAILED at %d\n") % (h.number, orig_start))
426 self.rej.append(h)
426 self.rej.append(h)
427 return -1
427 return -1
428
428
429 class hunk:
429 class hunk:
430 def __init__(self, desc, num, lr, context, create=False, remove=False):
430 def __init__(self, desc, num, lr, context, create=False, remove=False):
431 self.number = num
431 self.number = num
432 self.desc = desc
432 self.desc = desc
433 self.hunk = [ desc ]
433 self.hunk = [ desc ]
434 self.a = []
434 self.a = []
435 self.b = []
435 self.b = []
436 if context:
436 if context:
437 self.read_context_hunk(lr)
437 self.read_context_hunk(lr)
438 else:
438 else:
439 self.read_unified_hunk(lr)
439 self.read_unified_hunk(lr)
440 self.create = create
440 self.create = create
441 self.remove = remove and not create
441 self.remove = remove and not create
442
442
443 def read_unified_hunk(self, lr):
443 def read_unified_hunk(self, lr):
444 m = unidesc.match(self.desc)
444 m = unidesc.match(self.desc)
445 if not m:
445 if not m:
446 raise PatchError(_("bad hunk #%d") % self.number)
446 raise PatchError(_("bad hunk #%d") % self.number)
447 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
447 self.starta, foo, self.lena, self.startb, foo2, self.lenb = m.groups()
448 if self.lena == None:
448 if self.lena == None:
449 self.lena = 1
449 self.lena = 1
450 else:
450 else:
451 self.lena = int(self.lena)
451 self.lena = int(self.lena)
452 if self.lenb == None:
452 if self.lenb == None:
453 self.lenb = 1
453 self.lenb = 1
454 else:
454 else:
455 self.lenb = int(self.lenb)
455 self.lenb = int(self.lenb)
456 self.starta = int(self.starta)
456 self.starta = int(self.starta)
457 self.startb = int(self.startb)
457 self.startb = int(self.startb)
458 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
458 diffhelpers.addlines(lr, self.hunk, self.lena, self.lenb, self.a, self.b)
459 # if we hit eof before finishing out the hunk, the last line will
459 # if we hit eof before finishing out the hunk, the last line will
460 # be zero length. Lets try to fix it up.
460 # be zero length. Lets try to fix it up.
461 while len(self.hunk[-1]) == 0:
461 while len(self.hunk[-1]) == 0:
462 del self.hunk[-1]
462 del self.hunk[-1]
463 del self.a[-1]
463 del self.a[-1]
464 del self.b[-1]
464 del self.b[-1]
465 self.lena -= 1
465 self.lena -= 1
466 self.lenb -= 1
466 self.lenb -= 1
467
467
468 def read_context_hunk(self, lr):
468 def read_context_hunk(self, lr):
469 self.desc = lr.readline()
469 self.desc = lr.readline()
470 m = contextdesc.match(self.desc)
470 m = contextdesc.match(self.desc)
471 if not m:
471 if not m:
472 raise PatchError(_("bad hunk #%d") % self.number)
472 raise PatchError(_("bad hunk #%d") % self.number)
473 foo, self.starta, foo2, aend, foo3 = m.groups()
473 foo, self.starta, foo2, aend, foo3 = m.groups()
474 self.starta = int(self.starta)
474 self.starta = int(self.starta)
475 if aend == None:
475 if aend == None:
476 aend = self.starta
476 aend = self.starta
477 self.lena = int(aend) - self.starta
477 self.lena = int(aend) - self.starta
478 if self.starta:
478 if self.starta:
479 self.lena += 1
479 self.lena += 1
480 for x in xrange(self.lena):
480 for x in xrange(self.lena):
481 l = lr.readline()
481 l = lr.readline()
482 if l.startswith('---'):
482 if l.startswith('---'):
483 lr.push(l)
483 lr.push(l)
484 break
484 break
485 s = l[2:]
485 s = l[2:]
486 if l.startswith('- ') or l.startswith('! '):
486 if l.startswith('- ') or l.startswith('! '):
487 u = '-' + s
487 u = '-' + s
488 elif l.startswith(' '):
488 elif l.startswith(' '):
489 u = ' ' + s
489 u = ' ' + s
490 else:
490 else:
491 raise PatchError(_("bad hunk #%d old text line %d") %
491 raise PatchError(_("bad hunk #%d old text line %d") %
492 (self.number, x))
492 (self.number, x))
493 self.a.append(u)
493 self.a.append(u)
494 self.hunk.append(u)
494 self.hunk.append(u)
495
495
496 l = lr.readline()
496 l = lr.readline()
497 if l.startswith('\ '):
497 if l.startswith('\ '):
498 s = self.a[-1][:-1]
498 s = self.a[-1][:-1]
499 self.a[-1] = s
499 self.a[-1] = s
500 self.hunk[-1] = s
500 self.hunk[-1] = s
501 l = lr.readline()
501 l = lr.readline()
502 m = contextdesc.match(l)
502 m = contextdesc.match(l)
503 if not m:
503 if not m:
504 raise PatchError(_("bad hunk #%d") % self.number)
504 raise PatchError(_("bad hunk #%d") % self.number)
505 foo, self.startb, foo2, bend, foo3 = m.groups()
505 foo, self.startb, foo2, bend, foo3 = m.groups()
506 self.startb = int(self.startb)
506 self.startb = int(self.startb)
507 if bend == None:
507 if bend == None:
508 bend = self.startb
508 bend = self.startb
509 self.lenb = int(bend) - self.startb
509 self.lenb = int(bend) - self.startb
510 if self.startb:
510 if self.startb:
511 self.lenb += 1
511 self.lenb += 1
512 hunki = 1
512 hunki = 1
513 for x in xrange(self.lenb):
513 for x in xrange(self.lenb):
514 l = lr.readline()
514 l = lr.readline()
515 if l.startswith('\ '):
515 if l.startswith('\ '):
516 s = self.b[-1][:-1]
516 s = self.b[-1][:-1]
517 self.b[-1] = s
517 self.b[-1] = s
518 self.hunk[hunki-1] = s
518 self.hunk[hunki-1] = s
519 continue
519 continue
520 if not l:
520 if not l:
521 lr.push(l)
521 lr.push(l)
522 break
522 break
523 s = l[2:]
523 s = l[2:]
524 if l.startswith('+ ') or l.startswith('! '):
524 if l.startswith('+ ') or l.startswith('! '):
525 u = '+' + s
525 u = '+' + s
526 elif l.startswith(' '):
526 elif l.startswith(' '):
527 u = ' ' + s
527 u = ' ' + s
528 elif len(self.b) == 0:
528 elif len(self.b) == 0:
529 # this can happen when the hunk does not add any lines
529 # this can happen when the hunk does not add any lines
530 lr.push(l)
530 lr.push(l)
531 break
531 break
532 else:
532 else:
533 raise PatchError(_("bad hunk #%d old text line %d") %
533 raise PatchError(_("bad hunk #%d old text line %d") %
534 (self.number, x))
534 (self.number, x))
535 self.b.append(s)
535 self.b.append(s)
536 while True:
536 while True:
537 if hunki >= len(self.hunk):
537 if hunki >= len(self.hunk):
538 h = ""
538 h = ""
539 else:
539 else:
540 h = self.hunk[hunki]
540 h = self.hunk[hunki]
541 hunki += 1
541 hunki += 1
542 if h == u:
542 if h == u:
543 break
543 break
544 elif h.startswith('-'):
544 elif h.startswith('-'):
545 continue
545 continue
546 else:
546 else:
547 self.hunk.insert(hunki-1, u)
547 self.hunk.insert(hunki-1, u)
548 break
548 break
549
549
550 if not self.a:
550 if not self.a:
551 # this happens when lines were only added to the hunk
551 # this happens when lines were only added to the hunk
552 for x in self.hunk:
552 for x in self.hunk:
553 if x.startswith('-') or x.startswith(' '):
553 if x.startswith('-') or x.startswith(' '):
554 self.a.append(x)
554 self.a.append(x)
555 if not self.b:
555 if not self.b:
556 # this happens when lines were only deleted from the hunk
556 # this happens when lines were only deleted from the hunk
557 for x in self.hunk:
557 for x in self.hunk:
558 if x.startswith('+') or x.startswith(' '):
558 if x.startswith('+') or x.startswith(' '):
559 self.b.append(x[1:])
559 self.b.append(x[1:])
560 # @@ -start,len +start,len @@
560 # @@ -start,len +start,len @@
561 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
561 self.desc = "@@ -%d,%d +%d,%d @@\n" % (self.starta, self.lena,
562 self.startb, self.lenb)
562 self.startb, self.lenb)
563 self.hunk[0] = self.desc
563 self.hunk[0] = self.desc
564
564
565 def reverse(self):
565 def reverse(self):
566 self.create, self.remove = self.remove, self.create
566 self.create, self.remove = self.remove, self.create
567 origlena = self.lena
567 origlena = self.lena
568 origstarta = self.starta
568 origstarta = self.starta
569 self.lena = self.lenb
569 self.lena = self.lenb
570 self.starta = self.startb
570 self.starta = self.startb
571 self.lenb = origlena
571 self.lenb = origlena
572 self.startb = origstarta
572 self.startb = origstarta
573 self.a = []
573 self.a = []
574 self.b = []
574 self.b = []
575 # self.hunk[0] is the @@ description
575 # self.hunk[0] is the @@ description
576 for x in xrange(1, len(self.hunk)):
576 for x in xrange(1, len(self.hunk)):
577 o = self.hunk[x]
577 o = self.hunk[x]
578 if o.startswith('-'):
578 if o.startswith('-'):
579 n = '+' + o[1:]
579 n = '+' + o[1:]
580 self.b.append(o[1:])
580 self.b.append(o[1:])
581 elif o.startswith('+'):
581 elif o.startswith('+'):
582 n = '-' + o[1:]
582 n = '-' + o[1:]
583 self.a.append(n)
583 self.a.append(n)
584 else:
584 else:
585 n = o
585 n = o
586 self.b.append(o[1:])
586 self.b.append(o[1:])
587 self.a.append(o)
587 self.a.append(o)
588 self.hunk[x] = o
588 self.hunk[x] = o
589
589
590 def fix_newline(self):
590 def fix_newline(self):
591 diffhelpers.fix_newline(self.hunk, self.a, self.b)
591 diffhelpers.fix_newline(self.hunk, self.a, self.b)
592
592
593 def complete(self):
593 def complete(self):
594 return len(self.a) == self.lena and len(self.b) == self.lenb
594 return len(self.a) == self.lena and len(self.b) == self.lenb
595
595
596 def createfile(self):
596 def createfile(self):
597 return self.starta == 0 and self.lena == 0 and self.create
597 return self.starta == 0 and self.lena == 0 and self.create
598
598
599 def rmfile(self):
599 def rmfile(self):
600 return self.startb == 0 and self.lenb == 0 and self.remove
600 return self.startb == 0 and self.lenb == 0 and self.remove
601
601
602 def fuzzit(self, l, fuzz, toponly):
602 def fuzzit(self, l, fuzz, toponly):
603 # this removes context lines from the top and bottom of list 'l'. It
603 # this removes context lines from the top and bottom of list 'l'. It
604 # checks the hunk to make sure only context lines are removed, and then
604 # checks the hunk to make sure only context lines are removed, and then
605 # returns a new shortened list of lines.
605 # returns a new shortened list of lines.
606 fuzz = min(fuzz, len(l)-1)
606 fuzz = min(fuzz, len(l)-1)
607 if fuzz:
607 if fuzz:
608 top = 0
608 top = 0
609 bot = 0
609 bot = 0
610 hlen = len(self.hunk)
610 hlen = len(self.hunk)
611 for x in xrange(hlen-1):
611 for x in xrange(hlen-1):
612 # the hunk starts with the @@ line, so use x+1
612 # the hunk starts with the @@ line, so use x+1
613 if self.hunk[x+1][0] == ' ':
613 if self.hunk[x+1][0] == ' ':
614 top += 1
614 top += 1
615 else:
615 else:
616 break
616 break
617 if not toponly:
617 if not toponly:
618 for x in xrange(hlen-1):
618 for x in xrange(hlen-1):
619 if self.hunk[hlen-bot-1][0] == ' ':
619 if self.hunk[hlen-bot-1][0] == ' ':
620 bot += 1
620 bot += 1
621 else:
621 else:
622 break
622 break
623
623
624 # top and bot now count context in the hunk
624 # top and bot now count context in the hunk
625 # adjust them if either one is short
625 # adjust them if either one is short
626 context = max(top, bot, 3)
626 context = max(top, bot, 3)
627 if bot < context:
627 if bot < context:
628 bot = max(0, fuzz - (context - bot))
628 bot = max(0, fuzz - (context - bot))
629 else:
629 else:
630 bot = min(fuzz, bot)
630 bot = min(fuzz, bot)
631 if top < context:
631 if top < context:
632 top = max(0, fuzz - (context - top))
632 top = max(0, fuzz - (context - top))
633 else:
633 else:
634 top = min(fuzz, top)
634 top = min(fuzz, top)
635
635
636 return l[top:len(l)-bot]
636 return l[top:len(l)-bot]
637 return l
637 return l
638
638
639 def old(self, fuzz=0, toponly=False):
639 def old(self, fuzz=0, toponly=False):
640 return self.fuzzit(self.a, fuzz, toponly)
640 return self.fuzzit(self.a, fuzz, toponly)
641
641
642 def newctrl(self):
642 def newctrl(self):
643 res = []
643 res = []
644 for x in self.hunk:
644 for x in self.hunk:
645 c = x[0]
645 c = x[0]
646 if c == ' ' or c == '+':
646 if c == ' ' or c == '+':
647 res.append(x)
647 res.append(x)
648 return res
648 return res
649
649
650 def new(self, fuzz=0, toponly=False):
650 def new(self, fuzz=0, toponly=False):
651 return self.fuzzit(self.b, fuzz, toponly)
651 return self.fuzzit(self.b, fuzz, toponly)
652
652
653 class binhunk:
653 class binhunk:
654 'A binary patch file. Only understands literals so far.'
654 'A binary patch file. Only understands literals so far.'
655 def __init__(self, gitpatch):
655 def __init__(self, gitpatch):
656 self.gitpatch = gitpatch
656 self.gitpatch = gitpatch
657 self.text = None
657 self.text = None
658 self.hunk = ['GIT binary patch\n']
658 self.hunk = ['GIT binary patch\n']
659
659
660 def createfile(self):
660 def createfile(self):
661 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
661 return self.gitpatch.op in ('ADD', 'RENAME', 'COPY')
662
662
663 def rmfile(self):
663 def rmfile(self):
664 return self.gitpatch.op == 'DELETE'
664 return self.gitpatch.op == 'DELETE'
665
665
666 def complete(self):
666 def complete(self):
667 return self.text is not None
667 return self.text is not None
668
668
669 def new(self):
669 def new(self):
670 return [self.text]
670 return [self.text]
671
671
672 def extract(self, lr):
672 def extract(self, lr):
673 line = lr.readline()
673 line = lr.readline()
674 self.hunk.append(line)
674 self.hunk.append(line)
675 while line and not line.startswith('literal '):
675 while line and not line.startswith('literal '):
676 line = lr.readline()
676 line = lr.readline()
677 self.hunk.append(line)
677 self.hunk.append(line)
678 if not line:
678 if not line:
679 raise PatchError(_('could not extract binary patch'))
679 raise PatchError(_('could not extract binary patch'))
680 size = int(line[8:].rstrip())
680 size = int(line[8:].rstrip())
681 dec = []
681 dec = []
682 line = lr.readline()
682 line = lr.readline()
683 self.hunk.append(line)
683 self.hunk.append(line)
684 while len(line) > 1:
684 while len(line) > 1:
685 l = line[0]
685 l = line[0]
686 if l <= 'Z' and l >= 'A':
686 if l <= 'Z' and l >= 'A':
687 l = ord(l) - ord('A') + 1
687 l = ord(l) - ord('A') + 1
688 else:
688 else:
689 l = ord(l) - ord('a') + 27
689 l = ord(l) - ord('a') + 27
690 dec.append(base85.b85decode(line[1:-1])[:l])
690 dec.append(base85.b85decode(line[1:-1])[:l])
691 line = lr.readline()
691 line = lr.readline()
692 self.hunk.append(line)
692 self.hunk.append(line)
693 text = zlib.decompress(''.join(dec))
693 text = zlib.decompress(''.join(dec))
694 if len(text) != size:
694 if len(text) != size:
695 raise PatchError(_('binary patch is %d bytes, not %d') %
695 raise PatchError(_('binary patch is %d bytes, not %d') %
696 len(text), size)
696 len(text), size)
697 self.text = text
697 self.text = text
698
698
699 def parsefilename(str):
699 def parsefilename(str):
700 # --- filename \t|space stuff
700 # --- filename \t|space stuff
701 s = str[4:].rstrip('\r\n')
701 s = str[4:].rstrip('\r\n')
702 i = s.find('\t')
702 i = s.find('\t')
703 if i < 0:
703 if i < 0:
704 i = s.find(' ')
704 i = s.find(' ')
705 if i < 0:
705 if i < 0:
706 return s
706 return s
707 return s[:i]
707 return s[:i]
708
708
709 def selectfile(afile_orig, bfile_orig, hunk, strip, reverse):
709 def selectfile(afile_orig, bfile_orig, hunk, strip, reverse):
710 def pathstrip(path, count=1):
710 def pathstrip(path, count=1):
711 pathlen = len(path)
711 pathlen = len(path)
712 i = 0
712 i = 0
713 if count == 0:
713 if count == 0:
714 return '', path.rstrip()
714 return '', path.rstrip()
715 while count > 0:
715 while count > 0:
716 i = path.find('/', i)
716 i = path.find('/', i)
717 if i == -1:
717 if i == -1:
718 raise PatchError(_("unable to strip away %d dirs from %s") %
718 raise PatchError(_("unable to strip away %d dirs from %s") %
719 (count, path))
719 (count, path))
720 i += 1
720 i += 1
721 # consume '//' in the path
721 # consume '//' in the path
722 while i < pathlen - 1 and path[i] == '/':
722 while i < pathlen - 1 and path[i] == '/':
723 i += 1
723 i += 1
724 count -= 1
724 count -= 1
725 return path[:i].lstrip(), path[i:].rstrip()
725 return path[:i].lstrip(), path[i:].rstrip()
726
726
727 nulla = afile_orig == "/dev/null"
727 nulla = afile_orig == "/dev/null"
728 nullb = bfile_orig == "/dev/null"
728 nullb = bfile_orig == "/dev/null"
729 abase, afile = pathstrip(afile_orig, strip)
729 abase, afile = pathstrip(afile_orig, strip)
730 gooda = not nulla and os.path.exists(afile)
730 gooda = not nulla and os.path.exists(afile)
731 bbase, bfile = pathstrip(bfile_orig, strip)
731 bbase, bfile = pathstrip(bfile_orig, strip)
732 if afile == bfile:
732 if afile == bfile:
733 goodb = gooda
733 goodb = gooda
734 else:
734 else:
735 goodb = not nullb and os.path.exists(bfile)
735 goodb = not nullb and os.path.exists(bfile)
736 createfunc = hunk.createfile
736 createfunc = hunk.createfile
737 if reverse:
737 if reverse:
738 createfunc = hunk.rmfile
738 createfunc = hunk.rmfile
739 missing = not goodb and not gooda and not createfunc()
739 missing = not goodb and not gooda and not createfunc()
740 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
740 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
741 # diff is between a file and its backup. In this case, the original
741 # diff is between a file and its backup. In this case, the original
742 # file should be patched (see original mpatch code).
742 # file should be patched (see original mpatch code).
743 isbackup = (abase == bbase and bfile.startswith(afile))
743 isbackup = (abase == bbase and bfile.startswith(afile))
744 fname = None
744 fname = None
745 if not missing:
745 if not missing:
746 if gooda and goodb:
746 if gooda and goodb:
747 fname = isbackup and afile or bfile
747 fname = isbackup and afile or bfile
748 elif gooda:
748 elif gooda:
749 fname = afile
749 fname = afile
750
750
751 if not fname:
751 if not fname:
752 if not nullb:
752 if not nullb:
753 fname = isbackup and afile or bfile
753 fname = isbackup and afile or bfile
754 elif not nulla:
754 elif not nulla:
755 fname = afile
755 fname = afile
756 else:
756 else:
757 raise PatchError(_("undefined source and destination files"))
757 raise PatchError(_("undefined source and destination files"))
758
758
759 return fname, missing
759 return fname, missing
760
760
761 class linereader:
761 class linereader:
762 # simple class to allow pushing lines back into the input stream
762 # simple class to allow pushing lines back into the input stream
763 def __init__(self, fp):
763 def __init__(self, fp):
764 self.fp = fp
764 self.fp = fp
765 self.buf = []
765 self.buf = []
766
766
767 def push(self, line):
767 def push(self, line):
768 if line is not None:
768 if line is not None:
769 self.buf.append(line)
769 self.buf.append(line)
770
770
771 def readline(self):
771 def readline(self):
772 if self.buf:
772 if self.buf:
773 l = self.buf[0]
773 l = self.buf[0]
774 del self.buf[0]
774 del self.buf[0]
775 return l
775 return l
776 return self.fp.readline()
776 return self.fp.readline()
777
777
778 def __iter__(self):
778 def __iter__(self):
779 while 1:
779 while 1:
780 l = self.readline()
780 l = self.readline()
781 if not l:
781 if not l:
782 break
782 break
783 yield l
783 yield l
784
784
785 def scangitpatch(lr, firstline):
785 def scangitpatch(lr, firstline):
786 """
786 """
787 Git patches can emit:
787 Git patches can emit:
788 - rename a to b
788 - rename a to b
789 - change b
789 - change b
790 - copy a to c
790 - copy a to c
791 - change c
791 - change c
792
792
793 We cannot apply this sequence as-is, the renamed 'a' could not be
793 We cannot apply this sequence as-is, the renamed 'a' could not be
794 found for it would have been renamed already. And we cannot copy
794 found for it would have been renamed already. And we cannot copy
795 from 'b' instead because 'b' would have been changed already. So
795 from 'b' instead because 'b' would have been changed already. So
796 we scan the git patch for copy and rename commands so we can
796 we scan the git patch for copy and rename commands so we can
797 perform the copies ahead of time.
797 perform the copies ahead of time.
798 """
798 """
799 pos = 0
799 pos = 0
800 try:
800 try:
801 pos = lr.fp.tell()
801 pos = lr.fp.tell()
802 fp = lr.fp
802 fp = lr.fp
803 except IOError:
803 except IOError:
804 fp = cStringIO.StringIO(lr.fp.read())
804 fp = cStringIO.StringIO(lr.fp.read())
805 gitlr = linereader(fp)
805 gitlr = linereader(fp)
806 gitlr.push(firstline)
806 gitlr.push(firstline)
807 (dopatch, gitpatches) = readgitpatch(gitlr)
807 (dopatch, gitpatches) = readgitpatch(gitlr)
808 fp.seek(pos)
808 fp.seek(pos)
809 return dopatch, gitpatches
809 return dopatch, gitpatches
810
810
811 def iterhunks(ui, fp, sourcefile=None):
811 def iterhunks(ui, fp, sourcefile=None):
812 """Read a patch and yield the following events:
812 """Read a patch and yield the following events:
813 - ("file", afile, bfile, firsthunk): select a new target file.
813 - ("file", afile, bfile, firsthunk): select a new target file.
814 - ("hunk", hunk): a new hunk is ready to be applied, follows a
814 - ("hunk", hunk): a new hunk is ready to be applied, follows a
815 "file" event.
815 "file" event.
816 - ("git", gitchanges): current diff is in git format, gitchanges
816 - ("git", gitchanges): current diff is in git format, gitchanges
817 maps filenames to gitpatch records. Unique event.
817 maps filenames to gitpatch records. Unique event.
818 """
818 """
819 changed = {}
819 changed = {}
820 current_hunk = None
820 current_hunk = None
821 afile = ""
821 afile = ""
822 bfile = ""
822 bfile = ""
823 state = None
823 state = None
824 hunknum = 0
824 hunknum = 0
825 emitfile = False
825 emitfile = False
826 git = False
826 git = False
827
827
828 # our states
828 # our states
829 BFILE = 1
829 BFILE = 1
830 context = None
830 context = None
831 lr = linereader(fp)
831 lr = linereader(fp)
832 dopatch = True
832 dopatch = True
833 # gitworkdone is True if a git operation (copy, rename, ...) was
833 # gitworkdone is True if a git operation (copy, rename, ...) was
834 # performed already for the current file. Useful when the file
834 # performed already for the current file. Useful when the file
835 # section may have no hunk.
835 # section may have no hunk.
836 gitworkdone = False
836 gitworkdone = False
837
837
838 while True:
838 while True:
839 newfile = False
839 newfile = False
840 x = lr.readline()
840 x = lr.readline()
841 if not x:
841 if not x:
842 break
842 break
843 if current_hunk:
843 if current_hunk:
844 if x.startswith('\ '):
844 if x.startswith('\ '):
845 current_hunk.fix_newline()
845 current_hunk.fix_newline()
846 yield 'hunk', current_hunk
846 yield 'hunk', current_hunk
847 current_hunk = None
847 current_hunk = None
848 gitworkdone = False
848 gitworkdone = False
849 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
849 if ((sourcefile or state == BFILE) and ((not context and x[0] == '@') or
850 ((context or context == None) and x.startswith('***************')))):
850 ((context or context == None) and x.startswith('***************')))):
851 try:
851 try:
852 if context == None and x.startswith('***************'):
852 if context == None and x.startswith('***************'):
853 context = True
853 context = True
854 gpatch = changed.get(bfile)
854 gpatch = changed.get(bfile)
855 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
855 create = afile == '/dev/null' or gpatch and gpatch.op == 'ADD'
856 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
856 remove = bfile == '/dev/null' or gpatch and gpatch.op == 'DELETE'
857 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
857 current_hunk = hunk(x, hunknum + 1, lr, context, create, remove)
858 except PatchError, err:
858 except PatchError, err:
859 ui.debug(err)
859 ui.debug(err)
860 current_hunk = None
860 current_hunk = None
861 continue
861 continue
862 hunknum += 1
862 hunknum += 1
863 if emitfile:
863 if emitfile:
864 emitfile = False
864 emitfile = False
865 yield 'file', (afile, bfile, current_hunk)
865 yield 'file', (afile, bfile, current_hunk)
866 elif state == BFILE and x.startswith('GIT binary patch'):
866 elif state == BFILE and x.startswith('GIT binary patch'):
867 current_hunk = binhunk(changed[bfile])
867 current_hunk = binhunk(changed[bfile])
868 hunknum += 1
868 hunknum += 1
869 if emitfile:
869 if emitfile:
870 emitfile = False
870 emitfile = False
871 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
871 yield 'file', ('a/' + afile, 'b/' + bfile, current_hunk)
872 current_hunk.extract(lr)
872 current_hunk.extract(lr)
873 elif x.startswith('diff --git'):
873 elif x.startswith('diff --git'):
874 # check for git diff, scanning the whole patch file if needed
874 # check for git diff, scanning the whole patch file if needed
875 m = gitre.match(x)
875 m = gitre.match(x)
876 if m:
876 if m:
877 afile, bfile = m.group(1, 2)
877 afile, bfile = m.group(1, 2)
878 if not git:
878 if not git:
879 git = True
879 git = True
880 dopatch, gitpatches = scangitpatch(lr, x)
880 dopatch, gitpatches = scangitpatch(lr, x)
881 yield 'git', gitpatches
881 yield 'git', gitpatches
882 for gp in gitpatches:
882 for gp in gitpatches:
883 changed[gp.path] = gp
883 changed[gp.path] = gp
884 # else error?
884 # else error?
885 # copy/rename + modify should modify target, not source
885 # copy/rename + modify should modify target, not source
886 gp = changed.get(bfile)
886 gp = changed.get(bfile)
887 if gp and gp.op in ('COPY', 'DELETE', 'RENAME'):
887 if gp and gp.op in ('COPY', 'DELETE', 'RENAME'):
888 afile = bfile
888 afile = bfile
889 gitworkdone = True
889 gitworkdone = True
890 newfile = True
890 newfile = True
891 elif x.startswith('---'):
891 elif x.startswith('---'):
892 # check for a unified diff
892 # check for a unified diff
893 l2 = lr.readline()
893 l2 = lr.readline()
894 if not l2.startswith('+++'):
894 if not l2.startswith('+++'):
895 lr.push(l2)
895 lr.push(l2)
896 continue
896 continue
897 newfile = True
897 newfile = True
898 context = False
898 context = False
899 afile = parsefilename(x)
899 afile = parsefilename(x)
900 bfile = parsefilename(l2)
900 bfile = parsefilename(l2)
901 elif x.startswith('***'):
901 elif x.startswith('***'):
902 # check for a context diff
902 # check for a context diff
903 l2 = lr.readline()
903 l2 = lr.readline()
904 if not l2.startswith('---'):
904 if not l2.startswith('---'):
905 lr.push(l2)
905 lr.push(l2)
906 continue
906 continue
907 l3 = lr.readline()
907 l3 = lr.readline()
908 lr.push(l3)
908 lr.push(l3)
909 if not l3.startswith("***************"):
909 if not l3.startswith("***************"):
910 lr.push(l2)
910 lr.push(l2)
911 continue
911 continue
912 newfile = True
912 newfile = True
913 context = True
913 context = True
914 afile = parsefilename(x)
914 afile = parsefilename(x)
915 bfile = parsefilename(l2)
915 bfile = parsefilename(l2)
916
916
917 if newfile:
917 if newfile:
918 emitfile = True
918 emitfile = True
919 state = BFILE
919 state = BFILE
920 hunknum = 0
920 hunknum = 0
921 if current_hunk:
921 if current_hunk:
922 if current_hunk.complete():
922 if current_hunk.complete():
923 yield 'hunk', current_hunk
923 yield 'hunk', current_hunk
924 else:
924 else:
925 raise PatchError(_("malformed patch %s %s") % (afile,
925 raise PatchError(_("malformed patch %s %s") % (afile,
926 current_hunk.desc))
926 current_hunk.desc))
927
927
928 if hunknum == 0 and dopatch and not gitworkdone:
928 if hunknum == 0 and dopatch and not gitworkdone:
929 raise NoHunks
929 raise NoHunks
930
930
931 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False):
931 def applydiff(ui, fp, changed, strip=1, sourcefile=None, reverse=False):
932 """reads a patch from fp and tries to apply it. The dict 'changed' is
932 """reads a patch from fp and tries to apply it. The dict 'changed' is
933 filled in with all of the filenames changed by the patch. Returns 0
933 filled in with all of the filenames changed by the patch. Returns 0
934 for a clean patch, -1 if any rejects were found and 1 if there was
934 for a clean patch, -1 if any rejects were found and 1 if there was
935 any fuzz."""
935 any fuzz."""
936
936
937 rejects = 0
937 rejects = 0
938 err = 0
938 err = 0
939 current_file = None
939 current_file = None
940 gitpatches = None
940 gitpatches = None
941
941
942 def closefile():
942 def closefile():
943 if not current_file:
943 if not current_file:
944 return 0
944 return 0
945 current_file.close()
945 current_file.close()
946 return len(current_file.rej)
946 return len(current_file.rej)
947
947
948 for state, values in iterhunks(ui, fp, sourcefile):
948 for state, values in iterhunks(ui, fp, sourcefile):
949 if state == 'hunk':
949 if state == 'hunk':
950 if not current_file:
950 if not current_file:
951 continue
951 continue
952 current_hunk = values
952 current_hunk = values
953 ret = current_file.apply(current_hunk, reverse)
953 ret = current_file.apply(current_hunk, reverse)
954 if ret >= 0:
954 if ret >= 0:
955 changed.setdefault(current_file.fname, None)
955 changed.setdefault(current_file.fname, None)
956 if ret > 0:
956 if ret > 0:
957 err = 1
957 err = 1
958 elif state == 'file':
958 elif state == 'file':
959 rejects += closefile()
959 rejects += closefile()
960 afile, bfile, first_hunk = values
960 afile, bfile, first_hunk = values
961 try:
961 try:
962 if sourcefile:
962 if sourcefile:
963 current_file = patchfile(ui, sourcefile)
963 current_file = patchfile(ui, sourcefile)
964 else:
964 else:
965 current_file, missing = selectfile(afile, bfile, first_hunk,
965 current_file, missing = selectfile(afile, bfile, first_hunk,
966 strip, reverse)
966 strip, reverse)
967 current_file = patchfile(ui, current_file, missing)
967 current_file = patchfile(ui, current_file, missing)
968 except PatchError, err:
968 except PatchError, err:
969 ui.warn(str(err) + '\n')
969 ui.warn(str(err) + '\n')
970 current_file, current_hunk = None, None
970 current_file, current_hunk = None, None
971 rejects += 1
971 rejects += 1
972 continue
972 continue
973 elif state == 'git':
973 elif state == 'git':
974 gitpatches = values
974 gitpatches = values
975 cwd = os.getcwd()
975 cwd = os.getcwd()
976 for gp in gitpatches:
976 for gp in gitpatches:
977 if gp.op in ('COPY', 'RENAME'):
977 if gp.op in ('COPY', 'RENAME'):
978 src, dst = [util.canonpath(cwd, cwd, x)
978 src, dst = [util.canonpath(cwd, cwd, x)
979 for x in [gp.oldpath, gp.path]]
979 for x in [gp.oldpath, gp.path]]
980 copyfile(src, dst)
980 copyfile(src, dst)
981 changed[gp.path] = gp
981 changed[gp.path] = gp
982 else:
982 else:
983 raise util.Abort(_('unsupported parser state: %s') % state)
983 raise util.Abort(_('unsupported parser state: %s') % state)
984
984
985 rejects += closefile()
985 rejects += closefile()
986
986
987 if rejects:
987 if rejects:
988 return -1
988 return -1
989 return err
989 return err
990
990
991 def diffopts(ui, opts={}, untrusted=False):
991 def diffopts(ui, opts={}, untrusted=False):
992 def get(key, name=None, getter=ui.configbool):
992 def get(key, name=None, getter=ui.configbool):
993 return (opts.get(key) or
993 return (opts.get(key) or
994 getter('diff', name or key, None, untrusted=untrusted))
994 getter('diff', name or key, None, untrusted=untrusted))
995 return mdiff.diffopts(
995 return mdiff.diffopts(
996 text=opts.get('text'),
996 text=opts.get('text'),
997 git=get('git'),
997 git=get('git'),
998 nodates=get('nodates'),
998 nodates=get('nodates'),
999 showfunc=get('show_function', 'showfunc'),
999 showfunc=get('show_function', 'showfunc'),
1000 ignorews=get('ignore_all_space', 'ignorews'),
1000 ignorews=get('ignore_all_space', 'ignorews'),
1001 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1001 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
1002 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1002 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'),
1003 context=get('unified', getter=ui.config))
1003 context=get('unified', getter=ui.config))
1004
1004
1005 def updatedir(ui, repo, patches):
1005 def updatedir(ui, repo, patches):
1006 '''Update dirstate after patch application according to metadata'''
1006 '''Update dirstate after patch application according to metadata'''
1007 if not patches:
1007 if not patches:
1008 return
1008 return
1009 copies = []
1009 copies = []
1010 removes = {}
1010 removes = {}
1011 cfiles = patches.keys()
1011 cfiles = patches.keys()
1012 cwd = repo.getcwd()
1012 cwd = repo.getcwd()
1013 if cwd:
1013 if cwd:
1014 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1014 cfiles = [util.pathto(repo.root, cwd, f) for f in patches.keys()]
1015 for f in patches:
1015 for f in patches:
1016 gp = patches[f]
1016 gp = patches[f]
1017 if not gp:
1017 if not gp:
1018 continue
1018 continue
1019 if gp.op == 'RENAME':
1019 if gp.op == 'RENAME':
1020 copies.append((gp.oldpath, gp.path))
1020 copies.append((gp.oldpath, gp.path))
1021 removes[gp.oldpath] = 1
1021 removes[gp.oldpath] = 1
1022 elif gp.op == 'COPY':
1022 elif gp.op == 'COPY':
1023 copies.append((gp.oldpath, gp.path))
1023 copies.append((gp.oldpath, gp.path))
1024 elif gp.op == 'DELETE':
1024 elif gp.op == 'DELETE':
1025 removes[gp.path] = 1
1025 removes[gp.path] = 1
1026 for src, dst in copies:
1026 for src, dst in copies:
1027 repo.copy(src, dst)
1027 repo.copy(src, dst)
1028 removes = removes.keys()
1028 removes = removes.keys()
1029 if removes:
1029 if removes:
1030 repo.remove(util.sort(removes), True)
1030 repo.remove(util.sort(removes), True)
1031 for f in patches:
1031 for f in patches:
1032 gp = patches[f]
1032 gp = patches[f]
1033 if gp and gp.mode:
1033 if gp and gp.mode:
1034 islink, isexec = gp.mode
1034 islink, isexec = gp.mode
1035 dst = os.path.join(repo.root, gp.path)
1035 dst = os.path.join(repo.root, gp.path)
1036 # patch won't create empty files
1036 # patch won't create empty files
1037 if gp.op == 'ADD' and not os.path.exists(dst):
1037 if gp.op == 'ADD' and not os.path.exists(dst):
1038 flags = (isexec and 'x' or '') + (islink and 'l' or '')
1038 flags = (isexec and 'x' or '') + (islink and 'l' or '')
1039 repo.wwrite(gp.path, '', flags)
1039 repo.wwrite(gp.path, '', flags)
1040 else:
1040 else:
1041 util.set_flags(dst, islink, isexec)
1041 util.set_flags(dst, islink, isexec)
1042 cmdutil.addremove(repo, cfiles)
1042 cmdutil.addremove(repo, cfiles)
1043 files = patches.keys()
1043 files = patches.keys()
1044 files.extend([r for r in removes if r not in files])
1044 files.extend([r for r in removes if r not in files])
1045 return util.sort(files)
1045 return util.sort(files)
1046
1046
1047 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
1047 def externalpatch(patcher, args, patchname, ui, strip, cwd, files):
1048 """use <patcher> to apply <patchname> to the working directory.
1048 """use <patcher> to apply <patchname> to the working directory.
1049 returns whether patch was applied with fuzz factor."""
1049 returns whether patch was applied with fuzz factor."""
1050
1050
1051 fuzz = False
1051 fuzz = False
1052 if cwd:
1052 if cwd:
1053 args.append('-d %s' % util.shellquote(cwd))
1053 args.append('-d %s' % util.shellquote(cwd))
1054 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1054 fp = util.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
1055 util.shellquote(patchname)))
1055 util.shellquote(patchname)))
1056
1056
1057 for line in fp:
1057 for line in fp:
1058 line = line.rstrip()
1058 line = line.rstrip()
1059 ui.note(line + '\n')
1059 ui.note(line + '\n')
1060 if line.startswith('patching file '):
1060 if line.startswith('patching file '):
1061 pf = util.parse_patch_output(line)
1061 pf = util.parse_patch_output(line)
1062 printed_file = False
1062 printed_file = False
1063 files.setdefault(pf, None)
1063 files.setdefault(pf, None)
1064 elif line.find('with fuzz') >= 0:
1064 elif line.find('with fuzz') >= 0:
1065 fuzz = True
1065 fuzz = True
1066 if not printed_file:
1066 if not printed_file:
1067 ui.warn(pf + '\n')
1067 ui.warn(pf + '\n')
1068 printed_file = True
1068 printed_file = True
1069 ui.warn(line + '\n')
1069 ui.warn(line + '\n')
1070 elif line.find('saving rejects to file') >= 0:
1070 elif line.find('saving rejects to file') >= 0:
1071 ui.warn(line + '\n')
1071 ui.warn(line + '\n')
1072 elif line.find('FAILED') >= 0:
1072 elif line.find('FAILED') >= 0:
1073 if not printed_file:
1073 if not printed_file:
1074 ui.warn(pf + '\n')
1074 ui.warn(pf + '\n')
1075 printed_file = True
1075 printed_file = True
1076 ui.warn(line + '\n')
1076 ui.warn(line + '\n')
1077 code = fp.close()
1077 code = fp.close()
1078 if code:
1078 if code:
1079 raise PatchError(_("patch command failed: %s") %
1079 raise PatchError(_("patch command failed: %s") %
1080 util.explain_exit(code)[0])
1080 util.explain_exit(code)[0])
1081 return fuzz
1081 return fuzz
1082
1082
1083 def internalpatch(patchobj, ui, strip, cwd, files={}):
1083 def internalpatch(patchobj, ui, strip, cwd, files={}):
1084 """use builtin patch to apply <patchobj> to the working directory.
1084 """use builtin patch to apply <patchobj> to the working directory.
1085 returns whether patch was applied with fuzz factor."""
1085 returns whether patch was applied with fuzz factor."""
1086 try:
1086 try:
1087 fp = file(patchobj, 'rb')
1087 fp = file(patchobj, 'rb')
1088 except TypeError:
1088 except TypeError:
1089 fp = patchobj
1089 fp = patchobj
1090 if cwd:
1090 if cwd:
1091 curdir = os.getcwd()
1091 curdir = os.getcwd()
1092 os.chdir(cwd)
1092 os.chdir(cwd)
1093 try:
1093 try:
1094 ret = applydiff(ui, fp, files, strip=strip)
1094 ret = applydiff(ui, fp, files, strip=strip)
1095 finally:
1095 finally:
1096 if cwd:
1096 if cwd:
1097 os.chdir(curdir)
1097 os.chdir(curdir)
1098 if ret < 0:
1098 if ret < 0:
1099 raise PatchError
1099 raise PatchError
1100 return ret > 0
1100 return ret > 0
1101
1101
1102 def patch(patchname, ui, strip=1, cwd=None, files={}):
1102 def patch(patchname, ui, strip=1, cwd=None, files={}):
1103 """apply <patchname> to the working directory.
1103 """apply <patchname> to the working directory.
1104 returns whether patch was applied with fuzz factor."""
1104 returns whether patch was applied with fuzz factor."""
1105 patcher = ui.config('ui', 'patch')
1105 patcher = ui.config('ui', 'patch')
1106 args = []
1106 args = []
1107 try:
1107 try:
1108 if patcher:
1108 if patcher:
1109 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1109 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1110 files)
1110 files)
1111 else:
1111 else:
1112 try:
1112 try:
1113 return internalpatch(patchname, ui, strip, cwd, files)
1113 return internalpatch(patchname, ui, strip, cwd, files)
1114 except NoHunks:
1114 except NoHunks:
1115 patcher = util.find_exe('gpatch') or util.find_exe('patch')
1115 patcher = util.find_exe('gpatch') or util.find_exe('patch')
1116 ui.debug(_('no valid hunks found; trying with %r instead\n') %
1116 ui.debug(_('no valid hunks found; trying with %r instead\n') %
1117 patcher)
1117 patcher)
1118 if util.needbinarypatch():
1118 if util.needbinarypatch():
1119 args.append('--binary')
1119 args.append('--binary')
1120 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1120 return externalpatch(patcher, args, patchname, ui, strip, cwd,
1121 files)
1121 files)
1122 except PatchError, err:
1122 except PatchError, err:
1123 s = str(err)
1123 s = str(err)
1124 if s:
1124 if s:
1125 raise util.Abort(s)
1125 raise util.Abort(s)
1126 else:
1126 else:
1127 raise util.Abort(_('patch failed to apply'))
1127 raise util.Abort(_('patch failed to apply'))
1128
1128
1129 def b85diff(to, tn):
1129 def b85diff(to, tn):
1130 '''print base85-encoded binary diff'''
1130 '''print base85-encoded binary diff'''
1131 def gitindex(text):
1131 def gitindex(text):
1132 if not text:
1132 if not text:
1133 return '0' * 40
1133 return '0' * 40
1134 l = len(text)
1134 l = len(text)
1135 s = util.sha1('blob %d\0' % l)
1135 s = util.sha1('blob %d\0' % l)
1136 s.update(text)
1136 s.update(text)
1137 return s.hexdigest()
1137 return s.hexdigest()
1138
1138
1139 def fmtline(line):
1139 def fmtline(line):
1140 l = len(line)
1140 l = len(line)
1141 if l <= 26:
1141 if l <= 26:
1142 l = chr(ord('A') + l - 1)
1142 l = chr(ord('A') + l - 1)
1143 else:
1143 else:
1144 l = chr(l - 26 + ord('a') - 1)
1144 l = chr(l - 26 + ord('a') - 1)
1145 return '%c%s\n' % (l, base85.b85encode(line, True))
1145 return '%c%s\n' % (l, base85.b85encode(line, True))
1146
1146
1147 def chunk(text, csize=52):
1147 def chunk(text, csize=52):
1148 l = len(text)
1148 l = len(text)
1149 i = 0
1149 i = 0
1150 while i < l:
1150 while i < l:
1151 yield text[i:i+csize]
1151 yield text[i:i+csize]
1152 i += csize
1152 i += csize
1153
1153
1154 tohash = gitindex(to)
1154 tohash = gitindex(to)
1155 tnhash = gitindex(tn)
1155 tnhash = gitindex(tn)
1156 if tohash == tnhash:
1156 if tohash == tnhash:
1157 return ""
1157 return ""
1158
1158
1159 # TODO: deltas
1159 # TODO: deltas
1160 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1160 ret = ['index %s..%s\nGIT binary patch\nliteral %s\n' %
1161 (tohash, tnhash, len(tn))]
1161 (tohash, tnhash, len(tn))]
1162 for l in chunk(zlib.compress(tn)):
1162 for l in chunk(zlib.compress(tn)):
1163 ret.append(fmtline(l))
1163 ret.append(fmtline(l))
1164 ret.append('\n')
1164 ret.append('\n')
1165 return ''.join(ret)
1165 return ''.join(ret)
1166
1166
1167 def _addmodehdr(header, omode, nmode):
1167 def _addmodehdr(header, omode, nmode):
1168 if omode != nmode:
1168 if omode != nmode:
1169 header.append('old mode %s\n' % omode)
1169 header.append('old mode %s\n' % omode)
1170 header.append('new mode %s\n' % nmode)
1170 header.append('new mode %s\n' % nmode)
1171
1171
1172 def diff(repo, node1=None, node2=None, match=None, changes=None, opts=None):
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
1173 '''yields diff of changes to files between two nodes, or node and
1174 working directory.
1174 working directory.
1175
1175
1176 if node1 is None, use first dirstate parent instead.
1176 if node1 is None, use first dirstate parent instead.
1177 if node2 is None, compare node1 with working directory.'''
1177 if node2 is None, compare node1 with working directory.'''
1178
1178
1179 if not match:
1179 if not match:
1180 match = cmdutil.matchall(repo)
1180 match = cmdutil.matchall(repo)
1181
1181
1182 if opts is None:
1182 if opts is None:
1183 opts = mdiff.defaultopts
1183 opts = mdiff.defaultopts
1184
1184
1185 if not node1:
1185 if not node1:
1186 node1 = repo.dirstate.parents()[0]
1186 node1 = repo.dirstate.parents()[0]
1187
1187
1188 flcache = {}
1188 flcache = {}
1189 def getfilectx(f, ctx):
1189 def getfilectx(f, ctx):
1190 flctx = ctx.filectx(f, filelog=flcache.get(f))
1190 flctx = ctx.filectx(f, filelog=flcache.get(f))
1191 if f not in flcache:
1191 if f not in flcache:
1192 flcache[f] = flctx._filelog
1192 flcache[f] = flctx._filelog
1193 return flctx
1193 return flctx
1194
1194
1195 ctx1 = repo[node1]
1195 ctx1 = repo[node1]
1196 ctx2 = repo[node2]
1196 ctx2 = repo[node2]
1197
1197
1198 if not changes:
1198 if not changes:
1199 changes = repo.status(ctx1, ctx2, match=match)
1199 changes = repo.status(ctx1, ctx2, match=match)
1200 modified, added, removed = changes[:3]
1200 modified, added, removed = changes[:3]
1201
1201
1202 if not modified and not added and not removed:
1202 if not modified and not added and not removed:
1203 return
1203 return
1204
1204
1205 date1 = util.datestr(ctx1.date())
1205 date1 = util.datestr(ctx1.date())
1206 man1 = ctx1.manifest()
1206 man1 = ctx1.manifest()
1207
1207
1208 if repo.ui.quiet:
1208 if repo.ui.quiet:
1209 r = None
1209 r = None
1210 else:
1210 else:
1211 hexfunc = repo.ui.debugflag and hex or short
1211 hexfunc = repo.ui.debugflag and hex or short
1212 r = [hexfunc(node) for node in [node1, node2] if node]
1212 r = [hexfunc(node) for node in [node1, node2] if node]
1213
1213
1214 if opts.git:
1214 if opts.git:
1215 copy, diverge = copies.copies(repo, ctx1, ctx2, repo[nullid])
1215 copy, diverge = copies.copies(repo, ctx1, ctx2, repo[nullid])
1216 for k, v in copy.items():
1216 for k, v in copy.items():
1217 copy[v] = k
1217 copy[v] = k
1218
1218
1219 gone = {}
1219 gone = {}
1220 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1220 gitmode = {'l': '120000', 'x': '100755', '': '100644'}
1221
1221
1222 for f in util.sort(modified + added + removed):
1222 for f in util.sort(modified + added + removed):
1223 to = None
1223 to = None
1224 tn = None
1224 tn = None
1225 dodiff = True
1225 dodiff = True
1226 header = []
1226 header = []
1227 if f in man1:
1227 if f in man1:
1228 to = getfilectx(f, ctx1).data()
1228 to = getfilectx(f, ctx1).data()
1229 if f not in removed:
1229 if f not in removed:
1230 tn = getfilectx(f, ctx2).data()
1230 tn = getfilectx(f, ctx2).data()
1231 a, b = f, f
1231 a, b = f, f
1232 if opts.git:
1232 if opts.git:
1233 if f in added:
1233 if f in added:
1234 mode = gitmode[ctx2.flags(f)]
1234 mode = gitmode[ctx2.flags(f)]
1235 if f in copy:
1235 if f in copy:
1236 a = copy[f]
1236 a = copy[f]
1237 omode = gitmode[man1.flags(a)]
1237 omode = gitmode[man1.flags(a)]
1238 _addmodehdr(header, omode, mode)
1238 _addmodehdr(header, omode, mode)
1239 if a in removed and a not in gone:
1239 if a in removed and a not in gone:
1240 op = 'rename'
1240 op = 'rename'
1241 gone[a] = 1
1241 gone[a] = 1
1242 else:
1242 else:
1243 op = 'copy'
1243 op = 'copy'
1244 header.append('%s from %s\n' % (op, a))
1244 header.append('%s from %s\n' % (op, a))
1245 header.append('%s to %s\n' % (op, f))
1245 header.append('%s to %s\n' % (op, f))
1246 to = getfilectx(a, ctx1).data()
1246 to = getfilectx(a, ctx1).data()
1247 else:
1247 else:
1248 header.append('new file mode %s\n' % mode)
1248 header.append('new file mode %s\n' % mode)
1249 if util.binary(tn):
1249 if util.binary(tn):
1250 dodiff = 'binary'
1250 dodiff = 'binary'
1251 elif f in removed:
1251 elif f in removed:
1252 # have we already reported a copy above?
1252 # have we already reported a copy above?
1253 if f in copy and copy[f] in added and copy[copy[f]] == f:
1253 if f in copy and copy[f] in added and copy[copy[f]] == f:
1254 dodiff = False
1254 dodiff = False
1255 else:
1255 else:
1256 header.append('deleted file mode %s\n' %
1256 header.append('deleted file mode %s\n' %
1257 gitmode[man1.flags(f)])
1257 gitmode[man1.flags(f)])
1258 else:
1258 else:
1259 omode = gitmode[man1.flags(f)]
1259 omode = gitmode[man1.flags(f)]
1260 nmode = gitmode[ctx2.flags(f)]
1260 nmode = gitmode[ctx2.flags(f)]
1261 _addmodehdr(header, omode, nmode)
1261 _addmodehdr(header, omode, nmode)
1262 if util.binary(to) or util.binary(tn):
1262 if util.binary(to) or util.binary(tn):
1263 dodiff = 'binary'
1263 dodiff = 'binary'
1264 r = None
1264 r = None
1265 header.insert(0, mdiff.diffline(r, a, b, opts))
1265 header.insert(0, mdiff.diffline(r, a, b, opts))
1266 if dodiff:
1266 if dodiff:
1267 if dodiff == 'binary':
1267 if dodiff == 'binary':
1268 text = b85diff(to, tn)
1268 text = b85diff(to, tn)
1269 else:
1269 else:
1270 text = mdiff.unidiff(to, date1,
1270 text = mdiff.unidiff(to, date1,
1271 # ctx2 date may be dynamic
1271 # ctx2 date may be dynamic
1272 tn, util.datestr(ctx2.date()),
1272 tn, util.datestr(ctx2.date()),
1273 a, b, r, opts=opts)
1273 a, b, r, opts=opts)
1274 if header and (text or len(header) > 1):
1274 if header and (text or len(header) > 1):
1275 yield ''.join(header)
1275 yield ''.join(header)
1276 if text:
1276 if text:
1277 yield text
1277 yield text
1278
1278
1279 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1279 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1280 opts=None):
1280 opts=None):
1281 '''export changesets as hg patches.'''
1281 '''export changesets as hg patches.'''
1282
1282
1283 total = len(revs)
1283 total = len(revs)
1284 revwidth = max([len(str(rev)) for rev in revs])
1284 revwidth = max([len(str(rev)) for rev in revs])
1285
1285
1286 def single(rev, seqno, fp):
1286 def single(rev, seqno, fp):
1287 ctx = repo[rev]
1287 ctx = repo[rev]
1288 node = ctx.node()
1288 node = ctx.node()
1289 parents = [p.node() for p in ctx.parents() if p]
1289 parents = [p.node() for p in ctx.parents() if p]
1290 branch = ctx.branch()
1290 branch = ctx.branch()
1291 if switch_parent:
1291 if switch_parent:
1292 parents.reverse()
1292 parents.reverse()
1293 prev = (parents and parents[0]) or nullid
1293 prev = (parents and parents[0]) or nullid
1294
1294
1295 if not fp:
1295 if not fp:
1296 fp = cmdutil.make_file(repo, template, node, total=total,
1296 fp = cmdutil.make_file(repo, template, node, total=total,
1297 seqno=seqno, revwidth=revwidth)
1297 seqno=seqno, revwidth=revwidth,
1298 mode='ab')
1298 if fp != sys.stdout and hasattr(fp, 'name'):
1299 if fp != sys.stdout and hasattr(fp, 'name'):
1299 repo.ui.note("%s\n" % fp.name)
1300 repo.ui.note("%s\n" % fp.name)
1300
1301
1301 fp.write("# HG changeset patch\n")
1302 fp.write("# HG changeset patch\n")
1302 fp.write("# User %s\n" % ctx.user())
1303 fp.write("# User %s\n" % ctx.user())
1303 fp.write("# Date %d %d\n" % ctx.date())
1304 fp.write("# Date %d %d\n" % ctx.date())
1304 if branch and (branch != 'default'):
1305 if branch and (branch != 'default'):
1305 fp.write("# Branch %s\n" % branch)
1306 fp.write("# Branch %s\n" % branch)
1306 fp.write("# Node ID %s\n" % hex(node))
1307 fp.write("# Node ID %s\n" % hex(node))
1307 fp.write("# Parent %s\n" % hex(prev))
1308 fp.write("# Parent %s\n" % hex(prev))
1308 if len(parents) > 1:
1309 if len(parents) > 1:
1309 fp.write("# Parent %s\n" % hex(parents[1]))
1310 fp.write("# Parent %s\n" % hex(parents[1]))
1310 fp.write(ctx.description().rstrip())
1311 fp.write(ctx.description().rstrip())
1311 fp.write("\n\n")
1312 fp.write("\n\n")
1312
1313
1313 for chunk in diff(repo, prev, node, opts=opts):
1314 for chunk in diff(repo, prev, node, opts=opts):
1314 fp.write(chunk)
1315 fp.write(chunk)
1315 if fp not in (sys.stdout, repo.ui):
1316 if fp not in (sys.stdout, repo.ui):
1316 fp.close()
1317 fp.close()
1317
1318
1318 for seqno, rev in enumerate(revs):
1319 for seqno, rev in enumerate(revs):
1319 single(rev, seqno+1, fp)
1320 single(rev, seqno+1, fp)
1320
1321
1321 def diffstat(patchlines):
1322 def diffstat(patchlines):
1322 if not util.find_exe('diffstat'):
1323 if not util.find_exe('diffstat'):
1323 return
1324 return
1324 output = util.filter('\n'.join(patchlines),
1325 output = util.filter('\n'.join(patchlines),
1325 'diffstat -p1 -w79 2>%s' % util.nulldev)
1326 'diffstat -p1 -w79 2>%s' % util.nulldev)
1326 stat = [l.lstrip() for l in output.splitlines(True)]
1327 stat = [l.lstrip() for l in output.splitlines(True)]
1327 last = stat.pop()
1328 last = stat.pop()
1328 stat.insert(0, last)
1329 stat.insert(0, last)
1329 stat = ''.join(stat)
1330 stat = ''.join(stat)
1330 return stat
1331 return stat
@@ -1,15 +1,21 b''
1 #!/bin/sh
1 #!/bin/sh
2
2
3 hg init repo
3 hg init repo
4 cd repo
4 cd repo
5 touch foo
5 touch foo
6 hg add foo
6 hg add foo
7 for i in 0 1 2 3 4 5 6 7 8 9 10 11; do
7 for i in 0 1 2 3 4 5 6 7 8 9 10 11; do
8 echo "foo-$i" >> foo
8 echo "foo-$i" >> foo
9 hg ci -m "foo-$i" -d "0 0"
9 hg ci -m "foo-$i" -d "0 0"
10 done
10 done
11
11
12 for out in "%nof%N" "%%%H" "%b-%R" "%h" "%r"; do
12 for out in "%nof%N" "%%%H" "%b-%R" "%h" "%r"; do
13 echo "# foo-$out.patch"
13 echo "# foo-$out.patch"
14 hg export -v -o "foo-$out.patch" 2:tip
14 hg export -v -o "foo-$out.patch" 2:tip
15 done
15 done
16
17 echo "# exporting 4 changesets to a file"
18 hg export -o export_internal 1 2 3 4
19 grep HG export_internal | wc -l
20 echo "# exporting 4 changesets to a file"
21 hg export 1 2 3 4 | grep HG | wc -l
@@ -1,60 +1,64 b''
1 # foo-%nof%N.patch
1 # foo-%nof%N.patch
2 exporting patches:
2 exporting patches:
3 foo-01of10.patch
3 foo-01of10.patch
4 foo-02of10.patch
4 foo-02of10.patch
5 foo-03of10.patch
5 foo-03of10.patch
6 foo-04of10.patch
6 foo-04of10.patch
7 foo-05of10.patch
7 foo-05of10.patch
8 foo-06of10.patch
8 foo-06of10.patch
9 foo-07of10.patch
9 foo-07of10.patch
10 foo-08of10.patch
10 foo-08of10.patch
11 foo-09of10.patch
11 foo-09of10.patch
12 foo-10of10.patch
12 foo-10of10.patch
13 # foo-%%%H.patch
13 # foo-%%%H.patch
14 exporting patches:
14 exporting patches:
15 foo-%617188a1c80f869a7b66c85134da88a6fb145f67.patch
15 foo-%617188a1c80f869a7b66c85134da88a6fb145f67.patch
16 foo-%dd41a5ff707a5225204105611ba49cc5c229d55f.patch
16 foo-%dd41a5ff707a5225204105611ba49cc5c229d55f.patch
17 foo-%f95a5410f8664b6e1490a4af654e4b7d41a7b321.patch
17 foo-%f95a5410f8664b6e1490a4af654e4b7d41a7b321.patch
18 foo-%4346bcfde53b4d9042489078bcfa9c3e28201db2.patch
18 foo-%4346bcfde53b4d9042489078bcfa9c3e28201db2.patch
19 foo-%afda8c3a009cc99449a05ad8aa4655648c4ecd34.patch
19 foo-%afda8c3a009cc99449a05ad8aa4655648c4ecd34.patch
20 foo-%35284ce2b6b99c9d2ac66268fe99e68e1974e1aa.patch
20 foo-%35284ce2b6b99c9d2ac66268fe99e68e1974e1aa.patch
21 foo-%9688c41894e6931305fa7165a37f6568050b4e9b.patch
21 foo-%9688c41894e6931305fa7165a37f6568050b4e9b.patch
22 foo-%747d3c68f8ec44bb35816bfcd59aeb50b9654c2f.patch
22 foo-%747d3c68f8ec44bb35816bfcd59aeb50b9654c2f.patch
23 foo-%5f17a83f5fbd9414006a5e563eab4c8a00729efd.patch
23 foo-%5f17a83f5fbd9414006a5e563eab4c8a00729efd.patch
24 foo-%f3acbafac161ec68f1598af38f794f28847ca5d3.patch
24 foo-%f3acbafac161ec68f1598af38f794f28847ca5d3.patch
25 # foo-%b-%R.patch
25 # foo-%b-%R.patch
26 exporting patches:
26 exporting patches:
27 foo-repo-2.patch
27 foo-repo-2.patch
28 foo-repo-3.patch
28 foo-repo-3.patch
29 foo-repo-4.patch
29 foo-repo-4.patch
30 foo-repo-5.patch
30 foo-repo-5.patch
31 foo-repo-6.patch
31 foo-repo-6.patch
32 foo-repo-7.patch
32 foo-repo-7.patch
33 foo-repo-8.patch
33 foo-repo-8.patch
34 foo-repo-9.patch
34 foo-repo-9.patch
35 foo-repo-10.patch
35 foo-repo-10.patch
36 foo-repo-11.patch
36 foo-repo-11.patch
37 # foo-%h.patch
37 # foo-%h.patch
38 exporting patches:
38 exporting patches:
39 foo-617188a1c80f.patch
39 foo-617188a1c80f.patch
40 foo-dd41a5ff707a.patch
40 foo-dd41a5ff707a.patch
41 foo-f95a5410f866.patch
41 foo-f95a5410f866.patch
42 foo-4346bcfde53b.patch
42 foo-4346bcfde53b.patch
43 foo-afda8c3a009c.patch
43 foo-afda8c3a009c.patch
44 foo-35284ce2b6b9.patch
44 foo-35284ce2b6b9.patch
45 foo-9688c41894e6.patch
45 foo-9688c41894e6.patch
46 foo-747d3c68f8ec.patch
46 foo-747d3c68f8ec.patch
47 foo-5f17a83f5fbd.patch
47 foo-5f17a83f5fbd.patch
48 foo-f3acbafac161.patch
48 foo-f3acbafac161.patch
49 # foo-%r.patch
49 # foo-%r.patch
50 exporting patches:
50 exporting patches:
51 foo-02.patch
51 foo-02.patch
52 foo-03.patch
52 foo-03.patch
53 foo-04.patch
53 foo-04.patch
54 foo-05.patch
54 foo-05.patch
55 foo-06.patch
55 foo-06.patch
56 foo-07.patch
56 foo-07.patch
57 foo-08.patch
57 foo-08.patch
58 foo-09.patch
58 foo-09.patch
59 foo-10.patch
59 foo-10.patch
60 foo-11.patch
60 foo-11.patch
61 # exporting 4 changesets to a file
62 4
63 # exporting 4 changesets to a file
64 4
General Comments 0
You need to be logged in to leave comments. Login now