##// END OF EJS Templates
relink/progress: correctly handle the no file to relink case
Benoit Boissinot -
r11052:d6094402 stable
parent child Browse files
Show More
@@ -1,159 +1,159 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 from mercurial import cmdutil, hg, util
10 from mercurial import cmdutil, hg, util
11 from mercurial.i18n import _
11 from mercurial.i18n import _
12 import os, stat
12 import os, stat
13
13
14 def relink(ui, repo, origin=None, **opts):
14 def relink(ui, repo, origin=None, **opts):
15 """recreate hardlinks between two repositories
15 """recreate hardlinks between two repositories
16
16
17 When repositories are cloned locally, their data files will be
17 When repositories are cloned locally, their data files will be
18 hardlinked so that they only use the space of a single repository.
18 hardlinked so that they only use the space of a single repository.
19
19
20 Unfortunately, subsequent pulls into either repository will break
20 Unfortunately, subsequent pulls into either repository will break
21 hardlinks for any files touched by the new changesets, even if
21 hardlinks for any files touched by the new changesets, even if
22 both repositories end up pulling the same changes.
22 both repositories end up pulling the same changes.
23
23
24 Similarly, passing --rev to "hg clone" will fail to use any
24 Similarly, passing --rev to "hg clone" will fail to use any
25 hardlinks, falling back to a complete copy of the source
25 hardlinks, falling back to a complete copy of the source
26 repository.
26 repository.
27
27
28 This command lets you recreate those hardlinks and reclaim that
28 This command lets you recreate those hardlinks and reclaim that
29 wasted space.
29 wasted space.
30
30
31 This repository will be relinked to share space with ORIGIN, which
31 This repository will be relinked to share space with ORIGIN, which
32 must be on the same local disk. If ORIGIN is omitted, looks for
32 must be on the same local disk. If ORIGIN is omitted, looks for
33 "default-relink", then "default", in [paths].
33 "default-relink", then "default", in [paths].
34
34
35 Do not attempt any read operations on this repository while the
35 Do not attempt any read operations on this repository while the
36 command is running. (Both repositories will be locked against
36 command is running. (Both repositories will be locked against
37 writes.)
37 writes.)
38 """
38 """
39 if not hasattr(util, 'samefile') or not hasattr(util, 'samedevice'):
39 if not hasattr(util, 'samefile') or not hasattr(util, 'samedevice'):
40 raise util.Abort(_('hardlinks are not supported on this system'))
40 raise util.Abort(_('hardlinks are not supported on this system'))
41 src = hg.repository(
41 src = hg.repository(
42 cmdutil.remoteui(repo, opts),
42 cmdutil.remoteui(repo, opts),
43 ui.expandpath(origin or 'default-relink', origin or 'default'))
43 ui.expandpath(origin or 'default-relink', origin or 'default'))
44 if not src.local():
44 if not src.local():
45 raise util.Abort('must specify local origin repository')
45 raise util.Abort('must specify local origin repository')
46 ui.status(_('relinking %s to %s\n') % (src.store.path, repo.store.path))
46 ui.status(_('relinking %s to %s\n') % (src.store.path, repo.store.path))
47 locallock = repo.lock()
47 locallock = repo.lock()
48 try:
48 try:
49 remotelock = src.lock()
49 remotelock = src.lock()
50 try:
50 try:
51 candidates = sorted(collect(src.store.path, ui))
51 candidates = sorted(collect(src.store.path, ui))
52 targets = prune(candidates, src.store.path, repo.store.path, ui)
52 targets = prune(candidates, src.store.path, repo.store.path, ui)
53 do_relink(src.store.path, repo.store.path, targets, ui)
53 do_relink(src.store.path, repo.store.path, targets, ui)
54 finally:
54 finally:
55 remotelock.release()
55 remotelock.release()
56 finally:
56 finally:
57 locallock.release()
57 locallock.release()
58
58
59 def collect(src, ui):
59 def collect(src, ui):
60 seplen = len(os.path.sep)
60 seplen = len(os.path.sep)
61 candidates = []
61 candidates = []
62 for dirpath, dirnames, filenames in os.walk(src):
62 for dirpath, dirnames, filenames in os.walk(src):
63 relpath = dirpath[len(src) + seplen:]
63 relpath = dirpath[len(src) + seplen:]
64 for filename in filenames:
64 for filename in filenames:
65 if not filename[-2:] in ('.d', '.i'):
65 if not filename[-2:] in ('.d', '.i'):
66 continue
66 continue
67 st = os.stat(os.path.join(dirpath, filename))
67 st = os.stat(os.path.join(dirpath, filename))
68 if not stat.S_ISREG(st.st_mode):
68 if not stat.S_ISREG(st.st_mode):
69 continue
69 continue
70 candidates.append((os.path.join(relpath, filename), st))
70 candidates.append((os.path.join(relpath, filename), st))
71
71
72 ui.status(_('collected %d candidate storage files\n') % len(candidates))
72 ui.status(_('collected %d candidate storage files\n') % len(candidates))
73 return candidates
73 return candidates
74
74
75 def prune(candidates, src, dst, ui):
75 def prune(candidates, src, dst, ui):
76 def linkfilter(src, dst, st):
76 def linkfilter(src, dst, st):
77 try:
77 try:
78 ts = os.stat(dst)
78 ts = os.stat(dst)
79 except OSError:
79 except OSError:
80 # Destination doesn't have this file?
80 # Destination doesn't have this file?
81 return False
81 return False
82 if util.samefile(src, dst):
82 if util.samefile(src, dst):
83 return False
83 return False
84 if not util.samedevice(src, dst):
84 if not util.samedevice(src, dst):
85 # No point in continuing
85 # No point in continuing
86 raise util.Abort(
86 raise util.Abort(
87 _('source and destination are on different devices'))
87 _('source and destination are on different devices'))
88 if st.st_size != ts.st_size:
88 if st.st_size != ts.st_size:
89 return False
89 return False
90 return st
90 return st
91
91
92 targets = []
92 targets = []
93 for fn, st in candidates:
93 for fn, st in candidates:
94 srcpath = os.path.join(src, fn)
94 srcpath = os.path.join(src, fn)
95 tgt = os.path.join(dst, fn)
95 tgt = os.path.join(dst, fn)
96 ts = linkfilter(srcpath, tgt, st)
96 ts = linkfilter(srcpath, tgt, st)
97 if not ts:
97 if not ts:
98 ui.debug(_('not linkable: %s\n') % fn)
98 ui.debug(_('not linkable: %s\n') % fn)
99 continue
99 continue
100 targets.append((fn, ts.st_size))
100 targets.append((fn, ts.st_size))
101
101
102 ui.status(_('pruned down to %d probably relinkable files\n') % len(targets))
102 ui.status(_('pruned down to %d probably relinkable files\n') % len(targets))
103 return targets
103 return targets
104
104
105 def do_relink(src, dst, files, ui):
105 def do_relink(src, dst, files, ui):
106 def relinkfile(src, dst):
106 def relinkfile(src, dst):
107 bak = dst + '.bak'
107 bak = dst + '.bak'
108 os.rename(dst, bak)
108 os.rename(dst, bak)
109 try:
109 try:
110 util.os_link(src, dst)
110 util.os_link(src, dst)
111 except OSError:
111 except OSError:
112 os.rename(bak, dst)
112 os.rename(bak, dst)
113 raise
113 raise
114 os.remove(bak)
114 os.remove(bak)
115
115
116 CHUNKLEN = 65536
116 CHUNKLEN = 65536
117 relinked = 0
117 relinked = 0
118 savedbytes = 0
118 savedbytes = 0
119
119
120 pos = 0
120 pos = 0
121 total = len(files)
121 total = len(files)
122 for f, sz in files:
122 for f, sz in files:
123 pos += 1
123 pos += 1
124 source = os.path.join(src, f)
124 source = os.path.join(src, f)
125 tgt = os.path.join(dst, f)
125 tgt = os.path.join(dst, f)
126 # Binary mode, so that read() works correctly, especially on Windows
126 # Binary mode, so that read() works correctly, especially on Windows
127 sfp = file(source, 'rb')
127 sfp = file(source, 'rb')
128 dfp = file(tgt, 'rb')
128 dfp = file(tgt, 'rb')
129 sin = sfp.read(CHUNKLEN)
129 sin = sfp.read(CHUNKLEN)
130 while sin:
130 while sin:
131 din = dfp.read(CHUNKLEN)
131 din = dfp.read(CHUNKLEN)
132 if sin != din:
132 if sin != din:
133 break
133 break
134 sin = sfp.read(CHUNKLEN)
134 sin = sfp.read(CHUNKLEN)
135 sfp.close()
135 sfp.close()
136 dfp.close()
136 dfp.close()
137 if sin:
137 if sin:
138 ui.debug(_('not linkable: %s\n') % f)
138 ui.debug(_('not linkable: %s\n') % f)
139 continue
139 continue
140 try:
140 try:
141 relinkfile(source, tgt)
141 relinkfile(source, tgt)
142 ui.progress(_('relinking'), pos, f, _(' files'), total)
142 ui.progress(_('relinking'), pos, f, _(' files'), total)
143 relinked += 1
143 relinked += 1
144 savedbytes += sz
144 savedbytes += sz
145 except OSError, inst:
145 except OSError, inst:
146 ui.warn('%s: %s\n' % (tgt, str(inst)))
146 ui.warn('%s: %s\n' % (tgt, str(inst)))
147
147
148 ui.progress(_('relinking'), None, f, _(' files'), total)
148 ui.progress(_('relinking'), None, unit=_(' files'), total=total)
149
149
150 ui.status(_('relinked %d files (%d bytes reclaimed)\n') %
150 ui.status(_('relinked %d files (%d bytes reclaimed)\n') %
151 (relinked, savedbytes))
151 (relinked, savedbytes))
152
152
153 cmdtable = {
153 cmdtable = {
154 'relink': (
154 'relink': (
155 relink,
155 relink,
156 [],
156 [],
157 _('[ORIGIN]')
157 _('[ORIGIN]')
158 )
158 )
159 }
159 }
General Comments 0
You need to be logged in to leave comments. Login now