Show More
@@ -34,6 +34,8 b' def relink(ui, repo, origin=None, **opts' | |||||
34 | Do not attempt any read operations on this repository while the command is |
|
34 | Do not attempt any read operations on this repository while the command is | |
35 | running. (Both repositories will be locked against writes.) |
|
35 | running. (Both repositories will be locked against writes.) | |
36 | """ |
|
36 | """ | |
|
37 | if not hasattr(util, 'samefile') or not hasattr(util, 'samedevice'): | |||
|
38 | raise util.Abort(_('hardlinks are not supported on this system')) | |||
37 | src = hg.repository( |
|
39 | src = hg.repository( | |
38 | cmdutil.remoteui(repo, opts), |
|
40 | cmdutil.remoteui(repo, opts), | |
39 | ui.expandpath(origin or 'default-relink', origin or 'default')) |
|
41 | ui.expandpath(origin or 'default-relink', origin or 'default')) | |
@@ -45,7 +47,7 b' def relink(ui, repo, origin=None, **opts' | |||||
45 | remotelock = src.lock() |
|
47 | remotelock = src.lock() | |
46 | try: |
|
48 | try: | |
47 | candidates = collect(src.store.path, ui) |
|
49 | candidates = collect(src.store.path, ui) | |
48 | targets = prune(candidates, repo.store.path, ui) |
|
50 | targets = prune(candidates, src.store.path, repo.store.path, ui) | |
49 | do_relink(src.store.path, repo.store.path, targets, ui) |
|
51 | do_relink(src.store.path, repo.store.path, targets, ui) | |
50 | finally: |
|
52 | finally: | |
51 | remotelock.release() |
|
53 | remotelock.release() | |
@@ -68,16 +70,16 b' def collect(src, ui):' | |||||
68 | ui.status(_('collected %d candidate storage files\n') % len(candidates)) |
|
70 | ui.status(_('collected %d candidate storage files\n') % len(candidates)) | |
69 | return candidates |
|
71 | return candidates | |
70 |
|
72 | |||
71 | def prune(candidates, dst, ui): |
|
73 | def prune(candidates, src, dst, ui): | |
72 | def linkfilter(dst, st): |
|
74 | def linkfilter(src, dst, st): | |
73 | try: |
|
75 | try: | |
74 | ts = os.stat(dst) |
|
76 | ts = os.stat(dst) | |
75 | except OSError: |
|
77 | except OSError: | |
76 | # Destination doesn't have this file? |
|
78 | # Destination doesn't have this file? | |
77 | return False |
|
79 | return False | |
78 | if st.st_ino == ts.st_ino: |
|
80 | if util.samefile(src, dst): | |
79 | return False |
|
81 | return False | |
80 | if st.st_dev != ts.st_dev: |
|
82 | if not util.samedevice(src, dst): | |
81 | # No point in continuing |
|
83 | # No point in continuing | |
82 | raise util.Abort( |
|
84 | raise util.Abort( | |
83 | _('source and destination are on different devices')) |
|
85 | _('source and destination are on different devices')) | |
@@ -87,8 +89,9 b' def prune(candidates, dst, ui):' | |||||
87 |
|
89 | |||
88 | targets = [] |
|
90 | targets = [] | |
89 | for fn, st in candidates: |
|
91 | for fn, st in candidates: | |
|
92 | srcpath = os.path.join(src, fn) | |||
90 | tgt = os.path.join(dst, fn) |
|
93 | tgt = os.path.join(dst, fn) | |
91 | ts = linkfilter(tgt, st) |
|
94 | ts = linkfilter(srcpath, tgt, st) | |
92 | if not ts: |
|
95 | if not ts: | |
93 | ui.debug(_('not linkable: %s\n') % fn) |
|
96 | ui.debug(_('not linkable: %s\n') % fn) | |
94 | continue |
|
97 | continue | |
@@ -102,7 +105,7 b' def do_relink(src, dst, files, ui):' | |||||
102 | bak = dst + '.bak' |
|
105 | bak = dst + '.bak' | |
103 | os.rename(dst, bak) |
|
106 | os.rename(dst, bak) | |
104 | try: |
|
107 | try: | |
105 |
|
|
108 | util.os_link(src, dst) | |
106 | except OSError: |
|
109 | except OSError: | |
107 | os.rename(bak, dst) |
|
110 | os.rename(bak, dst) | |
108 | raise |
|
111 | raise | |
@@ -118,14 +121,17 b' def do_relink(src, dst, files, ui):' | |||||
118 | pos += 1 |
|
121 | pos += 1 | |
119 | source = os.path.join(src, f) |
|
122 | source = os.path.join(src, f) | |
120 | tgt = os.path.join(dst, f) |
|
123 | tgt = os.path.join(dst, f) | |
121 | sfp = file(source) |
|
124 | # Binary mode, so that read() works correctly, especially on Windows | |
122 |
|
|
125 | sfp = file(source, 'rb') | |
|
126 | dfp = file(tgt, 'rb') | |||
123 | sin = sfp.read(CHUNKLEN) |
|
127 | sin = sfp.read(CHUNKLEN) | |
124 | while sin: |
|
128 | while sin: | |
125 | din = dfp.read(CHUNKLEN) |
|
129 | din = dfp.read(CHUNKLEN) | |
126 | if sin != din: |
|
130 | if sin != din: | |
127 | break |
|
131 | break | |
128 | sin = sfp.read(CHUNKLEN) |
|
132 | sin = sfp.read(CHUNKLEN) | |
|
133 | sfp.close() | |||
|
134 | dfp.close() | |||
129 | if sin: |
|
135 | if sin: | |
130 | ui.debug(_('not linkable: %s\n') % f) |
|
136 | ui.debug(_('not linkable: %s\n') % f) | |
131 | continue |
|
137 | continue |
@@ -105,6 +105,18 b' def pconvert(path):' | |||||
105 | def localpath(path): |
|
105 | def localpath(path): | |
106 | return path |
|
106 | return path | |
107 |
|
107 | |||
|
108 | def samefile(fpath1, fpath2): | |||
|
109 | """Returns whether path1 and path2 refer to the same file. This is only | |||
|
110 | guaranteed to work for files, not directories.""" | |||
|
111 | return os.path.samefile(fpath1, fpath2) | |||
|
112 | ||||
|
113 | def samedevice(fpath1, fpath2): | |||
|
114 | """Returns whether fpath1 and fpath2 are on the same device. This is only | |||
|
115 | guaranteed to work for files, not directories.""" | |||
|
116 | st1 = os.lstat(fpath1) | |||
|
117 | st2 = os.lstat(fpath2) | |||
|
118 | return st1.st_dev == st2.st_dev | |||
|
119 | ||||
108 | if sys.platform == 'darwin': |
|
120 | if sys.platform == 'darwin': | |
109 | def realpath(path): |
|
121 | def realpath(path): | |
110 | ''' |
|
122 | ''' |
@@ -37,7 +37,7 b' def os_link(src, dst):' | |||||
37 | except NotImplementedError: # Another fake error win Win98 |
|
37 | except NotImplementedError: # Another fake error win Win98 | |
38 | raise OSError(errno.EINVAL, 'Hardlinking not supported') |
|
38 | raise OSError(errno.EINVAL, 'Hardlinking not supported') | |
39 |
|
39 | |||
40 |
def |
|
40 | def _getfileinfo(pathname): | |
41 | """Return number of hardlinks for the given file.""" |
|
41 | """Return number of hardlinks for the given file.""" | |
42 | try: |
|
42 | try: | |
43 | fh = win32file.CreateFile(pathname, |
|
43 | fh = win32file.CreateFile(pathname, | |
@@ -45,10 +45,39 b' def nlinks(pathname):' | |||||
45 | None, win32file.OPEN_EXISTING, 0, None) |
|
45 | None, win32file.OPEN_EXISTING, 0, None) | |
46 | res = win32file.GetFileInformationByHandle(fh) |
|
46 | res = win32file.GetFileInformationByHandle(fh) | |
47 | fh.Close() |
|
47 | fh.Close() | |
48 |
return res |
|
48 | return res | |
49 | except pywintypes.error: |
|
49 | except pywintypes.error: | |
|
50 | return None | |||
|
51 | ||||
|
52 | def nlinks(pathname): | |||
|
53 | """Return number of hardlinks for the given file.""" | |||
|
54 | res = _getfileinfo(pathname) | |||
|
55 | if res is not None: | |||
|
56 | return res[7] | |||
|
57 | else: | |||
50 | return os.lstat(pathname).st_nlink |
|
58 | return os.lstat(pathname).st_nlink | |
51 |
|
59 | |||
|
60 | def samefile(fpath1, fpath2): | |||
|
61 | """Returns whether fpath1 and fpath2 refer to the same file. This is only | |||
|
62 | guaranteed to work for files, not directories.""" | |||
|
63 | res1 = _getfileinfo(fpath1) | |||
|
64 | res2 = _getfileinfo(fpath2) | |||
|
65 | if res1 is not None and res2 is not None: | |||
|
66 | # Index 4 is the volume serial number, and 8 and 9 contain the file ID | |||
|
67 | return res1[4] == res2[4] and res1[8] == res2[8] and res1[9] == res2[9] | |||
|
68 | else: | |||
|
69 | return False | |||
|
70 | ||||
|
71 | def samedevice(fpath1, fpath2): | |||
|
72 | """Returns whether fpath1 and fpath2 are on the same device. This is only | |||
|
73 | guaranteed to work for files, not directories.""" | |||
|
74 | res1 = _getfileinfo(fpath1) | |||
|
75 | res2 = _getfileinfo(fpath2) | |||
|
76 | if res1 is not None and res2 is not None: | |||
|
77 | return res1[4] == res2[4] | |||
|
78 | else: | |||
|
79 | return False | |||
|
80 | ||||
52 | def testpid(pid): |
|
81 | def testpid(pid): | |
53 | '''return True if pid is still running or unable to |
|
82 | '''return True if pid is still running or unable to | |
54 | determine, False otherwise''' |
|
83 | determine, False otherwise''' |
@@ -3,10 +3,16 b'' | |||||
3 | echo "[extensions]" >> $HGRCPATH |
|
3 | echo "[extensions]" >> $HGRCPATH | |
4 | echo "relink=" >> $HGRCPATH |
|
4 | echo "relink=" >> $HGRCPATH | |
5 |
|
5 | |||
|
6 | fix_path() | |||
|
7 | { | |||
|
8 | tr '\\' / | |||
|
9 | } | |||
|
10 | ||||
6 | cat > arelinked.py <<EOF |
|
11 | cat > arelinked.py <<EOF | |
7 | import sys, os |
|
12 | import sys, os | |
|
13 | from mercurial import util | |||
8 | path1, path2 = sys.argv[1:3] |
|
14 | path1, path2 = sys.argv[1:3] | |
9 | if os.stat(path1).st_ino == os.stat(path2).st_ino: |
|
15 | if util.samefile(path1, path2): | |
10 | print '%s == %s' % (path1, path2) |
|
16 | print '%s == %s' % (path1, path2) | |
11 | else: |
|
17 | else: | |
12 | print '%s != %s' % (path1, path2) |
|
18 | print '%s != %s' % (path1, path2) | |
@@ -23,6 +29,8 b' hg ci -Am addfile' | |||||
23 | echo a >> a |
|
29 | echo a >> a | |
24 | echo a >> b |
|
30 | echo a >> b | |
25 | hg ci -Am changefiles |
|
31 | hg ci -Am changefiles | |
|
32 | # Test files are read in binary mode | |||
|
33 | python -c "file('.hg/store/data/dummy.i', 'wb').write('a\r\nb\n')" | |||
26 | cd .. |
|
34 | cd .. | |
27 |
|
35 | |||
28 | echo '% clone and pull to break links' |
|
36 | echo '% clone and pull to break links' | |
@@ -33,9 +41,11 b" echo 'username= A. Baz <a.baz@bar.com>' " | |||||
33 | hg pull -q |
|
41 | hg pull -q | |
34 | echo b >> b |
|
42 | echo b >> b | |
35 | hg ci -m changeb |
|
43 | hg ci -m changeb | |
|
44 | python -c "file('.hg/store/data/dummy.i', 'wb').write('a\nb\r\n')" | |||
36 |
|
45 | |||
37 | echo '% relink' |
|
46 | echo '% relink' | |
38 | hg relink --debug | sed 's:relinking.*store:relinking .hg/store:g' |
|
47 | hg relink --debug | sed 's:relinking.*store:relinking .hg/store:g' \ | |
|
48 | | fix_path | |||
39 | cd .. |
|
49 | cd .. | |
40 |
|
50 | |||
41 | echo '% check hardlinks' |
|
51 | echo '% check hardlinks' |
@@ -12,12 +12,13 b' 2 files updated, 0 files merged, 0 files' | |||||
12 | created new head |
|
12 | created new head | |
13 | % relink |
|
13 | % relink | |
14 | relinking .hg/store |
|
14 | relinking .hg/store | |
15 |
collected |
|
15 | collected 5 candidate storage files | |
16 | not linkable: 00changelog.i |
|
16 | not linkable: 00changelog.i | |
17 | not linkable: 00manifest.i |
|
17 | not linkable: 00manifest.i | |
18 | not linkable: data/b.i |
|
18 | not linkable: data/b.i | |
19 |
pruned down to |
|
19 | pruned down to 2 probably relinkable files | |
20 |
relink: data/a.i 1/ |
|
20 | relink: data/a.i 1/2 files ( 50%) | |
|
21 | not linkable: data/dummy.i | |||
21 | relinked 1 files (136 bytes reclaimed) |
|
22 | relinked 1 files (136 bytes reclaimed) | |
22 | % check hardlinks |
|
23 | % check hardlinks | |
23 | repo/.hg/store/data/a.i == clone/.hg/store/data/a.i |
|
24 | repo/.hg/store/data/a.i == clone/.hg/store/data/a.i |
General Comments 0
You need to be logged in to leave comments.
Login now