##// END OF EJS Templates
push: move bookmarks exchange in the exchange module...
Pierre-Yves David -
r20352:58300f61 default
parent child Browse files
Show More
@@ -1,442 +1,426 b''
1 # Mercurial bookmark support code
1 # Mercurial bookmark support code
2 #
2 #
3 # Copyright 2008 David Soria Parra <dsp@php.net>
3 # Copyright 2008 David Soria Parra <dsp@php.net>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from mercurial.i18n import _
8 from mercurial.i18n import _
9 from mercurial.node import hex, bin
9 from mercurial.node import hex, bin
10 from mercurial import encoding, error, util, obsolete
10 from mercurial import encoding, error, util, obsolete
11 import errno
11 import errno
12
12
13 class bmstore(dict):
13 class bmstore(dict):
14 """Storage for bookmarks.
14 """Storage for bookmarks.
15
15
16 This object should do all bookmark reads and writes, so that it's
16 This object should do all bookmark reads and writes, so that it's
17 fairly simple to replace the storage underlying bookmarks without
17 fairly simple to replace the storage underlying bookmarks without
18 having to clone the logic surrounding bookmarks.
18 having to clone the logic surrounding bookmarks.
19
19
20 This particular bmstore implementation stores bookmarks as
20 This particular bmstore implementation stores bookmarks as
21 {hash}\s{name}\n (the same format as localtags) in
21 {hash}\s{name}\n (the same format as localtags) in
22 .hg/bookmarks. The mapping is stored as {name: nodeid}.
22 .hg/bookmarks. The mapping is stored as {name: nodeid}.
23
23
24 This class does NOT handle the "current" bookmark state at this
24 This class does NOT handle the "current" bookmark state at this
25 time.
25 time.
26 """
26 """
27
27
28 def __init__(self, repo):
28 def __init__(self, repo):
29 dict.__init__(self)
29 dict.__init__(self)
30 self._repo = repo
30 self._repo = repo
31 try:
31 try:
32 for line in repo.vfs('bookmarks'):
32 for line in repo.vfs('bookmarks'):
33 line = line.strip()
33 line = line.strip()
34 if not line:
34 if not line:
35 continue
35 continue
36 if ' ' not in line:
36 if ' ' not in line:
37 repo.ui.warn(_('malformed line in .hg/bookmarks: %r\n')
37 repo.ui.warn(_('malformed line in .hg/bookmarks: %r\n')
38 % line)
38 % line)
39 continue
39 continue
40 sha, refspec = line.split(' ', 1)
40 sha, refspec = line.split(' ', 1)
41 refspec = encoding.tolocal(refspec)
41 refspec = encoding.tolocal(refspec)
42 try:
42 try:
43 self[refspec] = repo.changelog.lookup(sha)
43 self[refspec] = repo.changelog.lookup(sha)
44 except LookupError:
44 except LookupError:
45 pass
45 pass
46 except IOError, inst:
46 except IOError, inst:
47 if inst.errno != errno.ENOENT:
47 if inst.errno != errno.ENOENT:
48 raise
48 raise
49
49
50 def write(self):
50 def write(self):
51 '''Write bookmarks
51 '''Write bookmarks
52
52
53 Write the given bookmark => hash dictionary to the .hg/bookmarks file
53 Write the given bookmark => hash dictionary to the .hg/bookmarks file
54 in a format equal to those of localtags.
54 in a format equal to those of localtags.
55
55
56 We also store a backup of the previous state in undo.bookmarks that
56 We also store a backup of the previous state in undo.bookmarks that
57 can be copied back on rollback.
57 can be copied back on rollback.
58 '''
58 '''
59 repo = self._repo
59 repo = self._repo
60 if repo._bookmarkcurrent not in self:
60 if repo._bookmarkcurrent not in self:
61 unsetcurrent(repo)
61 unsetcurrent(repo)
62
62
63 wlock = repo.wlock()
63 wlock = repo.wlock()
64 try:
64 try:
65
65
66 file = repo.vfs('bookmarks', 'w', atomictemp=True)
66 file = repo.vfs('bookmarks', 'w', atomictemp=True)
67 for name, node in self.iteritems():
67 for name, node in self.iteritems():
68 file.write("%s %s\n" % (hex(node), encoding.fromlocal(name)))
68 file.write("%s %s\n" % (hex(node), encoding.fromlocal(name)))
69 file.close()
69 file.close()
70
70
71 # touch 00changelog.i so hgweb reloads bookmarks (no lock needed)
71 # touch 00changelog.i so hgweb reloads bookmarks (no lock needed)
72 try:
72 try:
73 repo.svfs.utime('00changelog.i', None)
73 repo.svfs.utime('00changelog.i', None)
74 except OSError:
74 except OSError:
75 pass
75 pass
76
76
77 finally:
77 finally:
78 wlock.release()
78 wlock.release()
79
79
80 def readcurrent(repo):
80 def readcurrent(repo):
81 '''Get the current bookmark
81 '''Get the current bookmark
82
82
83 If we use gittish branches we have a current bookmark that
83 If we use gittish branches we have a current bookmark that
84 we are on. This function returns the name of the bookmark. It
84 we are on. This function returns the name of the bookmark. It
85 is stored in .hg/bookmarks.current
85 is stored in .hg/bookmarks.current
86 '''
86 '''
87 mark = None
87 mark = None
88 try:
88 try:
89 file = repo.opener('bookmarks.current')
89 file = repo.opener('bookmarks.current')
90 except IOError, inst:
90 except IOError, inst:
91 if inst.errno != errno.ENOENT:
91 if inst.errno != errno.ENOENT:
92 raise
92 raise
93 return None
93 return None
94 try:
94 try:
95 # No readline() in osutil.posixfile, reading everything is cheap
95 # No readline() in osutil.posixfile, reading everything is cheap
96 mark = encoding.tolocal((file.readlines() or [''])[0])
96 mark = encoding.tolocal((file.readlines() or [''])[0])
97 if mark == '' or mark not in repo._bookmarks:
97 if mark == '' or mark not in repo._bookmarks:
98 mark = None
98 mark = None
99 finally:
99 finally:
100 file.close()
100 file.close()
101 return mark
101 return mark
102
102
103 def setcurrent(repo, mark):
103 def setcurrent(repo, mark):
104 '''Set the name of the bookmark that we are currently on
104 '''Set the name of the bookmark that we are currently on
105
105
106 Set the name of the bookmark that we are on (hg update <bookmark>).
106 Set the name of the bookmark that we are on (hg update <bookmark>).
107 The name is recorded in .hg/bookmarks.current
107 The name is recorded in .hg/bookmarks.current
108 '''
108 '''
109 if mark not in repo._bookmarks:
109 if mark not in repo._bookmarks:
110 raise AssertionError('bookmark %s does not exist!' % mark)
110 raise AssertionError('bookmark %s does not exist!' % mark)
111
111
112 current = repo._bookmarkcurrent
112 current = repo._bookmarkcurrent
113 if current == mark:
113 if current == mark:
114 return
114 return
115
115
116 wlock = repo.wlock()
116 wlock = repo.wlock()
117 try:
117 try:
118 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
118 file = repo.opener('bookmarks.current', 'w', atomictemp=True)
119 file.write(encoding.fromlocal(mark))
119 file.write(encoding.fromlocal(mark))
120 file.close()
120 file.close()
121 finally:
121 finally:
122 wlock.release()
122 wlock.release()
123 repo._bookmarkcurrent = mark
123 repo._bookmarkcurrent = mark
124
124
125 def unsetcurrent(repo):
125 def unsetcurrent(repo):
126 wlock = repo.wlock()
126 wlock = repo.wlock()
127 try:
127 try:
128 try:
128 try:
129 repo.vfs.unlink('bookmarks.current')
129 repo.vfs.unlink('bookmarks.current')
130 repo._bookmarkcurrent = None
130 repo._bookmarkcurrent = None
131 except OSError, inst:
131 except OSError, inst:
132 if inst.errno != errno.ENOENT:
132 if inst.errno != errno.ENOENT:
133 raise
133 raise
134 finally:
134 finally:
135 wlock.release()
135 wlock.release()
136
136
137 def iscurrent(repo, mark=None, parents=None):
137 def iscurrent(repo, mark=None, parents=None):
138 '''Tell whether the current bookmark is also active
138 '''Tell whether the current bookmark is also active
139
139
140 I.e., the bookmark listed in .hg/bookmarks.current also points to a
140 I.e., the bookmark listed in .hg/bookmarks.current also points to a
141 parent of the working directory.
141 parent of the working directory.
142 '''
142 '''
143 if not mark:
143 if not mark:
144 mark = repo._bookmarkcurrent
144 mark = repo._bookmarkcurrent
145 if not parents:
145 if not parents:
146 parents = [p.node() for p in repo[None].parents()]
146 parents = [p.node() for p in repo[None].parents()]
147 marks = repo._bookmarks
147 marks = repo._bookmarks
148 return (mark in marks and marks[mark] in parents)
148 return (mark in marks and marks[mark] in parents)
149
149
150 def updatecurrentbookmark(repo, oldnode, curbranch):
150 def updatecurrentbookmark(repo, oldnode, curbranch):
151 try:
151 try:
152 return update(repo, oldnode, repo.branchtip(curbranch))
152 return update(repo, oldnode, repo.branchtip(curbranch))
153 except error.RepoLookupError:
153 except error.RepoLookupError:
154 if curbranch == "default": # no default branch!
154 if curbranch == "default": # no default branch!
155 return update(repo, oldnode, repo.lookup("tip"))
155 return update(repo, oldnode, repo.lookup("tip"))
156 else:
156 else:
157 raise util.Abort(_("branch %s not found") % curbranch)
157 raise util.Abort(_("branch %s not found") % curbranch)
158
158
159 def deletedivergent(repo, deletefrom, bm):
159 def deletedivergent(repo, deletefrom, bm):
160 '''Delete divergent versions of bm on nodes in deletefrom.
160 '''Delete divergent versions of bm on nodes in deletefrom.
161
161
162 Return True if at least one bookmark was deleted, False otherwise.'''
162 Return True if at least one bookmark was deleted, False otherwise.'''
163 deleted = False
163 deleted = False
164 marks = repo._bookmarks
164 marks = repo._bookmarks
165 divergent = [b for b in marks if b.split('@', 1)[0] == bm.split('@', 1)[0]]
165 divergent = [b for b in marks if b.split('@', 1)[0] == bm.split('@', 1)[0]]
166 for mark in divergent:
166 for mark in divergent:
167 if mark and marks[mark] in deletefrom:
167 if mark and marks[mark] in deletefrom:
168 if mark != bm:
168 if mark != bm:
169 del marks[mark]
169 del marks[mark]
170 deleted = True
170 deleted = True
171 return deleted
171 return deleted
172
172
173 def calculateupdate(ui, repo, checkout):
173 def calculateupdate(ui, repo, checkout):
174 '''Return a tuple (targetrev, movemarkfrom) indicating the rev to
174 '''Return a tuple (targetrev, movemarkfrom) indicating the rev to
175 check out and where to move the active bookmark from, if needed.'''
175 check out and where to move the active bookmark from, if needed.'''
176 movemarkfrom = None
176 movemarkfrom = None
177 if checkout is None:
177 if checkout is None:
178 curmark = repo._bookmarkcurrent
178 curmark = repo._bookmarkcurrent
179 if iscurrent(repo):
179 if iscurrent(repo):
180 movemarkfrom = repo['.'].node()
180 movemarkfrom = repo['.'].node()
181 elif curmark:
181 elif curmark:
182 ui.status(_("updating to active bookmark %s\n") % curmark)
182 ui.status(_("updating to active bookmark %s\n") % curmark)
183 checkout = curmark
183 checkout = curmark
184 return (checkout, movemarkfrom)
184 return (checkout, movemarkfrom)
185
185
186 def update(repo, parents, node):
186 def update(repo, parents, node):
187 deletefrom = parents
187 deletefrom = parents
188 marks = repo._bookmarks
188 marks = repo._bookmarks
189 update = False
189 update = False
190 cur = repo._bookmarkcurrent
190 cur = repo._bookmarkcurrent
191 if not cur:
191 if not cur:
192 return False
192 return False
193
193
194 if marks[cur] in parents:
194 if marks[cur] in parents:
195 new = repo[node]
195 new = repo[node]
196 divs = [repo[b] for b in marks
196 divs = [repo[b] for b in marks
197 if b.split('@', 1)[0] == cur.split('@', 1)[0]]
197 if b.split('@', 1)[0] == cur.split('@', 1)[0]]
198 anc = repo.changelog.ancestors([new.rev()])
198 anc = repo.changelog.ancestors([new.rev()])
199 deletefrom = [b.node() for b in divs if b.rev() in anc or b == new]
199 deletefrom = [b.node() for b in divs if b.rev() in anc or b == new]
200 if validdest(repo, repo[marks[cur]], new):
200 if validdest(repo, repo[marks[cur]], new):
201 marks[cur] = new.node()
201 marks[cur] = new.node()
202 update = True
202 update = True
203
203
204 if deletedivergent(repo, deletefrom, cur):
204 if deletedivergent(repo, deletefrom, cur):
205 update = True
205 update = True
206
206
207 if update:
207 if update:
208 marks.write()
208 marks.write()
209 return update
209 return update
210
210
211 def listbookmarks(repo):
211 def listbookmarks(repo):
212 # We may try to list bookmarks on a repo type that does not
212 # We may try to list bookmarks on a repo type that does not
213 # support it (e.g., statichttprepository).
213 # support it (e.g., statichttprepository).
214 marks = getattr(repo, '_bookmarks', {})
214 marks = getattr(repo, '_bookmarks', {})
215
215
216 d = {}
216 d = {}
217 hasnode = repo.changelog.hasnode
217 hasnode = repo.changelog.hasnode
218 for k, v in marks.iteritems():
218 for k, v in marks.iteritems():
219 # don't expose local divergent bookmarks
219 # don't expose local divergent bookmarks
220 if hasnode(v) and ('@' not in k or k.endswith('@')):
220 if hasnode(v) and ('@' not in k or k.endswith('@')):
221 d[k] = hex(v)
221 d[k] = hex(v)
222 return d
222 return d
223
223
224 def pushbookmark(repo, key, old, new):
224 def pushbookmark(repo, key, old, new):
225 w = repo.wlock()
225 w = repo.wlock()
226 try:
226 try:
227 marks = repo._bookmarks
227 marks = repo._bookmarks
228 if hex(marks.get(key, '')) != old:
228 if hex(marks.get(key, '')) != old:
229 return False
229 return False
230 if new == '':
230 if new == '':
231 del marks[key]
231 del marks[key]
232 else:
232 else:
233 if new not in repo:
233 if new not in repo:
234 return False
234 return False
235 marks[key] = repo[new].node()
235 marks[key] = repo[new].node()
236 marks.write()
236 marks.write()
237 return True
237 return True
238 finally:
238 finally:
239 w.release()
239 w.release()
240
240
241 def compare(repo, srcmarks, dstmarks,
241 def compare(repo, srcmarks, dstmarks,
242 srchex=None, dsthex=None, targets=None):
242 srchex=None, dsthex=None, targets=None):
243 '''Compare bookmarks between srcmarks and dstmarks
243 '''Compare bookmarks between srcmarks and dstmarks
244
244
245 This returns tuple "(addsrc, adddst, advsrc, advdst, diverge,
245 This returns tuple "(addsrc, adddst, advsrc, advdst, diverge,
246 differ, invalid)", each are list of bookmarks below:
246 differ, invalid)", each are list of bookmarks below:
247
247
248 :addsrc: added on src side (removed on dst side, perhaps)
248 :addsrc: added on src side (removed on dst side, perhaps)
249 :adddst: added on dst side (removed on src side, perhaps)
249 :adddst: added on dst side (removed on src side, perhaps)
250 :advsrc: advanced on src side
250 :advsrc: advanced on src side
251 :advdst: advanced on dst side
251 :advdst: advanced on dst side
252 :diverge: diverge
252 :diverge: diverge
253 :differ: changed, but changeset referred on src is unknown on dst
253 :differ: changed, but changeset referred on src is unknown on dst
254 :invalid: unknown on both side
254 :invalid: unknown on both side
255
255
256 Each elements of lists in result tuple is tuple "(bookmark name,
256 Each elements of lists in result tuple is tuple "(bookmark name,
257 changeset ID on source side, changeset ID on destination
257 changeset ID on source side, changeset ID on destination
258 side)". Each changeset IDs are 40 hexadecimal digit string or
258 side)". Each changeset IDs are 40 hexadecimal digit string or
259 None.
259 None.
260
260
261 Changeset IDs of tuples in "addsrc", "adddst", "differ" or
261 Changeset IDs of tuples in "addsrc", "adddst", "differ" or
262 "invalid" list may be unknown for repo.
262 "invalid" list may be unknown for repo.
263
263
264 This function expects that "srcmarks" and "dstmarks" return
264 This function expects that "srcmarks" and "dstmarks" return
265 changeset ID in 40 hexadecimal digit string for specified
265 changeset ID in 40 hexadecimal digit string for specified
266 bookmark. If not so (e.g. bmstore "repo._bookmarks" returning
266 bookmark. If not so (e.g. bmstore "repo._bookmarks" returning
267 binary value), "srchex" or "dsthex" should be specified to convert
267 binary value), "srchex" or "dsthex" should be specified to convert
268 into such form.
268 into such form.
269
269
270 If "targets" is specified, only bookmarks listed in it are
270 If "targets" is specified, only bookmarks listed in it are
271 examined.
271 examined.
272 '''
272 '''
273 if not srchex:
273 if not srchex:
274 srchex = lambda x: x
274 srchex = lambda x: x
275 if not dsthex:
275 if not dsthex:
276 dsthex = lambda x: x
276 dsthex = lambda x: x
277
277
278 if targets:
278 if targets:
279 bset = set(targets)
279 bset = set(targets)
280 else:
280 else:
281 srcmarkset = set(srcmarks)
281 srcmarkset = set(srcmarks)
282 dstmarkset = set(dstmarks)
282 dstmarkset = set(dstmarks)
283 bset = srcmarkset ^ dstmarkset
283 bset = srcmarkset ^ dstmarkset
284 for b in srcmarkset & dstmarkset:
284 for b in srcmarkset & dstmarkset:
285 if srchex(srcmarks[b]) != dsthex(dstmarks[b]):
285 if srchex(srcmarks[b]) != dsthex(dstmarks[b]):
286 bset.add(b)
286 bset.add(b)
287
287
288 results = ([], [], [], [], [], [], [])
288 results = ([], [], [], [], [], [], [])
289 addsrc = results[0].append
289 addsrc = results[0].append
290 adddst = results[1].append
290 adddst = results[1].append
291 advsrc = results[2].append
291 advsrc = results[2].append
292 advdst = results[3].append
292 advdst = results[3].append
293 diverge = results[4].append
293 diverge = results[4].append
294 differ = results[5].append
294 differ = results[5].append
295 invalid = results[6].append
295 invalid = results[6].append
296
296
297 for b in sorted(bset):
297 for b in sorted(bset):
298 if b not in srcmarks:
298 if b not in srcmarks:
299 if b in dstmarks:
299 if b in dstmarks:
300 adddst((b, None, dsthex(dstmarks[b])))
300 adddst((b, None, dsthex(dstmarks[b])))
301 else:
301 else:
302 invalid((b, None, None))
302 invalid((b, None, None))
303 elif b not in dstmarks:
303 elif b not in dstmarks:
304 addsrc((b, srchex(srcmarks[b]), None))
304 addsrc((b, srchex(srcmarks[b]), None))
305 else:
305 else:
306 scid = srchex(srcmarks[b])
306 scid = srchex(srcmarks[b])
307 dcid = dsthex(dstmarks[b])
307 dcid = dsthex(dstmarks[b])
308 if scid in repo and dcid in repo:
308 if scid in repo and dcid in repo:
309 sctx = repo[scid]
309 sctx = repo[scid]
310 dctx = repo[dcid]
310 dctx = repo[dcid]
311 if sctx.rev() < dctx.rev():
311 if sctx.rev() < dctx.rev():
312 if validdest(repo, sctx, dctx):
312 if validdest(repo, sctx, dctx):
313 advdst((b, scid, dcid))
313 advdst((b, scid, dcid))
314 else:
314 else:
315 diverge((b, scid, dcid))
315 diverge((b, scid, dcid))
316 else:
316 else:
317 if validdest(repo, dctx, sctx):
317 if validdest(repo, dctx, sctx):
318 advsrc((b, scid, dcid))
318 advsrc((b, scid, dcid))
319 else:
319 else:
320 diverge((b, scid, dcid))
320 diverge((b, scid, dcid))
321 else:
321 else:
322 # it is too expensive to examine in detail, in this case
322 # it is too expensive to examine in detail, in this case
323 differ((b, scid, dcid))
323 differ((b, scid, dcid))
324
324
325 return results
325 return results
326
326
327 def _diverge(ui, b, path, localmarks):
327 def _diverge(ui, b, path, localmarks):
328 if b == '@':
328 if b == '@':
329 b = ''
329 b = ''
330 # find a unique @ suffix
330 # find a unique @ suffix
331 for x in range(1, 100):
331 for x in range(1, 100):
332 n = '%s@%d' % (b, x)
332 n = '%s@%d' % (b, x)
333 if n not in localmarks:
333 if n not in localmarks:
334 break
334 break
335 # try to use an @pathalias suffix
335 # try to use an @pathalias suffix
336 # if an @pathalias already exists, we overwrite (update) it
336 # if an @pathalias already exists, we overwrite (update) it
337 for p, u in ui.configitems("paths"):
337 for p, u in ui.configitems("paths"):
338 if path == u:
338 if path == u:
339 n = '%s@%s' % (b, p)
339 n = '%s@%s' % (b, p)
340 return n
340 return n
341
341
342 def updatefromremote(ui, repo, remotemarks, path):
342 def updatefromremote(ui, repo, remotemarks, path):
343 ui.debug("checking for updated bookmarks\n")
343 ui.debug("checking for updated bookmarks\n")
344 localmarks = repo._bookmarks
344 localmarks = repo._bookmarks
345 (addsrc, adddst, advsrc, advdst, diverge, differ, invalid
345 (addsrc, adddst, advsrc, advdst, diverge, differ, invalid
346 ) = compare(repo, remotemarks, localmarks, dsthex=hex)
346 ) = compare(repo, remotemarks, localmarks, dsthex=hex)
347
347
348 changed = []
348 changed = []
349 for b, scid, dcid in addsrc:
349 for b, scid, dcid in addsrc:
350 if scid in repo: # add remote bookmarks for changes we already have
350 if scid in repo: # add remote bookmarks for changes we already have
351 changed.append((b, bin(scid), ui.status,
351 changed.append((b, bin(scid), ui.status,
352 _("adding remote bookmark %s\n") % (b)))
352 _("adding remote bookmark %s\n") % (b)))
353 for b, scid, dcid in advsrc:
353 for b, scid, dcid in advsrc:
354 changed.append((b, bin(scid), ui.status,
354 changed.append((b, bin(scid), ui.status,
355 _("updating bookmark %s\n") % (b)))
355 _("updating bookmark %s\n") % (b)))
356 for b, scid, dcid in diverge:
356 for b, scid, dcid in diverge:
357 db = _diverge(ui, b, path, localmarks)
357 db = _diverge(ui, b, path, localmarks)
358 changed.append((db, bin(scid), ui.warn,
358 changed.append((db, bin(scid), ui.warn,
359 _("divergent bookmark %s stored as %s\n") % (b, db)))
359 _("divergent bookmark %s stored as %s\n") % (b, db)))
360 if changed:
360 if changed:
361 for b, node, writer, msg in sorted(changed):
361 for b, node, writer, msg in sorted(changed):
362 localmarks[b] = node
362 localmarks[b] = node
363 writer(msg)
363 writer(msg)
364 localmarks.write()
364 localmarks.write()
365
365
366 def updateremote(ui, repo, remote, revs):
367 ui.debug("checking for updated bookmarks\n")
368 revnums = map(repo.changelog.rev, revs or [])
369 ancestors = [a for a in repo.changelog.ancestors(revnums, inclusive=True)]
370 (addsrc, adddst, advsrc, advdst, diverge, differ, invalid
371 ) = compare(repo, repo._bookmarks, remote.listkeys('bookmarks'),
372 srchex=hex)
373
374 for b, scid, dcid in advsrc:
375 if ancestors and repo[scid].rev() not in ancestors:
376 continue
377 if remote.pushkey('bookmarks', b, dcid, scid):
378 ui.status(_("updating bookmark %s\n") % b)
379 else:
380 ui.warn(_('updating bookmark %s failed!\n') % b)
381
382 def pushtoremote(ui, repo, remote, targets):
366 def pushtoremote(ui, repo, remote, targets):
383 (addsrc, adddst, advsrc, advdst, diverge, differ, invalid
367 (addsrc, adddst, advsrc, advdst, diverge, differ, invalid
384 ) = compare(repo, repo._bookmarks, remote.listkeys('bookmarks'),
368 ) = compare(repo, repo._bookmarks, remote.listkeys('bookmarks'),
385 srchex=hex, targets=targets)
369 srchex=hex, targets=targets)
386 if invalid:
370 if invalid:
387 b, scid, dcid = invalid[0]
371 b, scid, dcid = invalid[0]
388 ui.warn(_('bookmark %s does not exist on the local '
372 ui.warn(_('bookmark %s does not exist on the local '
389 'or remote repository!\n') % b)
373 'or remote repository!\n') % b)
390 return 2
374 return 2
391
375
392 def push(b, old, new):
376 def push(b, old, new):
393 r = remote.pushkey('bookmarks', b, old, new)
377 r = remote.pushkey('bookmarks', b, old, new)
394 if not r:
378 if not r:
395 ui.warn(_('updating bookmark %s failed!\n') % b)
379 ui.warn(_('updating bookmark %s failed!\n') % b)
396 return 1
380 return 1
397 return 0
381 return 0
398 failed = 0
382 failed = 0
399 for b, scid, dcid in sorted(addsrc + advsrc + advdst + diverge + differ):
383 for b, scid, dcid in sorted(addsrc + advsrc + advdst + diverge + differ):
400 ui.status(_("exporting bookmark %s\n") % b)
384 ui.status(_("exporting bookmark %s\n") % b)
401 if dcid is None:
385 if dcid is None:
402 dcid = ''
386 dcid = ''
403 failed += push(b, dcid, scid)
387 failed += push(b, dcid, scid)
404 for b, scid, dcid in adddst:
388 for b, scid, dcid in adddst:
405 # treat as "deleted locally"
389 # treat as "deleted locally"
406 ui.status(_("deleting remote bookmark %s\n") % b)
390 ui.status(_("deleting remote bookmark %s\n") % b)
407 failed += push(b, dcid, '')
391 failed += push(b, dcid, '')
408
392
409 if failed:
393 if failed:
410 return 1
394 return 1
411
395
412 def diff(ui, dst, src):
396 def diff(ui, dst, src):
413 ui.status(_("searching for changed bookmarks\n"))
397 ui.status(_("searching for changed bookmarks\n"))
414
398
415 smarks = src.listkeys('bookmarks')
399 smarks = src.listkeys('bookmarks')
416 dmarks = dst.listkeys('bookmarks')
400 dmarks = dst.listkeys('bookmarks')
417
401
418 diff = sorted(set(smarks) - set(dmarks))
402 diff = sorted(set(smarks) - set(dmarks))
419 for k in diff:
403 for k in diff:
420 mark = ui.debugflag and smarks[k] or smarks[k][:12]
404 mark = ui.debugflag and smarks[k] or smarks[k][:12]
421 ui.write(" %-25s %s\n" % (k, mark))
405 ui.write(" %-25s %s\n" % (k, mark))
422
406
423 if len(diff) <= 0:
407 if len(diff) <= 0:
424 ui.status(_("no changed bookmarks found\n"))
408 ui.status(_("no changed bookmarks found\n"))
425 return 1
409 return 1
426 return 0
410 return 0
427
411
428 def validdest(repo, old, new):
412 def validdest(repo, old, new):
429 """Is the new bookmark destination a valid update from the old one"""
413 """Is the new bookmark destination a valid update from the old one"""
430 repo = repo.unfiltered()
414 repo = repo.unfiltered()
431 if old == new:
415 if old == new:
432 # Old == new -> nothing to update.
416 # Old == new -> nothing to update.
433 return False
417 return False
434 elif not old:
418 elif not old:
435 # old is nullrev, anything is valid.
419 # old is nullrev, anything is valid.
436 # (new != nullrev has been excluded by the previous check)
420 # (new != nullrev has been excluded by the previous check)
437 return True
421 return True
438 elif repo.obsstore:
422 elif repo.obsstore:
439 return new.node() in obsolete.foreground(repo, [old.node()])
423 return new.node() in obsolete.foreground(repo, [old.node()])
440 else:
424 else:
441 # still an independent clause as it is lazyer (and therefore faster)
425 # still an independent clause as it is lazyer (and therefore faster)
442 return old.descendant(new)
426 return old.descendant(new)
@@ -1,259 +1,276 b''
1 # exchange.py - utily to exchange data between repo.
1 # exchange.py - utily to exchange data between repo.
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from i18n import _
8 from i18n import _
9 from node import hex
9 from node import hex
10 import errno
10 import errno
11 import util, scmutil, changegroup
11 import util, scmutil, changegroup
12 import discovery, phases, obsolete, bookmarks
12 import discovery, phases, obsolete, bookmarks
13
13
14
14
15 class pushoperation(object):
15 class pushoperation(object):
16 """A object that represent a single push operation
16 """A object that represent a single push operation
17
17
18 It purpose is to carry push related state and very common operation.
18 It purpose is to carry push related state and very common operation.
19
19
20 A new should be created at the begining of each push and discarded
20 A new should be created at the begining of each push and discarded
21 afterward.
21 afterward.
22 """
22 """
23
23
24 def __init__(self, repo, remote, force=False, revs=None, newbranch=False):
24 def __init__(self, repo, remote, force=False, revs=None, newbranch=False):
25 # repo we push from
25 # repo we push from
26 self.repo = repo
26 self.repo = repo
27 self.ui = repo.ui
27 self.ui = repo.ui
28 # repo we push to
28 # repo we push to
29 self.remote = remote
29 self.remote = remote
30 # force option provided
30 # force option provided
31 self.force = force
31 self.force = force
32 # revs to be pushed (None is "all")
32 # revs to be pushed (None is "all")
33 self.revs = revs
33 self.revs = revs
34 # allow push of new branch
34 # allow push of new branch
35 self.newbranch = newbranch
35 self.newbranch = newbranch
36
36
37 def push(repo, remote, force=False, revs=None, newbranch=False):
37 def push(repo, remote, force=False, revs=None, newbranch=False):
38 '''Push outgoing changesets (limited by revs) from a local
38 '''Push outgoing changesets (limited by revs) from a local
39 repository to remote. Return an integer:
39 repository to remote. Return an integer:
40 - None means nothing to push
40 - None means nothing to push
41 - 0 means HTTP error
41 - 0 means HTTP error
42 - 1 means we pushed and remote head count is unchanged *or*
42 - 1 means we pushed and remote head count is unchanged *or*
43 we have outgoing changesets but refused to push
43 we have outgoing changesets but refused to push
44 - other values as described by addchangegroup()
44 - other values as described by addchangegroup()
45 '''
45 '''
46 pushop = pushoperation(repo, remote, force, revs, newbranch)
46 pushop = pushoperation(repo, remote, force, revs, newbranch)
47 if pushop.remote.local():
47 if pushop.remote.local():
48 missing = (set(pushop.repo.requirements)
48 missing = (set(pushop.repo.requirements)
49 - pushop.remote.local().supported)
49 - pushop.remote.local().supported)
50 if missing:
50 if missing:
51 msg = _("required features are not"
51 msg = _("required features are not"
52 " supported in the destination:"
52 " supported in the destination:"
53 " %s") % (', '.join(sorted(missing)))
53 " %s") % (', '.join(sorted(missing)))
54 raise util.Abort(msg)
54 raise util.Abort(msg)
55
55
56 # there are two ways to push to remote repo:
56 # there are two ways to push to remote repo:
57 #
57 #
58 # addchangegroup assumes local user can lock remote
58 # addchangegroup assumes local user can lock remote
59 # repo (local filesystem, old ssh servers).
59 # repo (local filesystem, old ssh servers).
60 #
60 #
61 # unbundle assumes local user cannot lock remote repo (new ssh
61 # unbundle assumes local user cannot lock remote repo (new ssh
62 # servers, http servers).
62 # servers, http servers).
63
63
64 if not pushop.remote.canpush():
64 if not pushop.remote.canpush():
65 raise util.Abort(_("destination does not support push"))
65 raise util.Abort(_("destination does not support push"))
66 unfi = pushop.repo.unfiltered()
66 unfi = pushop.repo.unfiltered()
67 def localphasemove(nodes, phase=phases.public):
67 def localphasemove(nodes, phase=phases.public):
68 """move <nodes> to <phase> in the local source repo"""
68 """move <nodes> to <phase> in the local source repo"""
69 if locallock is not None:
69 if locallock is not None:
70 phases.advanceboundary(pushop.repo, phase, nodes)
70 phases.advanceboundary(pushop.repo, phase, nodes)
71 else:
71 else:
72 # repo is not locked, do not change any phases!
72 # repo is not locked, do not change any phases!
73 # Informs the user that phases should have been moved when
73 # Informs the user that phases should have been moved when
74 # applicable.
74 # applicable.
75 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
75 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
76 phasestr = phases.phasenames[phase]
76 phasestr = phases.phasenames[phase]
77 if actualmoves:
77 if actualmoves:
78 pushop.ui.status(_('cannot lock source repo, skipping '
78 pushop.ui.status(_('cannot lock source repo, skipping '
79 'local %s phase update\n') % phasestr)
79 'local %s phase update\n') % phasestr)
80 # get local lock as we might write phase data
80 # get local lock as we might write phase data
81 locallock = None
81 locallock = None
82 try:
82 try:
83 locallock = pushop.repo.lock()
83 locallock = pushop.repo.lock()
84 except IOError, err:
84 except IOError, err:
85 if err.errno != errno.EACCES:
85 if err.errno != errno.EACCES:
86 raise
86 raise
87 # source repo cannot be locked.
87 # source repo cannot be locked.
88 # We do not abort the push, but just disable the local phase
88 # We do not abort the push, but just disable the local phase
89 # synchronisation.
89 # synchronisation.
90 msg = 'cannot lock source repository: %s\n' % err
90 msg = 'cannot lock source repository: %s\n' % err
91 pushop.ui.debug(msg)
91 pushop.ui.debug(msg)
92 try:
92 try:
93 pushop.repo.checkpush(pushop.force, pushop.revs)
93 pushop.repo.checkpush(pushop.force, pushop.revs)
94 lock = None
94 lock = None
95 unbundle = pushop.remote.capable('unbundle')
95 unbundle = pushop.remote.capable('unbundle')
96 if not unbundle:
96 if not unbundle:
97 lock = pushop.remote.lock()
97 lock = pushop.remote.lock()
98 try:
98 try:
99 # discovery
99 # discovery
100 fci = discovery.findcommonincoming
100 fci = discovery.findcommonincoming
101 commoninc = fci(unfi, pushop.remote, force=pushop.force)
101 commoninc = fci(unfi, pushop.remote, force=pushop.force)
102 common, inc, remoteheads = commoninc
102 common, inc, remoteheads = commoninc
103 fco = discovery.findcommonoutgoing
103 fco = discovery.findcommonoutgoing
104 outgoing = fco(unfi, pushop.remote, onlyheads=pushop.revs,
104 outgoing = fco(unfi, pushop.remote, onlyheads=pushop.revs,
105 commoninc=commoninc, force=pushop.force)
105 commoninc=commoninc, force=pushop.force)
106
106
107
107
108 if not outgoing.missing:
108 if not outgoing.missing:
109 # nothing to push
109 # nothing to push
110 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
110 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
111 ret = None
111 ret = None
112 else:
112 else:
113 # something to push
113 # something to push
114 if not pushop.force:
114 if not pushop.force:
115 # if repo.obsstore == False --> no obsolete
115 # if repo.obsstore == False --> no obsolete
116 # then, save the iteration
116 # then, save the iteration
117 if unfi.obsstore:
117 if unfi.obsstore:
118 # this message are here for 80 char limit reason
118 # this message are here for 80 char limit reason
119 mso = _("push includes obsolete changeset: %s!")
119 mso = _("push includes obsolete changeset: %s!")
120 mst = "push includes %s changeset: %s!"
120 mst = "push includes %s changeset: %s!"
121 # plain versions for i18n tool to detect them
121 # plain versions for i18n tool to detect them
122 _("push includes unstable changeset: %s!")
122 _("push includes unstable changeset: %s!")
123 _("push includes bumped changeset: %s!")
123 _("push includes bumped changeset: %s!")
124 _("push includes divergent changeset: %s!")
124 _("push includes divergent changeset: %s!")
125 # If we are to push if there is at least one
125 # If we are to push if there is at least one
126 # obsolete or unstable changeset in missing, at
126 # obsolete or unstable changeset in missing, at
127 # least one of the missinghead will be obsolete or
127 # least one of the missinghead will be obsolete or
128 # unstable. So checking heads only is ok
128 # unstable. So checking heads only is ok
129 for node in outgoing.missingheads:
129 for node in outgoing.missingheads:
130 ctx = unfi[node]
130 ctx = unfi[node]
131 if ctx.obsolete():
131 if ctx.obsolete():
132 raise util.Abort(mso % ctx)
132 raise util.Abort(mso % ctx)
133 elif ctx.troubled():
133 elif ctx.troubled():
134 raise util.Abort(_(mst)
134 raise util.Abort(_(mst)
135 % (ctx.troubles()[0],
135 % (ctx.troubles()[0],
136 ctx))
136 ctx))
137 newbm = pushop.ui.configlist('bookmarks', 'pushing')
137 newbm = pushop.ui.configlist('bookmarks', 'pushing')
138 discovery.checkheads(unfi, pushop.remote, outgoing,
138 discovery.checkheads(unfi, pushop.remote, outgoing,
139 remoteheads, pushop.newbranch,
139 remoteheads, pushop.newbranch,
140 bool(inc), newbm)
140 bool(inc), newbm)
141
141
142 # TODO: get bundlecaps from remote
142 # TODO: get bundlecaps from remote
143 bundlecaps = None
143 bundlecaps = None
144 # create a changegroup from local
144 # create a changegroup from local
145 if pushop.revs is None and not (outgoing.excluded
145 if pushop.revs is None and not (outgoing.excluded
146 or pushop.repo.changelog.filteredrevs):
146 or pushop.repo.changelog.filteredrevs):
147 # push everything,
147 # push everything,
148 # use the fast path, no race possible on push
148 # use the fast path, no race possible on push
149 bundler = changegroup.bundle10(pushop.repo, bundlecaps)
149 bundler = changegroup.bundle10(pushop.repo, bundlecaps)
150 cg = pushop.repo._changegroupsubset(outgoing,
150 cg = pushop.repo._changegroupsubset(outgoing,
151 bundler,
151 bundler,
152 'push',
152 'push',
153 fastpath=True)
153 fastpath=True)
154 else:
154 else:
155 cg = pushop.repo.getlocalbundle('push', outgoing,
155 cg = pushop.repo.getlocalbundle('push', outgoing,
156 bundlecaps)
156 bundlecaps)
157
157
158 # apply changegroup to remote
158 # apply changegroup to remote
159 if unbundle:
159 if unbundle:
160 # local repo finds heads on server, finds out what
160 # local repo finds heads on server, finds out what
161 # revs it must push. once revs transferred, if server
161 # revs it must push. once revs transferred, if server
162 # finds it has different heads (someone else won
162 # finds it has different heads (someone else won
163 # commit/push race), server aborts.
163 # commit/push race), server aborts.
164 if pushop.force:
164 if pushop.force:
165 remoteheads = ['force']
165 remoteheads = ['force']
166 # ssh: return remote's addchangegroup()
166 # ssh: return remote's addchangegroup()
167 # http: return remote's addchangegroup() or 0 for error
167 # http: return remote's addchangegroup() or 0 for error
168 ret = pushop.remote.unbundle(cg, remoteheads, 'push')
168 ret = pushop.remote.unbundle(cg, remoteheads, 'push')
169 else:
169 else:
170 # we return an integer indicating remote head count
170 # we return an integer indicating remote head count
171 # change
171 # change
172 ret = pushop.remote.addchangegroup(cg, 'push',
172 ret = pushop.remote.addchangegroup(cg, 'push',
173 pushop.repo.url())
173 pushop.repo.url())
174
174
175 if ret:
175 if ret:
176 # push succeed, synchronize target of the push
176 # push succeed, synchronize target of the push
177 cheads = outgoing.missingheads
177 cheads = outgoing.missingheads
178 elif pushop.revs is None:
178 elif pushop.revs is None:
179 # All out push fails. synchronize all common
179 # All out push fails. synchronize all common
180 cheads = outgoing.commonheads
180 cheads = outgoing.commonheads
181 else:
181 else:
182 # I want cheads = heads(::missingheads and ::commonheads)
182 # I want cheads = heads(::missingheads and ::commonheads)
183 # (missingheads is revs with secret changeset filtered out)
183 # (missingheads is revs with secret changeset filtered out)
184 #
184 #
185 # This can be expressed as:
185 # This can be expressed as:
186 # cheads = ( (missingheads and ::commonheads)
186 # cheads = ( (missingheads and ::commonheads)
187 # + (commonheads and ::missingheads))"
187 # + (commonheads and ::missingheads))"
188 # )
188 # )
189 #
189 #
190 # while trying to push we already computed the following:
190 # while trying to push we already computed the following:
191 # common = (::commonheads)
191 # common = (::commonheads)
192 # missing = ((commonheads::missingheads) - commonheads)
192 # missing = ((commonheads::missingheads) - commonheads)
193 #
193 #
194 # We can pick:
194 # We can pick:
195 # * missingheads part of common (::commonheads)
195 # * missingheads part of common (::commonheads)
196 common = set(outgoing.common)
196 common = set(outgoing.common)
197 nm = pushop.repo.changelog.nodemap
197 nm = pushop.repo.changelog.nodemap
198 cheads = [node for node in pushop.revs if nm[node] in common]
198 cheads = [node for node in pushop.revs if nm[node] in common]
199 # and
199 # and
200 # * commonheads parents on missing
200 # * commonheads parents on missing
201 revset = unfi.set('%ln and parents(roots(%ln))',
201 revset = unfi.set('%ln and parents(roots(%ln))',
202 outgoing.commonheads,
202 outgoing.commonheads,
203 outgoing.missing)
203 outgoing.missing)
204 cheads.extend(c.node() for c in revset)
204 cheads.extend(c.node() for c in revset)
205 # even when we don't push, exchanging phase data is useful
205 # even when we don't push, exchanging phase data is useful
206 remotephases = pushop.remote.listkeys('phases')
206 remotephases = pushop.remote.listkeys('phases')
207 if (pushop.ui.configbool('ui', '_usedassubrepo', False)
207 if (pushop.ui.configbool('ui', '_usedassubrepo', False)
208 and remotephases # server supports phases
208 and remotephases # server supports phases
209 and ret is None # nothing was pushed
209 and ret is None # nothing was pushed
210 and remotephases.get('publishing', False)):
210 and remotephases.get('publishing', False)):
211 # When:
211 # When:
212 # - this is a subrepo push
212 # - this is a subrepo push
213 # - and remote support phase
213 # - and remote support phase
214 # - and no changeset was pushed
214 # - and no changeset was pushed
215 # - and remote is publishing
215 # - and remote is publishing
216 # We may be in issue 3871 case!
216 # We may be in issue 3871 case!
217 # We drop the possible phase synchronisation done by
217 # We drop the possible phase synchronisation done by
218 # courtesy to publish changesets possibly locally draft
218 # courtesy to publish changesets possibly locally draft
219 # on the remote.
219 # on the remote.
220 remotephases = {'publishing': 'True'}
220 remotephases = {'publishing': 'True'}
221 if not remotephases: # old server or public only repo
221 if not remotephases: # old server or public only repo
222 localphasemove(cheads)
222 localphasemove(cheads)
223 # don't push any phase data as there is nothing to push
223 # don't push any phase data as there is nothing to push
224 else:
224 else:
225 ana = phases.analyzeremotephases(pushop.repo, cheads,
225 ana = phases.analyzeremotephases(pushop.repo, cheads,
226 remotephases)
226 remotephases)
227 pheads, droots = ana
227 pheads, droots = ana
228 ### Apply remote phase on local
228 ### Apply remote phase on local
229 if remotephases.get('publishing', False):
229 if remotephases.get('publishing', False):
230 localphasemove(cheads)
230 localphasemove(cheads)
231 else: # publish = False
231 else: # publish = False
232 localphasemove(pheads)
232 localphasemove(pheads)
233 localphasemove(cheads, phases.draft)
233 localphasemove(cheads, phases.draft)
234 ### Apply local phase on remote
234 ### Apply local phase on remote
235
235
236 # Get the list of all revs draft on remote by public here.
236 # Get the list of all revs draft on remote by public here.
237 # XXX Beware that revset break if droots is not strictly
237 # XXX Beware that revset break if droots is not strictly
238 # XXX root we may want to ensure it is but it is costly
238 # XXX root we may want to ensure it is but it is costly
239 outdated = unfi.set('heads((%ln::%ln) and public())',
239 outdated = unfi.set('heads((%ln::%ln) and public())',
240 droots, cheads)
240 droots, cheads)
241 for newremotehead in outdated:
241 for newremotehead in outdated:
242 r = pushop.remote.pushkey('phases',
242 r = pushop.remote.pushkey('phases',
243 newremotehead.hex(),
243 newremotehead.hex(),
244 str(phases.draft),
244 str(phases.draft),
245 str(phases.public))
245 str(phases.public))
246 if not r:
246 if not r:
247 pushop.ui.warn(_('updating %s to public failed!\n')
247 pushop.ui.warn(_('updating %s to public failed!\n')
248 % newremotehead)
248 % newremotehead)
249 pushop.ui.debug('try to push obsolete markers to remote\n')
249 pushop.ui.debug('try to push obsolete markers to remote\n')
250 obsolete.syncpush(pushop.repo, pushop.remote)
250 obsolete.syncpush(pushop.repo, pushop.remote)
251 finally:
251 finally:
252 if lock is not None:
252 if lock is not None:
253 lock.release()
253 lock.release()
254 finally:
254 finally:
255 if locallock is not None:
255 if locallock is not None:
256 locallock.release()
256 locallock.release()
257
257
258 bookmarks.updateremote(pushop.ui, unfi, pushop.remote, pushop.revs)
258 _pushbookmark(pushop.ui, unfi, pushop.remote, pushop.revs)
259 return ret
259 return ret
260
261 def _pushbookmark(ui, repo, remote, revs):
262 """Update bookmark position on remote"""
263 ui.debug("checking for updated bookmarks\n")
264 revnums = map(repo.changelog.rev, revs or [])
265 ancestors = [a for a in repo.changelog.ancestors(revnums, inclusive=True)]
266 (addsrc, adddst, advsrc, advdst, diverge, differ, invalid
267 ) = bookmarks.compare(repo, repo._bookmarks, remote.listkeys('bookmarks'),
268 srchex=hex)
269
270 for b, scid, dcid in advsrc:
271 if ancestors and repo[scid].rev() not in ancestors:
272 continue
273 if remote.pushkey('bookmarks', b, dcid, scid):
274 ui.status(_("updating bookmark %s\n") % b)
275 else:
276 ui.warn(_('updating bookmark %s failed!\n') % b)
General Comments 0
You need to be logged in to leave comments. Login now