##// END OF EJS Templates
share: allow dest to default to the basename of source
Matt Mackall -
r8807:8bf6eb68 default
parent child Browse files
Show More
@@ -1,31 +1,31
1 # Mercurial extension to provide the 'hg share' command
1 # Mercurial extension to provide the 'hg share' command
2 #
2 #
3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2006, 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, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 import os
8 import os
9 from mercurial.i18n import _
9 from mercurial.i18n import _
10 from mercurial import hg, commands
10 from mercurial import hg, commands
11
11
12 def share(ui, source, dest, noupdate=False):
12 def share(ui, source, dest=None, noupdate=False):
13 """create a new shared repository (experimental)
13 """create a new shared repository (experimental)
14
14
15 Initialize a new repository and working directory that shares its
15 Initialize a new repository and working directory that shares its
16 history with another repository.
16 history with another repository.
17
17
18 NOTE: actions that change history such as rollback or moving the
18 NOTE: actions that change history such as rollback or moving the
19 source may confuse sharers.
19 source may confuse sharers.
20 """
20 """
21
21
22 return hg.share(ui, source, dest, not noupdate)
22 return hg.share(ui, source, dest, not noupdate)
23
23
24 cmdtable = {
24 cmdtable = {
25 "share":
25 "share":
26 (share,
26 (share,
27 [('U', 'noupdate', None, _('do not create a working copy'))],
27 [('U', 'noupdate', None, _('do not create a working copy'))],
28 _('[-U] SOURCE DEST')),
28 _('[-U] SOURCE [DEST]')),
29 }
29 }
30
30
31 commands.norepo += " share"
31 commands.norepo += " share"
@@ -1,357 +1,360
1 # hg.py - repository classes for mercurial
1 # hg.py - repository classes for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2, incorporated herein by reference.
7 # GNU General Public License version 2, incorporated herein by reference.
8
8
9 from i18n import _
9 from i18n import _
10 from lock import release
10 from lock import release
11 import localrepo, bundlerepo, httprepo, sshrepo, statichttprepo
11 import localrepo, bundlerepo, httprepo, sshrepo, statichttprepo
12 import lock, util, extensions, error
12 import lock, util, extensions, error
13 import merge as _merge
13 import merge as _merge
14 import verify as _verify
14 import verify as _verify
15 import errno, os, shutil
15 import errno, os, shutil
16
16
17 def _local(path):
17 def _local(path):
18 return (os.path.isfile(util.drop_scheme('file', path)) and
18 return (os.path.isfile(util.drop_scheme('file', path)) and
19 bundlerepo or localrepo)
19 bundlerepo or localrepo)
20
20
21 def parseurl(url, revs=[]):
21 def parseurl(url, revs=[]):
22 '''parse url#branch, returning url, branch + revs'''
22 '''parse url#branch, returning url, branch + revs'''
23
23
24 if '#' not in url:
24 if '#' not in url:
25 return url, (revs or None), revs and revs[-1] or None
25 return url, (revs or None), revs and revs[-1] or None
26
26
27 url, branch = url.split('#', 1)
27 url, branch = url.split('#', 1)
28 checkout = revs and revs[-1] or branch
28 checkout = revs and revs[-1] or branch
29 return url, (revs or []) + [branch], checkout
29 return url, (revs or []) + [branch], checkout
30
30
31 schemes = {
31 schemes = {
32 'bundle': bundlerepo,
32 'bundle': bundlerepo,
33 'file': _local,
33 'file': _local,
34 'http': httprepo,
34 'http': httprepo,
35 'https': httprepo,
35 'https': httprepo,
36 'ssh': sshrepo,
36 'ssh': sshrepo,
37 'static-http': statichttprepo,
37 'static-http': statichttprepo,
38 }
38 }
39
39
40 def _lookup(path):
40 def _lookup(path):
41 scheme = 'file'
41 scheme = 'file'
42 if path:
42 if path:
43 c = path.find(':')
43 c = path.find(':')
44 if c > 0:
44 if c > 0:
45 scheme = path[:c]
45 scheme = path[:c]
46 thing = schemes.get(scheme) or schemes['file']
46 thing = schemes.get(scheme) or schemes['file']
47 try:
47 try:
48 return thing(path)
48 return thing(path)
49 except TypeError:
49 except TypeError:
50 return thing
50 return thing
51
51
52 def islocal(repo):
52 def islocal(repo):
53 '''return true if repo or path is local'''
53 '''return true if repo or path is local'''
54 if isinstance(repo, str):
54 if isinstance(repo, str):
55 try:
55 try:
56 return _lookup(repo).islocal(repo)
56 return _lookup(repo).islocal(repo)
57 except AttributeError:
57 except AttributeError:
58 return False
58 return False
59 return repo.local()
59 return repo.local()
60
60
61 def repository(ui, path='', create=False):
61 def repository(ui, path='', create=False):
62 """return a repository object for the specified path"""
62 """return a repository object for the specified path"""
63 repo = _lookup(path).instance(ui, path, create)
63 repo = _lookup(path).instance(ui, path, create)
64 ui = getattr(repo, "ui", ui)
64 ui = getattr(repo, "ui", ui)
65 for name, module in extensions.extensions():
65 for name, module in extensions.extensions():
66 hook = getattr(module, 'reposetup', None)
66 hook = getattr(module, 'reposetup', None)
67 if hook:
67 if hook:
68 hook(ui, repo)
68 hook(ui, repo)
69 return repo
69 return repo
70
70
71 def defaultdest(source):
71 def defaultdest(source):
72 '''return default destination of clone if none is given'''
72 '''return default destination of clone if none is given'''
73 return os.path.basename(os.path.normpath(source))
73 return os.path.basename(os.path.normpath(source))
74
74
75 def localpath(path):
75 def localpath(path):
76 if path.startswith('file://localhost/'):
76 if path.startswith('file://localhost/'):
77 return path[16:]
77 return path[16:]
78 if path.startswith('file://'):
78 if path.startswith('file://'):
79 return path[7:]
79 return path[7:]
80 if path.startswith('file:'):
80 if path.startswith('file:'):
81 return path[5:]
81 return path[5:]
82 return path
82 return path
83
83
84 def share(ui, source, dest, update=True):
84 def share(ui, source, dest=None, update=True):
85 '''create a shared repository'''
85 '''create a shared repository'''
86
86
87 if not islocal(source):
87 if not islocal(source):
88 raise util.Abort(_('can only share local repositories'))
88 raise util.Abort(_('can only share local repositories'))
89
89
90 if not dest:
91 dest = os.path.basename(source)
92
90 if isinstance(source, str):
93 if isinstance(source, str):
91 origsource = ui.expandpath(source)
94 origsource = ui.expandpath(source)
92 source, rev, checkout = parseurl(origsource, '')
95 source, rev, checkout = parseurl(origsource, '')
93 srcrepo = repository(ui, source)
96 srcrepo = repository(ui, source)
94 else:
97 else:
95 srcrepo = source
98 srcrepo = source
96 origsource = source = srcrepo.url()
99 origsource = source = srcrepo.url()
97 checkout = None
100 checkout = None
98
101
99 sharedpath = srcrepo.sharedpath # if our source is already sharing
102 sharedpath = srcrepo.sharedpath # if our source is already sharing
100
103
101 root = os.path.realpath(dest)
104 root = os.path.realpath(dest)
102 roothg = os.path.join(root, '.hg')
105 roothg = os.path.join(root, '.hg')
103
106
104 if os.path.exists(roothg):
107 if os.path.exists(roothg):
105 raise util.Abort(_('destination already exists'))
108 raise util.Abort(_('destination already exists'))
106
109
107 if not os.path.isdir(root):
110 if not os.path.isdir(root):
108 os.mkdir(root)
111 os.mkdir(root)
109 os.mkdir(roothg)
112 os.mkdir(roothg)
110
113
111 requirements = ''
114 requirements = ''
112 try:
115 try:
113 requirements = srcrepo.opener('requires').read()
116 requirements = srcrepo.opener('requires').read()
114 except IOError, inst:
117 except IOError, inst:
115 if inst.errno != errno.ENOENT:
118 if inst.errno != errno.ENOENT:
116 raise
119 raise
117
120
118 requirements += 'shared\n'
121 requirements += 'shared\n'
119 file(os.path.join(roothg, 'requires'), 'w').write(requirements)
122 file(os.path.join(roothg, 'requires'), 'w').write(requirements)
120 file(os.path.join(roothg, 'sharedpath'), 'w').write(sharedpath)
123 file(os.path.join(roothg, 'sharedpath'), 'w').write(sharedpath)
121
124
122 default = srcrepo.ui.config('paths', 'default')
125 default = srcrepo.ui.config('paths', 'default')
123 if default:
126 if default:
124 f = file(os.path.join(roothg, 'hgrc'), 'w')
127 f = file(os.path.join(roothg, 'hgrc'), 'w')
125 f.write('[paths]\ndefault = %s\n' % default)
128 f.write('[paths]\ndefault = %s\n' % default)
126 f.close()
129 f.close()
127
130
128 r = repository(ui, root)
131 r = repository(ui, root)
129
132
130 if update:
133 if update:
131 r.ui.status(_("updating working directory\n"))
134 r.ui.status(_("updating working directory\n"))
132 if update is not True:
135 if update is not True:
133 checkout = update
136 checkout = update
134 for test in (checkout, 'default', 'tip'):
137 for test in (checkout, 'default', 'tip'):
135 try:
138 try:
136 uprev = r.lookup(test)
139 uprev = r.lookup(test)
137 break
140 break
138 except:
141 except:
139 continue
142 continue
140 _update(r, uprev)
143 _update(r, uprev)
141
144
142 def clone(ui, source, dest=None, pull=False, rev=None, update=True,
145 def clone(ui, source, dest=None, pull=False, rev=None, update=True,
143 stream=False):
146 stream=False):
144 """Make a copy of an existing repository.
147 """Make a copy of an existing repository.
145
148
146 Create a copy of an existing repository in a new directory. The
149 Create a copy of an existing repository in a new directory. The
147 source and destination are URLs, as passed to the repository
150 source and destination are URLs, as passed to the repository
148 function. Returns a pair of repository objects, the source and
151 function. Returns a pair of repository objects, the source and
149 newly created destination.
152 newly created destination.
150
153
151 The location of the source is added to the new repository's
154 The location of the source is added to the new repository's
152 .hg/hgrc file, as the default to be used for future pulls and
155 .hg/hgrc file, as the default to be used for future pulls and
153 pushes.
156 pushes.
154
157
155 If an exception is raised, the partly cloned/updated destination
158 If an exception is raised, the partly cloned/updated destination
156 repository will be deleted.
159 repository will be deleted.
157
160
158 Arguments:
161 Arguments:
159
162
160 source: repository object or URL
163 source: repository object or URL
161
164
162 dest: URL of destination repository to create (defaults to base
165 dest: URL of destination repository to create (defaults to base
163 name of source repository)
166 name of source repository)
164
167
165 pull: always pull from source repository, even in local case
168 pull: always pull from source repository, even in local case
166
169
167 stream: stream raw data uncompressed from repository (fast over
170 stream: stream raw data uncompressed from repository (fast over
168 LAN, slow over WAN)
171 LAN, slow over WAN)
169
172
170 rev: revision to clone up to (implies pull=True)
173 rev: revision to clone up to (implies pull=True)
171
174
172 update: update working directory after clone completes, if
175 update: update working directory after clone completes, if
173 destination is local repository (True means update to default rev,
176 destination is local repository (True means update to default rev,
174 anything else is treated as a revision)
177 anything else is treated as a revision)
175 """
178 """
176
179
177 if isinstance(source, str):
180 if isinstance(source, str):
178 origsource = ui.expandpath(source)
181 origsource = ui.expandpath(source)
179 source, rev, checkout = parseurl(origsource, rev)
182 source, rev, checkout = parseurl(origsource, rev)
180 src_repo = repository(ui, source)
183 src_repo = repository(ui, source)
181 else:
184 else:
182 src_repo = source
185 src_repo = source
183 origsource = source = src_repo.url()
186 origsource = source = src_repo.url()
184 checkout = rev and rev[-1] or None
187 checkout = rev and rev[-1] or None
185
188
186 if dest is None:
189 if dest is None:
187 dest = defaultdest(source)
190 dest = defaultdest(source)
188 ui.status(_("destination directory: %s\n") % dest)
191 ui.status(_("destination directory: %s\n") % dest)
189
192
190 dest = localpath(dest)
193 dest = localpath(dest)
191 source = localpath(source)
194 source = localpath(source)
192
195
193 if os.path.exists(dest):
196 if os.path.exists(dest):
194 if not os.path.isdir(dest):
197 if not os.path.isdir(dest):
195 raise util.Abort(_("destination '%s' already exists") % dest)
198 raise util.Abort(_("destination '%s' already exists") % dest)
196 elif os.listdir(dest):
199 elif os.listdir(dest):
197 raise util.Abort(_("destination '%s' is not empty") % dest)
200 raise util.Abort(_("destination '%s' is not empty") % dest)
198
201
199 class DirCleanup(object):
202 class DirCleanup(object):
200 def __init__(self, dir_):
203 def __init__(self, dir_):
201 self.rmtree = shutil.rmtree
204 self.rmtree = shutil.rmtree
202 self.dir_ = dir_
205 self.dir_ = dir_
203 def close(self):
206 def close(self):
204 self.dir_ = None
207 self.dir_ = None
205 def cleanup(self):
208 def cleanup(self):
206 if self.dir_:
209 if self.dir_:
207 self.rmtree(self.dir_, True)
210 self.rmtree(self.dir_, True)
208
211
209 src_lock = dest_lock = dir_cleanup = None
212 src_lock = dest_lock = dir_cleanup = None
210 try:
213 try:
211 if islocal(dest):
214 if islocal(dest):
212 dir_cleanup = DirCleanup(dest)
215 dir_cleanup = DirCleanup(dest)
213
216
214 abspath = origsource
217 abspath = origsource
215 copy = False
218 copy = False
216 if src_repo.cancopy() and islocal(dest):
219 if src_repo.cancopy() and islocal(dest):
217 abspath = os.path.abspath(util.drop_scheme('file', origsource))
220 abspath = os.path.abspath(util.drop_scheme('file', origsource))
218 copy = not pull and not rev
221 copy = not pull and not rev
219
222
220 if copy:
223 if copy:
221 try:
224 try:
222 # we use a lock here because if we race with commit, we
225 # we use a lock here because if we race with commit, we
223 # can end up with extra data in the cloned revlogs that's
226 # can end up with extra data in the cloned revlogs that's
224 # not pointed to by changesets, thus causing verify to
227 # not pointed to by changesets, thus causing verify to
225 # fail
228 # fail
226 src_lock = src_repo.lock(wait=False)
229 src_lock = src_repo.lock(wait=False)
227 except error.LockError:
230 except error.LockError:
228 copy = False
231 copy = False
229
232
230 if copy:
233 if copy:
231 hgdir = os.path.realpath(os.path.join(dest, ".hg"))
234 hgdir = os.path.realpath(os.path.join(dest, ".hg"))
232 if not os.path.exists(dest):
235 if not os.path.exists(dest):
233 os.mkdir(dest)
236 os.mkdir(dest)
234 else:
237 else:
235 # only clean up directories we create ourselves
238 # only clean up directories we create ourselves
236 dir_cleanup.dir_ = hgdir
239 dir_cleanup.dir_ = hgdir
237 try:
240 try:
238 dest_path = hgdir
241 dest_path = hgdir
239 os.mkdir(dest_path)
242 os.mkdir(dest_path)
240 except OSError, inst:
243 except OSError, inst:
241 if inst.errno == errno.EEXIST:
244 if inst.errno == errno.EEXIST:
242 dir_cleanup.close()
245 dir_cleanup.close()
243 raise util.Abort(_("destination '%s' already exists")
246 raise util.Abort(_("destination '%s' already exists")
244 % dest)
247 % dest)
245 raise
248 raise
246
249
247 for f in src_repo.store.copylist():
250 for f in src_repo.store.copylist():
248 src = os.path.join(src_repo.path, f)
251 src = os.path.join(src_repo.path, f)
249 dst = os.path.join(dest_path, f)
252 dst = os.path.join(dest_path, f)
250 dstbase = os.path.dirname(dst)
253 dstbase = os.path.dirname(dst)
251 if dstbase and not os.path.exists(dstbase):
254 if dstbase and not os.path.exists(dstbase):
252 os.mkdir(dstbase)
255 os.mkdir(dstbase)
253 if os.path.exists(src):
256 if os.path.exists(src):
254 if dst.endswith('data'):
257 if dst.endswith('data'):
255 # lock to avoid premature writing to the target
258 # lock to avoid premature writing to the target
256 dest_lock = lock.lock(os.path.join(dstbase, "lock"))
259 dest_lock = lock.lock(os.path.join(dstbase, "lock"))
257 util.copyfiles(src, dst)
260 util.copyfiles(src, dst)
258
261
259 # we need to re-init the repo after manually copying the data
262 # we need to re-init the repo after manually copying the data
260 # into it
263 # into it
261 dest_repo = repository(ui, dest)
264 dest_repo = repository(ui, dest)
262
265
263 else:
266 else:
264 try:
267 try:
265 dest_repo = repository(ui, dest, create=True)
268 dest_repo = repository(ui, dest, create=True)
266 except OSError, inst:
269 except OSError, inst:
267 if inst.errno == errno.EEXIST:
270 if inst.errno == errno.EEXIST:
268 dir_cleanup.close()
271 dir_cleanup.close()
269 raise util.Abort(_("destination '%s' already exists")
272 raise util.Abort(_("destination '%s' already exists")
270 % dest)
273 % dest)
271 raise
274 raise
272
275
273 revs = None
276 revs = None
274 if rev:
277 if rev:
275 if 'lookup' not in src_repo.capabilities:
278 if 'lookup' not in src_repo.capabilities:
276 raise util.Abort(_("src repository does not support revision "
279 raise util.Abort(_("src repository does not support revision "
277 "lookup and so doesn't support clone by "
280 "lookup and so doesn't support clone by "
278 "revision"))
281 "revision"))
279 revs = [src_repo.lookup(r) for r in rev]
282 revs = [src_repo.lookup(r) for r in rev]
280 checkout = revs[0]
283 checkout = revs[0]
281 if dest_repo.local():
284 if dest_repo.local():
282 dest_repo.clone(src_repo, heads=revs, stream=stream)
285 dest_repo.clone(src_repo, heads=revs, stream=stream)
283 elif src_repo.local():
286 elif src_repo.local():
284 src_repo.push(dest_repo, revs=revs)
287 src_repo.push(dest_repo, revs=revs)
285 else:
288 else:
286 raise util.Abort(_("clone from remote to remote not supported"))
289 raise util.Abort(_("clone from remote to remote not supported"))
287
290
288 if dir_cleanup:
291 if dir_cleanup:
289 dir_cleanup.close()
292 dir_cleanup.close()
290
293
291 if dest_repo.local():
294 if dest_repo.local():
292 fp = dest_repo.opener("hgrc", "w", text=True)
295 fp = dest_repo.opener("hgrc", "w", text=True)
293 fp.write("[paths]\n")
296 fp.write("[paths]\n")
294 fp.write("default = %s\n" % abspath)
297 fp.write("default = %s\n" % abspath)
295 fp.close()
298 fp.close()
296
299
297 if update:
300 if update:
298 dest_repo.ui.status(_("updating working directory\n"))
301 dest_repo.ui.status(_("updating working directory\n"))
299 if update is not True:
302 if update is not True:
300 checkout = update
303 checkout = update
301 for test in (checkout, 'default', 'tip'):
304 for test in (checkout, 'default', 'tip'):
302 try:
305 try:
303 uprev = dest_repo.lookup(test)
306 uprev = dest_repo.lookup(test)
304 break
307 break
305 except:
308 except:
306 continue
309 continue
307 _update(dest_repo, uprev)
310 _update(dest_repo, uprev)
308
311
309 return src_repo, dest_repo
312 return src_repo, dest_repo
310 finally:
313 finally:
311 release(src_lock, dest_lock)
314 release(src_lock, dest_lock)
312 if dir_cleanup is not None:
315 if dir_cleanup is not None:
313 dir_cleanup.cleanup()
316 dir_cleanup.cleanup()
314
317
315 def _showstats(repo, stats):
318 def _showstats(repo, stats):
316 stats = ((stats[0], _("updated")),
319 stats = ((stats[0], _("updated")),
317 (stats[1], _("merged")),
320 (stats[1], _("merged")),
318 (stats[2], _("removed")),
321 (stats[2], _("removed")),
319 (stats[3], _("unresolved")))
322 (stats[3], _("unresolved")))
320 note = ", ".join([_("%d files %s") % s for s in stats])
323 note = ", ".join([_("%d files %s") % s for s in stats])
321 repo.ui.status("%s\n" % note)
324 repo.ui.status("%s\n" % note)
322
325
323 def update(repo, node):
326 def update(repo, node):
324 """update the working directory to node, merging linear changes"""
327 """update the working directory to node, merging linear changes"""
325 stats = _merge.update(repo, node, False, False, None)
328 stats = _merge.update(repo, node, False, False, None)
326 _showstats(repo, stats)
329 _showstats(repo, stats)
327 if stats[3]:
330 if stats[3]:
328 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges\n"))
331 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges\n"))
329 return stats[3] > 0
332 return stats[3] > 0
330
333
331 # naming conflict in clone()
334 # naming conflict in clone()
332 _update = update
335 _update = update
333
336
334 def clean(repo, node, show_stats=True):
337 def clean(repo, node, show_stats=True):
335 """forcibly switch the working directory to node, clobbering changes"""
338 """forcibly switch the working directory to node, clobbering changes"""
336 stats = _merge.update(repo, node, False, True, None)
339 stats = _merge.update(repo, node, False, True, None)
337 if show_stats: _showstats(repo, stats)
340 if show_stats: _showstats(repo, stats)
338 return stats[3] > 0
341 return stats[3] > 0
339
342
340 def merge(repo, node, force=None, remind=True):
343 def merge(repo, node, force=None, remind=True):
341 """branch merge with node, resolving changes"""
344 """branch merge with node, resolving changes"""
342 stats = _merge.update(repo, node, True, force, False)
345 stats = _merge.update(repo, node, True, force, False)
343 _showstats(repo, stats)
346 _showstats(repo, stats)
344 if stats[3]:
347 if stats[3]:
345 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges "
348 repo.ui.status(_("use 'hg resolve' to retry unresolved file merges "
346 "or 'hg up --clean' to abandon\n"))
349 "or 'hg up --clean' to abandon\n"))
347 elif remind:
350 elif remind:
348 repo.ui.status(_("(branch merge, don't forget to commit)\n"))
351 repo.ui.status(_("(branch merge, don't forget to commit)\n"))
349 return stats[3] > 0
352 return stats[3] > 0
350
353
351 def revert(repo, node, choose):
354 def revert(repo, node, choose):
352 """revert changes to revision in node without updating dirstate"""
355 """revert changes to revision in node without updating dirstate"""
353 return _merge.update(repo, node, False, True, choose)[3] > 0
356 return _merge.update(repo, node, False, True, choose)[3] > 0
354
357
355 def verify(repo):
358 def verify(repo):
356 """verify the consistency of a repository"""
359 """verify the consistency of a repository"""
357 return _verify.verify(repo)
360 return _verify.verify(repo)
General Comments 0
You need to be logged in to leave comments. Login now