##// END OF EJS Templates
push: move discovery in its own function...
Pierre-Yves David -
r20466:233623d5 default
parent child Browse files
Show More
@@ -1,333 +1,335 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 # did a local lock get acquired?
36 # did a local lock get acquired?
37 self.locallocked = None
37 self.locallocked = None
38 # Integer version of the push result
38 # Integer version of the push result
39 # - None means nothing to push
39 # - None means nothing to push
40 # - 0 means HTTP error
40 # - 0 means HTTP error
41 # - 1 means we pushed and remote head count is unchanged *or*
41 # - 1 means we pushed and remote head count is unchanged *or*
42 # we have outgoing changesets but refused to push
42 # we have outgoing changesets but refused to push
43 # - other values as described by addchangegroup()
43 # - other values as described by addchangegroup()
44 self.ret = None
44 self.ret = None
45 # discover.outgoing object (contains common and outgoin data)
45 # discover.outgoing object (contains common and outgoin data)
46 self.outgoing = None
46 self.outgoing = None
47 # all remote heads before the push
47 # all remote heads before the push
48 self.remoteheads = None
48 self.remoteheads = None
49 # testable as a boolean indicating if any nodes are missing locally.
49 # testable as a boolean indicating if any nodes are missing locally.
50 self.incoming = None
50 self.incoming = None
51
51
52 def push(repo, remote, force=False, revs=None, newbranch=False):
52 def push(repo, remote, force=False, revs=None, newbranch=False):
53 '''Push outgoing changesets (limited by revs) from a local
53 '''Push outgoing changesets (limited by revs) from a local
54 repository to remote. Return an integer:
54 repository to remote. Return an integer:
55 - None means nothing to push
55 - None means nothing to push
56 - 0 means HTTP error
56 - 0 means HTTP error
57 - 1 means we pushed and remote head count is unchanged *or*
57 - 1 means we pushed and remote head count is unchanged *or*
58 we have outgoing changesets but refused to push
58 we have outgoing changesets but refused to push
59 - other values as described by addchangegroup()
59 - other values as described by addchangegroup()
60 '''
60 '''
61 pushop = pushoperation(repo, remote, force, revs, newbranch)
61 pushop = pushoperation(repo, remote, force, revs, newbranch)
62 if pushop.remote.local():
62 if pushop.remote.local():
63 missing = (set(pushop.repo.requirements)
63 missing = (set(pushop.repo.requirements)
64 - pushop.remote.local().supported)
64 - pushop.remote.local().supported)
65 if missing:
65 if missing:
66 msg = _("required features are not"
66 msg = _("required features are not"
67 " supported in the destination:"
67 " supported in the destination:"
68 " %s") % (', '.join(sorted(missing)))
68 " %s") % (', '.join(sorted(missing)))
69 raise util.Abort(msg)
69 raise util.Abort(msg)
70
70
71 # there are two ways to push to remote repo:
71 # there are two ways to push to remote repo:
72 #
72 #
73 # addchangegroup assumes local user can lock remote
73 # addchangegroup assumes local user can lock remote
74 # repo (local filesystem, old ssh servers).
74 # repo (local filesystem, old ssh servers).
75 #
75 #
76 # unbundle assumes local user cannot lock remote repo (new ssh
76 # unbundle assumes local user cannot lock remote repo (new ssh
77 # servers, http servers).
77 # servers, http servers).
78
78
79 if not pushop.remote.canpush():
79 if not pushop.remote.canpush():
80 raise util.Abort(_("destination does not support push"))
80 raise util.Abort(_("destination does not support push"))
81 unfi = pushop.repo.unfiltered()
82 # get local lock as we might write phase data
81 # get local lock as we might write phase data
83 locallock = None
82 locallock = None
84 try:
83 try:
85 locallock = pushop.repo.lock()
84 locallock = pushop.repo.lock()
86 pushop.locallocked = True
85 pushop.locallocked = True
87 except IOError, err:
86 except IOError, err:
88 pushop.locallocked = False
87 pushop.locallocked = False
89 if err.errno != errno.EACCES:
88 if err.errno != errno.EACCES:
90 raise
89 raise
91 # source repo cannot be locked.
90 # source repo cannot be locked.
92 # We do not abort the push, but just disable the local phase
91 # We do not abort the push, but just disable the local phase
93 # synchronisation.
92 # synchronisation.
94 msg = 'cannot lock source repository: %s\n' % err
93 msg = 'cannot lock source repository: %s\n' % err
95 pushop.ui.debug(msg)
94 pushop.ui.debug(msg)
96 try:
95 try:
97 pushop.repo.checkpush(pushop.force, pushop.revs)
96 pushop.repo.checkpush(pushop.force, pushop.revs)
98 lock = None
97 lock = None
99 unbundle = pushop.remote.capable('unbundle')
98 unbundle = pushop.remote.capable('unbundle')
100 if not unbundle:
99 if not unbundle:
101 lock = pushop.remote.lock()
100 lock = pushop.remote.lock()
102 try:
101 try:
103 # discovery
102 _pushdiscovery(pushop)
104 fci = discovery.findcommonincoming
105 commoninc = fci(unfi, pushop.remote, force=pushop.force)
106 common, inc, remoteheads = commoninc
107 fco = discovery.findcommonoutgoing
108 outgoing = fco(unfi, pushop.remote, onlyheads=pushop.revs,
109 commoninc=commoninc, force=pushop.force)
110 pushop.outgoing = outgoing
111 pushop.remoteheads = remoteheads
112 pushop.incoming = inc
113
114 if _pushcheckoutgoing(pushop):
103 if _pushcheckoutgoing(pushop):
115 _pushchangeset(pushop)
104 _pushchangeset(pushop)
116 _pushsyncphase(pushop)
105 _pushsyncphase(pushop)
117 _pushobsolete(pushop)
106 _pushobsolete(pushop)
118 finally:
107 finally:
119 if lock is not None:
108 if lock is not None:
120 lock.release()
109 lock.release()
121 finally:
110 finally:
122 if locallock is not None:
111 if locallock is not None:
123 locallock.release()
112 locallock.release()
124
113
125 _pushbookmark(pushop)
114 _pushbookmark(pushop)
126 return pushop.ret
115 return pushop.ret
127
116
117 def _pushdiscovery(pushop):
118 # discovery
119 unfi = pushop.repo.unfiltered()
120 fci = discovery.findcommonincoming
121 commoninc = fci(unfi, pushop.remote, force=pushop.force)
122 common, inc, remoteheads = commoninc
123 fco = discovery.findcommonoutgoing
124 outgoing = fco(unfi, pushop.remote, onlyheads=pushop.revs,
125 commoninc=commoninc, force=pushop.force)
126 pushop.outgoing = outgoing
127 pushop.remoteheads = remoteheads
128 pushop.incoming = inc
129
128 def _pushcheckoutgoing(pushop):
130 def _pushcheckoutgoing(pushop):
129 outgoing = pushop.outgoing
131 outgoing = pushop.outgoing
130 unfi = pushop.repo.unfiltered()
132 unfi = pushop.repo.unfiltered()
131 if not outgoing.missing:
133 if not outgoing.missing:
132 # nothing to push
134 # nothing to push
133 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
135 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
134 return False
136 return False
135 # something to push
137 # something to push
136 if not pushop.force:
138 if not pushop.force:
137 # if repo.obsstore == False --> no obsolete
139 # if repo.obsstore == False --> no obsolete
138 # then, save the iteration
140 # then, save the iteration
139 if unfi.obsstore:
141 if unfi.obsstore:
140 # this message are here for 80 char limit reason
142 # this message are here for 80 char limit reason
141 mso = _("push includes obsolete changeset: %s!")
143 mso = _("push includes obsolete changeset: %s!")
142 mst = "push includes %s changeset: %s!"
144 mst = "push includes %s changeset: %s!"
143 # plain versions for i18n tool to detect them
145 # plain versions for i18n tool to detect them
144 _("push includes unstable changeset: %s!")
146 _("push includes unstable changeset: %s!")
145 _("push includes bumped changeset: %s!")
147 _("push includes bumped changeset: %s!")
146 _("push includes divergent changeset: %s!")
148 _("push includes divergent changeset: %s!")
147 # If we are to push if there is at least one
149 # If we are to push if there is at least one
148 # obsolete or unstable changeset in missing, at
150 # obsolete or unstable changeset in missing, at
149 # least one of the missinghead will be obsolete or
151 # least one of the missinghead will be obsolete or
150 # unstable. So checking heads only is ok
152 # unstable. So checking heads only is ok
151 for node in outgoing.missingheads:
153 for node in outgoing.missingheads:
152 ctx = unfi[node]
154 ctx = unfi[node]
153 if ctx.obsolete():
155 if ctx.obsolete():
154 raise util.Abort(mso % ctx)
156 raise util.Abort(mso % ctx)
155 elif ctx.troubled():
157 elif ctx.troubled():
156 raise util.Abort(_(mst)
158 raise util.Abort(_(mst)
157 % (ctx.troubles()[0],
159 % (ctx.troubles()[0],
158 ctx))
160 ctx))
159 newbm = pushop.ui.configlist('bookmarks', 'pushing')
161 newbm = pushop.ui.configlist('bookmarks', 'pushing')
160 discovery.checkheads(unfi, pushop.remote, outgoing,
162 discovery.checkheads(unfi, pushop.remote, outgoing,
161 pushop.remoteheads,
163 pushop.remoteheads,
162 pushop.newbranch,
164 pushop.newbranch,
163 bool(pushop.incoming),
165 bool(pushop.incoming),
164 newbm)
166 newbm)
165 return True
167 return True
166
168
167 def _pushchangeset(pushop):
169 def _pushchangeset(pushop):
168 """Make the actual push of changeset bundle to remote repo"""
170 """Make the actual push of changeset bundle to remote repo"""
169 outgoing = pushop.outgoing
171 outgoing = pushop.outgoing
170 unbundle = pushop.remote.capable('unbundle')
172 unbundle = pushop.remote.capable('unbundle')
171 # TODO: get bundlecaps from remote
173 # TODO: get bundlecaps from remote
172 bundlecaps = None
174 bundlecaps = None
173 # create a changegroup from local
175 # create a changegroup from local
174 if pushop.revs is None and not (outgoing.excluded
176 if pushop.revs is None and not (outgoing.excluded
175 or pushop.repo.changelog.filteredrevs):
177 or pushop.repo.changelog.filteredrevs):
176 # push everything,
178 # push everything,
177 # use the fast path, no race possible on push
179 # use the fast path, no race possible on push
178 bundler = changegroup.bundle10(pushop.repo, bundlecaps)
180 bundler = changegroup.bundle10(pushop.repo, bundlecaps)
179 cg = pushop.repo._changegroupsubset(outgoing,
181 cg = pushop.repo._changegroupsubset(outgoing,
180 bundler,
182 bundler,
181 'push',
183 'push',
182 fastpath=True)
184 fastpath=True)
183 else:
185 else:
184 cg = pushop.repo.getlocalbundle('push', outgoing, bundlecaps)
186 cg = pushop.repo.getlocalbundle('push', outgoing, bundlecaps)
185
187
186 # apply changegroup to remote
188 # apply changegroup to remote
187 if unbundle:
189 if unbundle:
188 # local repo finds heads on server, finds out what
190 # local repo finds heads on server, finds out what
189 # revs it must push. once revs transferred, if server
191 # revs it must push. once revs transferred, if server
190 # finds it has different heads (someone else won
192 # finds it has different heads (someone else won
191 # commit/push race), server aborts.
193 # commit/push race), server aborts.
192 if pushop.force:
194 if pushop.force:
193 remoteheads = ['force']
195 remoteheads = ['force']
194 else:
196 else:
195 remoteheads = pushop.remoteheads
197 remoteheads = pushop.remoteheads
196 # ssh: return remote's addchangegroup()
198 # ssh: return remote's addchangegroup()
197 # http: return remote's addchangegroup() or 0 for error
199 # http: return remote's addchangegroup() or 0 for error
198 pushop.ret = pushop.remote.unbundle(cg, remoteheads,
200 pushop.ret = pushop.remote.unbundle(cg, remoteheads,
199 'push')
201 'push')
200 else:
202 else:
201 # we return an integer indicating remote head count
203 # we return an integer indicating remote head count
202 # change
204 # change
203 pushop.ret = pushop.remote.addchangegroup(cg, 'push',
205 pushop.ret = pushop.remote.addchangegroup(cg, 'push',
204 pushop.repo.url())
206 pushop.repo.url())
205
207
206 def _pushsyncphase(pushop):
208 def _pushsyncphase(pushop):
207 """synchronise phase information locally and remotly"""
209 """synchronise phase information locally and remotly"""
208 unfi = pushop.repo.unfiltered()
210 unfi = pushop.repo.unfiltered()
209 if pushop.ret:
211 if pushop.ret:
210 # push succeed, synchronize target of the push
212 # push succeed, synchronize target of the push
211 cheads = pushop.outgoing.missingheads
213 cheads = pushop.outgoing.missingheads
212 elif pushop.revs is None:
214 elif pushop.revs is None:
213 # All out push fails. synchronize all common
215 # All out push fails. synchronize all common
214 cheads = pushop.outgoing.commonheads
216 cheads = pushop.outgoing.commonheads
215 else:
217 else:
216 # I want cheads = heads(::missingheads and ::commonheads)
218 # I want cheads = heads(::missingheads and ::commonheads)
217 # (missingheads is revs with secret changeset filtered out)
219 # (missingheads is revs with secret changeset filtered out)
218 #
220 #
219 # This can be expressed as:
221 # This can be expressed as:
220 # cheads = ( (missingheads and ::commonheads)
222 # cheads = ( (missingheads and ::commonheads)
221 # + (commonheads and ::missingheads))"
223 # + (commonheads and ::missingheads))"
222 # )
224 # )
223 #
225 #
224 # while trying to push we already computed the following:
226 # while trying to push we already computed the following:
225 # common = (::commonheads)
227 # common = (::commonheads)
226 # missing = ((commonheads::missingheads) - commonheads)
228 # missing = ((commonheads::missingheads) - commonheads)
227 #
229 #
228 # We can pick:
230 # We can pick:
229 # * missingheads part of common (::commonheads)
231 # * missingheads part of common (::commonheads)
230 common = set(pushop.outgoing.common)
232 common = set(pushop.outgoing.common)
231 nm = pushop.repo.changelog.nodemap
233 nm = pushop.repo.changelog.nodemap
232 cheads = [node for node in pushop.revs if nm[node] in common]
234 cheads = [node for node in pushop.revs if nm[node] in common]
233 # and
235 # and
234 # * commonheads parents on missing
236 # * commonheads parents on missing
235 revset = unfi.set('%ln and parents(roots(%ln))',
237 revset = unfi.set('%ln and parents(roots(%ln))',
236 pushop.outgoing.commonheads,
238 pushop.outgoing.commonheads,
237 pushop.outgoing.missing)
239 pushop.outgoing.missing)
238 cheads.extend(c.node() for c in revset)
240 cheads.extend(c.node() for c in revset)
239 # even when we don't push, exchanging phase data is useful
241 # even when we don't push, exchanging phase data is useful
240 remotephases = pushop.remote.listkeys('phases')
242 remotephases = pushop.remote.listkeys('phases')
241 if (pushop.ui.configbool('ui', '_usedassubrepo', False)
243 if (pushop.ui.configbool('ui', '_usedassubrepo', False)
242 and remotephases # server supports phases
244 and remotephases # server supports phases
243 and pushop.ret is None # nothing was pushed
245 and pushop.ret is None # nothing was pushed
244 and remotephases.get('publishing', False)):
246 and remotephases.get('publishing', False)):
245 # When:
247 # When:
246 # - this is a subrepo push
248 # - this is a subrepo push
247 # - and remote support phase
249 # - and remote support phase
248 # - and no changeset was pushed
250 # - and no changeset was pushed
249 # - and remote is publishing
251 # - and remote is publishing
250 # We may be in issue 3871 case!
252 # We may be in issue 3871 case!
251 # We drop the possible phase synchronisation done by
253 # We drop the possible phase synchronisation done by
252 # courtesy to publish changesets possibly locally draft
254 # courtesy to publish changesets possibly locally draft
253 # on the remote.
255 # on the remote.
254 remotephases = {'publishing': 'True'}
256 remotephases = {'publishing': 'True'}
255 if not remotephases: # old server or public only rer
257 if not remotephases: # old server or public only rer
256 _localphasemove(pushop, cheads)
258 _localphasemove(pushop, cheads)
257 # don't push any phase data as there is nothing to push
259 # don't push any phase data as there is nothing to push
258 else:
260 else:
259 ana = phases.analyzeremotephases(pushop.repo, cheads,
261 ana = phases.analyzeremotephases(pushop.repo, cheads,
260 remotephases)
262 remotephases)
261 pheads, droots = ana
263 pheads, droots = ana
262 ### Apply remote phase on local
264 ### Apply remote phase on local
263 if remotephases.get('publishing', False):
265 if remotephases.get('publishing', False):
264 _localphasemove(pushop, cheads)
266 _localphasemove(pushop, cheads)
265 else: # publish = False
267 else: # publish = False
266 _localphasemove(pushop, pheads)
268 _localphasemove(pushop, pheads)
267 _localphasemove(pushop, cheads, phases.draft)
269 _localphasemove(pushop, cheads, phases.draft)
268 ### Apply local phase on remote
270 ### Apply local phase on remote
269
271
270 # Get the list of all revs draft on remote by public here.
272 # Get the list of all revs draft on remote by public here.
271 # XXX Beware that revset break if droots is not strictly
273 # XXX Beware that revset break if droots is not strictly
272 # XXX root we may want to ensure it is but it is costly
274 # XXX root we may want to ensure it is but it is costly
273 outdated = unfi.set('heads((%ln::%ln) and public())',
275 outdated = unfi.set('heads((%ln::%ln) and public())',
274 droots, cheads)
276 droots, cheads)
275 for newremotehead in outdated:
277 for newremotehead in outdated:
276 r = pushop.remote.pushkey('phases',
278 r = pushop.remote.pushkey('phases',
277 newremotehead.hex(),
279 newremotehead.hex(),
278 str(phases.draft),
280 str(phases.draft),
279 str(phases.public))
281 str(phases.public))
280 if not r:
282 if not r:
281 pushop.ui.warn(_('updating %s to public failed!\n')
283 pushop.ui.warn(_('updating %s to public failed!\n')
282 % newremotehead)
284 % newremotehead)
283
285
284 def _localphasemove(pushop, nodes, phase=phases.public):
286 def _localphasemove(pushop, nodes, phase=phases.public):
285 """move <nodes> to <phase> in the local source repo"""
287 """move <nodes> to <phase> in the local source repo"""
286 if pushop.locallocked:
288 if pushop.locallocked:
287 phases.advanceboundary(pushop.repo, phase, nodes)
289 phases.advanceboundary(pushop.repo, phase, nodes)
288 else:
290 else:
289 # repo is not locked, do not change any phases!
291 # repo is not locked, do not change any phases!
290 # Informs the user that phases should have been moved when
292 # Informs the user that phases should have been moved when
291 # applicable.
293 # applicable.
292 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
294 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
293 phasestr = phases.phasenames[phase]
295 phasestr = phases.phasenames[phase]
294 if actualmoves:
296 if actualmoves:
295 pushop.ui.status(_('cannot lock source repo, skipping '
297 pushop.ui.status(_('cannot lock source repo, skipping '
296 'local %s phase update\n') % phasestr)
298 'local %s phase update\n') % phasestr)
297
299
298 def _pushobsolete(pushop):
300 def _pushobsolete(pushop):
299 """utility function to push obsolete markers to a remote"""
301 """utility function to push obsolete markers to a remote"""
300 pushop.ui.debug('try to push obsolete markers to remote\n')
302 pushop.ui.debug('try to push obsolete markers to remote\n')
301 repo = pushop.repo
303 repo = pushop.repo
302 remote = pushop.remote
304 remote = pushop.remote
303 if (obsolete._enabled and repo.obsstore and
305 if (obsolete._enabled and repo.obsstore and
304 'obsolete' in remote.listkeys('namespaces')):
306 'obsolete' in remote.listkeys('namespaces')):
305 rslts = []
307 rslts = []
306 remotedata = repo.listkeys('obsolete')
308 remotedata = repo.listkeys('obsolete')
307 for key in sorted(remotedata, reverse=True):
309 for key in sorted(remotedata, reverse=True):
308 # reverse sort to ensure we end with dump0
310 # reverse sort to ensure we end with dump0
309 data = remotedata[key]
311 data = remotedata[key]
310 rslts.append(remote.pushkey('obsolete', key, '', data))
312 rslts.append(remote.pushkey('obsolete', key, '', data))
311 if [r for r in rslts if not r]:
313 if [r for r in rslts if not r]:
312 msg = _('failed to push some obsolete markers!\n')
314 msg = _('failed to push some obsolete markers!\n')
313 repo.ui.warn(msg)
315 repo.ui.warn(msg)
314
316
315 def _pushbookmark(pushop):
317 def _pushbookmark(pushop):
316 """Update bookmark position on remote"""
318 """Update bookmark position on remote"""
317 ui = pushop.ui
319 ui = pushop.ui
318 repo = pushop.repo.unfiltered()
320 repo = pushop.repo.unfiltered()
319 remote = pushop.remote
321 remote = pushop.remote
320 ui.debug("checking for updated bookmarks\n")
322 ui.debug("checking for updated bookmarks\n")
321 revnums = map(repo.changelog.rev, pushop.revs or [])
323 revnums = map(repo.changelog.rev, pushop.revs or [])
322 ancestors = [a for a in repo.changelog.ancestors(revnums, inclusive=True)]
324 ancestors = [a for a in repo.changelog.ancestors(revnums, inclusive=True)]
323 (addsrc, adddst, advsrc, advdst, diverge, differ, invalid
325 (addsrc, adddst, advsrc, advdst, diverge, differ, invalid
324 ) = bookmarks.compare(repo, repo._bookmarks, remote.listkeys('bookmarks'),
326 ) = bookmarks.compare(repo, repo._bookmarks, remote.listkeys('bookmarks'),
325 srchex=hex)
327 srchex=hex)
326
328
327 for b, scid, dcid in advsrc:
329 for b, scid, dcid in advsrc:
328 if ancestors and repo[scid].rev() not in ancestors:
330 if ancestors and repo[scid].rev() not in ancestors:
329 continue
331 continue
330 if remote.pushkey('bookmarks', b, dcid, scid):
332 if remote.pushkey('bookmarks', b, dcid, scid):
331 ui.status(_("updating bookmark %s\n") % b)
333 ui.status(_("updating bookmark %s\n") % b)
332 else:
334 else:
333 ui.warn(_('updating bookmark %s failed!\n') % b)
335 ui.warn(_('updating bookmark %s failed!\n') % b)
General Comments 0
You need to be logged in to leave comments. Login now