##// END OF EJS Templates
path: use `get_unique_pull_path_obj` in `hg relink`...
marmoute -
r50625:c6ae9051 default
parent child Browse files
Show More
@@ -1,214 +1,214 b''
1 # Mercurial extension to provide 'hg relink' command
1 # Mercurial extension to provide 'hg relink' command
2 #
2 #
3 # Copyright (C) 2007 Brendan Cully <brendan@kublai.com>
3 # Copyright (C) 2007 Brendan Cully <brendan@kublai.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 """recreates hardlinks between repository clones"""
8 """recreates hardlinks between repository clones"""
9
9
10 import os
10 import os
11 import stat
11 import stat
12
12
13 from mercurial.i18n import _
13 from mercurial.i18n import _
14 from mercurial.pycompat import open
14 from mercurial.pycompat import open
15 from mercurial import (
15 from mercurial import (
16 error,
16 error,
17 hg,
17 hg,
18 registrar,
18 registrar,
19 util,
19 util,
20 )
20 )
21 from mercurial.utils import (
21 from mercurial.utils import (
22 stringutil,
22 stringutil,
23 urlutil,
23 urlutil,
24 )
24 )
25
25
26 cmdtable = {}
26 cmdtable = {}
27 command = registrar.command(cmdtable)
27 command = registrar.command(cmdtable)
28 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
28 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
29 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
29 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
30 # be specifying the version(s) of Mercurial they are tested with, or
30 # be specifying the version(s) of Mercurial they are tested with, or
31 # leave the attribute unspecified.
31 # leave the attribute unspecified.
32 testedwith = b'ships-with-hg-core'
32 testedwith = b'ships-with-hg-core'
33
33
34
34
35 @command(
35 @command(
36 b'relink', [], _(b'[ORIGIN]'), helpcategory=command.CATEGORY_MAINTENANCE
36 b'relink', [], _(b'[ORIGIN]'), helpcategory=command.CATEGORY_MAINTENANCE
37 )
37 )
38 def relink(ui, repo, origin=None, **opts):
38 def relink(ui, repo, origin=None, **opts):
39 """recreate hardlinks between two repositories
39 """recreate hardlinks between two repositories
40
40
41 When repositories are cloned locally, their data files will be
41 When repositories are cloned locally, their data files will be
42 hardlinked so that they only use the space of a single repository.
42 hardlinked so that they only use the space of a single repository.
43
43
44 Unfortunately, subsequent pulls into either repository will break
44 Unfortunately, subsequent pulls into either repository will break
45 hardlinks for any files touched by the new changesets, even if
45 hardlinks for any files touched by the new changesets, even if
46 both repositories end up pulling the same changes.
46 both repositories end up pulling the same changes.
47
47
48 Similarly, passing --rev to "hg clone" will fail to use any
48 Similarly, passing --rev to "hg clone" will fail to use any
49 hardlinks, falling back to a complete copy of the source
49 hardlinks, falling back to a complete copy of the source
50 repository.
50 repository.
51
51
52 This command lets you recreate those hardlinks and reclaim that
52 This command lets you recreate those hardlinks and reclaim that
53 wasted space.
53 wasted space.
54
54
55 This repository will be relinked to share space with ORIGIN, which
55 This repository will be relinked to share space with ORIGIN, which
56 must be on the same local disk. If ORIGIN is omitted, looks for
56 must be on the same local disk. If ORIGIN is omitted, looks for
57 "default-relink", then "default", in [paths].
57 "default-relink", then "default", in [paths].
58
58
59 Do not attempt any read operations on this repository while the
59 Do not attempt any read operations on this repository while the
60 command is running. (Both repositories will be locked against
60 command is running. (Both repositories will be locked against
61 writes.)
61 writes.)
62 """
62 """
63 if not util.safehasattr(util, b'samefile') or not util.safehasattr(
63 if not util.safehasattr(util, b'samefile') or not util.safehasattr(
64 util, b'samedevice'
64 util, b'samedevice'
65 ):
65 ):
66 raise error.Abort(_(b'hardlinks are not supported on this system'))
66 raise error.Abort(_(b'hardlinks are not supported on this system'))
67
67
68 if origin is None and b'default-relink' in ui.paths:
68 if origin is None and b'default-relink' in ui.paths:
69 origin = b'default-relink'
69 origin = b'default-relink'
70 path, __ = urlutil.get_unique_pull_path(b'relink', repo, ui, origin)
70 path = urlutil.get_unique_pull_path_obj(b'relink', ui, origin)
71 src = hg.repository(repo.baseui, path)
71 src = hg.repository(repo.baseui, path.loc)
72 ui.status(_(b'relinking %s to %s\n') % (src.store.path, repo.store.path))
72 ui.status(_(b'relinking %s to %s\n') % (src.store.path, repo.store.path))
73 if repo.root == src.root:
73 if repo.root == src.root:
74 ui.status(_(b'there is nothing to relink\n'))
74 ui.status(_(b'there is nothing to relink\n'))
75 return
75 return
76
76
77 if not util.samedevice(src.store.path, repo.store.path):
77 if not util.samedevice(src.store.path, repo.store.path):
78 # No point in continuing
78 # No point in continuing
79 raise error.Abort(_(b'source and destination are on different devices'))
79 raise error.Abort(_(b'source and destination are on different devices'))
80
80
81 with repo.lock(), src.lock():
81 with repo.lock(), src.lock():
82 candidates = sorted(collect(src, ui))
82 candidates = sorted(collect(src, ui))
83 targets = prune(candidates, src.store.path, repo.store.path, ui)
83 targets = prune(candidates, src.store.path, repo.store.path, ui)
84 do_relink(src.store.path, repo.store.path, targets, ui)
84 do_relink(src.store.path, repo.store.path, targets, ui)
85
85
86
86
87 def collect(src, ui):
87 def collect(src, ui):
88 seplen = len(os.path.sep)
88 seplen = len(os.path.sep)
89 candidates = []
89 candidates = []
90 live = len(src[b'tip'].manifest())
90 live = len(src[b'tip'].manifest())
91 # Your average repository has some files which were deleted before
91 # Your average repository has some files which were deleted before
92 # the tip revision. We account for that by assuming that there are
92 # the tip revision. We account for that by assuming that there are
93 # 3 tracked files for every 2 live files as of the tip version of
93 # 3 tracked files for every 2 live files as of the tip version of
94 # the repository.
94 # the repository.
95 #
95 #
96 # mozilla-central as of 2010-06-10 had a ratio of just over 7:5.
96 # mozilla-central as of 2010-06-10 had a ratio of just over 7:5.
97 total = live * 3 // 2
97 total = live * 3 // 2
98 src = src.store.path
98 src = src.store.path
99 progress = ui.makeprogress(_(b'collecting'), unit=_(b'files'), total=total)
99 progress = ui.makeprogress(_(b'collecting'), unit=_(b'files'), total=total)
100 pos = 0
100 pos = 0
101 ui.status(
101 ui.status(
102 _(b"tip has %d files, estimated total number of files: %d\n")
102 _(b"tip has %d files, estimated total number of files: %d\n")
103 % (live, total)
103 % (live, total)
104 )
104 )
105 for dirpath, dirnames, filenames in os.walk(src):
105 for dirpath, dirnames, filenames in os.walk(src):
106 dirnames.sort()
106 dirnames.sort()
107 relpath = dirpath[len(src) + seplen :]
107 relpath = dirpath[len(src) + seplen :]
108 for filename in sorted(filenames):
108 for filename in sorted(filenames):
109 if filename[-2:] not in (b'.d', b'.i'):
109 if filename[-2:] not in (b'.d', b'.i'):
110 continue
110 continue
111 st = os.stat(os.path.join(dirpath, filename))
111 st = os.stat(os.path.join(dirpath, filename))
112 if not stat.S_ISREG(st.st_mode):
112 if not stat.S_ISREG(st.st_mode):
113 continue
113 continue
114 pos += 1
114 pos += 1
115 candidates.append((os.path.join(relpath, filename), st))
115 candidates.append((os.path.join(relpath, filename), st))
116 progress.update(pos, item=filename)
116 progress.update(pos, item=filename)
117
117
118 progress.complete()
118 progress.complete()
119 ui.status(_(b'collected %d candidate storage files\n') % len(candidates))
119 ui.status(_(b'collected %d candidate storage files\n') % len(candidates))
120 return candidates
120 return candidates
121
121
122
122
123 def prune(candidates, src, dst, ui):
123 def prune(candidates, src, dst, ui):
124 def linkfilter(src, dst, st):
124 def linkfilter(src, dst, st):
125 try:
125 try:
126 ts = os.stat(dst)
126 ts = os.stat(dst)
127 except OSError:
127 except OSError:
128 # Destination doesn't have this file?
128 # Destination doesn't have this file?
129 return False
129 return False
130 if util.samefile(src, dst):
130 if util.samefile(src, dst):
131 return False
131 return False
132 if not util.samedevice(src, dst):
132 if not util.samedevice(src, dst):
133 # No point in continuing
133 # No point in continuing
134 raise error.Abort(
134 raise error.Abort(
135 _(b'source and destination are on different devices')
135 _(b'source and destination are on different devices')
136 )
136 )
137 if st.st_size != ts.st_size:
137 if st.st_size != ts.st_size:
138 return False
138 return False
139 return st
139 return st
140
140
141 targets = []
141 targets = []
142 progress = ui.makeprogress(
142 progress = ui.makeprogress(
143 _(b'pruning'), unit=_(b'files'), total=len(candidates)
143 _(b'pruning'), unit=_(b'files'), total=len(candidates)
144 )
144 )
145 pos = 0
145 pos = 0
146 for fn, st in candidates:
146 for fn, st in candidates:
147 pos += 1
147 pos += 1
148 srcpath = os.path.join(src, fn)
148 srcpath = os.path.join(src, fn)
149 tgt = os.path.join(dst, fn)
149 tgt = os.path.join(dst, fn)
150 ts = linkfilter(srcpath, tgt, st)
150 ts = linkfilter(srcpath, tgt, st)
151 if not ts:
151 if not ts:
152 ui.debug(b'not linkable: %s\n' % fn)
152 ui.debug(b'not linkable: %s\n' % fn)
153 continue
153 continue
154 targets.append((fn, ts.st_size))
154 targets.append((fn, ts.st_size))
155 progress.update(pos, item=fn)
155 progress.update(pos, item=fn)
156
156
157 progress.complete()
157 progress.complete()
158 ui.status(
158 ui.status(
159 _(b'pruned down to %d probably relinkable files\n') % len(targets)
159 _(b'pruned down to %d probably relinkable files\n') % len(targets)
160 )
160 )
161 return targets
161 return targets
162
162
163
163
164 def do_relink(src, dst, files, ui):
164 def do_relink(src, dst, files, ui):
165 def relinkfile(src, dst):
165 def relinkfile(src, dst):
166 bak = dst + b'.bak'
166 bak = dst + b'.bak'
167 os.rename(dst, bak)
167 os.rename(dst, bak)
168 try:
168 try:
169 util.oslink(src, dst)
169 util.oslink(src, dst)
170 except OSError:
170 except OSError:
171 os.rename(bak, dst)
171 os.rename(bak, dst)
172 raise
172 raise
173 os.remove(bak)
173 os.remove(bak)
174
174
175 CHUNKLEN = 65536
175 CHUNKLEN = 65536
176 relinked = 0
176 relinked = 0
177 savedbytes = 0
177 savedbytes = 0
178
178
179 progress = ui.makeprogress(
179 progress = ui.makeprogress(
180 _(b'relinking'), unit=_(b'files'), total=len(files)
180 _(b'relinking'), unit=_(b'files'), total=len(files)
181 )
181 )
182 pos = 0
182 pos = 0
183 for f, sz in files:
183 for f, sz in files:
184 pos += 1
184 pos += 1
185 source = os.path.join(src, f)
185 source = os.path.join(src, f)
186 tgt = os.path.join(dst, f)
186 tgt = os.path.join(dst, f)
187 # Binary mode, so that read() works correctly, especially on Windows
187 # Binary mode, so that read() works correctly, especially on Windows
188 sfp = open(source, b'rb')
188 sfp = open(source, b'rb')
189 dfp = open(tgt, b'rb')
189 dfp = open(tgt, b'rb')
190 sin = sfp.read(CHUNKLEN)
190 sin = sfp.read(CHUNKLEN)
191 while sin:
191 while sin:
192 din = dfp.read(CHUNKLEN)
192 din = dfp.read(CHUNKLEN)
193 if sin != din:
193 if sin != din:
194 break
194 break
195 sin = sfp.read(CHUNKLEN)
195 sin = sfp.read(CHUNKLEN)
196 sfp.close()
196 sfp.close()
197 dfp.close()
197 dfp.close()
198 if sin:
198 if sin:
199 ui.debug(b'not linkable: %s\n' % f)
199 ui.debug(b'not linkable: %s\n' % f)
200 continue
200 continue
201 try:
201 try:
202 relinkfile(source, tgt)
202 relinkfile(source, tgt)
203 progress.update(pos, item=f)
203 progress.update(pos, item=f)
204 relinked += 1
204 relinked += 1
205 savedbytes += sz
205 savedbytes += sz
206 except OSError as inst:
206 except OSError as inst:
207 ui.warn(b'%s: %s\n' % (tgt, stringutil.forcebytestr(inst)))
207 ui.warn(b'%s: %s\n' % (tgt, stringutil.forcebytestr(inst)))
208
208
209 progress.complete()
209 progress.complete()
210
210
211 ui.status(
211 ui.status(
212 _(b'relinked %d files (%s reclaimed)\n')
212 _(b'relinked %d files (%s reclaimed)\n')
213 % (relinked, util.bytecount(savedbytes))
213 % (relinked, util.bytecount(savedbytes))
214 )
214 )
General Comments 0
You need to be logged in to leave comments. Login now