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