##// END OF EJS Templates
pushkey: use UTF-8
Matt Mackall -
r13050:3790452d default
parent child Browse files
Show More
@@ -1,572 +1,572 b''
1 1 # Mercurial extension to provide the 'hg bookmark' command
2 2 #
3 3 # Copyright 2008 David Soria Parra <dsp@php.net>
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 '''track a line of development with movable markers
9 9
10 10 Bookmarks are local movable markers to changesets. Every bookmark
11 11 points to a changeset identified by its hash. If you commit a
12 12 changeset that is based on a changeset that has a bookmark on it, the
13 13 bookmark shifts to the new changeset.
14 14
15 15 It is possible to use bookmark names in every revision lookup (e.g.
16 16 :hg:`merge`, :hg:`update`).
17 17
18 18 By default, when several bookmarks point to the same changeset, they
19 19 will all move forward together. It is possible to obtain a more
20 20 git-like experience by adding the following configuration option to
21 21 your configuration file::
22 22
23 23 [bookmarks]
24 24 track.current = True
25 25
26 26 This will cause Mercurial to track the bookmark that you are currently
27 27 using, and only update it. This is similar to git's approach to
28 28 branching.
29 29 '''
30 30
31 31 from mercurial.i18n import _
32 32 from mercurial.node import nullid, nullrev, bin, hex, short
33 33 from mercurial import util, commands, repair, extensions, pushkey, hg, url
34 34 from mercurial import revset, encoding
35 35 import os
36 36
37 37 def write(repo):
38 38 '''Write bookmarks
39 39
40 40 Write the given bookmark => hash dictionary to the .hg/bookmarks file
41 41 in a format equal to those of localtags.
42 42
43 43 We also store a backup of the previous state in undo.bookmarks that
44 44 can be copied back on rollback.
45 45 '''
46 46 refs = repo._bookmarks
47 47 if os.path.exists(repo.join('bookmarks')):
48 48 util.copyfile(repo.join('bookmarks'), repo.join('undo.bookmarks'))
49 49 if repo._bookmarkcurrent not in refs:
50 50 setcurrent(repo, None)
51 51 wlock = repo.wlock()
52 52 try:
53 53 file = repo.opener('bookmarks', 'w', atomictemp=True)
54 54 for refspec, node in refs.iteritems():
55 55 file.write("%s %s\n" % (hex(node), encoding.fromlocal(refspec)))
56 56 file.rename()
57 57
58 58 # touch 00changelog.i so hgweb reloads bookmarks (no lock needed)
59 59 try:
60 60 os.utime(repo.sjoin('00changelog.i'), None)
61 61 except OSError:
62 62 pass
63 63
64 64 finally:
65 65 wlock.release()
66 66
67 67 def setcurrent(repo, mark):
68 68 '''Set the name of the bookmark that we are currently on
69 69
70 70 Set the name of the bookmark that we are on (hg update <bookmark>).
71 71 The name is recorded in .hg/bookmarks.current
72 72 '''
73 73 current = repo._bookmarkcurrent
74 74 if current == mark:
75 75 return
76 76
77 77 refs = repo._bookmarks
78 78
79 79 # do not update if we do update to a rev equal to the current bookmark
80 80 if (mark and mark not in refs and
81 81 current and refs[current] == repo.changectx('.').node()):
82 82 return
83 83 if mark not in refs:
84 84 mark = ''
85 85 wlock = repo.wlock()
86 86 try:
87 87 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
88 88 file.write(mark)
89 89 file.rename()
90 90 finally:
91 91 wlock.release()
92 92 repo._bookmarkcurrent = mark
93 93
94 94 def bookmark(ui, repo, mark=None, rev=None, force=False, delete=False, rename=None):
95 95 '''track a line of development with movable markers
96 96
97 97 Bookmarks are pointers to certain commits that move when
98 98 committing. Bookmarks are local. They can be renamed, copied and
99 99 deleted. It is possible to use bookmark names in :hg:`merge` and
100 100 :hg:`update` to merge and update respectively to a given bookmark.
101 101
102 102 You can use :hg:`bookmark NAME` to set a bookmark on the working
103 103 directory's parent revision with the given name. If you specify
104 104 a revision using -r REV (where REV may be an existing bookmark),
105 105 the bookmark is assigned to that revision.
106 106
107 107 Bookmarks can be pushed and pulled between repositories (see :hg:`help
108 108 push` and :hg:`help pull`). This requires the bookmark extension to be
109 109 enabled for both the local and remote repositories.
110 110 '''
111 111 hexfn = ui.debugflag and hex or short
112 112 marks = repo._bookmarks
113 113 cur = repo.changectx('.').node()
114 114
115 115 if rename:
116 116 if rename not in marks:
117 117 raise util.Abort(_("a bookmark of this name does not exist"))
118 118 if mark in marks and not force:
119 119 raise util.Abort(_("a bookmark of the same name already exists"))
120 120 if mark is None:
121 121 raise util.Abort(_("new bookmark name required"))
122 122 marks[mark] = marks[rename]
123 123 del marks[rename]
124 124 if repo._bookmarkcurrent == rename:
125 125 setcurrent(repo, mark)
126 126 write(repo)
127 127 return
128 128
129 129 if delete:
130 130 if mark is None:
131 131 raise util.Abort(_("bookmark name required"))
132 132 if mark not in marks:
133 133 raise util.Abort(_("a bookmark of this name does not exist"))
134 134 if mark == repo._bookmarkcurrent:
135 135 setcurrent(repo, None)
136 136 del marks[mark]
137 137 write(repo)
138 138 return
139 139
140 140 if mark is not None:
141 141 if "\n" in mark:
142 142 raise util.Abort(_("bookmark name cannot contain newlines"))
143 143 mark = mark.strip()
144 144 if not mark:
145 145 raise util.Abort(_("bookmark names cannot consist entirely of "
146 146 "whitespace"))
147 147 if mark in marks and not force:
148 148 raise util.Abort(_("a bookmark of the same name already exists"))
149 149 if ((mark in repo.branchtags() or mark == repo.dirstate.branch())
150 150 and not force):
151 151 raise util.Abort(
152 152 _("a bookmark cannot have the name of an existing branch"))
153 153 if rev:
154 154 marks[mark] = repo.lookup(rev)
155 155 else:
156 156 marks[mark] = repo.changectx('.').node()
157 157 setcurrent(repo, mark)
158 158 write(repo)
159 159 return
160 160
161 161 if mark is None:
162 162 if rev:
163 163 raise util.Abort(_("bookmark name required"))
164 164 if len(marks) == 0:
165 165 ui.status(_("no bookmarks set\n"))
166 166 else:
167 167 for bmark, n in marks.iteritems():
168 168 if ui.configbool('bookmarks', 'track.current'):
169 169 current = repo._bookmarkcurrent
170 170 if bmark == current and n == cur:
171 171 prefix, label = '*', 'bookmarks.current'
172 172 else:
173 173 prefix, label = ' ', ''
174 174 else:
175 175 if n == cur:
176 176 prefix, label = '*', 'bookmarks.current'
177 177 else:
178 178 prefix, label = ' ', ''
179 179
180 180 if ui.quiet:
181 181 ui.write("%s\n" % bmark, label=label)
182 182 else:
183 183 ui.write(" %s %-25s %d:%s\n" % (
184 184 prefix, bmark, repo.changelog.rev(n), hexfn(n)),
185 185 label=label)
186 186 return
187 187
188 188 def _revstostrip(changelog, node):
189 189 srev = changelog.rev(node)
190 190 tostrip = [srev]
191 191 saveheads = []
192 192 for r in xrange(srev, len(changelog)):
193 193 parents = changelog.parentrevs(r)
194 194 if parents[0] in tostrip or parents[1] in tostrip:
195 195 tostrip.append(r)
196 196 if parents[1] != nullrev:
197 197 for p in parents:
198 198 if p not in tostrip and p > srev:
199 199 saveheads.append(p)
200 200 return [r for r in tostrip if r not in saveheads]
201 201
202 202 def strip(oldstrip, ui, repo, node, backup="all"):
203 203 """Strip bookmarks if revisions are stripped using
204 204 the mercurial.strip method. This usually happens during
205 205 qpush and qpop"""
206 206 revisions = _revstostrip(repo.changelog, node)
207 207 marks = repo._bookmarks
208 208 update = []
209 209 for mark, n in marks.iteritems():
210 210 if repo.changelog.rev(n) in revisions:
211 211 update.append(mark)
212 212 oldstrip(ui, repo, node, backup)
213 213 if len(update) > 0:
214 214 for m in update:
215 215 marks[m] = repo.changectx('.').node()
216 216 write(repo)
217 217
218 218 def reposetup(ui, repo):
219 219 if not repo.local():
220 220 return
221 221
222 222 class bookmark_repo(repo.__class__):
223 223
224 224 @util.propertycache
225 225 def _bookmarks(self):
226 226 '''Parse .hg/bookmarks file and return a dictionary
227 227
228 228 Bookmarks are stored as {HASH}\\s{NAME}\\n (localtags format) values
229 229 in the .hg/bookmarks file.
230 230 Read the file and return a (name=>nodeid) dictionary
231 231 '''
232 232 try:
233 233 bookmarks = {}
234 234 for line in self.opener('bookmarks'):
235 235 sha, refspec = line.strip().split(' ', 1)
236 236 refspec = encoding.tolocal(refspec)
237 237 bookmarks[refspec] = self.changelog.lookup(sha)
238 238 except:
239 239 pass
240 240 return bookmarks
241 241
242 242 @util.propertycache
243 243 def _bookmarkcurrent(self):
244 244 '''Get the current bookmark
245 245
246 246 If we use gittishsh branches we have a current bookmark that
247 247 we are on. This function returns the name of the bookmark. It
248 248 is stored in .hg/bookmarks.current
249 249 '''
250 250 mark = None
251 251 if os.path.exists(self.join('bookmarks.current')):
252 252 file = self.opener('bookmarks.current')
253 253 # No readline() in posixfile_nt, reading everything is cheap
254 254 mark = (file.readlines() or [''])[0]
255 255 if mark == '':
256 256 mark = None
257 257 file.close()
258 258 return mark
259 259
260 260 def rollback(self, *args):
261 261 if os.path.exists(self.join('undo.bookmarks')):
262 262 util.rename(self.join('undo.bookmarks'), self.join('bookmarks'))
263 263 return super(bookmark_repo, self).rollback(*args)
264 264
265 265 def lookup(self, key):
266 266 if key in self._bookmarks:
267 267 key = self._bookmarks[key]
268 268 return super(bookmark_repo, self).lookup(key)
269 269
270 270 def _bookmarksupdate(self, parents, node):
271 271 marks = self._bookmarks
272 272 update = False
273 273 if ui.configbool('bookmarks', 'track.current'):
274 274 mark = self._bookmarkcurrent
275 275 if mark and marks[mark] in parents:
276 276 marks[mark] = node
277 277 update = True
278 278 else:
279 279 for mark, n in marks.items():
280 280 if n in parents:
281 281 marks[mark] = node
282 282 update = True
283 283 if update:
284 284 write(self)
285 285
286 286 def commitctx(self, ctx, error=False):
287 287 """Add a revision to the repository and
288 288 move the bookmark"""
289 289 wlock = self.wlock() # do both commit and bookmark with lock held
290 290 try:
291 291 node = super(bookmark_repo, self).commitctx(ctx, error)
292 292 if node is None:
293 293 return None
294 294 parents = self.changelog.parents(node)
295 295 if parents[1] == nullid:
296 296 parents = (parents[0],)
297 297
298 298 self._bookmarksupdate(parents, node)
299 299 return node
300 300 finally:
301 301 wlock.release()
302 302
303 303 def pull(self, remote, heads=None, force=False):
304 304 result = super(bookmark_repo, self).pull(remote, heads, force)
305 305
306 306 self.ui.debug("checking for updated bookmarks\n")
307 307 rb = remote.listkeys('bookmarks')
308 308 changed = False
309 309 for k in rb.keys():
310 310 if k in self._bookmarks:
311 311 nr, nl = rb[k], self._bookmarks[k]
312 312 if nr in self:
313 313 cr = self[nr]
314 314 cl = self[nl]
315 315 if cl.rev() >= cr.rev():
316 316 continue
317 317 if cr in cl.descendants():
318 318 self._bookmarks[k] = cr.node()
319 319 changed = True
320 320 self.ui.status(_("updating bookmark %s\n") % k)
321 321 else:
322 322 self.ui.warn(_("not updating divergent"
323 323 " bookmark %s\n") % k)
324 324 if changed:
325 325 write(repo)
326 326
327 327 return result
328 328
329 329 def push(self, remote, force=False, revs=None, newbranch=False):
330 330 result = super(bookmark_repo, self).push(remote, force, revs,
331 331 newbranch)
332 332
333 333 self.ui.debug("checking for updated bookmarks\n")
334 334 rb = remote.listkeys('bookmarks')
335 335 for k in rb.keys():
336 336 if k in self._bookmarks:
337 nr, nl = rb[k], self._bookmarks[k]
337 nr, nl = rb[k], hex(self._bookmarks[k])
338 338 if nr in self:
339 339 cr = self[nr]
340 340 cl = self[nl]
341 341 if cl in cr.descendants():
342 342 r = remote.pushkey('bookmarks', k, nr, nl)
343 343 if r:
344 344 self.ui.status(_("updating bookmark %s\n") % k)
345 345 else:
346 346 self.ui.warn(_('updating bookmark %s'
347 347 ' failed!\n') % k)
348 348
349 349 return result
350 350
351 351 def addchangegroup(self, *args, **kwargs):
352 352 result = super(bookmark_repo, self).addchangegroup(*args, **kwargs)
353 353 if result > 1:
354 354 # We have more heads than before
355 355 return result
356 356 node = self.changelog.tip()
357 357 parents = self.dirstate.parents()
358 358 self._bookmarksupdate(parents, node)
359 359 return result
360 360
361 361 def _findtags(self):
362 362 """Merge bookmarks with normal tags"""
363 363 (tags, tagtypes) = super(bookmark_repo, self)._findtags()
364 364 tags.update(self._bookmarks)
365 365 return (tags, tagtypes)
366 366
367 367 if hasattr(repo, 'invalidate'):
368 368 def invalidate(self):
369 369 super(bookmark_repo, self).invalidate()
370 370 for attr in ('_bookmarks', '_bookmarkcurrent'):
371 371 if attr in self.__dict__:
372 372 delattr(self, attr)
373 373
374 374 repo.__class__ = bookmark_repo
375 375
376 376 def listbookmarks(repo):
377 377 # We may try to list bookmarks on a repo type that does not
378 378 # support it (e.g., statichttprepository).
379 379 if not hasattr(repo, '_bookmarks'):
380 380 return {}
381 381
382 382 d = {}
383 383 for k, v in repo._bookmarks.iteritems():
384 384 d[k] = hex(v)
385 385 return d
386 386
387 387 def pushbookmark(repo, key, old, new):
388 388 w = repo.wlock()
389 389 try:
390 390 marks = repo._bookmarks
391 391 if hex(marks.get(key, '')) != old:
392 392 return False
393 393 if new == '':
394 394 del marks[key]
395 395 else:
396 396 if new not in repo:
397 397 return False
398 398 marks[key] = repo[new].node()
399 399 write(repo)
400 400 return True
401 401 finally:
402 402 w.release()
403 403
404 404 def pull(oldpull, ui, repo, source="default", **opts):
405 405 # translate bookmark args to rev args for actual pull
406 406 if opts.get('bookmark'):
407 407 # this is an unpleasant hack as pull will do this internally
408 408 source, branches = hg.parseurl(ui.expandpath(source),
409 409 opts.get('branch'))
410 410 other = hg.repository(hg.remoteui(repo, opts), source)
411 411 rb = other.listkeys('bookmarks')
412 412
413 413 for b in opts['bookmark']:
414 414 if b not in rb:
415 415 raise util.Abort(_('remote bookmark %s not found!') % b)
416 416 opts.setdefault('rev', []).append(b)
417 417
418 418 result = oldpull(ui, repo, source, **opts)
419 419
420 420 # update specified bookmarks
421 421 if opts.get('bookmark'):
422 422 for b in opts['bookmark']:
423 423 # explicit pull overrides local bookmark if any
424 424 ui.status(_("importing bookmark %s\n") % b)
425 425 repo._bookmarks[b] = repo[rb[b]].node()
426 426 write(repo)
427 427
428 428 return result
429 429
430 430 def push(oldpush, ui, repo, dest=None, **opts):
431 431 dopush = True
432 432 if opts.get('bookmark'):
433 433 dopush = False
434 434 for b in opts['bookmark']:
435 435 if b in repo._bookmarks:
436 436 dopush = True
437 437 opts.setdefault('rev', []).append(b)
438 438
439 439 result = 0
440 440 if dopush:
441 441 result = oldpush(ui, repo, dest, **opts)
442 442
443 443 if opts.get('bookmark'):
444 444 # this is an unpleasant hack as push will do this internally
445 445 dest = ui.expandpath(dest or 'default-push', dest or 'default')
446 446 dest, branches = hg.parseurl(dest, opts.get('branch'))
447 447 other = hg.repository(hg.remoteui(repo, opts), dest)
448 448 rb = other.listkeys('bookmarks')
449 449 for b in opts['bookmark']:
450 450 # explicit push overrides remote bookmark if any
451 451 if b in repo._bookmarks:
452 452 ui.status(_("exporting bookmark %s\n") % b)
453 453 new = repo[b].hex()
454 454 elif b in rb:
455 455 ui.status(_("deleting remote bookmark %s\n") % b)
456 456 new = '' # delete
457 457 else:
458 458 ui.warn(_('bookmark %s does not exist on the local '
459 459 'or remote repository!\n') % b)
460 460 return 2
461 461 old = rb.get(b, '')
462 462 r = other.pushkey('bookmarks', b, old, new)
463 463 if not r:
464 464 ui.warn(_('updating bookmark %s failed!\n') % b)
465 465 if not result:
466 466 result = 2
467 467
468 468 return result
469 469
470 470 def diffbookmarks(ui, repo, remote):
471 471 ui.status(_("searching for changed bookmarks\n"))
472 472
473 473 lmarks = repo.listkeys('bookmarks')
474 474 rmarks = remote.listkeys('bookmarks')
475 475
476 476 diff = sorted(set(rmarks) - set(lmarks))
477 477 for k in diff:
478 478 ui.write(" %-25s %s\n" % (k, rmarks[k][:12]))
479 479
480 480 if len(diff) <= 0:
481 481 ui.status(_("no changed bookmarks found\n"))
482 482 return 1
483 483 return 0
484 484
485 485 def incoming(oldincoming, ui, repo, source="default", **opts):
486 486 if opts.get('bookmarks'):
487 487 source, branches = hg.parseurl(ui.expandpath(source), opts.get('branch'))
488 488 other = hg.repository(hg.remoteui(repo, opts), source)
489 489 ui.status(_('comparing with %s\n') % url.hidepassword(source))
490 490 return diffbookmarks(ui, repo, other)
491 491 else:
492 492 return oldincoming(ui, repo, source, **opts)
493 493
494 494 def outgoing(oldoutgoing, ui, repo, dest=None, **opts):
495 495 if opts.get('bookmarks'):
496 496 dest = ui.expandpath(dest or 'default-push', dest or 'default')
497 497 dest, branches = hg.parseurl(dest, opts.get('branch'))
498 498 other = hg.repository(hg.remoteui(repo, opts), dest)
499 499 ui.status(_('comparing with %s\n') % url.hidepassword(dest))
500 500 return diffbookmarks(ui, other, repo)
501 501 else:
502 502 return oldoutgoing(ui, repo, dest, **opts)
503 503
504 504 def uisetup(ui):
505 505 extensions.wrapfunction(repair, "strip", strip)
506 506 if ui.configbool('bookmarks', 'track.current'):
507 507 extensions.wrapcommand(commands.table, 'update', updatecurbookmark)
508 508
509 509 entry = extensions.wrapcommand(commands.table, 'pull', pull)
510 510 entry[1].append(('B', 'bookmark', [],
511 511 _("bookmark to import"),
512 512 _('BOOKMARK')))
513 513 entry = extensions.wrapcommand(commands.table, 'push', push)
514 514 entry[1].append(('B', 'bookmark', [],
515 515 _("bookmark to export"),
516 516 _('BOOKMARK')))
517 517 entry = extensions.wrapcommand(commands.table, 'incoming', incoming)
518 518 entry[1].append(('B', 'bookmarks', False,
519 519 _("compare bookmark")))
520 520 entry = extensions.wrapcommand(commands.table, 'outgoing', outgoing)
521 521 entry[1].append(('B', 'bookmarks', False,
522 522 _("compare bookmark")))
523 523
524 524 pushkey.register('bookmarks', pushbookmark, listbookmarks)
525 525
526 526 def updatecurbookmark(orig, ui, repo, *args, **opts):
527 527 '''Set the current bookmark
528 528
529 529 If the user updates to a bookmark we update the .hg/bookmarks.current
530 530 file.
531 531 '''
532 532 res = orig(ui, repo, *args, **opts)
533 533 rev = opts['rev']
534 534 if not rev and len(args) > 0:
535 535 rev = args[0]
536 536 setcurrent(repo, rev)
537 537 return res
538 538
539 539 def bmrevset(repo, subset, x):
540 540 """``bookmark([name])``
541 541 The named bookmark or all bookmarks.
542 542 """
543 543 # i18n: "bookmark" is a keyword
544 544 args = revset.getargs(x, 0, 1, _('bookmark takes one or no arguments'))
545 545 if args:
546 546 bm = revset.getstring(args[0],
547 547 # i18n: "bookmark" is a keyword
548 548 _('the argument to bookmark must be a string'))
549 549 bmrev = listbookmarks(repo).get(bm, None)
550 550 if bmrev:
551 551 bmrev = repo.changelog.rev(bin(bmrev))
552 552 return [r for r in subset if r == bmrev]
553 553 bms = set([repo.changelog.rev(bin(r)) for r in listbookmarks(repo).values()])
554 554 return [r for r in subset if r in bms]
555 555
556 556 def extsetup(ui):
557 557 revset.symbols['bookmark'] = bmrevset
558 558
559 559 cmdtable = {
560 560 "bookmarks":
561 561 (bookmark,
562 562 [('f', 'force', False, _('force')),
563 563 ('r', 'rev', '', _('revision'), _('REV')),
564 564 ('d', 'delete', False, _('delete a given bookmark')),
565 565 ('m', 'rename', '', _('rename a given bookmark'), _('NAME'))],
566 566 _('hg bookmarks [-f] [-d] [-m NAME] [-r REV] [NAME]')),
567 567 }
568 568
569 569 colortable = {'bookmarks.current': 'green'}
570 570
571 571 # tell hggettext to extract docstrings from these functions:
572 572 i18nfunctions = [bmrevset]
@@ -1,331 +1,348 b''
1 1 # wireproto.py - generic wire protocol support functions
2 2 #
3 3 # Copyright 2005-2010 Matt Mackall <mpm@selenic.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 import urllib, tempfile, os, sys
9 9 from i18n import _
10 10 from node import bin, hex
11 11 import changegroup as changegroupmod
12 12 import repo, error, encoding, util, store
13 13 import pushkey as pushkeymod
14 14
15 15 # list of nodes encoding / decoding
16 16
17 17 def decodelist(l, sep=' '):
18 18 return map(bin, l.split(sep))
19 19
20 20 def encodelist(l, sep=' '):
21 21 return sep.join(map(hex, l))
22 22
23 23 # client side
24 24
25 25 class wirerepository(repo.repository):
26 26 def lookup(self, key):
27 27 self.requirecap('lookup', _('look up remote revision'))
28 28 d = self._call("lookup", key=encoding.fromlocal(key))
29 29 success, data = d[:-1].split(" ", 1)
30 30 if int(success):
31 31 return bin(data)
32 32 self._abort(error.RepoError(data))
33 33
34 34 def heads(self):
35 35 d = self._call("heads")
36 36 try:
37 37 return decodelist(d[:-1])
38 38 except:
39 39 self._abort(error.ResponseError(_("unexpected response:"), d))
40 40
41 41 def branchmap(self):
42 42 d = self._call("branchmap")
43 43 try:
44 44 branchmap = {}
45 45 for branchpart in d.splitlines():
46 46 branchname, branchheads = branchpart.split(' ', 1)
47 47 branchname = encoding.tolocal(urllib.unquote(branchname))
48 48 branchheads = decodelist(branchheads)
49 49 branchmap[branchname] = branchheads
50 50 return branchmap
51 51 except TypeError:
52 52 self._abort(error.ResponseError(_("unexpected response:"), d))
53 53
54 54 def branches(self, nodes):
55 55 n = encodelist(nodes)
56 56 d = self._call("branches", nodes=n)
57 57 try:
58 58 br = [tuple(decodelist(b)) for b in d.splitlines()]
59 59 return br
60 60 except:
61 61 self._abort(error.ResponseError(_("unexpected response:"), d))
62 62
63 63 def between(self, pairs):
64 64 batch = 8 # avoid giant requests
65 65 r = []
66 66 for i in xrange(0, len(pairs), batch):
67 67 n = " ".join([encodelist(p, '-') for p in pairs[i:i + batch]])
68 68 d = self._call("between", pairs=n)
69 69 try:
70 70 r.extend(l and decodelist(l) or [] for l in d.splitlines())
71 71 except:
72 72 self._abort(error.ResponseError(_("unexpected response:"), d))
73 73 return r
74 74
75 75 def pushkey(self, namespace, key, old, new):
76 76 if not self.capable('pushkey'):
77 77 return False
78 78 d = self._call("pushkey",
79 namespace=namespace, key=key, old=old, new=new)
79 namespace=encoding.fromlocal(namespace),
80 key=encoding.fromlocal(key),
81 old=encoding.fromlocal(old),
82 new=encoding.fromlocal(new))
80 83 return bool(int(d))
81 84
82 85 def listkeys(self, namespace):
83 86 if not self.capable('pushkey'):
84 87 return {}
85 d = self._call("listkeys", namespace=namespace)
88 d = self._call("listkeys", namespace=encoding.fromlocal(namespace))
86 89 r = {}
87 90 for l in d.splitlines():
88 91 k, v = l.split('\t')
89 r[k.decode('string-escape')] = v.decode('string-escape')
92 r[encoding.tolocal(k)] = encoding.tolocal(v)
90 93 return r
91 94
92 95 def stream_out(self):
93 96 return self._callstream('stream_out')
94 97
95 98 def changegroup(self, nodes, kind):
96 99 n = encodelist(nodes)
97 100 f = self._callstream("changegroup", roots=n)
98 101 return changegroupmod.unbundle10(self._decompress(f), 'UN')
99 102
100 103 def changegroupsubset(self, bases, heads, kind):
101 104 self.requirecap('changegroupsubset', _('look up remote changes'))
102 105 bases = encodelist(bases)
103 106 heads = encodelist(heads)
104 107 f = self._callstream("changegroupsubset",
105 108 bases=bases, heads=heads)
106 109 return changegroupmod.unbundle10(self._decompress(f), 'UN')
107 110
108 111 def unbundle(self, cg, heads, source):
109 112 '''Send cg (a readable file-like object representing the
110 113 changegroup to push, typically a chunkbuffer object) to the
111 114 remote server as a bundle. Return an integer indicating the
112 115 result of the push (see localrepository.addchangegroup()).'''
113 116
114 117 ret, output = self._callpush("unbundle", cg, heads=encodelist(heads))
115 118 if ret == "":
116 119 raise error.ResponseError(
117 120 _('push failed:'), output)
118 121 try:
119 122 ret = int(ret)
120 123 except ValueError:
121 124 raise error.ResponseError(
122 125 _('push failed (unexpected response):'), ret)
123 126
124 127 for l in output.splitlines(True):
125 128 self.ui.status(_('remote: '), l)
126 129 return ret
127 130
128 131 # server side
129 132
130 133 class streamres(object):
131 134 def __init__(self, gen):
132 135 self.gen = gen
133 136
134 137 class pushres(object):
135 138 def __init__(self, res):
136 139 self.res = res
137 140
138 141 class pusherr(object):
139 142 def __init__(self, res):
140 143 self.res = res
141 144
142 145 def dispatch(repo, proto, command):
143 146 func, spec = commands[command]
144 147 args = proto.getargs(spec)
145 148 return func(repo, proto, *args)
146 149
147 150 def between(repo, proto, pairs):
148 151 pairs = [decodelist(p, '-') for p in pairs.split(" ")]
149 152 r = []
150 153 for b in repo.between(pairs):
151 154 r.append(encodelist(b) + "\n")
152 155 return "".join(r)
153 156
154 157 def branchmap(repo, proto):
155 158 branchmap = repo.branchmap()
156 159 heads = []
157 160 for branch, nodes in branchmap.iteritems():
158 161 branchname = urllib.quote(encoding.fromlocal(branch))
159 162 branchnodes = encodelist(nodes)
160 163 heads.append('%s %s' % (branchname, branchnodes))
161 164 return '\n'.join(heads)
162 165
163 166 def branches(repo, proto, nodes):
164 167 nodes = decodelist(nodes)
165 168 r = []
166 169 for b in repo.branches(nodes):
167 170 r.append(encodelist(b) + "\n")
168 171 return "".join(r)
169 172
170 173 def capabilities(repo, proto):
171 174 caps = 'lookup changegroupsubset branchmap pushkey'.split()
172 175 if _allowstream(repo.ui):
173 176 requiredformats = repo.requirements & repo.supportedformats
174 177 # if our local revlogs are just revlogv1, add 'stream' cap
175 178 if not requiredformats - set(('revlogv1',)):
176 179 caps.append('stream')
177 180 # otherwise, add 'streamreqs' detailing our local revlog format
178 181 else:
179 182 caps.append('streamreqs=%s' % ','.join(requiredformats))
180 183 caps.append('unbundle=%s' % ','.join(changegroupmod.bundlepriority))
181 184 return ' '.join(caps)
182 185
183 186 def changegroup(repo, proto, roots):
184 187 nodes = decodelist(roots)
185 188 cg = repo.changegroup(nodes, 'serve')
186 189 return streamres(proto.groupchunks(cg))
187 190
188 191 def changegroupsubset(repo, proto, bases, heads):
189 192 bases = decodelist(bases)
190 193 heads = decodelist(heads)
191 194 cg = repo.changegroupsubset(bases, heads, 'serve')
192 195 return streamres(proto.groupchunks(cg))
193 196
194 197 def heads(repo, proto):
195 198 h = repo.heads()
196 199 return encodelist(h) + "\n"
197 200
198 201 def hello(repo, proto):
199 202 '''the hello command returns a set of lines describing various
200 203 interesting things about the server, in an RFC822-like format.
201 204 Currently the only one defined is "capabilities", which
202 205 consists of a line in the form:
203 206
204 207 capabilities: space separated list of tokens
205 208 '''
206 209 return "capabilities: %s\n" % (capabilities(repo, proto))
207 210
208 211 def listkeys(repo, proto, namespace):
209 d = pushkeymod.list(repo, namespace).items()
210 t = '\n'.join(['%s\t%s' % (k.encode('string-escape'),
211 v.encode('string-escape')) for k, v in d])
212 d = pushkeymod.list(repo, encoding.tolocal(namespace)).items()
213 t = '\n'.join(['%s\t%s' % (encoding.fromlocal(k), encoding.fromlocal(v))
214 for k, v in d])
212 215 return t
213 216
214 217 def lookup(repo, proto, key):
215 218 try:
216 219 r = hex(repo.lookup(encoding.tolocal(key)))
217 220 success = 1
218 221 except Exception, inst:
219 222 r = str(inst)
220 223 success = 0
221 224 return "%s %s\n" % (success, r)
222 225
223 226 def pushkey(repo, proto, namespace, key, old, new):
224 r = pushkeymod.push(repo, namespace, key, old, new)
227 # compatibility with pre-1.8 clients which were accidentally
228 # sending raw binary nodes rather than utf-8-encoded hex
229 if len(new) == 20 and new.encode('string-escape') != new:
230 # looks like it could be a binary node
231 try:
232 u = new.decode('utf-8')
233 new = encoding.tolocal(new) # but cleanly decodes as UTF-8
234 except UnicodeDecodeError:
235 pass # binary, leave unmodified
236 else:
237 new = encoding.tolocal(new) # normal path
238
239 r = pushkeymod.push(repo,
240 encoding.tolocal(namespace), encoding.tolocal(key),
241 encoding.tolocal(old), new)
225 242 return '%s\n' % int(r)
226 243
227 244 def _allowstream(ui):
228 245 return ui.configbool('server', 'uncompressed', True, untrusted=True)
229 246
230 247 def stream(repo, proto):
231 248 '''If the server supports streaming clone, it advertises the "stream"
232 249 capability with a value representing the version and flags of the repo
233 250 it is serving. Client checks to see if it understands the format.
234 251
235 252 The format is simple: the server writes out a line with the amount
236 253 of files, then the total amount of bytes to be transfered (separated
237 254 by a space). Then, for each file, the server first writes the filename
238 255 and filesize (separated by the null character), then the file contents.
239 256 '''
240 257
241 258 if not _allowstream(repo.ui):
242 259 return '1\n'
243 260
244 261 entries = []
245 262 total_bytes = 0
246 263 try:
247 264 # get consistent snapshot of repo, lock during scan
248 265 lock = repo.lock()
249 266 try:
250 267 repo.ui.debug('scanning\n')
251 268 for name, ename, size in repo.store.walk():
252 269 entries.append((name, size))
253 270 total_bytes += size
254 271 finally:
255 272 lock.release()
256 273 except error.LockError:
257 274 return '2\n' # error: 2
258 275
259 276 def streamer(repo, entries, total):
260 277 '''stream out all metadata files in repository.'''
261 278 yield '0\n' # success
262 279 repo.ui.debug('%d files, %d bytes to transfer\n' %
263 280 (len(entries), total_bytes))
264 281 yield '%d %d\n' % (len(entries), total_bytes)
265 282 for name, size in entries:
266 283 repo.ui.debug('sending %s (%d bytes)\n' % (name, size))
267 284 # partially encode name over the wire for backwards compat
268 285 yield '%s\0%d\n' % (store.encodedir(name), size)
269 286 for chunk in util.filechunkiter(repo.sopener(name), limit=size):
270 287 yield chunk
271 288
272 289 return streamres(streamer(repo, entries, total_bytes))
273 290
274 291 def unbundle(repo, proto, heads):
275 292 their_heads = decodelist(heads)
276 293
277 294 def check_heads():
278 295 heads = repo.heads()
279 296 return their_heads == ['force'] or their_heads == heads
280 297
281 298 proto.redirect()
282 299
283 300 # fail early if possible
284 301 if not check_heads():
285 302 return pusherr('unsynced changes')
286 303
287 304 # write bundle data to temporary file because it can be big
288 305 fd, tempname = tempfile.mkstemp(prefix='hg-unbundle-')
289 306 fp = os.fdopen(fd, 'wb+')
290 307 r = 0
291 308 try:
292 309 proto.getfile(fp)
293 310 lock = repo.lock()
294 311 try:
295 312 if not check_heads():
296 313 # someone else committed/pushed/unbundled while we
297 314 # were transferring data
298 315 return pusherr('unsynced changes')
299 316
300 317 # push can proceed
301 318 fp.seek(0)
302 319 gen = changegroupmod.readbundle(fp, None)
303 320
304 321 try:
305 322 r = repo.addchangegroup(gen, 'serve', proto._client(),
306 323 lock=lock)
307 324 except util.Abort, inst:
308 325 sys.stderr.write("abort: %s\n" % inst)
309 326 finally:
310 327 lock.release()
311 328 return pushres(r)
312 329
313 330 finally:
314 331 fp.close()
315 332 os.unlink(tempname)
316 333
317 334 commands = {
318 335 'between': (between, 'pairs'),
319 336 'branchmap': (branchmap, ''),
320 337 'branches': (branches, 'nodes'),
321 338 'capabilities': (capabilities, ''),
322 339 'changegroup': (changegroup, 'roots'),
323 340 'changegroupsubset': (changegroupsubset, 'bases heads'),
324 341 'heads': (heads, ''),
325 342 'hello': (hello, ''),
326 343 'listkeys': (listkeys, 'namespace'),
327 344 'lookup': (lookup, 'key'),
328 345 'pushkey': (pushkey, 'namespace key old new'),
329 346 'stream_out': (stream, ''),
330 347 'unbundle': (unbundle, 'heads'),
331 348 }
@@ -1,291 +1,291 b''
1 1
2 2 $ cp "$TESTDIR"/printenv.py .
3 3
4 4 This test tries to exercise the ssh functionality with a dummy script
5 5
6 6 $ cat <<EOF > dummyssh
7 7 > import sys
8 8 > import os
9 9 >
10 10 > os.chdir(os.path.dirname(sys.argv[0]))
11 11 > if sys.argv[1] != "user@dummy":
12 12 > sys.exit(-1)
13 13 >
14 14 > if not os.path.exists("dummyssh"):
15 15 > sys.exit(-1)
16 16 >
17 17 > os.environ["SSH_CLIENT"] = "127.0.0.1 1 2"
18 18 >
19 19 > log = open("dummylog", "ab")
20 20 > log.write("Got arguments")
21 21 > for i, arg in enumerate(sys.argv[1:]):
22 22 > log.write(" %d:%s" % (i+1, arg))
23 23 > log.write("\n")
24 24 > log.close()
25 25 > r = os.system(sys.argv[2])
26 26 > sys.exit(bool(r))
27 27 > EOF
28 28 $ cat <<EOF > badhook
29 29 > import sys
30 30 > sys.stdout.write("KABOOM\n")
31 31 > EOF
32 32
33 33 creating 'remote
34 34
35 35 $ hg init remote
36 36 $ cd remote
37 37 $ echo this > foo
38 38 $ echo this > fooO
39 39 $ hg ci -A -m "init" foo fooO
40 40 $ echo <<EOF > .hg/hgrc
41 41 > [server]
42 42 > uncompressed = True
43 43 >
44 44 > [extensions]
45 45 > bookmarks =
46 46 >
47 47 > [hooks]
48 48 > changegroup = python ../printenv.py changegroup-in-remote 0 ../dummylog
49 49 > EOF
50 50 $ cd ..
51 51
52 52 repo not found error
53 53
54 54 $ hg clone -e "python ./dummyssh" ssh://user@dummy/nonexistent local
55 55 remote: abort: There is no Mercurial repository here (.hg not found)!
56 56 abort: no suitable response from remote hg!
57 57 [255]
58 58
59 59 non-existent absolute path
60 60
61 61 $ hg clone -e "python ./dummyssh" ssh://user@dummy//`pwd`/nonexistent local
62 62 remote: abort: There is no Mercurial repository here (.hg not found)!
63 63 abort: no suitable response from remote hg!
64 64 [255]
65 65
66 66 clone remote via stream
67 67
68 68 $ hg clone -e "python ./dummyssh" --uncompressed ssh://user@dummy/remote local-stream
69 69 streaming all changes
70 70 4 files to transfer, 392 bytes of data
71 71 transferred 392 bytes in * seconds (*/sec) (glob)
72 72 updating to branch default
73 73 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
74 74 $ cd local-stream
75 75 $ hg verify
76 76 checking changesets
77 77 checking manifests
78 78 crosschecking files in changesets and manifests
79 79 checking files
80 80 2 files, 1 changesets, 2 total revisions
81 81 $ cd ..
82 82
83 83 clone remote via pull
84 84
85 85 $ hg clone -e "python ./dummyssh" ssh://user@dummy/remote local
86 86 requesting all changes
87 87 adding changesets
88 88 adding manifests
89 89 adding file changes
90 90 added 1 changesets with 2 changes to 2 files
91 91 updating to branch default
92 92 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
93 93
94 94 verify
95 95
96 96 $ cd local
97 97 $ hg verify
98 98 checking changesets
99 99 checking manifests
100 100 crosschecking files in changesets and manifests
101 101 checking files
102 102 2 files, 1 changesets, 2 total revisions
103 103 $ echo '[hooks]' >> .hg/hgrc
104 104 $ echo 'changegroup = python ../printenv.py changegroup-in-local 0 ../dummylog' >> .hg/hgrc
105 105
106 106 empty default pull
107 107
108 108 $ hg paths
109 109 default = ssh://user@dummy/remote
110 110 $ hg pull -e "python ../dummyssh"
111 111 pulling from ssh://user@dummy/remote
112 112 searching for changes
113 113 no changes found
114 114
115 115 local change
116 116
117 117 $ echo bleah > foo
118 118 $ hg ci -m "add"
119 119
120 120 updating rc
121 121
122 122 $ echo "default-push = ssh://user@dummy/remote" >> .hg/hgrc
123 123 $ echo "[ui]" >> .hg/hgrc
124 124 $ echo "ssh = python ../dummyssh" >> .hg/hgrc
125 125 $ echo '[extensions]' >> .hg/hgrc
126 126 $ echo 'bookmarks =' >> .hg/hgrc
127 127
128 128 find outgoing
129 129
130 130 $ hg out ssh://user@dummy/remote
131 131 comparing with ssh://user@dummy/remote
132 132 searching for changes
133 133 changeset: 1:a28a9d1a809c
134 134 tag: tip
135 135 user: test
136 136 date: Thu Jan 01 00:00:00 1970 +0000
137 137 summary: add
138 138
139 139
140 140 find incoming on the remote side
141 141
142 142 $ hg incoming -R ../remote -e "python ../dummyssh" ssh://user@dummy/local
143 143 comparing with ssh://user@dummy/local
144 144 searching for changes
145 145 changeset: 1:a28a9d1a809c
146 146 tag: tip
147 147 user: test
148 148 date: Thu Jan 01 00:00:00 1970 +0000
149 149 summary: add
150 150
151 151
152 152 find incoming on the remote side (using absolute path)
153 153
154 154 $ hg incoming -R ../remote -e "python ../dummyssh" "ssh://user@dummy/`pwd`"
155 155 comparing with ssh://user@dummy/$TESTTMP/local
156 156 searching for changes
157 157 changeset: 1:a28a9d1a809c
158 158 tag: tip
159 159 user: test
160 160 date: Thu Jan 01 00:00:00 1970 +0000
161 161 summary: add
162 162
163 163
164 164 push
165 165
166 166 $ hg push
167 167 pushing to ssh://user@dummy/remote
168 168 searching for changes
169 169 remote: adding changesets
170 170 remote: adding manifests
171 171 remote: adding file changes
172 172 remote: added 1 changesets with 1 changes to 1 files
173 173 $ cd ../remote
174 174
175 175 check remote tip
176 176
177 177 $ hg tip
178 178 changeset: 1:a28a9d1a809c
179 179 tag: tip
180 180 user: test
181 181 date: Thu Jan 01 00:00:00 1970 +0000
182 182 summary: add
183 183
184 184 $ hg verify
185 185 checking changesets
186 186 checking manifests
187 187 crosschecking files in changesets and manifests
188 188 checking files
189 189 2 files, 2 changesets, 3 total revisions
190 190 $ hg cat -r tip foo
191 191 bleah
192 192 $ echo z > z
193 193 $ hg ci -A -m z z
194 194 created new head
195 195
196 196 test pushkeys and bookmarks
197 197
198 198 $ cd ../local
199 199 $ echo '[extensions]' >> ../remote/.hg/hgrc
200 200 $ echo 'bookmarks =' >> ../remote/.hg/hgrc
201 201 $ hg debugpushkey --config ui.ssh="python ../dummyssh" ssh://user@dummy/remote namespaces
202 202 bookmarks
203 203 namespaces
204 204 $ hg book foo -r 0
205 205 $ hg out -B
206 206 comparing with ssh://user@dummy/remote
207 207 searching for changed bookmarks
208 208 foo 1160648e36ce
209 209 $ hg push -B foo
210 210 pushing to ssh://user@dummy/remote
211 211 searching for changes
212 212 no changes found
213 213 exporting bookmark foo
214 214 $ hg debugpushkey --config ui.ssh="python ../dummyssh" ssh://user@dummy/remote bookmarks
215 215 foo 1160648e36cec0054048a7edc4110c6f84fde594
216 216 $ hg book -f foo
217 $ hg push
217 $ hg push --traceback
218 218 pushing to ssh://user@dummy/remote
219 219 searching for changes
220 220 no changes found
221 221 updating bookmark foo
222 222 $ hg book -d foo
223 223 $ hg in -B
224 224 comparing with ssh://user@dummy/remote
225 225 searching for changed bookmarks
226 226 foo a28a9d1a809c
227 227 $ hg book -f -r 0 foo
228 228 $ hg pull -B foo
229 229 pulling from ssh://user@dummy/remote
230 230 searching for changes
231 231 no changes found
232 232 updating bookmark foo
233 233 importing bookmark foo
234 234 $ hg book -d foo
235 235 $ hg push -B foo
236 236 deleting remote bookmark foo
237 237
238 238 a bad, evil hook that prints to stdout
239 239
240 240 $ echo '[hooks]' >> ../remote/.hg/hgrc
241 241 $ echo 'changegroup.stdout = python ../badhook' >> ../remote/.hg/hgrc
242 242 $ echo r > r
243 243 $ hg ci -A -m z r
244 244
245 245 push should succeed even though it has an unexpected response
246 246
247 247 $ hg push
248 248 pushing to ssh://user@dummy/remote
249 249 searching for changes
250 250 note: unsynced remote changes!
251 251 remote: adding changesets
252 252 remote: adding manifests
253 253 remote: adding file changes
254 254 remote: added 1 changesets with 1 changes to 1 files
255 255 remote: KABOOM
256 256 $ hg -R ../remote heads
257 257 changeset: 3:1383141674ec
258 258 tag: tip
259 259 parent: 1:a28a9d1a809c
260 260 user: test
261 261 date: Thu Jan 01 00:00:00 1970 +0000
262 262 summary: z
263 263
264 264 changeset: 2:6c0482d977a3
265 265 parent: 0:1160648e36ce
266 266 user: test
267 267 date: Thu Jan 01 00:00:00 1970 +0000
268 268 summary: z
269 269
270 270 $ cd ..
271 271 $ cat dummylog
272 272 Got arguments 1:user@dummy 2:hg -R nonexistent serve --stdio
273 273 Got arguments 1:user@dummy 2:hg -R /$TESTTMP/nonexistent serve --stdio
274 274 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
275 275 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
276 276 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
277 277 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
278 278 Got arguments 1:user@dummy 2:hg -R local serve --stdio
279 279 Got arguments 1:user@dummy 2:hg -R $TESTTMP/local serve --stdio
280 280 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
281 281 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
282 282 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
283 283 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
284 284 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
285 285 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
286 286 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
287 287 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
288 288 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
289 289 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
290 290 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
291 291 Got arguments 1:user@dummy 2:hg -R remote serve --stdio
General Comments 0
You need to be logged in to leave comments. Login now