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