##// END OF EJS Templates
py3: replace file() with open()...
Pulkit Goyal -
r36412:4bc98356 default
parent child Browse files
Show More
@@ -1,373 +1,373 b''
1 # monotone.py - monotone support for the convert extension
1 # monotone.py - monotone support for the convert extension
2 #
2 #
3 # Copyright 2008, 2009 Mikkel Fahnoe Jorgensen <mikkel@dvide.com> and
3 # Copyright 2008, 2009 Mikkel Fahnoe Jorgensen <mikkel@dvide.com> and
4 # others
4 # others
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import os
10 import os
11 import re
11 import re
12
12
13 from mercurial.i18n import _
13 from mercurial.i18n import _
14 from mercurial import (
14 from mercurial import (
15 error,
15 error,
16 pycompat,
16 pycompat,
17 util,
17 util,
18 )
18 )
19
19
20 from . import common
20 from . import common
21
21
22 class monotone_source(common.converter_source, common.commandline):
22 class monotone_source(common.converter_source, common.commandline):
23 def __init__(self, ui, repotype, path=None, revs=None):
23 def __init__(self, ui, repotype, path=None, revs=None):
24 common.converter_source.__init__(self, ui, repotype, path, revs)
24 common.converter_source.__init__(self, ui, repotype, path, revs)
25 if revs and len(revs) > 1:
25 if revs and len(revs) > 1:
26 raise error.Abort(_('monotone source does not support specifying '
26 raise error.Abort(_('monotone source does not support specifying '
27 'multiple revs'))
27 'multiple revs'))
28 common.commandline.__init__(self, ui, 'mtn')
28 common.commandline.__init__(self, ui, 'mtn')
29
29
30 self.ui = ui
30 self.ui = ui
31 self.path = path
31 self.path = path
32 self.automatestdio = False
32 self.automatestdio = False
33 self.revs = revs
33 self.revs = revs
34
34
35 norepo = common.NoRepo(_("%s does not look like a monotone repository")
35 norepo = common.NoRepo(_("%s does not look like a monotone repository")
36 % path)
36 % path)
37 if not os.path.exists(os.path.join(path, '_MTN')):
37 if not os.path.exists(os.path.join(path, '_MTN')):
38 # Could be a monotone repository (SQLite db file)
38 # Could be a monotone repository (SQLite db file)
39 try:
39 try:
40 f = file(path, 'rb')
40 f = open(path, 'rb')
41 header = f.read(16)
41 header = f.read(16)
42 f.close()
42 f.close()
43 except IOError:
43 except IOError:
44 header = ''
44 header = ''
45 if header != 'SQLite format 3\x00':
45 if header != 'SQLite format 3\x00':
46 raise norepo
46 raise norepo
47
47
48 # regular expressions for parsing monotone output
48 # regular expressions for parsing monotone output
49 space = br'\s*'
49 space = br'\s*'
50 name = br'\s+"((?:\\"|[^"])*)"\s*'
50 name = br'\s+"((?:\\"|[^"])*)"\s*'
51 value = name
51 value = name
52 revision = br'\s+\[(\w+)\]\s*'
52 revision = br'\s+\[(\w+)\]\s*'
53 lines = br'(?:.|\n)+'
53 lines = br'(?:.|\n)+'
54
54
55 self.dir_re = re.compile(space + "dir" + name)
55 self.dir_re = re.compile(space + "dir" + name)
56 self.file_re = re.compile(space + "file" + name +
56 self.file_re = re.compile(space + "file" + name +
57 "content" + revision)
57 "content" + revision)
58 self.add_file_re = re.compile(space + "add_file" + name +
58 self.add_file_re = re.compile(space + "add_file" + name +
59 "content" + revision)
59 "content" + revision)
60 self.patch_re = re.compile(space + "patch" + name +
60 self.patch_re = re.compile(space + "patch" + name +
61 "from" + revision + "to" + revision)
61 "from" + revision + "to" + revision)
62 self.rename_re = re.compile(space + "rename" + name + "to" + name)
62 self.rename_re = re.compile(space + "rename" + name + "to" + name)
63 self.delete_re = re.compile(space + "delete" + name)
63 self.delete_re = re.compile(space + "delete" + name)
64 self.tag_re = re.compile(space + "tag" + name + "revision" +
64 self.tag_re = re.compile(space + "tag" + name + "revision" +
65 revision)
65 revision)
66 self.cert_re = re.compile(lines + space + "name" + name +
66 self.cert_re = re.compile(lines + space + "name" + name +
67 "value" + value)
67 "value" + value)
68
68
69 attr = space + "file" + lines + space + "attr" + space
69 attr = space + "file" + lines + space + "attr" + space
70 self.attr_execute_re = re.compile(attr + '"mtn:execute"' +
70 self.attr_execute_re = re.compile(attr + '"mtn:execute"' +
71 space + '"true"')
71 space + '"true"')
72
72
73 # cached data
73 # cached data
74 self.manifest_rev = None
74 self.manifest_rev = None
75 self.manifest = None
75 self.manifest = None
76 self.files = None
76 self.files = None
77 self.dirs = None
77 self.dirs = None
78
78
79 common.checktool('mtn', abort=False)
79 common.checktool('mtn', abort=False)
80
80
81 def mtnrun(self, *args, **kwargs):
81 def mtnrun(self, *args, **kwargs):
82 if self.automatestdio:
82 if self.automatestdio:
83 return self.mtnrunstdio(*args, **kwargs)
83 return self.mtnrunstdio(*args, **kwargs)
84 else:
84 else:
85 return self.mtnrunsingle(*args, **kwargs)
85 return self.mtnrunsingle(*args, **kwargs)
86
86
87 def mtnrunsingle(self, *args, **kwargs):
87 def mtnrunsingle(self, *args, **kwargs):
88 kwargs[r'd'] = self.path
88 kwargs[r'd'] = self.path
89 return self.run0('automate', *args, **kwargs)
89 return self.run0('automate', *args, **kwargs)
90
90
91 def mtnrunstdio(self, *args, **kwargs):
91 def mtnrunstdio(self, *args, **kwargs):
92 # Prepare the command in automate stdio format
92 # Prepare the command in automate stdio format
93 kwargs = pycompat.byteskwargs(kwargs)
93 kwargs = pycompat.byteskwargs(kwargs)
94 command = []
94 command = []
95 for k, v in kwargs.iteritems():
95 for k, v in kwargs.iteritems():
96 command.append("%s:%s" % (len(k), k))
96 command.append("%s:%s" % (len(k), k))
97 if v:
97 if v:
98 command.append("%s:%s" % (len(v), v))
98 command.append("%s:%s" % (len(v), v))
99 if command:
99 if command:
100 command.insert(0, 'o')
100 command.insert(0, 'o')
101 command.append('e')
101 command.append('e')
102
102
103 command.append('l')
103 command.append('l')
104 for arg in args:
104 for arg in args:
105 command += "%s:%s" % (len(arg), arg)
105 command += "%s:%s" % (len(arg), arg)
106 command.append('e')
106 command.append('e')
107 command = ''.join(command)
107 command = ''.join(command)
108
108
109 self.ui.debug("mtn: sending '%s'\n" % command)
109 self.ui.debug("mtn: sending '%s'\n" % command)
110 self.mtnwritefp.write(command)
110 self.mtnwritefp.write(command)
111 self.mtnwritefp.flush()
111 self.mtnwritefp.flush()
112
112
113 return self.mtnstdioreadcommandoutput(command)
113 return self.mtnstdioreadcommandoutput(command)
114
114
115 def mtnstdioreadpacket(self):
115 def mtnstdioreadpacket(self):
116 read = None
116 read = None
117 commandnbr = ''
117 commandnbr = ''
118 while read != ':':
118 while read != ':':
119 read = self.mtnreadfp.read(1)
119 read = self.mtnreadfp.read(1)
120 if not read:
120 if not read:
121 raise error.Abort(_('bad mtn packet - no end of commandnbr'))
121 raise error.Abort(_('bad mtn packet - no end of commandnbr'))
122 commandnbr += read
122 commandnbr += read
123 commandnbr = commandnbr[:-1]
123 commandnbr = commandnbr[:-1]
124
124
125 stream = self.mtnreadfp.read(1)
125 stream = self.mtnreadfp.read(1)
126 if stream not in 'mewptl':
126 if stream not in 'mewptl':
127 raise error.Abort(_('bad mtn packet - bad stream type %s') % stream)
127 raise error.Abort(_('bad mtn packet - bad stream type %s') % stream)
128
128
129 read = self.mtnreadfp.read(1)
129 read = self.mtnreadfp.read(1)
130 if read != ':':
130 if read != ':':
131 raise error.Abort(_('bad mtn packet - no divider before size'))
131 raise error.Abort(_('bad mtn packet - no divider before size'))
132
132
133 read = None
133 read = None
134 lengthstr = ''
134 lengthstr = ''
135 while read != ':':
135 while read != ':':
136 read = self.mtnreadfp.read(1)
136 read = self.mtnreadfp.read(1)
137 if not read:
137 if not read:
138 raise error.Abort(_('bad mtn packet - no end of packet size'))
138 raise error.Abort(_('bad mtn packet - no end of packet size'))
139 lengthstr += read
139 lengthstr += read
140 try:
140 try:
141 length = long(lengthstr[:-1])
141 length = long(lengthstr[:-1])
142 except TypeError:
142 except TypeError:
143 raise error.Abort(_('bad mtn packet - bad packet size %s')
143 raise error.Abort(_('bad mtn packet - bad packet size %s')
144 % lengthstr)
144 % lengthstr)
145
145
146 read = self.mtnreadfp.read(length)
146 read = self.mtnreadfp.read(length)
147 if len(read) != length:
147 if len(read) != length:
148 raise error.Abort(_("bad mtn packet - unable to read full packet "
148 raise error.Abort(_("bad mtn packet - unable to read full packet "
149 "read %s of %s") % (len(read), length))
149 "read %s of %s") % (len(read), length))
150
150
151 return (commandnbr, stream, length, read)
151 return (commandnbr, stream, length, read)
152
152
153 def mtnstdioreadcommandoutput(self, command):
153 def mtnstdioreadcommandoutput(self, command):
154 retval = []
154 retval = []
155 while True:
155 while True:
156 commandnbr, stream, length, output = self.mtnstdioreadpacket()
156 commandnbr, stream, length, output = self.mtnstdioreadpacket()
157 self.ui.debug('mtn: read packet %s:%s:%s\n' %
157 self.ui.debug('mtn: read packet %s:%s:%s\n' %
158 (commandnbr, stream, length))
158 (commandnbr, stream, length))
159
159
160 if stream == 'l':
160 if stream == 'l':
161 # End of command
161 # End of command
162 if output != '0':
162 if output != '0':
163 raise error.Abort(_("mtn command '%s' returned %s") %
163 raise error.Abort(_("mtn command '%s' returned %s") %
164 (command, output))
164 (command, output))
165 break
165 break
166 elif stream in 'ew':
166 elif stream in 'ew':
167 # Error, warning output
167 # Error, warning output
168 self.ui.warn(_('%s error:\n') % self.command)
168 self.ui.warn(_('%s error:\n') % self.command)
169 self.ui.warn(output)
169 self.ui.warn(output)
170 elif stream == 'p':
170 elif stream == 'p':
171 # Progress messages
171 # Progress messages
172 self.ui.debug('mtn: ' + output)
172 self.ui.debug('mtn: ' + output)
173 elif stream == 'm':
173 elif stream == 'm':
174 # Main stream - command output
174 # Main stream - command output
175 retval.append(output)
175 retval.append(output)
176
176
177 return ''.join(retval)
177 return ''.join(retval)
178
178
179 def mtnloadmanifest(self, rev):
179 def mtnloadmanifest(self, rev):
180 if self.manifest_rev == rev:
180 if self.manifest_rev == rev:
181 return
181 return
182 self.manifest = self.mtnrun("get_manifest_of", rev).split("\n\n")
182 self.manifest = self.mtnrun("get_manifest_of", rev).split("\n\n")
183 self.manifest_rev = rev
183 self.manifest_rev = rev
184 self.files = {}
184 self.files = {}
185 self.dirs = {}
185 self.dirs = {}
186
186
187 for e in self.manifest:
187 for e in self.manifest:
188 m = self.file_re.match(e)
188 m = self.file_re.match(e)
189 if m:
189 if m:
190 attr = ""
190 attr = ""
191 name = m.group(1)
191 name = m.group(1)
192 node = m.group(2)
192 node = m.group(2)
193 if self.attr_execute_re.match(e):
193 if self.attr_execute_re.match(e):
194 attr += "x"
194 attr += "x"
195 self.files[name] = (node, attr)
195 self.files[name] = (node, attr)
196 m = self.dir_re.match(e)
196 m = self.dir_re.match(e)
197 if m:
197 if m:
198 self.dirs[m.group(1)] = True
198 self.dirs[m.group(1)] = True
199
199
200 def mtnisfile(self, name, rev):
200 def mtnisfile(self, name, rev):
201 # a non-file could be a directory or a deleted or renamed file
201 # a non-file could be a directory or a deleted or renamed file
202 self.mtnloadmanifest(rev)
202 self.mtnloadmanifest(rev)
203 return name in self.files
203 return name in self.files
204
204
205 def mtnisdir(self, name, rev):
205 def mtnisdir(self, name, rev):
206 self.mtnloadmanifest(rev)
206 self.mtnloadmanifest(rev)
207 return name in self.dirs
207 return name in self.dirs
208
208
209 def mtngetcerts(self, rev):
209 def mtngetcerts(self, rev):
210 certs = {"author":"<missing>", "date":"<missing>",
210 certs = {"author":"<missing>", "date":"<missing>",
211 "changelog":"<missing>", "branch":"<missing>"}
211 "changelog":"<missing>", "branch":"<missing>"}
212 certlist = self.mtnrun("certs", rev)
212 certlist = self.mtnrun("certs", rev)
213 # mtn < 0.45:
213 # mtn < 0.45:
214 # key "test@selenic.com"
214 # key "test@selenic.com"
215 # mtn >= 0.45:
215 # mtn >= 0.45:
216 # key [ff58a7ffb771907c4ff68995eada1c4da068d328]
216 # key [ff58a7ffb771907c4ff68995eada1c4da068d328]
217 certlist = re.split('\n\n key ["\[]', certlist)
217 certlist = re.split('\n\n key ["\[]', certlist)
218 for e in certlist:
218 for e in certlist:
219 m = self.cert_re.match(e)
219 m = self.cert_re.match(e)
220 if m:
220 if m:
221 name, value = m.groups()
221 name, value = m.groups()
222 value = value.replace(r'\"', '"')
222 value = value.replace(r'\"', '"')
223 value = value.replace(r'\\', '\\')
223 value = value.replace(r'\\', '\\')
224 certs[name] = value
224 certs[name] = value
225 # Monotone may have subsecond dates: 2005-02-05T09:39:12.364306
225 # Monotone may have subsecond dates: 2005-02-05T09:39:12.364306
226 # and all times are stored in UTC
226 # and all times are stored in UTC
227 certs["date"] = certs["date"].split('.')[0] + " UTC"
227 certs["date"] = certs["date"].split('.')[0] + " UTC"
228 return certs
228 return certs
229
229
230 # implement the converter_source interface:
230 # implement the converter_source interface:
231
231
232 def getheads(self):
232 def getheads(self):
233 if not self.revs:
233 if not self.revs:
234 return self.mtnrun("leaves").splitlines()
234 return self.mtnrun("leaves").splitlines()
235 else:
235 else:
236 return self.revs
236 return self.revs
237
237
238 def getchanges(self, rev, full):
238 def getchanges(self, rev, full):
239 if full:
239 if full:
240 raise error.Abort(_("convert from monotone does not support "
240 raise error.Abort(_("convert from monotone does not support "
241 "--full"))
241 "--full"))
242 revision = self.mtnrun("get_revision", rev).split("\n\n")
242 revision = self.mtnrun("get_revision", rev).split("\n\n")
243 files = {}
243 files = {}
244 ignoremove = {}
244 ignoremove = {}
245 renameddirs = []
245 renameddirs = []
246 copies = {}
246 copies = {}
247 for e in revision:
247 for e in revision:
248 m = self.add_file_re.match(e)
248 m = self.add_file_re.match(e)
249 if m:
249 if m:
250 files[m.group(1)] = rev
250 files[m.group(1)] = rev
251 ignoremove[m.group(1)] = rev
251 ignoremove[m.group(1)] = rev
252 m = self.patch_re.match(e)
252 m = self.patch_re.match(e)
253 if m:
253 if m:
254 files[m.group(1)] = rev
254 files[m.group(1)] = rev
255 # Delete/rename is handled later when the convert engine
255 # Delete/rename is handled later when the convert engine
256 # discovers an IOError exception from getfile,
256 # discovers an IOError exception from getfile,
257 # but only if we add the "from" file to the list of changes.
257 # but only if we add the "from" file to the list of changes.
258 m = self.delete_re.match(e)
258 m = self.delete_re.match(e)
259 if m:
259 if m:
260 files[m.group(1)] = rev
260 files[m.group(1)] = rev
261 m = self.rename_re.match(e)
261 m = self.rename_re.match(e)
262 if m:
262 if m:
263 toname = m.group(2)
263 toname = m.group(2)
264 fromname = m.group(1)
264 fromname = m.group(1)
265 if self.mtnisfile(toname, rev):
265 if self.mtnisfile(toname, rev):
266 ignoremove[toname] = 1
266 ignoremove[toname] = 1
267 copies[toname] = fromname
267 copies[toname] = fromname
268 files[toname] = rev
268 files[toname] = rev
269 files[fromname] = rev
269 files[fromname] = rev
270 elif self.mtnisdir(toname, rev):
270 elif self.mtnisdir(toname, rev):
271 renameddirs.append((fromname, toname))
271 renameddirs.append((fromname, toname))
272
272
273 # Directory renames can be handled only once we have recorded
273 # Directory renames can be handled only once we have recorded
274 # all new files
274 # all new files
275 for fromdir, todir in renameddirs:
275 for fromdir, todir in renameddirs:
276 renamed = {}
276 renamed = {}
277 for tofile in self.files:
277 for tofile in self.files:
278 if tofile in ignoremove:
278 if tofile in ignoremove:
279 continue
279 continue
280 if tofile.startswith(todir + '/'):
280 if tofile.startswith(todir + '/'):
281 renamed[tofile] = fromdir + tofile[len(todir):]
281 renamed[tofile] = fromdir + tofile[len(todir):]
282 # Avoid chained moves like:
282 # Avoid chained moves like:
283 # d1(/a) => d3/d1(/a)
283 # d1(/a) => d3/d1(/a)
284 # d2 => d3
284 # d2 => d3
285 ignoremove[tofile] = 1
285 ignoremove[tofile] = 1
286 for tofile, fromfile in renamed.items():
286 for tofile, fromfile in renamed.items():
287 self.ui.debug (_("copying file in renamed directory "
287 self.ui.debug (_("copying file in renamed directory "
288 "from '%s' to '%s'")
288 "from '%s' to '%s'")
289 % (fromfile, tofile), '\n')
289 % (fromfile, tofile), '\n')
290 files[tofile] = rev
290 files[tofile] = rev
291 copies[tofile] = fromfile
291 copies[tofile] = fromfile
292 for fromfile in renamed.values():
292 for fromfile in renamed.values():
293 files[fromfile] = rev
293 files[fromfile] = rev
294
294
295 return (files.items(), copies, set())
295 return (files.items(), copies, set())
296
296
297 def getfile(self, name, rev):
297 def getfile(self, name, rev):
298 if not self.mtnisfile(name, rev):
298 if not self.mtnisfile(name, rev):
299 return None, None
299 return None, None
300 try:
300 try:
301 data = self.mtnrun("get_file_of", name, r=rev)
301 data = self.mtnrun("get_file_of", name, r=rev)
302 except Exception:
302 except Exception:
303 return None, None
303 return None, None
304 self.mtnloadmanifest(rev)
304 self.mtnloadmanifest(rev)
305 node, attr = self.files.get(name, (None, ""))
305 node, attr = self.files.get(name, (None, ""))
306 return data, attr
306 return data, attr
307
307
308 def getcommit(self, rev):
308 def getcommit(self, rev):
309 extra = {}
309 extra = {}
310 certs = self.mtngetcerts(rev)
310 certs = self.mtngetcerts(rev)
311 if certs.get('suspend') == certs["branch"]:
311 if certs.get('suspend') == certs["branch"]:
312 extra['close'] = 1
312 extra['close'] = 1
313 return common.commit(
313 return common.commit(
314 author=certs["author"],
314 author=certs["author"],
315 date=util.datestr(util.strdate(certs["date"], "%Y-%m-%dT%H:%M:%S")),
315 date=util.datestr(util.strdate(certs["date"], "%Y-%m-%dT%H:%M:%S")),
316 desc=certs["changelog"],
316 desc=certs["changelog"],
317 rev=rev,
317 rev=rev,
318 parents=self.mtnrun("parents", rev).splitlines(),
318 parents=self.mtnrun("parents", rev).splitlines(),
319 branch=certs["branch"],
319 branch=certs["branch"],
320 extra=extra)
320 extra=extra)
321
321
322 def gettags(self):
322 def gettags(self):
323 tags = {}
323 tags = {}
324 for e in self.mtnrun("tags").split("\n\n"):
324 for e in self.mtnrun("tags").split("\n\n"):
325 m = self.tag_re.match(e)
325 m = self.tag_re.match(e)
326 if m:
326 if m:
327 tags[m.group(1)] = m.group(2)
327 tags[m.group(1)] = m.group(2)
328 return tags
328 return tags
329
329
330 def getchangedfiles(self, rev, i):
330 def getchangedfiles(self, rev, i):
331 # This function is only needed to support --filemap
331 # This function is only needed to support --filemap
332 # ... and we don't support that
332 # ... and we don't support that
333 raise NotImplementedError
333 raise NotImplementedError
334
334
335 def before(self):
335 def before(self):
336 # Check if we have a new enough version to use automate stdio
336 # Check if we have a new enough version to use automate stdio
337 version = 0.0
337 version = 0.0
338 try:
338 try:
339 versionstr = self.mtnrunsingle("interface_version")
339 versionstr = self.mtnrunsingle("interface_version")
340 version = float(versionstr)
340 version = float(versionstr)
341 except Exception:
341 except Exception:
342 raise error.Abort(_("unable to determine mtn automate interface "
342 raise error.Abort(_("unable to determine mtn automate interface "
343 "version"))
343 "version"))
344
344
345 if version >= 12.0:
345 if version >= 12.0:
346 self.automatestdio = True
346 self.automatestdio = True
347 self.ui.debug("mtn automate version %s - using automate stdio\n" %
347 self.ui.debug("mtn automate version %s - using automate stdio\n" %
348 version)
348 version)
349
349
350 # launch the long-running automate stdio process
350 # launch the long-running automate stdio process
351 self.mtnwritefp, self.mtnreadfp = self._run2('automate', 'stdio',
351 self.mtnwritefp, self.mtnreadfp = self._run2('automate', 'stdio',
352 '-d', self.path)
352 '-d', self.path)
353 # read the headers
353 # read the headers
354 read = self.mtnreadfp.readline()
354 read = self.mtnreadfp.readline()
355 if read != 'format-version: 2\n':
355 if read != 'format-version: 2\n':
356 raise error.Abort(_('mtn automate stdio header unexpected: %s')
356 raise error.Abort(_('mtn automate stdio header unexpected: %s')
357 % read)
357 % read)
358 while read != '\n':
358 while read != '\n':
359 read = self.mtnreadfp.readline()
359 read = self.mtnreadfp.readline()
360 if not read:
360 if not read:
361 raise error.Abort(_("failed to reach end of mtn automate "
361 raise error.Abort(_("failed to reach end of mtn automate "
362 "stdio headers"))
362 "stdio headers"))
363 else:
363 else:
364 self.ui.debug("mtn automate version %s - not using automate stdio "
364 self.ui.debug("mtn automate version %s - not using automate stdio "
365 "(automate >= 12.0 - mtn >= 0.46 is needed)\n" % version)
365 "(automate >= 12.0 - mtn >= 0.46 is needed)\n" % version)
366
366
367 def after(self):
367 def after(self):
368 if self.automatestdio:
368 if self.automatestdio:
369 self.mtnwritefp.close()
369 self.mtnwritefp.close()
370 self.mtnwritefp = None
370 self.mtnwritefp = None
371 self.mtnreadfp.close()
371 self.mtnreadfp.close()
372 self.mtnreadfp = None
372 self.mtnreadfp = None
373
373
@@ -1,1870 +1,1870 b''
1 # rebase.py - rebasing feature for mercurial
1 # rebase.py - rebasing feature for mercurial
2 #
2 #
3 # Copyright 2008 Stefano Tortarolo <stefano.tortarolo at gmail dot com>
3 # Copyright 2008 Stefano Tortarolo <stefano.tortarolo at gmail dot com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 '''command to move sets of revisions to a different ancestor
8 '''command to move sets of revisions to a different ancestor
9
9
10 This extension lets you rebase changesets in an existing Mercurial
10 This extension lets you rebase changesets in an existing Mercurial
11 repository.
11 repository.
12
12
13 For more information:
13 For more information:
14 https://mercurial-scm.org/wiki/RebaseExtension
14 https://mercurial-scm.org/wiki/RebaseExtension
15 '''
15 '''
16
16
17 from __future__ import absolute_import
17 from __future__ import absolute_import
18
18
19 import errno
19 import errno
20 import os
20 import os
21
21
22 from mercurial.i18n import _
22 from mercurial.i18n import _
23 from mercurial.node import (
23 from mercurial.node import (
24 nullid,
24 nullid,
25 nullrev,
25 nullrev,
26 short,
26 short,
27 )
27 )
28 from mercurial import (
28 from mercurial import (
29 bookmarks,
29 bookmarks,
30 cmdutil,
30 cmdutil,
31 commands,
31 commands,
32 copies,
32 copies,
33 destutil,
33 destutil,
34 dirstateguard,
34 dirstateguard,
35 error,
35 error,
36 extensions,
36 extensions,
37 hg,
37 hg,
38 lock,
38 lock,
39 merge as mergemod,
39 merge as mergemod,
40 mergeutil,
40 mergeutil,
41 obsolete,
41 obsolete,
42 obsutil,
42 obsutil,
43 patch,
43 patch,
44 phases,
44 phases,
45 pycompat,
45 pycompat,
46 registrar,
46 registrar,
47 repair,
47 repair,
48 revset,
48 revset,
49 revsetlang,
49 revsetlang,
50 scmutil,
50 scmutil,
51 smartset,
51 smartset,
52 util,
52 util,
53 )
53 )
54
54
55 release = lock.release
55 release = lock.release
56
56
57 # The following constants are used throughout the rebase module. The ordering of
57 # The following constants are used throughout the rebase module. The ordering of
58 # their values must be maintained.
58 # their values must be maintained.
59
59
60 # Indicates that a revision needs to be rebased
60 # Indicates that a revision needs to be rebased
61 revtodo = -1
61 revtodo = -1
62 revtodostr = '-1'
62 revtodostr = '-1'
63
63
64 # legacy revstates no longer needed in current code
64 # legacy revstates no longer needed in current code
65 # -2: nullmerge, -3: revignored, -4: revprecursor, -5: revpruned
65 # -2: nullmerge, -3: revignored, -4: revprecursor, -5: revpruned
66 legacystates = {'-2', '-3', '-4', '-5'}
66 legacystates = {'-2', '-3', '-4', '-5'}
67
67
68 cmdtable = {}
68 cmdtable = {}
69 command = registrar.command(cmdtable)
69 command = registrar.command(cmdtable)
70 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
70 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
71 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
71 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
72 # be specifying the version(s) of Mercurial they are tested with, or
72 # be specifying the version(s) of Mercurial they are tested with, or
73 # leave the attribute unspecified.
73 # leave the attribute unspecified.
74 testedwith = 'ships-with-hg-core'
74 testedwith = 'ships-with-hg-core'
75
75
76 def _nothingtorebase():
76 def _nothingtorebase():
77 return 1
77 return 1
78
78
79 def _savegraft(ctx, extra):
79 def _savegraft(ctx, extra):
80 s = ctx.extra().get('source', None)
80 s = ctx.extra().get('source', None)
81 if s is not None:
81 if s is not None:
82 extra['source'] = s
82 extra['source'] = s
83 s = ctx.extra().get('intermediate-source', None)
83 s = ctx.extra().get('intermediate-source', None)
84 if s is not None:
84 if s is not None:
85 extra['intermediate-source'] = s
85 extra['intermediate-source'] = s
86
86
87 def _savebranch(ctx, extra):
87 def _savebranch(ctx, extra):
88 extra['branch'] = ctx.branch()
88 extra['branch'] = ctx.branch()
89
89
90 def _makeextrafn(copiers):
90 def _makeextrafn(copiers):
91 """make an extrafn out of the given copy-functions.
91 """make an extrafn out of the given copy-functions.
92
92
93 A copy function takes a context and an extra dict, and mutates the
93 A copy function takes a context and an extra dict, and mutates the
94 extra dict as needed based on the given context.
94 extra dict as needed based on the given context.
95 """
95 """
96 def extrafn(ctx, extra):
96 def extrafn(ctx, extra):
97 for c in copiers:
97 for c in copiers:
98 c(ctx, extra)
98 c(ctx, extra)
99 return extrafn
99 return extrafn
100
100
101 def _destrebase(repo, sourceset, destspace=None):
101 def _destrebase(repo, sourceset, destspace=None):
102 """small wrapper around destmerge to pass the right extra args
102 """small wrapper around destmerge to pass the right extra args
103
103
104 Please wrap destutil.destmerge instead."""
104 Please wrap destutil.destmerge instead."""
105 return destutil.destmerge(repo, action='rebase', sourceset=sourceset,
105 return destutil.destmerge(repo, action='rebase', sourceset=sourceset,
106 onheadcheck=False, destspace=destspace)
106 onheadcheck=False, destspace=destspace)
107
107
108 revsetpredicate = registrar.revsetpredicate()
108 revsetpredicate = registrar.revsetpredicate()
109
109
110 @revsetpredicate('_destrebase')
110 @revsetpredicate('_destrebase')
111 def _revsetdestrebase(repo, subset, x):
111 def _revsetdestrebase(repo, subset, x):
112 # ``_rebasedefaultdest()``
112 # ``_rebasedefaultdest()``
113
113
114 # default destination for rebase.
114 # default destination for rebase.
115 # # XXX: Currently private because I expect the signature to change.
115 # # XXX: Currently private because I expect the signature to change.
116 # # XXX: - bailing out in case of ambiguity vs returning all data.
116 # # XXX: - bailing out in case of ambiguity vs returning all data.
117 # i18n: "_rebasedefaultdest" is a keyword
117 # i18n: "_rebasedefaultdest" is a keyword
118 sourceset = None
118 sourceset = None
119 if x is not None:
119 if x is not None:
120 sourceset = revset.getset(repo, smartset.fullreposet(repo), x)
120 sourceset = revset.getset(repo, smartset.fullreposet(repo), x)
121 return subset & smartset.baseset([_destrebase(repo, sourceset)])
121 return subset & smartset.baseset([_destrebase(repo, sourceset)])
122
122
123 def _ctxdesc(ctx):
123 def _ctxdesc(ctx):
124 """short description for a context"""
124 """short description for a context"""
125 desc = '%d:%s "%s"' % (ctx.rev(), ctx,
125 desc = '%d:%s "%s"' % (ctx.rev(), ctx,
126 ctx.description().split('\n', 1)[0])
126 ctx.description().split('\n', 1)[0])
127 repo = ctx.repo()
127 repo = ctx.repo()
128 names = []
128 names = []
129 for nsname, ns in repo.names.iteritems():
129 for nsname, ns in repo.names.iteritems():
130 if nsname == 'branches':
130 if nsname == 'branches':
131 continue
131 continue
132 names.extend(ns.names(repo, ctx.node()))
132 names.extend(ns.names(repo, ctx.node()))
133 if names:
133 if names:
134 desc += ' (%s)' % ' '.join(names)
134 desc += ' (%s)' % ' '.join(names)
135 return desc
135 return desc
136
136
137 class rebaseruntime(object):
137 class rebaseruntime(object):
138 """This class is a container for rebase runtime state"""
138 """This class is a container for rebase runtime state"""
139 def __init__(self, repo, ui, inmemory=False, opts=None):
139 def __init__(self, repo, ui, inmemory=False, opts=None):
140 if opts is None:
140 if opts is None:
141 opts = {}
141 opts = {}
142
142
143 # prepared: whether we have rebasestate prepared or not. Currently it
143 # prepared: whether we have rebasestate prepared or not. Currently it
144 # decides whether "self.repo" is unfiltered or not.
144 # decides whether "self.repo" is unfiltered or not.
145 # The rebasestate has explicit hash to hash instructions not depending
145 # The rebasestate has explicit hash to hash instructions not depending
146 # on visibility. If rebasestate exists (in-memory or on-disk), use
146 # on visibility. If rebasestate exists (in-memory or on-disk), use
147 # unfiltered repo to avoid visibility issues.
147 # unfiltered repo to avoid visibility issues.
148 # Before knowing rebasestate (i.e. when starting a new rebase (not
148 # Before knowing rebasestate (i.e. when starting a new rebase (not
149 # --continue or --abort)), the original repo should be used so
149 # --continue or --abort)), the original repo should be used so
150 # visibility-dependent revsets are correct.
150 # visibility-dependent revsets are correct.
151 self.prepared = False
151 self.prepared = False
152 self._repo = repo
152 self._repo = repo
153
153
154 self.ui = ui
154 self.ui = ui
155 self.opts = opts
155 self.opts = opts
156 self.originalwd = None
156 self.originalwd = None
157 self.external = nullrev
157 self.external = nullrev
158 # Mapping between the old revision id and either what is the new rebased
158 # Mapping between the old revision id and either what is the new rebased
159 # revision or what needs to be done with the old revision. The state
159 # revision or what needs to be done with the old revision. The state
160 # dict will be what contains most of the rebase progress state.
160 # dict will be what contains most of the rebase progress state.
161 self.state = {}
161 self.state = {}
162 self.activebookmark = None
162 self.activebookmark = None
163 self.destmap = {}
163 self.destmap = {}
164 self.skipped = set()
164 self.skipped = set()
165
165
166 self.collapsef = opts.get('collapse', False)
166 self.collapsef = opts.get('collapse', False)
167 self.collapsemsg = cmdutil.logmessage(ui, opts)
167 self.collapsemsg = cmdutil.logmessage(ui, opts)
168 self.date = opts.get('date', None)
168 self.date = opts.get('date', None)
169
169
170 e = opts.get('extrafn') # internal, used by e.g. hgsubversion
170 e = opts.get('extrafn') # internal, used by e.g. hgsubversion
171 self.extrafns = [_savegraft]
171 self.extrafns = [_savegraft]
172 if e:
172 if e:
173 self.extrafns = [e]
173 self.extrafns = [e]
174
174
175 self.keepf = opts.get('keep', False)
175 self.keepf = opts.get('keep', False)
176 self.keepbranchesf = opts.get('keepbranches', False)
176 self.keepbranchesf = opts.get('keepbranches', False)
177 # keepopen is not meant for use on the command line, but by
177 # keepopen is not meant for use on the command line, but by
178 # other extensions
178 # other extensions
179 self.keepopen = opts.get('keepopen', False)
179 self.keepopen = opts.get('keepopen', False)
180 self.obsoletenotrebased = {}
180 self.obsoletenotrebased = {}
181 self.obsoletewithoutsuccessorindestination = set()
181 self.obsoletewithoutsuccessorindestination = set()
182 self.inmemory = inmemory
182 self.inmemory = inmemory
183
183
184 @property
184 @property
185 def repo(self):
185 def repo(self):
186 if self.prepared:
186 if self.prepared:
187 return self._repo.unfiltered()
187 return self._repo.unfiltered()
188 else:
188 else:
189 return self._repo
189 return self._repo
190
190
191 def storestatus(self, tr=None):
191 def storestatus(self, tr=None):
192 """Store the current status to allow recovery"""
192 """Store the current status to allow recovery"""
193 if tr:
193 if tr:
194 tr.addfilegenerator('rebasestate', ('rebasestate',),
194 tr.addfilegenerator('rebasestate', ('rebasestate',),
195 self._writestatus, location='plain')
195 self._writestatus, location='plain')
196 else:
196 else:
197 with self.repo.vfs("rebasestate", "w") as f:
197 with self.repo.vfs("rebasestate", "w") as f:
198 self._writestatus(f)
198 self._writestatus(f)
199
199
200 def _writestatus(self, f):
200 def _writestatus(self, f):
201 repo = self.repo
201 repo = self.repo
202 assert repo.filtername is None
202 assert repo.filtername is None
203 f.write(repo[self.originalwd].hex() + '\n')
203 f.write(repo[self.originalwd].hex() + '\n')
204 # was "dest". we now write dest per src root below.
204 # was "dest". we now write dest per src root below.
205 f.write('\n')
205 f.write('\n')
206 f.write(repo[self.external].hex() + '\n')
206 f.write(repo[self.external].hex() + '\n')
207 f.write('%d\n' % int(self.collapsef))
207 f.write('%d\n' % int(self.collapsef))
208 f.write('%d\n' % int(self.keepf))
208 f.write('%d\n' % int(self.keepf))
209 f.write('%d\n' % int(self.keepbranchesf))
209 f.write('%d\n' % int(self.keepbranchesf))
210 f.write('%s\n' % (self.activebookmark or ''))
210 f.write('%s\n' % (self.activebookmark or ''))
211 destmap = self.destmap
211 destmap = self.destmap
212 for d, v in self.state.iteritems():
212 for d, v in self.state.iteritems():
213 oldrev = repo[d].hex()
213 oldrev = repo[d].hex()
214 if v >= 0:
214 if v >= 0:
215 newrev = repo[v].hex()
215 newrev = repo[v].hex()
216 else:
216 else:
217 newrev = "%d" % v
217 newrev = "%d" % v
218 destnode = repo[destmap[d]].hex()
218 destnode = repo[destmap[d]].hex()
219 f.write("%s:%s:%s\n" % (oldrev, newrev, destnode))
219 f.write("%s:%s:%s\n" % (oldrev, newrev, destnode))
220 repo.ui.debug('rebase status stored\n')
220 repo.ui.debug('rebase status stored\n')
221
221
222 def restorestatus(self):
222 def restorestatus(self):
223 """Restore a previously stored status"""
223 """Restore a previously stored status"""
224 self.prepared = True
224 self.prepared = True
225 repo = self.repo
225 repo = self.repo
226 assert repo.filtername is None
226 assert repo.filtername is None
227 keepbranches = None
227 keepbranches = None
228 legacydest = None
228 legacydest = None
229 collapse = False
229 collapse = False
230 external = nullrev
230 external = nullrev
231 activebookmark = None
231 activebookmark = None
232 state = {}
232 state = {}
233 destmap = {}
233 destmap = {}
234
234
235 try:
235 try:
236 f = repo.vfs("rebasestate")
236 f = repo.vfs("rebasestate")
237 for i, l in enumerate(f.read().splitlines()):
237 for i, l in enumerate(f.read().splitlines()):
238 if i == 0:
238 if i == 0:
239 originalwd = repo[l].rev()
239 originalwd = repo[l].rev()
240 elif i == 1:
240 elif i == 1:
241 # this line should be empty in newer version. but legacy
241 # this line should be empty in newer version. but legacy
242 # clients may still use it
242 # clients may still use it
243 if l:
243 if l:
244 legacydest = repo[l].rev()
244 legacydest = repo[l].rev()
245 elif i == 2:
245 elif i == 2:
246 external = repo[l].rev()
246 external = repo[l].rev()
247 elif i == 3:
247 elif i == 3:
248 collapse = bool(int(l))
248 collapse = bool(int(l))
249 elif i == 4:
249 elif i == 4:
250 keep = bool(int(l))
250 keep = bool(int(l))
251 elif i == 5:
251 elif i == 5:
252 keepbranches = bool(int(l))
252 keepbranches = bool(int(l))
253 elif i == 6 and not (len(l) == 81 and ':' in l):
253 elif i == 6 and not (len(l) == 81 and ':' in l):
254 # line 6 is a recent addition, so for backwards
254 # line 6 is a recent addition, so for backwards
255 # compatibility check that the line doesn't look like the
255 # compatibility check that the line doesn't look like the
256 # oldrev:newrev lines
256 # oldrev:newrev lines
257 activebookmark = l
257 activebookmark = l
258 else:
258 else:
259 args = l.split(':')
259 args = l.split(':')
260 oldrev = args[0]
260 oldrev = args[0]
261 newrev = args[1]
261 newrev = args[1]
262 if newrev in legacystates:
262 if newrev in legacystates:
263 continue
263 continue
264 if len(args) > 2:
264 if len(args) > 2:
265 destnode = args[2]
265 destnode = args[2]
266 else:
266 else:
267 destnode = legacydest
267 destnode = legacydest
268 destmap[repo[oldrev].rev()] = repo[destnode].rev()
268 destmap[repo[oldrev].rev()] = repo[destnode].rev()
269 if newrev in (nullid, revtodostr):
269 if newrev in (nullid, revtodostr):
270 state[repo[oldrev].rev()] = revtodo
270 state[repo[oldrev].rev()] = revtodo
271 # Legacy compat special case
271 # Legacy compat special case
272 else:
272 else:
273 state[repo[oldrev].rev()] = repo[newrev].rev()
273 state[repo[oldrev].rev()] = repo[newrev].rev()
274
274
275 except IOError as err:
275 except IOError as err:
276 if err.errno != errno.ENOENT:
276 if err.errno != errno.ENOENT:
277 raise
277 raise
278 cmdutil.wrongtooltocontinue(repo, _('rebase'))
278 cmdutil.wrongtooltocontinue(repo, _('rebase'))
279
279
280 if keepbranches is None:
280 if keepbranches is None:
281 raise error.Abort(_('.hg/rebasestate is incomplete'))
281 raise error.Abort(_('.hg/rebasestate is incomplete'))
282
282
283 skipped = set()
283 skipped = set()
284 # recompute the set of skipped revs
284 # recompute the set of skipped revs
285 if not collapse:
285 if not collapse:
286 seen = set(destmap.values())
286 seen = set(destmap.values())
287 for old, new in sorted(state.items()):
287 for old, new in sorted(state.items()):
288 if new != revtodo and new in seen:
288 if new != revtodo and new in seen:
289 skipped.add(old)
289 skipped.add(old)
290 seen.add(new)
290 seen.add(new)
291 repo.ui.debug('computed skipped revs: %s\n' %
291 repo.ui.debug('computed skipped revs: %s\n' %
292 (' '.join('%d' % r for r in sorted(skipped)) or ''))
292 (' '.join('%d' % r for r in sorted(skipped)) or ''))
293 repo.ui.debug('rebase status resumed\n')
293 repo.ui.debug('rebase status resumed\n')
294
294
295 self.originalwd = originalwd
295 self.originalwd = originalwd
296 self.destmap = destmap
296 self.destmap = destmap
297 self.state = state
297 self.state = state
298 self.skipped = skipped
298 self.skipped = skipped
299 self.collapsef = collapse
299 self.collapsef = collapse
300 self.keepf = keep
300 self.keepf = keep
301 self.keepbranchesf = keepbranches
301 self.keepbranchesf = keepbranches
302 self.external = external
302 self.external = external
303 self.activebookmark = activebookmark
303 self.activebookmark = activebookmark
304
304
305 def _handleskippingobsolete(self, obsoleterevs, destmap):
305 def _handleskippingobsolete(self, obsoleterevs, destmap):
306 """Compute structures necessary for skipping obsolete revisions
306 """Compute structures necessary for skipping obsolete revisions
307
307
308 obsoleterevs: iterable of all obsolete revisions in rebaseset
308 obsoleterevs: iterable of all obsolete revisions in rebaseset
309 destmap: {srcrev: destrev} destination revisions
309 destmap: {srcrev: destrev} destination revisions
310 """
310 """
311 self.obsoletenotrebased = {}
311 self.obsoletenotrebased = {}
312 if not self.ui.configbool('experimental', 'rebaseskipobsolete'):
312 if not self.ui.configbool('experimental', 'rebaseskipobsolete'):
313 return
313 return
314 obsoleteset = set(obsoleterevs)
314 obsoleteset = set(obsoleterevs)
315 (self.obsoletenotrebased,
315 (self.obsoletenotrebased,
316 self.obsoletewithoutsuccessorindestination,
316 self.obsoletewithoutsuccessorindestination,
317 obsoleteextinctsuccessors) = _computeobsoletenotrebased(
317 obsoleteextinctsuccessors) = _computeobsoletenotrebased(
318 self.repo, obsoleteset, destmap)
318 self.repo, obsoleteset, destmap)
319 skippedset = set(self.obsoletenotrebased)
319 skippedset = set(self.obsoletenotrebased)
320 skippedset.update(self.obsoletewithoutsuccessorindestination)
320 skippedset.update(self.obsoletewithoutsuccessorindestination)
321 skippedset.update(obsoleteextinctsuccessors)
321 skippedset.update(obsoleteextinctsuccessors)
322 _checkobsrebase(self.repo, self.ui, obsoleteset, skippedset)
322 _checkobsrebase(self.repo, self.ui, obsoleteset, skippedset)
323
323
324 def _prepareabortorcontinue(self, isabort):
324 def _prepareabortorcontinue(self, isabort):
325 try:
325 try:
326 self.restorestatus()
326 self.restorestatus()
327 self.collapsemsg = restorecollapsemsg(self.repo, isabort)
327 self.collapsemsg = restorecollapsemsg(self.repo, isabort)
328 except error.RepoLookupError:
328 except error.RepoLookupError:
329 if isabort:
329 if isabort:
330 clearstatus(self.repo)
330 clearstatus(self.repo)
331 clearcollapsemsg(self.repo)
331 clearcollapsemsg(self.repo)
332 self.repo.ui.warn(_('rebase aborted (no revision is removed,'
332 self.repo.ui.warn(_('rebase aborted (no revision is removed,'
333 ' only broken state is cleared)\n'))
333 ' only broken state is cleared)\n'))
334 return 0
334 return 0
335 else:
335 else:
336 msg = _('cannot continue inconsistent rebase')
336 msg = _('cannot continue inconsistent rebase')
337 hint = _('use "hg rebase --abort" to clear broken state')
337 hint = _('use "hg rebase --abort" to clear broken state')
338 raise error.Abort(msg, hint=hint)
338 raise error.Abort(msg, hint=hint)
339 if isabort:
339 if isabort:
340 return abort(self.repo, self.originalwd, self.destmap,
340 return abort(self.repo, self.originalwd, self.destmap,
341 self.state, activebookmark=self.activebookmark)
341 self.state, activebookmark=self.activebookmark)
342
342
343 def _preparenewrebase(self, destmap):
343 def _preparenewrebase(self, destmap):
344 if not destmap:
344 if not destmap:
345 return _nothingtorebase()
345 return _nothingtorebase()
346
346
347 rebaseset = destmap.keys()
347 rebaseset = destmap.keys()
348 allowunstable = obsolete.isenabled(self.repo, obsolete.allowunstableopt)
348 allowunstable = obsolete.isenabled(self.repo, obsolete.allowunstableopt)
349 if (not (self.keepf or allowunstable)
349 if (not (self.keepf or allowunstable)
350 and self.repo.revs('first(children(%ld) - %ld)',
350 and self.repo.revs('first(children(%ld) - %ld)',
351 rebaseset, rebaseset)):
351 rebaseset, rebaseset)):
352 raise error.Abort(
352 raise error.Abort(
353 _("can't remove original changesets with"
353 _("can't remove original changesets with"
354 " unrebased descendants"),
354 " unrebased descendants"),
355 hint=_('use --keep to keep original changesets'))
355 hint=_('use --keep to keep original changesets'))
356
356
357 result = buildstate(self.repo, destmap, self.collapsef)
357 result = buildstate(self.repo, destmap, self.collapsef)
358
358
359 if not result:
359 if not result:
360 # Empty state built, nothing to rebase
360 # Empty state built, nothing to rebase
361 self.ui.status(_('nothing to rebase\n'))
361 self.ui.status(_('nothing to rebase\n'))
362 return _nothingtorebase()
362 return _nothingtorebase()
363
363
364 for root in self.repo.set('roots(%ld)', rebaseset):
364 for root in self.repo.set('roots(%ld)', rebaseset):
365 if not self.keepf and not root.mutable():
365 if not self.keepf and not root.mutable():
366 raise error.Abort(_("can't rebase public changeset %s")
366 raise error.Abort(_("can't rebase public changeset %s")
367 % root,
367 % root,
368 hint=_("see 'hg help phases' for details"))
368 hint=_("see 'hg help phases' for details"))
369
369
370 (self.originalwd, self.destmap, self.state) = result
370 (self.originalwd, self.destmap, self.state) = result
371 if self.collapsef:
371 if self.collapsef:
372 dests = set(self.destmap.values())
372 dests = set(self.destmap.values())
373 if len(dests) != 1:
373 if len(dests) != 1:
374 raise error.Abort(
374 raise error.Abort(
375 _('--collapse does not work with multiple destinations'))
375 _('--collapse does not work with multiple destinations'))
376 destrev = next(iter(dests))
376 destrev = next(iter(dests))
377 destancestors = self.repo.changelog.ancestors([destrev],
377 destancestors = self.repo.changelog.ancestors([destrev],
378 inclusive=True)
378 inclusive=True)
379 self.external = externalparent(self.repo, self.state, destancestors)
379 self.external = externalparent(self.repo, self.state, destancestors)
380
380
381 for destrev in sorted(set(destmap.values())):
381 for destrev in sorted(set(destmap.values())):
382 dest = self.repo[destrev]
382 dest = self.repo[destrev]
383 if dest.closesbranch() and not self.keepbranchesf:
383 if dest.closesbranch() and not self.keepbranchesf:
384 self.ui.status(_('reopening closed branch head %s\n') % dest)
384 self.ui.status(_('reopening closed branch head %s\n') % dest)
385
385
386 self.prepared = True
386 self.prepared = True
387
387
388 def _assignworkingcopy(self):
388 def _assignworkingcopy(self):
389 if self.inmemory:
389 if self.inmemory:
390 from mercurial.context import overlayworkingctx
390 from mercurial.context import overlayworkingctx
391 self.wctx = overlayworkingctx(self.repo)
391 self.wctx = overlayworkingctx(self.repo)
392 self.repo.ui.debug("rebasing in-memory\n")
392 self.repo.ui.debug("rebasing in-memory\n")
393 else:
393 else:
394 self.wctx = self.repo[None]
394 self.wctx = self.repo[None]
395 self.repo.ui.debug("rebasing on disk\n")
395 self.repo.ui.debug("rebasing on disk\n")
396 self.repo.ui.log("rebase", "", rebase_imm_used=self.wctx.isinmemory())
396 self.repo.ui.log("rebase", "", rebase_imm_used=self.wctx.isinmemory())
397
397
398 def _performrebase(self, tr):
398 def _performrebase(self, tr):
399 self._assignworkingcopy()
399 self._assignworkingcopy()
400 repo, ui = self.repo, self.ui
400 repo, ui = self.repo, self.ui
401 if self.keepbranchesf:
401 if self.keepbranchesf:
402 # insert _savebranch at the start of extrafns so if
402 # insert _savebranch at the start of extrafns so if
403 # there's a user-provided extrafn it can clobber branch if
403 # there's a user-provided extrafn it can clobber branch if
404 # desired
404 # desired
405 self.extrafns.insert(0, _savebranch)
405 self.extrafns.insert(0, _savebranch)
406 if self.collapsef:
406 if self.collapsef:
407 branches = set()
407 branches = set()
408 for rev in self.state:
408 for rev in self.state:
409 branches.add(repo[rev].branch())
409 branches.add(repo[rev].branch())
410 if len(branches) > 1:
410 if len(branches) > 1:
411 raise error.Abort(_('cannot collapse multiple named '
411 raise error.Abort(_('cannot collapse multiple named '
412 'branches'))
412 'branches'))
413
413
414 # Calculate self.obsoletenotrebased
414 # Calculate self.obsoletenotrebased
415 obsrevs = _filterobsoleterevs(self.repo, self.state)
415 obsrevs = _filterobsoleterevs(self.repo, self.state)
416 self._handleskippingobsolete(obsrevs, self.destmap)
416 self._handleskippingobsolete(obsrevs, self.destmap)
417
417
418 # Keep track of the active bookmarks in order to reset them later
418 # Keep track of the active bookmarks in order to reset them later
419 self.activebookmark = self.activebookmark or repo._activebookmark
419 self.activebookmark = self.activebookmark or repo._activebookmark
420 if self.activebookmark:
420 if self.activebookmark:
421 bookmarks.deactivate(repo)
421 bookmarks.deactivate(repo)
422
422
423 # Store the state before we begin so users can run 'hg rebase --abort'
423 # Store the state before we begin so users can run 'hg rebase --abort'
424 # if we fail before the transaction closes.
424 # if we fail before the transaction closes.
425 self.storestatus()
425 self.storestatus()
426
426
427 cands = [k for k, v in self.state.iteritems() if v == revtodo]
427 cands = [k for k, v in self.state.iteritems() if v == revtodo]
428 total = len(cands)
428 total = len(cands)
429 pos = 0
429 pos = 0
430 for subset in sortsource(self.destmap):
430 for subset in sortsource(self.destmap):
431 pos = self._performrebasesubset(tr, subset, pos, total)
431 pos = self._performrebasesubset(tr, subset, pos, total)
432 ui.progress(_('rebasing'), None)
432 ui.progress(_('rebasing'), None)
433 ui.note(_('rebase merging completed\n'))
433 ui.note(_('rebase merging completed\n'))
434
434
435 def _performrebasesubset(self, tr, subset, pos, total):
435 def _performrebasesubset(self, tr, subset, pos, total):
436 repo, ui, opts = self.repo, self.ui, self.opts
436 repo, ui, opts = self.repo, self.ui, self.opts
437 sortedrevs = repo.revs('sort(%ld, -topo)', subset)
437 sortedrevs = repo.revs('sort(%ld, -topo)', subset)
438 allowdivergence = self.ui.configbool(
438 allowdivergence = self.ui.configbool(
439 'experimental', 'evolution.allowdivergence')
439 'experimental', 'evolution.allowdivergence')
440 if not allowdivergence:
440 if not allowdivergence:
441 sortedrevs -= repo.revs(
441 sortedrevs -= repo.revs(
442 'descendants(%ld) and not %ld',
442 'descendants(%ld) and not %ld',
443 self.obsoletewithoutsuccessorindestination,
443 self.obsoletewithoutsuccessorindestination,
444 self.obsoletewithoutsuccessorindestination,
444 self.obsoletewithoutsuccessorindestination,
445 )
445 )
446 for rev in sortedrevs:
446 for rev in sortedrevs:
447 dest = self.destmap[rev]
447 dest = self.destmap[rev]
448 ctx = repo[rev]
448 ctx = repo[rev]
449 desc = _ctxdesc(ctx)
449 desc = _ctxdesc(ctx)
450 if self.state[rev] == rev:
450 if self.state[rev] == rev:
451 ui.status(_('already rebased %s\n') % desc)
451 ui.status(_('already rebased %s\n') % desc)
452 elif (not allowdivergence
452 elif (not allowdivergence
453 and rev in self.obsoletewithoutsuccessorindestination):
453 and rev in self.obsoletewithoutsuccessorindestination):
454 msg = _('note: not rebasing %s and its descendants as '
454 msg = _('note: not rebasing %s and its descendants as '
455 'this would cause divergence\n') % desc
455 'this would cause divergence\n') % desc
456 repo.ui.status(msg)
456 repo.ui.status(msg)
457 self.skipped.add(rev)
457 self.skipped.add(rev)
458 elif rev in self.obsoletenotrebased:
458 elif rev in self.obsoletenotrebased:
459 succ = self.obsoletenotrebased[rev]
459 succ = self.obsoletenotrebased[rev]
460 if succ is None:
460 if succ is None:
461 msg = _('note: not rebasing %s, it has no '
461 msg = _('note: not rebasing %s, it has no '
462 'successor\n') % desc
462 'successor\n') % desc
463 else:
463 else:
464 succdesc = _ctxdesc(repo[succ])
464 succdesc = _ctxdesc(repo[succ])
465 msg = (_('note: not rebasing %s, already in '
465 msg = (_('note: not rebasing %s, already in '
466 'destination as %s\n') % (desc, succdesc))
466 'destination as %s\n') % (desc, succdesc))
467 repo.ui.status(msg)
467 repo.ui.status(msg)
468 # Make clearrebased aware state[rev] is not a true successor
468 # Make clearrebased aware state[rev] is not a true successor
469 self.skipped.add(rev)
469 self.skipped.add(rev)
470 # Record rev as moved to its desired destination in self.state.
470 # Record rev as moved to its desired destination in self.state.
471 # This helps bookmark and working parent movement.
471 # This helps bookmark and working parent movement.
472 dest = max(adjustdest(repo, rev, self.destmap, self.state,
472 dest = max(adjustdest(repo, rev, self.destmap, self.state,
473 self.skipped))
473 self.skipped))
474 self.state[rev] = dest
474 self.state[rev] = dest
475 elif self.state[rev] == revtodo:
475 elif self.state[rev] == revtodo:
476 pos += 1
476 pos += 1
477 ui.status(_('rebasing %s\n') % desc)
477 ui.status(_('rebasing %s\n') % desc)
478 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, ctx)),
478 ui.progress(_("rebasing"), pos, ("%d:%s" % (rev, ctx)),
479 _('changesets'), total)
479 _('changesets'), total)
480 p1, p2, base = defineparents(repo, rev, self.destmap,
480 p1, p2, base = defineparents(repo, rev, self.destmap,
481 self.state, self.skipped,
481 self.state, self.skipped,
482 self.obsoletenotrebased)
482 self.obsoletenotrebased)
483 self.storestatus(tr=tr)
483 self.storestatus(tr=tr)
484 storecollapsemsg(repo, self.collapsemsg)
484 storecollapsemsg(repo, self.collapsemsg)
485 if len(repo[None].parents()) == 2:
485 if len(repo[None].parents()) == 2:
486 repo.ui.debug('resuming interrupted rebase\n')
486 repo.ui.debug('resuming interrupted rebase\n')
487 else:
487 else:
488 try:
488 try:
489 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
489 ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
490 'rebase')
490 'rebase')
491 stats = rebasenode(repo, rev, p1, base, self.state,
491 stats = rebasenode(repo, rev, p1, base, self.state,
492 self.collapsef, dest, wctx=self.wctx)
492 self.collapsef, dest, wctx=self.wctx)
493 if stats and stats[3] > 0:
493 if stats and stats[3] > 0:
494 if self.wctx.isinmemory():
494 if self.wctx.isinmemory():
495 raise error.InMemoryMergeConflictsError()
495 raise error.InMemoryMergeConflictsError()
496 else:
496 else:
497 raise error.InterventionRequired(
497 raise error.InterventionRequired(
498 _('unresolved conflicts (see hg '
498 _('unresolved conflicts (see hg '
499 'resolve, then hg rebase --continue)'))
499 'resolve, then hg rebase --continue)'))
500 finally:
500 finally:
501 ui.setconfig('ui', 'forcemerge', '', 'rebase')
501 ui.setconfig('ui', 'forcemerge', '', 'rebase')
502 if not self.collapsef:
502 if not self.collapsef:
503 merging = p2 != nullrev
503 merging = p2 != nullrev
504 editform = cmdutil.mergeeditform(merging, 'rebase')
504 editform = cmdutil.mergeeditform(merging, 'rebase')
505 editor = cmdutil.getcommiteditor(editform=editform,
505 editor = cmdutil.getcommiteditor(editform=editform,
506 **pycompat.strkwargs(opts))
506 **pycompat.strkwargs(opts))
507 if self.wctx.isinmemory():
507 if self.wctx.isinmemory():
508 newnode = concludememorynode(repo, rev, p1, p2,
508 newnode = concludememorynode(repo, rev, p1, p2,
509 wctx=self.wctx,
509 wctx=self.wctx,
510 extrafn=_makeextrafn(self.extrafns),
510 extrafn=_makeextrafn(self.extrafns),
511 editor=editor,
511 editor=editor,
512 keepbranches=self.keepbranchesf,
512 keepbranches=self.keepbranchesf,
513 date=self.date)
513 date=self.date)
514 mergemod.mergestate.clean(repo)
514 mergemod.mergestate.clean(repo)
515 else:
515 else:
516 newnode = concludenode(repo, rev, p1, p2,
516 newnode = concludenode(repo, rev, p1, p2,
517 extrafn=_makeextrafn(self.extrafns),
517 extrafn=_makeextrafn(self.extrafns),
518 editor=editor,
518 editor=editor,
519 keepbranches=self.keepbranchesf,
519 keepbranches=self.keepbranchesf,
520 date=self.date)
520 date=self.date)
521
521
522 if newnode is None:
522 if newnode is None:
523 # If it ended up being a no-op commit, then the normal
523 # If it ended up being a no-op commit, then the normal
524 # merge state clean-up path doesn't happen, so do it
524 # merge state clean-up path doesn't happen, so do it
525 # here. Fix issue5494
525 # here. Fix issue5494
526 mergemod.mergestate.clean(repo)
526 mergemod.mergestate.clean(repo)
527 else:
527 else:
528 # Skip commit if we are collapsing
528 # Skip commit if we are collapsing
529 if self.wctx.isinmemory():
529 if self.wctx.isinmemory():
530 self.wctx.setbase(repo[p1])
530 self.wctx.setbase(repo[p1])
531 else:
531 else:
532 repo.setparents(repo[p1].node())
532 repo.setparents(repo[p1].node())
533 newnode = None
533 newnode = None
534 # Update the state
534 # Update the state
535 if newnode is not None:
535 if newnode is not None:
536 self.state[rev] = repo[newnode].rev()
536 self.state[rev] = repo[newnode].rev()
537 ui.debug('rebased as %s\n' % short(newnode))
537 ui.debug('rebased as %s\n' % short(newnode))
538 else:
538 else:
539 if not self.collapsef:
539 if not self.collapsef:
540 ui.warn(_('note: rebase of %d:%s created no changes '
540 ui.warn(_('note: rebase of %d:%s created no changes '
541 'to commit\n') % (rev, ctx))
541 'to commit\n') % (rev, ctx))
542 self.skipped.add(rev)
542 self.skipped.add(rev)
543 self.state[rev] = p1
543 self.state[rev] = p1
544 ui.debug('next revision set to %d\n' % p1)
544 ui.debug('next revision set to %d\n' % p1)
545 else:
545 else:
546 ui.status(_('already rebased %s as %s\n') %
546 ui.status(_('already rebased %s as %s\n') %
547 (desc, repo[self.state[rev]]))
547 (desc, repo[self.state[rev]]))
548 return pos
548 return pos
549
549
550 def _finishrebase(self):
550 def _finishrebase(self):
551 repo, ui, opts = self.repo, self.ui, self.opts
551 repo, ui, opts = self.repo, self.ui, self.opts
552 fm = ui.formatter('rebase', opts)
552 fm = ui.formatter('rebase', opts)
553 fm.startitem()
553 fm.startitem()
554 if self.collapsef and not self.keepopen:
554 if self.collapsef and not self.keepopen:
555 p1, p2, _base = defineparents(repo, min(self.state), self.destmap,
555 p1, p2, _base = defineparents(repo, min(self.state), self.destmap,
556 self.state, self.skipped,
556 self.state, self.skipped,
557 self.obsoletenotrebased)
557 self.obsoletenotrebased)
558 editopt = opts.get('edit')
558 editopt = opts.get('edit')
559 editform = 'rebase.collapse'
559 editform = 'rebase.collapse'
560 if self.collapsemsg:
560 if self.collapsemsg:
561 commitmsg = self.collapsemsg
561 commitmsg = self.collapsemsg
562 else:
562 else:
563 commitmsg = 'Collapsed revision'
563 commitmsg = 'Collapsed revision'
564 for rebased in sorted(self.state):
564 for rebased in sorted(self.state):
565 if rebased not in self.skipped:
565 if rebased not in self.skipped:
566 commitmsg += '\n* %s' % repo[rebased].description()
566 commitmsg += '\n* %s' % repo[rebased].description()
567 editopt = True
567 editopt = True
568 editor = cmdutil.getcommiteditor(edit=editopt, editform=editform)
568 editor = cmdutil.getcommiteditor(edit=editopt, editform=editform)
569 revtoreuse = max(self.state)
569 revtoreuse = max(self.state)
570
570
571 dsguard = None
571 dsguard = None
572 if self.inmemory:
572 if self.inmemory:
573 newnode = concludememorynode(repo, revtoreuse, p1,
573 newnode = concludememorynode(repo, revtoreuse, p1,
574 self.external,
574 self.external,
575 commitmsg=commitmsg,
575 commitmsg=commitmsg,
576 extrafn=_makeextrafn(self.extrafns),
576 extrafn=_makeextrafn(self.extrafns),
577 editor=editor,
577 editor=editor,
578 keepbranches=self.keepbranchesf,
578 keepbranches=self.keepbranchesf,
579 date=self.date, wctx=self.wctx)
579 date=self.date, wctx=self.wctx)
580 else:
580 else:
581 if ui.configbool('rebase', 'singletransaction'):
581 if ui.configbool('rebase', 'singletransaction'):
582 dsguard = dirstateguard.dirstateguard(repo, 'rebase')
582 dsguard = dirstateguard.dirstateguard(repo, 'rebase')
583 with util.acceptintervention(dsguard):
583 with util.acceptintervention(dsguard):
584 newnode = concludenode(repo, revtoreuse, p1, self.external,
584 newnode = concludenode(repo, revtoreuse, p1, self.external,
585 commitmsg=commitmsg,
585 commitmsg=commitmsg,
586 extrafn=_makeextrafn(self.extrafns),
586 extrafn=_makeextrafn(self.extrafns),
587 editor=editor,
587 editor=editor,
588 keepbranches=self.keepbranchesf,
588 keepbranches=self.keepbranchesf,
589 date=self.date)
589 date=self.date)
590 if newnode is not None:
590 if newnode is not None:
591 newrev = repo[newnode].rev()
591 newrev = repo[newnode].rev()
592 for oldrev in self.state:
592 for oldrev in self.state:
593 self.state[oldrev] = newrev
593 self.state[oldrev] = newrev
594
594
595 if 'qtip' in repo.tags():
595 if 'qtip' in repo.tags():
596 updatemq(repo, self.state, self.skipped, **opts)
596 updatemq(repo, self.state, self.skipped, **opts)
597
597
598 # restore original working directory
598 # restore original working directory
599 # (we do this before stripping)
599 # (we do this before stripping)
600 newwd = self.state.get(self.originalwd, self.originalwd)
600 newwd = self.state.get(self.originalwd, self.originalwd)
601 if newwd < 0:
601 if newwd < 0:
602 # original directory is a parent of rebase set root or ignored
602 # original directory is a parent of rebase set root or ignored
603 newwd = self.originalwd
603 newwd = self.originalwd
604 if (newwd not in [c.rev() for c in repo[None].parents()] and
604 if (newwd not in [c.rev() for c in repo[None].parents()] and
605 not self.inmemory):
605 not self.inmemory):
606 ui.note(_("update back to initial working directory parent\n"))
606 ui.note(_("update back to initial working directory parent\n"))
607 hg.updaterepo(repo, newwd, False)
607 hg.updaterepo(repo, newwd, False)
608
608
609 collapsedas = None
609 collapsedas = None
610 if not self.keepf:
610 if not self.keepf:
611 if self.collapsef:
611 if self.collapsef:
612 collapsedas = newnode
612 collapsedas = newnode
613 clearrebased(ui, repo, self.destmap, self.state, self.skipped,
613 clearrebased(ui, repo, self.destmap, self.state, self.skipped,
614 collapsedas, self.keepf, fm=fm)
614 collapsedas, self.keepf, fm=fm)
615
615
616 clearstatus(repo)
616 clearstatus(repo)
617 clearcollapsemsg(repo)
617 clearcollapsemsg(repo)
618
618
619 ui.note(_("rebase completed\n"))
619 ui.note(_("rebase completed\n"))
620 util.unlinkpath(repo.sjoin('undo'), ignoremissing=True)
620 util.unlinkpath(repo.sjoin('undo'), ignoremissing=True)
621 if self.skipped:
621 if self.skipped:
622 skippedlen = len(self.skipped)
622 skippedlen = len(self.skipped)
623 ui.note(_("%d revisions have been skipped\n") % skippedlen)
623 ui.note(_("%d revisions have been skipped\n") % skippedlen)
624 fm.end()
624 fm.end()
625
625
626 if (self.activebookmark and self.activebookmark in repo._bookmarks and
626 if (self.activebookmark and self.activebookmark in repo._bookmarks and
627 repo['.'].node() == repo._bookmarks[self.activebookmark]):
627 repo['.'].node() == repo._bookmarks[self.activebookmark]):
628 bookmarks.activate(repo, self.activebookmark)
628 bookmarks.activate(repo, self.activebookmark)
629
629
630 @command('rebase',
630 @command('rebase',
631 [('s', 'source', '',
631 [('s', 'source', '',
632 _('rebase the specified changeset and descendants'), _('REV')),
632 _('rebase the specified changeset and descendants'), _('REV')),
633 ('b', 'base', '',
633 ('b', 'base', '',
634 _('rebase everything from branching point of specified changeset'),
634 _('rebase everything from branching point of specified changeset'),
635 _('REV')),
635 _('REV')),
636 ('r', 'rev', [],
636 ('r', 'rev', [],
637 _('rebase these revisions'),
637 _('rebase these revisions'),
638 _('REV')),
638 _('REV')),
639 ('d', 'dest', '',
639 ('d', 'dest', '',
640 _('rebase onto the specified changeset'), _('REV')),
640 _('rebase onto the specified changeset'), _('REV')),
641 ('', 'collapse', False, _('collapse the rebased changesets')),
641 ('', 'collapse', False, _('collapse the rebased changesets')),
642 ('m', 'message', '',
642 ('m', 'message', '',
643 _('use text as collapse commit message'), _('TEXT')),
643 _('use text as collapse commit message'), _('TEXT')),
644 ('e', 'edit', False, _('invoke editor on commit messages')),
644 ('e', 'edit', False, _('invoke editor on commit messages')),
645 ('l', 'logfile', '',
645 ('l', 'logfile', '',
646 _('read collapse commit message from file'), _('FILE')),
646 _('read collapse commit message from file'), _('FILE')),
647 ('k', 'keep', False, _('keep original changesets')),
647 ('k', 'keep', False, _('keep original changesets')),
648 ('', 'keepbranches', False, _('keep original branch names')),
648 ('', 'keepbranches', False, _('keep original branch names')),
649 ('D', 'detach', False, _('(DEPRECATED)')),
649 ('D', 'detach', False, _('(DEPRECATED)')),
650 ('i', 'interactive', False, _('(DEPRECATED)')),
650 ('i', 'interactive', False, _('(DEPRECATED)')),
651 ('t', 'tool', '', _('specify merge tool')),
651 ('t', 'tool', '', _('specify merge tool')),
652 ('c', 'continue', False, _('continue an interrupted rebase')),
652 ('c', 'continue', False, _('continue an interrupted rebase')),
653 ('a', 'abort', False, _('abort an interrupted rebase'))] +
653 ('a', 'abort', False, _('abort an interrupted rebase'))] +
654 cmdutil.formatteropts,
654 cmdutil.formatteropts,
655 _('[-s REV | -b REV] [-d REV] [OPTION]'))
655 _('[-s REV | -b REV] [-d REV] [OPTION]'))
656 def rebase(ui, repo, **opts):
656 def rebase(ui, repo, **opts):
657 """move changeset (and descendants) to a different branch
657 """move changeset (and descendants) to a different branch
658
658
659 Rebase uses repeated merging to graft changesets from one part of
659 Rebase uses repeated merging to graft changesets from one part of
660 history (the source) onto another (the destination). This can be
660 history (the source) onto another (the destination). This can be
661 useful for linearizing *local* changes relative to a master
661 useful for linearizing *local* changes relative to a master
662 development tree.
662 development tree.
663
663
664 Published commits cannot be rebased (see :hg:`help phases`).
664 Published commits cannot be rebased (see :hg:`help phases`).
665 To copy commits, see :hg:`help graft`.
665 To copy commits, see :hg:`help graft`.
666
666
667 If you don't specify a destination changeset (``-d/--dest``), rebase
667 If you don't specify a destination changeset (``-d/--dest``), rebase
668 will use the same logic as :hg:`merge` to pick a destination. if
668 will use the same logic as :hg:`merge` to pick a destination. if
669 the current branch contains exactly one other head, the other head
669 the current branch contains exactly one other head, the other head
670 is merged with by default. Otherwise, an explicit revision with
670 is merged with by default. Otherwise, an explicit revision with
671 which to merge with must be provided. (destination changeset is not
671 which to merge with must be provided. (destination changeset is not
672 modified by rebasing, but new changesets are added as its
672 modified by rebasing, but new changesets are added as its
673 descendants.)
673 descendants.)
674
674
675 Here are the ways to select changesets:
675 Here are the ways to select changesets:
676
676
677 1. Explicitly select them using ``--rev``.
677 1. Explicitly select them using ``--rev``.
678
678
679 2. Use ``--source`` to select a root changeset and include all of its
679 2. Use ``--source`` to select a root changeset and include all of its
680 descendants.
680 descendants.
681
681
682 3. Use ``--base`` to select a changeset; rebase will find ancestors
682 3. Use ``--base`` to select a changeset; rebase will find ancestors
683 and their descendants which are not also ancestors of the destination.
683 and their descendants which are not also ancestors of the destination.
684
684
685 4. If you do not specify any of ``--rev``, ``source``, or ``--base``,
685 4. If you do not specify any of ``--rev``, ``source``, or ``--base``,
686 rebase will use ``--base .`` as above.
686 rebase will use ``--base .`` as above.
687
687
688 If ``--source`` or ``--rev`` is used, special names ``SRC`` and ``ALLSRC``
688 If ``--source`` or ``--rev`` is used, special names ``SRC`` and ``ALLSRC``
689 can be used in ``--dest``. Destination would be calculated per source
689 can be used in ``--dest``. Destination would be calculated per source
690 revision with ``SRC`` substituted by that single source revision and
690 revision with ``SRC`` substituted by that single source revision and
691 ``ALLSRC`` substituted by all source revisions.
691 ``ALLSRC`` substituted by all source revisions.
692
692
693 Rebase will destroy original changesets unless you use ``--keep``.
693 Rebase will destroy original changesets unless you use ``--keep``.
694 It will also move your bookmarks (even if you do).
694 It will also move your bookmarks (even if you do).
695
695
696 Some changesets may be dropped if they do not contribute changes
696 Some changesets may be dropped if they do not contribute changes
697 (e.g. merges from the destination branch).
697 (e.g. merges from the destination branch).
698
698
699 Unlike ``merge``, rebase will do nothing if you are at the branch tip of
699 Unlike ``merge``, rebase will do nothing if you are at the branch tip of
700 a named branch with two heads. You will need to explicitly specify source
700 a named branch with two heads. You will need to explicitly specify source
701 and/or destination.
701 and/or destination.
702
702
703 If you need to use a tool to automate merge/conflict decisions, you
703 If you need to use a tool to automate merge/conflict decisions, you
704 can specify one with ``--tool``, see :hg:`help merge-tools`.
704 can specify one with ``--tool``, see :hg:`help merge-tools`.
705 As a caveat: the tool will not be used to mediate when a file was
705 As a caveat: the tool will not be used to mediate when a file was
706 deleted, there is no hook presently available for this.
706 deleted, there is no hook presently available for this.
707
707
708 If a rebase is interrupted to manually resolve a conflict, it can be
708 If a rebase is interrupted to manually resolve a conflict, it can be
709 continued with --continue/-c or aborted with --abort/-a.
709 continued with --continue/-c or aborted with --abort/-a.
710
710
711 .. container:: verbose
711 .. container:: verbose
712
712
713 Examples:
713 Examples:
714
714
715 - move "local changes" (current commit back to branching point)
715 - move "local changes" (current commit back to branching point)
716 to the current branch tip after a pull::
716 to the current branch tip after a pull::
717
717
718 hg rebase
718 hg rebase
719
719
720 - move a single changeset to the stable branch::
720 - move a single changeset to the stable branch::
721
721
722 hg rebase -r 5f493448 -d stable
722 hg rebase -r 5f493448 -d stable
723
723
724 - splice a commit and all its descendants onto another part of history::
724 - splice a commit and all its descendants onto another part of history::
725
725
726 hg rebase --source c0c3 --dest 4cf9
726 hg rebase --source c0c3 --dest 4cf9
727
727
728 - rebase everything on a branch marked by a bookmark onto the
728 - rebase everything on a branch marked by a bookmark onto the
729 default branch::
729 default branch::
730
730
731 hg rebase --base myfeature --dest default
731 hg rebase --base myfeature --dest default
732
732
733 - collapse a sequence of changes into a single commit::
733 - collapse a sequence of changes into a single commit::
734
734
735 hg rebase --collapse -r 1520:1525 -d .
735 hg rebase --collapse -r 1520:1525 -d .
736
736
737 - move a named branch while preserving its name::
737 - move a named branch while preserving its name::
738
738
739 hg rebase -r "branch(featureX)" -d 1.3 --keepbranches
739 hg rebase -r "branch(featureX)" -d 1.3 --keepbranches
740
740
741 - stabilize orphaned changesets so history looks linear::
741 - stabilize orphaned changesets so history looks linear::
742
742
743 hg rebase -r 'orphan()-obsolete()'\
743 hg rebase -r 'orphan()-obsolete()'\
744 -d 'first(max((successors(max(roots(ALLSRC) & ::SRC)^)-obsolete())::) +\
744 -d 'first(max((successors(max(roots(ALLSRC) & ::SRC)^)-obsolete())::) +\
745 max(::((roots(ALLSRC) & ::SRC)^)-obsolete()))'
745 max(::((roots(ALLSRC) & ::SRC)^)-obsolete()))'
746
746
747 Configuration Options:
747 Configuration Options:
748
748
749 You can make rebase require a destination if you set the following config
749 You can make rebase require a destination if you set the following config
750 option::
750 option::
751
751
752 [commands]
752 [commands]
753 rebase.requiredest = True
753 rebase.requiredest = True
754
754
755 By default, rebase will close the transaction after each commit. For
755 By default, rebase will close the transaction after each commit. For
756 performance purposes, you can configure rebase to use a single transaction
756 performance purposes, you can configure rebase to use a single transaction
757 across the entire rebase. WARNING: This setting introduces a significant
757 across the entire rebase. WARNING: This setting introduces a significant
758 risk of losing the work you've done in a rebase if the rebase aborts
758 risk of losing the work you've done in a rebase if the rebase aborts
759 unexpectedly::
759 unexpectedly::
760
760
761 [rebase]
761 [rebase]
762 singletransaction = True
762 singletransaction = True
763
763
764 By default, rebase writes to the working copy, but you can configure it to
764 By default, rebase writes to the working copy, but you can configure it to
765 run in-memory for for better performance, and to allow it to run if the
765 run in-memory for for better performance, and to allow it to run if the
766 working copy is dirty::
766 working copy is dirty::
767
767
768 [rebase]
768 [rebase]
769 experimental.inmemory = True
769 experimental.inmemory = True
770
770
771 Return Values:
771 Return Values:
772
772
773 Returns 0 on success, 1 if nothing to rebase or there are
773 Returns 0 on success, 1 if nothing to rebase or there are
774 unresolved conflicts.
774 unresolved conflicts.
775
775
776 """
776 """
777 inmemory = ui.configbool('rebase', 'experimental.inmemory')
777 inmemory = ui.configbool('rebase', 'experimental.inmemory')
778 if (opts.get('continue') or opts.get('abort') or
778 if (opts.get('continue') or opts.get('abort') or
779 repo.currenttransaction() is not None):
779 repo.currenttransaction() is not None):
780 # in-memory rebase is not compatible with resuming rebases.
780 # in-memory rebase is not compatible with resuming rebases.
781 # (Or if it is run within a transaction, since the restart logic can
781 # (Or if it is run within a transaction, since the restart logic can
782 # fail the entire transaction.)
782 # fail the entire transaction.)
783 inmemory = False
783 inmemory = False
784
784
785 if inmemory:
785 if inmemory:
786 try:
786 try:
787 # in-memory merge doesn't support conflicts, so if we hit any, abort
787 # in-memory merge doesn't support conflicts, so if we hit any, abort
788 # and re-run as an on-disk merge.
788 # and re-run as an on-disk merge.
789 return _origrebase(ui, repo, inmemory=inmemory, **opts)
789 return _origrebase(ui, repo, inmemory=inmemory, **opts)
790 except error.InMemoryMergeConflictsError:
790 except error.InMemoryMergeConflictsError:
791 ui.warn(_('hit merge conflicts; re-running rebase without in-memory'
791 ui.warn(_('hit merge conflicts; re-running rebase without in-memory'
792 ' merge\n'))
792 ' merge\n'))
793 _origrebase(ui, repo, **{'abort': True})
793 _origrebase(ui, repo, **{'abort': True})
794 return _origrebase(ui, repo, inmemory=False, **opts)
794 return _origrebase(ui, repo, inmemory=False, **opts)
795 else:
795 else:
796 return _origrebase(ui, repo, **opts)
796 return _origrebase(ui, repo, **opts)
797
797
798 def _origrebase(ui, repo, inmemory=False, **opts):
798 def _origrebase(ui, repo, inmemory=False, **opts):
799 opts = pycompat.byteskwargs(opts)
799 opts = pycompat.byteskwargs(opts)
800 rbsrt = rebaseruntime(repo, ui, inmemory, opts)
800 rbsrt = rebaseruntime(repo, ui, inmemory, opts)
801
801
802 with repo.wlock(), repo.lock():
802 with repo.wlock(), repo.lock():
803 # Validate input and define rebasing points
803 # Validate input and define rebasing points
804 destf = opts.get('dest', None)
804 destf = opts.get('dest', None)
805 srcf = opts.get('source', None)
805 srcf = opts.get('source', None)
806 basef = opts.get('base', None)
806 basef = opts.get('base', None)
807 revf = opts.get('rev', [])
807 revf = opts.get('rev', [])
808 # search default destination in this space
808 # search default destination in this space
809 # used in the 'hg pull --rebase' case, see issue 5214.
809 # used in the 'hg pull --rebase' case, see issue 5214.
810 destspace = opts.get('_destspace')
810 destspace = opts.get('_destspace')
811 contf = opts.get('continue')
811 contf = opts.get('continue')
812 abortf = opts.get('abort')
812 abortf = opts.get('abort')
813 if opts.get('interactive'):
813 if opts.get('interactive'):
814 try:
814 try:
815 if extensions.find('histedit'):
815 if extensions.find('histedit'):
816 enablehistedit = ''
816 enablehistedit = ''
817 except KeyError:
817 except KeyError:
818 enablehistedit = " --config extensions.histedit="
818 enablehistedit = " --config extensions.histedit="
819 help = "hg%s help -e histedit" % enablehistedit
819 help = "hg%s help -e histedit" % enablehistedit
820 msg = _("interactive history editing is supported by the "
820 msg = _("interactive history editing is supported by the "
821 "'histedit' extension (see \"%s\")") % help
821 "'histedit' extension (see \"%s\")") % help
822 raise error.Abort(msg)
822 raise error.Abort(msg)
823
823
824 if rbsrt.collapsemsg and not rbsrt.collapsef:
824 if rbsrt.collapsemsg and not rbsrt.collapsef:
825 raise error.Abort(
825 raise error.Abort(
826 _('message can only be specified with collapse'))
826 _('message can only be specified with collapse'))
827
827
828 if contf or abortf:
828 if contf or abortf:
829 if contf and abortf:
829 if contf and abortf:
830 raise error.Abort(_('cannot use both abort and continue'))
830 raise error.Abort(_('cannot use both abort and continue'))
831 if rbsrt.collapsef:
831 if rbsrt.collapsef:
832 raise error.Abort(
832 raise error.Abort(
833 _('cannot use collapse with continue or abort'))
833 _('cannot use collapse with continue or abort'))
834 if srcf or basef or destf:
834 if srcf or basef or destf:
835 raise error.Abort(
835 raise error.Abort(
836 _('abort and continue do not allow specifying revisions'))
836 _('abort and continue do not allow specifying revisions'))
837 if abortf and opts.get('tool', False):
837 if abortf and opts.get('tool', False):
838 ui.warn(_('tool option will be ignored\n'))
838 ui.warn(_('tool option will be ignored\n'))
839 if contf:
839 if contf:
840 ms = mergemod.mergestate.read(repo)
840 ms = mergemod.mergestate.read(repo)
841 mergeutil.checkunresolved(ms)
841 mergeutil.checkunresolved(ms)
842
842
843 retcode = rbsrt._prepareabortorcontinue(abortf)
843 retcode = rbsrt._prepareabortorcontinue(abortf)
844 if retcode is not None:
844 if retcode is not None:
845 return retcode
845 return retcode
846 else:
846 else:
847 destmap = _definedestmap(ui, repo, rbsrt, destf, srcf, basef, revf,
847 destmap = _definedestmap(ui, repo, rbsrt, destf, srcf, basef, revf,
848 destspace=destspace)
848 destspace=destspace)
849 retcode = rbsrt._preparenewrebase(destmap)
849 retcode = rbsrt._preparenewrebase(destmap)
850 if retcode is not None:
850 if retcode is not None:
851 return retcode
851 return retcode
852
852
853 tr = None
853 tr = None
854 dsguard = None
854 dsguard = None
855
855
856 singletr = ui.configbool('rebase', 'singletransaction')
856 singletr = ui.configbool('rebase', 'singletransaction')
857 if singletr:
857 if singletr:
858 tr = repo.transaction('rebase')
858 tr = repo.transaction('rebase')
859
859
860 # If `rebase.singletransaction` is enabled, wrap the entire operation in
860 # If `rebase.singletransaction` is enabled, wrap the entire operation in
861 # one transaction here. Otherwise, transactions are obtained when
861 # one transaction here. Otherwise, transactions are obtained when
862 # committing each node, which is slower but allows partial success.
862 # committing each node, which is slower but allows partial success.
863 with util.acceptintervention(tr):
863 with util.acceptintervention(tr):
864 # Same logic for the dirstate guard, except we don't create one when
864 # Same logic for the dirstate guard, except we don't create one when
865 # rebasing in-memory (it's not needed).
865 # rebasing in-memory (it's not needed).
866 if singletr and not inmemory:
866 if singletr and not inmemory:
867 dsguard = dirstateguard.dirstateguard(repo, 'rebase')
867 dsguard = dirstateguard.dirstateguard(repo, 'rebase')
868 with util.acceptintervention(dsguard):
868 with util.acceptintervention(dsguard):
869 rbsrt._performrebase(tr)
869 rbsrt._performrebase(tr)
870
870
871 rbsrt._finishrebase()
871 rbsrt._finishrebase()
872
872
873 def _definedestmap(ui, repo, rbsrt, destf=None, srcf=None, basef=None,
873 def _definedestmap(ui, repo, rbsrt, destf=None, srcf=None, basef=None,
874 revf=None, destspace=None):
874 revf=None, destspace=None):
875 """use revisions argument to define destmap {srcrev: destrev}"""
875 """use revisions argument to define destmap {srcrev: destrev}"""
876 if revf is None:
876 if revf is None:
877 revf = []
877 revf = []
878
878
879 # destspace is here to work around issues with `hg pull --rebase` see
879 # destspace is here to work around issues with `hg pull --rebase` see
880 # issue5214 for details
880 # issue5214 for details
881 if srcf and basef:
881 if srcf and basef:
882 raise error.Abort(_('cannot specify both a source and a base'))
882 raise error.Abort(_('cannot specify both a source and a base'))
883 if revf and basef:
883 if revf and basef:
884 raise error.Abort(_('cannot specify both a revision and a base'))
884 raise error.Abort(_('cannot specify both a revision and a base'))
885 if revf and srcf:
885 if revf and srcf:
886 raise error.Abort(_('cannot specify both a revision and a source'))
886 raise error.Abort(_('cannot specify both a revision and a source'))
887
887
888 if not rbsrt.inmemory:
888 if not rbsrt.inmemory:
889 cmdutil.checkunfinished(repo)
889 cmdutil.checkunfinished(repo)
890 cmdutil.bailifchanged(repo)
890 cmdutil.bailifchanged(repo)
891
891
892 if ui.configbool('commands', 'rebase.requiredest') and not destf:
892 if ui.configbool('commands', 'rebase.requiredest') and not destf:
893 raise error.Abort(_('you must specify a destination'),
893 raise error.Abort(_('you must specify a destination'),
894 hint=_('use: hg rebase -d REV'))
894 hint=_('use: hg rebase -d REV'))
895
895
896 dest = None
896 dest = None
897
897
898 if revf:
898 if revf:
899 rebaseset = scmutil.revrange(repo, revf)
899 rebaseset = scmutil.revrange(repo, revf)
900 if not rebaseset:
900 if not rebaseset:
901 ui.status(_('empty "rev" revision set - nothing to rebase\n'))
901 ui.status(_('empty "rev" revision set - nothing to rebase\n'))
902 return None
902 return None
903 elif srcf:
903 elif srcf:
904 src = scmutil.revrange(repo, [srcf])
904 src = scmutil.revrange(repo, [srcf])
905 if not src:
905 if not src:
906 ui.status(_('empty "source" revision set - nothing to rebase\n'))
906 ui.status(_('empty "source" revision set - nothing to rebase\n'))
907 return None
907 return None
908 rebaseset = repo.revs('(%ld)::', src)
908 rebaseset = repo.revs('(%ld)::', src)
909 assert rebaseset
909 assert rebaseset
910 else:
910 else:
911 base = scmutil.revrange(repo, [basef or '.'])
911 base = scmutil.revrange(repo, [basef or '.'])
912 if not base:
912 if not base:
913 ui.status(_('empty "base" revision set - '
913 ui.status(_('empty "base" revision set - '
914 "can't compute rebase set\n"))
914 "can't compute rebase set\n"))
915 return None
915 return None
916 if destf:
916 if destf:
917 # --base does not support multiple destinations
917 # --base does not support multiple destinations
918 dest = scmutil.revsingle(repo, destf)
918 dest = scmutil.revsingle(repo, destf)
919 else:
919 else:
920 dest = repo[_destrebase(repo, base, destspace=destspace)]
920 dest = repo[_destrebase(repo, base, destspace=destspace)]
921 destf = str(dest)
921 destf = str(dest)
922
922
923 roots = [] # selected children of branching points
923 roots = [] # selected children of branching points
924 bpbase = {} # {branchingpoint: [origbase]}
924 bpbase = {} # {branchingpoint: [origbase]}
925 for b in base: # group bases by branching points
925 for b in base: # group bases by branching points
926 bp = repo.revs('ancestor(%d, %d)', b, dest).first()
926 bp = repo.revs('ancestor(%d, %d)', b, dest).first()
927 bpbase[bp] = bpbase.get(bp, []) + [b]
927 bpbase[bp] = bpbase.get(bp, []) + [b]
928 if None in bpbase:
928 if None in bpbase:
929 # emulate the old behavior, showing "nothing to rebase" (a better
929 # emulate the old behavior, showing "nothing to rebase" (a better
930 # behavior may be abort with "cannot find branching point" error)
930 # behavior may be abort with "cannot find branching point" error)
931 bpbase.clear()
931 bpbase.clear()
932 for bp, bs in bpbase.iteritems(): # calculate roots
932 for bp, bs in bpbase.iteritems(): # calculate roots
933 roots += list(repo.revs('children(%d) & ancestors(%ld)', bp, bs))
933 roots += list(repo.revs('children(%d) & ancestors(%ld)', bp, bs))
934
934
935 rebaseset = repo.revs('%ld::', roots)
935 rebaseset = repo.revs('%ld::', roots)
936
936
937 if not rebaseset:
937 if not rebaseset:
938 # transform to list because smartsets are not comparable to
938 # transform to list because smartsets are not comparable to
939 # lists. This should be improved to honor laziness of
939 # lists. This should be improved to honor laziness of
940 # smartset.
940 # smartset.
941 if list(base) == [dest.rev()]:
941 if list(base) == [dest.rev()]:
942 if basef:
942 if basef:
943 ui.status(_('nothing to rebase - %s is both "base"'
943 ui.status(_('nothing to rebase - %s is both "base"'
944 ' and destination\n') % dest)
944 ' and destination\n') % dest)
945 else:
945 else:
946 ui.status(_('nothing to rebase - working directory '
946 ui.status(_('nothing to rebase - working directory '
947 'parent is also destination\n'))
947 'parent is also destination\n'))
948 elif not repo.revs('%ld - ::%d', base, dest):
948 elif not repo.revs('%ld - ::%d', base, dest):
949 if basef:
949 if basef:
950 ui.status(_('nothing to rebase - "base" %s is '
950 ui.status(_('nothing to rebase - "base" %s is '
951 'already an ancestor of destination '
951 'already an ancestor of destination '
952 '%s\n') %
952 '%s\n') %
953 ('+'.join(str(repo[r]) for r in base),
953 ('+'.join(str(repo[r]) for r in base),
954 dest))
954 dest))
955 else:
955 else:
956 ui.status(_('nothing to rebase - working '
956 ui.status(_('nothing to rebase - working '
957 'directory parent is already an '
957 'directory parent is already an '
958 'ancestor of destination %s\n') % dest)
958 'ancestor of destination %s\n') % dest)
959 else: # can it happen?
959 else: # can it happen?
960 ui.status(_('nothing to rebase from %s to %s\n') %
960 ui.status(_('nothing to rebase from %s to %s\n') %
961 ('+'.join(str(repo[r]) for r in base), dest))
961 ('+'.join(str(repo[r]) for r in base), dest))
962 return None
962 return None
963 # If rebasing the working copy parent, force in-memory merge to be off.
963 # If rebasing the working copy parent, force in-memory merge to be off.
964 #
964 #
965 # This is because the extra work of checking out the newly rebased commit
965 # This is because the extra work of checking out the newly rebased commit
966 # outweights the benefits of rebasing in-memory, and executing an extra
966 # outweights the benefits of rebasing in-memory, and executing an extra
967 # update command adds a bit of overhead, so better to just do it on disk. In
967 # update command adds a bit of overhead, so better to just do it on disk. In
968 # all other cases leave it on.
968 # all other cases leave it on.
969 #
969 #
970 # Note that there are cases where this isn't true -- e.g., rebasing large
970 # Note that there are cases where this isn't true -- e.g., rebasing large
971 # stacks that include the WCP. However, I'm not yet sure where the cutoff
971 # stacks that include the WCP. However, I'm not yet sure where the cutoff
972 # is.
972 # is.
973 rebasingwcp = repo['.'].rev() in rebaseset
973 rebasingwcp = repo['.'].rev() in rebaseset
974 ui.log("rebase", "", rebase_rebasing_wcp=rebasingwcp)
974 ui.log("rebase", "", rebase_rebasing_wcp=rebasingwcp)
975 if rbsrt.inmemory and rebasingwcp:
975 if rbsrt.inmemory and rebasingwcp:
976 rbsrt.inmemory = False
976 rbsrt.inmemory = False
977 # Check these since we did not before.
977 # Check these since we did not before.
978 cmdutil.checkunfinished(repo)
978 cmdutil.checkunfinished(repo)
979 cmdutil.bailifchanged(repo)
979 cmdutil.bailifchanged(repo)
980
980
981 if not destf:
981 if not destf:
982 dest = repo[_destrebase(repo, rebaseset, destspace=destspace)]
982 dest = repo[_destrebase(repo, rebaseset, destspace=destspace)]
983 destf = str(dest)
983 destf = str(dest)
984
984
985 allsrc = revsetlang.formatspec('%ld', rebaseset)
985 allsrc = revsetlang.formatspec('%ld', rebaseset)
986 alias = {'ALLSRC': allsrc}
986 alias = {'ALLSRC': allsrc}
987
987
988 if dest is None:
988 if dest is None:
989 try:
989 try:
990 # fast path: try to resolve dest without SRC alias
990 # fast path: try to resolve dest without SRC alias
991 dest = scmutil.revsingle(repo, destf, localalias=alias)
991 dest = scmutil.revsingle(repo, destf, localalias=alias)
992 except error.RepoLookupError:
992 except error.RepoLookupError:
993 # multi-dest path: resolve dest for each SRC separately
993 # multi-dest path: resolve dest for each SRC separately
994 destmap = {}
994 destmap = {}
995 for r in rebaseset:
995 for r in rebaseset:
996 alias['SRC'] = revsetlang.formatspec('%d', r)
996 alias['SRC'] = revsetlang.formatspec('%d', r)
997 # use repo.anyrevs instead of scmutil.revsingle because we
997 # use repo.anyrevs instead of scmutil.revsingle because we
998 # don't want to abort if destset is empty.
998 # don't want to abort if destset is empty.
999 destset = repo.anyrevs([destf], user=True, localalias=alias)
999 destset = repo.anyrevs([destf], user=True, localalias=alias)
1000 size = len(destset)
1000 size = len(destset)
1001 if size == 1:
1001 if size == 1:
1002 destmap[r] = destset.first()
1002 destmap[r] = destset.first()
1003 elif size == 0:
1003 elif size == 0:
1004 ui.note(_('skipping %s - empty destination\n') % repo[r])
1004 ui.note(_('skipping %s - empty destination\n') % repo[r])
1005 else:
1005 else:
1006 raise error.Abort(_('rebase destination for %s is not '
1006 raise error.Abort(_('rebase destination for %s is not '
1007 'unique') % repo[r])
1007 'unique') % repo[r])
1008
1008
1009 if dest is not None:
1009 if dest is not None:
1010 # single-dest case: assign dest to each rev in rebaseset
1010 # single-dest case: assign dest to each rev in rebaseset
1011 destrev = dest.rev()
1011 destrev = dest.rev()
1012 destmap = {r: destrev for r in rebaseset} # {srcrev: destrev}
1012 destmap = {r: destrev for r in rebaseset} # {srcrev: destrev}
1013
1013
1014 if not destmap:
1014 if not destmap:
1015 ui.status(_('nothing to rebase - empty destination\n'))
1015 ui.status(_('nothing to rebase - empty destination\n'))
1016 return None
1016 return None
1017
1017
1018 return destmap
1018 return destmap
1019
1019
1020 def externalparent(repo, state, destancestors):
1020 def externalparent(repo, state, destancestors):
1021 """Return the revision that should be used as the second parent
1021 """Return the revision that should be used as the second parent
1022 when the revisions in state is collapsed on top of destancestors.
1022 when the revisions in state is collapsed on top of destancestors.
1023 Abort if there is more than one parent.
1023 Abort if there is more than one parent.
1024 """
1024 """
1025 parents = set()
1025 parents = set()
1026 source = min(state)
1026 source = min(state)
1027 for rev in state:
1027 for rev in state:
1028 if rev == source:
1028 if rev == source:
1029 continue
1029 continue
1030 for p in repo[rev].parents():
1030 for p in repo[rev].parents():
1031 if (p.rev() not in state
1031 if (p.rev() not in state
1032 and p.rev() not in destancestors):
1032 and p.rev() not in destancestors):
1033 parents.add(p.rev())
1033 parents.add(p.rev())
1034 if not parents:
1034 if not parents:
1035 return nullrev
1035 return nullrev
1036 if len(parents) == 1:
1036 if len(parents) == 1:
1037 return parents.pop()
1037 return parents.pop()
1038 raise error.Abort(_('unable to collapse on top of %s, there is more '
1038 raise error.Abort(_('unable to collapse on top of %s, there is more '
1039 'than one external parent: %s') %
1039 'than one external parent: %s') %
1040 (max(destancestors),
1040 (max(destancestors),
1041 ', '.join(str(p) for p in sorted(parents))))
1041 ', '.join(str(p) for p in sorted(parents))))
1042
1042
1043 def concludememorynode(repo, rev, p1, p2, wctx=None,
1043 def concludememorynode(repo, rev, p1, p2, wctx=None,
1044 commitmsg=None, editor=None, extrafn=None,
1044 commitmsg=None, editor=None, extrafn=None,
1045 keepbranches=False, date=None):
1045 keepbranches=False, date=None):
1046 '''Commit the memory changes with parents p1 and p2. Reuse commit info from
1046 '''Commit the memory changes with parents p1 and p2. Reuse commit info from
1047 rev but also store useful information in extra.
1047 rev but also store useful information in extra.
1048 Return node of committed revision.'''
1048 Return node of committed revision.'''
1049 ctx = repo[rev]
1049 ctx = repo[rev]
1050 if commitmsg is None:
1050 if commitmsg is None:
1051 commitmsg = ctx.description()
1051 commitmsg = ctx.description()
1052 keepbranch = keepbranches and repo[p1].branch() != ctx.branch()
1052 keepbranch = keepbranches and repo[p1].branch() != ctx.branch()
1053 extra = {'rebase_source': ctx.hex()}
1053 extra = {'rebase_source': ctx.hex()}
1054 if extrafn:
1054 if extrafn:
1055 extrafn(ctx, extra)
1055 extrafn(ctx, extra)
1056
1056
1057 destphase = max(ctx.phase(), phases.draft)
1057 destphase = max(ctx.phase(), phases.draft)
1058 overrides = {('phases', 'new-commit'): destphase}
1058 overrides = {('phases', 'new-commit'): destphase}
1059 with repo.ui.configoverride(overrides, 'rebase'):
1059 with repo.ui.configoverride(overrides, 'rebase'):
1060 if keepbranch:
1060 if keepbranch:
1061 repo.ui.setconfig('ui', 'allowemptycommit', True)
1061 repo.ui.setconfig('ui', 'allowemptycommit', True)
1062 # Replicates the empty check in ``repo.commit``.
1062 # Replicates the empty check in ``repo.commit``.
1063 if wctx.isempty() and not repo.ui.configbool('ui', 'allowemptycommit'):
1063 if wctx.isempty() and not repo.ui.configbool('ui', 'allowemptycommit'):
1064 return None
1064 return None
1065
1065
1066 if date is None:
1066 if date is None:
1067 date = ctx.date()
1067 date = ctx.date()
1068
1068
1069 # By convention, ``extra['branch']`` (set by extrafn) clobbers
1069 # By convention, ``extra['branch']`` (set by extrafn) clobbers
1070 # ``branch`` (used when passing ``--keepbranches``).
1070 # ``branch`` (used when passing ``--keepbranches``).
1071 branch = repo[p1].branch()
1071 branch = repo[p1].branch()
1072 if 'branch' in extra:
1072 if 'branch' in extra:
1073 branch = extra['branch']
1073 branch = extra['branch']
1074
1074
1075 memctx = wctx.tomemctx(commitmsg, parents=(p1, p2), date=date,
1075 memctx = wctx.tomemctx(commitmsg, parents=(p1, p2), date=date,
1076 extra=extra, user=ctx.user(), branch=branch, editor=editor)
1076 extra=extra, user=ctx.user(), branch=branch, editor=editor)
1077 commitres = repo.commitctx(memctx)
1077 commitres = repo.commitctx(memctx)
1078 wctx.clean() # Might be reused
1078 wctx.clean() # Might be reused
1079 return commitres
1079 return commitres
1080
1080
1081 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None,
1081 def concludenode(repo, rev, p1, p2, commitmsg=None, editor=None, extrafn=None,
1082 keepbranches=False, date=None):
1082 keepbranches=False, date=None):
1083 '''Commit the wd changes with parents p1 and p2. Reuse commit info from rev
1083 '''Commit the wd changes with parents p1 and p2. Reuse commit info from rev
1084 but also store useful information in extra.
1084 but also store useful information in extra.
1085 Return node of committed revision.'''
1085 Return node of committed revision.'''
1086 dsguard = util.nullcontextmanager()
1086 dsguard = util.nullcontextmanager()
1087 if not repo.ui.configbool('rebase', 'singletransaction'):
1087 if not repo.ui.configbool('rebase', 'singletransaction'):
1088 dsguard = dirstateguard.dirstateguard(repo, 'rebase')
1088 dsguard = dirstateguard.dirstateguard(repo, 'rebase')
1089 with dsguard:
1089 with dsguard:
1090 repo.setparents(repo[p1].node(), repo[p2].node())
1090 repo.setparents(repo[p1].node(), repo[p2].node())
1091 ctx = repo[rev]
1091 ctx = repo[rev]
1092 if commitmsg is None:
1092 if commitmsg is None:
1093 commitmsg = ctx.description()
1093 commitmsg = ctx.description()
1094 keepbranch = keepbranches and repo[p1].branch() != ctx.branch()
1094 keepbranch = keepbranches and repo[p1].branch() != ctx.branch()
1095 extra = {'rebase_source': ctx.hex()}
1095 extra = {'rebase_source': ctx.hex()}
1096 if extrafn:
1096 if extrafn:
1097 extrafn(ctx, extra)
1097 extrafn(ctx, extra)
1098
1098
1099 destphase = max(ctx.phase(), phases.draft)
1099 destphase = max(ctx.phase(), phases.draft)
1100 overrides = {('phases', 'new-commit'): destphase}
1100 overrides = {('phases', 'new-commit'): destphase}
1101 with repo.ui.configoverride(overrides, 'rebase'):
1101 with repo.ui.configoverride(overrides, 'rebase'):
1102 if keepbranch:
1102 if keepbranch:
1103 repo.ui.setconfig('ui', 'allowemptycommit', True)
1103 repo.ui.setconfig('ui', 'allowemptycommit', True)
1104 # Commit might fail if unresolved files exist
1104 # Commit might fail if unresolved files exist
1105 if date is None:
1105 if date is None:
1106 date = ctx.date()
1106 date = ctx.date()
1107 newnode = repo.commit(text=commitmsg, user=ctx.user(),
1107 newnode = repo.commit(text=commitmsg, user=ctx.user(),
1108 date=date, extra=extra, editor=editor)
1108 date=date, extra=extra, editor=editor)
1109
1109
1110 repo.dirstate.setbranch(repo[newnode].branch())
1110 repo.dirstate.setbranch(repo[newnode].branch())
1111 return newnode
1111 return newnode
1112
1112
1113 def rebasenode(repo, rev, p1, base, state, collapse, dest, wctx):
1113 def rebasenode(repo, rev, p1, base, state, collapse, dest, wctx):
1114 'Rebase a single revision rev on top of p1 using base as merge ancestor'
1114 'Rebase a single revision rev on top of p1 using base as merge ancestor'
1115 # Merge phase
1115 # Merge phase
1116 # Update to destination and merge it with local
1116 # Update to destination and merge it with local
1117 if wctx.isinmemory():
1117 if wctx.isinmemory():
1118 wctx.setbase(repo[p1])
1118 wctx.setbase(repo[p1])
1119 else:
1119 else:
1120 if repo['.'].rev() != p1:
1120 if repo['.'].rev() != p1:
1121 repo.ui.debug(" update to %d:%s\n" % (p1, repo[p1]))
1121 repo.ui.debug(" update to %d:%s\n" % (p1, repo[p1]))
1122 mergemod.update(repo, p1, False, True)
1122 mergemod.update(repo, p1, False, True)
1123 else:
1123 else:
1124 repo.ui.debug(" already in destination\n")
1124 repo.ui.debug(" already in destination\n")
1125 # This is, alas, necessary to invalidate workingctx's manifest cache,
1125 # This is, alas, necessary to invalidate workingctx's manifest cache,
1126 # as well as other data we litter on it in other places.
1126 # as well as other data we litter on it in other places.
1127 wctx = repo[None]
1127 wctx = repo[None]
1128 repo.dirstate.write(repo.currenttransaction())
1128 repo.dirstate.write(repo.currenttransaction())
1129 repo.ui.debug(" merge against %d:%s\n" % (rev, repo[rev]))
1129 repo.ui.debug(" merge against %d:%s\n" % (rev, repo[rev]))
1130 if base is not None:
1130 if base is not None:
1131 repo.ui.debug(" detach base %d:%s\n" % (base, repo[base]))
1131 repo.ui.debug(" detach base %d:%s\n" % (base, repo[base]))
1132 # When collapsing in-place, the parent is the common ancestor, we
1132 # When collapsing in-place, the parent is the common ancestor, we
1133 # have to allow merging with it.
1133 # have to allow merging with it.
1134 stats = mergemod.update(repo, rev, True, True, base, collapse,
1134 stats = mergemod.update(repo, rev, True, True, base, collapse,
1135 labels=['dest', 'source'], wc=wctx)
1135 labels=['dest', 'source'], wc=wctx)
1136 if collapse:
1136 if collapse:
1137 copies.duplicatecopies(repo, wctx, rev, dest)
1137 copies.duplicatecopies(repo, wctx, rev, dest)
1138 else:
1138 else:
1139 # If we're not using --collapse, we need to
1139 # If we're not using --collapse, we need to
1140 # duplicate copies between the revision we're
1140 # duplicate copies between the revision we're
1141 # rebasing and its first parent, but *not*
1141 # rebasing and its first parent, but *not*
1142 # duplicate any copies that have already been
1142 # duplicate any copies that have already been
1143 # performed in the destination.
1143 # performed in the destination.
1144 p1rev = repo[rev].p1().rev()
1144 p1rev = repo[rev].p1().rev()
1145 copies.duplicatecopies(repo, wctx, rev, p1rev, skiprev=dest)
1145 copies.duplicatecopies(repo, wctx, rev, p1rev, skiprev=dest)
1146 return stats
1146 return stats
1147
1147
1148 def adjustdest(repo, rev, destmap, state, skipped):
1148 def adjustdest(repo, rev, destmap, state, skipped):
1149 """adjust rebase destination given the current rebase state
1149 """adjust rebase destination given the current rebase state
1150
1150
1151 rev is what is being rebased. Return a list of two revs, which are the
1151 rev is what is being rebased. Return a list of two revs, which are the
1152 adjusted destinations for rev's p1 and p2, respectively. If a parent is
1152 adjusted destinations for rev's p1 and p2, respectively. If a parent is
1153 nullrev, return dest without adjustment for it.
1153 nullrev, return dest without adjustment for it.
1154
1154
1155 For example, when doing rebasing B+E to F, C to G, rebase will first move B
1155 For example, when doing rebasing B+E to F, C to G, rebase will first move B
1156 to B1, and E's destination will be adjusted from F to B1.
1156 to B1, and E's destination will be adjusted from F to B1.
1157
1157
1158 B1 <- written during rebasing B
1158 B1 <- written during rebasing B
1159 |
1159 |
1160 F <- original destination of B, E
1160 F <- original destination of B, E
1161 |
1161 |
1162 | E <- rev, which is being rebased
1162 | E <- rev, which is being rebased
1163 | |
1163 | |
1164 | D <- prev, one parent of rev being checked
1164 | D <- prev, one parent of rev being checked
1165 | |
1165 | |
1166 | x <- skipped, ex. no successor or successor in (::dest)
1166 | x <- skipped, ex. no successor or successor in (::dest)
1167 | |
1167 | |
1168 | C <- rebased as C', different destination
1168 | C <- rebased as C', different destination
1169 | |
1169 | |
1170 | B <- rebased as B1 C'
1170 | B <- rebased as B1 C'
1171 |/ |
1171 |/ |
1172 A G <- destination of C, different
1172 A G <- destination of C, different
1173
1173
1174 Another example about merge changeset, rebase -r C+G+H -d K, rebase will
1174 Another example about merge changeset, rebase -r C+G+H -d K, rebase will
1175 first move C to C1, G to G1, and when it's checking H, the adjusted
1175 first move C to C1, G to G1, and when it's checking H, the adjusted
1176 destinations will be [C1, G1].
1176 destinations will be [C1, G1].
1177
1177
1178 H C1 G1
1178 H C1 G1
1179 /| | /
1179 /| | /
1180 F G |/
1180 F G |/
1181 K | | -> K
1181 K | | -> K
1182 | C D |
1182 | C D |
1183 | |/ |
1183 | |/ |
1184 | B | ...
1184 | B | ...
1185 |/ |/
1185 |/ |/
1186 A A
1186 A A
1187
1187
1188 Besides, adjust dest according to existing rebase information. For example,
1188 Besides, adjust dest according to existing rebase information. For example,
1189
1189
1190 B C D B needs to be rebased on top of C, C needs to be rebased on top
1190 B C D B needs to be rebased on top of C, C needs to be rebased on top
1191 \|/ of D. We will rebase C first.
1191 \|/ of D. We will rebase C first.
1192 A
1192 A
1193
1193
1194 C' After rebasing C, when considering B's destination, use C'
1194 C' After rebasing C, when considering B's destination, use C'
1195 | instead of the original C.
1195 | instead of the original C.
1196 B D
1196 B D
1197 \ /
1197 \ /
1198 A
1198 A
1199 """
1199 """
1200 # pick already rebased revs with same dest from state as interesting source
1200 # pick already rebased revs with same dest from state as interesting source
1201 dest = destmap[rev]
1201 dest = destmap[rev]
1202 source = [s for s, d in state.items()
1202 source = [s for s, d in state.items()
1203 if d > 0 and destmap[s] == dest and s not in skipped]
1203 if d > 0 and destmap[s] == dest and s not in skipped]
1204
1204
1205 result = []
1205 result = []
1206 for prev in repo.changelog.parentrevs(rev):
1206 for prev in repo.changelog.parentrevs(rev):
1207 adjusted = dest
1207 adjusted = dest
1208 if prev != nullrev:
1208 if prev != nullrev:
1209 candidate = repo.revs('max(%ld and (::%d))', source, prev).first()
1209 candidate = repo.revs('max(%ld and (::%d))', source, prev).first()
1210 if candidate is not None:
1210 if candidate is not None:
1211 adjusted = state[candidate]
1211 adjusted = state[candidate]
1212 if adjusted == dest and dest in state:
1212 if adjusted == dest and dest in state:
1213 adjusted = state[dest]
1213 adjusted = state[dest]
1214 if adjusted == revtodo:
1214 if adjusted == revtodo:
1215 # sortsource should produce an order that makes this impossible
1215 # sortsource should produce an order that makes this impossible
1216 raise error.ProgrammingError(
1216 raise error.ProgrammingError(
1217 'rev %d should be rebased already at this time' % dest)
1217 'rev %d should be rebased already at this time' % dest)
1218 result.append(adjusted)
1218 result.append(adjusted)
1219 return result
1219 return result
1220
1220
1221 def _checkobsrebase(repo, ui, rebaseobsrevs, rebaseobsskipped):
1221 def _checkobsrebase(repo, ui, rebaseobsrevs, rebaseobsskipped):
1222 """
1222 """
1223 Abort if rebase will create divergence or rebase is noop because of markers
1223 Abort if rebase will create divergence or rebase is noop because of markers
1224
1224
1225 `rebaseobsrevs`: set of obsolete revision in source
1225 `rebaseobsrevs`: set of obsolete revision in source
1226 `rebaseobsskipped`: set of revisions from source skipped because they have
1226 `rebaseobsskipped`: set of revisions from source skipped because they have
1227 successors in destination or no non-obsolete successor.
1227 successors in destination or no non-obsolete successor.
1228 """
1228 """
1229 # Obsolete node with successors not in dest leads to divergence
1229 # Obsolete node with successors not in dest leads to divergence
1230 divergenceok = ui.configbool('experimental',
1230 divergenceok = ui.configbool('experimental',
1231 'evolution.allowdivergence')
1231 'evolution.allowdivergence')
1232 divergencebasecandidates = rebaseobsrevs - rebaseobsskipped
1232 divergencebasecandidates = rebaseobsrevs - rebaseobsskipped
1233
1233
1234 if divergencebasecandidates and not divergenceok:
1234 if divergencebasecandidates and not divergenceok:
1235 divhashes = (str(repo[r])
1235 divhashes = (str(repo[r])
1236 for r in divergencebasecandidates)
1236 for r in divergencebasecandidates)
1237 msg = _("this rebase will cause "
1237 msg = _("this rebase will cause "
1238 "divergences from: %s")
1238 "divergences from: %s")
1239 h = _("to force the rebase please set "
1239 h = _("to force the rebase please set "
1240 "experimental.evolution.allowdivergence=True")
1240 "experimental.evolution.allowdivergence=True")
1241 raise error.Abort(msg % (",".join(divhashes),), hint=h)
1241 raise error.Abort(msg % (",".join(divhashes),), hint=h)
1242
1242
1243 def successorrevs(unfi, rev):
1243 def successorrevs(unfi, rev):
1244 """yield revision numbers for successors of rev"""
1244 """yield revision numbers for successors of rev"""
1245 assert unfi.filtername is None
1245 assert unfi.filtername is None
1246 nodemap = unfi.changelog.nodemap
1246 nodemap = unfi.changelog.nodemap
1247 for s in obsutil.allsuccessors(unfi.obsstore, [unfi[rev].node()]):
1247 for s in obsutil.allsuccessors(unfi.obsstore, [unfi[rev].node()]):
1248 if s in nodemap:
1248 if s in nodemap:
1249 yield nodemap[s]
1249 yield nodemap[s]
1250
1250
1251 def defineparents(repo, rev, destmap, state, skipped, obsskipped):
1251 def defineparents(repo, rev, destmap, state, skipped, obsskipped):
1252 """Return new parents and optionally a merge base for rev being rebased
1252 """Return new parents and optionally a merge base for rev being rebased
1253
1253
1254 The destination specified by "dest" cannot always be used directly because
1254 The destination specified by "dest" cannot always be used directly because
1255 previously rebase result could affect destination. For example,
1255 previously rebase result could affect destination. For example,
1256
1256
1257 D E rebase -r C+D+E -d B
1257 D E rebase -r C+D+E -d B
1258 |/ C will be rebased to C'
1258 |/ C will be rebased to C'
1259 B C D's new destination will be C' instead of B
1259 B C D's new destination will be C' instead of B
1260 |/ E's new destination will be C' instead of B
1260 |/ E's new destination will be C' instead of B
1261 A
1261 A
1262
1262
1263 The new parents of a merge is slightly more complicated. See the comment
1263 The new parents of a merge is slightly more complicated. See the comment
1264 block below.
1264 block below.
1265 """
1265 """
1266 # use unfiltered changelog since successorrevs may return filtered nodes
1266 # use unfiltered changelog since successorrevs may return filtered nodes
1267 assert repo.filtername is None
1267 assert repo.filtername is None
1268 cl = repo.changelog
1268 cl = repo.changelog
1269 def isancestor(a, b):
1269 def isancestor(a, b):
1270 # take revision numbers instead of nodes
1270 # take revision numbers instead of nodes
1271 if a == b:
1271 if a == b:
1272 return True
1272 return True
1273 elif a > b:
1273 elif a > b:
1274 return False
1274 return False
1275 return cl.isancestor(cl.node(a), cl.node(b))
1275 return cl.isancestor(cl.node(a), cl.node(b))
1276
1276
1277 dest = destmap[rev]
1277 dest = destmap[rev]
1278 oldps = repo.changelog.parentrevs(rev) # old parents
1278 oldps = repo.changelog.parentrevs(rev) # old parents
1279 newps = [nullrev, nullrev] # new parents
1279 newps = [nullrev, nullrev] # new parents
1280 dests = adjustdest(repo, rev, destmap, state, skipped)
1280 dests = adjustdest(repo, rev, destmap, state, skipped)
1281 bases = list(oldps) # merge base candidates, initially just old parents
1281 bases = list(oldps) # merge base candidates, initially just old parents
1282
1282
1283 if all(r == nullrev for r in oldps[1:]):
1283 if all(r == nullrev for r in oldps[1:]):
1284 # For non-merge changeset, just move p to adjusted dest as requested.
1284 # For non-merge changeset, just move p to adjusted dest as requested.
1285 newps[0] = dests[0]
1285 newps[0] = dests[0]
1286 else:
1286 else:
1287 # For merge changeset, if we move p to dests[i] unconditionally, both
1287 # For merge changeset, if we move p to dests[i] unconditionally, both
1288 # parents may change and the end result looks like "the merge loses a
1288 # parents may change and the end result looks like "the merge loses a
1289 # parent", which is a surprise. This is a limit because "--dest" only
1289 # parent", which is a surprise. This is a limit because "--dest" only
1290 # accepts one dest per src.
1290 # accepts one dest per src.
1291 #
1291 #
1292 # Therefore, only move p with reasonable conditions (in this order):
1292 # Therefore, only move p with reasonable conditions (in this order):
1293 # 1. use dest, if dest is a descendent of (p or one of p's successors)
1293 # 1. use dest, if dest is a descendent of (p or one of p's successors)
1294 # 2. use p's rebased result, if p is rebased (state[p] > 0)
1294 # 2. use p's rebased result, if p is rebased (state[p] > 0)
1295 #
1295 #
1296 # Comparing with adjustdest, the logic here does some additional work:
1296 # Comparing with adjustdest, the logic here does some additional work:
1297 # 1. decide which parents will not be moved towards dest
1297 # 1. decide which parents will not be moved towards dest
1298 # 2. if the above decision is "no", should a parent still be moved
1298 # 2. if the above decision is "no", should a parent still be moved
1299 # because it was rebased?
1299 # because it was rebased?
1300 #
1300 #
1301 # For example:
1301 # For example:
1302 #
1302 #
1303 # C # "rebase -r C -d D" is an error since none of the parents
1303 # C # "rebase -r C -d D" is an error since none of the parents
1304 # /| # can be moved. "rebase -r B+C -d D" will move C's parent
1304 # /| # can be moved. "rebase -r B+C -d D" will move C's parent
1305 # A B D # B (using rule "2."), since B will be rebased.
1305 # A B D # B (using rule "2."), since B will be rebased.
1306 #
1306 #
1307 # The loop tries to be not rely on the fact that a Mercurial node has
1307 # The loop tries to be not rely on the fact that a Mercurial node has
1308 # at most 2 parents.
1308 # at most 2 parents.
1309 for i, p in enumerate(oldps):
1309 for i, p in enumerate(oldps):
1310 np = p # new parent
1310 np = p # new parent
1311 if any(isancestor(x, dests[i]) for x in successorrevs(repo, p)):
1311 if any(isancestor(x, dests[i]) for x in successorrevs(repo, p)):
1312 np = dests[i]
1312 np = dests[i]
1313 elif p in state and state[p] > 0:
1313 elif p in state and state[p] > 0:
1314 np = state[p]
1314 np = state[p]
1315
1315
1316 # "bases" only record "special" merge bases that cannot be
1316 # "bases" only record "special" merge bases that cannot be
1317 # calculated from changelog DAG (i.e. isancestor(p, np) is False).
1317 # calculated from changelog DAG (i.e. isancestor(p, np) is False).
1318 # For example:
1318 # For example:
1319 #
1319 #
1320 # B' # rebase -s B -d D, when B was rebased to B'. dest for C
1320 # B' # rebase -s B -d D, when B was rebased to B'. dest for C
1321 # | C # is B', but merge base for C is B, instead of
1321 # | C # is B', but merge base for C is B, instead of
1322 # D | # changelog.ancestor(C, B') == A. If changelog DAG and
1322 # D | # changelog.ancestor(C, B') == A. If changelog DAG and
1323 # | B # "state" edges are merged (so there will be an edge from
1323 # | B # "state" edges are merged (so there will be an edge from
1324 # |/ # B to B'), the merge base is still ancestor(C, B') in
1324 # |/ # B to B'), the merge base is still ancestor(C, B') in
1325 # A # the merged graph.
1325 # A # the merged graph.
1326 #
1326 #
1327 # Also see https://bz.mercurial-scm.org/show_bug.cgi?id=1950#c8
1327 # Also see https://bz.mercurial-scm.org/show_bug.cgi?id=1950#c8
1328 # which uses "virtual null merge" to explain this situation.
1328 # which uses "virtual null merge" to explain this situation.
1329 if isancestor(p, np):
1329 if isancestor(p, np):
1330 bases[i] = nullrev
1330 bases[i] = nullrev
1331
1331
1332 # If one parent becomes an ancestor of the other, drop the ancestor
1332 # If one parent becomes an ancestor of the other, drop the ancestor
1333 for j, x in enumerate(newps[:i]):
1333 for j, x in enumerate(newps[:i]):
1334 if x == nullrev:
1334 if x == nullrev:
1335 continue
1335 continue
1336 if isancestor(np, x): # CASE-1
1336 if isancestor(np, x): # CASE-1
1337 np = nullrev
1337 np = nullrev
1338 elif isancestor(x, np): # CASE-2
1338 elif isancestor(x, np): # CASE-2
1339 newps[j] = np
1339 newps[j] = np
1340 np = nullrev
1340 np = nullrev
1341 # New parents forming an ancestor relationship does not
1341 # New parents forming an ancestor relationship does not
1342 # mean the old parents have a similar relationship. Do not
1342 # mean the old parents have a similar relationship. Do not
1343 # set bases[x] to nullrev.
1343 # set bases[x] to nullrev.
1344 bases[j], bases[i] = bases[i], bases[j]
1344 bases[j], bases[i] = bases[i], bases[j]
1345
1345
1346 newps[i] = np
1346 newps[i] = np
1347
1347
1348 # "rebasenode" updates to new p1, and the old p1 will be used as merge
1348 # "rebasenode" updates to new p1, and the old p1 will be used as merge
1349 # base. If only p2 changes, merging using unchanged p1 as merge base is
1349 # base. If only p2 changes, merging using unchanged p1 as merge base is
1350 # suboptimal. Therefore swap parents to make the merge sane.
1350 # suboptimal. Therefore swap parents to make the merge sane.
1351 if newps[1] != nullrev and oldps[0] == newps[0]:
1351 if newps[1] != nullrev and oldps[0] == newps[0]:
1352 assert len(newps) == 2 and len(oldps) == 2
1352 assert len(newps) == 2 and len(oldps) == 2
1353 newps.reverse()
1353 newps.reverse()
1354 bases.reverse()
1354 bases.reverse()
1355
1355
1356 # No parent change might be an error because we fail to make rev a
1356 # No parent change might be an error because we fail to make rev a
1357 # descendent of requested dest. This can happen, for example:
1357 # descendent of requested dest. This can happen, for example:
1358 #
1358 #
1359 # C # rebase -r C -d D
1359 # C # rebase -r C -d D
1360 # /| # None of A and B will be changed to D and rebase fails.
1360 # /| # None of A and B will be changed to D and rebase fails.
1361 # A B D
1361 # A B D
1362 if set(newps) == set(oldps) and dest not in newps:
1362 if set(newps) == set(oldps) and dest not in newps:
1363 raise error.Abort(_('cannot rebase %d:%s without '
1363 raise error.Abort(_('cannot rebase %d:%s without '
1364 'moving at least one of its parents')
1364 'moving at least one of its parents')
1365 % (rev, repo[rev]))
1365 % (rev, repo[rev]))
1366
1366
1367 # Source should not be ancestor of dest. The check here guarantees it's
1367 # Source should not be ancestor of dest. The check here guarantees it's
1368 # impossible. With multi-dest, the initial check does not cover complex
1368 # impossible. With multi-dest, the initial check does not cover complex
1369 # cases since we don't have abstractions to dry-run rebase cheaply.
1369 # cases since we don't have abstractions to dry-run rebase cheaply.
1370 if any(p != nullrev and isancestor(rev, p) for p in newps):
1370 if any(p != nullrev and isancestor(rev, p) for p in newps):
1371 raise error.Abort(_('source is ancestor of destination'))
1371 raise error.Abort(_('source is ancestor of destination'))
1372
1372
1373 # "rebasenode" updates to new p1, use the corresponding merge base.
1373 # "rebasenode" updates to new p1, use the corresponding merge base.
1374 if bases[0] != nullrev:
1374 if bases[0] != nullrev:
1375 base = bases[0]
1375 base = bases[0]
1376 else:
1376 else:
1377 base = None
1377 base = None
1378
1378
1379 # Check if the merge will contain unwanted changes. That may happen if
1379 # Check if the merge will contain unwanted changes. That may happen if
1380 # there are multiple special (non-changelog ancestor) merge bases, which
1380 # there are multiple special (non-changelog ancestor) merge bases, which
1381 # cannot be handled well by the 3-way merge algorithm. For example:
1381 # cannot be handled well by the 3-way merge algorithm. For example:
1382 #
1382 #
1383 # F
1383 # F
1384 # /|
1384 # /|
1385 # D E # "rebase -r D+E+F -d Z", when rebasing F, if "D" was chosen
1385 # D E # "rebase -r D+E+F -d Z", when rebasing F, if "D" was chosen
1386 # | | # as merge base, the difference between D and F will include
1386 # | | # as merge base, the difference between D and F will include
1387 # B C # C, so the rebased F will contain C surprisingly. If "E" was
1387 # B C # C, so the rebased F will contain C surprisingly. If "E" was
1388 # |/ # chosen, the rebased F will contain B.
1388 # |/ # chosen, the rebased F will contain B.
1389 # A Z
1389 # A Z
1390 #
1390 #
1391 # But our merge base candidates (D and E in above case) could still be
1391 # But our merge base candidates (D and E in above case) could still be
1392 # better than the default (ancestor(F, Z) == null). Therefore still
1392 # better than the default (ancestor(F, Z) == null). Therefore still
1393 # pick one (so choose p1 above).
1393 # pick one (so choose p1 above).
1394 if sum(1 for b in bases if b != nullrev) > 1:
1394 if sum(1 for b in bases if b != nullrev) > 1:
1395 unwanted = [None, None] # unwanted[i]: unwanted revs if choose bases[i]
1395 unwanted = [None, None] # unwanted[i]: unwanted revs if choose bases[i]
1396 for i, base in enumerate(bases):
1396 for i, base in enumerate(bases):
1397 if base == nullrev:
1397 if base == nullrev:
1398 continue
1398 continue
1399 # Revisions in the side (not chosen as merge base) branch that
1399 # Revisions in the side (not chosen as merge base) branch that
1400 # might contain "surprising" contents
1400 # might contain "surprising" contents
1401 siderevs = list(repo.revs('((%ld-%d) %% (%d+%d))',
1401 siderevs = list(repo.revs('((%ld-%d) %% (%d+%d))',
1402 bases, base, base, dest))
1402 bases, base, base, dest))
1403
1403
1404 # If those revisions are covered by rebaseset, the result is good.
1404 # If those revisions are covered by rebaseset, the result is good.
1405 # A merge in rebaseset would be considered to cover its ancestors.
1405 # A merge in rebaseset would be considered to cover its ancestors.
1406 if siderevs:
1406 if siderevs:
1407 rebaseset = [r for r, d in state.items()
1407 rebaseset = [r for r, d in state.items()
1408 if d > 0 and r not in obsskipped]
1408 if d > 0 and r not in obsskipped]
1409 merges = [r for r in rebaseset
1409 merges = [r for r in rebaseset
1410 if cl.parentrevs(r)[1] != nullrev]
1410 if cl.parentrevs(r)[1] != nullrev]
1411 unwanted[i] = list(repo.revs('%ld - (::%ld) - %ld',
1411 unwanted[i] = list(repo.revs('%ld - (::%ld) - %ld',
1412 siderevs, merges, rebaseset))
1412 siderevs, merges, rebaseset))
1413
1413
1414 # Choose a merge base that has a minimal number of unwanted revs.
1414 # Choose a merge base that has a minimal number of unwanted revs.
1415 l, i = min((len(revs), i)
1415 l, i = min((len(revs), i)
1416 for i, revs in enumerate(unwanted) if revs is not None)
1416 for i, revs in enumerate(unwanted) if revs is not None)
1417 base = bases[i]
1417 base = bases[i]
1418
1418
1419 # newps[0] should match merge base if possible. Currently, if newps[i]
1419 # newps[0] should match merge base if possible. Currently, if newps[i]
1420 # is nullrev, the only case is newps[i] and newps[j] (j < i), one is
1420 # is nullrev, the only case is newps[i] and newps[j] (j < i), one is
1421 # the other's ancestor. In that case, it's fine to not swap newps here.
1421 # the other's ancestor. In that case, it's fine to not swap newps here.
1422 # (see CASE-1 and CASE-2 above)
1422 # (see CASE-1 and CASE-2 above)
1423 if i != 0 and newps[i] != nullrev:
1423 if i != 0 and newps[i] != nullrev:
1424 newps[0], newps[i] = newps[i], newps[0]
1424 newps[0], newps[i] = newps[i], newps[0]
1425
1425
1426 # The merge will include unwanted revisions. Abort now. Revisit this if
1426 # The merge will include unwanted revisions. Abort now. Revisit this if
1427 # we have a more advanced merge algorithm that handles multiple bases.
1427 # we have a more advanced merge algorithm that handles multiple bases.
1428 if l > 0:
1428 if l > 0:
1429 unwanteddesc = _(' or ').join(
1429 unwanteddesc = _(' or ').join(
1430 (', '.join('%d:%s' % (r, repo[r]) for r in revs)
1430 (', '.join('%d:%s' % (r, repo[r]) for r in revs)
1431 for revs in unwanted if revs is not None))
1431 for revs in unwanted if revs is not None))
1432 raise error.Abort(
1432 raise error.Abort(
1433 _('rebasing %d:%s will include unwanted changes from %s')
1433 _('rebasing %d:%s will include unwanted changes from %s')
1434 % (rev, repo[rev], unwanteddesc))
1434 % (rev, repo[rev], unwanteddesc))
1435
1435
1436 repo.ui.debug(" future parents are %d and %d\n" % tuple(newps))
1436 repo.ui.debug(" future parents are %d and %d\n" % tuple(newps))
1437
1437
1438 return newps[0], newps[1], base
1438 return newps[0], newps[1], base
1439
1439
1440 def isagitpatch(repo, patchname):
1440 def isagitpatch(repo, patchname):
1441 'Return true if the given patch is in git format'
1441 'Return true if the given patch is in git format'
1442 mqpatch = os.path.join(repo.mq.path, patchname)
1442 mqpatch = os.path.join(repo.mq.path, patchname)
1443 for line in patch.linereader(file(mqpatch, 'rb')):
1443 for line in patch.linereader(open(mqpatch, 'rb')):
1444 if line.startswith('diff --git'):
1444 if line.startswith('diff --git'):
1445 return True
1445 return True
1446 return False
1446 return False
1447
1447
1448 def updatemq(repo, state, skipped, **opts):
1448 def updatemq(repo, state, skipped, **opts):
1449 'Update rebased mq patches - finalize and then import them'
1449 'Update rebased mq patches - finalize and then import them'
1450 mqrebase = {}
1450 mqrebase = {}
1451 mq = repo.mq
1451 mq = repo.mq
1452 original_series = mq.fullseries[:]
1452 original_series = mq.fullseries[:]
1453 skippedpatches = set()
1453 skippedpatches = set()
1454
1454
1455 for p in mq.applied:
1455 for p in mq.applied:
1456 rev = repo[p.node].rev()
1456 rev = repo[p.node].rev()
1457 if rev in state:
1457 if rev in state:
1458 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
1458 repo.ui.debug('revision %d is an mq patch (%s), finalize it.\n' %
1459 (rev, p.name))
1459 (rev, p.name))
1460 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
1460 mqrebase[rev] = (p.name, isagitpatch(repo, p.name))
1461 else:
1461 else:
1462 # Applied but not rebased, not sure this should happen
1462 # Applied but not rebased, not sure this should happen
1463 skippedpatches.add(p.name)
1463 skippedpatches.add(p.name)
1464
1464
1465 if mqrebase:
1465 if mqrebase:
1466 mq.finish(repo, mqrebase.keys())
1466 mq.finish(repo, mqrebase.keys())
1467
1467
1468 # We must start import from the newest revision
1468 # We must start import from the newest revision
1469 for rev in sorted(mqrebase, reverse=True):
1469 for rev in sorted(mqrebase, reverse=True):
1470 if rev not in skipped:
1470 if rev not in skipped:
1471 name, isgit = mqrebase[rev]
1471 name, isgit = mqrebase[rev]
1472 repo.ui.note(_('updating mq patch %s to %s:%s\n') %
1472 repo.ui.note(_('updating mq patch %s to %s:%s\n') %
1473 (name, state[rev], repo[state[rev]]))
1473 (name, state[rev], repo[state[rev]]))
1474 mq.qimport(repo, (), patchname=name, git=isgit,
1474 mq.qimport(repo, (), patchname=name, git=isgit,
1475 rev=[str(state[rev])])
1475 rev=[str(state[rev])])
1476 else:
1476 else:
1477 # Rebased and skipped
1477 # Rebased and skipped
1478 skippedpatches.add(mqrebase[rev][0])
1478 skippedpatches.add(mqrebase[rev][0])
1479
1479
1480 # Patches were either applied and rebased and imported in
1480 # Patches were either applied and rebased and imported in
1481 # order, applied and removed or unapplied. Discard the removed
1481 # order, applied and removed or unapplied. Discard the removed
1482 # ones while preserving the original series order and guards.
1482 # ones while preserving the original series order and guards.
1483 newseries = [s for s in original_series
1483 newseries = [s for s in original_series
1484 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
1484 if mq.guard_re.split(s, 1)[0] not in skippedpatches]
1485 mq.fullseries[:] = newseries
1485 mq.fullseries[:] = newseries
1486 mq.seriesdirty = True
1486 mq.seriesdirty = True
1487 mq.savedirty()
1487 mq.savedirty()
1488
1488
1489 def storecollapsemsg(repo, collapsemsg):
1489 def storecollapsemsg(repo, collapsemsg):
1490 'Store the collapse message to allow recovery'
1490 'Store the collapse message to allow recovery'
1491 collapsemsg = collapsemsg or ''
1491 collapsemsg = collapsemsg or ''
1492 f = repo.vfs("last-message.txt", "w")
1492 f = repo.vfs("last-message.txt", "w")
1493 f.write("%s\n" % collapsemsg)
1493 f.write("%s\n" % collapsemsg)
1494 f.close()
1494 f.close()
1495
1495
1496 def clearcollapsemsg(repo):
1496 def clearcollapsemsg(repo):
1497 'Remove collapse message file'
1497 'Remove collapse message file'
1498 repo.vfs.unlinkpath("last-message.txt", ignoremissing=True)
1498 repo.vfs.unlinkpath("last-message.txt", ignoremissing=True)
1499
1499
1500 def restorecollapsemsg(repo, isabort):
1500 def restorecollapsemsg(repo, isabort):
1501 'Restore previously stored collapse message'
1501 'Restore previously stored collapse message'
1502 try:
1502 try:
1503 f = repo.vfs("last-message.txt")
1503 f = repo.vfs("last-message.txt")
1504 collapsemsg = f.readline().strip()
1504 collapsemsg = f.readline().strip()
1505 f.close()
1505 f.close()
1506 except IOError as err:
1506 except IOError as err:
1507 if err.errno != errno.ENOENT:
1507 if err.errno != errno.ENOENT:
1508 raise
1508 raise
1509 if isabort:
1509 if isabort:
1510 # Oh well, just abort like normal
1510 # Oh well, just abort like normal
1511 collapsemsg = ''
1511 collapsemsg = ''
1512 else:
1512 else:
1513 raise error.Abort(_('missing .hg/last-message.txt for rebase'))
1513 raise error.Abort(_('missing .hg/last-message.txt for rebase'))
1514 return collapsemsg
1514 return collapsemsg
1515
1515
1516 def clearstatus(repo):
1516 def clearstatus(repo):
1517 'Remove the status files'
1517 'Remove the status files'
1518 # Make sure the active transaction won't write the state file
1518 # Make sure the active transaction won't write the state file
1519 tr = repo.currenttransaction()
1519 tr = repo.currenttransaction()
1520 if tr:
1520 if tr:
1521 tr.removefilegenerator('rebasestate')
1521 tr.removefilegenerator('rebasestate')
1522 repo.vfs.unlinkpath("rebasestate", ignoremissing=True)
1522 repo.vfs.unlinkpath("rebasestate", ignoremissing=True)
1523
1523
1524 def needupdate(repo, state):
1524 def needupdate(repo, state):
1525 '''check whether we should `update --clean` away from a merge, or if
1525 '''check whether we should `update --clean` away from a merge, or if
1526 somehow the working dir got forcibly updated, e.g. by older hg'''
1526 somehow the working dir got forcibly updated, e.g. by older hg'''
1527 parents = [p.rev() for p in repo[None].parents()]
1527 parents = [p.rev() for p in repo[None].parents()]
1528
1528
1529 # Are we in a merge state at all?
1529 # Are we in a merge state at all?
1530 if len(parents) < 2:
1530 if len(parents) < 2:
1531 return False
1531 return False
1532
1532
1533 # We should be standing on the first as-of-yet unrebased commit.
1533 # We should be standing on the first as-of-yet unrebased commit.
1534 firstunrebased = min([old for old, new in state.iteritems()
1534 firstunrebased = min([old for old, new in state.iteritems()
1535 if new == nullrev])
1535 if new == nullrev])
1536 if firstunrebased in parents:
1536 if firstunrebased in parents:
1537 return True
1537 return True
1538
1538
1539 return False
1539 return False
1540
1540
1541 def abort(repo, originalwd, destmap, state, activebookmark=None):
1541 def abort(repo, originalwd, destmap, state, activebookmark=None):
1542 '''Restore the repository to its original state. Additional args:
1542 '''Restore the repository to its original state. Additional args:
1543
1543
1544 activebookmark: the name of the bookmark that should be active after the
1544 activebookmark: the name of the bookmark that should be active after the
1545 restore'''
1545 restore'''
1546
1546
1547 try:
1547 try:
1548 # If the first commits in the rebased set get skipped during the rebase,
1548 # If the first commits in the rebased set get skipped during the rebase,
1549 # their values within the state mapping will be the dest rev id. The
1549 # their values within the state mapping will be the dest rev id. The
1550 # dstates list must must not contain the dest rev (issue4896)
1550 # dstates list must must not contain the dest rev (issue4896)
1551 dstates = [s for r, s in state.items() if s >= 0 and s != destmap[r]]
1551 dstates = [s for r, s in state.items() if s >= 0 and s != destmap[r]]
1552 immutable = [d for d in dstates if not repo[d].mutable()]
1552 immutable = [d for d in dstates if not repo[d].mutable()]
1553 cleanup = True
1553 cleanup = True
1554 if immutable:
1554 if immutable:
1555 repo.ui.warn(_("warning: can't clean up public changesets %s\n")
1555 repo.ui.warn(_("warning: can't clean up public changesets %s\n")
1556 % ', '.join(str(repo[r]) for r in immutable),
1556 % ', '.join(str(repo[r]) for r in immutable),
1557 hint=_("see 'hg help phases' for details"))
1557 hint=_("see 'hg help phases' for details"))
1558 cleanup = False
1558 cleanup = False
1559
1559
1560 descendants = set()
1560 descendants = set()
1561 if dstates:
1561 if dstates:
1562 descendants = set(repo.changelog.descendants(dstates))
1562 descendants = set(repo.changelog.descendants(dstates))
1563 if descendants - set(dstates):
1563 if descendants - set(dstates):
1564 repo.ui.warn(_("warning: new changesets detected on destination "
1564 repo.ui.warn(_("warning: new changesets detected on destination "
1565 "branch, can't strip\n"))
1565 "branch, can't strip\n"))
1566 cleanup = False
1566 cleanup = False
1567
1567
1568 if cleanup:
1568 if cleanup:
1569 shouldupdate = False
1569 shouldupdate = False
1570 rebased = [s for r, s in state.items()
1570 rebased = [s for r, s in state.items()
1571 if s >= 0 and s != destmap[r]]
1571 if s >= 0 and s != destmap[r]]
1572 if rebased:
1572 if rebased:
1573 strippoints = [
1573 strippoints = [
1574 c.node() for c in repo.set('roots(%ld)', rebased)]
1574 c.node() for c in repo.set('roots(%ld)', rebased)]
1575
1575
1576 updateifonnodes = set(rebased)
1576 updateifonnodes = set(rebased)
1577 updateifonnodes.update(destmap.values())
1577 updateifonnodes.update(destmap.values())
1578 updateifonnodes.add(originalwd)
1578 updateifonnodes.add(originalwd)
1579 shouldupdate = repo['.'].rev() in updateifonnodes
1579 shouldupdate = repo['.'].rev() in updateifonnodes
1580
1580
1581 # Update away from the rebase if necessary
1581 # Update away from the rebase if necessary
1582 if shouldupdate or needupdate(repo, state):
1582 if shouldupdate or needupdate(repo, state):
1583 mergemod.update(repo, originalwd, False, True)
1583 mergemod.update(repo, originalwd, False, True)
1584
1584
1585 # Strip from the first rebased revision
1585 # Strip from the first rebased revision
1586 if rebased:
1586 if rebased:
1587 # no backup of rebased cset versions needed
1587 # no backup of rebased cset versions needed
1588 repair.strip(repo.ui, repo, strippoints)
1588 repair.strip(repo.ui, repo, strippoints)
1589
1589
1590 if activebookmark and activebookmark in repo._bookmarks:
1590 if activebookmark and activebookmark in repo._bookmarks:
1591 bookmarks.activate(repo, activebookmark)
1591 bookmarks.activate(repo, activebookmark)
1592
1592
1593 finally:
1593 finally:
1594 clearstatus(repo)
1594 clearstatus(repo)
1595 clearcollapsemsg(repo)
1595 clearcollapsemsg(repo)
1596 repo.ui.warn(_('rebase aborted\n'))
1596 repo.ui.warn(_('rebase aborted\n'))
1597 return 0
1597 return 0
1598
1598
1599 def sortsource(destmap):
1599 def sortsource(destmap):
1600 """yield source revisions in an order that we only rebase things once
1600 """yield source revisions in an order that we only rebase things once
1601
1601
1602 If source and destination overlaps, we should filter out revisions
1602 If source and destination overlaps, we should filter out revisions
1603 depending on other revisions which hasn't been rebased yet.
1603 depending on other revisions which hasn't been rebased yet.
1604
1604
1605 Yield a sorted list of revisions each time.
1605 Yield a sorted list of revisions each time.
1606
1606
1607 For example, when rebasing A to B, B to C. This function yields [B], then
1607 For example, when rebasing A to B, B to C. This function yields [B], then
1608 [A], indicating B needs to be rebased first.
1608 [A], indicating B needs to be rebased first.
1609
1609
1610 Raise if there is a cycle so the rebase is impossible.
1610 Raise if there is a cycle so the rebase is impossible.
1611 """
1611 """
1612 srcset = set(destmap)
1612 srcset = set(destmap)
1613 while srcset:
1613 while srcset:
1614 srclist = sorted(srcset)
1614 srclist = sorted(srcset)
1615 result = []
1615 result = []
1616 for r in srclist:
1616 for r in srclist:
1617 if destmap[r] not in srcset:
1617 if destmap[r] not in srcset:
1618 result.append(r)
1618 result.append(r)
1619 if not result:
1619 if not result:
1620 raise error.Abort(_('source and destination form a cycle'))
1620 raise error.Abort(_('source and destination form a cycle'))
1621 srcset -= set(result)
1621 srcset -= set(result)
1622 yield result
1622 yield result
1623
1623
1624 def buildstate(repo, destmap, collapse):
1624 def buildstate(repo, destmap, collapse):
1625 '''Define which revisions are going to be rebased and where
1625 '''Define which revisions are going to be rebased and where
1626
1626
1627 repo: repo
1627 repo: repo
1628 destmap: {srcrev: destrev}
1628 destmap: {srcrev: destrev}
1629 '''
1629 '''
1630 rebaseset = destmap.keys()
1630 rebaseset = destmap.keys()
1631 originalwd = repo['.'].rev()
1631 originalwd = repo['.'].rev()
1632
1632
1633 # This check isn't strictly necessary, since mq detects commits over an
1633 # This check isn't strictly necessary, since mq detects commits over an
1634 # applied patch. But it prevents messing up the working directory when
1634 # applied patch. But it prevents messing up the working directory when
1635 # a partially completed rebase is blocked by mq.
1635 # a partially completed rebase is blocked by mq.
1636 if 'qtip' in repo.tags():
1636 if 'qtip' in repo.tags():
1637 mqapplied = set(repo[s.node].rev() for s in repo.mq.applied)
1637 mqapplied = set(repo[s.node].rev() for s in repo.mq.applied)
1638 if set(destmap.values()) & mqapplied:
1638 if set(destmap.values()) & mqapplied:
1639 raise error.Abort(_('cannot rebase onto an applied mq patch'))
1639 raise error.Abort(_('cannot rebase onto an applied mq patch'))
1640
1640
1641 # Get "cycle" error early by exhausting the generator.
1641 # Get "cycle" error early by exhausting the generator.
1642 sortedsrc = list(sortsource(destmap)) # a list of sorted revs
1642 sortedsrc = list(sortsource(destmap)) # a list of sorted revs
1643 if not sortedsrc:
1643 if not sortedsrc:
1644 raise error.Abort(_('no matching revisions'))
1644 raise error.Abort(_('no matching revisions'))
1645
1645
1646 # Only check the first batch of revisions to rebase not depending on other
1646 # Only check the first batch of revisions to rebase not depending on other
1647 # rebaseset. This means "source is ancestor of destination" for the second
1647 # rebaseset. This means "source is ancestor of destination" for the second
1648 # (and following) batches of revisions are not checked here. We rely on
1648 # (and following) batches of revisions are not checked here. We rely on
1649 # "defineparents" to do that check.
1649 # "defineparents" to do that check.
1650 roots = list(repo.set('roots(%ld)', sortedsrc[0]))
1650 roots = list(repo.set('roots(%ld)', sortedsrc[0]))
1651 if not roots:
1651 if not roots:
1652 raise error.Abort(_('no matching revisions'))
1652 raise error.Abort(_('no matching revisions'))
1653 def revof(r):
1653 def revof(r):
1654 return r.rev()
1654 return r.rev()
1655 roots = sorted(roots, key=revof)
1655 roots = sorted(roots, key=revof)
1656 state = dict.fromkeys(rebaseset, revtodo)
1656 state = dict.fromkeys(rebaseset, revtodo)
1657 emptyrebase = (len(sortedsrc) == 1)
1657 emptyrebase = (len(sortedsrc) == 1)
1658 for root in roots:
1658 for root in roots:
1659 dest = repo[destmap[root.rev()]]
1659 dest = repo[destmap[root.rev()]]
1660 commonbase = root.ancestor(dest)
1660 commonbase = root.ancestor(dest)
1661 if commonbase == root:
1661 if commonbase == root:
1662 raise error.Abort(_('source is ancestor of destination'))
1662 raise error.Abort(_('source is ancestor of destination'))
1663 if commonbase == dest:
1663 if commonbase == dest:
1664 wctx = repo[None]
1664 wctx = repo[None]
1665 if dest == wctx.p1():
1665 if dest == wctx.p1():
1666 # when rebasing to '.', it will use the current wd branch name
1666 # when rebasing to '.', it will use the current wd branch name
1667 samebranch = root.branch() == wctx.branch()
1667 samebranch = root.branch() == wctx.branch()
1668 else:
1668 else:
1669 samebranch = root.branch() == dest.branch()
1669 samebranch = root.branch() == dest.branch()
1670 if not collapse and samebranch and dest in root.parents():
1670 if not collapse and samebranch and dest in root.parents():
1671 # mark the revision as done by setting its new revision
1671 # mark the revision as done by setting its new revision
1672 # equal to its old (current) revisions
1672 # equal to its old (current) revisions
1673 state[root.rev()] = root.rev()
1673 state[root.rev()] = root.rev()
1674 repo.ui.debug('source is a child of destination\n')
1674 repo.ui.debug('source is a child of destination\n')
1675 continue
1675 continue
1676
1676
1677 emptyrebase = False
1677 emptyrebase = False
1678 repo.ui.debug('rebase onto %s starting from %s\n' % (dest, root))
1678 repo.ui.debug('rebase onto %s starting from %s\n' % (dest, root))
1679 if emptyrebase:
1679 if emptyrebase:
1680 return None
1680 return None
1681 for rev in sorted(state):
1681 for rev in sorted(state):
1682 parents = [p for p in repo.changelog.parentrevs(rev) if p != nullrev]
1682 parents = [p for p in repo.changelog.parentrevs(rev) if p != nullrev]
1683 # if all parents of this revision are done, then so is this revision
1683 # if all parents of this revision are done, then so is this revision
1684 if parents and all((state.get(p) == p for p in parents)):
1684 if parents and all((state.get(p) == p for p in parents)):
1685 state[rev] = rev
1685 state[rev] = rev
1686 return originalwd, destmap, state
1686 return originalwd, destmap, state
1687
1687
1688 def clearrebased(ui, repo, destmap, state, skipped, collapsedas=None,
1688 def clearrebased(ui, repo, destmap, state, skipped, collapsedas=None,
1689 keepf=False, fm=None):
1689 keepf=False, fm=None):
1690 """dispose of rebased revision at the end of the rebase
1690 """dispose of rebased revision at the end of the rebase
1691
1691
1692 If `collapsedas` is not None, the rebase was a collapse whose result if the
1692 If `collapsedas` is not None, the rebase was a collapse whose result if the
1693 `collapsedas` node.
1693 `collapsedas` node.
1694
1694
1695 If `keepf` is not True, the rebase has --keep set and no nodes should be
1695 If `keepf` is not True, the rebase has --keep set and no nodes should be
1696 removed (but bookmarks still need to be moved).
1696 removed (but bookmarks still need to be moved).
1697 """
1697 """
1698 tonode = repo.changelog.node
1698 tonode = repo.changelog.node
1699 replacements = {}
1699 replacements = {}
1700 moves = {}
1700 moves = {}
1701 for rev, newrev in sorted(state.items()):
1701 for rev, newrev in sorted(state.items()):
1702 if newrev >= 0 and newrev != rev:
1702 if newrev >= 0 and newrev != rev:
1703 oldnode = tonode(rev)
1703 oldnode = tonode(rev)
1704 newnode = collapsedas or tonode(newrev)
1704 newnode = collapsedas or tonode(newrev)
1705 moves[oldnode] = newnode
1705 moves[oldnode] = newnode
1706 if not keepf:
1706 if not keepf:
1707 if rev in skipped:
1707 if rev in skipped:
1708 succs = ()
1708 succs = ()
1709 else:
1709 else:
1710 succs = (newnode,)
1710 succs = (newnode,)
1711 replacements[oldnode] = succs
1711 replacements[oldnode] = succs
1712 scmutil.cleanupnodes(repo, replacements, 'rebase', moves)
1712 scmutil.cleanupnodes(repo, replacements, 'rebase', moves)
1713 if fm:
1713 if fm:
1714 hf = fm.hexfunc
1714 hf = fm.hexfunc
1715 fl = fm.formatlist
1715 fl = fm.formatlist
1716 fd = fm.formatdict
1716 fd = fm.formatdict
1717 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1717 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1718 for oldn, newn in replacements.iteritems()},
1718 for oldn, newn in replacements.iteritems()},
1719 key="oldnode", value="newnodes")
1719 key="oldnode", value="newnodes")
1720 fm.data(nodechanges=nodechanges)
1720 fm.data(nodechanges=nodechanges)
1721
1721
1722 def pullrebase(orig, ui, repo, *args, **opts):
1722 def pullrebase(orig, ui, repo, *args, **opts):
1723 'Call rebase after pull if the latter has been invoked with --rebase'
1723 'Call rebase after pull if the latter has been invoked with --rebase'
1724 ret = None
1724 ret = None
1725 if opts.get(r'rebase'):
1725 if opts.get(r'rebase'):
1726 if ui.configbool('commands', 'rebase.requiredest'):
1726 if ui.configbool('commands', 'rebase.requiredest'):
1727 msg = _('rebase destination required by configuration')
1727 msg = _('rebase destination required by configuration')
1728 hint = _('use hg pull followed by hg rebase -d DEST')
1728 hint = _('use hg pull followed by hg rebase -d DEST')
1729 raise error.Abort(msg, hint=hint)
1729 raise error.Abort(msg, hint=hint)
1730
1730
1731 with repo.wlock(), repo.lock():
1731 with repo.wlock(), repo.lock():
1732 if opts.get(r'update'):
1732 if opts.get(r'update'):
1733 del opts[r'update']
1733 del opts[r'update']
1734 ui.debug('--update and --rebase are not compatible, ignoring '
1734 ui.debug('--update and --rebase are not compatible, ignoring '
1735 'the update flag\n')
1735 'the update flag\n')
1736
1736
1737 cmdutil.checkunfinished(repo)
1737 cmdutil.checkunfinished(repo)
1738 cmdutil.bailifchanged(repo, hint=_('cannot pull with rebase: '
1738 cmdutil.bailifchanged(repo, hint=_('cannot pull with rebase: '
1739 'please commit or shelve your changes first'))
1739 'please commit or shelve your changes first'))
1740
1740
1741 revsprepull = len(repo)
1741 revsprepull = len(repo)
1742 origpostincoming = commands.postincoming
1742 origpostincoming = commands.postincoming
1743 def _dummy(*args, **kwargs):
1743 def _dummy(*args, **kwargs):
1744 pass
1744 pass
1745 commands.postincoming = _dummy
1745 commands.postincoming = _dummy
1746 try:
1746 try:
1747 ret = orig(ui, repo, *args, **opts)
1747 ret = orig(ui, repo, *args, **opts)
1748 finally:
1748 finally:
1749 commands.postincoming = origpostincoming
1749 commands.postincoming = origpostincoming
1750 revspostpull = len(repo)
1750 revspostpull = len(repo)
1751 if revspostpull > revsprepull:
1751 if revspostpull > revsprepull:
1752 # --rev option from pull conflict with rebase own --rev
1752 # --rev option from pull conflict with rebase own --rev
1753 # dropping it
1753 # dropping it
1754 if r'rev' in opts:
1754 if r'rev' in opts:
1755 del opts[r'rev']
1755 del opts[r'rev']
1756 # positional argument from pull conflicts with rebase's own
1756 # positional argument from pull conflicts with rebase's own
1757 # --source.
1757 # --source.
1758 if r'source' in opts:
1758 if r'source' in opts:
1759 del opts[r'source']
1759 del opts[r'source']
1760 # revsprepull is the len of the repo, not revnum of tip.
1760 # revsprepull is the len of the repo, not revnum of tip.
1761 destspace = list(repo.changelog.revs(start=revsprepull))
1761 destspace = list(repo.changelog.revs(start=revsprepull))
1762 opts[r'_destspace'] = destspace
1762 opts[r'_destspace'] = destspace
1763 try:
1763 try:
1764 rebase(ui, repo, **opts)
1764 rebase(ui, repo, **opts)
1765 except error.NoMergeDestAbort:
1765 except error.NoMergeDestAbort:
1766 # we can maybe update instead
1766 # we can maybe update instead
1767 rev, _a, _b = destutil.destupdate(repo)
1767 rev, _a, _b = destutil.destupdate(repo)
1768 if rev == repo['.'].rev():
1768 if rev == repo['.'].rev():
1769 ui.status(_('nothing to rebase\n'))
1769 ui.status(_('nothing to rebase\n'))
1770 else:
1770 else:
1771 ui.status(_('nothing to rebase - updating instead\n'))
1771 ui.status(_('nothing to rebase - updating instead\n'))
1772 # not passing argument to get the bare update behavior
1772 # not passing argument to get the bare update behavior
1773 # with warning and trumpets
1773 # with warning and trumpets
1774 commands.update(ui, repo)
1774 commands.update(ui, repo)
1775 else:
1775 else:
1776 if opts.get(r'tool'):
1776 if opts.get(r'tool'):
1777 raise error.Abort(_('--tool can only be used with --rebase'))
1777 raise error.Abort(_('--tool can only be used with --rebase'))
1778 ret = orig(ui, repo, *args, **opts)
1778 ret = orig(ui, repo, *args, **opts)
1779
1779
1780 return ret
1780 return ret
1781
1781
1782 def _filterobsoleterevs(repo, revs):
1782 def _filterobsoleterevs(repo, revs):
1783 """returns a set of the obsolete revisions in revs"""
1783 """returns a set of the obsolete revisions in revs"""
1784 return set(r for r in revs if repo[r].obsolete())
1784 return set(r for r in revs if repo[r].obsolete())
1785
1785
1786 def _computeobsoletenotrebased(repo, rebaseobsrevs, destmap):
1786 def _computeobsoletenotrebased(repo, rebaseobsrevs, destmap):
1787 """Return (obsoletenotrebased, obsoletewithoutsuccessorindestination).
1787 """Return (obsoletenotrebased, obsoletewithoutsuccessorindestination).
1788
1788
1789 `obsoletenotrebased` is a mapping mapping obsolete => successor for all
1789 `obsoletenotrebased` is a mapping mapping obsolete => successor for all
1790 obsolete nodes to be rebased given in `rebaseobsrevs`.
1790 obsolete nodes to be rebased given in `rebaseobsrevs`.
1791
1791
1792 `obsoletewithoutsuccessorindestination` is a set with obsolete revisions
1792 `obsoletewithoutsuccessorindestination` is a set with obsolete revisions
1793 without a successor in destination.
1793 without a successor in destination.
1794
1794
1795 `obsoleteextinctsuccessors` is a set of obsolete revisions with only
1795 `obsoleteextinctsuccessors` is a set of obsolete revisions with only
1796 obsolete successors.
1796 obsolete successors.
1797 """
1797 """
1798 obsoletenotrebased = {}
1798 obsoletenotrebased = {}
1799 obsoletewithoutsuccessorindestination = set([])
1799 obsoletewithoutsuccessorindestination = set([])
1800 obsoleteextinctsuccessors = set([])
1800 obsoleteextinctsuccessors = set([])
1801
1801
1802 assert repo.filtername is None
1802 assert repo.filtername is None
1803 cl = repo.changelog
1803 cl = repo.changelog
1804 nodemap = cl.nodemap
1804 nodemap = cl.nodemap
1805 extinctnodes = set(cl.node(r) for r in repo.revs('extinct()'))
1805 extinctnodes = set(cl.node(r) for r in repo.revs('extinct()'))
1806 for srcrev in rebaseobsrevs:
1806 for srcrev in rebaseobsrevs:
1807 srcnode = cl.node(srcrev)
1807 srcnode = cl.node(srcrev)
1808 destnode = cl.node(destmap[srcrev])
1808 destnode = cl.node(destmap[srcrev])
1809 # XXX: more advanced APIs are required to handle split correctly
1809 # XXX: more advanced APIs are required to handle split correctly
1810 successors = set(obsutil.allsuccessors(repo.obsstore, [srcnode]))
1810 successors = set(obsutil.allsuccessors(repo.obsstore, [srcnode]))
1811 # obsutil.allsuccessors includes node itself
1811 # obsutil.allsuccessors includes node itself
1812 successors.remove(srcnode)
1812 successors.remove(srcnode)
1813 if successors.issubset(extinctnodes):
1813 if successors.issubset(extinctnodes):
1814 # all successors are extinct
1814 # all successors are extinct
1815 obsoleteextinctsuccessors.add(srcrev)
1815 obsoleteextinctsuccessors.add(srcrev)
1816 if not successors:
1816 if not successors:
1817 # no successor
1817 # no successor
1818 obsoletenotrebased[srcrev] = None
1818 obsoletenotrebased[srcrev] = None
1819 else:
1819 else:
1820 for succnode in successors:
1820 for succnode in successors:
1821 if succnode not in nodemap:
1821 if succnode not in nodemap:
1822 continue
1822 continue
1823 if cl.isancestor(succnode, destnode):
1823 if cl.isancestor(succnode, destnode):
1824 obsoletenotrebased[srcrev] = nodemap[succnode]
1824 obsoletenotrebased[srcrev] = nodemap[succnode]
1825 break
1825 break
1826 else:
1826 else:
1827 # If 'srcrev' has a successor in rebase set but none in
1827 # If 'srcrev' has a successor in rebase set but none in
1828 # destination (which would be catched above), we shall skip it
1828 # destination (which would be catched above), we shall skip it
1829 # and its descendants to avoid divergence.
1829 # and its descendants to avoid divergence.
1830 if any(nodemap[s] in destmap for s in successors):
1830 if any(nodemap[s] in destmap for s in successors):
1831 obsoletewithoutsuccessorindestination.add(srcrev)
1831 obsoletewithoutsuccessorindestination.add(srcrev)
1832
1832
1833 return (
1833 return (
1834 obsoletenotrebased,
1834 obsoletenotrebased,
1835 obsoletewithoutsuccessorindestination,
1835 obsoletewithoutsuccessorindestination,
1836 obsoleteextinctsuccessors,
1836 obsoleteextinctsuccessors,
1837 )
1837 )
1838
1838
1839 def summaryhook(ui, repo):
1839 def summaryhook(ui, repo):
1840 if not repo.vfs.exists('rebasestate'):
1840 if not repo.vfs.exists('rebasestate'):
1841 return
1841 return
1842 try:
1842 try:
1843 rbsrt = rebaseruntime(repo, ui, {})
1843 rbsrt = rebaseruntime(repo, ui, {})
1844 rbsrt.restorestatus()
1844 rbsrt.restorestatus()
1845 state = rbsrt.state
1845 state = rbsrt.state
1846 except error.RepoLookupError:
1846 except error.RepoLookupError:
1847 # i18n: column positioning for "hg summary"
1847 # i18n: column positioning for "hg summary"
1848 msg = _('rebase: (use "hg rebase --abort" to clear broken state)\n')
1848 msg = _('rebase: (use "hg rebase --abort" to clear broken state)\n')
1849 ui.write(msg)
1849 ui.write(msg)
1850 return
1850 return
1851 numrebased = len([i for i in state.itervalues() if i >= 0])
1851 numrebased = len([i for i in state.itervalues() if i >= 0])
1852 # i18n: column positioning for "hg summary"
1852 # i18n: column positioning for "hg summary"
1853 ui.write(_('rebase: %s, %s (rebase --continue)\n') %
1853 ui.write(_('rebase: %s, %s (rebase --continue)\n') %
1854 (ui.label(_('%d rebased'), 'rebase.rebased') % numrebased,
1854 (ui.label(_('%d rebased'), 'rebase.rebased') % numrebased,
1855 ui.label(_('%d remaining'), 'rebase.remaining') %
1855 ui.label(_('%d remaining'), 'rebase.remaining') %
1856 (len(state) - numrebased)))
1856 (len(state) - numrebased)))
1857
1857
1858 def uisetup(ui):
1858 def uisetup(ui):
1859 #Replace pull with a decorator to provide --rebase option
1859 #Replace pull with a decorator to provide --rebase option
1860 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
1860 entry = extensions.wrapcommand(commands.table, 'pull', pullrebase)
1861 entry[1].append(('', 'rebase', None,
1861 entry[1].append(('', 'rebase', None,
1862 _("rebase working directory to branch head")))
1862 _("rebase working directory to branch head")))
1863 entry[1].append(('t', 'tool', '',
1863 entry[1].append(('t', 'tool', '',
1864 _("specify merge tool for rebase")))
1864 _("specify merge tool for rebase")))
1865 cmdutil.summaryhooks.add('rebase', summaryhook)
1865 cmdutil.summaryhooks.add('rebase', summaryhook)
1866 cmdutil.unfinishedstates.append(
1866 cmdutil.unfinishedstates.append(
1867 ['rebasestate', False, False, _('rebase in progress'),
1867 ['rebasestate', False, False, _('rebase in progress'),
1868 _("use 'hg rebase --continue' or 'hg rebase --abort'")])
1868 _("use 'hg rebase --continue' or 'hg rebase --abort'")])
1869 cmdutil.afterresolvedstates.append(
1869 cmdutil.afterresolvedstates.append(
1870 ['rebasestate', _('hg rebase --continue')])
1870 ['rebasestate', _('hg rebase --continue')])
@@ -1,195 +1,195 b''
1 # Mercurial extension to provide 'hg relink' command
1 # Mercurial extension to provide 'hg relink' command
2 #
2 #
3 # Copyright (C) 2007 Brendan Cully <brendan@kublai.com>
3 # Copyright (C) 2007 Brendan Cully <brendan@kublai.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """recreates hardlinks between repository clones"""
8 """recreates hardlinks between repository clones"""
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import os
11 import os
12 import stat
12 import stat
13
13
14 from mercurial.i18n import _
14 from mercurial.i18n import _
15 from mercurial import (
15 from mercurial import (
16 error,
16 error,
17 hg,
17 hg,
18 registrar,
18 registrar,
19 util,
19 util,
20 )
20 )
21
21
22 cmdtable = {}
22 cmdtable = {}
23 command = registrar.command(cmdtable)
23 command = registrar.command(cmdtable)
24 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
24 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
25 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
25 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
26 # be specifying the version(s) of Mercurial they are tested with, or
26 # be specifying the version(s) of Mercurial they are tested with, or
27 # leave the attribute unspecified.
27 # leave the attribute unspecified.
28 testedwith = 'ships-with-hg-core'
28 testedwith = 'ships-with-hg-core'
29
29
30 @command('relink', [], _('[ORIGIN]'))
30 @command('relink', [], _('[ORIGIN]'))
31 def relink(ui, repo, origin=None, **opts):
31 def relink(ui, repo, origin=None, **opts):
32 """recreate hardlinks between two repositories
32 """recreate hardlinks between two repositories
33
33
34 When repositories are cloned locally, their data files will be
34 When repositories are cloned locally, their data files will be
35 hardlinked so that they only use the space of a single repository.
35 hardlinked so that they only use the space of a single repository.
36
36
37 Unfortunately, subsequent pulls into either repository will break
37 Unfortunately, subsequent pulls into either repository will break
38 hardlinks for any files touched by the new changesets, even if
38 hardlinks for any files touched by the new changesets, even if
39 both repositories end up pulling the same changes.
39 both repositories end up pulling the same changes.
40
40
41 Similarly, passing --rev to "hg clone" will fail to use any
41 Similarly, passing --rev to "hg clone" will fail to use any
42 hardlinks, falling back to a complete copy of the source
42 hardlinks, falling back to a complete copy of the source
43 repository.
43 repository.
44
44
45 This command lets you recreate those hardlinks and reclaim that
45 This command lets you recreate those hardlinks and reclaim that
46 wasted space.
46 wasted space.
47
47
48 This repository will be relinked to share space with ORIGIN, which
48 This repository will be relinked to share space with ORIGIN, which
49 must be on the same local disk. If ORIGIN is omitted, looks for
49 must be on the same local disk. If ORIGIN is omitted, looks for
50 "default-relink", then "default", in [paths].
50 "default-relink", then "default", in [paths].
51
51
52 Do not attempt any read operations on this repository while the
52 Do not attempt any read operations on this repository while the
53 command is running. (Both repositories will be locked against
53 command is running. (Both repositories will be locked against
54 writes.)
54 writes.)
55 """
55 """
56 if (not util.safehasattr(util, 'samefile') or
56 if (not util.safehasattr(util, 'samefile') or
57 not util.safehasattr(util, 'samedevice')):
57 not util.safehasattr(util, 'samedevice')):
58 raise error.Abort(_('hardlinks are not supported on this system'))
58 raise error.Abort(_('hardlinks are not supported on this system'))
59 src = hg.repository(repo.baseui, ui.expandpath(origin or 'default-relink',
59 src = hg.repository(repo.baseui, ui.expandpath(origin or 'default-relink',
60 origin or 'default'))
60 origin or 'default'))
61 ui.status(_('relinking %s to %s\n') % (src.store.path, repo.store.path))
61 ui.status(_('relinking %s to %s\n') % (src.store.path, repo.store.path))
62 if repo.root == src.root:
62 if repo.root == src.root:
63 ui.status(_('there is nothing to relink\n'))
63 ui.status(_('there is nothing to relink\n'))
64 return
64 return
65
65
66 if not util.samedevice(src.store.path, repo.store.path):
66 if not util.samedevice(src.store.path, repo.store.path):
67 # No point in continuing
67 # No point in continuing
68 raise error.Abort(_('source and destination are on different devices'))
68 raise error.Abort(_('source and destination are on different devices'))
69
69
70 locallock = repo.lock()
70 locallock = repo.lock()
71 try:
71 try:
72 remotelock = src.lock()
72 remotelock = src.lock()
73 try:
73 try:
74 candidates = sorted(collect(src, ui))
74 candidates = sorted(collect(src, ui))
75 targets = prune(candidates, src.store.path, repo.store.path, ui)
75 targets = prune(candidates, src.store.path, repo.store.path, ui)
76 do_relink(src.store.path, repo.store.path, targets, ui)
76 do_relink(src.store.path, repo.store.path, targets, ui)
77 finally:
77 finally:
78 remotelock.release()
78 remotelock.release()
79 finally:
79 finally:
80 locallock.release()
80 locallock.release()
81
81
82 def collect(src, ui):
82 def collect(src, ui):
83 seplen = len(os.path.sep)
83 seplen = len(os.path.sep)
84 candidates = []
84 candidates = []
85 live = len(src['tip'].manifest())
85 live = len(src['tip'].manifest())
86 # Your average repository has some files which were deleted before
86 # Your average repository has some files which were deleted before
87 # the tip revision. We account for that by assuming that there are
87 # the tip revision. We account for that by assuming that there are
88 # 3 tracked files for every 2 live files as of the tip version of
88 # 3 tracked files for every 2 live files as of the tip version of
89 # the repository.
89 # the repository.
90 #
90 #
91 # mozilla-central as of 2010-06-10 had a ratio of just over 7:5.
91 # mozilla-central as of 2010-06-10 had a ratio of just over 7:5.
92 total = live * 3 // 2
92 total = live * 3 // 2
93 src = src.store.path
93 src = src.store.path
94 pos = 0
94 pos = 0
95 ui.status(_("tip has %d files, estimated total number of files: %d\n")
95 ui.status(_("tip has %d files, estimated total number of files: %d\n")
96 % (live, total))
96 % (live, total))
97 for dirpath, dirnames, filenames in os.walk(src):
97 for dirpath, dirnames, filenames in os.walk(src):
98 dirnames.sort()
98 dirnames.sort()
99 relpath = dirpath[len(src) + seplen:]
99 relpath = dirpath[len(src) + seplen:]
100 for filename in sorted(filenames):
100 for filename in sorted(filenames):
101 if filename[-2:] not in ('.d', '.i'):
101 if filename[-2:] not in ('.d', '.i'):
102 continue
102 continue
103 st = os.stat(os.path.join(dirpath, filename))
103 st = os.stat(os.path.join(dirpath, filename))
104 if not stat.S_ISREG(st.st_mode):
104 if not stat.S_ISREG(st.st_mode):
105 continue
105 continue
106 pos += 1
106 pos += 1
107 candidates.append((os.path.join(relpath, filename), st))
107 candidates.append((os.path.join(relpath, filename), st))
108 ui.progress(_('collecting'), pos, filename, _('files'), total)
108 ui.progress(_('collecting'), pos, filename, _('files'), total)
109
109
110 ui.progress(_('collecting'), None)
110 ui.progress(_('collecting'), None)
111 ui.status(_('collected %d candidate storage files\n') % len(candidates))
111 ui.status(_('collected %d candidate storage files\n') % len(candidates))
112 return candidates
112 return candidates
113
113
114 def prune(candidates, src, dst, ui):
114 def prune(candidates, src, dst, ui):
115 def linkfilter(src, dst, st):
115 def linkfilter(src, dst, st):
116 try:
116 try:
117 ts = os.stat(dst)
117 ts = os.stat(dst)
118 except OSError:
118 except OSError:
119 # Destination doesn't have this file?
119 # Destination doesn't have this file?
120 return False
120 return False
121 if util.samefile(src, dst):
121 if util.samefile(src, dst):
122 return False
122 return False
123 if not util.samedevice(src, dst):
123 if not util.samedevice(src, dst):
124 # No point in continuing
124 # No point in continuing
125 raise error.Abort(
125 raise error.Abort(
126 _('source and destination are on different devices'))
126 _('source and destination are on different devices'))
127 if st.st_size != ts.st_size:
127 if st.st_size != ts.st_size:
128 return False
128 return False
129 return st
129 return st
130
130
131 targets = []
131 targets = []
132 total = len(candidates)
132 total = len(candidates)
133 pos = 0
133 pos = 0
134 for fn, st in candidates:
134 for fn, st in candidates:
135 pos += 1
135 pos += 1
136 srcpath = os.path.join(src, fn)
136 srcpath = os.path.join(src, fn)
137 tgt = os.path.join(dst, fn)
137 tgt = os.path.join(dst, fn)
138 ts = linkfilter(srcpath, tgt, st)
138 ts = linkfilter(srcpath, tgt, st)
139 if not ts:
139 if not ts:
140 ui.debug('not linkable: %s\n' % fn)
140 ui.debug('not linkable: %s\n' % fn)
141 continue
141 continue
142 targets.append((fn, ts.st_size))
142 targets.append((fn, ts.st_size))
143 ui.progress(_('pruning'), pos, fn, _('files'), total)
143 ui.progress(_('pruning'), pos, fn, _('files'), total)
144
144
145 ui.progress(_('pruning'), None)
145 ui.progress(_('pruning'), None)
146 ui.status(_('pruned down to %d probably relinkable files\n') % len(targets))
146 ui.status(_('pruned down to %d probably relinkable files\n') % len(targets))
147 return targets
147 return targets
148
148
149 def do_relink(src, dst, files, ui):
149 def do_relink(src, dst, files, ui):
150 def relinkfile(src, dst):
150 def relinkfile(src, dst):
151 bak = dst + '.bak'
151 bak = dst + '.bak'
152 os.rename(dst, bak)
152 os.rename(dst, bak)
153 try:
153 try:
154 util.oslink(src, dst)
154 util.oslink(src, dst)
155 except OSError:
155 except OSError:
156 os.rename(bak, dst)
156 os.rename(bak, dst)
157 raise
157 raise
158 os.remove(bak)
158 os.remove(bak)
159
159
160 CHUNKLEN = 65536
160 CHUNKLEN = 65536
161 relinked = 0
161 relinked = 0
162 savedbytes = 0
162 savedbytes = 0
163
163
164 pos = 0
164 pos = 0
165 total = len(files)
165 total = len(files)
166 for f, sz in files:
166 for f, sz in files:
167 pos += 1
167 pos += 1
168 source = os.path.join(src, f)
168 source = os.path.join(src, f)
169 tgt = os.path.join(dst, f)
169 tgt = os.path.join(dst, f)
170 # Binary mode, so that read() works correctly, especially on Windows
170 # Binary mode, so that read() works correctly, especially on Windows
171 sfp = file(source, 'rb')
171 sfp = open(source, 'rb')
172 dfp = file(tgt, 'rb')
172 dfp = open(tgt, 'rb')
173 sin = sfp.read(CHUNKLEN)
173 sin = sfp.read(CHUNKLEN)
174 while sin:
174 while sin:
175 din = dfp.read(CHUNKLEN)
175 din = dfp.read(CHUNKLEN)
176 if sin != din:
176 if sin != din:
177 break
177 break
178 sin = sfp.read(CHUNKLEN)
178 sin = sfp.read(CHUNKLEN)
179 sfp.close()
179 sfp.close()
180 dfp.close()
180 dfp.close()
181 if sin:
181 if sin:
182 ui.debug('not linkable: %s\n' % f)
182 ui.debug('not linkable: %s\n' % f)
183 continue
183 continue
184 try:
184 try:
185 relinkfile(source, tgt)
185 relinkfile(source, tgt)
186 ui.progress(_('relinking'), pos, f, _('files'), total)
186 ui.progress(_('relinking'), pos, f, _('files'), total)
187 relinked += 1
187 relinked += 1
188 savedbytes += sz
188 savedbytes += sz
189 except OSError as inst:
189 except OSError as inst:
190 ui.warn('%s: %s\n' % (tgt, str(inst)))
190 ui.warn('%s: %s\n' % (tgt, str(inst)))
191
191
192 ui.progress(_('relinking'), None)
192 ui.progress(_('relinking'), None)
193
193
194 ui.status(_('relinked %d files (%s reclaimed)\n') %
194 ui.status(_('relinked %d files (%s reclaimed)\n') %
195 (relinked, util.bytecount(savedbytes)))
195 (relinked, util.bytecount(savedbytes)))
@@ -1,885 +1,885 b''
1 #require serve
1 #require serve
2
2
3 Some tests for hgweb. Tests static files, plain files and different 404's.
3 Some tests for hgweb. Tests static files, plain files and different 404's.
4
4
5 $ hg init test
5 $ hg init test
6 $ cd test
6 $ cd test
7 $ mkdir da
7 $ mkdir da
8 $ echo foo > da/foo
8 $ echo foo > da/foo
9 $ echo foo > foo
9 $ echo foo > foo
10 $ hg ci -Ambase
10 $ hg ci -Ambase
11 adding da/foo
11 adding da/foo
12 adding foo
12 adding foo
13 $ hg bookmark -r0 '@'
13 $ hg bookmark -r0 '@'
14 $ hg bookmark -r0 'a b c'
14 $ hg bookmark -r0 'a b c'
15 $ hg bookmark -r0 'd/e/f'
15 $ hg bookmark -r0 'd/e/f'
16 $ hg serve -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
16 $ hg serve -n test -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
17 $ cat hg.pid >> $DAEMON_PIDS
17 $ cat hg.pid >> $DAEMON_PIDS
18
18
19 manifest
19 manifest
20
20
21 $ (get-with-headers.py localhost:$HGPORT 'file/tip/?style=raw')
21 $ (get-with-headers.py localhost:$HGPORT 'file/tip/?style=raw')
22 200 Script output follows
22 200 Script output follows
23
23
24
24
25 drwxr-xr-x da
25 drwxr-xr-x da
26 -rw-r--r-- 4 foo
26 -rw-r--r-- 4 foo
27
27
28
28
29 $ (get-with-headers.py localhost:$HGPORT 'file/tip/da?style=raw')
29 $ (get-with-headers.py localhost:$HGPORT 'file/tip/da?style=raw')
30 200 Script output follows
30 200 Script output follows
31
31
32
32
33 -rw-r--r-- 4 foo
33 -rw-r--r-- 4 foo
34
34
35
35
36
36
37 plain file
37 plain file
38
38
39 $ get-with-headers.py localhost:$HGPORT 'file/tip/foo?style=raw'
39 $ get-with-headers.py localhost:$HGPORT 'file/tip/foo?style=raw'
40 200 Script output follows
40 200 Script output follows
41
41
42 foo
42 foo
43
43
44 should give a 404 - static file that does not exist
44 should give a 404 - static file that does not exist
45
45
46 $ get-with-headers.py localhost:$HGPORT 'static/bogus'
46 $ get-with-headers.py localhost:$HGPORT 'static/bogus'
47 404 Not Found
47 404 Not Found
48
48
49 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
49 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
50 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
50 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
51 <head>
51 <head>
52 <link rel="icon" href="/static/hgicon.png" type="image/png" />
52 <link rel="icon" href="/static/hgicon.png" type="image/png" />
53 <meta name="robots" content="index, nofollow" />
53 <meta name="robots" content="index, nofollow" />
54 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
54 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
55 <script type="text/javascript" src="/static/mercurial.js"></script>
55 <script type="text/javascript" src="/static/mercurial.js"></script>
56
56
57 <title>test: error</title>
57 <title>test: error</title>
58 </head>
58 </head>
59 <body>
59 <body>
60
60
61 <div class="container">
61 <div class="container">
62 <div class="menu">
62 <div class="menu">
63 <div class="logo">
63 <div class="logo">
64 <a href="https://mercurial-scm.org/">
64 <a href="https://mercurial-scm.org/">
65 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
65 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
66 </div>
66 </div>
67 <ul>
67 <ul>
68 <li><a href="/shortlog">log</a></li>
68 <li><a href="/shortlog">log</a></li>
69 <li><a href="/graph">graph</a></li>
69 <li><a href="/graph">graph</a></li>
70 <li><a href="/tags">tags</a></li>
70 <li><a href="/tags">tags</a></li>
71 <li><a href="/bookmarks">bookmarks</a></li>
71 <li><a href="/bookmarks">bookmarks</a></li>
72 <li><a href="/branches">branches</a></li>
72 <li><a href="/branches">branches</a></li>
73 </ul>
73 </ul>
74 <ul>
74 <ul>
75 <li><a href="/help">help</a></li>
75 <li><a href="/help">help</a></li>
76 </ul>
76 </ul>
77 </div>
77 </div>
78
78
79 <div class="main">
79 <div class="main">
80
80
81 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
81 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
82 <h3>error</h3>
82 <h3>error</h3>
83
83
84
84
85 <form class="search" action="/log">
85 <form class="search" action="/log">
86
86
87 <p><input name="rev" id="search1" type="text" size="30" value="" /></p>
87 <p><input name="rev" id="search1" type="text" size="30" value="" /></p>
88 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
88 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
89 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
89 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
90 </form>
90 </form>
91
91
92 <div class="description">
92 <div class="description">
93 <p>
93 <p>
94 An error occurred while processing your request:
94 An error occurred while processing your request:
95 </p>
95 </p>
96 <p>
96 <p>
97 Not Found
97 Not Found
98 </p>
98 </p>
99 </div>
99 </div>
100 </div>
100 </div>
101 </div>
101 </div>
102
102
103
103
104
104
105 </body>
105 </body>
106 </html>
106 </html>
107
107
108 [1]
108 [1]
109
109
110 should give a 404 - bad revision
110 should give a 404 - bad revision
111
111
112 $ get-with-headers.py localhost:$HGPORT 'file/spam/foo?style=raw'
112 $ get-with-headers.py localhost:$HGPORT 'file/spam/foo?style=raw'
113 404 Not Found
113 404 Not Found
114
114
115
115
116 error: revision not found: spam
116 error: revision not found: spam
117 [1]
117 [1]
118
118
119 should give a 400 - bad command
119 should give a 400 - bad command
120
120
121 $ get-with-headers.py localhost:$HGPORT 'file/tip/foo?cmd=spam&style=raw'
121 $ get-with-headers.py localhost:$HGPORT 'file/tip/foo?cmd=spam&style=raw'
122 400* (glob)
122 400* (glob)
123
123
124
124
125 error: no such method: spam
125 error: no such method: spam
126 [1]
126 [1]
127
127
128 $ get-with-headers.py --headeronly localhost:$HGPORT '?cmd=spam'
128 $ get-with-headers.py --headeronly localhost:$HGPORT '?cmd=spam'
129 400 no such method: spam
129 400 no such method: spam
130 [1]
130 [1]
131
131
132 should give a 400 - bad command as a part of url path (issue4071)
132 should give a 400 - bad command as a part of url path (issue4071)
133
133
134 $ get-with-headers.py --headeronly localhost:$HGPORT 'spam'
134 $ get-with-headers.py --headeronly localhost:$HGPORT 'spam'
135 400 no such method: spam
135 400 no such method: spam
136 [1]
136 [1]
137
137
138 $ get-with-headers.py --headeronly localhost:$HGPORT 'raw-spam'
138 $ get-with-headers.py --headeronly localhost:$HGPORT 'raw-spam'
139 400 no such method: spam
139 400 no such method: spam
140 [1]
140 [1]
141
141
142 $ get-with-headers.py --headeronly localhost:$HGPORT 'spam/tip/foo'
142 $ get-with-headers.py --headeronly localhost:$HGPORT 'spam/tip/foo'
143 400 no such method: spam
143 400 no such method: spam
144 [1]
144 [1]
145
145
146 should give a 404 - file does not exist
146 should give a 404 - file does not exist
147
147
148 $ get-with-headers.py localhost:$HGPORT 'file/tip/bork?style=raw'
148 $ get-with-headers.py localhost:$HGPORT 'file/tip/bork?style=raw'
149 404 Not Found
149 404 Not Found
150
150
151
151
152 error: bork@2ef0ac749a14: not found in manifest
152 error: bork@2ef0ac749a14: not found in manifest
153 [1]
153 [1]
154 $ get-with-headers.py localhost:$HGPORT 'file/tip/bork'
154 $ get-with-headers.py localhost:$HGPORT 'file/tip/bork'
155 404 Not Found
155 404 Not Found
156
156
157 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
157 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
158 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
158 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
159 <head>
159 <head>
160 <link rel="icon" href="/static/hgicon.png" type="image/png" />
160 <link rel="icon" href="/static/hgicon.png" type="image/png" />
161 <meta name="robots" content="index, nofollow" />
161 <meta name="robots" content="index, nofollow" />
162 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
162 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
163 <script type="text/javascript" src="/static/mercurial.js"></script>
163 <script type="text/javascript" src="/static/mercurial.js"></script>
164
164
165 <title>test: error</title>
165 <title>test: error</title>
166 </head>
166 </head>
167 <body>
167 <body>
168
168
169 <div class="container">
169 <div class="container">
170 <div class="menu">
170 <div class="menu">
171 <div class="logo">
171 <div class="logo">
172 <a href="https://mercurial-scm.org/">
172 <a href="https://mercurial-scm.org/">
173 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
173 <img src="/static/hglogo.png" width=75 height=90 border=0 alt="mercurial" /></a>
174 </div>
174 </div>
175 <ul>
175 <ul>
176 <li><a href="/shortlog">log</a></li>
176 <li><a href="/shortlog">log</a></li>
177 <li><a href="/graph">graph</a></li>
177 <li><a href="/graph">graph</a></li>
178 <li><a href="/tags">tags</a></li>
178 <li><a href="/tags">tags</a></li>
179 <li><a href="/bookmarks">bookmarks</a></li>
179 <li><a href="/bookmarks">bookmarks</a></li>
180 <li><a href="/branches">branches</a></li>
180 <li><a href="/branches">branches</a></li>
181 </ul>
181 </ul>
182 <ul>
182 <ul>
183 <li><a href="/help">help</a></li>
183 <li><a href="/help">help</a></li>
184 </ul>
184 </ul>
185 </div>
185 </div>
186
186
187 <div class="main">
187 <div class="main">
188
188
189 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
189 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
190 <h3>error</h3>
190 <h3>error</h3>
191
191
192
192
193 <form class="search" action="/log">
193 <form class="search" action="/log">
194
194
195 <p><input name="rev" id="search1" type="text" size="30" value="" /></p>
195 <p><input name="rev" id="search1" type="text" size="30" value="" /></p>
196 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
196 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
197 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
197 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
198 </form>
198 </form>
199
199
200 <div class="description">
200 <div class="description">
201 <p>
201 <p>
202 An error occurred while processing your request:
202 An error occurred while processing your request:
203 </p>
203 </p>
204 <p>
204 <p>
205 bork@2ef0ac749a14: not found in manifest
205 bork@2ef0ac749a14: not found in manifest
206 </p>
206 </p>
207 </div>
207 </div>
208 </div>
208 </div>
209 </div>
209 </div>
210
210
211
211
212
212
213 </body>
213 </body>
214 </html>
214 </html>
215
215
216 [1]
216 [1]
217 $ get-with-headers.py localhost:$HGPORT 'diff/tip/bork?style=raw'
217 $ get-with-headers.py localhost:$HGPORT 'diff/tip/bork?style=raw'
218 404 Not Found
218 404 Not Found
219
219
220
220
221 error: bork@2ef0ac749a14: not found in manifest
221 error: bork@2ef0ac749a14: not found in manifest
222 [1]
222 [1]
223
223
224 try bad style
224 try bad style
225
225
226 $ (get-with-headers.py localhost:$HGPORT 'file/tip/?style=foobar')
226 $ (get-with-headers.py localhost:$HGPORT 'file/tip/?style=foobar')
227 200 Script output follows
227 200 Script output follows
228
228
229 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
229 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
230 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
230 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US">
231 <head>
231 <head>
232 <link rel="icon" href="/static/hgicon.png" type="image/png" />
232 <link rel="icon" href="/static/hgicon.png" type="image/png" />
233 <meta name="robots" content="index, nofollow" />
233 <meta name="robots" content="index, nofollow" />
234 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
234 <link rel="stylesheet" href="/static/style-paper.css" type="text/css" />
235 <script type="text/javascript" src="/static/mercurial.js"></script>
235 <script type="text/javascript" src="/static/mercurial.js"></script>
236
236
237 <title>test: 2ef0ac749a14 /</title>
237 <title>test: 2ef0ac749a14 /</title>
238 </head>
238 </head>
239 <body>
239 <body>
240
240
241 <div class="container">
241 <div class="container">
242 <div class="menu">
242 <div class="menu">
243 <div class="logo">
243 <div class="logo">
244 <a href="https://mercurial-scm.org/">
244 <a href="https://mercurial-scm.org/">
245 <img src="/static/hglogo.png" alt="mercurial" /></a>
245 <img src="/static/hglogo.png" alt="mercurial" /></a>
246 </div>
246 </div>
247 <ul>
247 <ul>
248 <li><a href="/shortlog/tip">log</a></li>
248 <li><a href="/shortlog/tip">log</a></li>
249 <li><a href="/graph/tip">graph</a></li>
249 <li><a href="/graph/tip">graph</a></li>
250 <li><a href="/tags">tags</a></li>
250 <li><a href="/tags">tags</a></li>
251 <li><a href="/bookmarks">bookmarks</a></li>
251 <li><a href="/bookmarks">bookmarks</a></li>
252 <li><a href="/branches">branches</a></li>
252 <li><a href="/branches">branches</a></li>
253 </ul>
253 </ul>
254 <ul>
254 <ul>
255 <li><a href="/rev/tip">changeset</a></li>
255 <li><a href="/rev/tip">changeset</a></li>
256 <li class="active">browse</li>
256 <li class="active">browse</li>
257 </ul>
257 </ul>
258 <ul>
258 <ul>
259
259
260 </ul>
260 </ul>
261 <ul>
261 <ul>
262 <li><a href="/help">help</a></li>
262 <li><a href="/help">help</a></li>
263 </ul>
263 </ul>
264 </div>
264 </div>
265
265
266 <div class="main">
266 <div class="main">
267 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
267 <h2 class="breadcrumb"><a href="/">Mercurial</a> </h2>
268 <h3>
268 <h3>
269 directory / @ 0:<a href="/rev/2ef0ac749a14">2ef0ac749a14</a>
269 directory / @ 0:<a href="/rev/2ef0ac749a14">2ef0ac749a14</a>
270 <span class="phase">draft</span> <span class="branchhead">default</span> <span class="tag">tip</span> <span class="tag">@</span> <span class="tag">a b c</span> <span class="tag">d/e/f</span>
270 <span class="phase">draft</span> <span class="branchhead">default</span> <span class="tag">tip</span> <span class="tag">@</span> <span class="tag">a b c</span> <span class="tag">d/e/f</span>
271 </h3>
271 </h3>
272
272
273
273
274 <form class="search" action="/log">
274 <form class="search" action="/log">
275
275
276 <p><input name="rev" id="search1" type="text" size="30" value="" /></p>
276 <p><input name="rev" id="search1" type="text" size="30" value="" /></p>
277 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
277 <div id="hint">Find changesets by keywords (author, files, the commit message), revision
278 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
278 number or hash, or <a href="/help/revsets">revset expression</a>.</div>
279 </form>
279 </form>
280
280
281 <table class="bigtable">
281 <table class="bigtable">
282 <thead>
282 <thead>
283 <tr>
283 <tr>
284 <th class="name">name</th>
284 <th class="name">name</th>
285 <th class="size">size</th>
285 <th class="size">size</th>
286 <th class="permissions">permissions</th>
286 <th class="permissions">permissions</th>
287 </tr>
287 </tr>
288 </thead>
288 </thead>
289 <tbody class="stripes2">
289 <tbody class="stripes2">
290 <tr class="fileline">
290 <tr class="fileline">
291 <td class="name"><a href="/file/tip/">[up]</a></td>
291 <td class="name"><a href="/file/tip/">[up]</a></td>
292 <td class="size"></td>
292 <td class="size"></td>
293 <td class="permissions">drwxr-xr-x</td>
293 <td class="permissions">drwxr-xr-x</td>
294 </tr>
294 </tr>
295
295
296 <tr class="fileline">
296 <tr class="fileline">
297 <td class="name">
297 <td class="name">
298 <a href="/file/tip/da">
298 <a href="/file/tip/da">
299 <img src="/static/coal-folder.png" alt="dir."/> da/
299 <img src="/static/coal-folder.png" alt="dir."/> da/
300 </a>
300 </a>
301 <a href="/file/tip/da/">
301 <a href="/file/tip/da/">
302
302
303 </a>
303 </a>
304 </td>
304 </td>
305 <td class="size"></td>
305 <td class="size"></td>
306 <td class="permissions">drwxr-xr-x</td>
306 <td class="permissions">drwxr-xr-x</td>
307 </tr>
307 </tr>
308
308
309 <tr class="fileline">
309 <tr class="fileline">
310 <td class="filename">
310 <td class="filename">
311 <a href="/file/tip/foo">
311 <a href="/file/tip/foo">
312 <img src="/static/coal-file.png" alt="file"/> foo
312 <img src="/static/coal-file.png" alt="file"/> foo
313 </a>
313 </a>
314 </td>
314 </td>
315 <td class="size">4</td>
315 <td class="size">4</td>
316 <td class="permissions">-rw-r--r--</td>
316 <td class="permissions">-rw-r--r--</td>
317 </tr>
317 </tr>
318 </tbody>
318 </tbody>
319 </table>
319 </table>
320 </div>
320 </div>
321 </div>
321 </div>
322
322
323
323
324 </body>
324 </body>
325 </html>
325 </html>
326
326
327
327
328 stop and restart
328 stop and restart
329
329
330 $ killdaemons.py
330 $ killdaemons.py
331 $ hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log
331 $ hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log
332 $ cat hg.pid >> $DAEMON_PIDS
332 $ cat hg.pid >> $DAEMON_PIDS
333
333
334 Test the access/error files are opened in append mode
334 Test the access/error files are opened in append mode
335
335
336 $ $PYTHON -c "print len(file('access.log').readlines()), 'log lines written'"
336 $ $PYTHON -c "print len(open('access.log', 'rb').readlines()), 'log lines written'"
337 14 log lines written
337 14 log lines written
338
338
339 static file
339 static file
340
340
341 $ get-with-headers.py --twice localhost:$HGPORT 'static/style-gitweb.css' - date etag server
341 $ get-with-headers.py --twice localhost:$HGPORT 'static/style-gitweb.css' - date etag server
342 200 Script output follows
342 200 Script output follows
343 content-length: 9126
343 content-length: 9126
344 content-type: text/css
344 content-type: text/css
345
345
346 body { font-family: sans-serif; font-size: 12px; border:solid #d9d8d1; border-width:1px; margin:10px; background: white; color: black; }
346 body { font-family: sans-serif; font-size: 12px; border:solid #d9d8d1; border-width:1px; margin:10px; background: white; color: black; }
347 a { color:#0000cc; }
347 a { color:#0000cc; }
348 a:hover, a:visited, a:active { color:#880000; }
348 a:hover, a:visited, a:active { color:#880000; }
349 div.page_header { height:25px; padding:8px; font-size:18px; font-weight:bold; background-color:#d9d8d1; }
349 div.page_header { height:25px; padding:8px; font-size:18px; font-weight:bold; background-color:#d9d8d1; }
350 div.page_header a:visited { color:#0000cc; }
350 div.page_header a:visited { color:#0000cc; }
351 div.page_header a:hover { color:#880000; }
351 div.page_header a:hover { color:#880000; }
352 div.page_nav {
352 div.page_nav {
353 padding:8px;
353 padding:8px;
354 display: flex;
354 display: flex;
355 justify-content: space-between;
355 justify-content: space-between;
356 align-items: center;
356 align-items: center;
357 }
357 }
358 div.page_nav a:visited { color:#0000cc; }
358 div.page_nav a:visited { color:#0000cc; }
359 div.extra_nav {
359 div.extra_nav {
360 padding: 8px;
360 padding: 8px;
361 }
361 }
362 div.extra_nav a:visited {
362 div.extra_nav a:visited {
363 color: #0000cc;
363 color: #0000cc;
364 }
364 }
365 div.page_path { padding:8px; border:solid #d9d8d1; border-width:0px 0px 1px}
365 div.page_path { padding:8px; border:solid #d9d8d1; border-width:0px 0px 1px}
366 div.page_footer { padding:4px 8px; background-color: #d9d8d1; }
366 div.page_footer { padding:4px 8px; background-color: #d9d8d1; }
367 div.page_footer_text { float:left; color:#555555; font-style:italic; }
367 div.page_footer_text { float:left; color:#555555; font-style:italic; }
368 div.page_body { padding:8px; }
368 div.page_body { padding:8px; }
369 div.title, a.title {
369 div.title, a.title {
370 display:block; padding:6px 8px;
370 display:block; padding:6px 8px;
371 font-weight:bold; background-color:#edece6; text-decoration:none; color:#000000;
371 font-weight:bold; background-color:#edece6; text-decoration:none; color:#000000;
372 }
372 }
373 a.title:hover { background-color: #d9d8d1; }
373 a.title:hover { background-color: #d9d8d1; }
374 div.title_text { padding:6px 0px; border: solid #d9d8d1; border-width:0px 0px 1px; }
374 div.title_text { padding:6px 0px; border: solid #d9d8d1; border-width:0px 0px 1px; }
375 div.log_body { padding:8px 8px 8px 150px; }
375 div.log_body { padding:8px 8px 8px 150px; }
376 .age { white-space:nowrap; }
376 .age { white-space:nowrap; }
377 a.title span.age { position:relative; float:left; width:142px; font-style:italic; }
377 a.title span.age { position:relative; float:left; width:142px; font-style:italic; }
378 div.log_link {
378 div.log_link {
379 padding:0px 8px;
379 padding:0px 8px;
380 font-size:10px; font-family:sans-serif; font-style:normal;
380 font-size:10px; font-family:sans-serif; font-style:normal;
381 position:relative; float:left; width:136px;
381 position:relative; float:left; width:136px;
382 }
382 }
383 div.list_head { padding:6px 8px 4px; border:solid #d9d8d1; border-width:1px 0px 0px; font-style:italic; }
383 div.list_head { padding:6px 8px 4px; border:solid #d9d8d1; border-width:1px 0px 0px; font-style:italic; }
384 a.list { text-decoration:none; color:#000000; }
384 a.list { text-decoration:none; color:#000000; }
385 a.list:hover { text-decoration:underline; color:#880000; }
385 a.list:hover { text-decoration:underline; color:#880000; }
386 table { padding:8px 4px; }
386 table { padding:8px 4px; }
387 th { padding:2px 5px; font-size:12px; text-align:left; }
387 th { padding:2px 5px; font-size:12px; text-align:left; }
388 .parity0 { background-color:#ffffff; }
388 .parity0 { background-color:#ffffff; }
389 tr.dark, .parity1, pre.sourcelines.stripes > :nth-child(4n+4) { background-color:#f6f6f0; }
389 tr.dark, .parity1, pre.sourcelines.stripes > :nth-child(4n+4) { background-color:#f6f6f0; }
390 tr.light:hover, .parity0:hover, tr.dark:hover, .parity1:hover,
390 tr.light:hover, .parity0:hover, tr.dark:hover, .parity1:hover,
391 pre.sourcelines.stripes > :nth-child(4n+2):hover,
391 pre.sourcelines.stripes > :nth-child(4n+2):hover,
392 pre.sourcelines.stripes > :nth-child(4n+4):hover,
392 pre.sourcelines.stripes > :nth-child(4n+4):hover,
393 pre.sourcelines.stripes > :nth-child(4n+1):hover + :nth-child(4n+2),
393 pre.sourcelines.stripes > :nth-child(4n+1):hover + :nth-child(4n+2),
394 pre.sourcelines.stripes > :nth-child(4n+3):hover + :nth-child(4n+4) { background-color:#edece6; }
394 pre.sourcelines.stripes > :nth-child(4n+3):hover + :nth-child(4n+4) { background-color:#edece6; }
395 td { padding:2px 5px; font-size:12px; vertical-align:top; }
395 td { padding:2px 5px; font-size:12px; vertical-align:top; }
396 td.closed { background-color: #99f; }
396 td.closed { background-color: #99f; }
397 td.link { padding:2px 5px; font-family:sans-serif; font-size:10px; }
397 td.link { padding:2px 5px; font-family:sans-serif; font-size:10px; }
398 td.indexlinks { white-space: nowrap; }
398 td.indexlinks { white-space: nowrap; }
399 td.indexlinks a {
399 td.indexlinks a {
400 padding: 2px 5px; line-height: 10px;
400 padding: 2px 5px; line-height: 10px;
401 border: 1px solid;
401 border: 1px solid;
402 color: #ffffff; background-color: #7777bb;
402 color: #ffffff; background-color: #7777bb;
403 border-color: #aaaadd #333366 #333366 #aaaadd;
403 border-color: #aaaadd #333366 #333366 #aaaadd;
404 font-weight: bold; text-align: center; text-decoration: none;
404 font-weight: bold; text-align: center; text-decoration: none;
405 font-size: 10px;
405 font-size: 10px;
406 }
406 }
407 td.indexlinks a:hover { background-color: #6666aa; }
407 td.indexlinks a:hover { background-color: #6666aa; }
408 div.pre { font-family:monospace; font-size:12px; white-space:pre; }
408 div.pre { font-family:monospace; font-size:12px; white-space:pre; }
409
409
410 .search {
410 .search {
411 margin-right: 8px;
411 margin-right: 8px;
412 }
412 }
413
413
414 div#hint {
414 div#hint {
415 position: absolute;
415 position: absolute;
416 display: none;
416 display: none;
417 width: 250px;
417 width: 250px;
418 padding: 5px;
418 padding: 5px;
419 background: #ffc;
419 background: #ffc;
420 border: 1px solid yellow;
420 border: 1px solid yellow;
421 border-radius: 5px;
421 border-radius: 5px;
422 }
422 }
423
423
424 #searchform:hover div#hint { display: block; }
424 #searchform:hover div#hint { display: block; }
425
425
426 tr.thisrev a { color:#999999; text-decoration: none; }
426 tr.thisrev a { color:#999999; text-decoration: none; }
427 tr.thisrev pre { color:#009900; }
427 tr.thisrev pre { color:#009900; }
428 td.annotate {
428 td.annotate {
429 white-space: nowrap;
429 white-space: nowrap;
430 }
430 }
431 div.annotate-info {
431 div.annotate-info {
432 z-index: 5;
432 z-index: 5;
433 display: none;
433 display: none;
434 position: absolute;
434 position: absolute;
435 background-color: #FFFFFF;
435 background-color: #FFFFFF;
436 border: 1px solid #d9d8d1;
436 border: 1px solid #d9d8d1;
437 text-align: left;
437 text-align: left;
438 color: #000000;
438 color: #000000;
439 padding: 5px;
439 padding: 5px;
440 }
440 }
441 div.annotate-info a { color: #0000FF; text-decoration: underline; }
441 div.annotate-info a { color: #0000FF; text-decoration: underline; }
442 td.annotate:hover div.annotate-info { display: inline; }
442 td.annotate:hover div.annotate-info { display: inline; }
443
443
444 #diffopts-form {
444 #diffopts-form {
445 padding-left: 8px;
445 padding-left: 8px;
446 display: none;
446 display: none;
447 }
447 }
448
448
449 .linenr { color:#999999; text-decoration:none }
449 .linenr { color:#999999; text-decoration:none }
450 div.rss_logo { float: right; white-space: nowrap; }
450 div.rss_logo { float: right; white-space: nowrap; }
451 div.rss_logo a {
451 div.rss_logo a {
452 padding:3px 6px; line-height:10px;
452 padding:3px 6px; line-height:10px;
453 border:1px solid; border-color:#fcc7a5 #7d3302 #3e1a01 #ff954e;
453 border:1px solid; border-color:#fcc7a5 #7d3302 #3e1a01 #ff954e;
454 color:#ffffff; background-color:#ff6600;
454 color:#ffffff; background-color:#ff6600;
455 font-weight:bold; font-family:sans-serif; font-size:10px;
455 font-weight:bold; font-family:sans-serif; font-size:10px;
456 text-align:center; text-decoration:none;
456 text-align:center; text-decoration:none;
457 }
457 }
458 div.rss_logo a:hover { background-color:#ee5500; }
458 div.rss_logo a:hover { background-color:#ee5500; }
459 pre { margin: 0; }
459 pre { margin: 0; }
460 span.logtags span {
460 span.logtags span {
461 padding: 0px 4px;
461 padding: 0px 4px;
462 font-size: 10px;
462 font-size: 10px;
463 font-weight: normal;
463 font-weight: normal;
464 border: 1px solid;
464 border: 1px solid;
465 background-color: #ffaaff;
465 background-color: #ffaaff;
466 border-color: #ffccff #ff00ee #ff00ee #ffccff;
466 border-color: #ffccff #ff00ee #ff00ee #ffccff;
467 }
467 }
468 span.logtags span.phasetag {
468 span.logtags span.phasetag {
469 background-color: #dfafff;
469 background-color: #dfafff;
470 border-color: #e2b8ff #ce48ff #ce48ff #e2b8ff;
470 border-color: #e2b8ff #ce48ff #ce48ff #e2b8ff;
471 }
471 }
472 span.logtags span.obsoletetag {
472 span.logtags span.obsoletetag {
473 background-color: #dddddd;
473 background-color: #dddddd;
474 border-color: #e4e4e4 #a3a3a3 #a3a3a3 #e4e4e4;
474 border-color: #e4e4e4 #a3a3a3 #a3a3a3 #e4e4e4;
475 }
475 }
476 span.logtags span.instabilitytag {
476 span.logtags span.instabilitytag {
477 background-color: #ffb1c0;
477 background-color: #ffb1c0;
478 border-color: #ffbbc8 #ff4476 #ff4476 #ffbbc8;
478 border-color: #ffbbc8 #ff4476 #ff4476 #ffbbc8;
479 }
479 }
480 span.logtags span.tagtag {
480 span.logtags span.tagtag {
481 background-color: #ffffaa;
481 background-color: #ffffaa;
482 border-color: #ffffcc #ffee00 #ffee00 #ffffcc;
482 border-color: #ffffcc #ffee00 #ffee00 #ffffcc;
483 }
483 }
484 span.logtags span.branchtag {
484 span.logtags span.branchtag {
485 background-color: #aaffaa;
485 background-color: #aaffaa;
486 border-color: #ccffcc #00cc33 #00cc33 #ccffcc;
486 border-color: #ccffcc #00cc33 #00cc33 #ccffcc;
487 }
487 }
488 span.logtags span.inbranchtag {
488 span.logtags span.inbranchtag {
489 background-color: #d5dde6;
489 background-color: #d5dde6;
490 border-color: #e3ecf4 #9398f4 #9398f4 #e3ecf4;
490 border-color: #e3ecf4 #9398f4 #9398f4 #e3ecf4;
491 }
491 }
492 span.logtags span.bookmarktag {
492 span.logtags span.bookmarktag {
493 background-color: #afdffa;
493 background-color: #afdffa;
494 border-color: #ccecff #46ace6 #46ace6 #ccecff;
494 border-color: #ccecff #46ace6 #46ace6 #ccecff;
495 }
495 }
496 span.difflineplus { color:#008800; }
496 span.difflineplus { color:#008800; }
497 span.difflineminus { color:#cc0000; }
497 span.difflineminus { color:#cc0000; }
498 span.difflineat { color:#990099; }
498 span.difflineat { color:#990099; }
499 div.diffblocks { counter-reset: lineno; }
499 div.diffblocks { counter-reset: lineno; }
500 div.diffblock { counter-increment: lineno; }
500 div.diffblock { counter-increment: lineno; }
501 pre.sourcelines { position: relative; counter-reset: lineno; }
501 pre.sourcelines { position: relative; counter-reset: lineno; }
502 pre.sourcelines > span {
502 pre.sourcelines > span {
503 display: inline-block;
503 display: inline-block;
504 box-sizing: border-box;
504 box-sizing: border-box;
505 width: 100%;
505 width: 100%;
506 padding: 0 0 0 5em;
506 padding: 0 0 0 5em;
507 counter-increment: lineno;
507 counter-increment: lineno;
508 vertical-align: top;
508 vertical-align: top;
509 }
509 }
510 pre.sourcelines > span:before {
510 pre.sourcelines > span:before {
511 -moz-user-select: -moz-none;
511 -moz-user-select: -moz-none;
512 -khtml-user-select: none;
512 -khtml-user-select: none;
513 -webkit-user-select: none;
513 -webkit-user-select: none;
514 -ms-user-select: none;
514 -ms-user-select: none;
515 user-select: none;
515 user-select: none;
516 display: inline-block;
516 display: inline-block;
517 margin-left: -6em;
517 margin-left: -6em;
518 width: 4em;
518 width: 4em;
519 color: #999;
519 color: #999;
520 text-align: right;
520 text-align: right;
521 content: counters(lineno,".");
521 content: counters(lineno,".");
522 float: left;
522 float: left;
523 }
523 }
524 pre.sourcelines > a {
524 pre.sourcelines > a {
525 display: inline-block;
525 display: inline-block;
526 position: absolute;
526 position: absolute;
527 left: 0px;
527 left: 0px;
528 width: 4em;
528 width: 4em;
529 height: 1em;
529 height: 1em;
530 }
530 }
531 tr:target td,
531 tr:target td,
532 pre.sourcelines > span:target,
532 pre.sourcelines > span:target,
533 pre.sourcelines.stripes > span:target {
533 pre.sourcelines.stripes > span:target {
534 background-color: #bfdfff;
534 background-color: #bfdfff;
535 }
535 }
536
536
537 .description {
537 .description {
538 font-family: monospace;
538 font-family: monospace;
539 white-space: pre;
539 white-space: pre;
540 }
540 }
541
541
542 /* Followlines */
542 /* Followlines */
543 tbody.sourcelines > tr.followlines-selected,
543 tbody.sourcelines > tr.followlines-selected,
544 pre.sourcelines > span.followlines-selected {
544 pre.sourcelines > span.followlines-selected {
545 background-color: #99C7E9 !important;
545 background-color: #99C7E9 !important;
546 }
546 }
547
547
548 div#followlines {
548 div#followlines {
549 background-color: #FFF;
549 background-color: #FFF;
550 border: 1px solid #d9d8d1;
550 border: 1px solid #d9d8d1;
551 padding: 5px;
551 padding: 5px;
552 position: fixed;
552 position: fixed;
553 }
553 }
554
554
555 div.followlines-cancel {
555 div.followlines-cancel {
556 text-align: right;
556 text-align: right;
557 }
557 }
558
558
559 div.followlines-cancel > button {
559 div.followlines-cancel > button {
560 line-height: 80%;
560 line-height: 80%;
561 padding: 0;
561 padding: 0;
562 border: 0;
562 border: 0;
563 border-radius: 2px;
563 border-radius: 2px;
564 background-color: inherit;
564 background-color: inherit;
565 font-weight: bold;
565 font-weight: bold;
566 }
566 }
567
567
568 div.followlines-cancel > button:hover {
568 div.followlines-cancel > button:hover {
569 color: #FFFFFF;
569 color: #FFFFFF;
570 background-color: #CF1F1F;
570 background-color: #CF1F1F;
571 }
571 }
572
572
573 div.followlines-link {
573 div.followlines-link {
574 margin: 2px;
574 margin: 2px;
575 margin-top: 4px;
575 margin-top: 4px;
576 font-family: sans-serif;
576 font-family: sans-serif;
577 }
577 }
578
578
579 .btn-followlines {
579 .btn-followlines {
580 display: none;
580 display: none;
581 cursor: pointer;
581 cursor: pointer;
582 box-sizing: content-box;
582 box-sizing: content-box;
583 font-size: 11px;
583 font-size: 11px;
584 width: 13px;
584 width: 13px;
585 height: 13px;
585 height: 13px;
586 border-radius: 3px;
586 border-radius: 3px;
587 margin: 0px;
587 margin: 0px;
588 margin-top: -2px;
588 margin-top: -2px;
589 padding: 0px;
589 padding: 0px;
590 background-color: #E5FDE5;
590 background-color: #E5FDE5;
591 border: 1px solid #9BC19B;
591 border: 1px solid #9BC19B;
592 font-family: monospace;
592 font-family: monospace;
593 text-align: center;
593 text-align: center;
594 line-height: 5px;
594 line-height: 5px;
595 }
595 }
596
596
597 tr .btn-followlines {
597 tr .btn-followlines {
598 position: absolute;
598 position: absolute;
599 }
599 }
600
600
601 span .btn-followlines {
601 span .btn-followlines {
602 float: left;
602 float: left;
603 }
603 }
604
604
605 span.followlines-select .btn-followlines {
605 span.followlines-select .btn-followlines {
606 margin-left: -1.6em;
606 margin-left: -1.6em;
607 }
607 }
608
608
609 .btn-followlines:hover {
609 .btn-followlines:hover {
610 transform: scale(1.1, 1.1);
610 transform: scale(1.1, 1.1);
611 }
611 }
612
612
613 .btn-followlines .followlines-plus {
613 .btn-followlines .followlines-plus {
614 color: green;
614 color: green;
615 }
615 }
616
616
617 .btn-followlines .followlines-minus {
617 .btn-followlines .followlines-minus {
618 color: red;
618 color: red;
619 }
619 }
620
620
621 .btn-followlines-end {
621 .btn-followlines-end {
622 background-color: #ffdcdc;
622 background-color: #ffdcdc;
623 }
623 }
624
624
625 .sourcelines tr:hover .btn-followlines,
625 .sourcelines tr:hover .btn-followlines,
626 .sourcelines span.followlines-select:hover > .btn-followlines {
626 .sourcelines span.followlines-select:hover > .btn-followlines {
627 display: inline;
627 display: inline;
628 }
628 }
629
629
630 .btn-followlines-hidden,
630 .btn-followlines-hidden,
631 .sourcelines tr:hover .btn-followlines-hidden {
631 .sourcelines tr:hover .btn-followlines-hidden {
632 display: none;
632 display: none;
633 }
633 }
634
634
635 /* Graph */
635 /* Graph */
636 div#wrapper {
636 div#wrapper {
637 position: relative;
637 position: relative;
638 margin: 0;
638 margin: 0;
639 padding: 0;
639 padding: 0;
640 margin-top: 3px;
640 margin-top: 3px;
641 }
641 }
642
642
643 canvas {
643 canvas {
644 position: absolute;
644 position: absolute;
645 z-index: 5;
645 z-index: 5;
646 top: -0.9em;
646 top: -0.9em;
647 margin: 0;
647 margin: 0;
648 }
648 }
649
649
650 ul#graphnodes {
650 ul#graphnodes {
651 list-style: none inside none;
651 list-style: none inside none;
652 padding: 0;
652 padding: 0;
653 margin: 0;
653 margin: 0;
654 }
654 }
655
655
656 ul#graphnodes li {
656 ul#graphnodes li {
657 position: relative;
657 position: relative;
658 height: 37px;
658 height: 37px;
659 overflow: visible;
659 overflow: visible;
660 padding-top: 2px;
660 padding-top: 2px;
661 }
661 }
662
662
663 ul#graphnodes li .fg {
663 ul#graphnodes li .fg {
664 position: absolute;
664 position: absolute;
665 z-index: 10;
665 z-index: 10;
666 }
666 }
667
667
668 ul#graphnodes li .info {
668 ul#graphnodes li .info {
669 font-size: 100%;
669 font-size: 100%;
670 font-style: italic;
670 font-style: italic;
671 }
671 }
672
672
673 /* Comparison */
673 /* Comparison */
674 .legend {
674 .legend {
675 padding: 1.5% 0 1.5% 0;
675 padding: 1.5% 0 1.5% 0;
676 }
676 }
677
677
678 .legendinfo {
678 .legendinfo {
679 border: 1px solid #d9d8d1;
679 border: 1px solid #d9d8d1;
680 font-size: 80%;
680 font-size: 80%;
681 text-align: center;
681 text-align: center;
682 padding: 0.5%;
682 padding: 0.5%;
683 }
683 }
684
684
685 .equal {
685 .equal {
686 background-color: #ffffff;
686 background-color: #ffffff;
687 }
687 }
688
688
689 .delete {
689 .delete {
690 background-color: #faa;
690 background-color: #faa;
691 color: #333;
691 color: #333;
692 }
692 }
693
693
694 .insert {
694 .insert {
695 background-color: #ffa;
695 background-color: #ffa;
696 }
696 }
697
697
698 .replace {
698 .replace {
699 background-color: #e8e8e8;
699 background-color: #e8e8e8;
700 }
700 }
701
701
702 .comparison {
702 .comparison {
703 overflow-x: auto;
703 overflow-x: auto;
704 }
704 }
705
705
706 .header th {
706 .header th {
707 text-align: center;
707 text-align: center;
708 }
708 }
709
709
710 .block {
710 .block {
711 border-top: 1px solid #d9d8d1;
711 border-top: 1px solid #d9d8d1;
712 }
712 }
713
713
714 .scroll-loading {
714 .scroll-loading {
715 -webkit-animation: change_color 1s linear 0s infinite alternate;
715 -webkit-animation: change_color 1s linear 0s infinite alternate;
716 -moz-animation: change_color 1s linear 0s infinite alternate;
716 -moz-animation: change_color 1s linear 0s infinite alternate;
717 -o-animation: change_color 1s linear 0s infinite alternate;
717 -o-animation: change_color 1s linear 0s infinite alternate;
718 animation: change_color 1s linear 0s infinite alternate;
718 animation: change_color 1s linear 0s infinite alternate;
719 }
719 }
720
720
721 @-webkit-keyframes change_color {
721 @-webkit-keyframes change_color {
722 from { background-color: #A0CEFF; } to { }
722 from { background-color: #A0CEFF; } to { }
723 }
723 }
724 @-moz-keyframes change_color {
724 @-moz-keyframes change_color {
725 from { background-color: #A0CEFF; } to { }
725 from { background-color: #A0CEFF; } to { }
726 }
726 }
727 @-o-keyframes change_color {
727 @-o-keyframes change_color {
728 from { background-color: #A0CEFF; } to { }
728 from { background-color: #A0CEFF; } to { }
729 }
729 }
730 @keyframes change_color {
730 @keyframes change_color {
731 from { background-color: #A0CEFF; } to { }
731 from { background-color: #A0CEFF; } to { }
732 }
732 }
733
733
734 .scroll-loading-error {
734 .scroll-loading-error {
735 background-color: #FFCCCC !important;
735 background-color: #FFCCCC !important;
736 }
736 }
737
737
738 #doc {
738 #doc {
739 margin: 0 8px;
739 margin: 0 8px;
740 }
740 }
741 304 Not Modified
741 304 Not Modified
742
742
743
743
744 phase changes are refreshed (issue4061)
744 phase changes are refreshed (issue4061)
745
745
746 $ echo bar >> foo
746 $ echo bar >> foo
747 $ hg ci -msecret --secret
747 $ hg ci -msecret --secret
748 $ get-with-headers.py localhost:$HGPORT 'log?style=raw'
748 $ get-with-headers.py localhost:$HGPORT 'log?style=raw'
749 200 Script output follows
749 200 Script output follows
750
750
751
751
752 # HG changelog
752 # HG changelog
753 # Node ID 2ef0ac749a14e4f57a5a822464a0902c6f7f448f
753 # Node ID 2ef0ac749a14e4f57a5a822464a0902c6f7f448f
754
754
755 changeset: 2ef0ac749a14e4f57a5a822464a0902c6f7f448f
755 changeset: 2ef0ac749a14e4f57a5a822464a0902c6f7f448f
756 revision: 0
756 revision: 0
757 user: test
757 user: test
758 date: Thu, 01 Jan 1970 00:00:00 +0000
758 date: Thu, 01 Jan 1970 00:00:00 +0000
759 summary: base
759 summary: base
760 branch: default
760 branch: default
761 tag: tip
761 tag: tip
762 bookmark: @
762 bookmark: @
763 bookmark: a b c
763 bookmark: a b c
764 bookmark: d/e/f
764 bookmark: d/e/f
765
765
766
766
767 $ hg phase --draft tip
767 $ hg phase --draft tip
768 $ get-with-headers.py localhost:$HGPORT 'log?style=raw'
768 $ get-with-headers.py localhost:$HGPORT 'log?style=raw'
769 200 Script output follows
769 200 Script output follows
770
770
771
771
772 # HG changelog
772 # HG changelog
773 # Node ID a084749e708a9c4c0a5b652a2a446322ce290e04
773 # Node ID a084749e708a9c4c0a5b652a2a446322ce290e04
774
774
775 changeset: a084749e708a9c4c0a5b652a2a446322ce290e04
775 changeset: a084749e708a9c4c0a5b652a2a446322ce290e04
776 revision: 1
776 revision: 1
777 user: test
777 user: test
778 date: Thu, 01 Jan 1970 00:00:00 +0000
778 date: Thu, 01 Jan 1970 00:00:00 +0000
779 summary: secret
779 summary: secret
780 branch: default
780 branch: default
781 tag: tip
781 tag: tip
782
782
783 changeset: 2ef0ac749a14e4f57a5a822464a0902c6f7f448f
783 changeset: 2ef0ac749a14e4f57a5a822464a0902c6f7f448f
784 revision: 0
784 revision: 0
785 user: test
785 user: test
786 date: Thu, 01 Jan 1970 00:00:00 +0000
786 date: Thu, 01 Jan 1970 00:00:00 +0000
787 summary: base
787 summary: base
788 bookmark: @
788 bookmark: @
789 bookmark: a b c
789 bookmark: a b c
790 bookmark: d/e/f
790 bookmark: d/e/f
791
791
792
792
793
793
794 access bookmarks
794 access bookmarks
795
795
796 $ get-with-headers.py localhost:$HGPORT 'rev/@?style=paper' | egrep '^200|changeset 0:'
796 $ get-with-headers.py localhost:$HGPORT 'rev/@?style=paper' | egrep '^200|changeset 0:'
797 200 Script output follows
797 200 Script output follows
798 changeset 0:<a href="/rev/2ef0ac749a14?style=paper">2ef0ac749a14</a>
798 changeset 0:<a href="/rev/2ef0ac749a14?style=paper">2ef0ac749a14</a>
799
799
800 $ get-with-headers.py localhost:$HGPORT 'rev/%40?style=paper' | egrep '^200|changeset 0:'
800 $ get-with-headers.py localhost:$HGPORT 'rev/%40?style=paper' | egrep '^200|changeset 0:'
801 200 Script output follows
801 200 Script output follows
802 changeset 0:<a href="/rev/2ef0ac749a14?style=paper">2ef0ac749a14</a>
802 changeset 0:<a href="/rev/2ef0ac749a14?style=paper">2ef0ac749a14</a>
803
803
804 $ get-with-headers.py localhost:$HGPORT 'rev/a%20b%20c?style=paper' | egrep '^200|changeset 0:'
804 $ get-with-headers.py localhost:$HGPORT 'rev/a%20b%20c?style=paper' | egrep '^200|changeset 0:'
805 200 Script output follows
805 200 Script output follows
806 changeset 0:<a href="/rev/2ef0ac749a14?style=paper">2ef0ac749a14</a>
806 changeset 0:<a href="/rev/2ef0ac749a14?style=paper">2ef0ac749a14</a>
807
807
808 $ get-with-headers.py localhost:$HGPORT 'rev/d%252Fe%252Ff?style=paper' | egrep '^200|changeset 0:'
808 $ get-with-headers.py localhost:$HGPORT 'rev/d%252Fe%252Ff?style=paper' | egrep '^200|changeset 0:'
809 200 Script output follows
809 200 Script output follows
810 changeset 0:<a href="/rev/2ef0ac749a14?style=paper">2ef0ac749a14</a>
810 changeset 0:<a href="/rev/2ef0ac749a14?style=paper">2ef0ac749a14</a>
811
811
812 no style can be loaded from directories other than the specified paths
812 no style can be loaded from directories other than the specified paths
813
813
814 $ mkdir -p x/templates/fallback
814 $ mkdir -p x/templates/fallback
815 $ cat <<EOF > x/templates/fallback/map
815 $ cat <<EOF > x/templates/fallback/map
816 > default = 'shortlog'
816 > default = 'shortlog'
817 > shortlog = 'fall back to default\n'
817 > shortlog = 'fall back to default\n'
818 > mimetype = 'text/plain'
818 > mimetype = 'text/plain'
819 > EOF
819 > EOF
820 $ cat <<EOF > x/map
820 $ cat <<EOF > x/map
821 > default = 'shortlog'
821 > default = 'shortlog'
822 > shortlog = 'access to outside of templates directory\n'
822 > shortlog = 'access to outside of templates directory\n'
823 > mimetype = 'text/plain'
823 > mimetype = 'text/plain'
824 > EOF
824 > EOF
825
825
826 $ killdaemons.py
826 $ killdaemons.py
827 $ hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log \
827 $ hg serve -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log \
828 > --config web.style=fallback --config web.templates=x/templates
828 > --config web.style=fallback --config web.templates=x/templates
829 $ cat hg.pid >> $DAEMON_PIDS
829 $ cat hg.pid >> $DAEMON_PIDS
830
830
831 $ get-with-headers.py localhost:$HGPORT "?style=`pwd`/x"
831 $ get-with-headers.py localhost:$HGPORT "?style=`pwd`/x"
832 200 Script output follows
832 200 Script output follows
833
833
834 fall back to default
834 fall back to default
835
835
836 $ get-with-headers.py localhost:$HGPORT '?style=..'
836 $ get-with-headers.py localhost:$HGPORT '?style=..'
837 200 Script output follows
837 200 Script output follows
838
838
839 fall back to default
839 fall back to default
840
840
841 $ get-with-headers.py localhost:$HGPORT '?style=./..'
841 $ get-with-headers.py localhost:$HGPORT '?style=./..'
842 200 Script output follows
842 200 Script output follows
843
843
844 fall back to default
844 fall back to default
845
845
846 $ get-with-headers.py localhost:$HGPORT '?style=.../.../'
846 $ get-with-headers.py localhost:$HGPORT '?style=.../.../'
847 200 Script output follows
847 200 Script output follows
848
848
849 fall back to default
849 fall back to default
850
850
851 errors
851 errors
852
852
853 $ cat errors.log
853 $ cat errors.log
854
854
855 Uncaught exceptions result in a logged error and canned HTTP response
855 Uncaught exceptions result in a logged error and canned HTTP response
856
856
857 $ killdaemons.py
857 $ killdaemons.py
858 $ hg serve --config extensions.hgweberror=$TESTDIR/hgweberror.py -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
858 $ hg serve --config extensions.hgweberror=$TESTDIR/hgweberror.py -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
859 $ cat hg.pid >> $DAEMON_PIDS
859 $ cat hg.pid >> $DAEMON_PIDS
860
860
861 $ get-with-headers.py localhost:$HGPORT 'raiseerror' transfer-encoding content-type
861 $ get-with-headers.py localhost:$HGPORT 'raiseerror' transfer-encoding content-type
862 500 Internal Server Error
862 500 Internal Server Error
863 transfer-encoding: chunked
863 transfer-encoding: chunked
864
864
865 Internal Server Error (no-eol)
865 Internal Server Error (no-eol)
866 [1]
866 [1]
867
867
868 $ killdaemons.py
868 $ killdaemons.py
869 $ head -1 errors.log
869 $ head -1 errors.log
870 .* Exception happened during processing request '/raiseerror': (re)
870 .* Exception happened during processing request '/raiseerror': (re)
871
871
872 Uncaught exception after partial content sent
872 Uncaught exception after partial content sent
873
873
874 $ hg serve --config extensions.hgweberror=$TESTDIR/hgweberror.py -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
874 $ hg serve --config extensions.hgweberror=$TESTDIR/hgweberror.py -p $HGPORT -d --pid-file=hg.pid -A access.log -E errors.log
875 $ cat hg.pid >> $DAEMON_PIDS
875 $ cat hg.pid >> $DAEMON_PIDS
876 $ get-with-headers.py localhost:$HGPORT 'raiseerror?partialresponse=1' transfer-encoding content-type
876 $ get-with-headers.py localhost:$HGPORT 'raiseerror?partialresponse=1' transfer-encoding content-type
877 200 Script output follows
877 200 Script output follows
878 transfer-encoding: chunked
878 transfer-encoding: chunked
879 content-type: text/plain
879 content-type: text/plain
880
880
881 partial content
881 partial content
882 Internal Server Error (no-eol)
882 Internal Server Error (no-eol)
883
883
884 $ killdaemons.py
884 $ killdaemons.py
885 $ cd ..
885 $ cd ..
@@ -1,83 +1,83 b''
1 $ echo '[extensions]' >> $HGRCPATH
1 $ echo '[extensions]' >> $HGRCPATH
2 $ echo 'strip =' >> $HGRCPATH
2 $ echo 'strip =' >> $HGRCPATH
3
3
4 $ cat >findbranch.py <<EOF
4 $ cat >findbranch.py <<EOF
5 > from __future__ import absolute_import
5 > from __future__ import absolute_import
6 > import re
6 > import re
7 > import sys
7 > import sys
8 >
8 >
9 > head_re = re.compile('^#(?:(?:\\s+([A-Za-z][A-Za-z0-9_]*)(?:\\s.*)?)|(?:\\s*))$')
9 > head_re = re.compile('^#(?:(?:\\s+([A-Za-z][A-Za-z0-9_]*)(?:\\s.*)?)|(?:\\s*))$')
10 >
10 >
11 > for line in sys.stdin:
11 > for line in sys.stdin:
12 > hmatch = head_re.match(line)
12 > hmatch = head_re.match(line)
13 > if not hmatch:
13 > if not hmatch:
14 > sys.exit(1)
14 > sys.exit(1)
15 > if hmatch.group(1) == 'Branch':
15 > if hmatch.group(1) == 'Branch':
16 > sys.exit(0)
16 > sys.exit(0)
17 > sys.exit(1)
17 > sys.exit(1)
18 > EOF
18 > EOF
19
19
20 $ hg init a
20 $ hg init a
21 $ cd a
21 $ cd a
22 $ echo "Rev 1" >rev
22 $ echo "Rev 1" >rev
23 $ hg add rev
23 $ hg add rev
24 $ hg commit -m "No branch."
24 $ hg commit -m "No branch."
25 $ hg branch abranch
25 $ hg branch abranch
26 marked working directory as branch abranch
26 marked working directory as branch abranch
27 (branches are permanent and global, did you want a bookmark?)
27 (branches are permanent and global, did you want a bookmark?)
28 $ echo "Rev 2" >rev
28 $ echo "Rev 2" >rev
29 $ hg commit -m "With branch."
29 $ hg commit -m "With branch."
30
30
31 $ hg export 0 > ../r0.patch
31 $ hg export 0 > ../r0.patch
32 $ hg export 1 > ../r1.patch
32 $ hg export 1 > ../r1.patch
33 $ cd ..
33 $ cd ..
34
34
35 $ if $PYTHON findbranch.py < r0.patch; then
35 $ if $PYTHON findbranch.py < r0.patch; then
36 > echo "Export of default branch revision has Branch header" 1>&2
36 > echo "Export of default branch revision has Branch header" 1>&2
37 > exit 1
37 > exit 1
38 > fi
38 > fi
39
39
40 $ if $PYTHON findbranch.py < r1.patch; then
40 $ if $PYTHON findbranch.py < r1.patch; then
41 > : # Do nothing
41 > : # Do nothing
42 > else
42 > else
43 > echo "Export of branch revision is missing Branch header" 1>&2
43 > echo "Export of branch revision is missing Branch header" 1>&2
44 > exit 1
44 > exit 1
45 > fi
45 > fi
46
46
47 Make sure import still works with branch information in patches.
47 Make sure import still works with branch information in patches.
48
48
49 $ hg init b
49 $ hg init b
50 $ cd b
50 $ cd b
51 $ hg import ../r0.patch
51 $ hg import ../r0.patch
52 applying ../r0.patch
52 applying ../r0.patch
53 $ hg import ../r1.patch
53 $ hg import ../r1.patch
54 applying ../r1.patch
54 applying ../r1.patch
55 $ cd ..
55 $ cd ..
56
56
57 $ hg init c
57 $ hg init c
58 $ cd c
58 $ cd c
59 $ hg import --exact --no-commit ../r0.patch
59 $ hg import --exact --no-commit ../r0.patch
60 applying ../r0.patch
60 applying ../r0.patch
61 warning: can't check exact import with --no-commit
61 warning: can't check exact import with --no-commit
62 $ hg st
62 $ hg st
63 A rev
63 A rev
64 $ hg revert -a
64 $ hg revert -a
65 forgetting rev
65 forgetting rev
66 $ rm rev
66 $ rm rev
67 $ hg import --exact ../r0.patch
67 $ hg import --exact ../r0.patch
68 applying ../r0.patch
68 applying ../r0.patch
69 $ hg import --exact ../r1.patch
69 $ hg import --exact ../r1.patch
70 applying ../r1.patch
70 applying ../r1.patch
71
71
72 Test --exact and patch header separators (issue3356)
72 Test --exact and patch header separators (issue3356)
73
73
74 $ hg strip --no-backup .
74 $ hg strip --no-backup .
75 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
75 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
76 >>> import re
76 >>> import re
77 >>> p = file('../r1.patch', 'rb').read()
77 >>> p = open('../r1.patch', 'rb').read()
78 >>> p = re.sub(r'Parent\s+', 'Parent ', p)
78 >>> p = re.sub(r'Parent\s+', 'Parent ', p)
79 >>> file('../r1-ws.patch', 'wb').write(p)
79 >>> open('../r1-ws.patch', 'wb').write(p)
80 $ hg import --exact ../r1-ws.patch
80 $ hg import --exact ../r1-ws.patch
81 applying ../r1-ws.patch
81 applying ../r1-ws.patch
82
82
83 $ cd ..
83 $ cd ..
@@ -1,126 +1,126 b''
1 Test applying context diffs
1 Test applying context diffs
2
2
3 $ cat > writepatterns.py <<EOF
3 $ cat > writepatterns.py <<EOF
4 > import sys
4 > import sys
5 >
5 >
6 > path = sys.argv[1]
6 > path = sys.argv[1]
7 > lasteol = sys.argv[2] == '1'
7 > lasteol = sys.argv[2] == '1'
8 > patterns = sys.argv[3:]
8 > patterns = sys.argv[3:]
9 >
9 >
10 > fp = file(path, 'wb')
10 > fp = open(path, 'wb')
11 > for i, pattern in enumerate(patterns):
11 > for i, pattern in enumerate(patterns):
12 > count = int(pattern[0:-1])
12 > count = int(pattern[0:-1])
13 > char = pattern[-1] + '\n'
13 > char = pattern[-1] + '\n'
14 > if not lasteol and i == len(patterns) - 1:
14 > if not lasteol and i == len(patterns) - 1:
15 > fp.write((char*count)[:-1])
15 > fp.write((char*count)[:-1])
16 > else:
16 > else:
17 > fp.write(char*count)
17 > fp.write(char*count)
18 > fp.close()
18 > fp.close()
19 > EOF
19 > EOF
20 $ cat > cat.py <<EOF
20 $ cat > cat.py <<EOF
21 > import sys
21 > import sys
22 > sys.stdout.write(repr(file(sys.argv[1], 'rb').read()) + '\n')
22 > sys.stdout.write(repr(open(sys.argv[1], 'rb').read()) + '\n')
23 > EOF
23 > EOF
24
24
25 Initialize the test repository
25 Initialize the test repository
26
26
27 $ hg init repo
27 $ hg init repo
28 $ cd repo
28 $ cd repo
29 $ $PYTHON ../writepatterns.py a 0 5A 1B 5C 1D
29 $ $PYTHON ../writepatterns.py a 0 5A 1B 5C 1D
30 $ $PYTHON ../writepatterns.py b 1 1A 1B
30 $ $PYTHON ../writepatterns.py b 1 1A 1B
31 $ $PYTHON ../writepatterns.py c 1 5A
31 $ $PYTHON ../writepatterns.py c 1 5A
32 $ $PYTHON ../writepatterns.py d 1 5A 1B
32 $ $PYTHON ../writepatterns.py d 1 5A 1B
33 $ hg add
33 $ hg add
34 adding a
34 adding a
35 adding b
35 adding b
36 adding c
36 adding c
37 adding d
37 adding d
38 $ hg ci -m addfiles
38 $ hg ci -m addfiles
39
39
40 Add file, missing a last end of line
40 Add file, missing a last end of line
41
41
42 $ hg import --no-commit - <<EOF
42 $ hg import --no-commit - <<EOF
43 > *** /dev/null 2010-10-16 18:05:49.000000000 +0200
43 > *** /dev/null 2010-10-16 18:05:49.000000000 +0200
44 > --- b/newnoeol 2010-10-16 18:23:26.000000000 +0200
44 > --- b/newnoeol 2010-10-16 18:23:26.000000000 +0200
45 > ***************
45 > ***************
46 > *** 0 ****
46 > *** 0 ****
47 > --- 1,2 ----
47 > --- 1,2 ----
48 > + a
48 > + a
49 > + b
49 > + b
50 > \ No newline at end of file
50 > \ No newline at end of file
51 > *** a/a Sat Oct 16 16:35:51 2010
51 > *** a/a Sat Oct 16 16:35:51 2010
52 > --- b/a Sat Oct 16 16:35:51 2010
52 > --- b/a Sat Oct 16 16:35:51 2010
53 > ***************
53 > ***************
54 > *** 3,12 ****
54 > *** 3,12 ****
55 > A
55 > A
56 > A
56 > A
57 > A
57 > A
58 > ! B
58 > ! B
59 > C
59 > C
60 > C
60 > C
61 > C
61 > C
62 > C
62 > C
63 > C
63 > C
64 > ! D
64 > ! D
65 > \ No newline at end of file
65 > \ No newline at end of file
66 > --- 3,13 ----
66 > --- 3,13 ----
67 > A
67 > A
68 > A
68 > A
69 > A
69 > A
70 > ! E
70 > ! E
71 > C
71 > C
72 > C
72 > C
73 > C
73 > C
74 > C
74 > C
75 > C
75 > C
76 > ! F
76 > ! F
77 > ! F
77 > ! F
78 >
78 >
79 > *** a/b 2010-10-16 18:40:38.000000000 +0200
79 > *** a/b 2010-10-16 18:40:38.000000000 +0200
80 > --- /dev/null 2010-10-16 18:05:49.000000000 +0200
80 > --- /dev/null 2010-10-16 18:05:49.000000000 +0200
81 > ***************
81 > ***************
82 > *** 1,2 ****
82 > *** 1,2 ****
83 > - A
83 > - A
84 > - B
84 > - B
85 > --- 0 ----
85 > --- 0 ----
86 > *** a/c Sat Oct 16 21:34:26 2010
86 > *** a/c Sat Oct 16 21:34:26 2010
87 > --- b/c Sat Oct 16 21:34:27 2010
87 > --- b/c Sat Oct 16 21:34:27 2010
88 > ***************
88 > ***************
89 > *** 3,5 ****
89 > *** 3,5 ****
90 > --- 3,7 ----
90 > --- 3,7 ----
91 > A
91 > A
92 > A
92 > A
93 > A
93 > A
94 > + B
94 > + B
95 > + B
95 > + B
96 > *** a/d Sat Oct 16 21:47:20 2010
96 > *** a/d Sat Oct 16 21:47:20 2010
97 > --- b/d Sat Oct 16 21:47:22 2010
97 > --- b/d Sat Oct 16 21:47:22 2010
98 > ***************
98 > ***************
99 > *** 2,6 ****
99 > *** 2,6 ****
100 > A
100 > A
101 > A
101 > A
102 > A
102 > A
103 > - A
103 > - A
104 > - B
104 > - B
105 > --- 2,4 ----
105 > --- 2,4 ----
106 > EOF
106 > EOF
107 applying patch from stdin
107 applying patch from stdin
108 $ hg st
108 $ hg st
109 M a
109 M a
110 M c
110 M c
111 M d
111 M d
112 A newnoeol
112 A newnoeol
113 R b
113 R b
114
114
115 What's in a
115 What's in a
116
116
117 $ $PYTHON ../cat.py a
117 $ $PYTHON ../cat.py a
118 'A\nA\nA\nA\nA\nE\nC\nC\nC\nC\nC\nF\nF\n'
118 'A\nA\nA\nA\nA\nE\nC\nC\nC\nC\nC\nF\nF\n'
119 $ $PYTHON ../cat.py newnoeol
119 $ $PYTHON ../cat.py newnoeol
120 'a\nb'
120 'a\nb'
121 $ $PYTHON ../cat.py c
121 $ $PYTHON ../cat.py c
122 'A\nA\nA\nA\nA\nB\nB\n'
122 'A\nA\nA\nA\nA\nB\nB\n'
123 $ $PYTHON ../cat.py d
123 $ $PYTHON ../cat.py d
124 'A\nA\nA\nA\n'
124 'A\nA\nA\nA\n'
125
125
126 $ cd ..
126 $ cd ..
@@ -1,618 +1,618 b''
1 $ cat > $TESTTMP/filter.py <<EOF
1 $ cat > $TESTTMP/filter.py <<EOF
2 > from __future__ import absolute_import, print_function
2 > from __future__ import absolute_import, print_function
3 > import re
3 > import re
4 > import sys
4 > import sys
5 > print(re.sub("\n[ \t]", " ", sys.stdin.read()), end="")
5 > print(re.sub("\n[ \t]", " ", sys.stdin.read()), end="")
6 > EOF
6 > EOF
7
7
8 $ cat <<EOF >> $HGRCPATH
8 $ cat <<EOF >> $HGRCPATH
9 > [extensions]
9 > [extensions]
10 > notify=
10 > notify=
11 >
11 >
12 > [hooks]
12 > [hooks]
13 > incoming.notify = python:hgext.notify.hook
13 > incoming.notify = python:hgext.notify.hook
14 >
14 >
15 > [notify]
15 > [notify]
16 > sources = pull
16 > sources = pull
17 > diffstat = False
17 > diffstat = False
18 >
18 >
19 > [usersubs]
19 > [usersubs]
20 > foo@bar = *
20 > foo@bar = *
21 >
21 >
22 > [reposubs]
22 > [reposubs]
23 > * = baz
23 > * = baz
24 > EOF
24 > EOF
25 $ hg help notify
25 $ hg help notify
26 notify extension - hooks for sending email push notifications
26 notify extension - hooks for sending email push notifications
27
27
28 This extension implements hooks to send email notifications when changesets
28 This extension implements hooks to send email notifications when changesets
29 are sent from or received by the local repository.
29 are sent from or received by the local repository.
30
30
31 First, enable the extension as explained in 'hg help extensions', and register
31 First, enable the extension as explained in 'hg help extensions', and register
32 the hook you want to run. "incoming" and "changegroup" hooks are run when
32 the hook you want to run. "incoming" and "changegroup" hooks are run when
33 changesets are received, while "outgoing" hooks are for changesets sent to
33 changesets are received, while "outgoing" hooks are for changesets sent to
34 another repository:
34 another repository:
35
35
36 [hooks]
36 [hooks]
37 # one email for each incoming changeset
37 # one email for each incoming changeset
38 incoming.notify = python:hgext.notify.hook
38 incoming.notify = python:hgext.notify.hook
39 # one email for all incoming changesets
39 # one email for all incoming changesets
40 changegroup.notify = python:hgext.notify.hook
40 changegroup.notify = python:hgext.notify.hook
41
41
42 # one email for all outgoing changesets
42 # one email for all outgoing changesets
43 outgoing.notify = python:hgext.notify.hook
43 outgoing.notify = python:hgext.notify.hook
44
44
45 This registers the hooks. To enable notification, subscribers must be assigned
45 This registers the hooks. To enable notification, subscribers must be assigned
46 to repositories. The "[usersubs]" section maps multiple repositories to a
46 to repositories. The "[usersubs]" section maps multiple repositories to a
47 given recipient. The "[reposubs]" section maps multiple recipients to a single
47 given recipient. The "[reposubs]" section maps multiple recipients to a single
48 repository:
48 repository:
49
49
50 [usersubs]
50 [usersubs]
51 # key is subscriber email, value is a comma-separated list of repo patterns
51 # key is subscriber email, value is a comma-separated list of repo patterns
52 user@host = pattern
52 user@host = pattern
53
53
54 [reposubs]
54 [reposubs]
55 # key is repo pattern, value is a comma-separated list of subscriber emails
55 # key is repo pattern, value is a comma-separated list of subscriber emails
56 pattern = user@host
56 pattern = user@host
57
57
58 A "pattern" is a "glob" matching the absolute path to a repository, optionally
58 A "pattern" is a "glob" matching the absolute path to a repository, optionally
59 combined with a revset expression. A revset expression, if present, is
59 combined with a revset expression. A revset expression, if present, is
60 separated from the glob by a hash. Example:
60 separated from the glob by a hash. Example:
61
61
62 [reposubs]
62 [reposubs]
63 */widgets#branch(release) = qa-team@example.com
63 */widgets#branch(release) = qa-team@example.com
64
64
65 This sends to "qa-team@example.com" whenever a changeset on the "release"
65 This sends to "qa-team@example.com" whenever a changeset on the "release"
66 branch triggers a notification in any repository ending in "widgets".
66 branch triggers a notification in any repository ending in "widgets".
67
67
68 In order to place them under direct user management, "[usersubs]" and
68 In order to place them under direct user management, "[usersubs]" and
69 "[reposubs]" sections may be placed in a separate "hgrc" file and incorporated
69 "[reposubs]" sections may be placed in a separate "hgrc" file and incorporated
70 by reference:
70 by reference:
71
71
72 [notify]
72 [notify]
73 config = /path/to/subscriptionsfile
73 config = /path/to/subscriptionsfile
74
74
75 Notifications will not be sent until the "notify.test" value is set to
75 Notifications will not be sent until the "notify.test" value is set to
76 "False"; see below.
76 "False"; see below.
77
77
78 Notifications content can be tweaked with the following configuration entries:
78 Notifications content can be tweaked with the following configuration entries:
79
79
80 notify.test
80 notify.test
81 If "True", print messages to stdout instead of sending them. Default: True.
81 If "True", print messages to stdout instead of sending them. Default: True.
82
82
83 notify.sources
83 notify.sources
84 Space-separated list of change sources. Notifications are activated only
84 Space-separated list of change sources. Notifications are activated only
85 when a changeset's source is in this list. Sources may be:
85 when a changeset's source is in this list. Sources may be:
86
86
87 "serve" changesets received via http or ssh
87 "serve" changesets received via http or ssh
88 "pull" changesets received via "hg pull"
88 "pull" changesets received via "hg pull"
89 "unbundle" changesets received via "hg unbundle"
89 "unbundle" changesets received via "hg unbundle"
90 "push" changesets sent or received via "hg push"
90 "push" changesets sent or received via "hg push"
91 "bundle" changesets sent via "hg unbundle"
91 "bundle" changesets sent via "hg unbundle"
92
92
93 Default: serve.
93 Default: serve.
94
94
95 notify.strip
95 notify.strip
96 Number of leading slashes to strip from url paths. By default, notifications
96 Number of leading slashes to strip from url paths. By default, notifications
97 reference repositories with their absolute path. "notify.strip" lets you
97 reference repositories with their absolute path. "notify.strip" lets you
98 turn them into relative paths. For example, "notify.strip=3" will change
98 turn them into relative paths. For example, "notify.strip=3" will change
99 "/long/path/repository" into "repository". Default: 0.
99 "/long/path/repository" into "repository". Default: 0.
100
100
101 notify.domain
101 notify.domain
102 Default email domain for sender or recipients with no explicit domain.
102 Default email domain for sender or recipients with no explicit domain.
103
103
104 notify.style
104 notify.style
105 Style file to use when formatting emails.
105 Style file to use when formatting emails.
106
106
107 notify.template
107 notify.template
108 Template to use when formatting emails.
108 Template to use when formatting emails.
109
109
110 notify.incoming
110 notify.incoming
111 Template to use when run as an incoming hook, overriding "notify.template".
111 Template to use when run as an incoming hook, overriding "notify.template".
112
112
113 notify.outgoing
113 notify.outgoing
114 Template to use when run as an outgoing hook, overriding "notify.template".
114 Template to use when run as an outgoing hook, overriding "notify.template".
115
115
116 notify.changegroup
116 notify.changegroup
117 Template to use when running as a changegroup hook, overriding
117 Template to use when running as a changegroup hook, overriding
118 "notify.template".
118 "notify.template".
119
119
120 notify.maxdiff
120 notify.maxdiff
121 Maximum number of diff lines to include in notification email. Set to 0 to
121 Maximum number of diff lines to include in notification email. Set to 0 to
122 disable the diff, or -1 to include all of it. Default: 300.
122 disable the diff, or -1 to include all of it. Default: 300.
123
123
124 notify.maxsubject
124 notify.maxsubject
125 Maximum number of characters in email's subject line. Default: 67.
125 Maximum number of characters in email's subject line. Default: 67.
126
126
127 notify.diffstat
127 notify.diffstat
128 Set to True to include a diffstat before diff content. Default: True.
128 Set to True to include a diffstat before diff content. Default: True.
129
129
130 notify.merge
130 notify.merge
131 If True, send notifications for merge changesets. Default: True.
131 If True, send notifications for merge changesets. Default: True.
132
132
133 notify.mbox
133 notify.mbox
134 If set, append mails to this mbox file instead of sending. Default: None.
134 If set, append mails to this mbox file instead of sending. Default: None.
135
135
136 notify.fromauthor
136 notify.fromauthor
137 If set, use the committer of the first changeset in a changegroup for the
137 If set, use the committer of the first changeset in a changegroup for the
138 "From" field of the notification mail. If not set, take the user from the
138 "From" field of the notification mail. If not set, take the user from the
139 pushing repo. Default: False.
139 pushing repo. Default: False.
140
140
141 If set, the following entries will also be used to customize the
141 If set, the following entries will also be used to customize the
142 notifications:
142 notifications:
143
143
144 email.from
144 email.from
145 Email "From" address to use if none can be found in the generated email
145 Email "From" address to use if none can be found in the generated email
146 content.
146 content.
147
147
148 web.baseurl
148 web.baseurl
149 Root repository URL to combine with repository paths when making references.
149 Root repository URL to combine with repository paths when making references.
150 See also "notify.strip".
150 See also "notify.strip".
151
151
152 no commands defined
152 no commands defined
153 $ hg init a
153 $ hg init a
154 $ echo a > a/a
154 $ echo a > a/a
155
155
156 commit
156 commit
157
157
158 $ hg --cwd a commit -Ama -d '0 0'
158 $ hg --cwd a commit -Ama -d '0 0'
159 adding a
159 adding a
160
160
161
161
162 clone
162 clone
163
163
164 $ hg --traceback clone a b
164 $ hg --traceback clone a b
165 updating to branch default
165 updating to branch default
166 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
166 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
167 $ echo a >> a/a
167 $ echo a >> a/a
168
168
169 commit
169 commit
170
170
171 $ hg --traceback --cwd a commit -Amb -d '1 0'
171 $ hg --traceback --cwd a commit -Amb -d '1 0'
172
172
173 on Mac OS X 10.5 the tmp path is very long so would get stripped in the subject line
173 on Mac OS X 10.5 the tmp path is very long so would get stripped in the subject line
174
174
175 $ cat <<EOF >> $HGRCPATH
175 $ cat <<EOF >> $HGRCPATH
176 > [notify]
176 > [notify]
177 > maxsubject = 200
177 > maxsubject = 200
178 > EOF
178 > EOF
179
179
180 the python call below wraps continuation lines, which appear on Mac OS X 10.5 because
180 the python call below wraps continuation lines, which appear on Mac OS X 10.5 because
181 of the very long subject line
181 of the very long subject line
182 pull (minimal config)
182 pull (minimal config)
183
183
184 $ hg --traceback --cwd b pull ../a | $PYTHON $TESTTMP/filter.py
184 $ hg --traceback --cwd b pull ../a | $PYTHON $TESTTMP/filter.py
185 pulling from ../a
185 pulling from ../a
186 searching for changes
186 searching for changes
187 adding changesets
187 adding changesets
188 adding manifests
188 adding manifests
189 adding file changes
189 adding file changes
190 added 1 changesets with 1 changes to 1 files
190 added 1 changesets with 1 changes to 1 files
191 new changesets 0647d048b600
191 new changesets 0647d048b600
192 MIME-Version: 1.0
192 MIME-Version: 1.0
193 Content-Type: text/plain; charset="us-ascii"
193 Content-Type: text/plain; charset="us-ascii"
194 Content-Transfer-Encoding: 7bit
194 Content-Transfer-Encoding: 7bit
195 Date: * (glob)
195 Date: * (glob)
196 Subject: changeset in $TESTTMP/b: b
196 Subject: changeset in $TESTTMP/b: b
197 From: test
197 From: test
198 X-Hg-Notification: changeset 0647d048b600
198 X-Hg-Notification: changeset 0647d048b600
199 Message-Id: <*> (glob)
199 Message-Id: <*> (glob)
200 To: baz, foo@bar
200 To: baz, foo@bar
201
201
202 changeset 0647d048b600 in $TESTTMP/b
202 changeset 0647d048b600 in $TESTTMP/b
203 details: $TESTTMP/b?cmd=changeset;node=0647d048b600
203 details: $TESTTMP/b?cmd=changeset;node=0647d048b600
204 description: b
204 description: b
205
205
206 diffs (6 lines):
206 diffs (6 lines):
207
207
208 diff -r cb9a9f314b8b -r 0647d048b600 a
208 diff -r cb9a9f314b8b -r 0647d048b600 a
209 --- a/a Thu Jan 01 00:00:00 1970 +0000
209 --- a/a Thu Jan 01 00:00:00 1970 +0000
210 +++ b/a Thu Jan 01 00:00:01 1970 +0000
210 +++ b/a Thu Jan 01 00:00:01 1970 +0000
211 @@ -1,1 +1,2 @@ a
211 @@ -1,1 +1,2 @@ a
212 +a
212 +a
213 (run 'hg update' to get a working copy)
213 (run 'hg update' to get a working copy)
214
214
215 $ cat <<EOF >> $HGRCPATH
215 $ cat <<EOF >> $HGRCPATH
216 > [notify]
216 > [notify]
217 > config = `pwd`/.notify.conf
217 > config = `pwd`/.notify.conf
218 > domain = test.com
218 > domain = test.com
219 > strip = 42
219 > strip = 42
220 > template = Subject: {desc|firstline|strip}\nFrom: {author}\nX-Test: foo\n\nchangeset {node|short} in {webroot}\ndescription:\n\t{desc|tabindent|strip}
220 > template = Subject: {desc|firstline|strip}\nFrom: {author}\nX-Test: foo\n\nchangeset {node|short} in {webroot}\ndescription:\n\t{desc|tabindent|strip}
221 >
221 >
222 > [web]
222 > [web]
223 > baseurl = http://test/
223 > baseurl = http://test/
224 > EOF
224 > EOF
225
225
226 fail for config file is missing
226 fail for config file is missing
227
227
228 $ hg --cwd b rollback
228 $ hg --cwd b rollback
229 repository tip rolled back to revision 0 (undo pull)
229 repository tip rolled back to revision 0 (undo pull)
230 $ hg --cwd b pull ../a 2>&1 | grep 'error.*\.notify\.conf' > /dev/null && echo pull failed
230 $ hg --cwd b pull ../a 2>&1 | grep 'error.*\.notify\.conf' > /dev/null && echo pull failed
231 pull failed
231 pull failed
232 $ touch ".notify.conf"
232 $ touch ".notify.conf"
233
233
234 pull
234 pull
235
235
236 $ hg --cwd b rollback
236 $ hg --cwd b rollback
237 repository tip rolled back to revision 0 (undo pull)
237 repository tip rolled back to revision 0 (undo pull)
238 $ hg --traceback --cwd b pull ../a | $PYTHON $TESTTMP/filter.py
238 $ hg --traceback --cwd b pull ../a | $PYTHON $TESTTMP/filter.py
239 pulling from ../a
239 pulling from ../a
240 searching for changes
240 searching for changes
241 adding changesets
241 adding changesets
242 adding manifests
242 adding manifests
243 adding file changes
243 adding file changes
244 added 1 changesets with 1 changes to 1 files
244 added 1 changesets with 1 changes to 1 files
245 new changesets 0647d048b600
245 new changesets 0647d048b600
246 MIME-Version: 1.0
246 MIME-Version: 1.0
247 Content-Type: text/plain; charset="us-ascii"
247 Content-Type: text/plain; charset="us-ascii"
248 Content-Transfer-Encoding: 7bit
248 Content-Transfer-Encoding: 7bit
249 X-Test: foo
249 X-Test: foo
250 Date: * (glob)
250 Date: * (glob)
251 Subject: b
251 Subject: b
252 From: test@test.com
252 From: test@test.com
253 X-Hg-Notification: changeset 0647d048b600
253 X-Hg-Notification: changeset 0647d048b600
254 Message-Id: <*> (glob)
254 Message-Id: <*> (glob)
255 To: baz@test.com, foo@bar
255 To: baz@test.com, foo@bar
256
256
257 changeset 0647d048b600 in b
257 changeset 0647d048b600 in b
258 description: b
258 description: b
259 diffs (6 lines):
259 diffs (6 lines):
260
260
261 diff -r cb9a9f314b8b -r 0647d048b600 a
261 diff -r cb9a9f314b8b -r 0647d048b600 a
262 --- a/a Thu Jan 01 00:00:00 1970 +0000
262 --- a/a Thu Jan 01 00:00:00 1970 +0000
263 +++ b/a Thu Jan 01 00:00:01 1970 +0000
263 +++ b/a Thu Jan 01 00:00:01 1970 +0000
264 @@ -1,1 +1,2 @@ a
264 @@ -1,1 +1,2 @@ a
265 +a
265 +a
266 (run 'hg update' to get a working copy)
266 (run 'hg update' to get a working copy)
267
267
268 $ cat << EOF >> $HGRCPATH
268 $ cat << EOF >> $HGRCPATH
269 > [hooks]
269 > [hooks]
270 > incoming.notify = python:hgext.notify.hook
270 > incoming.notify = python:hgext.notify.hook
271 >
271 >
272 > [notify]
272 > [notify]
273 > sources = pull
273 > sources = pull
274 > diffstat = True
274 > diffstat = True
275 > EOF
275 > EOF
276
276
277 pull
277 pull
278
278
279 $ hg --cwd b rollback
279 $ hg --cwd b rollback
280 repository tip rolled back to revision 0 (undo pull)
280 repository tip rolled back to revision 0 (undo pull)
281 $ hg --traceback --cwd b pull ../a | $PYTHON $TESTTMP/filter.py
281 $ hg --traceback --cwd b pull ../a | $PYTHON $TESTTMP/filter.py
282 pulling from ../a
282 pulling from ../a
283 searching for changes
283 searching for changes
284 adding changesets
284 adding changesets
285 adding manifests
285 adding manifests
286 adding file changes
286 adding file changes
287 added 1 changesets with 1 changes to 1 files
287 added 1 changesets with 1 changes to 1 files
288 new changesets 0647d048b600
288 new changesets 0647d048b600
289 MIME-Version: 1.0
289 MIME-Version: 1.0
290 Content-Type: text/plain; charset="us-ascii"
290 Content-Type: text/plain; charset="us-ascii"
291 Content-Transfer-Encoding: 7bit
291 Content-Transfer-Encoding: 7bit
292 X-Test: foo
292 X-Test: foo
293 Date: * (glob)
293 Date: * (glob)
294 Subject: b
294 Subject: b
295 From: test@test.com
295 From: test@test.com
296 X-Hg-Notification: changeset 0647d048b600
296 X-Hg-Notification: changeset 0647d048b600
297 Message-Id: <*> (glob)
297 Message-Id: <*> (glob)
298 To: baz@test.com, foo@bar
298 To: baz@test.com, foo@bar
299
299
300 changeset 0647d048b600 in b
300 changeset 0647d048b600 in b
301 description: b
301 description: b
302 diffstat:
302 diffstat:
303 a | 1 + 1 files changed, 1 insertions(+), 0 deletions(-)
303 a | 1 + 1 files changed, 1 insertions(+), 0 deletions(-)
304
304
305 diffs (6 lines):
305 diffs (6 lines):
306
306
307 diff -r cb9a9f314b8b -r 0647d048b600 a
307 diff -r cb9a9f314b8b -r 0647d048b600 a
308 --- a/a Thu Jan 01 00:00:00 1970 +0000
308 --- a/a Thu Jan 01 00:00:00 1970 +0000
309 +++ b/a Thu Jan 01 00:00:01 1970 +0000
309 +++ b/a Thu Jan 01 00:00:01 1970 +0000
310 @@ -1,1 +1,2 @@ a
310 @@ -1,1 +1,2 @@ a
311 +a
311 +a
312 (run 'hg update' to get a working copy)
312 (run 'hg update' to get a working copy)
313
313
314 test merge
314 test merge
315
315
316 $ cd a
316 $ cd a
317 $ hg up -C 0
317 $ hg up -C 0
318 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
318 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
319 $ echo a >> a
319 $ echo a >> a
320 $ hg ci -Am adda2 -d '2 0'
320 $ hg ci -Am adda2 -d '2 0'
321 created new head
321 created new head
322 $ hg merge
322 $ hg merge
323 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
323 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
324 (branch merge, don't forget to commit)
324 (branch merge, don't forget to commit)
325 $ hg ci -m merge -d '3 0'
325 $ hg ci -m merge -d '3 0'
326 $ cd ..
326 $ cd ..
327 $ hg --traceback --cwd b pull ../a | $PYTHON $TESTTMP/filter.py
327 $ hg --traceback --cwd b pull ../a | $PYTHON $TESTTMP/filter.py
328 pulling from ../a
328 pulling from ../a
329 searching for changes
329 searching for changes
330 adding changesets
330 adding changesets
331 adding manifests
331 adding manifests
332 adding file changes
332 adding file changes
333 added 2 changesets with 0 changes to 0 files
333 added 2 changesets with 0 changes to 0 files
334 new changesets 0a184ce6067f:6a0cf76b2701
334 new changesets 0a184ce6067f:6a0cf76b2701
335 MIME-Version: 1.0
335 MIME-Version: 1.0
336 Content-Type: text/plain; charset="us-ascii"
336 Content-Type: text/plain; charset="us-ascii"
337 Content-Transfer-Encoding: 7bit
337 Content-Transfer-Encoding: 7bit
338 X-Test: foo
338 X-Test: foo
339 Date: * (glob)
339 Date: * (glob)
340 Subject: adda2
340 Subject: adda2
341 From: test@test.com
341 From: test@test.com
342 X-Hg-Notification: changeset 0a184ce6067f
342 X-Hg-Notification: changeset 0a184ce6067f
343 Message-Id: <*> (glob)
343 Message-Id: <*> (glob)
344 To: baz@test.com, foo@bar
344 To: baz@test.com, foo@bar
345
345
346 changeset 0a184ce6067f in b
346 changeset 0a184ce6067f in b
347 description: adda2
347 description: adda2
348 diffstat:
348 diffstat:
349 a | 1 + 1 files changed, 1 insertions(+), 0 deletions(-)
349 a | 1 + 1 files changed, 1 insertions(+), 0 deletions(-)
350
350
351 diffs (6 lines):
351 diffs (6 lines):
352
352
353 diff -r cb9a9f314b8b -r 0a184ce6067f a
353 diff -r cb9a9f314b8b -r 0a184ce6067f a
354 --- a/a Thu Jan 01 00:00:00 1970 +0000
354 --- a/a Thu Jan 01 00:00:00 1970 +0000
355 +++ b/a Thu Jan 01 00:00:02 1970 +0000
355 +++ b/a Thu Jan 01 00:00:02 1970 +0000
356 @@ -1,1 +1,2 @@ a
356 @@ -1,1 +1,2 @@ a
357 +a
357 +a
358 MIME-Version: 1.0
358 MIME-Version: 1.0
359 Content-Type: text/plain; charset="us-ascii"
359 Content-Type: text/plain; charset="us-ascii"
360 Content-Transfer-Encoding: 7bit
360 Content-Transfer-Encoding: 7bit
361 X-Test: foo
361 X-Test: foo
362 Date: * (glob)
362 Date: * (glob)
363 Subject: merge
363 Subject: merge
364 From: test@test.com
364 From: test@test.com
365 X-Hg-Notification: changeset 6a0cf76b2701
365 X-Hg-Notification: changeset 6a0cf76b2701
366 Message-Id: <*> (glob)
366 Message-Id: <*> (glob)
367 To: baz@test.com, foo@bar
367 To: baz@test.com, foo@bar
368
368
369 changeset 6a0cf76b2701 in b
369 changeset 6a0cf76b2701 in b
370 description: merge
370 description: merge
371 (run 'hg update' to get a working copy)
371 (run 'hg update' to get a working copy)
372
372
373 non-ascii content and truncation of multi-byte subject
373 non-ascii content and truncation of multi-byte subject
374
374
375 $ cat <<EOF >> $HGRCPATH
375 $ cat <<EOF >> $HGRCPATH
376 > [notify]
376 > [notify]
377 > maxsubject = 4
377 > maxsubject = 4
378 > EOF
378 > EOF
379 $ echo a >> a/a
379 $ echo a >> a/a
380 $ hg --cwd a --encoding utf-8 commit -A -d '0 0' \
380 $ hg --cwd a --encoding utf-8 commit -A -d '0 0' \
381 > -m `$PYTHON -c 'print "\xc3\xa0\xc3\xa1\xc3\xa2\xc3\xa3\xc3\xa4"'`
381 > -m `$PYTHON -c 'print "\xc3\xa0\xc3\xa1\xc3\xa2\xc3\xa3\xc3\xa4"'`
382 $ hg --traceback --cwd b --encoding utf-8 pull ../a | \
382 $ hg --traceback --cwd b --encoding utf-8 pull ../a | \
383 > $PYTHON $TESTTMP/filter.py
383 > $PYTHON $TESTTMP/filter.py
384 pulling from ../a
384 pulling from ../a
385 searching for changes
385 searching for changes
386 adding changesets
386 adding changesets
387 adding manifests
387 adding manifests
388 adding file changes
388 adding file changes
389 added 1 changesets with 1 changes to 1 files
389 added 1 changesets with 1 changes to 1 files
390 new changesets 7ea05ad269dc
390 new changesets 7ea05ad269dc
391 MIME-Version: 1.0
391 MIME-Version: 1.0
392 Content-Type: text/plain; charset="us-ascii"
392 Content-Type: text/plain; charset="us-ascii"
393 Content-Transfer-Encoding: 8bit
393 Content-Transfer-Encoding: 8bit
394 X-Test: foo
394 X-Test: foo
395 Date: * (glob)
395 Date: * (glob)
396 Subject: \xc3\xa0... (esc)
396 Subject: \xc3\xa0... (esc)
397 From: test@test.com
397 From: test@test.com
398 X-Hg-Notification: changeset 7ea05ad269dc
398 X-Hg-Notification: changeset 7ea05ad269dc
399 Message-Id: <*> (glob)
399 Message-Id: <*> (glob)
400 To: baz@test.com, foo@bar
400 To: baz@test.com, foo@bar
401
401
402 changeset 7ea05ad269dc in b
402 changeset 7ea05ad269dc in b
403 description: \xc3\xa0\xc3\xa1\xc3\xa2\xc3\xa3\xc3\xa4 (esc)
403 description: \xc3\xa0\xc3\xa1\xc3\xa2\xc3\xa3\xc3\xa4 (esc)
404 diffstat:
404 diffstat:
405 a | 1 + 1 files changed, 1 insertions(+), 0 deletions(-)
405 a | 1 + 1 files changed, 1 insertions(+), 0 deletions(-)
406
406
407 diffs (7 lines):
407 diffs (7 lines):
408
408
409 diff -r 6a0cf76b2701 -r 7ea05ad269dc a
409 diff -r 6a0cf76b2701 -r 7ea05ad269dc a
410 --- a/a Thu Jan 01 00:00:03 1970 +0000
410 --- a/a Thu Jan 01 00:00:03 1970 +0000
411 +++ b/a Thu Jan 01 00:00:00 1970 +0000
411 +++ b/a Thu Jan 01 00:00:00 1970 +0000
412 @@ -1,2 +1,3 @@ a a
412 @@ -1,2 +1,3 @@ a a
413 +a
413 +a
414 (run 'hg update' to get a working copy)
414 (run 'hg update' to get a working copy)
415
415
416 long lines
416 long lines
417
417
418 $ cat <<EOF >> $HGRCPATH
418 $ cat <<EOF >> $HGRCPATH
419 > [notify]
419 > [notify]
420 > maxsubject = 67
420 > maxsubject = 67
421 > test = False
421 > test = False
422 > mbox = mbox
422 > mbox = mbox
423 > EOF
423 > EOF
424 $ $PYTHON -c 'file("a/a", "ab").write("no" * 500 + "\xd1\x84" + "\n")'
424 $ $PYTHON -c 'open("a/a", "ab").write("no" * 500 + "\xd1\x84" + "\n")'
425 $ hg --cwd a commit -A -m "long line"
425 $ hg --cwd a commit -A -m "long line"
426 $ hg --traceback --cwd b pull ../a
426 $ hg --traceback --cwd b pull ../a
427 pulling from ../a
427 pulling from ../a
428 searching for changes
428 searching for changes
429 adding changesets
429 adding changesets
430 adding manifests
430 adding manifests
431 adding file changes
431 adding file changes
432 added 1 changesets with 1 changes to 1 files
432 added 1 changesets with 1 changes to 1 files
433 new changesets a323cae54f6e
433 new changesets a323cae54f6e
434 notify: sending 2 subscribers 1 changes
434 notify: sending 2 subscribers 1 changes
435 (run 'hg update' to get a working copy)
435 (run 'hg update' to get a working copy)
436 $ $PYTHON $TESTTMP/filter.py < b/mbox
436 $ $PYTHON $TESTTMP/filter.py < b/mbox
437 From test@test.com ... ... .. ..:..:.. .... (re)
437 From test@test.com ... ... .. ..:..:.. .... (re)
438 MIME-Version: 1.0
438 MIME-Version: 1.0
439 Content-Type: text/plain; charset="*" (glob)
439 Content-Type: text/plain; charset="*" (glob)
440 Content-Transfer-Encoding: quoted-printable
440 Content-Transfer-Encoding: quoted-printable
441 X-Test: foo
441 X-Test: foo
442 Date: * (glob)
442 Date: * (glob)
443 Subject: long line
443 Subject: long line
444 From: test@test.com
444 From: test@test.com
445 X-Hg-Notification: changeset a323cae54f6e
445 X-Hg-Notification: changeset a323cae54f6e
446 Message-Id: <hg.a323cae54f6e.*.*@*> (glob)
446 Message-Id: <hg.a323cae54f6e.*.*@*> (glob)
447 To: baz@test.com, foo@bar
447 To: baz@test.com, foo@bar
448
448
449 changeset a323cae54f6e in b
449 changeset a323cae54f6e in b
450 description: long line
450 description: long line
451 diffstat:
451 diffstat:
452 a | 1 + 1 files changed, 1 insertions(+), 0 deletions(-)
452 a | 1 + 1 files changed, 1 insertions(+), 0 deletions(-)
453
453
454 diffs (8 lines):
454 diffs (8 lines):
455
455
456 diff -r 7ea05ad269dc -r a323cae54f6e a
456 diff -r 7ea05ad269dc -r a323cae54f6e a
457 --- a/a Thu Jan 01 00:00:00 1970 +0000
457 --- a/a Thu Jan 01 00:00:00 1970 +0000
458 +++ b/a Thu Jan 01 00:00:00 1970 +0000
458 +++ b/a Thu Jan 01 00:00:00 1970 +0000
459 @@ -1,3 +1,4 @@ a a a
459 @@ -1,3 +1,4 @@ a a a
460 +nonononononononononononononononononononononononononononononononononononono=
460 +nonononononononononononononononononononononononononononononononononononono=
461 nononononononononononononononononononononononononononononononononononononon=
461 nononononononononononononononononononononononononononononononononononononon=
462 ononononononononononononononononononononononononononononononononononononono=
462 ononononononononononononononononononononononononononononononononononononono=
463 nononononononononononononononononononononononononononononononononononononon=
463 nononononononononononononononononononononononononononononononononononononon=
464 ononononononononononononononononononononononononononononononononononononono=
464 ononononononononononononononononononononononononononononononononononononono=
465 nononononononononononononononononononononononononononononononononononononon=
465 nononononononononononononononononononononononononononononononononononononon=
466 ononononononononononononononononononononononononononononononononononononono=
466 ononononononononononononononononononononononononononononononononononononono=
467 nononononononononononononononononononononononononononononononononononononon=
467 nononononononononononononononononononononononononononononononononononononon=
468 ononononononononononononononononononononononononononononononononononononono=
468 ononononononononononononononononononononononononononononononononononononono=
469 nononononononononononononononononononononononononononononononononononononon=
469 nononononononononononononononononononononononononononononononononononononon=
470 ononononononononononononononononononononononononononononononononononononono=
470 ononononononononononononononononononononononononononononononononononononono=
471 nononononononononononononononononononononononononononononononononononononon=
471 nononononononononononononononononononononononononononononononononononononon=
472 ononononononononononononononononononononononononononononononononononononono=
472 ononononononononononononononononononononononononononononononononononononono=
473 nonononononononononononono=D1=84
473 nonononononononononononono=D1=84
474
474
475 revset selection: send to address that matches branch and repo
475 revset selection: send to address that matches branch and repo
476
476
477 $ cat << EOF >> $HGRCPATH
477 $ cat << EOF >> $HGRCPATH
478 > [hooks]
478 > [hooks]
479 > incoming.notify = python:hgext.notify.hook
479 > incoming.notify = python:hgext.notify.hook
480 >
480 >
481 > [notify]
481 > [notify]
482 > sources = pull
482 > sources = pull
483 > test = True
483 > test = True
484 > diffstat = False
484 > diffstat = False
485 > maxdiff = 0
485 > maxdiff = 0
486 >
486 >
487 > [reposubs]
487 > [reposubs]
488 > */a#branch(test) = will_no_be_send@example.com
488 > */a#branch(test) = will_no_be_send@example.com
489 > */b#branch(test) = notify@example.com
489 > */b#branch(test) = notify@example.com
490 > EOF
490 > EOF
491 $ hg --cwd a branch test
491 $ hg --cwd a branch test
492 marked working directory as branch test
492 marked working directory as branch test
493 (branches are permanent and global, did you want a bookmark?)
493 (branches are permanent and global, did you want a bookmark?)
494 $ echo a >> a/a
494 $ echo a >> a/a
495 $ hg --cwd a ci -m test -d '1 0'
495 $ hg --cwd a ci -m test -d '1 0'
496 $ hg --traceback --cwd b pull ../a | $PYTHON $TESTTMP/filter.py
496 $ hg --traceback --cwd b pull ../a | $PYTHON $TESTTMP/filter.py
497 pulling from ../a
497 pulling from ../a
498 searching for changes
498 searching for changes
499 adding changesets
499 adding changesets
500 adding manifests
500 adding manifests
501 adding file changes
501 adding file changes
502 added 1 changesets with 1 changes to 1 files
502 added 1 changesets with 1 changes to 1 files
503 new changesets b7cf10b2bdec
503 new changesets b7cf10b2bdec
504 MIME-Version: 1.0
504 MIME-Version: 1.0
505 Content-Type: text/plain; charset="us-ascii"
505 Content-Type: text/plain; charset="us-ascii"
506 Content-Transfer-Encoding: 7bit
506 Content-Transfer-Encoding: 7bit
507 X-Test: foo
507 X-Test: foo
508 Date: * (glob)
508 Date: * (glob)
509 Subject: test
509 Subject: test
510 From: test@test.com
510 From: test@test.com
511 X-Hg-Notification: changeset b7cf10b2bdec
511 X-Hg-Notification: changeset b7cf10b2bdec
512 Message-Id: <hg.b7cf10b2bdec.*.*@*> (glob)
512 Message-Id: <hg.b7cf10b2bdec.*.*@*> (glob)
513 To: baz@test.com, foo@bar, notify@example.com
513 To: baz@test.com, foo@bar, notify@example.com
514
514
515 changeset b7cf10b2bdec in b
515 changeset b7cf10b2bdec in b
516 description: test
516 description: test
517 (run 'hg update' to get a working copy)
517 (run 'hg update' to get a working copy)
518
518
519 revset selection: don't send to address that waits for mails
519 revset selection: don't send to address that waits for mails
520 from different branch
520 from different branch
521
521
522 $ hg --cwd a update default
522 $ hg --cwd a update default
523 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
523 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
524 $ echo a >> a/a
524 $ echo a >> a/a
525 $ hg --cwd a ci -m test -d '1 0'
525 $ hg --cwd a ci -m test -d '1 0'
526 $ hg --traceback --cwd b pull ../a | $PYTHON $TESTTMP/filter.py
526 $ hg --traceback --cwd b pull ../a | $PYTHON $TESTTMP/filter.py
527 pulling from ../a
527 pulling from ../a
528 searching for changes
528 searching for changes
529 adding changesets
529 adding changesets
530 adding manifests
530 adding manifests
531 adding file changes
531 adding file changes
532 added 1 changesets with 0 changes to 0 files (+1 heads)
532 added 1 changesets with 0 changes to 0 files (+1 heads)
533 new changesets 5a07df312a79
533 new changesets 5a07df312a79
534 MIME-Version: 1.0
534 MIME-Version: 1.0
535 Content-Type: text/plain; charset="us-ascii"
535 Content-Type: text/plain; charset="us-ascii"
536 Content-Transfer-Encoding: 7bit
536 Content-Transfer-Encoding: 7bit
537 X-Test: foo
537 X-Test: foo
538 Date: * (glob)
538 Date: * (glob)
539 Subject: test
539 Subject: test
540 From: test@test.com
540 From: test@test.com
541 X-Hg-Notification: changeset 5a07df312a79
541 X-Hg-Notification: changeset 5a07df312a79
542 Message-Id: <hg.5a07df312a79.*.*@*> (glob)
542 Message-Id: <hg.5a07df312a79.*.*@*> (glob)
543 To: baz@test.com, foo@bar
543 To: baz@test.com, foo@bar
544
544
545 changeset 5a07df312a79 in b
545 changeset 5a07df312a79 in b
546 description: test
546 description: test
547 (run 'hg heads' to see heads)
547 (run 'hg heads' to see heads)
548
548
549 default template:
549 default template:
550
550
551 $ grep -v '^template =' $HGRCPATH > "$HGRCPATH.new"
551 $ grep -v '^template =' $HGRCPATH > "$HGRCPATH.new"
552 $ mv "$HGRCPATH.new" $HGRCPATH
552 $ mv "$HGRCPATH.new" $HGRCPATH
553 $ echo a >> a/a
553 $ echo a >> a/a
554 $ hg --cwd a commit -m 'default template'
554 $ hg --cwd a commit -m 'default template'
555 $ hg --cwd b pull ../a -q | $PYTHON $TESTTMP/filter.py
555 $ hg --cwd b pull ../a -q | $PYTHON $TESTTMP/filter.py
556 MIME-Version: 1.0
556 MIME-Version: 1.0
557 Content-Type: text/plain; charset="us-ascii"
557 Content-Type: text/plain; charset="us-ascii"
558 Content-Transfer-Encoding: 7bit
558 Content-Transfer-Encoding: 7bit
559 Date: * (glob)
559 Date: * (glob)
560 Subject: changeset in b: default template
560 Subject: changeset in b: default template
561 From: test@test.com
561 From: test@test.com
562 X-Hg-Notification: changeset f5e8ec95bf59
562 X-Hg-Notification: changeset f5e8ec95bf59
563 Message-Id: <hg.f5e8ec95bf59.*.*@*> (glob)
563 Message-Id: <hg.f5e8ec95bf59.*.*@*> (glob)
564 To: baz@test.com, foo@bar
564 To: baz@test.com, foo@bar
565
565
566 changeset f5e8ec95bf59 in $TESTTMP/b
566 changeset f5e8ec95bf59 in $TESTTMP/b
567 details: http://test/b?cmd=changeset;node=f5e8ec95bf59
567 details: http://test/b?cmd=changeset;node=f5e8ec95bf59
568 description: default template
568 description: default template
569
569
570 with style:
570 with style:
571
571
572 $ cat <<EOF > notifystyle.map
572 $ cat <<EOF > notifystyle.map
573 > changeset = "Subject: {desc|firstline|strip}
573 > changeset = "Subject: {desc|firstline|strip}
574 > From: {author}
574 > From: {author}
575 > {""}
575 > {""}
576 > changeset {node|short}"
576 > changeset {node|short}"
577 > EOF
577 > EOF
578 $ cat <<EOF >> $HGRCPATH
578 $ cat <<EOF >> $HGRCPATH
579 > [notify]
579 > [notify]
580 > style = $TESTTMP/notifystyle.map
580 > style = $TESTTMP/notifystyle.map
581 > EOF
581 > EOF
582 $ echo a >> a/a
582 $ echo a >> a/a
583 $ hg --cwd a commit -m 'with style'
583 $ hg --cwd a commit -m 'with style'
584 $ hg --cwd b pull ../a -q | $PYTHON $TESTTMP/filter.py
584 $ hg --cwd b pull ../a -q | $PYTHON $TESTTMP/filter.py
585 MIME-Version: 1.0
585 MIME-Version: 1.0
586 Content-Type: text/plain; charset="us-ascii"
586 Content-Type: text/plain; charset="us-ascii"
587 Content-Transfer-Encoding: 7bit
587 Content-Transfer-Encoding: 7bit
588 Date: * (glob)
588 Date: * (glob)
589 Subject: with style
589 Subject: with style
590 From: test@test.com
590 From: test@test.com
591 X-Hg-Notification: changeset 9e2c3a8e9c43
591 X-Hg-Notification: changeset 9e2c3a8e9c43
592 Message-Id: <hg.9e2c3a8e9c43.*.*@*> (glob)
592 Message-Id: <hg.9e2c3a8e9c43.*.*@*> (glob)
593 To: baz@test.com, foo@bar
593 To: baz@test.com, foo@bar
594
594
595 changeset 9e2c3a8e9c43
595 changeset 9e2c3a8e9c43
596
596
597 with template (overrides style):
597 with template (overrides style):
598
598
599 $ cat <<EOF >> $HGRCPATH
599 $ cat <<EOF >> $HGRCPATH
600 > template = Subject: {node|short}: {desc|firstline|strip}
600 > template = Subject: {node|short}: {desc|firstline|strip}
601 > From: {author}
601 > From: {author}
602 > {""}
602 > {""}
603 > {desc}
603 > {desc}
604 > EOF
604 > EOF
605 $ echo a >> a/a
605 $ echo a >> a/a
606 $ hg --cwd a commit -m 'with template'
606 $ hg --cwd a commit -m 'with template'
607 $ hg --cwd b pull ../a -q | $PYTHON $TESTTMP/filter.py
607 $ hg --cwd b pull ../a -q | $PYTHON $TESTTMP/filter.py
608 MIME-Version: 1.0
608 MIME-Version: 1.0
609 Content-Type: text/plain; charset="us-ascii"
609 Content-Type: text/plain; charset="us-ascii"
610 Content-Transfer-Encoding: 7bit
610 Content-Transfer-Encoding: 7bit
611 Date: * (glob)
611 Date: * (glob)
612 Subject: e2cbf5bf18a7: with template
612 Subject: e2cbf5bf18a7: with template
613 From: test@test.com
613 From: test@test.com
614 X-Hg-Notification: changeset e2cbf5bf18a7
614 X-Hg-Notification: changeset e2cbf5bf18a7
615 Message-Id: <hg.e2cbf5bf18a7.*.*@*> (glob)
615 Message-Id: <hg.e2cbf5bf18a7.*.*@*> (glob)
616 To: baz@test.com, foo@bar
616 To: baz@test.com, foo@bar
617
617
618 with template
618 with template
General Comments 0
You need to be logged in to leave comments. Login now