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