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