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