##// END OF EJS Templates
merge with crew-stable
Alexis S. L. Carvalho -
r4096:49237d6a merge default
parent child Browse files
Show More
@@ -1,187 +1,192 b''
1 1 # extdiff.py - external diff program support for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7 #
8 8 # The `extdiff' Mercurial extension allows you to use external programs
9 9 # to compare revisions, or revision with working dir. The external diff
10 10 # programs are called with a configurable set of options and two
11 11 # non-option arguments: paths to directories containing snapshots of
12 12 # files to compare.
13 13 #
14 14 # To enable this extension:
15 15 #
16 16 # [extensions]
17 17 # hgext.extdiff =
18 18 #
19 19 # The `extdiff' extension also allows to configure new diff commands, so
20 20 # you do not need to type "hg extdiff -p kdiff3" always.
21 21 #
22 22 # [extdiff]
23 23 # # add new command that runs GNU diff(1) in 'context diff' mode
24 24 # cmd.cdiff = gdiff
25 25 # opts.cdiff = -Nprc5
26 26
27 27 # # add new command called vdiff, runs kdiff3
28 28 # cmd.vdiff = kdiff3
29 29
30 30 # # add new command called meld, runs meld (no need to name twice)
31 31 # cmd.meld =
32 32
33 33 # # add new command called vimdiff, runs gvimdiff with DirDiff plugin
34 34 # #(see http://www.vim.org/scripts/script.php?script_id=102)
35 35 # # Non english user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
36 36 # # your .vimrc
37 37 # cmd.vimdiff = gvim
38 38 # opts.vimdiff = -f '+next' '+execute "DirDiff" argv(0) argv(1)'
39 39 #
40 40 # Each custom diff commands can have two parts: a `cmd' and an `opts'
41 41 # part. The cmd.xxx option defines the name of an executable program
42 42 # that will be run, and opts.xxx defines a set of command-line options
43 43 # which will be inserted to the command between the program name and
44 44 # the files/directories to diff (i.e. the cdiff example above).
45 45 #
46 46 # You can use -I/-X and list of file or directory names like normal
47 47 # "hg diff" command. The `extdiff' extension makes snapshots of only
48 48 # needed files, so running the external diff program will actually be
49 49 # pretty fast (at least faster than having to compare the entire tree).
50 50
51 51 from mercurial.i18n import _
52 52 from mercurial.node import *
53 53 from mercurial import cmdutil, util
54 54 import os, shutil, tempfile
55 55
56 56 def dodiff(ui, repo, diffcmd, diffopts, pats, opts):
57 57 def snapshot_node(files, node):
58 58 '''snapshot files as of some revision'''
59 59 mf = repo.changectx(node).manifest()
60 dirname = '%s.%s' % (os.path.basename(repo.root), short(node))
60 dirname = os.path.basename(repo.root)
61 if dirname == "":
62 dirname = "root"
63 dirname = '%s.%s' % (dirname, short(node))
61 64 base = os.path.join(tmproot, dirname)
62 65 os.mkdir(base)
63 66 if not ui.quiet:
64 67 ui.write_err(_('making snapshot of %d files from rev %s\n') %
65 68 (len(files), short(node)))
66 69 for fn in files:
67 70 if not fn in mf:
68 71 # skipping new file after a merge ?
69 72 continue
70 73 wfn = util.pconvert(fn)
71 74 ui.note(' %s\n' % wfn)
72 75 dest = os.path.join(base, wfn)
73 76 destdir = os.path.dirname(dest)
74 77 if not os.path.isdir(destdir):
75 78 os.makedirs(destdir)
76 79 data = repo.wwritedata(wfn, repo.file(wfn).read(mf[wfn]))
77 open(dest, 'w').write(data)
80 open(dest, 'wb').write(data)
78 81 return dirname
79 82
80 83 def snapshot_wdir(files):
81 84 '''snapshot files from working directory.
82 85 if not using snapshot, -I/-X does not work and recursive diff
83 86 in tools like kdiff3 and meld displays too many files.'''
84 87 dirname = os.path.basename(repo.root)
88 if dirname == "":
89 dirname = "root"
85 90 base = os.path.join(tmproot, dirname)
86 91 os.mkdir(base)
87 92 if not ui.quiet:
88 93 ui.write_err(_('making snapshot of %d files from working dir\n') %
89 94 (len(files)))
90 95 for fn in files:
91 96 wfn = util.pconvert(fn)
92 97 ui.note(' %s\n' % wfn)
93 98 dest = os.path.join(base, wfn)
94 99 destdir = os.path.dirname(dest)
95 100 if not os.path.isdir(destdir):
96 101 os.makedirs(destdir)
97 fp = open(dest, 'w')
102 fp = open(dest, 'wb')
98 103 for chunk in util.filechunkiter(repo.wopener(wfn)):
99 104 fp.write(chunk)
100 105 return dirname
101 106
102 107 node1, node2 = cmdutil.revpair(repo, opts['rev'])
103 108 files, matchfn, anypats = cmdutil.matchpats(repo, pats, opts)
104 109 modified, added, removed, deleted, unknown = repo.status(
105 110 node1, node2, files, match=matchfn)[:5]
106 111 if not (modified or added or removed):
107 112 return 0
108 113
109 114 tmproot = tempfile.mkdtemp(prefix='extdiff.')
110 115 try:
111 116 dir1 = snapshot_node(modified + removed, node1)
112 117 if node2:
113 118 dir2 = snapshot_node(modified + added, node2)
114 119 else:
115 120 dir2 = snapshot_wdir(modified + added)
116 121 cmdline = ('%s %s %s %s' %
117 122 (util.shellquote(diffcmd), ' '.join(diffopts),
118 123 util.shellquote(dir1), util.shellquote(dir2)))
119 124 ui.debug('running %r in %s\n' % (cmdline, tmproot))
120 125 util.system(cmdline, cwd=tmproot)
121 126 return 1
122 127 finally:
123 128 ui.note(_('cleaning up temp directory\n'))
124 129 shutil.rmtree(tmproot)
125 130
126 131 def extdiff(ui, repo, *pats, **opts):
127 132 '''use external program to diff repository (or selected files)
128 133
129 134 Show differences between revisions for the specified files, using
130 135 an external program. The default program used is diff, with
131 136 default options "-Npru".
132 137
133 138 To select a different program, use the -p option. The program
134 139 will be passed the names of two directories to compare. To pass
135 140 additional options to the program, use the -o option. These will
136 141 be passed before the names of the directories to compare.
137 142
138 143 When two revision arguments are given, then changes are
139 144 shown between those revisions. If only one revision is
140 145 specified then that revision is compared to the working
141 146 directory, and, when no revisions are specified, the
142 147 working directory files are compared to its parent.'''
143 148 program = opts['program'] or 'diff'
144 149 if opts['program']:
145 150 option = opts['option']
146 151 else:
147 152 option = opts['option'] or ['-Npru']
148 153 return dodiff(ui, repo, program, option, pats, opts)
149 154
150 155 cmdtable = {
151 156 "extdiff":
152 157 (extdiff,
153 158 [('p', 'program', '', _('comparison program to run')),
154 159 ('o', 'option', [], _('pass option to comparison program')),
155 160 ('r', 'rev', [], _('revision')),
156 161 ('I', 'include', [], _('include names matching the given patterns')),
157 162 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
158 163 _('hg extdiff [OPT]... [FILE]...')),
159 164 }
160 165
161 166 def uisetup(ui):
162 167 for cmd, path in ui.configitems('extdiff'):
163 168 if not cmd.startswith('cmd.'): continue
164 169 cmd = cmd[4:]
165 170 if not path: path = cmd
166 171 diffopts = ui.config('extdiff', 'opts.' + cmd, '')
167 172 diffopts = diffopts and [diffopts] or []
168 173 def save(cmd, path, diffopts):
169 174 '''use closure to save diff command to use'''
170 175 def mydiff(ui, repo, *pats, **opts):
171 176 return dodiff(ui, repo, path, diffopts, pats, opts)
172 177 mydiff.__doc__ = '''use %(path)r to diff repository (or selected files)
173 178
174 179 Show differences between revisions for the specified
175 180 files, using the %(path)r program.
176 181
177 182 When two revision arguments are given, then changes are
178 183 shown between those revisions. If only one revision is
179 184 specified then that revision is compared to the working
180 185 directory, and, when no revisions are specified, the
181 186 working directory files are compared to its parent.''' % {
182 187 'path': path,
183 188 }
184 189 return mydiff
185 190 cmdtable[cmd] = (save(cmd, path, diffopts),
186 191 cmdtable['extdiff'][1][1:],
187 192 _('hg %s [OPT]... [FILE]...') % cmd)
@@ -1,2196 +1,2194 b''
1 1 # queue.py - patch queues for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Chris Mason <mason@suse.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 '''patch management and development
9 9
10 10 This extension lets you work with a stack of patches in a Mercurial
11 11 repository. It manages two stacks of patches - all known patches, and
12 12 applied patches (subset of known patches).
13 13
14 14 Known patches are represented as patch files in the .hg/patches
15 15 directory. Applied patches are both patch files and changesets.
16 16
17 17 Common tasks (use "hg help command" for more details):
18 18
19 19 prepare repository to work with patches qinit
20 20 create new patch qnew
21 21 import existing patch qimport
22 22
23 23 print patch series qseries
24 24 print applied patches qapplied
25 25 print name of top applied patch qtop
26 26
27 27 add known patch to applied stack qpush
28 28 remove patch from applied stack qpop
29 29 refresh contents of top applied patch qrefresh
30 30 '''
31 31
32 32 from mercurial.i18n import _
33 33 from mercurial import commands, cmdutil, hg, patch, revlog, util, changegroup
34 34 import os, sys, re, errno
35 35
36 36 commands.norepo += " qclone qversion"
37 37
38 38 # Patch names looks like unix-file names.
39 39 # They must be joinable with queue directory and result in the patch path.
40 40 normname = util.normpath
41 41
42 42 class statusentry:
43 43 def __init__(self, rev, name=None):
44 44 if not name:
45 45 fields = rev.split(':', 1)
46 46 if len(fields) == 2:
47 47 self.rev, self.name = fields
48 48 else:
49 49 self.rev, self.name = None, None
50 50 else:
51 51 self.rev, self.name = rev, name
52 52
53 53 def __str__(self):
54 54 return self.rev + ':' + self.name
55 55
56 56 class queue:
57 57 def __init__(self, ui, path, patchdir=None):
58 58 self.basepath = path
59 59 self.path = patchdir or os.path.join(path, "patches")
60 60 self.opener = util.opener(self.path)
61 61 self.ui = ui
62 62 self.applied = []
63 63 self.full_series = []
64 64 self.applied_dirty = 0
65 65 self.series_dirty = 0
66 66 self.series_path = "series"
67 67 self.status_path = "status"
68 68 self.guards_path = "guards"
69 69 self.active_guards = None
70 70 self.guards_dirty = False
71 71 self._diffopts = None
72 72
73 73 if os.path.exists(self.join(self.series_path)):
74 74 self.full_series = self.opener(self.series_path).read().splitlines()
75 75 self.parse_series()
76 76
77 77 if os.path.exists(self.join(self.status_path)):
78 78 lines = self.opener(self.status_path).read().splitlines()
79 79 self.applied = [statusentry(l) for l in lines]
80 80
81 81 def diffopts(self):
82 82 if self._diffopts is None:
83 83 self._diffopts = patch.diffopts(self.ui)
84 84 return self._diffopts
85 85
86 86 def join(self, *p):
87 87 return os.path.join(self.path, *p)
88 88
89 89 def find_series(self, patch):
90 90 pre = re.compile("(\s*)([^#]+)")
91 91 index = 0
92 92 for l in self.full_series:
93 93 m = pre.match(l)
94 94 if m:
95 95 s = m.group(2)
96 96 s = s.rstrip()
97 97 if s == patch:
98 98 return index
99 99 index += 1
100 100 return None
101 101
102 102 guard_re = re.compile(r'\s?#([-+][^-+# \t\r\n\f][^# \t\r\n\f]*)')
103 103
104 104 def parse_series(self):
105 105 self.series = []
106 106 self.series_guards = []
107 107 for l in self.full_series:
108 108 h = l.find('#')
109 109 if h == -1:
110 110 patch = l
111 111 comment = ''
112 112 elif h == 0:
113 113 continue
114 114 else:
115 115 patch = l[:h]
116 116 comment = l[h:]
117 117 patch = patch.strip()
118 118 if patch:
119 119 if patch in self.series:
120 120 raise util.Abort(_('%s appears more than once in %s') %
121 121 (patch, self.join(self.series_path)))
122 122 self.series.append(patch)
123 123 self.series_guards.append(self.guard_re.findall(comment))
124 124
125 125 def check_guard(self, guard):
126 126 bad_chars = '# \t\r\n\f'
127 127 first = guard[0]
128 128 for c in '-+':
129 129 if first == c:
130 130 return (_('guard %r starts with invalid character: %r') %
131 131 (guard, c))
132 132 for c in bad_chars:
133 133 if c in guard:
134 134 return _('invalid character in guard %r: %r') % (guard, c)
135 135
136 136 def set_active(self, guards):
137 137 for guard in guards:
138 138 bad = self.check_guard(guard)
139 139 if bad:
140 140 raise util.Abort(bad)
141 141 guards = dict.fromkeys(guards).keys()
142 142 guards.sort()
143 143 self.ui.debug('active guards: %s\n' % ' '.join(guards))
144 144 self.active_guards = guards
145 145 self.guards_dirty = True
146 146
147 147 def active(self):
148 148 if self.active_guards is None:
149 149 self.active_guards = []
150 150 try:
151 151 guards = self.opener(self.guards_path).read().split()
152 152 except IOError, err:
153 153 if err.errno != errno.ENOENT: raise
154 154 guards = []
155 155 for i, guard in enumerate(guards):
156 156 bad = self.check_guard(guard)
157 157 if bad:
158 158 self.ui.warn('%s:%d: %s\n' %
159 159 (self.join(self.guards_path), i + 1, bad))
160 160 else:
161 161 self.active_guards.append(guard)
162 162 return self.active_guards
163 163
164 164 def set_guards(self, idx, guards):
165 165 for g in guards:
166 166 if len(g) < 2:
167 167 raise util.Abort(_('guard %r too short') % g)
168 168 if g[0] not in '-+':
169 169 raise util.Abort(_('guard %r starts with invalid char') % g)
170 170 bad = self.check_guard(g[1:])
171 171 if bad:
172 172 raise util.Abort(bad)
173 173 drop = self.guard_re.sub('', self.full_series[idx])
174 174 self.full_series[idx] = drop + ''.join([' #' + g for g in guards])
175 175 self.parse_series()
176 176 self.series_dirty = True
177 177
178 178 def pushable(self, idx):
179 179 if isinstance(idx, str):
180 180 idx = self.series.index(idx)
181 181 patchguards = self.series_guards[idx]
182 182 if not patchguards:
183 183 return True, None
184 184 default = False
185 185 guards = self.active()
186 186 exactneg = [g for g in patchguards if g[0] == '-' and g[1:] in guards]
187 187 if exactneg:
188 188 return False, exactneg[0]
189 189 pos = [g for g in patchguards if g[0] == '+']
190 190 exactpos = [g for g in pos if g[1:] in guards]
191 191 if pos:
192 192 if exactpos:
193 193 return True, exactpos[0]
194 194 return False, pos
195 195 return True, ''
196 196
197 197 def explain_pushable(self, idx, all_patches=False):
198 198 write = all_patches and self.ui.write or self.ui.warn
199 199 if all_patches or self.ui.verbose:
200 200 if isinstance(idx, str):
201 201 idx = self.series.index(idx)
202 202 pushable, why = self.pushable(idx)
203 203 if all_patches and pushable:
204 204 if why is None:
205 205 write(_('allowing %s - no guards in effect\n') %
206 206 self.series[idx])
207 207 else:
208 208 if not why:
209 209 write(_('allowing %s - no matching negative guards\n') %
210 210 self.series[idx])
211 211 else:
212 212 write(_('allowing %s - guarded by %r\n') %
213 213 (self.series[idx], why))
214 214 if not pushable:
215 215 if why:
216 216 write(_('skipping %s - guarded by %r\n') %
217 217 (self.series[idx], why))
218 218 else:
219 219 write(_('skipping %s - no matching guards\n') %
220 220 self.series[idx])
221 221
222 222 def save_dirty(self):
223 223 def write_list(items, path):
224 224 fp = self.opener(path, 'w')
225 225 for i in items:
226 226 print >> fp, i
227 227 fp.close()
228 228 if self.applied_dirty: write_list(map(str, self.applied), self.status_path)
229 229 if self.series_dirty: write_list(self.full_series, self.series_path)
230 230 if self.guards_dirty: write_list(self.active_guards, self.guards_path)
231 231
232 232 def readheaders(self, patch):
233 233 def eatdiff(lines):
234 234 while lines:
235 235 l = lines[-1]
236 236 if (l.startswith("diff -") or
237 237 l.startswith("Index:") or
238 238 l.startswith("===========")):
239 239 del lines[-1]
240 240 else:
241 241 break
242 242 def eatempty(lines):
243 243 while lines:
244 244 l = lines[-1]
245 245 if re.match('\s*$', l):
246 246 del lines[-1]
247 247 else:
248 248 break
249 249
250 250 pf = self.join(patch)
251 251 message = []
252 252 comments = []
253 253 user = None
254 254 date = None
255 255 format = None
256 256 subject = None
257 257 diffstart = 0
258 258
259 259 for line in file(pf):
260 260 line = line.rstrip()
261 261 if line.startswith('diff --git'):
262 262 diffstart = 2
263 263 break
264 264 if diffstart:
265 265 if line.startswith('+++ '):
266 266 diffstart = 2
267 267 break
268 268 if line.startswith("--- "):
269 269 diffstart = 1
270 270 continue
271 271 elif format == "hgpatch":
272 272 # parse values when importing the result of an hg export
273 273 if line.startswith("# User "):
274 274 user = line[7:]
275 275 elif line.startswith("# Date "):
276 276 date = line[7:]
277 277 elif not line.startswith("# ") and line:
278 278 message.append(line)
279 279 format = None
280 280 elif line == '# HG changeset patch':
281 281 format = "hgpatch"
282 282 elif (format != "tagdone" and (line.startswith("Subject: ") or
283 283 line.startswith("subject: "))):
284 284 subject = line[9:]
285 285 format = "tag"
286 286 elif (format != "tagdone" and (line.startswith("From: ") or
287 287 line.startswith("from: "))):
288 288 user = line[6:]
289 289 format = "tag"
290 290 elif format == "tag" and line == "":
291 291 # when looking for tags (subject: from: etc) they
292 292 # end once you find a blank line in the source
293 293 format = "tagdone"
294 294 elif message or line:
295 295 message.append(line)
296 296 comments.append(line)
297 297
298 298 eatdiff(message)
299 299 eatdiff(comments)
300 300 eatempty(message)
301 301 eatempty(comments)
302 302
303 303 # make sure message isn't empty
304 304 if format and format.startswith("tag") and subject:
305 305 message.insert(0, "")
306 306 message.insert(0, subject)
307 307 return (message, comments, user, date, diffstart > 1)
308 308
309 309 def printdiff(self, repo, node1, node2=None, files=None,
310 310 fp=None, changes=None, opts={}):
311 311 fns, matchfn, anypats = cmdutil.matchpats(repo, files, opts)
312 312
313 313 patch.diff(repo, node1, node2, fns, match=matchfn,
314 314 fp=fp, changes=changes, opts=self.diffopts())
315 315
316 316 def mergeone(self, repo, mergeq, head, patch, rev, wlock):
317 317 # first try just applying the patch
318 318 (err, n) = self.apply(repo, [ patch ], update_status=False,
319 319 strict=True, merge=rev, wlock=wlock)
320 320
321 321 if err == 0:
322 322 return (err, n)
323 323
324 324 if n is None:
325 325 raise util.Abort(_("apply failed for patch %s") % patch)
326 326
327 327 self.ui.warn("patch didn't work out, merging %s\n" % patch)
328 328
329 329 # apply failed, strip away that rev and merge.
330 330 hg.clean(repo, head, wlock=wlock)
331 331 self.strip(repo, n, update=False, backup='strip', wlock=wlock)
332 332
333 333 ctx = repo.changectx(rev)
334 334 ret = hg.merge(repo, rev, wlock=wlock)
335 335 if ret:
336 336 raise util.Abort(_("update returned %d") % ret)
337 337 n = repo.commit(None, ctx.description(), ctx.user(),
338 338 force=1, wlock=wlock)
339 339 if n == None:
340 340 raise util.Abort(_("repo commit failed"))
341 341 try:
342 342 message, comments, user, date, patchfound = mergeq.readheaders(patch)
343 343 except:
344 344 raise util.Abort(_("unable to read %s") % patch)
345 345
346 346 patchf = self.opener(patch, "w")
347 347 if comments:
348 348 comments = "\n".join(comments) + '\n\n'
349 349 patchf.write(comments)
350 350 self.printdiff(repo, head, n, fp=patchf)
351 351 patchf.close()
352 352 return (0, n)
353 353
354 354 def qparents(self, repo, rev=None):
355 355 if rev is None:
356 356 (p1, p2) = repo.dirstate.parents()
357 357 if p2 == revlog.nullid:
358 358 return p1
359 359 if len(self.applied) == 0:
360 360 return None
361 361 return revlog.bin(self.applied[-1].rev)
362 362 pp = repo.changelog.parents(rev)
363 363 if pp[1] != revlog.nullid:
364 364 arevs = [ x.rev for x in self.applied ]
365 365 p0 = revlog.hex(pp[0])
366 366 p1 = revlog.hex(pp[1])
367 367 if p0 in arevs:
368 368 return pp[0]
369 369 if p1 in arevs:
370 370 return pp[1]
371 371 return pp[0]
372 372
373 373 def mergepatch(self, repo, mergeq, series, wlock):
374 374 if len(self.applied) == 0:
375 375 # each of the patches merged in will have two parents. This
376 376 # can confuse the qrefresh, qdiff, and strip code because it
377 377 # needs to know which parent is actually in the patch queue.
378 378 # so, we insert a merge marker with only one parent. This way
379 379 # the first patch in the queue is never a merge patch
380 380 #
381 381 pname = ".hg.patches.merge.marker"
382 382 n = repo.commit(None, '[mq]: merge marker', user=None, force=1,
383 383 wlock=wlock)
384 384 self.applied.append(statusentry(revlog.hex(n), pname))
385 385 self.applied_dirty = 1
386 386
387 387 head = self.qparents(repo)
388 388
389 389 for patch in series:
390 390 patch = mergeq.lookup(patch, strict=True)
391 391 if not patch:
392 392 self.ui.warn("patch %s does not exist\n" % patch)
393 393 return (1, None)
394 394 pushable, reason = self.pushable(patch)
395 395 if not pushable:
396 396 self.explain_pushable(patch, all_patches=True)
397 397 continue
398 398 info = mergeq.isapplied(patch)
399 399 if not info:
400 400 self.ui.warn("patch %s is not applied\n" % patch)
401 401 return (1, None)
402 402 rev = revlog.bin(info[1])
403 403 (err, head) = self.mergeone(repo, mergeq, head, patch, rev, wlock)
404 404 if head:
405 405 self.applied.append(statusentry(revlog.hex(head), patch))
406 406 self.applied_dirty = 1
407 407 if err:
408 408 return (err, head)
409 409 return (0, head)
410 410
411 411 def patch(self, repo, patchfile):
412 412 '''Apply patchfile to the working directory.
413 413 patchfile: file name of patch'''
414 414 files = {}
415 415 try:
416 416 fuzz = patch.patch(patchfile, self.ui, strip=1, cwd=repo.root,
417 417 files=files)
418 418 except Exception, inst:
419 419 self.ui.note(str(inst) + '\n')
420 420 if not self.ui.verbose:
421 421 self.ui.warn("patch failed, unable to continue (try -v)\n")
422 422 return (False, files, False)
423 423
424 424 return (True, files, fuzz)
425 425
426 426 def apply(self, repo, series, list=False, update_status=True,
427 427 strict=False, patchdir=None, merge=None, wlock=None):
428 428 # TODO unify with commands.py
429 429 if not patchdir:
430 430 patchdir = self.path
431 431 err = 0
432 432 if not wlock:
433 433 wlock = repo.wlock()
434 434 lock = repo.lock()
435 435 tr = repo.transaction()
436 436 n = None
437 437 for patchname in series:
438 438 pushable, reason = self.pushable(patchname)
439 439 if not pushable:
440 440 self.explain_pushable(patchname, all_patches=True)
441 441 continue
442 442 self.ui.warn("applying %s\n" % patchname)
443 443 pf = os.path.join(patchdir, patchname)
444 444
445 445 try:
446 446 message, comments, user, date, patchfound = self.readheaders(patchname)
447 447 except:
448 448 self.ui.warn("Unable to read %s\n" % patchname)
449 449 err = 1
450 450 break
451 451
452 452 if not message:
453 453 message = "imported patch %s\n" % patchname
454 454 else:
455 455 if list:
456 456 message.append("\nimported patch %s" % patchname)
457 457 message = '\n'.join(message)
458 458
459 459 (patcherr, files, fuzz) = self.patch(repo, pf)
460 460 patcherr = not patcherr
461 461
462 462 if merge and files:
463 463 # Mark as merged and update dirstate parent info
464 464 repo.dirstate.update(repo.dirstate.filterfiles(files.keys()), 'm')
465 465 p1, p2 = repo.dirstate.parents()
466 466 repo.dirstate.setparents(p1, merge)
467 467 files = patch.updatedir(self.ui, repo, files, wlock=wlock)
468 468 n = repo.commit(files, message, user, date, force=1, lock=lock,
469 469 wlock=wlock)
470 470
471 471 if n == None:
472 472 raise util.Abort(_("repo commit failed"))
473 473
474 474 if update_status:
475 475 self.applied.append(statusentry(revlog.hex(n), patchname))
476 476
477 477 if patcherr:
478 478 if not patchfound:
479 479 self.ui.warn("patch %s is empty\n" % patchname)
480 480 err = 0
481 481 else:
482 482 self.ui.warn("patch failed, rejects left in working dir\n")
483 483 err = 1
484 484 break
485 485
486 486 if fuzz and strict:
487 487 self.ui.warn("fuzz found when applying patch, stopping\n")
488 488 err = 1
489 489 break
490 490 tr.close()
491 491 return (err, n)
492 492
493 493 def delete(self, repo, patches, opts):
494 494 realpatches = []
495 495 for patch in patches:
496 496 patch = self.lookup(patch, strict=True)
497 497 info = self.isapplied(patch)
498 498 if info:
499 499 raise util.Abort(_("cannot delete applied patch %s") % patch)
500 500 if patch not in self.series:
501 501 raise util.Abort(_("patch %s not in series file") % patch)
502 502 realpatches.append(patch)
503 503
504 504 appliedbase = 0
505 505 if opts.get('rev'):
506 506 if not self.applied:
507 507 raise util.Abort(_('no patches applied'))
508 508 revs = cmdutil.revrange(repo, opts['rev'])
509 509 if len(revs) > 1 and revs[0] > revs[1]:
510 510 revs.reverse()
511 511 for rev in revs:
512 512 if appliedbase >= len(self.applied):
513 513 raise util.Abort(_("revision %d is not managed") % rev)
514 514
515 515 base = revlog.bin(self.applied[appliedbase].rev)
516 516 node = repo.changelog.node(rev)
517 517 if node != base:
518 518 raise util.Abort(_("cannot delete revision %d above "
519 519 "applied patches") % rev)
520 520 realpatches.append(self.applied[appliedbase].name)
521 521 appliedbase += 1
522 522
523 523 if not opts.get('keep'):
524 524 r = self.qrepo()
525 525 if r:
526 526 r.remove(realpatches, True)
527 527 else:
528 528 for p in realpatches:
529 529 os.unlink(self.join(p))
530 530
531 531 if appliedbase:
532 532 del self.applied[:appliedbase]
533 533 self.applied_dirty = 1
534 534 indices = [self.find_series(p) for p in realpatches]
535 535 indices.sort()
536 536 for i in indices[-1::-1]:
537 537 del self.full_series[i]
538 538 self.parse_series()
539 539 self.series_dirty = 1
540 540
541 541 def check_toppatch(self, repo):
542 542 if len(self.applied) > 0:
543 543 top = revlog.bin(self.applied[-1].rev)
544 544 pp = repo.dirstate.parents()
545 545 if top not in pp:
546 546 raise util.Abort(_("queue top not at same revision as working directory"))
547 547 return top
548 548 return None
549 549 def check_localchanges(self, repo, force=False, refresh=True):
550 550 m, a, r, d = repo.status()[:4]
551 551 if m or a or r or d:
552 552 if not force:
553 553 if refresh:
554 554 raise util.Abort(_("local changes found, refresh first"))
555 555 else:
556 556 raise util.Abort(_("local changes found"))
557 557 return m, a, r, d
558 558 def new(self, repo, patch, msg=None, force=None):
559 559 if os.path.exists(self.join(patch)):
560 560 raise util.Abort(_('patch "%s" already exists') % patch)
561 561 m, a, r, d = self.check_localchanges(repo, force)
562 562 commitfiles = m + a + r
563 563 self.check_toppatch(repo)
564 564 wlock = repo.wlock()
565 565 insert = self.full_series_end()
566 566 if msg:
567 567 n = repo.commit(commitfiles, "[mq]: %s" % msg, force=True,
568 568 wlock=wlock)
569 569 else:
570 570 n = repo.commit(commitfiles,
571 571 "New patch: %s" % patch, force=True, wlock=wlock)
572 572 if n == None:
573 573 raise util.Abort(_("repo commit failed"))
574 574 self.full_series[insert:insert] = [patch]
575 575 self.applied.append(statusentry(revlog.hex(n), patch))
576 576 self.parse_series()
577 577 self.series_dirty = 1
578 578 self.applied_dirty = 1
579 579 p = self.opener(patch, "w")
580 580 if msg:
581 581 msg = msg + "\n"
582 582 p.write(msg)
583 583 p.close()
584 584 wlock = None
585 585 r = self.qrepo()
586 586 if r: r.add([patch])
587 587 if commitfiles:
588 588 self.refresh(repo, short=True)
589 589
590 590 def strip(self, repo, rev, update=True, backup="all", wlock=None):
591 591 def limitheads(chlog, stop):
592 592 """return the list of all nodes that have no children"""
593 593 p = {}
594 594 h = []
595 595 stoprev = 0
596 596 if stop in chlog.nodemap:
597 597 stoprev = chlog.rev(stop)
598 598
599 599 for r in xrange(chlog.count() - 1, -1, -1):
600 600 n = chlog.node(r)
601 601 if n not in p:
602 602 h.append(n)
603 603 if n == stop:
604 604 break
605 605 if r < stoprev:
606 606 break
607 607 for pn in chlog.parents(n):
608 608 p[pn] = 1
609 609 return h
610 610
611 611 def bundle(cg):
612 612 backupdir = repo.join("strip-backup")
613 613 if not os.path.isdir(backupdir):
614 614 os.mkdir(backupdir)
615 615 name = os.path.join(backupdir, "%s" % revlog.short(rev))
616 616 name = savename(name)
617 617 self.ui.warn("saving bundle to %s\n" % name)
618 618 return changegroup.writebundle(cg, name, "HG10BZ")
619 619
620 620 def stripall(revnum):
621 621 mm = repo.changectx(rev).manifest()
622 622 seen = {}
623 623
624 624 for x in xrange(revnum, repo.changelog.count()):
625 625 for f in repo.changectx(x).files():
626 626 if f in seen:
627 627 continue
628 628 seen[f] = 1
629 629 if f in mm:
630 630 filerev = mm[f]
631 631 else:
632 632 filerev = 0
633 633 seen[f] = filerev
634 634 # we go in two steps here so the strip loop happens in a
635 635 # sensible order. When stripping many files, this helps keep
636 636 # our disk access patterns under control.
637 637 seen_list = seen.keys()
638 638 seen_list.sort()
639 639 for f in seen_list:
640 640 ff = repo.file(f)
641 641 filerev = seen[f]
642 642 if filerev != 0:
643 643 if filerev in ff.nodemap:
644 644 filerev = ff.rev(filerev)
645 645 else:
646 646 filerev = 0
647 647 ff.strip(filerev, revnum)
648 648
649 649 if not wlock:
650 650 wlock = repo.wlock()
651 651 lock = repo.lock()
652 652 chlog = repo.changelog
653 653 # TODO delete the undo files, and handle undo of merge sets
654 654 pp = chlog.parents(rev)
655 655 revnum = chlog.rev(rev)
656 656
657 657 if update:
658 658 self.check_localchanges(repo, refresh=False)
659 659 urev = self.qparents(repo, rev)
660 660 hg.clean(repo, urev, wlock=wlock)
661 661 repo.dirstate.write()
662 662
663 663 # save is a list of all the branches we are truncating away
664 664 # that we actually want to keep. changegroup will be used
665 665 # to preserve them and add them back after the truncate
666 666 saveheads = []
667 667 savebases = {}
668 668
669 669 heads = limitheads(chlog, rev)
670 670 seen = {}
671 671
672 672 # search through all the heads, finding those where the revision
673 673 # we want to strip away is an ancestor. Also look for merges
674 674 # that might be turned into new heads by the strip.
675 675 while heads:
676 676 h = heads.pop()
677 677 n = h
678 678 while True:
679 679 seen[n] = 1
680 680 pp = chlog.parents(n)
681 681 if pp[1] != revlog.nullid:
682 682 for p in pp:
683 683 if chlog.rev(p) > revnum and p not in seen:
684 684 heads.append(p)
685 685 if pp[0] == revlog.nullid:
686 686 break
687 687 if chlog.rev(pp[0]) < revnum:
688 688 break
689 689 n = pp[0]
690 690 if n == rev:
691 691 break
692 692 r = chlog.reachable(h, rev)
693 693 if rev not in r:
694 694 saveheads.append(h)
695 695 for x in r:
696 696 if chlog.rev(x) > revnum:
697 697 savebases[x] = 1
698 698
699 699 # create a changegroup for all the branches we need to keep
700 700 if backup == "all":
701 701 backupch = repo.changegroupsubset([rev], chlog.heads(), 'strip')
702 702 bundle(backupch)
703 703 if saveheads:
704 704 backupch = repo.changegroupsubset(savebases.keys(), saveheads, 'strip')
705 705 chgrpfile = bundle(backupch)
706 706
707 707 stripall(revnum)
708 708
709 709 change = chlog.read(rev)
710 710 chlog.strip(revnum, revnum)
711 711 repo.manifest.strip(repo.manifest.rev(change[0]), revnum)
712 712 if saveheads:
713 713 self.ui.status("adding branch\n")
714 714 commands.unbundle(self.ui, repo, "file:%s" % chgrpfile,
715 715 update=False)
716 716 if backup != "strip":
717 717 os.unlink(chgrpfile)
718 718
719 719 def isapplied(self, patch):
720 720 """returns (index, rev, patch)"""
721 721 for i in xrange(len(self.applied)):
722 722 a = self.applied[i]
723 723 if a.name == patch:
724 724 return (i, a.rev, a.name)
725 725 return None
726 726
727 727 # if the exact patch name does not exist, we try a few
728 728 # variations. If strict is passed, we try only #1
729 729 #
730 730 # 1) a number to indicate an offset in the series file
731 731 # 2) a unique substring of the patch name was given
732 732 # 3) patchname[-+]num to indicate an offset in the series file
733 733 def lookup(self, patch, strict=False):
734 734 patch = patch and str(patch)
735 735
736 736 def partial_name(s):
737 737 if s in self.series:
738 738 return s
739 739 matches = [x for x in self.series if s in x]
740 740 if len(matches) > 1:
741 741 self.ui.warn(_('patch name "%s" is ambiguous:\n') % s)
742 742 for m in matches:
743 743 self.ui.warn(' %s\n' % m)
744 744 return None
745 745 if matches:
746 746 return matches[0]
747 747 if len(self.series) > 0 and len(self.applied) > 0:
748 748 if s == 'qtip':
749 749 return self.series[self.series_end(True)-1]
750 750 if s == 'qbase':
751 751 return self.series[0]
752 752 return None
753 753 if patch == None:
754 754 return None
755 755
756 756 # we don't want to return a partial match until we make
757 757 # sure the file name passed in does not exist (checked below)
758 758 res = partial_name(patch)
759 759 if res and res == patch:
760 760 return res
761 761
762 762 if not os.path.isfile(self.join(patch)):
763 763 try:
764 764 sno = int(patch)
765 765 except(ValueError, OverflowError):
766 766 pass
767 767 else:
768 768 if sno < len(self.series):
769 769 return self.series[sno]
770 770 if not strict:
771 771 # return any partial match made above
772 772 if res:
773 773 return res
774 774 minus = patch.rfind('-')
775 775 if minus >= 0:
776 776 res = partial_name(patch[:minus])
777 777 if res:
778 778 i = self.series.index(res)
779 779 try:
780 780 off = int(patch[minus+1:] or 1)
781 781 except(ValueError, OverflowError):
782 782 pass
783 783 else:
784 784 if i - off >= 0:
785 785 return self.series[i - off]
786 786 plus = patch.rfind('+')
787 787 if plus >= 0:
788 788 res = partial_name(patch[:plus])
789 789 if res:
790 790 i = self.series.index(res)
791 791 try:
792 792 off = int(patch[plus+1:] or 1)
793 793 except(ValueError, OverflowError):
794 794 pass
795 795 else:
796 796 if i + off < len(self.series):
797 797 return self.series[i + off]
798 798 raise util.Abort(_("patch %s not in series") % patch)
799 799
800 800 def push(self, repo, patch=None, force=False, list=False,
801 801 mergeq=None, wlock=None):
802 802 if not wlock:
803 803 wlock = repo.wlock()
804 804 patch = self.lookup(patch)
805 805 if patch and self.isapplied(patch):
806 806 raise util.Abort(_("patch %s is already applied") % patch)
807 807 if self.series_end() == len(self.series):
808 808 raise util.Abort(_("patch series fully applied"))
809 809 if not force:
810 810 self.check_localchanges(repo)
811 811
812 812 self.applied_dirty = 1;
813 813 start = self.series_end()
814 814 if start > 0:
815 815 self.check_toppatch(repo)
816 816 if not patch:
817 817 patch = self.series[start]
818 818 end = start + 1
819 819 else:
820 820 end = self.series.index(patch, start) + 1
821 821 s = self.series[start:end]
822 822 if mergeq:
823 823 ret = self.mergepatch(repo, mergeq, s, wlock)
824 824 else:
825 825 ret = self.apply(repo, s, list, wlock=wlock)
826 826 top = self.applied[-1].name
827 827 if ret[0]:
828 828 self.ui.write("Errors during apply, please fix and refresh %s\n" %
829 829 top)
830 830 else:
831 831 self.ui.write("Now at: %s\n" % top)
832 832 return ret[0]
833 833
834 834 def pop(self, repo, patch=None, force=False, update=True, all=False,
835 835 wlock=None):
836 836 def getfile(f, rev):
837 837 t = repo.file(f).read(rev)
838 838 repo.wfile(f, "w").write(t)
839 839
840 840 if not wlock:
841 841 wlock = repo.wlock()
842 842 if patch:
843 843 # index, rev, patch
844 844 info = self.isapplied(patch)
845 845 if not info:
846 846 patch = self.lookup(patch)
847 847 info = self.isapplied(patch)
848 848 if not info:
849 849 raise util.Abort(_("patch %s is not applied") % patch)
850 850 if len(self.applied) == 0:
851 851 raise util.Abort(_("no patches applied"))
852 852
853 853 if not update:
854 854 parents = repo.dirstate.parents()
855 855 rr = [ revlog.bin(x.rev) for x in self.applied ]
856 856 for p in parents:
857 857 if p in rr:
858 858 self.ui.warn("qpop: forcing dirstate update\n")
859 859 update = True
860 860
861 861 if not force and update:
862 862 self.check_localchanges(repo)
863 863
864 864 self.applied_dirty = 1;
865 865 end = len(self.applied)
866 866 if not patch:
867 867 if all:
868 868 popi = 0
869 869 else:
870 870 popi = len(self.applied) - 1
871 871 else:
872 872 popi = info[0] + 1
873 873 if popi >= end:
874 874 self.ui.warn("qpop: %s is already at the top\n" % patch)
875 875 return
876 876 info = [ popi ] + [self.applied[popi].rev, self.applied[popi].name]
877 877
878 878 start = info[0]
879 879 rev = revlog.bin(info[1])
880 880
881 881 # we know there are no local changes, so we can make a simplified
882 882 # form of hg.update.
883 883 if update:
884 884 top = self.check_toppatch(repo)
885 885 qp = self.qparents(repo, rev)
886 886 changes = repo.changelog.read(qp)
887 887 mmap = repo.manifest.read(changes[0])
888 888 m, a, r, d, u = repo.status(qp, top)[:5]
889 889 if d:
890 890 raise util.Abort("deletions found between repo revs")
891 891 for f in m:
892 892 getfile(f, mmap[f])
893 893 for f in r:
894 894 getfile(f, mmap[f])
895 895 util.set_exec(repo.wjoin(f), mmap.execf(f))
896 896 repo.dirstate.update(m + r, 'n')
897 897 for f in a:
898 898 try:
899 899 os.unlink(repo.wjoin(f))
900 900 except OSError, e:
901 901 if e.errno != errno.ENOENT:
902 902 raise
903 903 try: os.removedirs(os.path.dirname(repo.wjoin(f)))
904 904 except: pass
905 905 if a:
906 906 repo.dirstate.forget(a)
907 907 repo.dirstate.setparents(qp, revlog.nullid)
908 908 self.strip(repo, rev, update=False, backup='strip', wlock=wlock)
909 909 del self.applied[start:end]
910 910 if len(self.applied):
911 911 self.ui.write("Now at: %s\n" % self.applied[-1].name)
912 912 else:
913 913 self.ui.write("Patch queue now empty\n")
914 914
915 915 def diff(self, repo, pats, opts):
916 916 top = self.check_toppatch(repo)
917 917 if not top:
918 918 self.ui.write("No patches applied\n")
919 919 return
920 920 qp = self.qparents(repo, top)
921 921 if opts.get('git'):
922 922 self.diffopts().git = True
923 923 self.printdiff(repo, qp, files=pats, opts=opts)
924 924
925 925 def refresh(self, repo, pats=None, **opts):
926 926 if len(self.applied) == 0:
927 927 self.ui.write("No patches applied\n")
928 928 return 1
929 929 wlock = repo.wlock()
930 930 self.check_toppatch(repo)
931 931 (top, patchfn) = (self.applied[-1].rev, self.applied[-1].name)
932 932 top = revlog.bin(top)
933 933 cparents = repo.changelog.parents(top)
934 934 patchparent = self.qparents(repo, top)
935 935 message, comments, user, date, patchfound = self.readheaders(patchfn)
936 936
937 937 patchf = self.opener(patchfn, "w")
938 938 msg = opts.get('msg', '').rstrip()
939 939 if msg:
940 940 if comments:
941 941 # Remove existing message.
942 942 ci = 0
943 943 for mi in xrange(len(message)):
944 944 while message[mi] != comments[ci]:
945 945 ci += 1
946 946 del comments[ci]
947 947 comments.append(msg)
948 948 if comments:
949 949 comments = "\n".join(comments) + '\n\n'
950 950 patchf.write(comments)
951 951
952 952 if opts.get('git'):
953 953 self.diffopts().git = True
954 954 fns, matchfn, anypats = cmdutil.matchpats(repo, pats, opts)
955 955 tip = repo.changelog.tip()
956 956 if top == tip:
957 957 # if the top of our patch queue is also the tip, there is an
958 958 # optimization here. We update the dirstate in place and strip
959 959 # off the tip commit. Then just commit the current directory
960 960 # tree. We can also send repo.commit the list of files
961 961 # changed to speed up the diff
962 962 #
963 963 # in short mode, we only diff the files included in the
964 964 # patch already
965 965 #
966 966 # this should really read:
967 967 # mm, dd, aa, aa2, uu = repo.status(tip, patchparent)[:5]
968 968 # but we do it backwards to take advantage of manifest/chlog
969 969 # caching against the next repo.status call
970 970 #
971 971 mm, aa, dd, aa2, uu = repo.status(patchparent, tip)[:5]
972 972 changes = repo.changelog.read(tip)
973 973 man = repo.manifest.read(changes[0])
974 974 aaa = aa[:]
975 975 if opts.get('short'):
976 976 filelist = mm + aa + dd
977 977 else:
978 978 filelist = None
979 979 m, a, r, d, u = repo.status(files=filelist)[:5]
980 980
981 981 # we might end up with files that were added between tip and
982 982 # the dirstate parent, but then changed in the local dirstate.
983 983 # in this case, we want them to only show up in the added section
984 984 for x in m:
985 985 if x not in aa:
986 986 mm.append(x)
987 987 # we might end up with files added by the local dirstate that
988 988 # were deleted by the patch. In this case, they should only
989 989 # show up in the changed section.
990 990 for x in a:
991 991 if x in dd:
992 992 del dd[dd.index(x)]
993 993 mm.append(x)
994 994 else:
995 995 aa.append(x)
996 996 # make sure any files deleted in the local dirstate
997 997 # are not in the add or change column of the patch
998 998 forget = []
999 999 for x in d + r:
1000 1000 if x in aa:
1001 1001 del aa[aa.index(x)]
1002 1002 forget.append(x)
1003 1003 continue
1004 1004 elif x in mm:
1005 1005 del mm[mm.index(x)]
1006 1006 dd.append(x)
1007 1007
1008 1008 m = util.unique(mm)
1009 1009 r = util.unique(dd)
1010 1010 a = util.unique(aa)
1011 1011 filelist = filter(matchfn, util.unique(m + r + a))
1012 1012 patch.diff(repo, patchparent, files=filelist, match=matchfn,
1013 1013 fp=patchf, changes=(m, a, r, [], u),
1014 1014 opts=self.diffopts())
1015 1015 patchf.close()
1016 1016
1017 1017 repo.dirstate.setparents(*cparents)
1018 1018 copies = {}
1019 1019 for dst in a:
1020 1020 src = repo.dirstate.copied(dst)
1021 1021 if src is None:
1022 1022 continue
1023 1023 copies.setdefault(src, []).append(dst)
1024 1024 repo.dirstate.update(a, 'a')
1025 1025 # remember the copies between patchparent and tip
1026 1026 # this may be slow, so don't do it if we're not tracking copies
1027 1027 if self.diffopts().git:
1028 1028 for dst in aaa:
1029 1029 f = repo.file(dst)
1030 1030 src = f.renamed(man[dst])
1031 1031 if src:
1032 1032 copies[src[0]] = copies.get(dst, [])
1033 1033 if dst in a:
1034 1034 copies[src[0]].append(dst)
1035 1035 # we can't copy a file created by the patch itself
1036 1036 if dst in copies:
1037 1037 del copies[dst]
1038 1038 for src, dsts in copies.iteritems():
1039 1039 for dst in dsts:
1040 1040 repo.dirstate.copy(src, dst)
1041 1041 repo.dirstate.update(r, 'r')
1042 1042 # if the patch excludes a modified file, mark that file with mtime=0
1043 1043 # so status can see it.
1044 1044 mm = []
1045 1045 for i in xrange(len(m)-1, -1, -1):
1046 1046 if not matchfn(m[i]):
1047 1047 mm.append(m[i])
1048 1048 del m[i]
1049 1049 repo.dirstate.update(m, 'n')
1050 1050 repo.dirstate.update(mm, 'n', st_mtime=0)
1051 1051 repo.dirstate.forget(forget)
1052 1052
1053 1053 if not msg:
1054 1054 if not message:
1055 1055 message = "patch queue: %s\n" % patchfn
1056 1056 else:
1057 1057 message = "\n".join(message)
1058 1058 else:
1059 1059 message = msg
1060 1060
1061 1061 self.strip(repo, top, update=False, backup='strip', wlock=wlock)
1062 1062 n = repo.commit(filelist, message, changes[1], force=1, wlock=wlock)
1063 1063 self.applied[-1] = statusentry(revlog.hex(n), patchfn)
1064 1064 self.applied_dirty = 1
1065 1065 else:
1066 1066 self.printdiff(repo, patchparent, fp=patchf)
1067 1067 patchf.close()
1068 1068 added = repo.status()[1]
1069 1069 for a in added:
1070 1070 f = repo.wjoin(a)
1071 1071 try:
1072 1072 os.unlink(f)
1073 1073 except OSError, e:
1074 1074 if e.errno != errno.ENOENT:
1075 1075 raise
1076 1076 try: os.removedirs(os.path.dirname(f))
1077 1077 except: pass
1078 1078 # forget the file copies in the dirstate
1079 1079 # push should readd the files later on
1080 1080 repo.dirstate.forget(added)
1081 1081 self.pop(repo, force=True, wlock=wlock)
1082 1082 self.push(repo, force=True, wlock=wlock)
1083 1083
1084 1084 def init(self, repo, create=False):
1085 1085 if not create and os.path.isdir(self.path):
1086 1086 raise util.Abort(_("patch queue directory already exists"))
1087 1087 try:
1088 1088 os.mkdir(self.path)
1089 1089 except OSError, inst:
1090 1090 if inst.errno != errno.EEXIST or not create:
1091 1091 raise
1092 1092 if create:
1093 1093 return self.qrepo(create=True)
1094 1094
1095 1095 def unapplied(self, repo, patch=None):
1096 1096 if patch and patch not in self.series:
1097 1097 raise util.Abort(_("patch %s is not in series file") % patch)
1098 1098 if not patch:
1099 1099 start = self.series_end()
1100 1100 else:
1101 1101 start = self.series.index(patch) + 1
1102 1102 unapplied = []
1103 1103 for i in xrange(start, len(self.series)):
1104 1104 pushable, reason = self.pushable(i)
1105 1105 if pushable:
1106 1106 unapplied.append((i, self.series[i]))
1107 1107 self.explain_pushable(i)
1108 1108 return unapplied
1109 1109
1110 1110 def qseries(self, repo, missing=None, start=0, length=0, status=None,
1111 1111 summary=False):
1112 1112 def displayname(patchname):
1113 1113 if summary:
1114 1114 msg = self.readheaders(patchname)[0]
1115 1115 msg = msg and ': ' + msg[0] or ': '
1116 1116 else:
1117 1117 msg = ''
1118 1118 return '%s%s' % (patchname, msg)
1119 1119
1120 1120 def pname(i):
1121 1121 if status == 'A':
1122 1122 return self.applied[i].name
1123 1123 else:
1124 1124 return self.series[i]
1125 1125
1126 1126 applied = dict.fromkeys([p.name for p in self.applied])
1127 1127 if not length:
1128 1128 length = len(self.series) - start
1129 1129 if not missing:
1130 1130 for i in xrange(start, start+length):
1131 1131 pfx = ''
1132 1132 patch = pname(i)
1133 1133 if self.ui.verbose:
1134 1134 if patch in applied:
1135 1135 stat = 'A'
1136 1136 elif self.pushable(i)[0]:
1137 1137 stat = 'U'
1138 1138 else:
1139 1139 stat = 'G'
1140 1140 pfx = '%d %s ' % (i, stat)
1141 1141 self.ui.write('%s%s\n' % (pfx, displayname(patch)))
1142 1142 else:
1143 1143 msng_list = []
1144 1144 for root, dirs, files in os.walk(self.path):
1145 1145 d = root[len(self.path) + 1:]
1146 1146 for f in files:
1147 1147 fl = os.path.join(d, f)
1148 1148 if (fl not in self.series and
1149 1149 fl not in (self.status_path, self.series_path)
1150 1150 and not fl.startswith('.')):
1151 1151 msng_list.append(fl)
1152 1152 msng_list.sort()
1153 1153 for x in msng_list:
1154 1154 pfx = self.ui.verbose and ('D ') or ''
1155 1155 self.ui.write("%s%s\n" % (pfx, displayname(x)))
1156 1156
1157 1157 def issaveline(self, l):
1158 1158 if l.name == '.hg.patches.save.line':
1159 1159 return True
1160 1160
1161 1161 def qrepo(self, create=False):
1162 1162 if create or os.path.isdir(self.join(".hg")):
1163 1163 return hg.repository(self.ui, path=self.path, create=create)
1164 1164
1165 1165 def restore(self, repo, rev, delete=None, qupdate=None):
1166 1166 c = repo.changelog.read(rev)
1167 1167 desc = c[4].strip()
1168 1168 lines = desc.splitlines()
1169 1169 i = 0
1170 1170 datastart = None
1171 1171 series = []
1172 1172 applied = []
1173 1173 qpp = None
1174 1174 for i in xrange(0, len(lines)):
1175 1175 if lines[i] == 'Patch Data:':
1176 1176 datastart = i + 1
1177 1177 elif lines[i].startswith('Dirstate:'):
1178 1178 l = lines[i].rstrip()
1179 1179 l = l[10:].split(' ')
1180 1180 qpp = [ hg.bin(x) for x in l ]
1181 1181 elif datastart != None:
1182 1182 l = lines[i].rstrip()
1183 1183 se = statusentry(l)
1184 1184 file_ = se.name
1185 1185 if se.rev:
1186 1186 applied.append(se)
1187 1187 else:
1188 1188 series.append(file_)
1189 1189 if datastart == None:
1190 1190 self.ui.warn("No saved patch data found\n")
1191 1191 return 1
1192 1192 self.ui.warn("restoring status: %s\n" % lines[0])
1193 1193 self.full_series = series
1194 1194 self.applied = applied
1195 1195 self.parse_series()
1196 1196 self.series_dirty = 1
1197 1197 self.applied_dirty = 1
1198 1198 heads = repo.changelog.heads()
1199 1199 if delete:
1200 1200 if rev not in heads:
1201 1201 self.ui.warn("save entry has children, leaving it alone\n")
1202 1202 else:
1203 1203 self.ui.warn("removing save entry %s\n" % hg.short(rev))
1204 1204 pp = repo.dirstate.parents()
1205 1205 if rev in pp:
1206 1206 update = True
1207 1207 else:
1208 1208 update = False
1209 1209 self.strip(repo, rev, update=update, backup='strip')
1210 1210 if qpp:
1211 1211 self.ui.warn("saved queue repository parents: %s %s\n" %
1212 1212 (hg.short(qpp[0]), hg.short(qpp[1])))
1213 1213 if qupdate:
1214 1214 print "queue directory updating"
1215 1215 r = self.qrepo()
1216 1216 if not r:
1217 1217 self.ui.warn("Unable to load queue repository\n")
1218 1218 return 1
1219 1219 hg.clean(r, qpp[0])
1220 1220
1221 1221 def save(self, repo, msg=None):
1222 1222 if len(self.applied) == 0:
1223 1223 self.ui.warn("save: no patches applied, exiting\n")
1224 1224 return 1
1225 1225 if self.issaveline(self.applied[-1]):
1226 1226 self.ui.warn("status is already saved\n")
1227 1227 return 1
1228 1228
1229 1229 ar = [ ':' + x for x in self.full_series ]
1230 1230 if not msg:
1231 1231 msg = "hg patches saved state"
1232 1232 else:
1233 1233 msg = "hg patches: " + msg.rstrip('\r\n')
1234 1234 r = self.qrepo()
1235 1235 if r:
1236 1236 pp = r.dirstate.parents()
1237 1237 msg += "\nDirstate: %s %s" % (hg.hex(pp[0]), hg.hex(pp[1]))
1238 1238 msg += "\n\nPatch Data:\n"
1239 1239 text = msg + "\n".join([str(x) for x in self.applied]) + '\n' + (ar and
1240 1240 "\n".join(ar) + '\n' or "")
1241 1241 n = repo.commit(None, text, user=None, force=1)
1242 1242 if not n:
1243 1243 self.ui.warn("repo commit failed\n")
1244 1244 return 1
1245 1245 self.applied.append(statusentry(revlog.hex(n),'.hg.patches.save.line'))
1246 1246 self.applied_dirty = 1
1247 1247
1248 1248 def full_series_end(self):
1249 1249 if len(self.applied) > 0:
1250 1250 p = self.applied[-1].name
1251 1251 end = self.find_series(p)
1252 1252 if end == None:
1253 1253 return len(self.full_series)
1254 1254 return end + 1
1255 1255 return 0
1256 1256
1257 1257 def series_end(self, all_patches=False):
1258 1258 end = 0
1259 1259 def next(start):
1260 1260 if all_patches:
1261 1261 return start
1262 1262 i = start
1263 1263 while i < len(self.series):
1264 1264 p, reason = self.pushable(i)
1265 1265 if p:
1266 1266 break
1267 1267 self.explain_pushable(i)
1268 1268 i += 1
1269 1269 return i
1270 1270 if len(self.applied) > 0:
1271 1271 p = self.applied[-1].name
1272 1272 try:
1273 1273 end = self.series.index(p)
1274 1274 except ValueError:
1275 1275 return 0
1276 1276 return next(end + 1)
1277 1277 return next(end)
1278 1278
1279 1279 def appliedname(self, index):
1280 1280 pname = self.applied[index].name
1281 1281 if not self.ui.verbose:
1282 1282 p = pname
1283 1283 else:
1284 1284 p = str(self.series.index(pname)) + " " + pname
1285 1285 return p
1286 1286
1287 1287 def qimport(self, repo, files, patchname=None, rev=None, existing=None,
1288 1288 force=None, git=False):
1289 1289 def checkseries(patchname):
1290 1290 if patchname in self.series:
1291 1291 raise util.Abort(_('patch %s is already in the series file')
1292 1292 % patchname)
1293 1293 def checkfile(patchname):
1294 1294 if not force and os.path.exists(self.join(patchname)):
1295 1295 raise util.Abort(_('patch "%s" already exists')
1296 1296 % patchname)
1297 1297
1298 1298 if rev:
1299 1299 if files:
1300 1300 raise util.Abort(_('option "-r" not valid when importing '
1301 1301 'files'))
1302 1302 rev = cmdutil.revrange(repo, rev)
1303 1303 rev.sort(lambda x, y: cmp(y, x))
1304 1304 if (len(files) > 1 or len(rev) > 1) and patchname:
1305 1305 raise util.Abort(_('option "-n" not valid when importing multiple '
1306 1306 'patches'))
1307 1307 i = 0
1308 1308 added = []
1309 1309 if rev:
1310 1310 # If mq patches are applied, we can only import revisions
1311 1311 # that form a linear path to qbase.
1312 1312 # Otherwise, they should form a linear path to a head.
1313 1313 heads = repo.changelog.heads(repo.changelog.node(rev[-1]))
1314 1314 if len(heads) > 1:
1315 1315 raise util.Abort(_('revision %d is the root of more than one '
1316 1316 'branch') % rev[-1])
1317 1317 if self.applied:
1318 1318 base = revlog.hex(repo.changelog.node(rev[0]))
1319 1319 if base in [n.rev for n in self.applied]:
1320 1320 raise util.Abort(_('revision %d is already managed')
1321 1321 % rev[0])
1322 1322 if heads != [revlog.bin(self.applied[-1].rev)]:
1323 1323 raise util.Abort(_('revision %d is not the parent of '
1324 1324 'the queue') % rev[0])
1325 1325 base = repo.changelog.rev(revlog.bin(self.applied[0].rev))
1326 1326 lastparent = repo.changelog.parentrevs(base)[0]
1327 1327 else:
1328 1328 if heads != [repo.changelog.node(rev[0])]:
1329 1329 raise util.Abort(_('revision %d has unmanaged children')
1330 1330 % rev[0])
1331 1331 lastparent = None
1332 1332
1333 1333 if git:
1334 1334 self.diffopts().git = True
1335 1335
1336 1336 for r in rev:
1337 1337 p1, p2 = repo.changelog.parentrevs(r)
1338 1338 n = repo.changelog.node(r)
1339 1339 if p2 != revlog.nullrev:
1340 1340 raise util.Abort(_('cannot import merge revision %d') % r)
1341 1341 if lastparent and lastparent != r:
1342 1342 raise util.Abort(_('revision %d is not the parent of %d')
1343 1343 % (r, lastparent))
1344 1344 lastparent = p1
1345 1345
1346 1346 if not patchname:
1347 1347 patchname = normname('%d.diff' % r)
1348 1348 checkseries(patchname)
1349 1349 checkfile(patchname)
1350 1350 self.full_series.insert(0, patchname)
1351 1351
1352 1352 patchf = self.opener(patchname, "w")
1353 1353 patch.export(repo, [n], fp=patchf, opts=self.diffopts())
1354 1354 patchf.close()
1355 1355
1356 1356 se = statusentry(revlog.hex(n), patchname)
1357 1357 self.applied.insert(0, se)
1358 1358
1359 1359 added.append(patchname)
1360 1360 patchname = None
1361 1361 self.parse_series()
1362 1362 self.applied_dirty = 1
1363 1363
1364 1364 for filename in files:
1365 1365 if existing:
1366 1366 if filename == '-':
1367 1367 raise util.Abort(_('-e is incompatible with import from -'))
1368 1368 if not patchname:
1369 1369 patchname = normname(filename)
1370 1370 if not os.path.isfile(self.join(patchname)):
1371 1371 raise util.Abort(_("patch %s does not exist") % patchname)
1372 1372 else:
1373 1373 try:
1374 1374 if filename == '-':
1375 1375 if not patchname:
1376 1376 raise util.Abort(_('need --name to import a patch from -'))
1377 1377 text = sys.stdin.read()
1378 1378 else:
1379 1379 text = file(filename).read()
1380 1380 except IOError:
1381 1381 raise util.Abort(_("unable to read %s") % patchname)
1382 1382 if not patchname:
1383 1383 patchname = normname(os.path.basename(filename))
1384 1384 checkfile(patchname)
1385 1385 patchf = self.opener(patchname, "w")
1386 1386 patchf.write(text)
1387 1387 checkseries(patchname)
1388 1388 index = self.full_series_end() + i
1389 1389 self.full_series[index:index] = [patchname]
1390 1390 self.parse_series()
1391 1391 self.ui.warn("adding %s to series file\n" % patchname)
1392 1392 i += 1
1393 1393 added.append(patchname)
1394 1394 patchname = None
1395 1395 self.series_dirty = 1
1396 1396 qrepo = self.qrepo()
1397 1397 if qrepo:
1398 1398 qrepo.add(added)
1399 1399
1400 1400 def delete(ui, repo, *patches, **opts):
1401 1401 """remove patches from queue
1402 1402
1403 1403 With --rev, mq will stop managing the named revisions. The
1404 1404 patches must be applied and at the base of the stack. This option
1405 1405 is useful when the patches have been applied upstream.
1406 1406
1407 1407 Otherwise, the patches must not be applied.
1408 1408
1409 1409 With --keep, the patch files are preserved in the patch directory."""
1410 1410 q = repo.mq
1411 1411 q.delete(repo, patches, opts)
1412 1412 q.save_dirty()
1413 1413 return 0
1414 1414
1415 1415 def applied(ui, repo, patch=None, **opts):
1416 1416 """print the patches already applied"""
1417 1417 q = repo.mq
1418 1418 if patch:
1419 1419 if patch not in q.series:
1420 1420 raise util.Abort(_("patch %s is not in series file") % patch)
1421 1421 end = q.series.index(patch) + 1
1422 1422 else:
1423 1423 end = len(q.applied)
1424 1424 if not end:
1425 1425 return
1426 1426
1427 1427 return q.qseries(repo, length=end, status='A', summary=opts.get('summary'))
1428 1428
1429 1429 def unapplied(ui, repo, patch=None, **opts):
1430 1430 """print the patches not yet applied"""
1431 1431 q = repo.mq
1432 1432 if patch:
1433 1433 if patch not in q.series:
1434 1434 raise util.Abort(_("patch %s is not in series file") % patch)
1435 1435 start = q.series.index(patch) + 1
1436 1436 else:
1437 1437 start = q.series_end()
1438 1438 q.qseries(repo, start=start, summary=opts.get('summary'))
1439 1439
1440 1440 def qimport(ui, repo, *filename, **opts):
1441 1441 """import a patch
1442 1442
1443 1443 The patch will have the same name as its source file unless you
1444 1444 give it a new one with --name.
1445 1445
1446 1446 You can register an existing patch inside the patch directory
1447 1447 with the --existing flag.
1448 1448
1449 1449 With --force, an existing patch of the same name will be overwritten.
1450 1450
1451 1451 An existing changeset may be placed under mq control with --rev
1452 1452 (e.g. qimport --rev tip -n patch will place tip under mq control).
1453 1453 With --git, patches imported with --rev will use the git diff
1454 1454 format.
1455 1455 """
1456 1456 q = repo.mq
1457 1457 q.qimport(repo, filename, patchname=opts['name'],
1458 1458 existing=opts['existing'], force=opts['force'], rev=opts['rev'],
1459 1459 git=opts['git'])
1460 1460 q.save_dirty()
1461 1461 return 0
1462 1462
1463 1463 def init(ui, repo, **opts):
1464 1464 """init a new queue repository
1465 1465
1466 1466 The queue repository is unversioned by default. If -c is
1467 1467 specified, qinit will create a separate nested repository
1468 1468 for patches. Use qcommit to commit changes to this queue
1469 1469 repository."""
1470 1470 q = repo.mq
1471 1471 r = q.init(repo, create=opts['create_repo'])
1472 1472 q.save_dirty()
1473 1473 if r:
1474 1474 if not os.path.exists(r.wjoin('.hgignore')):
1475 1475 fp = r.wopener('.hgignore', 'w')
1476 1476 fp.write('syntax: glob\n')
1477 1477 fp.write('status\n')
1478 1478 fp.write('guards\n')
1479 1479 fp.close()
1480 1480 if not os.path.exists(r.wjoin('series')):
1481 1481 r.wopener('series', 'w').close()
1482 1482 r.add(['.hgignore', 'series'])
1483 1483 commands.add(ui, r)
1484 1484 return 0
1485 1485
1486 1486 def clone(ui, source, dest=None, **opts):
1487 1487 '''clone main and patch repository at same time
1488 1488
1489 1489 If source is local, destination will have no patches applied. If
1490 1490 source is remote, this command can not check if patches are
1491 1491 applied in source, so cannot guarantee that patches are not
1492 1492 applied in destination. If you clone remote repository, be sure
1493 1493 before that it has no patches applied.
1494 1494
1495 1495 Source patch repository is looked for in <src>/.hg/patches by
1496 1496 default. Use -p <url> to change.
1497 1497 '''
1498 1498 commands.setremoteconfig(ui, opts)
1499 1499 if dest is None:
1500 1500 dest = hg.defaultdest(source)
1501 1501 sr = hg.repository(ui, ui.expandpath(source))
1502 1502 qbase, destrev = None, None
1503 1503 if sr.local():
1504 reposetup(ui, sr)
1505 1504 if sr.mq.applied:
1506 1505 qbase = revlog.bin(sr.mq.applied[0].rev)
1507 1506 if not hg.islocal(dest):
1508 1507 destrev = sr.parents(qbase)[0]
1509 1508 ui.note(_('cloning main repo\n'))
1510 1509 sr, dr = hg.clone(ui, sr, dest,
1511 1510 pull=opts['pull'],
1512 1511 rev=destrev,
1513 1512 update=False,
1514 1513 stream=opts['uncompressed'])
1515 1514 ui.note(_('cloning patch repo\n'))
1516 1515 spr, dpr = hg.clone(ui, opts['patches'] or (sr.url() + '/.hg/patches'),
1517 1516 dr.url() + '/.hg/patches',
1518 1517 pull=opts['pull'],
1519 1518 update=not opts['noupdate'],
1520 1519 stream=opts['uncompressed'])
1521 1520 if dr.local():
1522 1521 if qbase:
1523 1522 ui.note(_('stripping applied patches from destination repo\n'))
1524 reposetup(ui, dr)
1525 1523 dr.mq.strip(dr, qbase, update=False, backup=None)
1526 1524 if not opts['noupdate']:
1527 1525 ui.note(_('updating destination repo\n'))
1528 1526 hg.update(dr, dr.changelog.tip())
1529 1527
1530 1528 def commit(ui, repo, *pats, **opts):
1531 1529 """commit changes in the queue repository"""
1532 1530 q = repo.mq
1533 1531 r = q.qrepo()
1534 1532 if not r: raise util.Abort('no queue repository')
1535 1533 commands.commit(r.ui, r, *pats, **opts)
1536 1534
1537 1535 def series(ui, repo, **opts):
1538 1536 """print the entire series file"""
1539 1537 repo.mq.qseries(repo, missing=opts['missing'], summary=opts['summary'])
1540 1538 return 0
1541 1539
1542 1540 def top(ui, repo, **opts):
1543 1541 """print the name of the current patch"""
1544 1542 q = repo.mq
1545 1543 t = len(q.applied)
1546 1544 if t:
1547 1545 return q.qseries(repo, start=t-1, length=1, status='A',
1548 1546 summary=opts.get('summary'))
1549 1547 else:
1550 1548 ui.write("No patches applied\n")
1551 1549 return 1
1552 1550
1553 1551 def next(ui, repo, **opts):
1554 1552 """print the name of the next patch"""
1555 1553 q = repo.mq
1556 1554 end = q.series_end()
1557 1555 if end == len(q.series):
1558 1556 ui.write("All patches applied\n")
1559 1557 return 1
1560 1558 return q.qseries(repo, start=end, length=1, summary=opts.get('summary'))
1561 1559
1562 1560 def prev(ui, repo, **opts):
1563 1561 """print the name of the previous patch"""
1564 1562 q = repo.mq
1565 1563 l = len(q.applied)
1566 1564 if l == 1:
1567 1565 ui.write("Only one patch applied\n")
1568 1566 return 1
1569 1567 if not l:
1570 1568 ui.write("No patches applied\n")
1571 1569 return 1
1572 1570 return q.qseries(repo, start=l-2, length=1, status='A',
1573 1571 summary=opts.get('summary'))
1574 1572
1575 1573 def new(ui, repo, patch, **opts):
1576 1574 """create a new patch
1577 1575
1578 1576 qnew creates a new patch on top of the currently-applied patch
1579 1577 (if any). It will refuse to run if there are any outstanding
1580 1578 changes unless -f is specified, in which case the patch will
1581 1579 be initialised with them.
1582 1580
1583 1581 -e, -m or -l set the patch header as well as the commit message.
1584 1582 If none is specified, the patch header is empty and the
1585 1583 commit message is 'New patch: PATCH'"""
1586 1584 q = repo.mq
1587 1585 message = commands.logmessage(opts)
1588 1586 if opts['edit']:
1589 1587 message = ui.edit(message, ui.username())
1590 1588 q.new(repo, patch, msg=message, force=opts['force'])
1591 1589 q.save_dirty()
1592 1590 return 0
1593 1591
1594 1592 def refresh(ui, repo, *pats, **opts):
1595 1593 """update the current patch
1596 1594
1597 1595 If any file patterns are provided, the refreshed patch will contain only
1598 1596 the modifications that match those patterns; the remaining modifications
1599 1597 will remain in the working directory.
1600 1598
1601 1599 hg add/remove/copy/rename work as usual, though you might want to use
1602 1600 git-style patches (--git or [diff] git=1) to track copies and renames.
1603 1601 """
1604 1602 q = repo.mq
1605 1603 message = commands.logmessage(opts)
1606 1604 if opts['edit']:
1607 1605 if message:
1608 1606 raise util.Abort(_('option "-e" incompatible with "-m" or "-l"'))
1609 1607 patch = q.applied[-1].name
1610 1608 (message, comment, user, date, hasdiff) = q.readheaders(patch)
1611 1609 message = ui.edit('\n'.join(message), user or ui.username())
1612 1610 ret = q.refresh(repo, pats, msg=message, **opts)
1613 1611 q.save_dirty()
1614 1612 return ret
1615 1613
1616 1614 def diff(ui, repo, *pats, **opts):
1617 1615 """diff of the current patch"""
1618 1616 repo.mq.diff(repo, pats, opts)
1619 1617 return 0
1620 1618
1621 1619 def fold(ui, repo, *files, **opts):
1622 1620 """fold the named patches into the current patch
1623 1621
1624 1622 Patches must not yet be applied. Each patch will be successively
1625 1623 applied to the current patch in the order given. If all the
1626 1624 patches apply successfully, the current patch will be refreshed
1627 1625 with the new cumulative patch, and the folded patches will
1628 1626 be deleted. With -k/--keep, the folded patch files will not
1629 1627 be removed afterwards.
1630 1628
1631 1629 The header for each folded patch will be concatenated with
1632 1630 the current patch header, separated by a line of '* * *'."""
1633 1631
1634 1632 q = repo.mq
1635 1633
1636 1634 if not files:
1637 1635 raise util.Abort(_('qfold requires at least one patch name'))
1638 1636 if not q.check_toppatch(repo):
1639 1637 raise util.Abort(_('No patches applied'))
1640 1638
1641 1639 message = commands.logmessage(opts)
1642 1640 if opts['edit']:
1643 1641 if message:
1644 1642 raise util.Abort(_('option "-e" incompatible with "-m" or "-l"'))
1645 1643
1646 1644 parent = q.lookup('qtip')
1647 1645 patches = []
1648 1646 messages = []
1649 1647 for f in files:
1650 1648 p = q.lookup(f)
1651 1649 if p in patches or p == parent:
1652 1650 ui.warn(_('Skipping already folded patch %s') % p)
1653 1651 if q.isapplied(p):
1654 1652 raise util.Abort(_('qfold cannot fold already applied patch %s') % p)
1655 1653 patches.append(p)
1656 1654
1657 1655 for p in patches:
1658 1656 if not message:
1659 1657 messages.append(q.readheaders(p)[0])
1660 1658 pf = q.join(p)
1661 1659 (patchsuccess, files, fuzz) = q.patch(repo, pf)
1662 1660 if not patchsuccess:
1663 1661 raise util.Abort(_('Error folding patch %s') % p)
1664 1662 patch.updatedir(ui, repo, files)
1665 1663
1666 1664 if not message:
1667 1665 message, comments, user = q.readheaders(parent)[0:3]
1668 1666 for msg in messages:
1669 1667 message.append('* * *')
1670 1668 message.extend(msg)
1671 1669 message = '\n'.join(message)
1672 1670
1673 1671 if opts['edit']:
1674 1672 message = ui.edit(message, user or ui.username())
1675 1673
1676 1674 q.refresh(repo, msg=message)
1677 1675 q.delete(repo, patches, opts)
1678 1676 q.save_dirty()
1679 1677
1680 1678 def guard(ui, repo, *args, **opts):
1681 1679 '''set or print guards for a patch
1682 1680
1683 1681 Guards control whether a patch can be pushed. A patch with no
1684 1682 guards is always pushed. A patch with a positive guard ("+foo") is
1685 1683 pushed only if the qselect command has activated it. A patch with
1686 1684 a negative guard ("-foo") is never pushed if the qselect command
1687 1685 has activated it.
1688 1686
1689 1687 With no arguments, print the currently active guards.
1690 1688 With arguments, set guards for the named patch.
1691 1689
1692 1690 To set a negative guard "-foo" on topmost patch ("--" is needed so
1693 1691 hg will not interpret "-foo" as an option):
1694 1692 hg qguard -- -foo
1695 1693
1696 1694 To set guards on another patch:
1697 1695 hg qguard other.patch +2.6.17 -stable
1698 1696 '''
1699 1697 def status(idx):
1700 1698 guards = q.series_guards[idx] or ['unguarded']
1701 1699 ui.write('%s: %s\n' % (q.series[idx], ' '.join(guards)))
1702 1700 q = repo.mq
1703 1701 patch = None
1704 1702 args = list(args)
1705 1703 if opts['list']:
1706 1704 if args or opts['none']:
1707 1705 raise util.Abort(_('cannot mix -l/--list with options or arguments'))
1708 1706 for i in xrange(len(q.series)):
1709 1707 status(i)
1710 1708 return
1711 1709 if not args or args[0][0:1] in '-+':
1712 1710 if not q.applied:
1713 1711 raise util.Abort(_('no patches applied'))
1714 1712 patch = q.applied[-1].name
1715 1713 if patch is None and args[0][0:1] not in '-+':
1716 1714 patch = args.pop(0)
1717 1715 if patch is None:
1718 1716 raise util.Abort(_('no patch to work with'))
1719 1717 if args or opts['none']:
1720 1718 q.set_guards(q.find_series(patch), args)
1721 1719 q.save_dirty()
1722 1720 else:
1723 1721 status(q.series.index(q.lookup(patch)))
1724 1722
1725 1723 def header(ui, repo, patch=None):
1726 1724 """Print the header of the topmost or specified patch"""
1727 1725 q = repo.mq
1728 1726
1729 1727 if patch:
1730 1728 patch = q.lookup(patch)
1731 1729 else:
1732 1730 if not q.applied:
1733 1731 ui.write('No patches applied\n')
1734 1732 return 1
1735 1733 patch = q.lookup('qtip')
1736 1734 message = repo.mq.readheaders(patch)[0]
1737 1735
1738 1736 ui.write('\n'.join(message) + '\n')
1739 1737
1740 1738 def lastsavename(path):
1741 1739 (directory, base) = os.path.split(path)
1742 1740 names = os.listdir(directory)
1743 1741 namere = re.compile("%s.([0-9]+)" % base)
1744 1742 maxindex = None
1745 1743 maxname = None
1746 1744 for f in names:
1747 1745 m = namere.match(f)
1748 1746 if m:
1749 1747 index = int(m.group(1))
1750 1748 if maxindex == None or index > maxindex:
1751 1749 maxindex = index
1752 1750 maxname = f
1753 1751 if maxname:
1754 1752 return (os.path.join(directory, maxname), maxindex)
1755 1753 return (None, None)
1756 1754
1757 1755 def savename(path):
1758 1756 (last, index) = lastsavename(path)
1759 1757 if last is None:
1760 1758 index = 0
1761 1759 newpath = path + ".%d" % (index + 1)
1762 1760 return newpath
1763 1761
1764 1762 def push(ui, repo, patch=None, **opts):
1765 1763 """push the next patch onto the stack"""
1766 1764 q = repo.mq
1767 1765 mergeq = None
1768 1766
1769 1767 if opts['all']:
1770 1768 if not q.series:
1771 1769 raise util.Abort(_('no patches in series'))
1772 1770 patch = q.series[-1]
1773 1771 if opts['merge']:
1774 1772 if opts['name']:
1775 1773 newpath = opts['name']
1776 1774 else:
1777 1775 newpath, i = lastsavename(q.path)
1778 1776 if not newpath:
1779 1777 ui.warn("no saved queues found, please use -n\n")
1780 1778 return 1
1781 1779 mergeq = queue(ui, repo.join(""), newpath)
1782 1780 ui.warn("merging with queue at: %s\n" % mergeq.path)
1783 1781 ret = q.push(repo, patch, force=opts['force'], list=opts['list'],
1784 1782 mergeq=mergeq)
1785 1783 q.save_dirty()
1786 1784 return ret
1787 1785
1788 1786 def pop(ui, repo, patch=None, **opts):
1789 1787 """pop the current patch off the stack"""
1790 1788 localupdate = True
1791 1789 if opts['name']:
1792 1790 q = queue(ui, repo.join(""), repo.join(opts['name']))
1793 1791 ui.warn('using patch queue: %s\n' % q.path)
1794 1792 localupdate = False
1795 1793 else:
1796 1794 q = repo.mq
1797 1795 q.pop(repo, patch, force=opts['force'], update=localupdate, all=opts['all'])
1798 1796 q.save_dirty()
1799 1797 return 0
1800 1798
1801 1799 def rename(ui, repo, patch, name=None, **opts):
1802 1800 """rename a patch
1803 1801
1804 1802 With one argument, renames the current patch to PATCH1.
1805 1803 With two arguments, renames PATCH1 to PATCH2."""
1806 1804
1807 1805 q = repo.mq
1808 1806
1809 1807 if not name:
1810 1808 name = patch
1811 1809 patch = None
1812 1810
1813 1811 if patch:
1814 1812 patch = q.lookup(patch)
1815 1813 else:
1816 1814 if not q.applied:
1817 1815 ui.write(_('No patches applied\n'))
1818 1816 return
1819 1817 patch = q.lookup('qtip')
1820 1818 absdest = q.join(name)
1821 1819 if os.path.isdir(absdest):
1822 1820 name = normname(os.path.join(name, os.path.basename(patch)))
1823 1821 absdest = q.join(name)
1824 1822 if os.path.exists(absdest):
1825 1823 raise util.Abort(_('%s already exists') % absdest)
1826 1824
1827 1825 if name in q.series:
1828 1826 raise util.Abort(_('A patch named %s already exists in the series file') % name)
1829 1827
1830 1828 if ui.verbose:
1831 1829 ui.write('Renaming %s to %s\n' % (patch, name))
1832 1830 i = q.find_series(patch)
1833 1831 guards = q.guard_re.findall(q.full_series[i])
1834 1832 q.full_series[i] = name + ''.join([' #' + g for g in guards])
1835 1833 q.parse_series()
1836 1834 q.series_dirty = 1
1837 1835
1838 1836 info = q.isapplied(patch)
1839 1837 if info:
1840 1838 q.applied[info[0]] = statusentry(info[1], name)
1841 1839 q.applied_dirty = 1
1842 1840
1843 1841 util.rename(q.join(patch), absdest)
1844 1842 r = q.qrepo()
1845 1843 if r:
1846 1844 wlock = r.wlock()
1847 1845 if r.dirstate.state(name) == 'r':
1848 1846 r.undelete([name], wlock)
1849 1847 r.copy(patch, name, wlock)
1850 1848 r.remove([patch], False, wlock)
1851 1849
1852 1850 q.save_dirty()
1853 1851
1854 1852 def restore(ui, repo, rev, **opts):
1855 1853 """restore the queue state saved by a rev"""
1856 1854 rev = repo.lookup(rev)
1857 1855 q = repo.mq
1858 1856 q.restore(repo, rev, delete=opts['delete'],
1859 1857 qupdate=opts['update'])
1860 1858 q.save_dirty()
1861 1859 return 0
1862 1860
1863 1861 def save(ui, repo, **opts):
1864 1862 """save current queue state"""
1865 1863 q = repo.mq
1866 1864 message = commands.logmessage(opts)
1867 1865 ret = q.save(repo, msg=message)
1868 1866 if ret:
1869 1867 return ret
1870 1868 q.save_dirty()
1871 1869 if opts['copy']:
1872 1870 path = q.path
1873 1871 if opts['name']:
1874 1872 newpath = os.path.join(q.basepath, opts['name'])
1875 1873 if os.path.exists(newpath):
1876 1874 if not os.path.isdir(newpath):
1877 1875 raise util.Abort(_('destination %s exists and is not '
1878 1876 'a directory') % newpath)
1879 1877 if not opts['force']:
1880 1878 raise util.Abort(_('destination %s exists, '
1881 1879 'use -f to force') % newpath)
1882 1880 else:
1883 1881 newpath = savename(path)
1884 1882 ui.warn("copy %s to %s\n" % (path, newpath))
1885 1883 util.copyfiles(path, newpath)
1886 1884 if opts['empty']:
1887 1885 try:
1888 1886 os.unlink(q.join(q.status_path))
1889 1887 except:
1890 1888 pass
1891 1889 return 0
1892 1890
1893 1891 def strip(ui, repo, rev, **opts):
1894 1892 """strip a revision and all later revs on the same branch"""
1895 1893 rev = repo.lookup(rev)
1896 1894 backup = 'all'
1897 1895 if opts['backup']:
1898 1896 backup = 'strip'
1899 1897 elif opts['nobackup']:
1900 1898 backup = 'none'
1901 1899 update = repo.dirstate.parents()[0] != revlog.nullid
1902 1900 repo.mq.strip(repo, rev, backup=backup, update=update)
1903 1901 return 0
1904 1902
1905 1903 def select(ui, repo, *args, **opts):
1906 1904 '''set or print guarded patches to push
1907 1905
1908 1906 Use the qguard command to set or print guards on patch, then use
1909 1907 qselect to tell mq which guards to use. A patch will be pushed if it
1910 1908 has no guards or any positive guards match the currently selected guard,
1911 1909 but will not be pushed if any negative guards match the current guard.
1912 1910 For example:
1913 1911
1914 1912 qguard foo.patch -stable (negative guard)
1915 1913 qguard bar.patch +stable (positive guard)
1916 1914 qselect stable
1917 1915
1918 1916 This activates the "stable" guard. mq will skip foo.patch (because
1919 1917 it has a negative match) but push bar.patch (because it
1920 1918 has a positive match).
1921 1919
1922 1920 With no arguments, prints the currently active guards.
1923 1921 With one argument, sets the active guard.
1924 1922
1925 1923 Use -n/--none to deactivate guards (no other arguments needed).
1926 1924 When no guards are active, patches with positive guards are skipped
1927 1925 and patches with negative guards are pushed.
1928 1926
1929 1927 qselect can change the guards on applied patches. It does not pop
1930 1928 guarded patches by default. Use --pop to pop back to the last applied
1931 1929 patch that is not guarded. Use --reapply (which implies --pop) to push
1932 1930 back to the current patch afterwards, but skip guarded patches.
1933 1931
1934 1932 Use -s/--series to print a list of all guards in the series file (no
1935 1933 other arguments needed). Use -v for more information.'''
1936 1934
1937 1935 q = repo.mq
1938 1936 guards = q.active()
1939 1937 if args or opts['none']:
1940 1938 old_unapplied = q.unapplied(repo)
1941 1939 old_guarded = [i for i in xrange(len(q.applied)) if
1942 1940 not q.pushable(i)[0]]
1943 1941 q.set_active(args)
1944 1942 q.save_dirty()
1945 1943 if not args:
1946 1944 ui.status(_('guards deactivated\n'))
1947 1945 if not opts['pop'] and not opts['reapply']:
1948 1946 unapplied = q.unapplied(repo)
1949 1947 guarded = [i for i in xrange(len(q.applied))
1950 1948 if not q.pushable(i)[0]]
1951 1949 if len(unapplied) != len(old_unapplied):
1952 1950 ui.status(_('number of unguarded, unapplied patches has '
1953 1951 'changed from %d to %d\n') %
1954 1952 (len(old_unapplied), len(unapplied)))
1955 1953 if len(guarded) != len(old_guarded):
1956 1954 ui.status(_('number of guarded, applied patches has changed '
1957 1955 'from %d to %d\n') %
1958 1956 (len(old_guarded), len(guarded)))
1959 1957 elif opts['series']:
1960 1958 guards = {}
1961 1959 noguards = 0
1962 1960 for gs in q.series_guards:
1963 1961 if not gs:
1964 1962 noguards += 1
1965 1963 for g in gs:
1966 1964 guards.setdefault(g, 0)
1967 1965 guards[g] += 1
1968 1966 if ui.verbose:
1969 1967 guards['NONE'] = noguards
1970 1968 guards = guards.items()
1971 1969 guards.sort(lambda a, b: cmp(a[0][1:], b[0][1:]))
1972 1970 if guards:
1973 1971 ui.note(_('guards in series file:\n'))
1974 1972 for guard, count in guards:
1975 1973 ui.note('%2d ' % count)
1976 1974 ui.write(guard, '\n')
1977 1975 else:
1978 1976 ui.note(_('no guards in series file\n'))
1979 1977 else:
1980 1978 if guards:
1981 1979 ui.note(_('active guards:\n'))
1982 1980 for g in guards:
1983 1981 ui.write(g, '\n')
1984 1982 else:
1985 1983 ui.write(_('no active guards\n'))
1986 1984 reapply = opts['reapply'] and q.applied and q.appliedname(-1)
1987 1985 popped = False
1988 1986 if opts['pop'] or opts['reapply']:
1989 1987 for i in xrange(len(q.applied)):
1990 1988 pushable, reason = q.pushable(i)
1991 1989 if not pushable:
1992 1990 ui.status(_('popping guarded patches\n'))
1993 1991 popped = True
1994 1992 if i == 0:
1995 1993 q.pop(repo, all=True)
1996 1994 else:
1997 1995 q.pop(repo, i-1)
1998 1996 break
1999 1997 if popped:
2000 1998 try:
2001 1999 if reapply:
2002 2000 ui.status(_('reapplying unguarded patches\n'))
2003 2001 q.push(repo, reapply)
2004 2002 finally:
2005 2003 q.save_dirty()
2006 2004
2007 2005 def reposetup(ui, repo):
2008 2006 class mqrepo(repo.__class__):
2009 2007 def abort_if_wdir_patched(self, errmsg, force=False):
2010 2008 if self.mq.applied and not force:
2011 2009 parent = revlog.hex(self.dirstate.parents()[0])
2012 2010 if parent in [s.rev for s in self.mq.applied]:
2013 2011 raise util.Abort(errmsg)
2014 2012
2015 2013 def commit(self, *args, **opts):
2016 2014 if len(args) >= 6:
2017 2015 force = args[5]
2018 2016 else:
2019 2017 force = opts.get('force')
2020 2018 self.abort_if_wdir_patched(
2021 2019 _('cannot commit over an applied mq patch'),
2022 2020 force)
2023 2021
2024 2022 return super(mqrepo, self).commit(*args, **opts)
2025 2023
2026 2024 def push(self, remote, force=False, revs=None):
2027 2025 if self.mq.applied and not force and not revs:
2028 2026 raise util.Abort(_('source has mq patches applied'))
2029 2027 return super(mqrepo, self).push(remote, force, revs)
2030 2028
2031 2029 def tags(self):
2032 2030 if self.tagscache:
2033 2031 return self.tagscache
2034 2032
2035 2033 tagscache = super(mqrepo, self).tags()
2036 2034
2037 2035 q = self.mq
2038 2036 if not q.applied:
2039 2037 return tagscache
2040 2038
2041 2039 mqtags = [(patch.rev, patch.name) for patch in q.applied]
2042 2040 mqtags.append((mqtags[-1][0], 'qtip'))
2043 2041 mqtags.append((mqtags[0][0], 'qbase'))
2044 2042 for patch in mqtags:
2045 2043 if patch[1] in tagscache:
2046 2044 self.ui.warn('Tag %s overrides mq patch of the same name\n' % patch[1])
2047 2045 else:
2048 2046 tagscache[patch[1]] = revlog.bin(patch[0])
2049 2047
2050 2048 return tagscache
2051 2049
2052 2050 def _branchtags(self):
2053 2051 q = self.mq
2054 2052 if not q.applied:
2055 2053 return super(mqrepo, self)._branchtags()
2056 2054
2057 2055 self.branchcache = {} # avoid recursion in changectx
2058 2056 cl = self.changelog
2059 2057 partial, last, lrev = self._readbranchcache()
2060 2058
2061 2059 qbase = cl.rev(revlog.bin(q.applied[0].rev))
2062 2060 start = lrev + 1
2063 2061 if start < qbase:
2064 2062 # update the cache (excluding the patches) and save it
2065 2063 self._updatebranchcache(partial, lrev+1, qbase)
2066 2064 self._writebranchcache(partial, cl.node(qbase-1), qbase-1)
2067 2065 start = qbase
2068 2066 # if start = qbase, the cache is as updated as it should be.
2069 2067 # if start > qbase, the cache includes (part of) the patches.
2070 2068 # we might as well use it, but we won't save it.
2071 2069
2072 2070 # update the cache up to the tip
2073 2071 self._updatebranchcache(partial, start, cl.count())
2074 2072
2075 2073 return partial
2076 2074
2077 2075 if repo.local():
2078 2076 repo.__class__ = mqrepo
2079 2077 repo.mq = queue(ui, repo.join(""))
2080 2078
2081 2079 seriesopts = [('s', 'summary', None, _('print first line of patch header'))]
2082 2080
2083 2081 cmdtable = {
2084 2082 "qapplied": (applied, [] + seriesopts, 'hg qapplied [-s] [PATCH]'),
2085 2083 "qclone": (clone,
2086 2084 [('', 'pull', None, _('use pull protocol to copy metadata')),
2087 2085 ('U', 'noupdate', None, _('do not update the new working directories')),
2088 2086 ('', 'uncompressed', None,
2089 2087 _('use uncompressed transfer (fast over LAN)')),
2090 2088 ('e', 'ssh', '', _('specify ssh command to use')),
2091 2089 ('p', 'patches', '', _('location of source patch repo')),
2092 2090 ('', 'remotecmd', '',
2093 2091 _('specify hg command to run on the remote side'))],
2094 2092 'hg qclone [OPTION]... SOURCE [DEST]'),
2095 2093 "qcommit|qci":
2096 2094 (commit,
2097 2095 commands.table["^commit|ci"][1],
2098 2096 'hg qcommit [OPTION]... [FILE]...'),
2099 2097 "^qdiff": (diff,
2100 2098 [('g', 'git', None, _('use git extended diff format')),
2101 2099 ('I', 'include', [], _('include names matching the given patterns')),
2102 2100 ('X', 'exclude', [], _('exclude names matching the given patterns'))],
2103 2101 'hg qdiff [-I] [-X] [FILE]...'),
2104 2102 "qdelete|qremove|qrm":
2105 2103 (delete,
2106 2104 [('k', 'keep', None, _('keep patch file')),
2107 2105 ('r', 'rev', [], _('stop managing a revision'))],
2108 2106 'hg qdelete [-k] [-r REV]... PATCH...'),
2109 2107 'qfold':
2110 2108 (fold,
2111 2109 [('e', 'edit', None, _('edit patch header')),
2112 2110 ('k', 'keep', None, _('keep folded patch files'))
2113 2111 ] + commands.commitopts,
2114 2112 'hg qfold [-e] [-m <text>] [-l <file] PATCH...'),
2115 2113 'qguard': (guard, [('l', 'list', None, _('list all patches and guards')),
2116 2114 ('n', 'none', None, _('drop all guards'))],
2117 2115 'hg qguard [PATCH] [+GUARD...] [-GUARD...]'),
2118 2116 'qheader': (header, [],
2119 2117 _('hg qheader [PATCH]')),
2120 2118 "^qimport":
2121 2119 (qimport,
2122 2120 [('e', 'existing', None, 'import file in patch dir'),
2123 2121 ('n', 'name', '', 'patch file name'),
2124 2122 ('f', 'force', None, 'overwrite existing files'),
2125 2123 ('r', 'rev', [], 'place existing revisions under mq control'),
2126 2124 ('g', 'git', None, _('use git extended diff format'))],
2127 2125 'hg qimport [-e] [-n NAME] [-f] [-g] [-r REV]... FILE...'),
2128 2126 "^qinit":
2129 2127 (init,
2130 2128 [('c', 'create-repo', None, 'create queue repository')],
2131 2129 'hg qinit [-c]'),
2132 2130 "qnew":
2133 2131 (new,
2134 2132 [('e', 'edit', None, _('edit commit message')),
2135 2133 ('f', 'force', None, _('import uncommitted changes into patch'))
2136 2134 ] + commands.commitopts,
2137 2135 'hg qnew [-e] [-m TEXT] [-l FILE] [-f] PATCH'),
2138 2136 "qnext": (next, [] + seriesopts, 'hg qnext [-s]'),
2139 2137 "qprev": (prev, [] + seriesopts, 'hg qprev [-s]'),
2140 2138 "^qpop":
2141 2139 (pop,
2142 2140 [('a', 'all', None, 'pop all patches'),
2143 2141 ('n', 'name', '', 'queue name to pop'),
2144 2142 ('f', 'force', None, 'forget any local changes')],
2145 2143 'hg qpop [-a] [-n NAME] [-f] [PATCH | INDEX]'),
2146 2144 "^qpush":
2147 2145 (push,
2148 2146 [('f', 'force', None, 'apply if the patch has rejects'),
2149 2147 ('l', 'list', None, 'list patch name in commit text'),
2150 2148 ('a', 'all', None, 'apply all patches'),
2151 2149 ('m', 'merge', None, 'merge from another queue'),
2152 2150 ('n', 'name', '', 'merge queue name')],
2153 2151 'hg qpush [-f] [-l] [-a] [-m] [-n NAME] [PATCH | INDEX]'),
2154 2152 "^qrefresh":
2155 2153 (refresh,
2156 2154 [('e', 'edit', None, _('edit commit message')),
2157 2155 ('g', 'git', None, _('use git extended diff format')),
2158 2156 ('s', 'short', None, 'refresh only files already in the patch'),
2159 2157 ('I', 'include', [], _('include names matching the given patterns')),
2160 2158 ('X', 'exclude', [], _('exclude names matching the given patterns'))
2161 2159 ] + commands.commitopts,
2162 2160 'hg qrefresh [-I] [-X] [-e] [-m TEXT] [-l FILE] [-s] FILES...'),
2163 2161 'qrename|qmv':
2164 2162 (rename, [], 'hg qrename PATCH1 [PATCH2]'),
2165 2163 "qrestore":
2166 2164 (restore,
2167 2165 [('d', 'delete', None, 'delete save entry'),
2168 2166 ('u', 'update', None, 'update queue working dir')],
2169 2167 'hg qrestore [-d] [-u] REV'),
2170 2168 "qsave":
2171 2169 (save,
2172 2170 [('c', 'copy', None, 'copy patch directory'),
2173 2171 ('n', 'name', '', 'copy directory name'),
2174 2172 ('e', 'empty', None, 'clear queue status file'),
2175 2173 ('f', 'force', None, 'force copy')] + commands.commitopts,
2176 2174 'hg qsave [-m TEXT] [-l FILE] [-c] [-n NAME] [-e] [-f]'),
2177 2175 "qselect": (select,
2178 2176 [('n', 'none', None, _('disable all guards')),
2179 2177 ('s', 'series', None, _('list all guards in series file')),
2180 2178 ('', 'pop', None,
2181 2179 _('pop to before first guarded applied patch')),
2182 2180 ('', 'reapply', None, _('pop, then reapply patches'))],
2183 2181 'hg qselect [OPTION...] [GUARD...]'),
2184 2182 "qseries":
2185 2183 (series,
2186 2184 [('m', 'missing', None, 'print patches not in series')] + seriesopts,
2187 2185 'hg qseries [-ms]'),
2188 2186 "^strip":
2189 2187 (strip,
2190 2188 [('f', 'force', None, 'force multi-head removal'),
2191 2189 ('b', 'backup', None, 'bundle unrelated changesets'),
2192 2190 ('n', 'nobackup', None, 'no backups')],
2193 2191 'hg strip [-f] [-b] [-n] REV'),
2194 2192 "qtop": (top, [] + seriesopts, 'hg qtop [-s]'),
2195 2193 "qunapplied": (unapplied, [] + seriesopts, 'hg qunapplied [-s] [PATCH]'),
2196 2194 }
@@ -1,281 +1,282 b''
1 1 # notify.py - email notifications for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7 #
8 8 # hook extension to email notifications to people when changesets are
9 9 # committed to a repo they subscribe to.
10 10 #
11 11 # default mode is to print messages to stdout, for testing and
12 12 # configuring.
13 13 #
14 14 # to use, configure notify extension and enable in hgrc like this:
15 15 #
16 16 # [extensions]
17 17 # hgext.notify =
18 18 #
19 19 # [hooks]
20 20 # # one email for each incoming changeset
21 21 # incoming.notify = python:hgext.notify.hook
22 22 # # batch emails when many changesets incoming at one time
23 23 # changegroup.notify = python:hgext.notify.hook
24 24 #
25 25 # [notify]
26 26 # # config items go in here
27 27 #
28 28 # config items:
29 29 #
30 30 # REQUIRED:
31 31 # config = /path/to/file # file containing subscriptions
32 32 #
33 33 # OPTIONAL:
34 34 # test = True # print messages to stdout for testing
35 35 # strip = 3 # number of slashes to strip for url paths
36 36 # domain = example.com # domain to use if committer missing domain
37 37 # style = ... # style file to use when formatting email
38 38 # template = ... # template to use when formatting email
39 39 # incoming = ... # template to use when run as incoming hook
40 40 # changegroup = ... # template when run as changegroup hook
41 41 # maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
42 42 # maxsubject = 67 # truncate subject line longer than this
43 43 # diffstat = True # add a diffstat before the diff content
44 44 # sources = serve # notify if source of incoming changes in this list
45 45 # # (serve == ssh or http, push, pull, bundle)
46 46 # [email]
47 47 # from = user@host.com # email address to send as if none given
48 48 # [web]
49 49 # baseurl = http://hgserver/... # root of hg web site for browsing commits
50 50 #
51 51 # notify config file has same format as regular hgrc. it has two
52 52 # sections so you can express subscriptions in whatever way is handier
53 53 # for you.
54 54 #
55 55 # [usersubs]
56 56 # # key is subscriber email, value is ","-separated list of glob patterns
57 57 # user@host = pattern
58 58 #
59 59 # [reposubs]
60 60 # # key is glob pattern, value is ","-separated list of subscriber emails
61 61 # pattern = user@host
62 62 #
63 63 # glob patterns are matched against path to repo root.
64 64 #
65 65 # if you like, you can put notify config file in repo that users can
66 66 # push changes to, they can manage their own subscriptions.
67 67
68 68 from mercurial.i18n import _
69 69 from mercurial.node import *
70 70 from mercurial import patch, cmdutil, templater, util, mail
71 71 import email.Parser, fnmatch, socket, time
72 72
73 73 # template for single changeset can include email headers.
74 74 single_template = '''
75 75 Subject: changeset in {webroot}: {desc|firstline|strip}
76 76 From: {author}
77 77
78 78 changeset {node|short} in {root}
79 79 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
80 80 description:
81 81 \t{desc|tabindent|strip}
82 82 '''.lstrip()
83 83
84 84 # template for multiple changesets should not contain email headers,
85 85 # because only first set of headers will be used and result will look
86 86 # strange.
87 87 multiple_template = '''
88 88 changeset {node|short} in {root}
89 89 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
90 90 summary: {desc|firstline}
91 91 '''
92 92
93 93 deftemplates = {
94 94 'changegroup': multiple_template,
95 95 }
96 96
97 97 class notifier(object):
98 98 '''email notification class.'''
99 99
100 100 def __init__(self, ui, repo, hooktype):
101 101 self.ui = ui
102 102 cfg = self.ui.config('notify', 'config')
103 103 if cfg:
104 104 self.ui.readsections(cfg, 'usersubs', 'reposubs')
105 105 self.repo = repo
106 106 self.stripcount = int(self.ui.config('notify', 'strip', 0))
107 107 self.root = self.strip(self.repo.root)
108 108 self.domain = self.ui.config('notify', 'domain')
109 109 self.subs = self.subscribers()
110 110
111 111 mapfile = self.ui.config('notify', 'style')
112 112 template = (self.ui.config('notify', hooktype) or
113 113 self.ui.config('notify', 'template'))
114 114 self.t = cmdutil.changeset_templater(self.ui, self.repo,
115 115 False, mapfile, False)
116 116 if not mapfile and not template:
117 117 template = deftemplates.get(hooktype) or single_template
118 118 if template:
119 119 template = templater.parsestring(template, quoted=False)
120 120 self.t.use_template(template)
121 121
122 122 def strip(self, path):
123 123 '''strip leading slashes from local path, turn into web-safe path.'''
124 124
125 125 path = util.pconvert(path)
126 126 count = self.stripcount
127 127 while count > 0:
128 128 c = path.find('/')
129 129 if c == -1:
130 130 break
131 131 path = path[c+1:]
132 132 count -= 1
133 133 return path
134 134
135 135 def fixmail(self, addr):
136 136 '''try to clean up email addresses.'''
137 137
138 138 addr = templater.email(addr.strip())
139 a = addr.find('@localhost')
140 if a != -1:
141 addr = addr[:a]
142 if '@' not in addr:
143 return addr + '@' + self.domain
139 if self.domain:
140 a = addr.find('@localhost')
141 if a != -1:
142 addr = addr[:a]
143 if '@' not in addr:
144 return addr + '@' + self.domain
144 145 return addr
145 146
146 147 def subscribers(self):
147 148 '''return list of email addresses of subscribers to this repo.'''
148 149
149 150 subs = {}
150 151 for user, pats in self.ui.configitems('usersubs'):
151 152 for pat in pats.split(','):
152 153 if fnmatch.fnmatch(self.repo.root, pat.strip()):
153 154 subs[self.fixmail(user)] = 1
154 155 for pat, users in self.ui.configitems('reposubs'):
155 156 if fnmatch.fnmatch(self.repo.root, pat):
156 157 for user in users.split(','):
157 158 subs[self.fixmail(user)] = 1
158 159 subs = subs.keys()
159 160 subs.sort()
160 161 return subs
161 162
162 163 def url(self, path=None):
163 164 return self.ui.config('web', 'baseurl') + (path or self.root)
164 165
165 166 def node(self, node):
166 167 '''format one changeset.'''
167 168
168 169 self.t.show(changenode=node, changes=self.repo.changelog.read(node),
169 170 baseurl=self.ui.config('web', 'baseurl'),
170 171 root=self.repo.root,
171 172 webroot=self.root)
172 173
173 174 def skipsource(self, source):
174 175 '''true if incoming changes from this source should be skipped.'''
175 176 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
176 177 return source not in ok_sources
177 178
178 179 def send(self, node, count, data):
179 180 '''send message.'''
180 181
181 182 p = email.Parser.Parser()
182 183 msg = p.parsestr(data)
183 184
184 185 def fix_subject():
185 186 '''try to make subject line exist and be useful.'''
186 187
187 188 subject = msg['Subject']
188 189 if not subject:
189 190 if count > 1:
190 191 subject = _('%s: %d new changesets') % (self.root, count)
191 192 else:
192 193 changes = self.repo.changelog.read(node)
193 194 s = changes[4].lstrip().split('\n', 1)[0].rstrip()
194 195 subject = '%s: %s' % (self.root, s)
195 196 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
196 197 if maxsubject and len(subject) > maxsubject:
197 198 subject = subject[:maxsubject-3] + '...'
198 199 del msg['Subject']
199 200 msg['Subject'] = subject
200 201
201 202 def fix_sender():
202 203 '''try to make message have proper sender.'''
203 204
204 205 sender = msg['From']
205 206 if not sender:
206 207 sender = self.ui.config('email', 'from') or self.ui.username()
207 208 if '@' not in sender or '@localhost' in sender:
208 209 sender = self.fixmail(sender)
209 210 del msg['From']
210 211 msg['From'] = sender
211 212
212 213 fix_subject()
213 214 fix_sender()
214 215
215 216 msg['X-Hg-Notification'] = 'changeset ' + short(node)
216 217 if not msg['Message-Id']:
217 218 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
218 219 (short(node), int(time.time()),
219 220 hash(self.repo.root), socket.getfqdn()))
220 221 msg['To'] = ', '.join(self.subs)
221 222
222 223 msgtext = msg.as_string(0)
223 224 if self.ui.configbool('notify', 'test', True):
224 225 self.ui.write(msgtext)
225 226 if not msgtext.endswith('\n'):
226 227 self.ui.write('\n')
227 228 else:
228 229 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
229 230 (len(self.subs), count))
230 231 mail.sendmail(self.ui, templater.email(msg['From']),
231 232 self.subs, msgtext)
232 233
233 234 def diff(self, node, ref):
234 235 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
235 236 if maxdiff == 0:
236 237 return
237 238 prev = self.repo.changelog.parents(node)[0]
238 239 self.ui.pushbuffer()
239 240 patch.diff(self.repo, prev, ref)
240 241 difflines = self.ui.popbuffer().splitlines(1)
241 242 if self.ui.configbool('notify', 'diffstat', True):
242 243 s = patch.diffstat(difflines)
243 244 # s may be nil, don't include the header if it is
244 245 if s:
245 246 self.ui.write('\ndiffstat:\n\n%s' % s)
246 247 if maxdiff > 0 and len(difflines) > maxdiff:
247 248 self.ui.write(_('\ndiffs (truncated from %d to %d lines):\n\n') %
248 249 (len(difflines), maxdiff))
249 250 difflines = difflines[:maxdiff]
250 251 elif difflines:
251 252 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
252 253 self.ui.write(*difflines)
253 254
254 255 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
255 256 '''send email notifications to interested subscribers.
256 257
257 258 if used as changegroup hook, send one email for all changesets in
258 259 changegroup. else send one email per changeset.'''
259 260 n = notifier(ui, repo, hooktype)
260 261 if not n.subs:
261 262 ui.debug(_('notify: no subscribers to repo %s\n') % n.root)
262 263 return
263 264 if n.skipsource(source):
264 265 ui.debug(_('notify: changes have source "%s" - skipping\n') %
265 266 source)
266 267 return
267 268 node = bin(node)
268 269 ui.pushbuffer()
269 270 if hooktype == 'changegroup':
270 271 start = repo.changelog.rev(node)
271 272 end = repo.changelog.count()
272 273 count = end - start
273 274 for rev in xrange(start, end):
274 275 n.node(repo.changelog.node(rev))
275 276 n.diff(node, repo.changelog.tip())
276 277 else:
277 278 count = 1
278 279 n.node(node)
279 280 n.diff(node, node)
280 281 data = ui.popbuffer()
281 282 n.send(node, count, data)
@@ -1,1152 +1,1156 b''
1 1 # hgweb/hgweb_mod.py - Web interface for a repository.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 import os, mimetypes, re, zlib, mimetools, cStringIO, sys
10 10 import tempfile, urllib, bz2
11 11 from mercurial.node import *
12 12 from mercurial.i18n import gettext as _
13 13 from mercurial import mdiff, ui, hg, util, archival, streamclone, patch
14 14 from mercurial import revlog, templater
15 15 from common import get_mtime, staticfile, style_map
16 16
17 17 def _up(p):
18 18 if p[0] != "/":
19 19 p = "/" + p
20 20 if p[-1] == "/":
21 21 p = p[:-1]
22 22 up = os.path.dirname(p)
23 23 if up == "/":
24 24 return "/"
25 25 return up + "/"
26 26
27 27 def revnavgen(pos, pagelen, limit, nodefunc):
28 28 def seq(factor, limit=None):
29 29 if limit:
30 30 yield limit
31 31 if limit >= 20 and limit <= 40:
32 32 yield 50
33 33 else:
34 34 yield 1 * factor
35 35 yield 3 * factor
36 36 for f in seq(factor * 10):
37 37 yield f
38 38
39 39 def nav(**map):
40 40 l = []
41 41 last = 0
42 42 for f in seq(1, pagelen):
43 43 if f < pagelen or f <= last:
44 44 continue
45 45 if f > limit:
46 46 break
47 47 last = f
48 48 if pos + f < limit:
49 49 l.append(("+%d" % f, hex(nodefunc(pos + f).node())))
50 50 if pos - f >= 0:
51 51 l.insert(0, ("-%d" % f, hex(nodefunc(pos - f).node())))
52 52
53 53 try:
54 54 yield {"label": "(0)", "node": hex(nodefunc('0').node())}
55 55
56 56 for label, node in l:
57 57 yield {"label": label, "node": node}
58 58
59 59 yield {"label": "tip", "node": "tip"}
60 60 except hg.RepoError:
61 61 pass
62 62
63 63 return nav
64 64
65 65 class hgweb(object):
66 66 def __init__(self, repo, name=None):
67 67 if type(repo) == type(""):
68 68 self.repo = hg.repository(ui.ui(report_untrusted=False), repo)
69 69 else:
70 70 self.repo = repo
71 71
72 72 self.mtime = -1
73 73 self.reponame = name
74 74 self.archives = 'zip', 'gz', 'bz2'
75 75 self.stripecount = 1
76 76 # a repo owner may set web.templates in .hg/hgrc to get any file
77 77 # readable by the user running the CGI script
78 78 self.templatepath = self.config("web", "templates",
79 79 templater.templatepath(),
80 80 untrusted=False)
81 81
82 82 # The CGI scripts are often run by a user different from the repo owner.
83 83 # Trust the settings from the .hg/hgrc files by default.
84 84 def config(self, section, name, default=None, untrusted=True):
85 85 return self.repo.ui.config(section, name, default,
86 86 untrusted=untrusted)
87 87
88 88 def configbool(self, section, name, default=False, untrusted=True):
89 89 return self.repo.ui.configbool(section, name, default,
90 90 untrusted=untrusted)
91 91
92 92 def configlist(self, section, name, default=None, untrusted=True):
93 93 return self.repo.ui.configlist(section, name, default,
94 94 untrusted=untrusted)
95 95
96 96 def refresh(self):
97 97 mtime = get_mtime(self.repo.root)
98 98 if mtime != self.mtime:
99 99 self.mtime = mtime
100 100 self.repo = hg.repository(self.repo.ui, self.repo.root)
101 101 self.maxchanges = int(self.config("web", "maxchanges", 10))
102 102 self.stripecount = int(self.config("web", "stripes", 1))
103 103 self.maxshortchanges = int(self.config("web", "maxshortchanges", 60))
104 104 self.maxfiles = int(self.config("web", "maxfiles", 10))
105 105 self.allowpull = self.configbool("web", "allowpull", True)
106 106
107 107 def archivelist(self, nodeid):
108 108 allowed = self.configlist("web", "allow_archive")
109 109 for i, spec in self.archive_specs.iteritems():
110 110 if i in allowed or self.configbool("web", "allow" + i):
111 111 yield {"type" : i, "extension" : spec[2], "node" : nodeid}
112 112
113 113 def listfilediffs(self, files, changeset):
114 114 for f in files[:self.maxfiles]:
115 115 yield self.t("filedifflink", node=hex(changeset), file=f)
116 116 if len(files) > self.maxfiles:
117 117 yield self.t("fileellipses")
118 118
119 119 def siblings(self, siblings=[], hiderev=None, **args):
120 120 siblings = [s for s in siblings if s.node() != nullid]
121 121 if len(siblings) == 1 and siblings[0].rev() == hiderev:
122 122 return
123 123 for s in siblings:
124 124 d = {'node': hex(s.node()), 'rev': s.rev()}
125 125 if hasattr(s, 'path'):
126 126 d['file'] = s.path()
127 127 d.update(args)
128 128 yield d
129 129
130 130 def renamelink(self, fl, node):
131 131 r = fl.renamed(node)
132 132 if r:
133 133 return [dict(file=r[0], node=hex(r[1]))]
134 134 return []
135 135
136 136 def showtag(self, t1, node=nullid, **args):
137 137 for t in self.repo.nodetags(node):
138 138 yield self.t(t1, tag=t, **args)
139 139
140 140 def diff(self, node1, node2, files):
141 141 def filterfiles(filters, files):
142 142 l = [x for x in files if x in filters]
143 143
144 144 for t in filters:
145 145 if t and t[-1] != os.sep:
146 146 t += os.sep
147 147 l += [x for x in files if x.startswith(t)]
148 148 return l
149 149
150 150 parity = [0]
151 151 def diffblock(diff, f, fn):
152 152 yield self.t("diffblock",
153 153 lines=prettyprintlines(diff),
154 154 parity=parity[0],
155 155 file=f,
156 156 filenode=hex(fn or nullid))
157 157 parity[0] = 1 - parity[0]
158 158
159 159 def prettyprintlines(diff):
160 160 for l in diff.splitlines(1):
161 161 if l.startswith('+'):
162 162 yield self.t("difflineplus", line=l)
163 163 elif l.startswith('-'):
164 164 yield self.t("difflineminus", line=l)
165 165 elif l.startswith('@'):
166 166 yield self.t("difflineat", line=l)
167 167 else:
168 168 yield self.t("diffline", line=l)
169 169
170 170 r = self.repo
171 171 c1 = r.changectx(node1)
172 172 c2 = r.changectx(node2)
173 173 date1 = util.datestr(c1.date())
174 174 date2 = util.datestr(c2.date())
175 175
176 176 modified, added, removed, deleted, unknown = r.status(node1, node2)[:5]
177 177 if files:
178 178 modified, added, removed = map(lambda x: filterfiles(files, x),
179 179 (modified, added, removed))
180 180
181 181 diffopts = patch.diffopts(self.repo.ui, untrusted=True)
182 182 for f in modified:
183 183 to = c1.filectx(f).data()
184 184 tn = c2.filectx(f).data()
185 185 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
186 186 opts=diffopts), f, tn)
187 187 for f in added:
188 188 to = None
189 189 tn = c2.filectx(f).data()
190 190 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
191 191 opts=diffopts), f, tn)
192 192 for f in removed:
193 193 to = c1.filectx(f).data()
194 194 tn = None
195 195 yield diffblock(mdiff.unidiff(to, date1, tn, date2, f,
196 196 opts=diffopts), f, tn)
197 197
198 198 def changelog(self, ctx, shortlog=False):
199 199 def changelist(**map):
200 200 parity = (start - end) & 1
201 201 cl = self.repo.changelog
202 202 l = [] # build a list in forward order for efficiency
203 203 for i in xrange(start, end):
204 204 ctx = self.repo.changectx(i)
205 205 n = ctx.node()
206 206
207 207 l.insert(0, {"parity": parity,
208 208 "author": ctx.user(),
209 209 "parent": self.siblings(ctx.parents(), i - 1),
210 210 "child": self.siblings(ctx.children(), i + 1),
211 211 "changelogtag": self.showtag("changelogtag",n),
212 212 "desc": ctx.description(),
213 213 "date": ctx.date(),
214 214 "files": self.listfilediffs(ctx.files(), n),
215 215 "rev": i,
216 216 "node": hex(n)})
217 217 parity = 1 - parity
218 218
219 219 for e in l:
220 220 yield e
221 221
222 222 maxchanges = shortlog and self.maxshortchanges or self.maxchanges
223 223 cl = self.repo.changelog
224 224 count = cl.count()
225 225 pos = ctx.rev()
226 226 start = max(0, pos - maxchanges + 1)
227 227 end = min(count, start + maxchanges)
228 228 pos = end - 1
229 229
230 230 changenav = revnavgen(pos, maxchanges, count, self.repo.changectx)
231 231
232 232 yield self.t(shortlog and 'shortlog' or 'changelog',
233 233 changenav=changenav,
234 234 node=hex(cl.tip()),
235 235 rev=pos, changesets=count, entries=changelist,
236 236 archives=self.archivelist("tip"))
237 237
238 238 def search(self, query):
239 239
240 240 def changelist(**map):
241 241 cl = self.repo.changelog
242 242 count = 0
243 243 qw = query.lower().split()
244 244
245 245 def revgen():
246 246 for i in xrange(cl.count() - 1, 0, -100):
247 247 l = []
248 248 for j in xrange(max(0, i - 100), i):
249 249 ctx = self.repo.changectx(j)
250 250 l.append(ctx)
251 251 l.reverse()
252 252 for e in l:
253 253 yield e
254 254
255 255 for ctx in revgen():
256 256 miss = 0
257 257 for q in qw:
258 258 if not (q in ctx.user().lower() or
259 259 q in ctx.description().lower() or
260 260 q in " ".join(ctx.files()[:20]).lower()):
261 261 miss = 1
262 262 break
263 263 if miss:
264 264 continue
265 265
266 266 count += 1
267 267 n = ctx.node()
268 268
269 269 yield self.t('searchentry',
270 270 parity=self.stripes(count),
271 271 author=ctx.user(),
272 272 parent=self.siblings(ctx.parents()),
273 273 child=self.siblings(ctx.children()),
274 274 changelogtag=self.showtag("changelogtag",n),
275 275 desc=ctx.description(),
276 276 date=ctx.date(),
277 277 files=self.listfilediffs(ctx.files(), n),
278 278 rev=ctx.rev(),
279 279 node=hex(n))
280 280
281 281 if count >= self.maxchanges:
282 282 break
283 283
284 284 cl = self.repo.changelog
285 285
286 286 yield self.t('search',
287 287 query=query,
288 288 node=hex(cl.tip()),
289 289 entries=changelist)
290 290
291 291 def changeset(self, ctx):
292 292 n = ctx.node()
293 293 parents = ctx.parents()
294 294 p1 = parents[0].node()
295 295
296 296 files = []
297 297 parity = 0
298 298 for f in ctx.files():
299 299 files.append(self.t("filenodelink",
300 300 node=hex(n), file=f,
301 301 parity=parity))
302 302 parity = 1 - parity
303 303
304 304 def diff(**map):
305 305 yield self.diff(p1, n, None)
306 306
307 307 yield self.t('changeset',
308 308 diff=diff,
309 309 rev=ctx.rev(),
310 310 node=hex(n),
311 311 parent=self.siblings(parents),
312 312 child=self.siblings(ctx.children()),
313 313 changesettag=self.showtag("changesettag",n),
314 314 author=ctx.user(),
315 315 desc=ctx.description(),
316 316 date=ctx.date(),
317 317 files=files,
318 318 archives=self.archivelist(hex(n)))
319 319
320 320 def filelog(self, fctx):
321 321 f = fctx.path()
322 322 fl = fctx.filelog()
323 323 count = fl.count()
324 324 pagelen = self.maxshortchanges
325 325 pos = fctx.filerev()
326 326 start = max(0, pos - pagelen + 1)
327 327 end = min(count, start + pagelen)
328 328 pos = end - 1
329 329
330 330 def entries(**map):
331 331 l = []
332 332 parity = (count - 1) & 1
333 333
334 334 for i in xrange(start, end):
335 335 ctx = fctx.filectx(i)
336 336 n = fl.node(i)
337 337
338 338 l.insert(0, {"parity": parity,
339 339 "filerev": i,
340 340 "file": f,
341 341 "node": hex(ctx.node()),
342 342 "author": ctx.user(),
343 343 "date": ctx.date(),
344 344 "rename": self.renamelink(fl, n),
345 345 "parent": self.siblings(fctx.parents()),
346 346 "child": self.siblings(fctx.children()),
347 347 "desc": ctx.description()})
348 348 parity = 1 - parity
349 349
350 350 for e in l:
351 351 yield e
352 352
353 353 nodefunc = lambda x: fctx.filectx(fileid=x)
354 354 nav = revnavgen(pos, pagelen, count, nodefunc)
355 355 yield self.t("filelog", file=f, node=hex(fctx.node()), nav=nav,
356 356 entries=entries)
357 357
358 358 def filerevision(self, fctx):
359 359 f = fctx.path()
360 360 text = fctx.data()
361 361 fl = fctx.filelog()
362 362 n = fctx.filenode()
363 363
364 364 mt = mimetypes.guess_type(f)[0]
365 365 rawtext = text
366 366 if util.binary(text):
367 367 mt = mt or 'application/octet-stream'
368 368 text = "(binary:%s)" % mt
369 369 mt = mt or 'text/plain'
370 370
371 371 def lines():
372 372 for l, t in enumerate(text.splitlines(1)):
373 373 yield {"line": t,
374 374 "linenumber": "% 6d" % (l + 1),
375 375 "parity": self.stripes(l)}
376 376
377 377 yield self.t("filerevision",
378 378 file=f,
379 379 path=_up(f),
380 380 text=lines(),
381 381 raw=rawtext,
382 382 mimetype=mt,
383 383 rev=fctx.rev(),
384 384 node=hex(fctx.node()),
385 385 author=fctx.user(),
386 386 date=fctx.date(),
387 387 desc=fctx.description(),
388 388 parent=self.siblings(fctx.parents()),
389 389 child=self.siblings(fctx.children()),
390 390 rename=self.renamelink(fl, n),
391 391 permissions=fctx.manifest().execf(f))
392 392
393 393 def fileannotate(self, fctx):
394 394 f = fctx.path()
395 395 n = fctx.filenode()
396 396 fl = fctx.filelog()
397 397
398 398 def annotate(**map):
399 399 parity = 0
400 400 last = None
401 401 for f, l in fctx.annotate(follow=True):
402 402 fnode = f.filenode()
403 403 name = self.repo.ui.shortuser(f.user())
404 404
405 405 if last != fnode:
406 406 parity = 1 - parity
407 407 last = fnode
408 408
409 409 yield {"parity": parity,
410 410 "node": hex(f.node()),
411 411 "rev": f.rev(),
412 412 "author": name,
413 413 "file": f.path(),
414 414 "line": l}
415 415
416 416 yield self.t("fileannotate",
417 417 file=f,
418 418 annotate=annotate,
419 419 path=_up(f),
420 420 rev=fctx.rev(),
421 421 node=hex(fctx.node()),
422 422 author=fctx.user(),
423 423 date=fctx.date(),
424 424 desc=fctx.description(),
425 425 rename=self.renamelink(fl, n),
426 426 parent=self.siblings(fctx.parents()),
427 427 child=self.siblings(fctx.children()),
428 428 permissions=fctx.manifest().execf(f))
429 429
430 430 def manifest(self, ctx, path):
431 431 mf = ctx.manifest()
432 432 node = ctx.node()
433 433
434 434 files = {}
435 435
436 436 if path and path[-1] != "/":
437 437 path += "/"
438 438 l = len(path)
439 439 abspath = "/" + path
440 440
441 441 for f, n in mf.items():
442 442 if f[:l] != path:
443 443 continue
444 444 remain = f[l:]
445 445 if "/" in remain:
446 446 short = remain[:remain.index("/") + 1] # bleah
447 447 files[short] = (f, None)
448 448 else:
449 449 short = os.path.basename(remain)
450 450 files[short] = (f, n)
451 451
452 452 def filelist(**map):
453 453 parity = 0
454 454 fl = files.keys()
455 455 fl.sort()
456 456 for f in fl:
457 457 full, fnode = files[f]
458 458 if not fnode:
459 459 continue
460 460
461 461 yield {"file": full,
462 462 "parity": self.stripes(parity),
463 463 "basename": f,
464 464 "size": ctx.filectx(full).size(),
465 465 "permissions": mf.execf(full)}
466 466 parity += 1
467 467
468 468 def dirlist(**map):
469 469 parity = 0
470 470 fl = files.keys()
471 471 fl.sort()
472 472 for f in fl:
473 473 full, fnode = files[f]
474 474 if fnode:
475 475 continue
476 476
477 477 yield {"parity": self.stripes(parity),
478 478 "path": os.path.join(abspath, f),
479 479 "basename": f[:-1]}
480 480 parity += 1
481 481
482 482 yield self.t("manifest",
483 483 rev=ctx.rev(),
484 484 node=hex(node),
485 485 path=abspath,
486 486 up=_up(abspath),
487 487 fentries=filelist,
488 488 dentries=dirlist,
489 489 archives=self.archivelist(hex(node)))
490 490
491 491 def tags(self):
492 492 i = self.repo.tagslist()
493 493 i.reverse()
494 494
495 495 def entries(notip=False, **map):
496 496 parity = 0
497 497 for k, n in i:
498 498 if notip and k == "tip":
499 499 continue
500 500 yield {"parity": self.stripes(parity),
501 501 "tag": k,
502 502 "date": self.repo.changectx(n).date(),
503 503 "node": hex(n)}
504 504 parity += 1
505 505
506 506 yield self.t("tags",
507 507 node=hex(self.repo.changelog.tip()),
508 508 entries=lambda **x: entries(False, **x),
509 509 entriesnotip=lambda **x: entries(True, **x))
510 510
511 511 def summary(self):
512 512 i = self.repo.tagslist()
513 513 i.reverse()
514 514
515 515 def tagentries(**map):
516 516 parity = 0
517 517 count = 0
518 518 for k, n in i:
519 519 if k == "tip": # skip tip
520 520 continue;
521 521
522 522 count += 1
523 523 if count > 10: # limit to 10 tags
524 524 break;
525 525
526 526 yield self.t("tagentry",
527 527 parity=self.stripes(parity),
528 528 tag=k,
529 529 node=hex(n),
530 530 date=self.repo.changectx(n).date())
531 531 parity += 1
532 532
533 533 def heads(**map):
534 534 parity = 0
535 535 count = 0
536 536
537 537 for node in self.repo.heads():
538 538 count += 1
539 539 if count > 10:
540 540 break;
541 541
542 542 ctx = self.repo.changectx(node)
543 543
544 544 yield {'parity': self.stripes(parity),
545 545 'branch': ctx.branch(),
546 546 'node': hex(node),
547 547 'date': ctx.date()}
548 548 parity += 1
549 549
550 550 def changelist(**map):
551 551 parity = 0
552 552 l = [] # build a list in forward order for efficiency
553 553 for i in xrange(start, end):
554 554 ctx = self.repo.changectx(i)
555 555 hn = hex(ctx.node())
556 556
557 557 l.insert(0, self.t(
558 558 'shortlogentry',
559 559 parity=parity,
560 560 author=ctx.user(),
561 561 desc=ctx.description(),
562 562 date=ctx.date(),
563 563 rev=i,
564 564 node=hn))
565 565 parity = 1 - parity
566 566
567 567 yield l
568 568
569 569 cl = self.repo.changelog
570 570 count = cl.count()
571 571 start = max(0, count - self.maxchanges)
572 572 end = min(count, start + self.maxchanges)
573 573
574 574 yield self.t("summary",
575 575 desc=self.config("web", "description", "unknown"),
576 576 owner=(self.config("ui", "username") or # preferred
577 577 self.config("web", "contact") or # deprecated
578 578 self.config("web", "author", "unknown")), # also
579 579 lastchange=cl.read(cl.tip())[2],
580 580 tags=tagentries,
581 581 heads=heads,
582 582 shortlog=changelist,
583 583 node=hex(cl.tip()),
584 584 archives=self.archivelist("tip"))
585 585
586 586 def filediff(self, fctx):
587 587 n = fctx.node()
588 588 path = fctx.path()
589 589 parents = fctx.parents()
590 590 p1 = parents and parents[0].node() or nullid
591 591
592 592 def diff(**map):
593 593 yield self.diff(p1, n, [path])
594 594
595 595 yield self.t("filediff",
596 596 file=path,
597 597 node=hex(n),
598 598 rev=fctx.rev(),
599 599 parent=self.siblings(parents),
600 600 child=self.siblings(fctx.children()),
601 601 diff=diff)
602 602
603 603 archive_specs = {
604 604 'bz2': ('application/x-tar', 'tbz2', '.tar.bz2', None),
605 605 'gz': ('application/x-tar', 'tgz', '.tar.gz', None),
606 606 'zip': ('application/zip', 'zip', '.zip', None),
607 607 }
608 608
609 609 def archive(self, req, cnode, type_):
610 610 reponame = re.sub(r"\W+", "-", os.path.basename(self.reponame))
611 611 name = "%s-%s" % (reponame, short(cnode))
612 612 mimetype, artype, extension, encoding = self.archive_specs[type_]
613 613 headers = [('Content-type', mimetype),
614 614 ('Content-disposition', 'attachment; filename=%s%s' %
615 615 (name, extension))]
616 616 if encoding:
617 617 headers.append(('Content-encoding', encoding))
618 618 req.header(headers)
619 619 archival.archive(self.repo, req.out, cnode, artype, prefix=name)
620 620
621 621 # add tags to things
622 622 # tags -> list of changesets corresponding to tags
623 623 # find tag, changeset, file
624 624
625 625 def cleanpath(self, path):
626 626 path = path.lstrip('/')
627 627 return util.canonpath(self.repo.root, '', path)
628 628
629 629 def run(self):
630 630 if not os.environ.get('GATEWAY_INTERFACE', '').startswith("CGI/1."):
631 631 raise RuntimeError("This function is only intended to be called while running as a CGI script.")
632 632 import mercurial.hgweb.wsgicgi as wsgicgi
633 633 from request import wsgiapplication
634 634 def make_web_app():
635 635 return self
636 636 wsgicgi.launch(wsgiapplication(make_web_app))
637 637
638 638 def run_wsgi(self, req):
639 639 def header(**map):
640 640 header_file = cStringIO.StringIO(
641 641 ''.join(self.t("header", encoding=util._encoding, **map)))
642 642 msg = mimetools.Message(header_file, 0)
643 643 req.header(msg.items())
644 644 yield header_file.read()
645 645
646 646 def rawfileheader(**map):
647 647 req.header([('Content-type', map['mimetype']),
648 648 ('Content-disposition', 'filename=%s' % map['file']),
649 649 ('Content-length', str(len(map['raw'])))])
650 650 yield ''
651 651
652 652 def footer(**map):
653 653 yield self.t("footer", **map)
654 654
655 655 def motd(**map):
656 656 yield self.config("web", "motd", "")
657 657
658 658 def expand_form(form):
659 659 shortcuts = {
660 660 'cl': [('cmd', ['changelog']), ('rev', None)],
661 661 'sl': [('cmd', ['shortlog']), ('rev', None)],
662 662 'cs': [('cmd', ['changeset']), ('node', None)],
663 663 'f': [('cmd', ['file']), ('filenode', None)],
664 664 'fl': [('cmd', ['filelog']), ('filenode', None)],
665 665 'fd': [('cmd', ['filediff']), ('node', None)],
666 666 'fa': [('cmd', ['annotate']), ('filenode', None)],
667 667 'mf': [('cmd', ['manifest']), ('manifest', None)],
668 668 'ca': [('cmd', ['archive']), ('node', None)],
669 669 'tags': [('cmd', ['tags'])],
670 670 'tip': [('cmd', ['changeset']), ('node', ['tip'])],
671 671 'static': [('cmd', ['static']), ('file', None)]
672 672 }
673 673
674 674 for k in shortcuts.iterkeys():
675 675 if form.has_key(k):
676 676 for name, value in shortcuts[k]:
677 677 if value is None:
678 678 value = form[k]
679 679 form[name] = value
680 680 del form[k]
681 681
682 682 def rewrite_request(req):
683 683 '''translate new web interface to traditional format'''
684 684
685 685 def spliturl(req):
686 686 def firstitem(query):
687 687 return query.split('&', 1)[0].split(';', 1)[0]
688 688
689 689 def normurl(url):
690 690 inner = '/'.join([x for x in url.split('/') if x])
691 691 tl = len(url) > 1 and url.endswith('/') and '/' or ''
692 692
693 693 return '%s%s%s' % (url.startswith('/') and '/' or '',
694 694 inner, tl)
695 695
696 696 root = normurl(urllib.unquote(req.env.get('REQUEST_URI', '').split('?', 1)[0]))
697 697 pi = normurl(req.env.get('PATH_INFO', ''))
698 698 if pi:
699 699 # strip leading /
700 700 pi = pi[1:]
701 701 if pi:
702 702 root = root[:-len(pi)]
703 703 if req.env.has_key('REPO_NAME'):
704 704 rn = req.env['REPO_NAME'] + '/'
705 705 root += rn
706 706 query = pi[len(rn):]
707 707 else:
708 708 query = pi
709 709 else:
710 710 root += '?'
711 711 query = firstitem(req.env['QUERY_STRING'])
712 712
713 713 return (root, query)
714 714
715 715 req.url, query = spliturl(req)
716 716
717 717 if req.form.has_key('cmd'):
718 718 # old style
719 719 return
720 720
721 721 args = query.split('/', 2)
722 722 if not args or not args[0]:
723 723 return
724 724
725 725 cmd = args.pop(0)
726 726 style = cmd.rfind('-')
727 727 if style != -1:
728 728 req.form['style'] = [cmd[:style]]
729 729 cmd = cmd[style+1:]
730 730 # avoid accepting e.g. style parameter as command
731 731 if hasattr(self, 'do_' + cmd):
732 732 req.form['cmd'] = [cmd]
733 733
734 734 if args and args[0]:
735 735 node = args.pop(0)
736 736 req.form['node'] = [node]
737 737 if args:
738 738 req.form['file'] = args
739 739
740 740 if cmd == 'static':
741 741 req.form['file'] = req.form['node']
742 742 elif cmd == 'archive':
743 743 fn = req.form['node'][0]
744 744 for type_, spec in self.archive_specs.iteritems():
745 745 ext = spec[2]
746 746 if fn.endswith(ext):
747 747 req.form['node'] = [fn[:-len(ext)]]
748 748 req.form['type'] = [type_]
749 749
750 750 def sessionvars(**map):
751 751 fields = []
752 752 if req.form.has_key('style'):
753 753 style = req.form['style'][0]
754 754 if style != self.config('web', 'style', ''):
755 755 fields.append(('style', style))
756 756
757 757 separator = req.url[-1] == '?' and ';' or '?'
758 758 for name, value in fields:
759 759 yield dict(name=name, value=value, separator=separator)
760 760 separator = ';'
761 761
762 762 self.refresh()
763 763
764 764 expand_form(req.form)
765 765 rewrite_request(req)
766 766
767 767 style = self.config("web", "style", "")
768 768 if req.form.has_key('style'):
769 769 style = req.form['style'][0]
770 770 mapfile = style_map(self.templatepath, style)
771 771
772 772 port = req.env["SERVER_PORT"]
773 773 port = port != "80" and (":" + port) or ""
774 774 urlbase = 'http://%s%s' % (req.env['SERVER_NAME'], port)
775 775 staticurl = self.config("web", "staticurl") or req.url + 'static/'
776 776 if not staticurl.endswith('/'):
777 777 staticurl += '/'
778 778
779 779 if not self.reponame:
780 780 self.reponame = (self.config("web", "name")
781 781 or req.env.get('REPO_NAME')
782 782 or req.url.strip('/') or self.repo.root)
783 783
784 784 self.t = templater.templater(mapfile, templater.common_filters,
785 785 defaults={"url": req.url,
786 786 "staticurl": staticurl,
787 787 "urlbase": urlbase,
788 788 "repo": self.reponame,
789 789 "header": header,
790 790 "footer": footer,
791 791 "motd": motd,
792 792 "rawfileheader": rawfileheader,
793 793 "sessionvars": sessionvars
794 794 })
795 795
796 796 if not req.form.has_key('cmd'):
797 797 req.form['cmd'] = [self.t.cache['default']]
798 798
799 799 cmd = req.form['cmd'][0]
800 800
801 801 method = getattr(self, 'do_' + cmd, None)
802 802 if method:
803 803 try:
804 804 method(req)
805 805 except (hg.RepoError, revlog.RevlogError), inst:
806 806 req.write(self.t("error", error=str(inst)))
807 807 else:
808 808 req.write(self.t("error", error='No such method: ' + cmd))
809 809
810 810 def changectx(self, req):
811 811 if req.form.has_key('node'):
812 812 changeid = req.form['node'][0]
813 813 elif req.form.has_key('manifest'):
814 814 changeid = req.form['manifest'][0]
815 815 else:
816 816 changeid = self.repo.changelog.count() - 1
817 817
818 818 try:
819 819 ctx = self.repo.changectx(changeid)
820 820 except hg.RepoError:
821 821 man = self.repo.manifest
822 822 mn = man.lookup(changeid)
823 823 ctx = self.repo.changectx(man.linkrev(mn))
824 824
825 825 return ctx
826 826
827 827 def filectx(self, req):
828 828 path = self.cleanpath(req.form['file'][0])
829 829 if req.form.has_key('node'):
830 830 changeid = req.form['node'][0]
831 831 else:
832 832 changeid = req.form['filenode'][0]
833 833 try:
834 834 ctx = self.repo.changectx(changeid)
835 835 fctx = ctx.filectx(path)
836 836 except hg.RepoError:
837 837 fctx = self.repo.filectx(path, fileid=changeid)
838 838
839 839 return fctx
840 840
841 841 def stripes(self, parity):
842 842 "make horizontal stripes for easier reading"
843 843 if self.stripecount:
844 844 return (1 + parity / self.stripecount) & 1
845 845 else:
846 846 return 0
847 847
848 848 def do_log(self, req):
849 849 if req.form.has_key('file') and req.form['file'][0]:
850 850 self.do_filelog(req)
851 851 else:
852 852 self.do_changelog(req)
853 853
854 854 def do_rev(self, req):
855 855 self.do_changeset(req)
856 856
857 857 def do_file(self, req):
858 858 path = self.cleanpath(req.form.get('file', [''])[0])
859 859 if path:
860 860 try:
861 861 req.write(self.filerevision(self.filectx(req)))
862 862 return
863 863 except revlog.LookupError:
864 864 pass
865 865
866 866 req.write(self.manifest(self.changectx(req), path))
867 867
868 868 def do_diff(self, req):
869 869 self.do_filediff(req)
870 870
871 871 def do_changelog(self, req, shortlog = False):
872 872 if req.form.has_key('node'):
873 873 ctx = self.changectx(req)
874 874 else:
875 875 if req.form.has_key('rev'):
876 876 hi = req.form['rev'][0]
877 877 else:
878 878 hi = self.repo.changelog.count() - 1
879 879 try:
880 880 ctx = self.repo.changectx(hi)
881 881 except hg.RepoError:
882 882 req.write(self.search(hi)) # XXX redirect to 404 page?
883 883 return
884 884
885 885 req.write(self.changelog(ctx, shortlog = shortlog))
886 886
887 887 def do_shortlog(self, req):
888 888 self.do_changelog(req, shortlog = True)
889 889
890 890 def do_changeset(self, req):
891 891 req.write(self.changeset(self.changectx(req)))
892 892
893 893 def do_manifest(self, req):
894 894 req.write(self.manifest(self.changectx(req),
895 895 self.cleanpath(req.form['path'][0])))
896 896
897 897 def do_tags(self, req):
898 898 req.write(self.tags())
899 899
900 900 def do_summary(self, req):
901 901 req.write(self.summary())
902 902
903 903 def do_filediff(self, req):
904 904 req.write(self.filediff(self.filectx(req)))
905 905
906 906 def do_annotate(self, req):
907 907 req.write(self.fileannotate(self.filectx(req)))
908 908
909 909 def do_filelog(self, req):
910 910 req.write(self.filelog(self.filectx(req)))
911 911
912 912 def do_lookup(self, req):
913 913 try:
914 914 r = hex(self.repo.lookup(req.form['key'][0]))
915 915 success = 1
916 916 except Exception,inst:
917 917 r = str(inst)
918 918 success = 0
919 919 resp = "%s %s\n" % (success, r)
920 920 req.httphdr("application/mercurial-0.1", length=len(resp))
921 921 req.write(resp)
922 922
923 923 def do_heads(self, req):
924 924 resp = " ".join(map(hex, self.repo.heads())) + "\n"
925 925 req.httphdr("application/mercurial-0.1", length=len(resp))
926 926 req.write(resp)
927 927
928 928 def do_branches(self, req):
929 929 nodes = []
930 930 if req.form.has_key('nodes'):
931 931 nodes = map(bin, req.form['nodes'][0].split(" "))
932 932 resp = cStringIO.StringIO()
933 933 for b in self.repo.branches(nodes):
934 934 resp.write(" ".join(map(hex, b)) + "\n")
935 935 resp = resp.getvalue()
936 936 req.httphdr("application/mercurial-0.1", length=len(resp))
937 937 req.write(resp)
938 938
939 939 def do_between(self, req):
940 940 if req.form.has_key('pairs'):
941 941 pairs = [map(bin, p.split("-"))
942 942 for p in req.form['pairs'][0].split(" ")]
943 943 resp = cStringIO.StringIO()
944 944 for b in self.repo.between(pairs):
945 945 resp.write(" ".join(map(hex, b)) + "\n")
946 946 resp = resp.getvalue()
947 947 req.httphdr("application/mercurial-0.1", length=len(resp))
948 948 req.write(resp)
949 949
950 950 def do_changegroup(self, req):
951 951 req.httphdr("application/mercurial-0.1")
952 952 nodes = []
953 953 if not self.allowpull:
954 954 return
955 955
956 956 if req.form.has_key('roots'):
957 957 nodes = map(bin, req.form['roots'][0].split(" "))
958 958
959 959 z = zlib.compressobj()
960 960 f = self.repo.changegroup(nodes, 'serve')
961 961 while 1:
962 962 chunk = f.read(4096)
963 963 if not chunk:
964 964 break
965 965 req.write(z.compress(chunk))
966 966
967 967 req.write(z.flush())
968 968
969 969 def do_changegroupsubset(self, req):
970 970 req.httphdr("application/mercurial-0.1")
971 971 bases = []
972 972 heads = []
973 973 if not self.allowpull:
974 974 return
975 975
976 976 if req.form.has_key('bases'):
977 977 bases = [bin(x) for x in req.form['bases'][0].split(' ')]
978 978 if req.form.has_key('heads'):
979 979 heads = [bin(x) for x in req.form['heads'][0].split(' ')]
980 980
981 981 z = zlib.compressobj()
982 982 f = self.repo.changegroupsubset(bases, heads, 'serve')
983 983 while 1:
984 984 chunk = f.read(4096)
985 985 if not chunk:
986 986 break
987 987 req.write(z.compress(chunk))
988 988
989 989 req.write(z.flush())
990 990
991 991 def do_archive(self, req):
992 992 changeset = self.repo.lookup(req.form['node'][0])
993 993 type_ = req.form['type'][0]
994 994 allowed = self.configlist("web", "allow_archive")
995 995 if (type_ in self.archives and (type_ in allowed or
996 996 self.configbool("web", "allow" + type_, False))):
997 997 self.archive(req, changeset, type_)
998 998 return
999 999
1000 1000 req.write(self.t("error"))
1001 1001
1002 1002 def do_static(self, req):
1003 1003 fname = req.form['file'][0]
1004 1004 # a repo owner may set web.static in .hg/hgrc to get any file
1005 1005 # readable by the user running the CGI script
1006 1006 static = self.config("web", "static",
1007 1007 os.path.join(self.templatepath, "static"),
1008 1008 untrusted=False)
1009 1009 req.write(staticfile(static, fname, req)
1010 1010 or self.t("error", error="%r not found" % fname))
1011 1011
1012 1012 def do_capabilities(self, req):
1013 1013 caps = ['lookup', 'changegroupsubset']
1014 1014 if self.configbool('server', 'uncompressed'):
1015 1015 caps.append('stream=%d' % self.repo.revlogversion)
1016 1016 # XXX: make configurable and/or share code with do_unbundle:
1017 1017 unbundleversions = ['HG10GZ', 'HG10BZ', 'HG10UN']
1018 1018 if unbundleversions:
1019 1019 caps.append('unbundle=%s' % ','.join(unbundleversions))
1020 1020 resp = ' '.join(caps)
1021 1021 req.httphdr("application/mercurial-0.1", length=len(resp))
1022 1022 req.write(resp)
1023 1023
1024 1024 def check_perm(self, req, op, default):
1025 1025 '''check permission for operation based on user auth.
1026 1026 return true if op allowed, else false.
1027 1027 default is policy to use if no config given.'''
1028 1028
1029 1029 user = req.env.get('REMOTE_USER')
1030 1030
1031 1031 deny = self.configlist('web', 'deny_' + op)
1032 1032 if deny and (not user or deny == ['*'] or user in deny):
1033 1033 return False
1034 1034
1035 1035 allow = self.configlist('web', 'allow_' + op)
1036 1036 return (allow and (allow == ['*'] or user in allow)) or default
1037 1037
1038 1038 def do_unbundle(self, req):
1039 1039 def bail(response, headers={}):
1040 1040 length = int(req.env['CONTENT_LENGTH'])
1041 1041 for s in util.filechunkiter(req, limit=length):
1042 1042 # drain incoming bundle, else client will not see
1043 1043 # response when run outside cgi script
1044 1044 pass
1045 1045 req.httphdr("application/mercurial-0.1", headers=headers)
1046 1046 req.write('0\n')
1047 1047 req.write(response)
1048 1048
1049 1049 # require ssl by default, auth info cannot be sniffed and
1050 1050 # replayed
1051 1051 ssl_req = self.configbool('web', 'push_ssl', True)
1052 1052 if ssl_req:
1053 1053 if not req.env.get('HTTPS'):
1054 1054 bail(_('ssl required\n'))
1055 1055 return
1056 1056 proto = 'https'
1057 1057 else:
1058 1058 proto = 'http'
1059 1059
1060 1060 # do not allow push unless explicitly allowed
1061 1061 if not self.check_perm(req, 'push', False):
1062 1062 bail(_('push not authorized\n'),
1063 1063 headers={'status': '401 Unauthorized'})
1064 1064 return
1065 1065
1066 1066 req.httphdr("application/mercurial-0.1")
1067 1067
1068 1068 their_heads = req.form['heads'][0].split(' ')
1069 1069
1070 1070 def check_heads():
1071 1071 heads = map(hex, self.repo.heads())
1072 1072 return their_heads == [hex('force')] or their_heads == heads
1073 1073
1074 1074 # fail early if possible
1075 1075 if not check_heads():
1076 1076 bail(_('unsynced changes\n'))
1077 1077 return
1078 1078
1079 1079 # do not lock repo until all changegroup data is
1080 1080 # streamed. save to temporary file.
1081 1081
1082 1082 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
1083 1083 fp = os.fdopen(fd, 'wb+')
1084 1084 try:
1085 1085 length = int(req.env['CONTENT_LENGTH'])
1086 1086 for s in util.filechunkiter(req, limit=length):
1087 1087 fp.write(s)
1088 1088
1089 1089 lock = self.repo.lock()
1090 1090 try:
1091 1091 if not check_heads():
1092 1092 req.write('0\n')
1093 1093 req.write(_('unsynced changes\n'))
1094 1094 return
1095 1095
1096 1096 fp.seek(0)
1097 1097 header = fp.read(6)
1098 1098 if not header.startswith("HG"):
1099 1099 # old client with uncompressed bundle
1100 1100 def generator(f):
1101 1101 yield header
1102 1102 for chunk in f:
1103 1103 yield chunk
1104 1104 elif not header.startswith("HG10"):
1105 1105 req.write("0\n")
1106 1106 req.write(_("unknown bundle version\n"))
1107 1107 return
1108 1108 elif header == "HG10GZ":
1109 1109 def generator(f):
1110 1110 zd = zlib.decompressobj()
1111 1111 for chunk in f:
1112 1112 yield zd.decompress(chunk)
1113 1113 elif header == "HG10BZ":
1114 1114 def generator(f):
1115 1115 zd = bz2.BZ2Decompressor()
1116 1116 zd.decompress("BZ")
1117 1117 for chunk in f:
1118 1118 yield zd.decompress(chunk)
1119 1119 elif header == "HG10UN":
1120 1120 def generator(f):
1121 1121 for chunk in f:
1122 1122 yield chunk
1123 1123 else:
1124 1124 req.write("0\n")
1125 1125 req.write(_("unknown bundle compression type\n"))
1126 1126 return
1127 1127 gen = generator(util.filechunkiter(fp, 4096))
1128 1128
1129 1129 # send addchangegroup output to client
1130 1130
1131 1131 old_stdout = sys.stdout
1132 1132 sys.stdout = cStringIO.StringIO()
1133 1133
1134 1134 try:
1135 1135 url = 'remote:%s:%s' % (proto,
1136 1136 req.env.get('REMOTE_HOST', ''))
1137 ret = self.repo.addchangegroup(util.chunkbuffer(gen),
1138 'serve', url)
1137 try:
1138 ret = self.repo.addchangegroup(util.chunkbuffer(gen),
1139 'serve', url)
1140 except util.Abort, inst:
1141 sys.stdout.write("abort: %s\n" % inst)
1142 ret = 0
1139 1143 finally:
1140 1144 val = sys.stdout.getvalue()
1141 1145 sys.stdout = old_stdout
1142 1146 req.write('%d\n' % ret)
1143 1147 req.write(val)
1144 1148 finally:
1145 1149 lock.release()
1146 1150 finally:
1147 1151 fp.close()
1148 1152 os.unlink(tempname)
1149 1153
1150 1154 def do_stream_out(self, req):
1151 1155 req.httphdr("application/mercurial-0.1")
1152 1156 streamclone.stream_out(self.repo, req)
@@ -1,246 +1,246 b''
1 1 # hgweb/server.py - The standalone hg web server.
2 2 #
3 3 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
4 4 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
5 5 #
6 6 # This software may be used and distributed according to the terms
7 7 # of the GNU General Public License, incorporated herein by reference.
8 8
9 9 import os, sys, errno, urllib, BaseHTTPServer, socket, SocketServer, traceback
10 10 from mercurial import ui, hg, util, templater
11 11 from hgweb_mod import hgweb
12 12 from hgwebdir_mod import hgwebdir
13 13 from request import wsgiapplication
14 14 from mercurial.i18n import gettext as _
15 15
16 16 def _splitURI(uri):
17 17 """ Return path and query splited from uri
18 18
19 19 Just like CGI environment, the path is unquoted, the query is
20 20 not.
21 21 """
22 22 if '?' in uri:
23 23 path, query = uri.split('?', 1)
24 24 else:
25 25 path, query = uri, ''
26 26 return urllib.unquote(path), query
27 27
28 28 class _error_logger(object):
29 29 def __init__(self, handler):
30 30 self.handler = handler
31 31 def flush(self):
32 32 pass
33 33 def write(self, str):
34 34 self.writelines(str.split('\n'))
35 35 def writelines(self, seq):
36 36 for msg in seq:
37 37 self.handler.log_error("HG error: %s", msg)
38 38
39 39 class _hgwebhandler(object, BaseHTTPServer.BaseHTTPRequestHandler):
40 40 def __init__(self, *args, **kargs):
41 41 self.protocol_version = 'HTTP/1.1'
42 42 BaseHTTPServer.BaseHTTPRequestHandler.__init__(self, *args, **kargs)
43 43
44 44 def log_error(self, format, *args):
45 45 errorlog = self.server.errorlog
46 46 errorlog.write("%s - - [%s] %s\n" % (self.address_string(),
47 47 self.log_date_time_string(),
48 48 format % args))
49 49
50 50 def log_message(self, format, *args):
51 51 accesslog = self.server.accesslog
52 52 accesslog.write("%s - - [%s] %s\n" % (self.address_string(),
53 53 self.log_date_time_string(),
54 54 format % args))
55 55
56 56 def do_POST(self):
57 57 try:
58 58 try:
59 59 self.do_hgweb()
60 60 except socket.error, inst:
61 61 if inst[0] != errno.EPIPE:
62 62 raise
63 63 except StandardError, inst:
64 64 self._start_response("500 Internal Server Error", [])
65 65 self._write("Internal Server Error")
66 66 tb = "".join(traceback.format_exception(*sys.exc_info()))
67 67 self.log_error("Exception happened during processing request '%s':\n%s",
68 68 self.path, tb)
69 69
70 70 def do_GET(self):
71 71 self.do_POST()
72 72
73 73 def do_hgweb(self):
74 74 path_info, query = _splitURI(self.path)
75 75
76 76 env = {}
77 77 env['GATEWAY_INTERFACE'] = 'CGI/1.1'
78 78 env['REQUEST_METHOD'] = self.command
79 79 env['SERVER_NAME'] = self.server.server_name
80 80 env['SERVER_PORT'] = str(self.server.server_port)
81 81 env['REQUEST_URI'] = self.path
82 82 env['PATH_INFO'] = path_info
83 83 if query:
84 84 env['QUERY_STRING'] = query
85 85 host = self.address_string()
86 86 if host != self.client_address[0]:
87 87 env['REMOTE_HOST'] = host
88 88 env['REMOTE_ADDR'] = self.client_address[0]
89 89
90 90 if self.headers.typeheader is None:
91 91 env['CONTENT_TYPE'] = self.headers.type
92 92 else:
93 93 env['CONTENT_TYPE'] = self.headers.typeheader
94 94 length = self.headers.getheader('content-length')
95 95 if length:
96 96 env['CONTENT_LENGTH'] = length
97 97 for header in [h for h in self.headers.keys() \
98 98 if h not in ('content-type', 'content-length')]:
99 99 hkey = 'HTTP_' + header.replace('-', '_').upper()
100 100 hval = self.headers.getheader(header)
101 101 hval = hval.replace('\n', '').strip()
102 102 if hval:
103 103 env[hkey] = hval
104 104 env['SERVER_PROTOCOL'] = self.request_version
105 105 env['wsgi.version'] = (1, 0)
106 106 env['wsgi.url_scheme'] = 'http'
107 107 env['wsgi.input'] = self.rfile
108 108 env['wsgi.errors'] = _error_logger(self)
109 109 env['wsgi.multithread'] = isinstance(self.server,
110 110 SocketServer.ThreadingMixIn)
111 111 env['wsgi.multiprocess'] = isinstance(self.server,
112 112 SocketServer.ForkingMixIn)
113 113 env['wsgi.run_once'] = 0
114 114
115 115 self.close_connection = True
116 116 self.saved_status = None
117 117 self.saved_headers = []
118 118 self.sent_headers = False
119 119 self.length = None
120 120 req = self.server.reqmaker(env, self._start_response)
121 121 for data in req:
122 122 if data:
123 123 self._write(data)
124 124
125 125 def send_headers(self):
126 126 if not self.saved_status:
127 127 raise AssertionError("Sending headers before start_response() called")
128 128 saved_status = self.saved_status.split(None, 1)
129 129 saved_status[0] = int(saved_status[0])
130 130 self.send_response(*saved_status)
131 131 should_close = True
132 132 for h in self.saved_headers:
133 133 self.send_header(*h)
134 134 if h[0].lower() == 'content-length':
135 135 should_close = False
136 136 self.length = int(h[1])
137 137 # The value of the Connection header is a list of case-insensitive
138 138 # tokens separated by commas and optional whitespace.
139 139 if 'close' in [token.strip().lower() for token in
140 140 self.headers.get('connection', '').split(',')]:
141 141 should_close = True
142 142 if should_close:
143 143 self.send_header('Connection', 'close')
144 144 self.close_connection = should_close
145 145 self.end_headers()
146 146 self.sent_headers = True
147 147
148 148 def _start_response(self, http_status, headers, exc_info=None):
149 149 code, msg = http_status.split(None, 1)
150 150 code = int(code)
151 151 self.saved_status = http_status
152 152 bad_headers = ('connection', 'transfer-encoding')
153 153 self.saved_headers = [ h for h in headers \
154 154 if h[0].lower() not in bad_headers ]
155 155 return self._write
156 156
157 157 def _write(self, data):
158 158 if not self.saved_status:
159 159 raise AssertionError("data written before start_response() called")
160 160 elif not self.sent_headers:
161 161 self.send_headers()
162 162 if self.length is not None:
163 163 if len(data) > self.length:
164 164 raise AssertionError("Content-length header sent, but more bytes than specified are being written.")
165 165 self.length = self.length - len(data)
166 166 self.wfile.write(data)
167 167 self.wfile.flush()
168 168
169 169 def create_server(ui, repo):
170 170 use_threads = True
171 171
172 172 def openlog(opt, default):
173 173 if opt and opt != '-':
174 174 return open(opt, 'w')
175 175 return default
176 176
177 177 address = ui.config("web", "address", "")
178 178 port = int(ui.config("web", "port", 8000))
179 179 use_ipv6 = ui.configbool("web", "ipv6")
180 180 webdir_conf = ui.config("web", "webdir_conf")
181 181 accesslog = openlog(ui.config("web", "accesslog", "-"), sys.stdout)
182 182 errorlog = openlog(ui.config("web", "errorlog", "-"), sys.stderr)
183 183
184 184 if use_threads:
185 185 try:
186 186 from threading import activeCount
187 187 except ImportError:
188 188 use_threads = False
189 189
190 190 if use_threads:
191 191 _mixin = SocketServer.ThreadingMixIn
192 192 else:
193 193 if hasattr(os, "fork"):
194 194 _mixin = SocketServer.ForkingMixIn
195 195 else:
196 196 class _mixin:
197 197 pass
198 198
199 199 class MercurialHTTPServer(object, _mixin, BaseHTTPServer.HTTPServer):
200 200 def __init__(self, *args, **kargs):
201 201 BaseHTTPServer.HTTPServer.__init__(self, *args, **kargs)
202 202 self.accesslog = accesslog
203 203 self.errorlog = errorlog
204 204 self.repo = repo
205 205 self.webdir_conf = webdir_conf
206 206 self.webdirmaker = hgwebdir
207 207 self.repoviewmaker = hgweb
208 208 self.reqmaker = wsgiapplication(self.make_handler)
209 209 self.daemon_threads = True
210 210
211 211 addr, port = self.socket.getsockname()[:2]
212 212 if addr in ('0.0.0.0', '::'):
213 213 addr = socket.gethostname()
214 214 else:
215 215 try:
216 216 addr = socket.gethostbyaddr(addr)[0]
217 217 except socket.error:
218 218 pass
219 219 self.addr, self.port = addr, port
220 220
221 221 def make_handler(self):
222 222 if self.webdir_conf:
223 223 hgwebobj = self.webdirmaker(self.webdir_conf, ui)
224 224 elif self.repo is not None:
225 hgwebobj = self.repoviewmaker(repo.__class__(repo.ui,
226 repo.origroot))
225 hgwebobj = self.repoviewmaker(hg.repository(repo.ui,
226 repo.root))
227 227 else:
228 228 raise hg.RepoError(_("There is no Mercurial repository here"
229 229 " (.hg not found)"))
230 230 return hgwebobj
231 231
232 232 class IPv6HTTPServer(MercurialHTTPServer):
233 233 address_family = getattr(socket, 'AF_INET6', None)
234 234
235 235 def __init__(self, *args, **kwargs):
236 236 if self.address_family is None:
237 237 raise hg.RepoError(_('IPv6 not available on this system'))
238 238 super(IPv6HTTPServer, self).__init__(*args, **kwargs)
239 239
240 240 try:
241 241 if use_ipv6:
242 242 return IPv6HTTPServer((address, port), _hgwebhandler)
243 243 else:
244 244 return MercurialHTTPServer((address, port), _hgwebhandler)
245 245 except socket.error, inst:
246 246 raise util.Abort(_('cannot start server: %s') % inst.args[1])
@@ -1,67 +1,70 b''
1 1 # mail.py - mail sending bits for mercurial
2 2 #
3 3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from i18n import _
9 import os, smtplib, templater, util
9 import os, smtplib, templater, util, socket
10 10
11 11 def _smtp(ui):
12 12 '''send mail using smtp.'''
13 13
14 14 local_hostname = ui.config('smtp', 'local_hostname')
15 15 s = smtplib.SMTP(local_hostname=local_hostname)
16 16 mailhost = ui.config('smtp', 'host')
17 17 if not mailhost:
18 18 raise util.Abort(_('no [smtp]host in hgrc - cannot send mail'))
19 19 mailport = int(ui.config('smtp', 'port', 25))
20 20 ui.note(_('sending mail: smtp host %s, port %s\n') %
21 21 (mailhost, mailport))
22 22 s.connect(host=mailhost, port=mailport)
23 23 if ui.configbool('smtp', 'tls'):
24 if not hasattr(socket, 'ssl'):
25 raise util.Abort(_("can't use TLS: Python SSL support "
26 "not installed"))
24 27 ui.note(_('(using tls)\n'))
25 28 s.ehlo()
26 29 s.starttls()
27 30 s.ehlo()
28 31 username = ui.config('smtp', 'username')
29 32 password = ui.config('smtp', 'password')
30 33 if username and password:
31 34 ui.note(_('(authenticating to mail server as %s)\n') %
32 35 (username))
33 36 s.login(username, password)
34 37 return s
35 38
36 39 class _sendmail(object):
37 40 '''send mail using sendmail.'''
38 41
39 42 def __init__(self, ui, program):
40 43 self.ui = ui
41 44 self.program = program
42 45
43 46 def sendmail(self, sender, recipients, msg):
44 47 cmdline = '%s -f %s %s' % (
45 48 self.program, templater.email(sender),
46 49 ' '.join(map(templater.email, recipients)))
47 50 self.ui.note(_('sending mail: %s\n') % cmdline)
48 51 fp = os.popen(cmdline, 'w')
49 52 fp.write(msg)
50 53 ret = fp.close()
51 54 if ret:
52 55 raise util.Abort('%s %s' % (
53 56 os.path.basename(self.program.split(None, 1)[0]),
54 57 util.explain_exit(ret)[0]))
55 58
56 59 def connect(ui):
57 60 '''make a mail connection. object returned has one method, sendmail.
58 61 call as sendmail(sender, list-of-recipients, msg).'''
59 62
60 63 method = ui.config('email', 'method', 'smtp')
61 64 if method == 'smtp':
62 65 return _smtp(ui)
63 66
64 67 return _sendmail(ui, method)
65 68
66 69 def sendmail(ui, sender, recipients, msg):
67 70 return connect(ui).sendmail(sender, recipients, msg)
@@ -1,648 +1,650 b''
1 1 # patch.py - patch file parsing routines
2 2 #
3 3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 from i18n import _
9 9 from node import *
10 10 import base85, cmdutil, mdiff, util, context, revlog
11 11 import cStringIO, email.Parser, os, popen2, re, sha
12 12 import sys, tempfile, zlib
13 13
14 14 # helper functions
15 15
16 16 def copyfile(src, dst, basedir=None):
17 17 if not basedir:
18 18 basedir = os.getcwd()
19 19
20 20 abssrc, absdst = [os.path.join(basedir, n) for n in (src, dst)]
21 21 if os.path.exists(absdst):
22 22 raise util.Abort(_("cannot create %s: destination already exists") %
23 23 dst)
24 24
25 25 targetdir = os.path.dirname(absdst)
26 26 if not os.path.isdir(targetdir):
27 27 os.makedirs(targetdir)
28 28
29 29 util.copyfile(abssrc, absdst)
30 30
31 31 # public functions
32 32
33 33 def extract(ui, fileobj):
34 34 '''extract patch from data read from fileobj.
35 35
36 36 patch can be normal patch or contained in email message.
37 37
38 38 return tuple (filename, message, user, date). any item in returned
39 39 tuple can be None. if filename is None, fileobj did not contain
40 40 patch. caller must unlink filename when done.'''
41 41
42 42 # attempt to detect the start of a patch
43 43 # (this heuristic is borrowed from quilt)
44 44 diffre = re.compile(r'^(?:Index:[ \t]|diff[ \t]|RCS file: |' +
45 45 'retrieving revision [0-9]+(\.[0-9]+)*$|' +
46 46 '(---|\*\*\*)[ \t])', re.MULTILINE)
47 47
48 48 fd, tmpname = tempfile.mkstemp(prefix='hg-patch-')
49 49 tmpfp = os.fdopen(fd, 'w')
50 50 try:
51 51 hgpatch = False
52 52
53 53 msg = email.Parser.Parser().parse(fileobj)
54 54
55 55 message = msg['Subject']
56 56 user = msg['From']
57 57 # should try to parse msg['Date']
58 58 date = None
59 59
60 60 if message:
61 61 message = message.replace('\n\t', ' ')
62 62 ui.debug('Subject: %s\n' % message)
63 63 if user:
64 64 ui.debug('From: %s\n' % user)
65 65 diffs_seen = 0
66 66 ok_types = ('text/plain', 'text/x-diff', 'text/x-patch')
67 67
68 68 for part in msg.walk():
69 69 content_type = part.get_content_type()
70 70 ui.debug('Content-Type: %s\n' % content_type)
71 71 if content_type not in ok_types:
72 72 continue
73 73 payload = part.get_payload(decode=True)
74 74 m = diffre.search(payload)
75 75 if m:
76 76 ui.debug(_('found patch at byte %d\n') % m.start(0))
77 77 diffs_seen += 1
78 78 cfp = cStringIO.StringIO()
79 79 if message:
80 80 cfp.write(message)
81 81 cfp.write('\n')
82 82 for line in payload[:m.start(0)].splitlines():
83 83 if line.startswith('# HG changeset patch'):
84 84 ui.debug(_('patch generated by hg export\n'))
85 85 hgpatch = True
86 86 # drop earlier commit message content
87 87 cfp.seek(0)
88 88 cfp.truncate()
89 89 elif hgpatch:
90 90 if line.startswith('# User '):
91 91 user = line[7:]
92 92 ui.debug('From: %s\n' % user)
93 93 elif line.startswith("# Date "):
94 94 date = line[7:]
95 95 if not line.startswith('# '):
96 96 cfp.write(line)
97 97 cfp.write('\n')
98 98 message = cfp.getvalue()
99 99 if tmpfp:
100 100 tmpfp.write(payload)
101 101 if not payload.endswith('\n'):
102 102 tmpfp.write('\n')
103 103 elif not diffs_seen and message and content_type == 'text/plain':
104 104 message += '\n' + payload
105 105 except:
106 106 tmpfp.close()
107 107 os.unlink(tmpname)
108 108 raise
109 109
110 110 tmpfp.close()
111 111 if not diffs_seen:
112 112 os.unlink(tmpname)
113 113 return None, message, user, date
114 114 return tmpname, message, user, date
115 115
116 116 GP_PATCH = 1 << 0 # we have to run patch
117 117 GP_FILTER = 1 << 1 # there's some copy/rename operation
118 118 GP_BINARY = 1 << 2 # there's a binary patch
119 119
120 120 def readgitpatch(patchname):
121 121 """extract git-style metadata about patches from <patchname>"""
122 122 class gitpatch:
123 123 "op is one of ADD, DELETE, RENAME, MODIFY or COPY"
124 124 def __init__(self, path):
125 125 self.path = path
126 126 self.oldpath = None
127 127 self.mode = None
128 128 self.op = 'MODIFY'
129 129 self.copymod = False
130 130 self.lineno = 0
131 131 self.binary = False
132 132
133 133 # Filter patch for git information
134 134 gitre = re.compile('diff --git a/(.*) b/(.*)')
135 135 pf = file(patchname)
136 136 gp = None
137 137 gitpatches = []
138 138 # Can have a git patch with only metadata, causing patch to complain
139 139 dopatch = 0
140 140
141 141 lineno = 0
142 142 for line in pf:
143 143 lineno += 1
144 144 if line.startswith('diff --git'):
145 145 m = gitre.match(line)
146 146 if m:
147 147 if gp:
148 148 gitpatches.append(gp)
149 149 src, dst = m.group(1, 2)
150 150 gp = gitpatch(dst)
151 151 gp.lineno = lineno
152 152 elif gp:
153 153 if line.startswith('--- '):
154 154 if gp.op in ('COPY', 'RENAME'):
155 155 gp.copymod = True
156 156 dopatch |= GP_FILTER
157 157 gitpatches.append(gp)
158 158 gp = None
159 159 dopatch |= GP_PATCH
160 160 continue
161 161 if line.startswith('rename from '):
162 162 gp.op = 'RENAME'
163 163 gp.oldpath = line[12:].rstrip()
164 164 elif line.startswith('rename to '):
165 165 gp.path = line[10:].rstrip()
166 166 elif line.startswith('copy from '):
167 167 gp.op = 'COPY'
168 168 gp.oldpath = line[10:].rstrip()
169 169 elif line.startswith('copy to '):
170 170 gp.path = line[8:].rstrip()
171 171 elif line.startswith('deleted file'):
172 172 gp.op = 'DELETE'
173 173 elif line.startswith('new file mode '):
174 174 gp.op = 'ADD'
175 175 gp.mode = int(line.rstrip()[-3:], 8)
176 176 elif line.startswith('new mode '):
177 177 gp.mode = int(line.rstrip()[-3:], 8)
178 178 elif line.startswith('GIT binary patch'):
179 179 dopatch |= GP_BINARY
180 180 gp.binary = True
181 181 if gp:
182 182 gitpatches.append(gp)
183 183
184 184 if not gitpatches:
185 185 dopatch = GP_PATCH
186 186
187 187 return (dopatch, gitpatches)
188 188
189 189 def dogitpatch(patchname, gitpatches, cwd=None):
190 190 """Preprocess git patch so that vanilla patch can handle it"""
191 191 def extractbin(fp):
192 192 i = [0] # yuck
193 193 def readline():
194 194 i[0] += 1
195 195 return fp.readline().rstrip()
196 196 line = readline()
197 197 while line and not line.startswith('literal '):
198 198 line = readline()
199 199 if not line:
200 200 return None, i[0]
201 201 size = int(line[8:])
202 202 dec = []
203 203 line = readline()
204 204 while line:
205 205 l = line[0]
206 206 if l <= 'Z' and l >= 'A':
207 207 l = ord(l) - ord('A') + 1
208 208 else:
209 209 l = ord(l) - ord('a') + 27
210 210 dec.append(base85.b85decode(line[1:])[:l])
211 211 line = readline()
212 212 text = zlib.decompress(''.join(dec))
213 213 if len(text) != size:
214 214 raise util.Abort(_('binary patch is %d bytes, not %d') %
215 215 (len(text), size))
216 216 return text, i[0]
217 217
218 218 pf = file(patchname)
219 219 pfline = 1
220 220
221 221 fd, patchname = tempfile.mkstemp(prefix='hg-patch-')
222 222 tmpfp = os.fdopen(fd, 'w')
223 223
224 224 try:
225 225 for i in xrange(len(gitpatches)):
226 226 p = gitpatches[i]
227 227 if not p.copymod and not p.binary:
228 228 continue
229 229
230 230 # rewrite patch hunk
231 231 while pfline < p.lineno:
232 232 tmpfp.write(pf.readline())
233 233 pfline += 1
234 234
235 235 if p.binary:
236 236 text, delta = extractbin(pf)
237 237 if not text:
238 238 raise util.Abort(_('binary patch extraction failed'))
239 239 pfline += delta
240 240 if not cwd:
241 241 cwd = os.getcwd()
242 242 absdst = os.path.join(cwd, p.path)
243 243 basedir = os.path.dirname(absdst)
244 244 if not os.path.isdir(basedir):
245 245 os.makedirs(basedir)
246 246 out = file(absdst, 'wb')
247 247 out.write(text)
248 248 out.close()
249 249 elif p.copymod:
250 250 copyfile(p.oldpath, p.path, basedir=cwd)
251 251 tmpfp.write('diff --git a/%s b/%s\n' % (p.path, p.path))
252 252 line = pf.readline()
253 253 pfline += 1
254 254 while not line.startswith('--- a/'):
255 255 tmpfp.write(line)
256 256 line = pf.readline()
257 257 pfline += 1
258 258 tmpfp.write('--- a/%s\n' % p.path)
259 259
260 260 line = pf.readline()
261 261 while line:
262 262 tmpfp.write(line)
263 263 line = pf.readline()
264 264 except:
265 265 tmpfp.close()
266 266 os.unlink(patchname)
267 267 raise
268 268
269 269 tmpfp.close()
270 270 return patchname
271 271
272 272 def patch(patchname, ui, strip=1, cwd=None, files={}):
273 273 """apply the patch <patchname> to the working directory.
274 274 a list of patched files is returned"""
275 275
276 276 # helper function
277 277 def __patch(patchname):
278 278 """patch and updates the files and fuzz variables"""
279 279 fuzz = False
280 280
281 281 patcher = util.find_in_path('gpatch', os.environ.get('PATH', ''),
282 282 'patch')
283 283 args = []
284 284 if cwd:
285 285 args.append('-d %s' % util.shellquote(cwd))
286 286 fp = os.popen('%s %s -p%d < %s' % (patcher, ' '.join(args), strip,
287 287 util.shellquote(patchname)))
288 288
289 289 for line in fp:
290 290 line = line.rstrip()
291 291 ui.note(line + '\n')
292 292 if line.startswith('patching file '):
293 293 pf = util.parse_patch_output(line)
294 294 printed_file = False
295 295 files.setdefault(pf, (None, None))
296 296 elif line.find('with fuzz') >= 0:
297 297 fuzz = True
298 298 if not printed_file:
299 299 ui.warn(pf + '\n')
300 300 printed_file = True
301 301 ui.warn(line + '\n')
302 302 elif line.find('saving rejects to file') >= 0:
303 303 ui.warn(line + '\n')
304 304 elif line.find('FAILED') >= 0:
305 305 if not printed_file:
306 306 ui.warn(pf + '\n')
307 307 printed_file = True
308 308 ui.warn(line + '\n')
309 309 code = fp.close()
310 310 if code:
311 311 raise util.Abort(_("patch command failed: %s") %
312 312 util.explain_exit(code)[0])
313 313 return fuzz
314 314
315 315 (dopatch, gitpatches) = readgitpatch(patchname)
316 316 for gp in gitpatches:
317 317 files[gp.path] = (gp.op, gp)
318 318
319 319 fuzz = False
320 320 if dopatch:
321 321 filterpatch = dopatch & (GP_FILTER | GP_BINARY)
322 322 if filterpatch:
323 323 patchname = dogitpatch(patchname, gitpatches, cwd=cwd)
324 324 try:
325 325 if dopatch & GP_PATCH:
326 326 fuzz = __patch(patchname)
327 327 finally:
328 328 if filterpatch:
329 329 os.unlink(patchname)
330 330
331 331 return fuzz
332 332
333 333 def diffopts(ui, opts={}, untrusted=False):
334 334 def get(key, name=None):
335 335 return (opts.get(key) or
336 336 ui.configbool('diff', name or key, None, untrusted=untrusted))
337 337 return mdiff.diffopts(
338 338 text=opts.get('text'),
339 339 git=get('git'),
340 340 nodates=get('nodates'),
341 341 showfunc=get('show_function', 'showfunc'),
342 342 ignorews=get('ignore_all_space', 'ignorews'),
343 343 ignorewsamount=get('ignore_space_change', 'ignorewsamount'),
344 344 ignoreblanklines=get('ignore_blank_lines', 'ignoreblanklines'))
345 345
346 346 def updatedir(ui, repo, patches, wlock=None):
347 347 '''Update dirstate after patch application according to metadata'''
348 348 if not patches:
349 349 return
350 350 copies = []
351 351 removes = {}
352 352 cfiles = patches.keys()
353 353 cwd = repo.getcwd()
354 354 if cwd:
355 355 cfiles = [util.pathto(cwd, f) for f in patches.keys()]
356 356 for f in patches:
357 357 ctype, gp = patches[f]
358 358 if ctype == 'RENAME':
359 359 copies.append((gp.oldpath, gp.path, gp.copymod))
360 360 removes[gp.oldpath] = 1
361 361 elif ctype == 'COPY':
362 362 copies.append((gp.oldpath, gp.path, gp.copymod))
363 363 elif ctype == 'DELETE':
364 364 removes[gp.path] = 1
365 365 for src, dst, after in copies:
366 366 if not after:
367 367 copyfile(src, dst, repo.root)
368 368 repo.copy(src, dst, wlock=wlock)
369 369 removes = removes.keys()
370 370 if removes:
371 371 removes.sort()
372 372 repo.remove(removes, True, wlock=wlock)
373 373 for f in patches:
374 374 ctype, gp = patches[f]
375 375 if gp and gp.mode:
376 376 x = gp.mode & 0100 != 0
377 377 dst = os.path.join(repo.root, gp.path)
378 378 # patch won't create empty files
379 379 if ctype == 'ADD' and not os.path.exists(dst):
380 380 repo.wwrite(gp.path, '', x and 'x' or '')
381 381 else:
382 382 util.set_exec(dst, x)
383 383 cmdutil.addremove(repo, cfiles, wlock=wlock)
384 384 files = patches.keys()
385 385 files.extend([r for r in removes if r not in files])
386 386 files.sort()
387 387
388 388 return files
389 389
390 390 def b85diff(fp, to, tn):
391 391 '''print base85-encoded binary diff'''
392 392 def gitindex(text):
393 393 if not text:
394 394 return '0' * 40
395 395 l = len(text)
396 396 s = sha.new('blob %d\0' % l)
397 397 s.update(text)
398 398 return s.hexdigest()
399 399
400 400 def fmtline(line):
401 401 l = len(line)
402 402 if l <= 26:
403 403 l = chr(ord('A') + l - 1)
404 404 else:
405 405 l = chr(l - 26 + ord('a') - 1)
406 406 return '%c%s\n' % (l, base85.b85encode(line, True))
407 407
408 408 def chunk(text, csize=52):
409 409 l = len(text)
410 410 i = 0
411 411 while i < l:
412 412 yield text[i:i+csize]
413 413 i += csize
414 414
415 if to == tn:
416 return
415 417 # TODO: deltas
416 418 l = len(tn)
417 419 fp.write('index %s..%s\nGIT binary patch\nliteral %s\n' %
418 420 (gitindex(to), gitindex(tn), len(tn)))
419 421
420 422 tn = ''.join([fmtline(l) for l in chunk(zlib.compress(tn))])
421 423 fp.write(tn)
422 424 fp.write('\n')
423 425
424 426 def diff(repo, node1=None, node2=None, files=None, match=util.always,
425 427 fp=None, changes=None, opts=None):
426 428 '''print diff of changes to files between two nodes, or node and
427 429 working directory.
428 430
429 431 if node1 is None, use first dirstate parent instead.
430 432 if node2 is None, compare node1 with working directory.'''
431 433
432 434 if opts is None:
433 435 opts = mdiff.defaultopts
434 436 if fp is None:
435 437 fp = repo.ui
436 438
437 439 if not node1:
438 440 node1 = repo.dirstate.parents()[0]
439 441
440 442 ccache = {}
441 443 def getctx(r):
442 444 if r not in ccache:
443 445 ccache[r] = context.changectx(repo, r)
444 446 return ccache[r]
445 447
446 448 flcache = {}
447 449 def getfilectx(f, ctx):
448 450 flctx = ctx.filectx(f, filelog=flcache.get(f))
449 451 if f not in flcache:
450 452 flcache[f] = flctx._filelog
451 453 return flctx
452 454
453 455 # reading the data for node1 early allows it to play nicely
454 456 # with repo.status and the revlog cache.
455 457 ctx1 = context.changectx(repo, node1)
456 458 # force manifest reading
457 459 man1 = ctx1.manifest()
458 460 date1 = util.datestr(ctx1.date())
459 461
460 462 if not changes:
461 463 changes = repo.status(node1, node2, files, match=match)[:5]
462 464 modified, added, removed, deleted, unknown = changes
463 465 if files:
464 466 def filterfiles(filters):
465 467 l = [x for x in filters if x in files]
466 468
467 469 for t in files:
468 470 if not t.endswith("/"):
469 471 t += "/"
470 472 l += [x for x in filters if x.startswith(t)]
471 473 return l
472 474
473 475 modified, added, removed = map(filterfiles, (modified, added, removed))
474 476
475 477 if not modified and not added and not removed:
476 478 return
477 479
478 480 if node2:
479 481 ctx2 = context.changectx(repo, node2)
480 482 else:
481 483 ctx2 = context.workingctx(repo)
482 484 man2 = ctx2.manifest()
483 485
484 486 # returns False if there was no rename between ctx1 and ctx2
485 487 # returns None if the file was created between ctx1 and ctx2
486 488 # returns the (file, node) present in ctx1 that was renamed to f in ctx2
487 489 def renamed(f):
488 490 startrev = ctx1.rev()
489 491 c = ctx2
490 492 crev = c.rev()
491 493 if crev is None:
492 494 crev = repo.changelog.count()
493 495 orig = f
494 496 while crev > startrev:
495 497 if f in c.files():
496 498 try:
497 499 src = getfilectx(f, c).renamed()
498 500 except revlog.LookupError:
499 501 return None
500 502 if src:
501 503 f = src[0]
502 504 crev = c.parents()[0].rev()
503 505 # try to reuse
504 506 c = getctx(crev)
505 507 if f not in man1:
506 508 return None
507 509 if f == orig:
508 510 return False
509 511 return f
510 512
511 513 if repo.ui.quiet:
512 514 r = None
513 515 else:
514 516 hexfunc = repo.ui.debugflag and hex or short
515 517 r = [hexfunc(node) for node in [node1, node2] if node]
516 518
517 519 if opts.git:
518 520 copied = {}
519 521 for f in added:
520 522 src = renamed(f)
521 523 if src:
522 524 copied[f] = src
523 525 srcs = [x[1] for x in copied.items()]
524 526
525 527 all = modified + added + removed
526 528 all.sort()
527 529 gone = {}
528 530
529 531 for f in all:
530 532 to = None
531 533 tn = None
532 534 dodiff = True
533 535 header = []
534 536 if f in man1:
535 537 to = getfilectx(f, ctx1).data()
536 538 if f not in removed:
537 539 tn = getfilectx(f, ctx2).data()
538 540 if opts.git:
539 541 def gitmode(x):
540 542 return x and '100755' or '100644'
541 543 def addmodehdr(header, omode, nmode):
542 544 if omode != nmode:
543 545 header.append('old mode %s\n' % omode)
544 546 header.append('new mode %s\n' % nmode)
545 547
546 548 a, b = f, f
547 549 if f in added:
548 550 mode = gitmode(man2.execf(f))
549 551 if f in copied:
550 552 a = copied[f]
551 553 omode = gitmode(man1.execf(a))
552 554 addmodehdr(header, omode, mode)
553 555 if a in removed and a not in gone:
554 556 op = 'rename'
555 557 gone[a] = 1
556 558 else:
557 559 op = 'copy'
558 560 header.append('%s from %s\n' % (op, a))
559 561 header.append('%s to %s\n' % (op, f))
560 562 to = getfilectx(a, ctx1).data()
561 563 else:
562 564 header.append('new file mode %s\n' % mode)
563 if util.binary(tn):
564 dodiff = 'binary'
565 if util.binary(tn):
566 dodiff = 'binary'
565 567 elif f in removed:
566 568 if f in srcs:
567 569 dodiff = False
568 570 else:
569 571 mode = gitmode(man1.execf(f))
570 572 header.append('deleted file mode %s\n' % mode)
571 573 else:
572 574 omode = gitmode(man1.execf(f))
573 575 nmode = gitmode(man2.execf(f))
574 576 addmodehdr(header, omode, nmode)
575 577 if util.binary(to) or util.binary(tn):
576 578 dodiff = 'binary'
577 579 r = None
578 580 header.insert(0, 'diff --git a/%s b/%s\n' % (a, b))
579 581 if dodiff == 'binary':
580 582 fp.write(''.join(header))
581 583 b85diff(fp, to, tn)
582 584 elif dodiff:
583 585 text = mdiff.unidiff(to, date1,
584 586 # ctx2 date may be dynamic
585 587 tn, util.datestr(ctx2.date()),
586 588 f, r, opts=opts)
587 589 if text or len(header) > 1:
588 590 fp.write(''.join(header))
589 591 fp.write(text)
590 592
591 593 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
592 594 opts=None):
593 595 '''export changesets as hg patches.'''
594 596
595 597 total = len(revs)
596 598 revwidth = max([len(str(rev)) for rev in revs])
597 599
598 600 def single(rev, seqno, fp):
599 601 ctx = repo.changectx(rev)
600 602 node = ctx.node()
601 603 parents = [p.node() for p in ctx.parents() if p]
602 604 if switch_parent:
603 605 parents.reverse()
604 606 prev = (parents and parents[0]) or nullid
605 607
606 608 if not fp:
607 609 fp = cmdutil.make_file(repo, template, node, total=total,
608 610 seqno=seqno, revwidth=revwidth)
609 611 if fp not in (sys.stdout, repo.ui):
610 612 repo.ui.note("%s\n" % fp.name)
611 613
612 614 fp.write("# HG changeset patch\n")
613 615 fp.write("# User %s\n" % ctx.user())
614 616 fp.write("# Date %d %d\n" % ctx.date())
615 617 fp.write("# Node ID %s\n" % hex(node))
616 618 fp.write("# Parent %s\n" % hex(prev))
617 619 if len(parents) > 1:
618 620 fp.write("# Parent %s\n" % hex(parents[1]))
619 621 fp.write(ctx.description().rstrip())
620 622 fp.write("\n\n")
621 623
622 624 diff(repo, prev, node, fp=fp, opts=opts)
623 625 if fp not in (sys.stdout, repo.ui):
624 626 fp.close()
625 627
626 628 for seqno, rev in enumerate(revs):
627 629 single(rev, seqno+1, fp)
628 630
629 631 def diffstat(patchlines):
630 632 fd, name = tempfile.mkstemp(prefix="hg-patchbomb-", suffix=".txt")
631 633 try:
632 634 p = popen2.Popen3('diffstat -p1 -w79 2>/dev/null > ' + name)
633 635 try:
634 636 for line in patchlines: print >> p.tochild, line
635 637 p.tochild.close()
636 638 if p.wait(): return
637 639 fp = os.fdopen(fd, 'r')
638 640 stat = []
639 641 for line in fp: stat.append(line.lstrip())
640 642 last = stat.pop()
641 643 stat.insert(0, last)
642 644 stat = ''.join(stat)
643 645 if stat.startswith('0 files'): raise ValueError
644 646 return stat
645 647 except: raise
646 648 finally:
647 649 try: os.unlink(name)
648 650 except: pass
@@ -1,1425 +1,1443 b''
1 1 """
2 2 util.py - Mercurial utility functions and platform specfic implementations
3 3
4 4 Copyright 2005 K. Thananchayan <thananck@yahoo.com>
5 5 Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
6 6 Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
7 7
8 8 This software may be used and distributed according to the terms
9 9 of the GNU General Public License, incorporated herein by reference.
10 10
11 11 This contains helper routines that are independent of the SCM core and hide
12 12 platform-specific details from the core.
13 13 """
14 14
15 15 from i18n import _
16 16 import cStringIO, errno, getpass, popen2, re, shutil, sys, tempfile
17 17 import os, threading, time, calendar, ConfigParser, locale, glob
18 18
19 19 try:
20 20 _encoding = os.environ.get("HGENCODING") or locale.getpreferredencoding() \
21 21 or "ascii"
22 22 except locale.Error:
23 23 _encoding = 'ascii'
24 24 _encodingmode = os.environ.get("HGENCODINGMODE", "strict")
25 25 _fallbackencoding = 'ISO-8859-1'
26 26
27 27 def tolocal(s):
28 28 """
29 29 Convert a string from internal UTF-8 to local encoding
30 30
31 31 All internal strings should be UTF-8 but some repos before the
32 32 implementation of locale support may contain latin1 or possibly
33 33 other character sets. We attempt to decode everything strictly
34 34 using UTF-8, then Latin-1, and failing that, we use UTF-8 and
35 35 replace unknown characters.
36 36 """
37 37 for e in ('UTF-8', _fallbackencoding):
38 38 try:
39 39 u = s.decode(e) # attempt strict decoding
40 40 return u.encode(_encoding, "replace")
41 41 except LookupError, k:
42 42 raise Abort(_("%s, please check your locale settings") % k)
43 43 except UnicodeDecodeError:
44 44 pass
45 45 u = s.decode("utf-8", "replace") # last ditch
46 46 return u.encode(_encoding, "replace")
47 47
48 48 def fromlocal(s):
49 49 """
50 50 Convert a string from the local character encoding to UTF-8
51 51
52 52 We attempt to decode strings using the encoding mode set by
53 53 HG_ENCODINGMODE, which defaults to 'strict'. In this mode, unknown
54 54 characters will cause an error message. Other modes include
55 55 'replace', which replaces unknown characters with a special
56 56 Unicode character, and 'ignore', which drops the character.
57 57 """
58 58 try:
59 59 return s.decode(_encoding, _encodingmode).encode("utf-8")
60 60 except UnicodeDecodeError, inst:
61 61 sub = s[max(0, inst.start-10):inst.start+10]
62 62 raise Abort("decoding near '%s': %s!" % (sub, inst))
63 63 except LookupError, k:
64 64 raise Abort(_("%s, please check your locale settings") % k)
65 65
66 66 def locallen(s):
67 67 """Find the length in characters of a local string"""
68 68 return len(s.decode(_encoding, "replace"))
69 69
70 70 def localsub(s, a, b=None):
71 71 try:
72 72 u = s.decode(_encoding, _encodingmode)
73 73 if b is not None:
74 74 u = u[a:b]
75 75 else:
76 76 u = u[:a]
77 77 return u.encode(_encoding, _encodingmode)
78 78 except UnicodeDecodeError, inst:
79 79 sub = s[max(0, inst.start-10), inst.start+10]
80 80 raise Abort(_("decoding near '%s': %s!\n") % (sub, inst))
81 81
82 82 # used by parsedate
83 83 defaultdateformats = (
84 84 '%Y-%m-%d %H:%M:%S',
85 85 '%Y-%m-%d %I:%M:%S%p',
86 86 '%Y-%m-%d %H:%M',
87 87 '%Y-%m-%d %I:%M%p',
88 88 '%Y-%m-%d',
89 89 '%m-%d',
90 90 '%m/%d',
91 91 '%m/%d/%y',
92 92 '%m/%d/%Y',
93 93 '%a %b %d %H:%M:%S %Y',
94 94 '%a %b %d %I:%M:%S%p %Y',
95 95 '%b %d %H:%M:%S %Y',
96 96 '%b %d %I:%M:%S%p %Y',
97 97 '%b %d %H:%M:%S',
98 98 '%b %d %I:%M:%S%p',
99 99 '%b %d %H:%M',
100 100 '%b %d %I:%M%p',
101 101 '%b %d %Y',
102 102 '%b %d',
103 103 '%H:%M:%S',
104 104 '%I:%M:%SP',
105 105 '%H:%M',
106 106 '%I:%M%p',
107 107 )
108 108
109 109 extendeddateformats = defaultdateformats + (
110 110 "%Y",
111 111 "%Y-%m",
112 112 "%b",
113 113 "%b %Y",
114 114 )
115 115
116 116 class SignalInterrupt(Exception):
117 117 """Exception raised on SIGTERM and SIGHUP."""
118 118
119 119 # differences from SafeConfigParser:
120 120 # - case-sensitive keys
121 121 # - allows values that are not strings (this means that you may not
122 122 # be able to save the configuration to a file)
123 123 class configparser(ConfigParser.SafeConfigParser):
124 124 def optionxform(self, optionstr):
125 125 return optionstr
126 126
127 127 def set(self, section, option, value):
128 128 return ConfigParser.ConfigParser.set(self, section, option, value)
129 129
130 130 def _interpolate(self, section, option, rawval, vars):
131 131 if not isinstance(rawval, basestring):
132 132 return rawval
133 133 return ConfigParser.SafeConfigParser._interpolate(self, section,
134 134 option, rawval, vars)
135 135
136 136 def cachefunc(func):
137 137 '''cache the result of function calls'''
138 138 # XXX doesn't handle keywords args
139 139 cache = {}
140 140 if func.func_code.co_argcount == 1:
141 141 # we gain a small amount of time because
142 142 # we don't need to pack/unpack the list
143 143 def f(arg):
144 144 if arg not in cache:
145 145 cache[arg] = func(arg)
146 146 return cache[arg]
147 147 else:
148 148 def f(*args):
149 149 if args not in cache:
150 150 cache[args] = func(*args)
151 151 return cache[args]
152 152
153 153 return f
154 154
155 155 def pipefilter(s, cmd):
156 156 '''filter string S through command CMD, returning its output'''
157 157 (pout, pin) = popen2.popen2(cmd, -1, 'b')
158 158 def writer():
159 159 try:
160 160 pin.write(s)
161 161 pin.close()
162 162 except IOError, inst:
163 163 if inst.errno != errno.EPIPE:
164 164 raise
165 165
166 166 # we should use select instead on UNIX, but this will work on most
167 167 # systems, including Windows
168 168 w = threading.Thread(target=writer)
169 169 w.start()
170 170 f = pout.read()
171 171 pout.close()
172 172 w.join()
173 173 return f
174 174
175 175 def tempfilter(s, cmd):
176 176 '''filter string S through a pair of temporary files with CMD.
177 177 CMD is used as a template to create the real command to be run,
178 178 with the strings INFILE and OUTFILE replaced by the real names of
179 179 the temporary files generated.'''
180 180 inname, outname = None, None
181 181 try:
182 182 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
183 183 fp = os.fdopen(infd, 'wb')
184 184 fp.write(s)
185 185 fp.close()
186 186 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
187 187 os.close(outfd)
188 188 cmd = cmd.replace('INFILE', inname)
189 189 cmd = cmd.replace('OUTFILE', outname)
190 190 code = os.system(cmd)
191 191 if code: raise Abort(_("command '%s' failed: %s") %
192 192 (cmd, explain_exit(code)))
193 193 return open(outname, 'rb').read()
194 194 finally:
195 195 try:
196 196 if inname: os.unlink(inname)
197 197 except: pass
198 198 try:
199 199 if outname: os.unlink(outname)
200 200 except: pass
201 201
202 202 filtertable = {
203 203 'tempfile:': tempfilter,
204 204 'pipe:': pipefilter,
205 205 }
206 206
207 207 def filter(s, cmd):
208 208 "filter a string through a command that transforms its input to its output"
209 209 for name, fn in filtertable.iteritems():
210 210 if cmd.startswith(name):
211 211 return fn(s, cmd[len(name):].lstrip())
212 212 return pipefilter(s, cmd)
213 213
214 214 def find_in_path(name, path, default=None):
215 215 '''find name in search path. path can be string (will be split
216 216 with os.pathsep), or iterable thing that returns strings. if name
217 217 found, return path to name. else return default.'''
218 218 if isinstance(path, str):
219 219 path = path.split(os.pathsep)
220 220 for p in path:
221 221 p_name = os.path.join(p, name)
222 222 if os.path.exists(p_name):
223 223 return p_name
224 224 return default
225 225
226 226 def binary(s):
227 227 """return true if a string is binary data using diff's heuristic"""
228 228 if s and '\0' in s[:4096]:
229 229 return True
230 230 return False
231 231
232 232 def unique(g):
233 233 """return the uniq elements of iterable g"""
234 234 seen = {}
235 235 l = []
236 236 for f in g:
237 237 if f not in seen:
238 238 seen[f] = 1
239 239 l.append(f)
240 240 return l
241 241
242 242 class Abort(Exception):
243 243 """Raised if a command needs to print an error and exit."""
244 244
245 245 class UnexpectedOutput(Abort):
246 246 """Raised to print an error with part of output and exit."""
247 247
248 248 def always(fn): return True
249 249 def never(fn): return False
250 250
251 251 def expand_glob(pats):
252 252 '''On Windows, expand the implicit globs in a list of patterns'''
253 253 if os.name != 'nt':
254 254 return list(pats)
255 255 ret = []
256 256 for p in pats:
257 257 kind, name = patkind(p, None)
258 258 if kind is None:
259 259 globbed = glob.glob(name)
260 260 if globbed:
261 261 ret.extend(globbed)
262 262 continue
263 263 # if we couldn't expand the glob, just keep it around
264 264 ret.append(p)
265 265 return ret
266 266
267 267 def patkind(name, dflt_pat='glob'):
268 268 """Split a string into an optional pattern kind prefix and the
269 269 actual pattern."""
270 270 for prefix in 're', 'glob', 'path', 'relglob', 'relpath', 'relre':
271 271 if name.startswith(prefix + ':'): return name.split(':', 1)
272 272 return dflt_pat, name
273 273
274 274 def globre(pat, head='^', tail='$'):
275 275 "convert a glob pattern into a regexp"
276 276 i, n = 0, len(pat)
277 277 res = ''
278 278 group = False
279 279 def peek(): return i < n and pat[i]
280 280 while i < n:
281 281 c = pat[i]
282 282 i = i+1
283 283 if c == '*':
284 284 if peek() == '*':
285 285 i += 1
286 286 res += '.*'
287 287 else:
288 288 res += '[^/]*'
289 289 elif c == '?':
290 290 res += '.'
291 291 elif c == '[':
292 292 j = i
293 293 if j < n and pat[j] in '!]':
294 294 j += 1
295 295 while j < n and pat[j] != ']':
296 296 j += 1
297 297 if j >= n:
298 298 res += '\\['
299 299 else:
300 300 stuff = pat[i:j].replace('\\','\\\\')
301 301 i = j + 1
302 302 if stuff[0] == '!':
303 303 stuff = '^' + stuff[1:]
304 304 elif stuff[0] == '^':
305 305 stuff = '\\' + stuff
306 306 res = '%s[%s]' % (res, stuff)
307 307 elif c == '{':
308 308 group = True
309 309 res += '(?:'
310 310 elif c == '}' and group:
311 311 res += ')'
312 312 group = False
313 313 elif c == ',' and group:
314 314 res += '|'
315 315 elif c == '\\':
316 316 p = peek()
317 317 if p:
318 318 i += 1
319 319 res += re.escape(p)
320 320 else:
321 321 res += re.escape(c)
322 322 else:
323 323 res += re.escape(c)
324 324 return head + res + tail
325 325
326 326 _globchars = {'[': 1, '{': 1, '*': 1, '?': 1}
327 327
328 328 def pathto(n1, n2):
329 329 '''return the relative path from one place to another.
330 330 n1 should use os.sep to separate directories
331 331 n2 should use "/" to separate directories
332 332 returns an os.sep-separated path.
333 333 '''
334 334 if not n1: return localpath(n2)
335 335 a, b = n1.split(os.sep), n2.split('/')
336 336 a.reverse()
337 337 b.reverse()
338 338 while a and b and a[-1] == b[-1]:
339 339 a.pop()
340 340 b.pop()
341 341 b.reverse()
342 342 return os.sep.join((['..'] * len(a)) + b)
343 343
344 344 def canonpath(root, cwd, myname):
345 345 """return the canonical path of myname, given cwd and root"""
346 346 if root == os.sep:
347 347 rootsep = os.sep
348 348 elif root.endswith(os.sep):
349 349 rootsep = root
350 350 else:
351 351 rootsep = root + os.sep
352 352 name = myname
353 353 if not os.path.isabs(name):
354 354 name = os.path.join(root, cwd, name)
355 355 name = os.path.normpath(name)
356 356 if name != rootsep and name.startswith(rootsep):
357 357 name = name[len(rootsep):]
358 358 audit_path(name)
359 359 return pconvert(name)
360 360 elif name == root:
361 361 return ''
362 362 else:
363 363 # Determine whether `name' is in the hierarchy at or beneath `root',
364 364 # by iterating name=dirname(name) until that causes no change (can't
365 365 # check name == '/', because that doesn't work on windows). For each
366 366 # `name', compare dev/inode numbers. If they match, the list `rel'
367 367 # holds the reversed list of components making up the relative file
368 368 # name we want.
369 369 root_st = os.stat(root)
370 370 rel = []
371 371 while True:
372 372 try:
373 373 name_st = os.stat(name)
374 374 except OSError:
375 375 break
376 376 if samestat(name_st, root_st):
377 if not rel:
378 # name was actually the same as root (maybe a symlink)
379 return ''
377 380 rel.reverse()
378 381 name = os.path.join(*rel)
379 382 audit_path(name)
380 383 return pconvert(name)
381 384 dirname, basename = os.path.split(name)
382 385 rel.append(basename)
383 386 if dirname == name:
384 387 break
385 388 name = dirname
386 389
387 390 raise Abort('%s not under root' % myname)
388 391
389 392 def matcher(canonroot, cwd='', names=['.'], inc=[], exc=[], head='', src=None):
390 393 return _matcher(canonroot, cwd, names, inc, exc, head, 'glob', src)
391 394
392 395 def cmdmatcher(canonroot, cwd='', names=['.'], inc=[], exc=[], head='',
393 396 src=None, globbed=False):
394 397 if not globbed:
395 398 names = expand_glob(names)
396 399 return _matcher(canonroot, cwd, names, inc, exc, head, 'relpath', src)
397 400
398 401 def _matcher(canonroot, cwd, names, inc, exc, head, dflt_pat, src):
399 402 """build a function to match a set of file patterns
400 403
401 404 arguments:
402 405 canonroot - the canonical root of the tree you're matching against
403 406 cwd - the current working directory, if relevant
404 407 names - patterns to find
405 408 inc - patterns to include
406 409 exc - patterns to exclude
407 410 head - a regex to prepend to patterns to control whether a match is rooted
408 411
409 412 a pattern is one of:
410 413 'glob:<rooted glob>'
411 414 're:<rooted regexp>'
412 415 'path:<rooted path>'
413 416 'relglob:<relative glob>'
414 417 'relpath:<relative path>'
415 418 'relre:<relative regexp>'
416 419 '<rooted path or regexp>'
417 420
418 421 returns:
419 422 a 3-tuple containing
420 423 - list of explicit non-pattern names passed in
421 424 - a bool match(filename) function
422 425 - a bool indicating if any patterns were passed in
423 426
424 427 todo:
425 428 make head regex a rooted bool
426 429 """
427 430
428 431 def contains_glob(name):
429 432 for c in name:
430 433 if c in _globchars: return True
431 434 return False
432 435
433 436 def regex(kind, name, tail):
434 437 '''convert a pattern into a regular expression'''
435 438 if kind == 're':
436 439 return name
437 440 elif kind == 'path':
438 441 return '^' + re.escape(name) + '(?:/|$)'
439 442 elif kind == 'relglob':
440 443 return head + globre(name, '(?:|.*/)', tail)
441 444 elif kind == 'relpath':
442 445 return head + re.escape(name) + tail
443 446 elif kind == 'relre':
444 447 if name.startswith('^'):
445 448 return name
446 449 return '.*' + name
447 450 return head + globre(name, '', tail)
448 451
449 452 def matchfn(pats, tail):
450 453 """build a matching function from a set of patterns"""
451 454 if not pats:
452 455 return
453 456 matches = []
454 457 for k, p in pats:
455 458 try:
456 459 pat = '(?:%s)' % regex(k, p, tail)
457 460 matches.append(re.compile(pat).match)
458 461 except re.error:
459 462 if src: raise Abort("%s: invalid pattern (%s): %s" % (src, k, p))
460 463 else: raise Abort("invalid pattern (%s): %s" % (k, p))
461 464
462 465 def buildfn(text):
463 466 for m in matches:
464 467 r = m(text)
465 468 if r:
466 469 return r
467 470
468 471 return buildfn
469 472
470 473 def globprefix(pat):
471 474 '''return the non-glob prefix of a path, e.g. foo/* -> foo'''
472 475 root = []
473 476 for p in pat.split(os.sep):
474 477 if contains_glob(p): break
475 478 root.append(p)
476 479 return '/'.join(root)
477 480
478 481 pats = []
479 482 files = []
480 483 roots = []
481 484 for kind, name in [patkind(p, dflt_pat) for p in names]:
482 485 if kind in ('glob', 'relpath'):
483 486 name = canonpath(canonroot, cwd, name)
484 487 if name == '':
485 488 kind, name = 'glob', '**'
486 489 if kind in ('glob', 'path', 're'):
487 490 pats.append((kind, name))
488 491 if kind == 'glob':
489 492 root = globprefix(name)
490 493 if root: roots.append(root)
491 494 elif kind == 'relpath':
492 495 files.append((kind, name))
493 496 roots.append(name)
494 497
495 498 patmatch = matchfn(pats, '$') or always
496 499 filematch = matchfn(files, '(?:/|$)') or always
497 500 incmatch = always
498 501 if inc:
499 502 inckinds = [patkind(canonpath(canonroot, cwd, i)) for i in inc]
500 503 incmatch = matchfn(inckinds, '(?:/|$)')
501 504 excmatch = lambda fn: False
502 505 if exc:
503 506 exckinds = [patkind(canonpath(canonroot, cwd, x)) for x in exc]
504 507 excmatch = matchfn(exckinds, '(?:/|$)')
505 508
506 509 return (roots,
507 510 lambda fn: (incmatch(fn) and not excmatch(fn) and
508 511 (fn.endswith('/') or
509 512 (not pats and not files) or
510 513 (pats and patmatch(fn)) or
511 514 (files and filematch(fn)))),
512 515 (inc or exc or (pats and pats != [('glob', '**')])) and True)
513 516
514 517 def system(cmd, environ={}, cwd=None, onerr=None, errprefix=None):
515 518 '''enhanced shell command execution.
516 519 run with environment maybe modified, maybe in different dir.
517 520
518 521 if command fails and onerr is None, return status. if ui object,
519 522 print error message and return status, else raise onerr object as
520 523 exception.'''
521 524 def py2shell(val):
522 525 'convert python object into string that is useful to shell'
523 526 if val in (None, False):
524 527 return '0'
525 528 if val == True:
526 529 return '1'
527 530 return str(val)
528 531 oldenv = {}
529 532 for k in environ:
530 533 oldenv[k] = os.environ.get(k)
531 534 if cwd is not None:
532 535 oldcwd = os.getcwd()
533 536 origcmd = cmd
534 537 if os.name == 'nt':
535 538 cmd = '"%s"' % cmd
536 539 try:
537 540 for k, v in environ.iteritems():
538 541 os.environ[k] = py2shell(v)
539 542 if cwd is not None and oldcwd != cwd:
540 543 os.chdir(cwd)
541 544 rc = os.system(cmd)
542 545 if rc and onerr:
543 546 errmsg = '%s %s' % (os.path.basename(origcmd.split(None, 1)[0]),
544 547 explain_exit(rc)[0])
545 548 if errprefix:
546 549 errmsg = '%s: %s' % (errprefix, errmsg)
547 550 try:
548 551 onerr.warn(errmsg + '\n')
549 552 except AttributeError:
550 553 raise onerr(errmsg)
551 554 return rc
552 555 finally:
553 556 for k, v in oldenv.iteritems():
554 557 if v is None:
555 558 del os.environ[k]
556 559 else:
557 560 os.environ[k] = v
558 561 if cwd is not None and oldcwd != cwd:
559 562 os.chdir(oldcwd)
560 563
561 564 def rename(src, dst):
562 565 """forcibly rename a file"""
563 566 try:
564 567 os.rename(src, dst)
565 568 except OSError, err:
566 569 # on windows, rename to existing file is not allowed, so we
567 570 # must delete destination first. but if file is open, unlink
568 571 # schedules it for delete but does not delete it. rename
569 572 # happens immediately even for open files, so we create
570 573 # temporary file, delete it, rename destination to that name,
571 574 # then delete that. then rename is safe to do.
572 575 fd, temp = tempfile.mkstemp(dir=os.path.dirname(dst) or '.')
573 576 os.close(fd)
574 577 os.unlink(temp)
575 578 os.rename(dst, temp)
576 579 os.unlink(temp)
577 580 os.rename(src, dst)
578 581
579 582 def unlink(f):
580 583 """unlink and remove the directory if it is empty"""
581 584 os.unlink(f)
582 585 # try removing directories that might now be empty
583 586 try:
584 587 os.removedirs(os.path.dirname(f))
585 588 except OSError:
586 589 pass
587 590
588 591 def copyfile(src, dest):
589 592 "copy a file, preserving mode"
590 593 try:
591 594 shutil.copyfile(src, dest)
592 595 shutil.copymode(src, dest)
593 596 except shutil.Error, inst:
594 597 raise Abort(str(inst))
595 598
596 599 def copyfiles(src, dst, hardlink=None):
597 600 """Copy a directory tree using hardlinks if possible"""
598 601
599 602 if hardlink is None:
600 603 hardlink = (os.stat(src).st_dev ==
601 604 os.stat(os.path.dirname(dst)).st_dev)
602 605
603 606 if os.path.isdir(src):
604 607 os.mkdir(dst)
605 608 for name in os.listdir(src):
606 609 srcname = os.path.join(src, name)
607 610 dstname = os.path.join(dst, name)
608 611 copyfiles(srcname, dstname, hardlink)
609 612 else:
610 613 if hardlink:
611 614 try:
612 615 os_link(src, dst)
613 616 except (IOError, OSError):
614 617 hardlink = False
615 618 shutil.copy(src, dst)
616 619 else:
617 620 shutil.copy(src, dst)
618 621
619 622 def audit_path(path):
620 623 """Abort if path contains dangerous components"""
621 624 parts = os.path.normcase(path).split(os.sep)
622 625 if (os.path.splitdrive(path)[0] or parts[0] in ('.hg', '')
623 626 or os.pardir in parts):
624 627 raise Abort(_("path contains illegal component: %s\n") % path)
625 628
626 629 def _makelock_file(info, pathname):
627 630 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
628 631 os.write(ld, info)
629 632 os.close(ld)
630 633
631 634 def _readlock_file(pathname):
632 635 return posixfile(pathname).read()
633 636
634 637 def nlinks(pathname):
635 638 """Return number of hardlinks for the given file."""
636 639 return os.lstat(pathname).st_nlink
637 640
638 641 if hasattr(os, 'link'):
639 642 os_link = os.link
640 643 else:
641 644 def os_link(src, dst):
642 645 raise OSError(0, _("Hardlinks not supported"))
643 646
644 647 def fstat(fp):
645 648 '''stat file object that may not have fileno method.'''
646 649 try:
647 650 return os.fstat(fp.fileno())
648 651 except AttributeError:
649 652 return os.stat(fp.name)
650 653
651 654 posixfile = file
652 655
653 656 def is_win_9x():
654 657 '''return true if run on windows 95, 98 or me.'''
655 658 try:
656 659 return sys.getwindowsversion()[3] == 1
657 660 except AttributeError:
658 661 return os.name == 'nt' and 'command' in os.environ.get('comspec', '')
659 662
660 663 getuser_fallback = None
661 664
662 665 def getuser():
663 666 '''return name of current user'''
664 667 try:
665 668 return getpass.getuser()
666 669 except ImportError:
667 670 # import of pwd will fail on windows - try fallback
668 671 if getuser_fallback:
669 672 return getuser_fallback()
670 673 # raised if win32api not available
671 674 raise Abort(_('user name not available - set USERNAME '
672 675 'environment variable'))
673 676
674 677 def username(uid=None):
675 678 """Return the name of the user with the given uid.
676 679
677 680 If uid is None, return the name of the current user."""
678 681 try:
679 682 import pwd
680 683 if uid is None:
681 684 uid = os.getuid()
682 685 try:
683 686 return pwd.getpwuid(uid)[0]
684 687 except KeyError:
685 688 return str(uid)
686 689 except ImportError:
687 690 return None
688 691
689 692 def groupname(gid=None):
690 693 """Return the name of the group with the given gid.
691 694
692 695 If gid is None, return the name of the current group."""
693 696 try:
694 697 import grp
695 698 if gid is None:
696 699 gid = os.getgid()
697 700 try:
698 701 return grp.getgrgid(gid)[0]
699 702 except KeyError:
700 703 return str(gid)
701 704 except ImportError:
702 705 return None
703 706
704 707 # File system features
705 708
706 709 def checkfolding(path):
707 710 """
708 711 Check whether the given path is on a case-sensitive filesystem
709 712
710 713 Requires a path (like /foo/.hg) ending with a foldable final
711 714 directory component.
712 715 """
713 716 s1 = os.stat(path)
714 717 d, b = os.path.split(path)
715 718 p2 = os.path.join(d, b.upper())
716 719 if path == p2:
717 720 p2 = os.path.join(d, b.lower())
718 721 try:
719 722 s2 = os.stat(p2)
720 723 if s2 == s1:
721 724 return False
722 725 return True
723 726 except:
724 727 return True
725 728
726 729 def checkexec(path):
727 730 """
728 731 Check whether the given path is on a filesystem with UNIX-like exec flags
729 732
730 733 Requires a directory (like /foo/.hg)
731 734 """
732 735 fh, fn = tempfile.mkstemp("", "", path)
733 736 os.close(fh)
734 737 m = os.stat(fn).st_mode
735 738 os.chmod(fn, m ^ 0111)
736 739 r = (os.stat(fn).st_mode != m)
737 740 os.unlink(fn)
738 741 return r
739 742
740 743 def execfunc(path, fallback):
741 744 '''return an is_exec() function with default to fallback'''
742 745 if checkexec(path):
743 746 return lambda x: is_exec(os.path.join(path, x))
744 747 return fallback
745 748
746 749 def checklink(path):
747 750 """check whether the given path is on a symlink-capable filesystem"""
748 751 # mktemp is not racy because symlink creation will fail if the
749 752 # file already exists
750 753 name = tempfile.mktemp(dir=path)
751 754 try:
752 755 os.symlink(".", name)
753 756 os.unlink(name)
754 757 return True
755 758 except (OSError, AttributeError):
756 759 return False
757 760
758 761 def linkfunc(path, fallback):
759 762 '''return an is_link() function with default to fallback'''
760 763 if checklink(path):
761 764 return lambda x: is_link(os.path.join(path, x))
762 765 return fallback
763 766
764 767 # Platform specific variants
765 768 if os.name == 'nt':
766 769 import msvcrt
767 770 nulldev = 'NUL:'
768 771
769 772 class winstdout:
770 773 '''stdout on windows misbehaves if sent through a pipe'''
771 774
772 775 def __init__(self, fp):
773 776 self.fp = fp
774 777
775 778 def __getattr__(self, key):
776 779 return getattr(self.fp, key)
777 780
778 781 def close(self):
779 782 try:
780 783 self.fp.close()
781 784 except: pass
782 785
783 786 def write(self, s):
784 787 try:
785 788 return self.fp.write(s)
786 789 except IOError, inst:
787 790 if inst.errno != 0: raise
788 791 self.close()
789 792 raise IOError(errno.EPIPE, 'Broken pipe')
790 793
791 794 sys.stdout = winstdout(sys.stdout)
792 795
793 796 def system_rcpath():
794 797 try:
795 798 return system_rcpath_win32()
796 799 except:
797 800 return [r'c:\mercurial\mercurial.ini']
798 801
799 802 def os_rcpath():
800 803 '''return default os-specific hgrc search path'''
801 804 path = system_rcpath()
802 805 path.extend(user_rcpath())
803 806 path = [os.path.normpath(f) for f in path]
804 807 return path
805 808
806 809 def user_rcpath():
807 810 '''return os-specific hgrc search path to the user dir'''
808 811 path = [os.path.join(os.path.expanduser('~'), 'mercurial.ini')]
809 812 userprofile = os.environ.get('USERPROFILE')
810 813 if userprofile:
811 814 path.append(os.path.join(userprofile, 'mercurial.ini'))
812 815 return path
813 816
814 817 def parse_patch_output(output_line):
815 818 """parses the output produced by patch and returns the file name"""
816 819 pf = output_line[14:]
817 820 if pf[0] == '`':
818 821 pf = pf[1:-1] # Remove the quotes
819 822 return pf
820 823
821 824 def testpid(pid):
822 825 '''return False if pid dead, True if running or not known'''
823 826 return True
824 827
825 828 def set_exec(f, mode):
826 829 pass
827 830
828 831 def set_link(f, mode):
829 832 pass
830 833
831 834 def set_binary(fd):
832 835 msvcrt.setmode(fd.fileno(), os.O_BINARY)
833 836
834 837 def pconvert(path):
835 838 return path.replace("\\", "/")
836 839
837 840 def localpath(path):
838 841 return path.replace('/', '\\')
839 842
840 843 def normpath(path):
841 844 return pconvert(os.path.normpath(path))
842 845
843 846 makelock = _makelock_file
844 847 readlock = _readlock_file
845 848
846 849 def samestat(s1, s2):
847 850 return False
848 851
852 # A sequence of backslashes is special iff it precedes a double quote:
853 # - if there's an even number of backslashes, the double quote is not
854 # quoted (i.e. it ends the quoted region)
855 # - if there's an odd number of backslashes, the double quote is quoted
856 # - in both cases, every pair of backslashes is unquoted into a single
857 # backslash
858 # (See http://msdn2.microsoft.com/en-us/library/a1y7w461.aspx )
859 # So, to quote a string, we must surround it in double quotes, double
860 # the number of backslashes that preceed double quotes and add another
861 # backslash before every double quote (being careful with the double
862 # quote we've appended to the end)
863 _quotere = None
849 864 def shellquote(s):
850 return '"%s"' % s.replace('"', '\\"')
865 global _quotere
866 if _quotere is None:
867 _quotere = re.compile(r'(\\*)("|\\$)')
868 return '"%s"' % _quotere.sub(r'\1\1\\\2', s)
851 869
852 870 def explain_exit(code):
853 871 return _("exited with status %d") % code, code
854 872
855 873 # if you change this stub into a real check, please try to implement the
856 874 # username and groupname functions above, too.
857 875 def isowner(fp, st=None):
858 876 return True
859 877
860 878 try:
861 879 # override functions with win32 versions if possible
862 880 from util_win32 import *
863 881 if not is_win_9x():
864 882 posixfile = posixfile_nt
865 883 except ImportError:
866 884 pass
867 885
868 886 else:
869 887 nulldev = '/dev/null'
870 888 _umask = os.umask(0)
871 889 os.umask(_umask)
872 890
873 891 def rcfiles(path):
874 892 rcs = [os.path.join(path, 'hgrc')]
875 893 rcdir = os.path.join(path, 'hgrc.d')
876 894 try:
877 895 rcs.extend([os.path.join(rcdir, f) for f in os.listdir(rcdir)
878 896 if f.endswith(".rc")])
879 897 except OSError:
880 898 pass
881 899 return rcs
882 900
883 901 def os_rcpath():
884 902 '''return default os-specific hgrc search path'''
885 903 path = system_rcpath()
886 904 path.extend(user_rcpath())
887 905 path = [os.path.normpath(f) for f in path]
888 906 return path
889 907
890 908 def system_rcpath():
891 909 path = []
892 910 # old mod_python does not set sys.argv
893 911 if len(getattr(sys, 'argv', [])) > 0:
894 912 path.extend(rcfiles(os.path.dirname(sys.argv[0]) +
895 913 '/../etc/mercurial'))
896 914 path.extend(rcfiles('/etc/mercurial'))
897 915 return path
898 916
899 917 def user_rcpath():
900 918 return [os.path.expanduser('~/.hgrc')]
901 919
902 920 def parse_patch_output(output_line):
903 921 """parses the output produced by patch and returns the file name"""
904 922 pf = output_line[14:]
905 923 if pf.startswith("'") and pf.endswith("'") and " " in pf:
906 924 pf = pf[1:-1] # Remove the quotes
907 925 return pf
908 926
909 927 def is_exec(f):
910 928 """check whether a file is executable"""
911 929 return (os.lstat(f).st_mode & 0100 != 0)
912 930
913 931 def set_exec(f, mode):
914 932 s = os.lstat(f).st_mode
915 933 if (s & 0100 != 0) == mode:
916 934 return
917 935 if mode:
918 936 # Turn on +x for every +r bit when making a file executable
919 937 # and obey umask.
920 938 os.chmod(f, s | (s & 0444) >> 2 & ~_umask)
921 939 else:
922 940 os.chmod(f, s & 0666)
923 941
924 942 def is_link(f):
925 943 """check whether a file is a symlink"""
926 944 return (os.lstat(f).st_mode & 0120000 == 0120000)
927 945
928 946 def set_link(f, mode):
929 947 """make a file a symbolic link/regular file
930 948
931 949 if a file is changed to a link, its contents become the link data
932 950 if a link is changed to a file, its link data become its contents
933 951 """
934 952
935 953 m = is_link(f)
936 954 if m == bool(mode):
937 955 return
938 956
939 957 if mode: # switch file to link
940 958 data = file(f).read()
941 959 os.unlink(f)
942 960 os.symlink(data, f)
943 961 else:
944 962 data = os.readlink(f)
945 963 os.unlink(f)
946 964 file(f, "w").write(data)
947 965
948 966 def set_binary(fd):
949 967 pass
950 968
951 969 def pconvert(path):
952 970 return path
953 971
954 972 def localpath(path):
955 973 return path
956 974
957 975 normpath = os.path.normpath
958 976 samestat = os.path.samestat
959 977
960 978 def makelock(info, pathname):
961 979 try:
962 980 os.symlink(info, pathname)
963 981 except OSError, why:
964 982 if why.errno == errno.EEXIST:
965 983 raise
966 984 else:
967 985 _makelock_file(info, pathname)
968 986
969 987 def readlock(pathname):
970 988 try:
971 989 return os.readlink(pathname)
972 990 except OSError, why:
973 991 if why.errno == errno.EINVAL:
974 992 return _readlock_file(pathname)
975 993 else:
976 994 raise
977 995
978 996 def shellquote(s):
979 997 return "'%s'" % s.replace("'", "'\\''")
980 998
981 999 def testpid(pid):
982 1000 '''return False if pid dead, True if running or not sure'''
983 1001 try:
984 1002 os.kill(pid, 0)
985 1003 return True
986 1004 except OSError, inst:
987 1005 return inst.errno != errno.ESRCH
988 1006
989 1007 def explain_exit(code):
990 1008 """return a 2-tuple (desc, code) describing a process's status"""
991 1009 if os.WIFEXITED(code):
992 1010 val = os.WEXITSTATUS(code)
993 1011 return _("exited with status %d") % val, val
994 1012 elif os.WIFSIGNALED(code):
995 1013 val = os.WTERMSIG(code)
996 1014 return _("killed by signal %d") % val, val
997 1015 elif os.WIFSTOPPED(code):
998 1016 val = os.WSTOPSIG(code)
999 1017 return _("stopped by signal %d") % val, val
1000 1018 raise ValueError(_("invalid exit code"))
1001 1019
1002 1020 def isowner(fp, st=None):
1003 1021 """Return True if the file object f belongs to the current user.
1004 1022
1005 1023 The return value of a util.fstat(f) may be passed as the st argument.
1006 1024 """
1007 1025 if st is None:
1008 1026 st = fstat(fp)
1009 1027 return st.st_uid == os.getuid()
1010 1028
1011 1029 def _buildencodefun():
1012 1030 e = '_'
1013 1031 win_reserved = [ord(x) for x in '\\:*?"<>|']
1014 1032 cmap = dict([ (chr(x), chr(x)) for x in xrange(127) ])
1015 1033 for x in (range(32) + range(126, 256) + win_reserved):
1016 1034 cmap[chr(x)] = "~%02x" % x
1017 1035 for x in range(ord("A"), ord("Z")+1) + [ord(e)]:
1018 1036 cmap[chr(x)] = e + chr(x).lower()
1019 1037 dmap = {}
1020 1038 for k, v in cmap.iteritems():
1021 1039 dmap[v] = k
1022 1040 def decode(s):
1023 1041 i = 0
1024 1042 while i < len(s):
1025 1043 for l in xrange(1, 4):
1026 1044 try:
1027 1045 yield dmap[s[i:i+l]]
1028 1046 i += l
1029 1047 break
1030 1048 except KeyError:
1031 1049 pass
1032 1050 else:
1033 1051 raise KeyError
1034 1052 return (lambda s: "".join([cmap[c] for c in s]),
1035 1053 lambda s: "".join(list(decode(s))))
1036 1054
1037 1055 encodefilename, decodefilename = _buildencodefun()
1038 1056
1039 1057 def encodedopener(openerfn, fn):
1040 1058 def o(path, *args, **kw):
1041 1059 return openerfn(fn(path), *args, **kw)
1042 1060 return o
1043 1061
1044 1062 def opener(base, audit=True):
1045 1063 """
1046 1064 return a function that opens files relative to base
1047 1065
1048 1066 this function is used to hide the details of COW semantics and
1049 1067 remote file access from higher level code.
1050 1068 """
1051 1069 p = base
1052 1070 audit_p = audit
1053 1071
1054 1072 def mktempcopy(name):
1055 1073 d, fn = os.path.split(name)
1056 1074 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
1057 1075 os.close(fd)
1058 1076 ofp = posixfile(temp, "wb")
1059 1077 try:
1060 1078 try:
1061 1079 ifp = posixfile(name, "rb")
1062 1080 except IOError, inst:
1063 1081 if not getattr(inst, 'filename', None):
1064 1082 inst.filename = name
1065 1083 raise
1066 1084 for chunk in filechunkiter(ifp):
1067 1085 ofp.write(chunk)
1068 1086 ifp.close()
1069 1087 ofp.close()
1070 1088 except:
1071 1089 try: os.unlink(temp)
1072 1090 except: pass
1073 1091 raise
1074 1092 st = os.lstat(name)
1075 1093 os.chmod(temp, st.st_mode)
1076 1094 return temp
1077 1095
1078 1096 class atomictempfile(posixfile):
1079 1097 """the file will only be copied when rename is called"""
1080 1098 def __init__(self, name, mode):
1081 1099 self.__name = name
1082 1100 self.temp = mktempcopy(name)
1083 1101 posixfile.__init__(self, self.temp, mode)
1084 1102 def rename(self):
1085 1103 if not self.closed:
1086 1104 posixfile.close(self)
1087 1105 rename(self.temp, localpath(self.__name))
1088 1106 def __del__(self):
1089 1107 if not self.closed:
1090 1108 try:
1091 1109 os.unlink(self.temp)
1092 1110 except: pass
1093 1111 posixfile.close(self)
1094 1112
1095 1113 class atomicfile(atomictempfile):
1096 1114 """the file will only be copied on close"""
1097 1115 def __init__(self, name, mode):
1098 1116 atomictempfile.__init__(self, name, mode)
1099 1117 def close(self):
1100 1118 self.rename()
1101 1119 def __del__(self):
1102 1120 self.rename()
1103 1121
1104 1122 def o(path, mode="r", text=False, atomic=False, atomictemp=False):
1105 1123 if audit_p:
1106 1124 audit_path(path)
1107 1125 f = os.path.join(p, path)
1108 1126
1109 1127 if not text:
1110 1128 mode += "b" # for that other OS
1111 1129
1112 1130 if mode[0] != "r":
1113 1131 try:
1114 1132 nlink = nlinks(f)
1115 1133 except OSError:
1116 1134 d = os.path.dirname(f)
1117 1135 if not os.path.isdir(d):
1118 1136 os.makedirs(d)
1119 1137 else:
1120 1138 if atomic:
1121 1139 return atomicfile(f, mode)
1122 1140 elif atomictemp:
1123 1141 return atomictempfile(f, mode)
1124 1142 if nlink > 1:
1125 1143 rename(mktempcopy(f), f)
1126 1144 return posixfile(f, mode)
1127 1145
1128 1146 return o
1129 1147
1130 1148 class chunkbuffer(object):
1131 1149 """Allow arbitrary sized chunks of data to be efficiently read from an
1132 1150 iterator over chunks of arbitrary size."""
1133 1151
1134 1152 def __init__(self, in_iter, targetsize = 2**16):
1135 1153 """in_iter is the iterator that's iterating over the input chunks.
1136 1154 targetsize is how big a buffer to try to maintain."""
1137 1155 self.in_iter = iter(in_iter)
1138 1156 self.buf = ''
1139 1157 self.targetsize = int(targetsize)
1140 1158 if self.targetsize <= 0:
1141 1159 raise ValueError(_("targetsize must be greater than 0, was %d") %
1142 1160 targetsize)
1143 1161 self.iterempty = False
1144 1162
1145 1163 def fillbuf(self):
1146 1164 """Ignore target size; read every chunk from iterator until empty."""
1147 1165 if not self.iterempty:
1148 1166 collector = cStringIO.StringIO()
1149 1167 collector.write(self.buf)
1150 1168 for ch in self.in_iter:
1151 1169 collector.write(ch)
1152 1170 self.buf = collector.getvalue()
1153 1171 self.iterempty = True
1154 1172
1155 1173 def read(self, l):
1156 1174 """Read L bytes of data from the iterator of chunks of data.
1157 1175 Returns less than L bytes if the iterator runs dry."""
1158 1176 if l > len(self.buf) and not self.iterempty:
1159 1177 # Clamp to a multiple of self.targetsize
1160 1178 targetsize = self.targetsize * ((l // self.targetsize) + 1)
1161 1179 collector = cStringIO.StringIO()
1162 1180 collector.write(self.buf)
1163 1181 collected = len(self.buf)
1164 1182 for chunk in self.in_iter:
1165 1183 collector.write(chunk)
1166 1184 collected += len(chunk)
1167 1185 if collected >= targetsize:
1168 1186 break
1169 1187 if collected < targetsize:
1170 1188 self.iterempty = True
1171 1189 self.buf = collector.getvalue()
1172 1190 s, self.buf = self.buf[:l], buffer(self.buf, l)
1173 1191 return s
1174 1192
1175 1193 def filechunkiter(f, size=65536, limit=None):
1176 1194 """Create a generator that produces the data in the file size
1177 1195 (default 65536) bytes at a time, up to optional limit (default is
1178 1196 to read all data). Chunks may be less than size bytes if the
1179 1197 chunk is the last chunk in the file, or the file is a socket or
1180 1198 some other type of file that sometimes reads less data than is
1181 1199 requested."""
1182 1200 assert size >= 0
1183 1201 assert limit is None or limit >= 0
1184 1202 while True:
1185 1203 if limit is None: nbytes = size
1186 1204 else: nbytes = min(limit, size)
1187 1205 s = nbytes and f.read(nbytes)
1188 1206 if not s: break
1189 1207 if limit: limit -= len(s)
1190 1208 yield s
1191 1209
1192 1210 def makedate():
1193 1211 lt = time.localtime()
1194 1212 if lt[8] == 1 and time.daylight:
1195 1213 tz = time.altzone
1196 1214 else:
1197 1215 tz = time.timezone
1198 1216 return time.mktime(lt), tz
1199 1217
1200 1218 def datestr(date=None, format='%a %b %d %H:%M:%S %Y', timezone=True):
1201 1219 """represent a (unixtime, offset) tuple as a localized time.
1202 1220 unixtime is seconds since the epoch, and offset is the time zone's
1203 1221 number of seconds away from UTC. if timezone is false, do not
1204 1222 append time zone to string."""
1205 1223 t, tz = date or makedate()
1206 1224 s = time.strftime(format, time.gmtime(float(t) - tz))
1207 1225 if timezone:
1208 1226 s += " %+03d%02d" % (-tz / 3600, ((-tz % 3600) / 60))
1209 1227 return s
1210 1228
1211 1229 def strdate(string, format, defaults):
1212 1230 """parse a localized time string and return a (unixtime, offset) tuple.
1213 1231 if the string cannot be parsed, ValueError is raised."""
1214 1232 def timezone(string):
1215 1233 tz = string.split()[-1]
1216 1234 if tz[0] in "+-" and len(tz) == 5 and tz[1:].isdigit():
1217 1235 tz = int(tz)
1218 1236 offset = - 3600 * (tz / 100) - 60 * (tz % 100)
1219 1237 return offset
1220 1238 if tz == "GMT" or tz == "UTC":
1221 1239 return 0
1222 1240 return None
1223 1241
1224 1242 # NOTE: unixtime = localunixtime + offset
1225 1243 offset, date = timezone(string), string
1226 1244 if offset != None:
1227 1245 date = " ".join(string.split()[:-1])
1228 1246
1229 1247 # add missing elements from defaults
1230 1248 for part in defaults:
1231 1249 found = [True for p in part if ("%"+p) in format]
1232 1250 if not found:
1233 1251 date += "@" + defaults[part]
1234 1252 format += "@%" + part[0]
1235 1253
1236 1254 timetuple = time.strptime(date, format)
1237 1255 localunixtime = int(calendar.timegm(timetuple))
1238 1256 if offset is None:
1239 1257 # local timezone
1240 1258 unixtime = int(time.mktime(timetuple))
1241 1259 offset = unixtime - localunixtime
1242 1260 else:
1243 1261 unixtime = localunixtime + offset
1244 1262 return unixtime, offset
1245 1263
1246 1264 def parsedate(string, formats=None, defaults=None):
1247 1265 """parse a localized time string and return a (unixtime, offset) tuple.
1248 1266 The date may be a "unixtime offset" string or in one of the specified
1249 1267 formats."""
1250 1268 if not string:
1251 1269 return 0, 0
1252 1270 if not formats:
1253 1271 formats = defaultdateformats
1254 1272 string = string.strip()
1255 1273 try:
1256 1274 when, offset = map(int, string.split(' '))
1257 1275 except ValueError:
1258 1276 # fill out defaults
1259 1277 if not defaults:
1260 1278 defaults = {}
1261 1279 now = makedate()
1262 1280 for part in "d mb yY HI M S".split():
1263 1281 if part not in defaults:
1264 1282 if part[0] in "HMS":
1265 1283 defaults[part] = "00"
1266 1284 elif part[0] in "dm":
1267 1285 defaults[part] = "1"
1268 1286 else:
1269 1287 defaults[part] = datestr(now, "%" + part[0], False)
1270 1288
1271 1289 for format in formats:
1272 1290 try:
1273 1291 when, offset = strdate(string, format, defaults)
1274 1292 except ValueError:
1275 1293 pass
1276 1294 else:
1277 1295 break
1278 1296 else:
1279 1297 raise Abort(_('invalid date: %r ') % string)
1280 1298 # validate explicit (probably user-specified) date and
1281 1299 # time zone offset. values must fit in signed 32 bits for
1282 1300 # current 32-bit linux runtimes. timezones go from UTC-12
1283 1301 # to UTC+14
1284 1302 if abs(when) > 0x7fffffff:
1285 1303 raise Abort(_('date exceeds 32 bits: %d') % when)
1286 1304 if offset < -50400 or offset > 43200:
1287 1305 raise Abort(_('impossible time zone offset: %d') % offset)
1288 1306 return when, offset
1289 1307
1290 1308 def matchdate(date):
1291 1309 """Return a function that matches a given date match specifier
1292 1310
1293 1311 Formats include:
1294 1312
1295 1313 '{date}' match a given date to the accuracy provided
1296 1314
1297 1315 '<{date}' on or before a given date
1298 1316
1299 1317 '>{date}' on or after a given date
1300 1318
1301 1319 """
1302 1320
1303 1321 def lower(date):
1304 1322 return parsedate(date, extendeddateformats)[0]
1305 1323
1306 1324 def upper(date):
1307 1325 d = dict(mb="12", HI="23", M="59", S="59")
1308 1326 for days in "31 30 29".split():
1309 1327 try:
1310 1328 d["d"] = days
1311 1329 return parsedate(date, extendeddateformats, d)[0]
1312 1330 except:
1313 1331 pass
1314 1332 d["d"] = "28"
1315 1333 return parsedate(date, extendeddateformats, d)[0]
1316 1334
1317 1335 if date[0] == "<":
1318 1336 when = upper(date[1:])
1319 1337 return lambda x: x <= when
1320 1338 elif date[0] == ">":
1321 1339 when = lower(date[1:])
1322 1340 return lambda x: x >= when
1323 1341 elif date[0] == "-":
1324 1342 try:
1325 1343 days = int(date[1:])
1326 1344 except ValueError:
1327 1345 raise Abort(_("invalid day spec: %s") % date[1:])
1328 1346 when = makedate()[0] - days * 3600 * 24
1329 1347 return lambda x: x >= when
1330 1348 elif " to " in date:
1331 1349 a, b = date.split(" to ")
1332 1350 start, stop = lower(a), upper(b)
1333 1351 return lambda x: x >= start and x <= stop
1334 1352 else:
1335 1353 start, stop = lower(date), upper(date)
1336 1354 return lambda x: x >= start and x <= stop
1337 1355
1338 1356 def shortuser(user):
1339 1357 """Return a short representation of a user name or email address."""
1340 1358 f = user.find('@')
1341 1359 if f >= 0:
1342 1360 user = user[:f]
1343 1361 f = user.find('<')
1344 1362 if f >= 0:
1345 1363 user = user[f+1:]
1346 1364 f = user.find(' ')
1347 1365 if f >= 0:
1348 1366 user = user[:f]
1349 1367 f = user.find('.')
1350 1368 if f >= 0:
1351 1369 user = user[:f]
1352 1370 return user
1353 1371
1354 1372 def ellipsis(text, maxlength=400):
1355 1373 """Trim string to at most maxlength (default: 400) characters."""
1356 1374 if len(text) <= maxlength:
1357 1375 return text
1358 1376 else:
1359 1377 return "%s..." % (text[:maxlength-3])
1360 1378
1361 1379 def walkrepos(path):
1362 1380 '''yield every hg repository under path, recursively.'''
1363 1381 def errhandler(err):
1364 1382 if err.filename == path:
1365 1383 raise err
1366 1384
1367 1385 for root, dirs, files in os.walk(path, onerror=errhandler):
1368 1386 for d in dirs:
1369 1387 if d == '.hg':
1370 1388 yield root
1371 1389 dirs[:] = []
1372 1390 break
1373 1391
1374 1392 _rcpath = None
1375 1393
1376 1394 def rcpath():
1377 1395 '''return hgrc search path. if env var HGRCPATH is set, use it.
1378 1396 for each item in path, if directory, use files ending in .rc,
1379 1397 else use item.
1380 1398 make HGRCPATH empty to only look in .hg/hgrc of current repo.
1381 1399 if no HGRCPATH, use default os-specific path.'''
1382 1400 global _rcpath
1383 1401 if _rcpath is None:
1384 1402 if 'HGRCPATH' in os.environ:
1385 1403 _rcpath = []
1386 1404 for p in os.environ['HGRCPATH'].split(os.pathsep):
1387 1405 if not p: continue
1388 1406 if os.path.isdir(p):
1389 1407 for f in os.listdir(p):
1390 1408 if f.endswith('.rc'):
1391 1409 _rcpath.append(os.path.join(p, f))
1392 1410 else:
1393 1411 _rcpath.append(p)
1394 1412 else:
1395 1413 _rcpath = os_rcpath()
1396 1414 return _rcpath
1397 1415
1398 1416 def bytecount(nbytes):
1399 1417 '''return byte count formatted as readable string, with units'''
1400 1418
1401 1419 units = (
1402 1420 (100, 1<<30, _('%.0f GB')),
1403 1421 (10, 1<<30, _('%.1f GB')),
1404 1422 (1, 1<<30, _('%.2f GB')),
1405 1423 (100, 1<<20, _('%.0f MB')),
1406 1424 (10, 1<<20, _('%.1f MB')),
1407 1425 (1, 1<<20, _('%.2f MB')),
1408 1426 (100, 1<<10, _('%.0f KB')),
1409 1427 (10, 1<<10, _('%.1f KB')),
1410 1428 (1, 1<<10, _('%.2f KB')),
1411 1429 (1, 1, _('%.0f bytes')),
1412 1430 )
1413 1431
1414 1432 for multiplier, divisor, format in units:
1415 1433 if nbytes >= divisor * multiplier:
1416 1434 return format % (nbytes / float(divisor))
1417 1435 return units[-1][2] % nbytes
1418 1436
1419 1437 def drop_scheme(scheme, path):
1420 1438 sc = scheme + ':'
1421 1439 if path.startswith(sc):
1422 1440 path = path[len(sc):]
1423 1441 if path.startswith('//'):
1424 1442 path = path[2:]
1425 1443 return path
@@ -1,137 +1,142 b''
1 1 #!/bin/sh
2 2
3 3 hg init a
4 4 cd a
5 5
6 6 echo start > start
7 7 hg ci -Amstart -d '0 0'
8 8 echo new > new
9 9 hg ci -Amnew -d '0 0'
10 10 echo '% new file'
11 11 hg diff --git -r 0
12 12
13 13 hg cp new copy
14 14 hg ci -mcopy -d '0 0'
15 15 echo '% copy'
16 16 hg diff --git -r 1:tip
17 17
18 18 hg mv copy rename
19 19 hg ci -mrename -d '0 0'
20 20 echo '% rename'
21 21 hg diff --git -r 2:tip
22 22
23 23 hg rm rename
24 24 hg ci -mdelete -d '0 0'
25 25 echo '% delete'
26 26 hg diff --git -r 3:tip
27 27
28 28 cat > src <<EOF
29 29 1
30 30 2
31 31 3
32 32 4
33 33 5
34 34 EOF
35 35 hg ci -Amsrc -d '0 0'
36 36 chmod +x src
37 37 hg ci -munexec -d '0 0'
38 38 echo '% chmod 644'
39 39 hg diff --git -r 5:tip
40 40
41 41 hg mv src dst
42 42 chmod -x dst
43 43 echo a >> dst
44 44 hg ci -mrenamemod -d '0 0'
45 45 echo '% rename+mod+chmod'
46 46 hg diff --git -r 6:tip
47 47
48 48 echo '% nonexistent in tip+chmod'
49 49 hg diff --git -r 5:6
50 50
51 51 echo '% binary diff'
52 52 cp $TESTDIR/binfile.bin .
53 53 hg add binfile.bin
54 54 hg diff --git > b.diff
55 55 cat b.diff
56 56
57 57 echo '% import binary diff'
58 58 hg revert binfile.bin
59 59 rm binfile.bin
60 60 hg import -mfoo b.diff
61 61 cmp binfile.bin $TESTDIR/binfile.bin
62 62
63 63 echo
64 echo '% rename binary file'
65 hg mv binfile.bin renamed.bin
66 hg diff --git
67
68 echo
64 69 echo '% diff across many revisions'
65 70 hg mv dst dst2
66 71 hg ci -m 'mv dst dst2' -d '0 0'
67 72
68 73 echo >> start
69 74 hg ci -m 'change start' -d '0 0'
70 75
71 76 hg revert -r -2 start
72 77 hg mv dst2 dst3
73 78 hg ci -m 'mv dst2 dst3; revert start' -d '0 0'
74 79
75 80 hg diff --git -r 9:11
76 81
77 82 echo a >> foo
78 83 hg add foo
79 84 hg ci -m 'add foo'
80 85 echo b >> foo
81 86 hg ci -m 'change foo'
82 87 hg mv foo bar
83 88 hg ci -m 'mv foo bar'
84 89 echo c >> bar
85 90 hg ci -m 'change bar'
86 91
87 92 echo
88 93 echo '% file created before r1 and renamed before r2'
89 94 hg diff --git -r -3:-1
90 95 echo
91 96 echo '% file created in r1 and renamed before r2'
92 97 hg diff --git -r -4:-1
93 98 echo
94 99 echo '% file created after r1 and renamed before r2'
95 100 hg diff --git -r -5:-1
96 101
97 102 echo
98 103 echo '% comparing with the working dir'
99 104 echo >> start
100 105 hg ci -m 'change start again' -d '0 0'
101 106
102 107 echo > created
103 108 hg add created
104 109 hg ci -m 'add created'
105 110
106 111 hg mv created created2
107 112 hg ci -m 'mv created created2'
108 113
109 114 hg mv created2 created3
110 115 echo "% there's a copy in the working dir..."
111 116 hg diff --git
112 117 echo
113 118 echo "% ...but there's another copy between the original rev and the wd"
114 119 hg diff --git -r -2
115 120 echo
116 121 echo "% ...but the source of the copy was created after the original rev"
117 122 hg diff --git -r -3
118 123 hg ci -m 'mv created2 created3'
119 124
120 125 echo > brand-new
121 126 hg add brand-new
122 127 hg ci -m 'add brand-new'
123 128 hg mv brand-new brand-new2
124 129 echo '% created in parent of wd; renamed in the wd'
125 130 hg diff --git
126 131
127 132 echo
128 133 echo '% created between r1 and parent of wd; renamed in the wd'
129 134 hg diff --git -r -2
130 135 hg ci -m 'mv brand-new brand-new2'
131 136
132 137 echo '% one file is copied to many destinations and removed'
133 138 hg cp brand-new2 brand-new3
134 139 hg mv brand-new2 brand-new3-2
135 140 hg ci -m 'multiple renames/copies'
136 141 hg diff --git -r -2 -r -1
137 142
@@ -1,142 +1,147 b''
1 1 adding start
2 2 adding new
3 3 % new file
4 4 diff --git a/new b/new
5 5 new file mode 100644
6 6 --- /dev/null
7 7 +++ b/new
8 8 @@ -0,0 +1,1 @@
9 9 +new
10 10 % copy
11 11 diff --git a/new b/copy
12 12 copy from new
13 13 copy to copy
14 14 % rename
15 15 diff --git a/copy b/rename
16 16 rename from copy
17 17 rename to rename
18 18 % delete
19 19 diff --git a/rename b/rename
20 20 deleted file mode 100644
21 21 --- a/rename
22 22 +++ /dev/null
23 23 @@ -1,1 +0,0 @@
24 24 -new
25 25 adding src
26 26 % chmod 644
27 27 diff --git a/src b/src
28 28 old mode 100644
29 29 new mode 100755
30 30 % rename+mod+chmod
31 31 diff --git a/src b/dst
32 32 old mode 100755
33 33 new mode 100644
34 34 rename from src
35 35 rename to dst
36 36 --- a/dst
37 37 +++ b/dst
38 38 @@ -3,3 +3,4 @@ 3
39 39 3
40 40 4
41 41 5
42 42 +a
43 43 % nonexistent in tip+chmod
44 44 diff --git a/src b/src
45 45 old mode 100644
46 46 new mode 100755
47 47 % binary diff
48 48 diff --git a/binfile.bin b/binfile.bin
49 49 new file mode 100644
50 50 index 0000000000000000000000000000000000000000..37ba3d1c6f17137d9c5f5776fa040caf5fe73ff9
51 51 GIT binary patch
52 52 literal 593
53 53 zc$@)I0<QguP)<h;3K|Lk000e1NJLTq000mG000mO0ssI2kdbIM00009a7bBm000XU
54 54 z000XU0RWnu7ytkO2XskIMF-Uh9TW;VpMjwv0005-Nkl<ZD9@FWPs=e;7{<>W$NUkd
55 55 zX$nnYLt$-$V!?uy+1V%`z&Eh=ah|duER<4|QWhju3gb^nF*8iYobxWG-qqXl=2~5M
56 56 z*IoDB)sG^CfNuoBmqLTVU^<;@nwHP!1wrWd`{(mHo6VNXWtyh{alzqmsH*yYzpvLT
57 57 zLdY<T=ks|woh-`&01!ej#(xbV1f|pI*=%;d-%F*E*X#ZH`4I%6SS+$EJDE&ct=8po
58 58 ziN#{?_j|kD%Cd|oiqds`xm@;oJ-^?NG3Gdqrs?5u*zI;{nogxsx~^|Fn^Y?Gdc6<;
59 59 zfMJ+iF1J`LMx&A2?dEwNW8ClebzPTbIh{@$hS6*`kH@1d%Lo7fA#}N1)oN7`gm$~V
60 60 z+wDx#)OFqMcE{s!JN0-xhG8ItAjVkJwEcb`3WWlJfU2r?;Pd%dmR+q@mSri5q9_W-
61 61 zaR2~ECX?B2w+zELozC0s*6Z~|QG^f{3I#<`?)Q7U-JZ|q5W;9Q8i_=pBuSzunx=U;
62 62 z9C)5jBoYw9^?EHyQl(M}1OlQcCX>lXB*ODN003Z&P17_@)3Pi=i0wb04<W?v-u}7K
63 63 zXmmQA+wDgE!qR9o8jr`%=ab_&uh(l?R=r;Tjiqon91I2-hIu?57~@*4h7h9uORK#=
64 64 fQItJW-{SoTm)8|5##k|m00000NkvXXu0mjf{mKw4
65 65
66 66 % import binary diff
67 67 applying b.diff
68 68
69 % rename binary file
70 diff --git a/binfile.bin b/renamed.bin
71 rename from binfile.bin
72 rename to renamed.bin
73
69 74 % diff across many revisions
70 75 diff --git a/dst2 b/dst3
71 76 rename from dst2
72 77 rename to dst3
73 78
74 79 % file created before r1 and renamed before r2
75 80 diff --git a/foo b/bar
76 81 rename from foo
77 82 rename to bar
78 83 --- a/bar
79 84 +++ b/bar
80 85 @@ -1,2 +1,3 @@ a
81 86 a
82 87 b
83 88 +c
84 89
85 90 % file created in r1 and renamed before r2
86 91 diff --git a/foo b/bar
87 92 rename from foo
88 93 rename to bar
89 94 --- a/bar
90 95 +++ b/bar
91 96 @@ -1,1 +1,3 @@ a
92 97 a
93 98 +b
94 99 +c
95 100
96 101 % file created after r1 and renamed before r2
97 102 diff --git a/bar b/bar
98 103 new file mode 100644
99 104 --- /dev/null
100 105 +++ b/bar
101 106 @@ -0,0 +1,3 @@
102 107 +a
103 108 +b
104 109 +c
105 110
106 111 % comparing with the working dir
107 112 % there's a copy in the working dir...
108 113 diff --git a/created2 b/created3
109 114 rename from created2
110 115 rename to created3
111 116
112 117 % ...but there's another copy between the original rev and the wd
113 118 diff --git a/created b/created3
114 119 rename from created
115 120 rename to created3
116 121
117 122 % ...but the source of the copy was created after the original rev
118 123 diff --git a/created3 b/created3
119 124 new file mode 100644
120 125 --- /dev/null
121 126 +++ b/created3
122 127 @@ -0,0 +1,1 @@
123 128 +
124 129 % created in parent of wd; renamed in the wd
125 130 diff --git a/brand-new b/brand-new2
126 131 rename from brand-new
127 132 rename to brand-new2
128 133
129 134 % created between r1 and parent of wd; renamed in the wd
130 135 diff --git a/brand-new2 b/brand-new2
131 136 new file mode 100644
132 137 --- /dev/null
133 138 +++ b/brand-new2
134 139 @@ -0,0 +1,1 @@
135 140 +
136 141 % one file is copied to many destinations and removed
137 142 diff --git a/brand-new2 b/brand-new3
138 143 rename from brand-new2
139 144 rename to brand-new3
140 145 diff --git a/brand-new2 b/brand-new3-2
141 146 copy from brand-new2
142 147 copy to brand-new3-2
@@ -1,301 +1,339 b''
1 1 #!/bin/sh
2 2
3 3 echo "[extensions]" >> $HGRCPATH
4 4 echo "mq=" >> $HGRCPATH
5 5
6 6 echo % help
7 7 hg help mq
8 8
9 9 hg init a
10 10 cd a
11 11 echo a > a
12 12 hg ci -Ama
13 13
14 14 hg clone . ../k
15 15
16 16 mkdir b
17 17 echo z > b/z
18 18 hg ci -Ama
19 19
20 20 echo % qinit
21 21
22 22 hg qinit
23 23
24 24 cd ..
25 25 hg init b
26 26
27 27 echo % -R qinit
28 28
29 29 hg -R b qinit
30 30
31 31 hg init c
32 32
33 33 echo % qinit -c
34 34
35 35 hg --cwd c qinit -c
36 36 hg -R c/.hg/patches st
37 37
38 38 echo % qnew implies add
39 39
40 40 hg -R c qnew test.patch
41 41 hg -R c/.hg/patches st
42 42
43 43 echo '% qinit; qinit -c'
44 44 hg init d
45 45 cd d
46 46 hg qinit
47 47 hg qinit -c
48 48 # qinit -c should create both files if they don't exist
49 49 echo ' .hgignore:'
50 50 cat .hg/patches/.hgignore
51 51 echo ' series:'
52 52 cat .hg/patches/series
53 53 hg qinit -c 2>&1 | sed -e 's/repository.*already/repository already/'
54 54 cd ..
55 55
56 56 echo '% qinit; <stuff>; qinit -c'
57 57 hg init e
58 58 cd e
59 59 hg qnew A
60 60 echo foo > foo
61 61 hg add foo
62 62 hg qrefresh
63 63 hg qnew B
64 64 echo >> foo
65 65 hg qrefresh
66 66 echo status >> .hg/patches/.hgignore
67 67 echo bleh >> .hg/patches/.hgignore
68 68 hg qinit -c
69 69 hg -R .hg/patches status
70 70 # qinit -c shouldn't touch these files if they already exist
71 71 echo ' .hgignore:'
72 72 cat .hg/patches/.hgignore
73 73 echo ' series:'
74 74 cat .hg/patches/series
75 75 cd ..
76 76
77 77 cd a
78 78
79 79 echo % qnew -m
80 80
81 81 hg qnew -m 'foo bar' test.patch
82 82 cat .hg/patches/test.patch
83 83
84 84 echo % qrefresh
85 85
86 86 echo a >> a
87 87 hg qrefresh
88 88 sed -e "s/^\(diff -r \)\([a-f0-9]* \)/\1 x/" \
89 89 -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \
90 90 -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" .hg/patches/test.patch
91 91
92 92 echo % qpop
93 93
94 94 hg qpop
95 95
96 96 echo % qpush
97 97
98 98 hg qpush
99 99
100 100 cd ..
101 101
102 102 echo % pop/push outside repo
103 103
104 104 hg -R a qpop
105 105 hg -R a qpush
106 106
107 107 cd a
108 108 hg qnew test2.patch
109 109
110 110 echo % qrefresh in subdir
111 111
112 112 cd b
113 113 echo a > a
114 114 hg add a
115 115 hg qrefresh
116 116
117 117 echo % pop/push -a in subdir
118 118
119 119 hg qpop -a
120 120 hg --traceback qpush -a
121 121
122 122 echo % qseries
123 123 hg qseries
124 124 hg qpop
125 125 hg qseries -vs
126 126 hg qpush
127 127
128 128 echo % qapplied
129 129 hg qapplied
130 130
131 131 echo % qtop
132 132 hg qtop
133 133
134 134 echo % qprev
135 135 hg qprev
136 136
137 137 echo % qnext
138 138 hg qnext
139 139
140 140 echo % pop, qnext, qprev, qapplied
141 141 hg qpop
142 142 hg qnext
143 143 hg qprev
144 144 hg qapplied
145 145
146 146 echo % commit should fail
147 147 hg commit
148 148
149 149 echo % push should fail
150 150 hg push ../../k
151 151
152 152 echo % qunapplied
153 153 hg qunapplied
154 154
155 155 echo % qpush/qpop with index
156 156 hg qnew test1b.patch
157 157 echo 1b > 1b
158 158 hg add 1b
159 159 hg qrefresh
160 160 hg qpush 2
161 161 hg qpop 0
162 162 hg qpush test.patch+1
163 163 hg qpush test.patch+2
164 164 hg qpop test2.patch-1
165 165 hg qpop test2.patch-2
166 166 hg qpush test1b.patch+1
167 167
168 168 echo % push should succeed
169 169 hg qpop -a
170 170 hg push ../../k
171 171
172 172 echo % strip
173 173 cd ../../b
174 174 echo x>x
175 175 hg ci -Ama
176 176 hg strip tip 2>&1 | sed 's/\(saving bundle to \).*/\1/'
177 177 hg unbundle .hg/strip-backup/*
178 178
179 179 echo '% cd b; hg qrefresh'
180 180 hg init refresh
181 181 cd refresh
182 182 echo a > a
183 183 hg ci -Ama -d'0 0'
184 184 hg qnew -mfoo foo
185 185 echo a >> a
186 186 hg qrefresh
187 187 mkdir b
188 188 cd b
189 189 echo f > f
190 190 hg add f
191 191 hg qrefresh
192 192 sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \
193 193 -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" ../.hg/patches/foo
194 194 echo % hg qrefresh .
195 195 hg qrefresh .
196 196 sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \
197 197 -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" ../.hg/patches/foo
198 198 hg status
199 199
200 200 echo % qpush failure
201 201 cd ..
202 202 hg qrefresh
203 203 hg qnew -mbar bar
204 204 echo foo > foo
205 205 echo bar > bar
206 206 hg add foo bar
207 207 hg qrefresh
208 208 hg qpop -a
209 209 echo bar > foo
210 210 hg qpush -a
211 211 hg st
212 212
213 213 cat >>$HGRCPATH <<EOF
214 214 [diff]
215 215 git = True
216 216 EOF
217 217 cd ..
218 218 hg init git
219 219 cd git
220 220 hg qinit
221 221
222 222 hg qnew -m'new file' new
223 223 echo foo > new
224 224 chmod +x new
225 225 hg add new
226 226 hg qrefresh
227 227 sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \
228 228 -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" .hg/patches/new
229 229
230 230 hg qnew -m'copy file' copy
231 231 hg cp new copy
232 232 hg qrefresh
233 233 sed -e "s/\(+++ [a-zA-Z0-9_/.-]*\).*/\1/" \
234 234 -e "s/\(--- [a-zA-Z0-9_/.-]*\).*/\1/" .hg/patches/copy
235 235
236 236 hg qpop
237 237 hg qpush
238 238 hg qdiff
239 239 cat >>$HGRCPATH <<EOF
240 240 [diff]
241 241 git = False
242 242 EOF
243 243 hg qdiff --git
244 244
245 245 cd ..
246 246 hg init slow
247 247 cd slow
248 248 hg qinit
249 249 echo foo > foo
250 250 hg add foo
251 251 hg ci -m 'add foo'
252 252 hg qnew bar
253 253 echo bar > bar
254 254 hg add bar
255 255 hg mv foo baz
256 256 hg qrefresh --git
257 257 hg up -C 0
258 258 echo >> foo
259 259 hg ci -m 'change foo'
260 260 hg up -C 1
261 261 hg qrefresh --git 2>&1 | grep -v 'saving bundle'
262 262 cat .hg/patches/bar
263 263 hg log -vC --template '{rev} {file_copies%filecopy}\n' -r .
264 264 hg qrefresh --git
265 265 cat .hg/patches/bar
266 266 hg log -vC --template '{rev} {file_copies%filecopy}\n' -r .
267 267
268 268 echo
269 269 hg up -C 1
270 270 echo >> foo
271 271 hg ci -m 'change foo again'
272 272 hg up -C 2
273 273 hg mv bar quux
274 274 hg mv baz bleh
275 275 hg qrefresh --git 2>&1 | grep -v 'saving bundle'
276 276 cat .hg/patches/bar
277 277 hg log -vC --template '{rev} {file_copies%filecopy}\n' -r .
278 278 hg mv quux fred
279 279 hg mv bleh barney
280 280 hg qrefresh --git
281 281 cat .hg/patches/bar
282 282 hg log -vC --template '{rev} {file_copies%filecopy}\n' -r .
283 283
284 284 echo '% strip again'
285 285 cd ..
286 286 hg init strip
287 287 cd strip
288 288 touch foo
289 289 hg add foo
290 290 hg ci -m 'add foo' -d '0 0'
291 291 echo >> foo
292 292 hg ci -m 'change foo 1' -d '0 0'
293 293 hg up -C 0
294 294 echo 1 >> foo
295 295 hg ci -m 'change foo 2' -d '0 0'
296 296 HGMERGE=true hg merge
297 297 hg ci -m merge -d '0 0'
298 298 hg log
299 299 hg strip 1 2>&1 | sed 's/\(saving bundle to \).*/\1/'
300 300 hg log
301 cd ..
301 302
303 echo '% qclone'
304 qlog()
305 {
306 echo 'main repo:'
307 hg log --template ' rev {rev}: {desc}\n'
308 echo 'patch repo:'
309 hg -R .hg/patches log --template ' rev {rev}: {desc}\n'
310 }
311 hg init qclonesource
312 cd qclonesource
313 echo foo > foo
314 hg add foo
315 hg ci -m 'add foo'
316 hg qinit -c
317 hg qnew patch1
318 echo bar >> foo
319 hg qrefresh -m 'change foo'
320 hg qci -m checkpoint
321 qlog
322 cd ..
323
324 # repo with patches applied
325 hg qclone qclonesource qclonedest
326 cd qclonedest
327 qlog
328 cd ..
329
330 # repo with patches unapplied
331 cd qclonesource
332 hg qpop -a
333 qlog
334 cd ..
335 hg qclone qclonesource qclonedest2
336 cd qclonedest2
337 qlog
338 cd ..
339
@@ -1,335 +1,358 b''
1 1 % help
2 2 mq extension - patch management and development
3 3
4 4 This extension lets you work with a stack of patches in a Mercurial
5 5 repository. It manages two stacks of patches - all known patches, and
6 6 applied patches (subset of known patches).
7 7
8 8 Known patches are represented as patch files in the .hg/patches
9 9 directory. Applied patches are both patch files and changesets.
10 10
11 11 Common tasks (use "hg help command" for more details):
12 12
13 13 prepare repository to work with patches qinit
14 14 create new patch qnew
15 15 import existing patch qimport
16 16
17 17 print patch series qseries
18 18 print applied patches qapplied
19 19 print name of top applied patch qtop
20 20
21 21 add known patch to applied stack qpush
22 22 remove patch from applied stack qpop
23 23 refresh contents of top applied patch qrefresh
24 24
25 25 list of commands (use "hg help -v mq" to show aliases and global options):
26 26
27 27 qapplied print the patches already applied
28 28 qclone clone main and patch repository at same time
29 29 qcommit commit changes in the queue repository
30 30 qdelete remove patches from queue
31 31 qdiff diff of the current patch
32 32 qfold fold the named patches into the current patch
33 33 qguard set or print guards for a patch
34 34 qheader Print the header of the topmost or specified patch
35 35 qimport import a patch
36 36 qinit init a new queue repository
37 37 qnew create a new patch
38 38 qnext print the name of the next patch
39 39 qpop pop the current patch off the stack
40 40 qprev print the name of the previous patch
41 41 qpush push the next patch onto the stack
42 42 qrefresh update the current patch
43 43 qrename rename a patch
44 44 qrestore restore the queue state saved by a rev
45 45 qsave save current queue state
46 46 qselect set or print guarded patches to push
47 47 qseries print the entire series file
48 48 qtop print the name of the current patch
49 49 qunapplied print the patches not yet applied
50 50 strip strip a revision and all later revs on the same branch
51 51 adding a
52 52 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
53 53 adding b/z
54 54 % qinit
55 55 % -R qinit
56 56 % qinit -c
57 57 A .hgignore
58 58 A series
59 59 % qnew implies add
60 60 A .hgignore
61 61 A series
62 62 A test.patch
63 63 % qinit; qinit -c
64 64 .hgignore:
65 65 syntax: glob
66 66 status
67 67 guards
68 68 series:
69 69 abort: repository already exists!
70 70 % qinit; <stuff>; qinit -c
71 71 adding A
72 72 adding B
73 73 A .hgignore
74 74 A A
75 75 A B
76 76 A series
77 77 .hgignore:
78 78 status
79 79 bleh
80 80 series:
81 81 A
82 82 B
83 83 % qnew -m
84 84 foo bar
85 85 % qrefresh
86 86 foo bar
87 87
88 88 diff -r xa
89 89 --- a/a
90 90 +++ b/a
91 91 @@ -1,1 +1,2 @@ a
92 92 a
93 93 +a
94 94 % qpop
95 95 Patch queue now empty
96 96 % qpush
97 97 applying test.patch
98 98 Now at: test.patch
99 99 % pop/push outside repo
100 100 Patch queue now empty
101 101 applying test.patch
102 102 Now at: test.patch
103 103 % qrefresh in subdir
104 104 % pop/push -a in subdir
105 105 Patch queue now empty
106 106 applying test.patch
107 107 applying test2.patch
108 108 Now at: test2.patch
109 109 % qseries
110 110 test.patch
111 111 test2.patch
112 112 Now at: test.patch
113 113 0 A test.patch: foo bar
114 114 1 U test2.patch:
115 115 applying test2.patch
116 116 Now at: test2.patch
117 117 % qapplied
118 118 test.patch
119 119 test2.patch
120 120 % qtop
121 121 test2.patch
122 122 % qprev
123 123 test.patch
124 124 % qnext
125 125 All patches applied
126 126 % pop, qnext, qprev, qapplied
127 127 Now at: test.patch
128 128 test2.patch
129 129 Only one patch applied
130 130 test.patch
131 131 % commit should fail
132 132 abort: cannot commit over an applied mq patch
133 133 % push should fail
134 134 pushing to ../../k
135 135 abort: source has mq patches applied
136 136 % qunapplied
137 137 test2.patch
138 138 % qpush/qpop with index
139 139 applying test2.patch
140 140 Now at: test2.patch
141 141 Now at: test.patch
142 142 applying test1b.patch
143 143 Now at: test1b.patch
144 144 applying test2.patch
145 145 Now at: test2.patch
146 146 Now at: test1b.patch
147 147 Now at: test.patch
148 148 applying test1b.patch
149 149 applying test2.patch
150 150 Now at: test2.patch
151 151 % push should succeed
152 152 Patch queue now empty
153 153 pushing to ../../k
154 154 searching for changes
155 155 adding changesets
156 156 adding manifests
157 157 adding file changes
158 158 added 1 changesets with 1 changes to 1 files
159 159 % strip
160 160 adding x
161 161 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
162 162 saving bundle to
163 163 adding changesets
164 164 adding manifests
165 165 adding file changes
166 166 added 1 changesets with 1 changes to 1 files
167 167 (run 'hg update' to get a working copy)
168 168 % cd b; hg qrefresh
169 169 adding a
170 170 foo
171 171
172 172 diff -r cb9a9f314b8b a
173 173 --- a/a
174 174 +++ b/a
175 175 @@ -1,1 +1,2 @@ a
176 176 a
177 177 +a
178 178 diff -r cb9a9f314b8b b/f
179 179 --- /dev/null
180 180 +++ b/b/f
181 181 @@ -0,0 +1,1 @@
182 182 +f
183 183 % hg qrefresh .
184 184 foo
185 185
186 186 diff -r cb9a9f314b8b b/f
187 187 --- /dev/null
188 188 +++ b/b/f
189 189 @@ -0,0 +1,1 @@
190 190 +f
191 191 M a
192 192 % qpush failure
193 193 Patch queue now empty
194 194 applying foo
195 195 applying bar
196 196 1 out of 1 hunk ignored -- saving rejects to file foo.rej
197 197 patch failed, unable to continue (try -v)
198 198 patch failed, rejects left in working dir
199 199 Errors during apply, please fix and refresh bar
200 200 ? foo
201 201 ? foo.rej
202 202 new file
203 203
204 204 diff --git a/new b/new
205 205 new file mode 100755
206 206 --- /dev/null
207 207 +++ b/new
208 208 @@ -0,0 +1,1 @@
209 209 +foo
210 210 copy file
211 211
212 212 diff --git a/new b/copy
213 213 copy from new
214 214 copy to copy
215 215 Now at: new
216 216 applying copy
217 217 Now at: copy
218 218 diff --git a/new b/copy
219 219 copy from new
220 220 copy to copy
221 221 diff --git a/new b/copy
222 222 copy from new
223 223 copy to copy
224 224 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
225 225 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
226 226 adding branch
227 227 adding changesets
228 228 adding manifests
229 229 adding file changes
230 230 added 1 changesets with 1 changes to 1 files
231 231 (run 'hg update' to get a working copy)
232 232 Patch queue now empty
233 233 applying bar
234 234 Now at: bar
235 235 diff --git a/bar b/bar
236 236 new file mode 100644
237 237 --- /dev/null
238 238 +++ b/bar
239 239 @@ -0,0 +1,1 @@
240 240 +bar
241 241 diff --git a/foo b/baz
242 242 rename from foo
243 243 rename to baz
244 244 2 baz (foo)
245 245 diff --git a/bar b/bar
246 246 new file mode 100644
247 247 --- /dev/null
248 248 +++ b/bar
249 249 @@ -0,0 +1,1 @@
250 250 +bar
251 251 diff --git a/foo b/baz
252 252 rename from foo
253 253 rename to baz
254 254 2 baz (foo)
255 255
256 256 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
257 257 2 files updated, 0 files merged, 1 files removed, 0 files unresolved
258 258 adding branch
259 259 adding changesets
260 260 adding manifests
261 261 adding file changes
262 262 added 1 changesets with 1 changes to 1 files
263 263 (run 'hg update' to get a working copy)
264 264 Patch queue now empty
265 265 applying bar
266 266 Now at: bar
267 267 diff --git a/foo b/bleh
268 268 rename from foo
269 269 rename to bleh
270 270 diff --git a/quux b/quux
271 271 new file mode 100644
272 272 --- /dev/null
273 273 +++ b/quux
274 274 @@ -0,0 +1,1 @@
275 275 +bar
276 276 3 bleh (foo)
277 277 diff --git a/foo b/barney
278 278 rename from foo
279 279 rename to barney
280 280 diff --git a/fred b/fred
281 281 new file mode 100644
282 282 --- /dev/null
283 283 +++ b/fred
284 284 @@ -0,0 +1,1 @@
285 285 +bar
286 286 3 barney (foo)
287 287 % strip again
288 288 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
289 289 merging foo
290 290 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
291 291 (branch merge, don't forget to commit)
292 292 changeset: 3:99615015637b
293 293 tag: tip
294 294 parent: 2:20cbbe65cff7
295 295 parent: 1:d2871fc282d4
296 296 user: test
297 297 date: Thu Jan 01 00:00:00 1970 +0000
298 298 summary: merge
299 299
300 300 changeset: 2:20cbbe65cff7
301 301 parent: 0:53245c60e682
302 302 user: test
303 303 date: Thu Jan 01 00:00:00 1970 +0000
304 304 summary: change foo 2
305 305
306 306 changeset: 1:d2871fc282d4
307 307 user: test
308 308 date: Thu Jan 01 00:00:00 1970 +0000
309 309 summary: change foo 1
310 310
311 311 changeset: 0:53245c60e682
312 312 user: test
313 313 date: Thu Jan 01 00:00:00 1970 +0000
314 314 summary: add foo
315 315
316 316 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
317 317 saving bundle to
318 318 saving bundle to
319 319 adding branch
320 320 adding changesets
321 321 adding manifests
322 322 adding file changes
323 323 added 1 changesets with 1 changes to 1 files
324 324 (run 'hg update' to get a working copy)
325 325 changeset: 1:20cbbe65cff7
326 326 tag: tip
327 327 user: test
328 328 date: Thu Jan 01 00:00:00 1970 +0000
329 329 summary: change foo 2
330 330
331 331 changeset: 0:53245c60e682
332 332 user: test
333 333 date: Thu Jan 01 00:00:00 1970 +0000
334 334 summary: add foo
335 335
336 % qclone
337 main repo:
338 rev 1: change foo
339 rev 0: add foo
340 patch repo:
341 rev 0: checkpoint
342 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
343 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
344 main repo:
345 rev 0: add foo
346 patch repo:
347 rev 0: checkpoint
348 Patch queue now empty
349 main repo:
350 rev 0: add foo
351 patch repo:
352 rev 0: checkpoint
353 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
354 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
355 main repo:
356 rev 0: add foo
357 patch repo:
358 rev 0: checkpoint
@@ -1,40 +1,54 b''
1 1 #!/bin/sh
2 2
3 3 cat <<EOF >> $HGRCPATH
4 4 [extensions]
5 5 notify=
6 6
7 7 [hooks]
8 8 incoming.notify = python:hgext.notify.hook
9 9
10 10 [notify]
11 config = $HGTMP/.notify.conf
12 11 sources = pull
13 domain = test.com
14 strip = 3
15 template = Subject: {desc|firstline|strip}\nFrom: {author}\n\nchangeset {node|short} in {webroot}\ndescription:\n\t{desc|tabindent|strip}
16 12 diffstat = False
17 13
18 [web]
19 baseurl = http://test/
20
21 14 [usersubs]
22 15 foo@bar = *
16
17 [reposubs]
18 * = baz
23 19 EOF
24 20
25 21 hg help notify
26 22 hg init a
27 23 echo a > a/a
28 24 echo % commit
29 25 hg --traceback --cwd a commit -Ama -d '0 0'
30 26
31 27 echo % clone
32 28 hg --traceback clone a b
33 29
34 30 echo a >> a/a
35 31 echo % commit
36 32 hg --traceback --cwd a commit -Amb -d '1 0'
37 33
34 echo '% pull (minimal config)'
35 hg --traceback --cwd b pull ../a 2>&1 | sed -e 's/\(Message-Id:\).*/\1/' \
36 -e 's/changeset \([0-9a-f]* \)\?in .*test-notif/changeset \1in test-notif/' \
37 -e 's/^details: .*test-notify/details: test-notify/'
38
39 cat <<EOF >> $HGRCPATH
40 [notify]
41 config = $HGTMP/.notify.conf
42 domain = test.com
43 strip = 3
44 template = Subject: {desc|firstline|strip}\nFrom: {author}\n\nchangeset {node|short} in {webroot}\ndescription:\n\t{desc|tabindent|strip}
45
46 [web]
47 baseurl = http://test/
48 EOF
49
38 50 echo % pull
51 hg --cwd b rollback
39 52 hg --traceback --cwd b pull ../a 2>&1 | sed -e 's/\(Message-Id:\).*/\1/' \
40 53 -e 's/changeset \([0-9a-f]*\) in .*/changeset \1/'
54
@@ -1,33 +1,61 b''
1 1 notify extension - No help text available
2 2
3 3 no commands defined
4 4 % commit
5 5 adding a
6 6 % clone
7 7 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
8 8 % commit
9 % pull (minimal config)
10 pulling from ../a
11 searching for changes
12 adding changesets
13 adding manifests
14 adding file changes
15 added 1 changesets with 1 changes to 1 files
16 Subject: changeset in test-notify/b: b
17 From: test
18 X-Hg-Notification: changeset 0647d048b600
19 Message-Id:
20 To: baz, foo@bar
21
22 changeset 0647d048b600 in test-notify/b
23 details: test-notify/b?cmd=changeset;node=0647d048b600
24 description:
25 b
26
27 diffs (6 lines):
28
29 diff -r cb9a9f314b8b -r 0647d048b600 a
30 --- a/a Thu Jan 01 00:00:00 1970 +0000
31 +++ b/a Thu Jan 01 00:00:01 1970 +0000
32 @@ -1,1 +1,2 @@ a
33 a
34 +a
35 (run 'hg update' to get a working copy)
9 36 % pull
37 rolling back last transaction
10 38 pulling from ../a
11 39 searching for changes
12 40 adding changesets
13 41 adding manifests
14 42 adding file changes
15 43 added 1 changesets with 1 changes to 1 files
16 44 Subject: b
17 45 From: test@test.com
18 46 X-Hg-Notification: changeset 0647d048b600
19 47 Message-Id:
20 To: foo@bar
48 To: baz@test.com, foo@bar
21 49
22 50 changeset 0647d048b600
23 51 description:
24 52 b
25 53 diffs (6 lines):
26 54
27 55 diff -r cb9a9f314b8b -r 0647d048b600 a
28 56 --- a/a Thu Jan 01 00:00:00 1970 +0000
29 57 +++ b/a Thu Jan 01 00:00:01 1970 +0000
30 58 @@ -1,1 +1,2 @@ a
31 59 a
32 60 +a
33 61 (run 'hg update' to get a working copy)
General Comments 0
You need to be logged in to leave comments. Login now