Merge lp:~jelmer/brz/win-symlink-warning into lp:brz
- win-symlink-warning
- Merge into trunk
Proposed by
Jelmer Vernooij
Status: | Superseded | ||||
---|---|---|---|---|---|
Proposed branch: | lp:~jelmer/brz/win-symlink-warning | ||||
Merge into: | lp:brz | ||||
Diff against target: |
1464 lines (+487/-201) 27 files modified
breezy/bzr/dirstate.py (+15/-11) breezy/bzr/inventorytree.py (+1/-1) breezy/bzr/workingtree.py (+1/-0) breezy/bzr/workingtree_4.py (+2/-1) breezy/commit.py (+5/-0) breezy/delta.py (+7/-3) breezy/diff.py (+5/-0) breezy/errors.py (+0/-15) breezy/git/memorytree.py (+7/-0) breezy/git/tree.py (+8/-1) breezy/git/workingtree.py (+6/-8) breezy/memorytree.py (+21/-4) breezy/mutabletree.py (+25/-7) breezy/osutils.py (+55/-2) breezy/tests/per_tree/test_symlinks.py (+40/-0) breezy/tests/test_commit.py (+31/-0) breezy/tests/test_errors.py (+0/-14) breezy/tests/test_memorytree.py (+65/-70) breezy/tests/test_osutils.py (+21/-0) breezy/tests/test_transform.py (+55/-11) breezy/tests/test_workingtree.py (+33/-0) breezy/transform.py (+15/-10) breezy/transport/local.py (+1/-1) breezy/transport/memory.py (+52/-34) breezy/tree.py (+7/-1) breezy/workingtree.py (+4/-7) doc/en/release-notes/brz-3.0.txt (+5/-0) |
||||
To merge this branch: | bzr merge lp:~jelmer/brz/win-symlink-warning | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Breezy developers | Pending | ||
Review via email: mp+363899@code.launchpad.net |
This proposal has been superseded by a proposal from 2019-03-04.
Commit message
Description of the change
Allow symbolic links to exist when checking out trees on Windows.
Warnings are printed for the symbolic links, but the rest of the tree is still
checked out. Operations that work across the tree and require on-disk access
(e.g. "commit", "diff", "status") will warn about the symbolic links but not
otherwise prentend that the files are missing.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'breezy/bzr/dirstate.py' | |||
2 | --- breezy/bzr/dirstate.py 2018-11-18 19:48:57 +0000 | |||
3 | +++ breezy/bzr/dirstate.py 2019-03-04 01:57:09 +0000 | |||
4 | @@ -366,7 +366,8 @@ | |||
5 | 366 | HEADER_FORMAT_2 = b'#bazaar dirstate flat format 2\n' | 366 | HEADER_FORMAT_2 = b'#bazaar dirstate flat format 2\n' |
6 | 367 | HEADER_FORMAT_3 = b'#bazaar dirstate flat format 3\n' | 367 | HEADER_FORMAT_3 = b'#bazaar dirstate flat format 3\n' |
7 | 368 | 368 | ||
9 | 369 | def __init__(self, path, sha1_provider, worth_saving_limit=0): | 369 | def __init__(self, path, sha1_provider, worth_saving_limit=0, |
10 | 370 | use_filesystem_for_exec=True): | ||
11 | 370 | """Create a DirState object. | 371 | """Create a DirState object. |
12 | 371 | 372 | ||
13 | 372 | :param path: The path at which the dirstate file on disk should live. | 373 | :param path: The path at which the dirstate file on disk should live. |
14 | @@ -375,6 +376,8 @@ | |||
15 | 375 | entries is known, only bother saving the dirstate if more than | 376 | entries is known, only bother saving the dirstate if more than |
16 | 376 | this count of entries have changed. | 377 | this count of entries have changed. |
17 | 377 | -1 means never save hash changes, 0 means always save hash changes. | 378 | -1 means never save hash changes, 0 means always save hash changes. |
18 | 379 | :param use_filesystem_for_exec: Whether to trust the filesystem | ||
19 | 380 | for executable bit information | ||
20 | 378 | """ | 381 | """ |
21 | 379 | # _header_state and _dirblock_state represent the current state | 382 | # _header_state and _dirblock_state represent the current state |
22 | 380 | # of the dirstate metadata and the per-row data respectiely. | 383 | # of the dirstate metadata and the per-row data respectiely. |
23 | @@ -423,6 +426,7 @@ | |||
24 | 423 | self._worth_saving_limit = worth_saving_limit | 426 | self._worth_saving_limit = worth_saving_limit |
25 | 424 | self._config_stack = config.LocationStack(urlutils.local_path_to_url( | 427 | self._config_stack = config.LocationStack(urlutils.local_path_to_url( |
26 | 425 | path)) | 428 | path)) |
27 | 429 | self._use_filesystem_for_exec = use_filesystem_for_exec | ||
28 | 426 | 430 | ||
29 | 427 | def __repr__(self): | 431 | def __repr__(self): |
30 | 428 | return "%s(%r)" % \ | 432 | return "%s(%r)" % \ |
31 | @@ -1949,14 +1953,10 @@ | |||
32 | 1949 | 1953 | ||
33 | 1950 | def _is_executable(self, mode, old_executable): | 1954 | def _is_executable(self, mode, old_executable): |
34 | 1951 | """Is this file executable?""" | 1955 | """Is this file executable?""" |
43 | 1952 | return bool(S_IEXEC & mode) | 1956 | if self._use_filesystem_for_exec: |
44 | 1953 | 1957 | return bool(S_IEXEC & mode) | |
45 | 1954 | def _is_executable_win32(self, mode, old_executable): | 1958 | else: |
46 | 1955 | """On win32 the executable bit is stored in the dirstate.""" | 1959 | return old_executable |
39 | 1956 | return old_executable | ||
40 | 1957 | |||
41 | 1958 | if sys.platform == 'win32': | ||
42 | 1959 | _is_executable = _is_executable_win32 | ||
47 | 1960 | 1960 | ||
48 | 1961 | def _read_link(self, abspath, old_link): | 1961 | def _read_link(self, abspath, old_link): |
49 | 1962 | """Read the target of a symlink""" | 1962 | """Read the target of a symlink""" |
50 | @@ -2403,7 +2403,8 @@ | |||
51 | 2403 | return len(self._parents) - len(self._ghosts) | 2403 | return len(self._parents) - len(self._ghosts) |
52 | 2404 | 2404 | ||
53 | 2405 | @classmethod | 2405 | @classmethod |
55 | 2406 | def on_file(cls, path, sha1_provider=None, worth_saving_limit=0): | 2406 | def on_file(cls, path, sha1_provider=None, worth_saving_limit=0, |
56 | 2407 | use_filesystem_for_exec=True): | ||
57 | 2407 | """Construct a DirState on the file at path "path". | 2408 | """Construct a DirState on the file at path "path". |
58 | 2408 | 2409 | ||
59 | 2409 | :param path: The path at which the dirstate file on disk should live. | 2410 | :param path: The path at which the dirstate file on disk should live. |
60 | @@ -2412,12 +2413,15 @@ | |||
61 | 2412 | :param worth_saving_limit: when the exact number of hash changed | 2413 | :param worth_saving_limit: when the exact number of hash changed |
62 | 2413 | entries is known, only bother saving the dirstate if more than | 2414 | entries is known, only bother saving the dirstate if more than |
63 | 2414 | this count of entries have changed. -1 means never save. | 2415 | this count of entries have changed. -1 means never save. |
64 | 2416 | :param use_filesystem_for_exec: Whether to trust the filesystem | ||
65 | 2417 | for executable bit information | ||
66 | 2415 | :return: An unlocked DirState object, associated with the given path. | 2418 | :return: An unlocked DirState object, associated with the given path. |
67 | 2416 | """ | 2419 | """ |
68 | 2417 | if sha1_provider is None: | 2420 | if sha1_provider is None: |
69 | 2418 | sha1_provider = DefaultSHA1Provider() | 2421 | sha1_provider = DefaultSHA1Provider() |
70 | 2419 | result = cls(path, sha1_provider, | 2422 | result = cls(path, sha1_provider, |
72 | 2420 | worth_saving_limit=worth_saving_limit) | 2423 | worth_saving_limit=worth_saving_limit, |
73 | 2424 | use_filesystem_for_exec=use_filesystem_for_exec) | ||
74 | 2421 | return result | 2425 | return result |
75 | 2422 | 2426 | ||
76 | 2423 | def _read_dirblocks_if_needed(self): | 2427 | def _read_dirblocks_if_needed(self): |
77 | 2424 | 2428 | ||
78 | === modified file 'breezy/bzr/inventorytree.py' | |||
79 | --- breezy/bzr/inventorytree.py 2019-02-14 03:30:18 +0000 | |||
80 | +++ breezy/bzr/inventorytree.py 2019-03-04 01:57:09 +0000 | |||
81 | @@ -573,7 +573,7 @@ | |||
82 | 573 | # expand any symlinks in the directory part, while leaving the | 573 | # expand any symlinks in the directory part, while leaving the |
83 | 574 | # filename alone | 574 | # filename alone |
84 | 575 | # only expanding if symlinks are supported avoids windows path bugs | 575 | # only expanding if symlinks are supported avoids windows path bugs |
86 | 576 | if osutils.has_symlinks(): | 576 | if self.tree.supports_symlinks(): |
87 | 577 | file_list = list(map(osutils.normalizepath, file_list)) | 577 | file_list = list(map(osutils.normalizepath, file_list)) |
88 | 578 | 578 | ||
89 | 579 | user_dirs = {} | 579 | user_dirs = {} |
90 | 580 | 580 | ||
91 | === modified file 'breezy/bzr/workingtree.py' | |||
92 | --- breezy/bzr/workingtree.py 2018-12-11 00:51:46 +0000 | |||
93 | +++ breezy/bzr/workingtree.py 2019-03-04 01:57:09 +0000 | |||
94 | @@ -96,6 +96,7 @@ | |||
95 | 96 | # impossible as there is no clear relationship between the working tree format | 96 | # impossible as there is no clear relationship between the working tree format |
96 | 97 | # and the conflict list file format. | 97 | # and the conflict list file format. |
97 | 98 | CONFLICT_HEADER_1 = b"BZR conflict list format 1" | 98 | CONFLICT_HEADER_1 = b"BZR conflict list format 1" |
98 | 99 | ERROR_PATH_NOT_FOUND = 3 # WindowsError errno code, equivalent to ENOENT | ||
99 | 99 | 100 | ||
100 | 100 | 101 | ||
101 | 101 | class InventoryWorkingTree(WorkingTree, MutableInventoryTree): | 102 | class InventoryWorkingTree(WorkingTree, MutableInventoryTree): |
102 | 102 | 103 | ||
103 | === modified file 'breezy/bzr/workingtree_4.py' | |||
104 | --- breezy/bzr/workingtree_4.py 2019-02-04 19:39:30 +0000 | |||
105 | +++ breezy/bzr/workingtree_4.py 2019-03-04 01:57:09 +0000 | |||
106 | @@ -256,7 +256,8 @@ | |||
107 | 256 | local_path = self.controldir.get_workingtree_transport( | 256 | local_path = self.controldir.get_workingtree_transport( |
108 | 257 | None).local_abspath('dirstate') | 257 | None).local_abspath('dirstate') |
109 | 258 | self._dirstate = dirstate.DirState.on_file( | 258 | self._dirstate = dirstate.DirState.on_file( |
111 | 259 | local_path, self._sha1_provider(), self._worth_saving_limit()) | 259 | local_path, self._sha1_provider(), self._worth_saving_limit(), |
112 | 260 | self._supports_executable()) | ||
113 | 260 | return self._dirstate | 261 | return self._dirstate |
114 | 261 | 262 | ||
115 | 262 | def _sha1_provider(self): | 263 | def _sha1_provider(self): |
116 | 263 | 264 | ||
117 | === modified file 'breezy/commit.py' | |||
118 | --- breezy/commit.py 2018-11-25 20:44:56 +0000 | |||
119 | +++ breezy/commit.py 2019-03-04 01:57:09 +0000 | |||
120 | @@ -64,6 +64,7 @@ | |||
121 | 64 | StrictCommitFailed | 64 | StrictCommitFailed |
122 | 65 | ) | 65 | ) |
123 | 66 | from .osutils import (get_user_encoding, | 66 | from .osutils import (get_user_encoding, |
124 | 67 | has_symlinks, | ||
125 | 67 | is_inside_any, | 68 | is_inside_any, |
126 | 68 | minimum_path_selection, | 69 | minimum_path_selection, |
127 | 69 | ) | 70 | ) |
128 | @@ -709,6 +710,10 @@ | |||
129 | 709 | # 'missing' path | 710 | # 'missing' path |
130 | 710 | if report_changes: | 711 | if report_changes: |
131 | 711 | reporter.missing(new_path) | 712 | reporter.missing(new_path) |
132 | 713 | if change[6][0] == 'symlink' and not self.work_tree.supports_symlinks(): | ||
133 | 714 | trace.warning('Ignoring "%s" as symlinks are not ' | ||
134 | 715 | 'supported on this filesystem.' % (change[1][0],)) | ||
135 | 716 | continue | ||
136 | 712 | deleted_paths.append(change[1][1]) | 717 | deleted_paths.append(change[1][1]) |
137 | 713 | # Reset the new path (None) and new versioned flag (False) | 718 | # Reset the new path (None) and new versioned flag (False) |
138 | 714 | change = (change[0], (change[1][0], None), change[2], | 719 | change = (change[0], (change[1][0], None), change[2], |
139 | 715 | 720 | ||
140 | === modified file 'breezy/delta.py' | |||
141 | --- breezy/delta.py 2018-11-11 04:08:32 +0000 | |||
142 | +++ breezy/delta.py 2019-03-04 01:57:09 +0000 | |||
143 | @@ -18,11 +18,11 @@ | |||
144 | 18 | 18 | ||
145 | 19 | from breezy import ( | 19 | from breezy import ( |
146 | 20 | osutils, | 20 | osutils, |
147 | 21 | trace, | ||
148 | 21 | ) | 22 | ) |
149 | 22 | from .sixish import ( | 23 | from .sixish import ( |
150 | 23 | StringIO, | 24 | StringIO, |
151 | 24 | ) | 25 | ) |
152 | 25 | from .trace import is_quiet | ||
153 | 26 | 26 | ||
154 | 27 | 27 | ||
155 | 28 | class TreeDelta(object): | 28 | class TreeDelta(object): |
156 | @@ -141,7 +141,11 @@ | |||
157 | 141 | if fully_present[1] is True: | 141 | if fully_present[1] is True: |
158 | 142 | delta.added.append((path[1], file_id, kind[1])) | 142 | delta.added.append((path[1], file_id, kind[1])) |
159 | 143 | else: | 143 | else: |
161 | 144 | delta.removed.append((path[0], file_id, kind[0])) | 144 | if kind[0] == 'symlink' and not new_tree.supports_symlinks(): |
162 | 145 | trace.warning('Ignoring "%s" as symlinks ' | ||
163 | 146 | 'are not supported on this filesystem.' % (path[0],)) | ||
164 | 147 | else: | ||
165 | 148 | delta.removed.append((path[0], file_id, kind[0])) | ||
166 | 145 | elif fully_present[0] is False: | 149 | elif fully_present[0] is False: |
167 | 146 | delta.missing.append((path[1], file_id, kind[1])) | 150 | delta.missing.append((path[1], file_id, kind[1])) |
168 | 147 | elif name[0] != name[1] or parent_id[0] != parent_id[1]: | 151 | elif name[0] != name[1] or parent_id[0] != parent_id[1]: |
169 | @@ -253,7 +257,7 @@ | |||
170 | 253 | :param kind: A pair of file kinds, as generated by Tree.iter_changes. | 257 | :param kind: A pair of file kinds, as generated by Tree.iter_changes. |
171 | 254 | None indicates no file present. | 258 | None indicates no file present. |
172 | 255 | """ | 259 | """ |
174 | 256 | if is_quiet(): | 260 | if trace.is_quiet(): |
175 | 257 | return | 261 | return |
176 | 258 | if paths[1] == '' and versioned == 'added' and self.suppress_root_add: | 262 | if paths[1] == '' and versioned == 'added' and self.suppress_root_add: |
177 | 259 | return | 263 | return |
178 | 260 | 264 | ||
179 | === modified file 'breezy/diff.py' | |||
180 | --- breezy/diff.py 2018-11-25 21:48:55 +0000 | |||
181 | +++ breezy/diff.py 2019-03-04 01:57:09 +0000 | |||
182 | @@ -982,6 +982,11 @@ | |||
183 | 982 | # is, missing) in both trees are skipped as well. | 982 | # is, missing) in both trees are skipped as well. |
184 | 983 | if parent == (None, None) or kind == (None, None): | 983 | if parent == (None, None) or kind == (None, None): |
185 | 984 | continue | 984 | continue |
186 | 985 | if kind[0] == 'symlink' and not self.new_tree.supports_symlinks(): | ||
187 | 986 | warning( | ||
188 | 987 | 'Ignoring "%s" as symlinks are not ' | ||
189 | 988 | 'supported on this filesystem.' % (paths[0],)) | ||
190 | 989 | continue | ||
191 | 985 | oldpath, newpath = paths | 990 | oldpath, newpath = paths |
192 | 986 | oldpath_encoded = get_encoded_path(paths[0]) | 991 | oldpath_encoded = get_encoded_path(paths[0]) |
193 | 987 | newpath_encoded = get_encoded_path(paths[1]) | 992 | newpath_encoded = get_encoded_path(paths[1]) |
194 | 988 | 993 | ||
195 | === modified file 'breezy/errors.py' | |||
196 | --- breezy/errors.py 2018-11-11 04:08:32 +0000 | |||
197 | +++ breezy/errors.py 2019-03-04 01:57:09 +0000 | |||
198 | @@ -2303,21 +2303,6 @@ | |||
199 | 2303 | ' (See brz shelve --list).%(more)s') | 2303 | ' (See brz shelve --list).%(more)s') |
200 | 2304 | 2304 | ||
201 | 2305 | 2305 | ||
202 | 2306 | class UnableCreateSymlink(BzrError): | ||
203 | 2307 | |||
204 | 2308 | _fmt = 'Unable to create symlink %(path_str)son this platform' | ||
205 | 2309 | |||
206 | 2310 | def __init__(self, path=None): | ||
207 | 2311 | path_str = '' | ||
208 | 2312 | if path: | ||
209 | 2313 | try: | ||
210 | 2314 | path_str = repr(str(path)) | ||
211 | 2315 | except UnicodeEncodeError: | ||
212 | 2316 | path_str = repr(path) | ||
213 | 2317 | path_str += ' ' | ||
214 | 2318 | self.path_str = path_str | ||
215 | 2319 | |||
216 | 2320 | |||
217 | 2321 | class UnableEncodePath(BzrError): | 2306 | class UnableEncodePath(BzrError): |
218 | 2322 | 2307 | ||
219 | 2323 | _fmt = ('Unable to encode %(kind)s path %(path)r in ' | 2308 | _fmt = ('Unable to encode %(kind)s path %(path)r in ' |
220 | 2324 | 2309 | ||
221 | === modified file 'breezy/git/memorytree.py' | |||
222 | --- breezy/git/memorytree.py 2018-11-18 00:25:19 +0000 | |||
223 | +++ breezy/git/memorytree.py 2019-03-04 01:57:09 +0000 | |||
224 | @@ -58,6 +58,9 @@ | |||
225 | 58 | self._lock_mode = None | 58 | self._lock_mode = None |
226 | 59 | self._populate_from_branch() | 59 | self._populate_from_branch() |
227 | 60 | 60 | ||
228 | 61 | def _supports_executable(self): | ||
229 | 62 | return True | ||
230 | 63 | |||
231 | 61 | @property | 64 | @property |
232 | 62 | def controldir(self): | 65 | def controldir(self): |
233 | 63 | return self.branch.controldir | 66 | return self.branch.controldir |
234 | @@ -258,3 +261,7 @@ | |||
235 | 258 | def kind(self, p): | 261 | def kind(self, p): |
236 | 259 | stat_value = self._file_transport.stat(p) | 262 | stat_value = self._file_transport.stat(p) |
237 | 260 | return osutils.file_kind_from_stat_mode(stat_value.st_mode) | 263 | return osutils.file_kind_from_stat_mode(stat_value.st_mode) |
238 | 264 | |||
239 | 265 | def get_symlink_target(self, path): | ||
240 | 266 | with self.lock_read(): | ||
241 | 267 | return self._file_transport.readlink(path) | ||
242 | 261 | 268 | ||
243 | === modified file 'breezy/git/tree.py' | |||
244 | --- breezy/git/tree.py 2018-12-18 20:55:37 +0000 | |||
245 | +++ breezy/git/tree.py 2019-03-04 01:57:09 +0000 | |||
246 | @@ -1447,6 +1447,7 @@ | |||
247 | 1447 | # Report dirified directories to commit_tree first, so that they can be | 1447 | # Report dirified directories to commit_tree first, so that they can be |
248 | 1448 | # replaced with non-empty directories if they have contents. | 1448 | # replaced with non-empty directories if they have contents. |
249 | 1449 | dirified = [] | 1449 | dirified = [] |
250 | 1450 | trust_executable = target._supports_executable() | ||
251 | 1450 | for path, index_entry in target._recurse_index_entries(): | 1451 | for path, index_entry in target._recurse_index_entries(): |
252 | 1451 | try: | 1452 | try: |
253 | 1452 | live_entry = target._live_entry(path) | 1453 | live_entry = target._live_entry(path) |
254 | @@ -1465,7 +1466,13 @@ | |||
255 | 1465 | else: | 1466 | else: |
256 | 1466 | raise | 1467 | raise |
257 | 1467 | else: | 1468 | else: |
259 | 1468 | blobs[path] = (live_entry.sha, cleanup_mode(live_entry.mode)) | 1469 | mode = live_entry.mode |
260 | 1470 | if not trust_executable: | ||
261 | 1471 | if mode_is_executable(index_entry.mode): | ||
262 | 1472 | mode |= 0o111 | ||
263 | 1473 | else: | ||
264 | 1474 | mode &= ~0o111 | ||
265 | 1475 | blobs[path] = (live_entry.sha, cleanup_mode(mode)) | ||
266 | 1469 | if want_unversioned: | 1476 | if want_unversioned: |
267 | 1470 | for e in target.extras(): | 1477 | for e in target.extras(): |
268 | 1471 | st = target._lstat(e) | 1478 | st = target._lstat(e) |
269 | 1472 | 1479 | ||
270 | === modified file 'breezy/git/workingtree.py' | |||
271 | --- breezy/git/workingtree.py 2019-02-05 04:00:02 +0000 | |||
272 | +++ breezy/git/workingtree.py 2019-03-04 01:57:09 +0000 | |||
273 | @@ -414,7 +414,7 @@ | |||
274 | 414 | # expand any symlinks in the directory part, while leaving the | 414 | # expand any symlinks in the directory part, while leaving the |
275 | 415 | # filename alone | 415 | # filename alone |
276 | 416 | # only expanding if symlinks are supported avoids windows path bugs | 416 | # only expanding if symlinks are supported avoids windows path bugs |
278 | 417 | if osutils.has_symlinks(): | 417 | if self.supports_symlinks(): |
279 | 418 | file_list = list(map(osutils.normalizepath, file_list)) | 418 | file_list = list(map(osutils.normalizepath, file_list)) |
280 | 419 | 419 | ||
281 | 420 | conflicts_related = set() | 420 | conflicts_related = set() |
282 | @@ -743,8 +743,7 @@ | |||
283 | 743 | 743 | ||
284 | 744 | def is_executable(self, path): | 744 | def is_executable(self, path): |
285 | 745 | with self.lock_read(): | 745 | with self.lock_read(): |
288 | 746 | if getattr(self, "_supports_executable", | 746 | if self._supports_executable(): |
287 | 747 | osutils.supports_executable)(): | ||
289 | 748 | mode = self._lstat(path).st_mode | 747 | mode = self._lstat(path).st_mode |
290 | 749 | else: | 748 | else: |
291 | 750 | (index, subpath) = self._lookup_index(path.encode('utf-8')) | 749 | (index, subpath) = self._lookup_index(path.encode('utf-8')) |
292 | @@ -755,10 +754,8 @@ | |||
293 | 755 | return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode) | 754 | return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode) |
294 | 756 | 755 | ||
295 | 757 | def _is_executable_from_path_and_stat(self, path, stat_result): | 756 | def _is_executable_from_path_and_stat(self, path, stat_result): |
300 | 758 | if getattr(self, "_supports_executable", | 757 | if self._supports_executable(): |
301 | 759 | osutils.supports_executable)(): | 758 | return self._is_executable_from_path_and_stat_from_stat(path, stat_result) |
298 | 760 | return self._is_executable_from_path_and_stat_from_stat( | ||
299 | 761 | path, stat_result) | ||
302 | 762 | else: | 759 | else: |
303 | 763 | return self._is_executable_from_path_and_stat_from_basis( | 760 | return self._is_executable_from_path_and_stat_from_basis( |
304 | 764 | path, stat_result) | 761 | path, stat_result) |
305 | @@ -1166,7 +1163,8 @@ | |||
306 | 1166 | self.store, | 1163 | self.store, |
307 | 1167 | None | 1164 | None |
308 | 1168 | if self.branch.head is None | 1165 | if self.branch.head is None |
310 | 1169 | else self.store[self.branch.head].tree) | 1166 | else self.store[self.branch.head].tree, |
311 | 1167 | honor_filemode=self._supports_executable()) | ||
312 | 1170 | 1168 | ||
313 | 1171 | def reset_state(self, revision_ids=None): | 1169 | def reset_state(self, revision_ids=None): |
314 | 1172 | """Reset the state of the working tree. | 1170 | """Reset the state of the working tree. |
315 | 1173 | 1171 | ||
316 | === modified file 'breezy/memorytree.py' | |||
317 | --- breezy/memorytree.py 2018-11-18 00:25:19 +0000 | |||
318 | +++ breezy/memorytree.py 2019-03-04 01:57:09 +0000 | |||
319 | @@ -22,6 +22,7 @@ | |||
320 | 22 | from __future__ import absolute_import | 22 | from __future__ import absolute_import |
321 | 23 | 23 | ||
322 | 24 | import os | 24 | import os |
323 | 25 | import stat | ||
324 | 25 | 26 | ||
325 | 26 | from . import ( | 27 | from . import ( |
326 | 27 | errors, | 28 | errors, |
327 | @@ -50,6 +51,9 @@ | |||
328 | 50 | self._locks = 0 | 51 | self._locks = 0 |
329 | 51 | self._lock_mode = None | 52 | self._lock_mode = None |
330 | 52 | 53 | ||
331 | 54 | def supports_symlinks(self): | ||
332 | 55 | return True | ||
333 | 56 | |||
334 | 53 | def get_config_stack(self): | 57 | def get_config_stack(self): |
335 | 54 | return self.branch.get_config_stack() | 58 | return self.branch.get_config_stack() |
336 | 55 | 59 | ||
337 | @@ -62,7 +66,15 @@ | |||
338 | 62 | with self.lock_tree_write(): | 66 | with self.lock_tree_write(): |
339 | 63 | for f, file_id, kind in zip(files, ids, kinds): | 67 | for f, file_id, kind in zip(files, ids, kinds): |
340 | 64 | if kind is None: | 68 | if kind is None: |
342 | 65 | kind = 'file' | 69 | st_mode = self._file_transport.stat(f).st_mode |
343 | 70 | if stat.S_ISREG(st_mode): | ||
344 | 71 | kind = 'file' | ||
345 | 72 | elif stat.S_ISLNK(st_mode): | ||
346 | 73 | kind = 'symlink' | ||
347 | 74 | elif stat.S_ISDIR(st_mode): | ||
348 | 75 | kind = 'directory' | ||
349 | 76 | else: | ||
350 | 77 | raise AssertionError('Unknown file kind') | ||
351 | 66 | if file_id is None: | 78 | if file_id is None: |
352 | 67 | self._inventory.add_path(f, kind=kind) | 79 | self._inventory.add_path(f, kind=kind) |
353 | 68 | else: | 80 | else: |
354 | @@ -127,7 +139,7 @@ | |||
355 | 127 | # memory tree does not support nested trees yet. | 139 | # memory tree does not support nested trees yet. |
356 | 128 | return kind, None, None, None | 140 | return kind, None, None, None |
357 | 129 | elif kind == 'symlink': | 141 | elif kind == 'symlink': |
359 | 130 | raise NotImplementedError('symlink support') | 142 | return kind, None, None, self._inventory[id].symlink_target |
360 | 131 | else: | 143 | else: |
361 | 132 | raise NotImplementedError('unknown kind') | 144 | raise NotImplementedError('unknown kind') |
362 | 133 | 145 | ||
363 | @@ -148,8 +160,7 @@ | |||
364 | 148 | return self._inventory.get_entry_by_path(path).executable | 160 | return self._inventory.get_entry_by_path(path).executable |
365 | 149 | 161 | ||
366 | 150 | def kind(self, path): | 162 | def kind(self, path): |
369 | 151 | file_id = self.path2id(path) | 163 | return self._inventory.get_entry_by_path(path).kind |
368 | 152 | return self._inventory[file_id].kind | ||
370 | 153 | 164 | ||
371 | 154 | def mkdir(self, path, file_id=None): | 165 | def mkdir(self, path, file_id=None): |
372 | 155 | """See MutableTree.mkdir().""" | 166 | """See MutableTree.mkdir().""" |
373 | @@ -227,6 +238,8 @@ | |||
374 | 227 | continue | 238 | continue |
375 | 228 | if entry.kind == 'directory': | 239 | if entry.kind == 'directory': |
376 | 229 | self._file_transport.mkdir(path) | 240 | self._file_transport.mkdir(path) |
377 | 241 | elif entry.kind == 'symlink': | ||
378 | 242 | self._file_transport.symlink(entry.symlink_target, path) | ||
379 | 230 | elif entry.kind == 'file': | 243 | elif entry.kind == 'file': |
380 | 231 | self._file_transport.put_file( | 244 | self._file_transport.put_file( |
381 | 232 | path, self._basis_tree.get_file(path)) | 245 | path, self._basis_tree.get_file(path)) |
382 | @@ -302,6 +315,10 @@ | |||
383 | 302 | else: | 315 | else: |
384 | 303 | raise | 316 | raise |
385 | 304 | 317 | ||
386 | 318 | def get_symlink_target(self, path): | ||
387 | 319 | with self.lock_read(): | ||
388 | 320 | return self._file_transport.readlink(path) | ||
389 | 321 | |||
390 | 305 | def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False): | 322 | def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False): |
391 | 306 | """See MutableTree.set_parent_trees().""" | 323 | """See MutableTree.set_parent_trees().""" |
392 | 307 | if len(parents_list) == 0: | 324 | if len(parents_list) == 0: |
393 | 308 | 325 | ||
394 | === modified file 'breezy/mutabletree.py' | |||
395 | --- breezy/mutabletree.py 2018-11-18 00:25:19 +0000 | |||
396 | +++ breezy/mutabletree.py 2019-03-04 01:57:09 +0000 | |||
397 | @@ -197,14 +197,32 @@ | |||
398 | 197 | if _from_tree is None: | 197 | if _from_tree is None: |
399 | 198 | _from_tree = self.basis_tree() | 198 | _from_tree = self.basis_tree() |
400 | 199 | changes = self.iter_changes(_from_tree) | 199 | changes = self.iter_changes(_from_tree) |
405 | 200 | try: | 200 | if self.supports_symlinks(): |
406 | 201 | change = next(changes) | 201 | # Fast path for has_changes. |
407 | 202 | # Exclude root (talk about black magic... --vila 20090629) | 202 | try: |
404 | 203 | if change[4] == (None, None): | ||
408 | 204 | change = next(changes) | 203 | change = next(changes) |
412 | 205 | return True | 204 | # Exclude root (talk about black magic... --vila 20090629) |
413 | 206 | except StopIteration: | 205 | if change[4] == (None, None): |
414 | 207 | # No changes | 206 | change = next(changes) |
415 | 207 | return True | ||
416 | 208 | except StopIteration: | ||
417 | 209 | # No changes | ||
418 | 210 | return False | ||
419 | 211 | else: | ||
420 | 212 | # Slow path for has_changes. | ||
421 | 213 | # Handle platforms that do not support symlinks in the | ||
422 | 214 | # conditional below. This is slower than the try/except | ||
423 | 215 | # approach below that but we don't have a choice as we | ||
424 | 216 | # need to be sure that all symlinks are removed from the | ||
425 | 217 | # entire changeset. This is because in plantforms that | ||
426 | 218 | # do not support symlinks, they show up as None in the | ||
427 | 219 | # working copy as compared to the repository. | ||
428 | 220 | # Also, exclude root as mention in the above fast path. | ||
429 | 221 | changes = filter( | ||
430 | 222 | lambda c: c[6][0] != 'symlink' and c[4] != (None, None), | ||
431 | 223 | changes) | ||
432 | 224 | if len(changes) > 0: | ||
433 | 225 | return True | ||
434 | 208 | return False | 226 | return False |
435 | 209 | 227 | ||
436 | 210 | def check_changed_or_out_of_date(self, strict, opt_name, | 228 | def check_changed_or_out_of_date(self, strict, opt_name, |
437 | 211 | 229 | ||
438 | === modified file 'breezy/osutils.py' | |||
439 | --- breezy/osutils.py 2019-03-02 23:49:52 +0000 | |||
440 | +++ breezy/osutils.py 2019-03-04 01:57:09 +0000 | |||
441 | @@ -1662,8 +1662,42 @@ | |||
442 | 1662 | _terminal_size = _ioctl_terminal_size | 1662 | _terminal_size = _ioctl_terminal_size |
443 | 1663 | 1663 | ||
444 | 1664 | 1664 | ||
447 | 1665 | def supports_executable(): | 1665 | def supports_executable(path): |
448 | 1666 | return sys.platform != "win32" | 1666 | """Return if filesystem at path supports executable bit. |
449 | 1667 | |||
450 | 1668 | :param path: Path for which to check the file system | ||
451 | 1669 | :return: boolean indicating whether executable bit can be stored/relied upon | ||
452 | 1670 | """ | ||
453 | 1671 | if sys.platform == 'win32': | ||
454 | 1672 | return False | ||
455 | 1673 | try: | ||
456 | 1674 | fs_type = get_fs_type(path) | ||
457 | 1675 | except errors.DependencyNotPresent: | ||
458 | 1676 | # TODO(jelmer): Warn here? | ||
459 | 1677 | pass | ||
460 | 1678 | else: | ||
461 | 1679 | if fs_type in ('vfat', 'ntfs'): | ||
462 | 1680 | # filesystems known to not support executable bit | ||
463 | 1681 | return False | ||
464 | 1682 | return True | ||
465 | 1683 | |||
466 | 1684 | |||
467 | 1685 | def supports_symlinks(path): | ||
468 | 1686 | """Return if the filesystem at path supports the creation of symbolic links. | ||
469 | 1687 | |||
470 | 1688 | """ | ||
471 | 1689 | if not has_symlinks(): | ||
472 | 1690 | return False | ||
473 | 1691 | try: | ||
474 | 1692 | fs_type = get_fs_type(path) | ||
475 | 1693 | except errors.DependencyNotPresent: | ||
476 | 1694 | # TODO(jelmer): Warn here? | ||
477 | 1695 | pass | ||
478 | 1696 | else: | ||
479 | 1697 | if fs_type in ('vfat', 'ntfs'): | ||
480 | 1698 | # filesystems known to not support executable bit | ||
481 | 1699 | return False | ||
482 | 1700 | return True | ||
483 | 1667 | 1701 | ||
484 | 1668 | 1702 | ||
485 | 1669 | def supports_posix_readonly(): | 1703 | def supports_posix_readonly(): |
486 | @@ -2602,6 +2636,25 @@ | |||
487 | 2602 | return False | 2636 | return False |
488 | 2603 | 2637 | ||
489 | 2604 | 2638 | ||
490 | 2639 | def get_fs_type(path): | ||
491 | 2640 | """Return the filesystem type for the partition a path is in. | ||
492 | 2641 | |||
493 | 2642 | :param path: Path to search filesystem type for | ||
494 | 2643 | :return: A FS type, as string. E.g. "ext2" | ||
495 | 2644 | """ | ||
496 | 2645 | # TODO(jelmer): It would be nice to avoid an extra dependency here, but the only | ||
497 | 2646 | # alternative is reading platform-specific files under /proc :( | ||
498 | 2647 | try: | ||
499 | 2648 | import psutil | ||
500 | 2649 | except ImportError as e: | ||
501 | 2650 | raise errors.DependencyNotPresent('psutil', e) | ||
502 | 2651 | for part in sorted(psutil.disk_partitions(), key=lambda x: len(x.mountpoint), reverse=True): | ||
503 | 2652 | if is_inside(part.mountpoint, path): | ||
504 | 2653 | return part.fstype | ||
505 | 2654 | # Unable to parse the file? Since otherwise at least the entry for / should match.. | ||
506 | 2655 | return None | ||
507 | 2656 | |||
508 | 2657 | |||
509 | 2605 | if PY3: | 2658 | if PY3: |
510 | 2606 | perf_counter = time.perf_counter | 2659 | perf_counter = time.perf_counter |
511 | 2607 | else: | 2660 | else: |
512 | 2608 | 2661 | ||
513 | === modified file 'breezy/tests/per_tree/test_symlinks.py' | |||
514 | --- breezy/tests/per_tree/test_symlinks.py 2018-11-11 04:08:32 +0000 | |||
515 | +++ breezy/tests/per_tree/test_symlinks.py 2019-03-04 01:57:09 +0000 | |||
516 | @@ -18,8 +18,10 @@ | |||
517 | 18 | 18 | ||
518 | 19 | 19 | ||
519 | 20 | from breezy import ( | 20 | from breezy import ( |
520 | 21 | osutils, | ||
521 | 21 | tests, | 22 | tests, |
522 | 22 | ) | 23 | ) |
523 | 24 | from breezy.git.branch import GitBranch | ||
524 | 23 | from breezy.tests import ( | 25 | from breezy.tests import ( |
525 | 24 | per_tree, | 26 | per_tree, |
526 | 25 | ) | 27 | ) |
527 | @@ -35,6 +37,13 @@ | |||
528 | 35 | return next(tree.iter_entries_by_dir(specific_files=[path]))[1] | 37 | return next(tree.iter_entries_by_dir(specific_files=[path]))[1] |
529 | 36 | 38 | ||
530 | 37 | 39 | ||
531 | 40 | class TestSymlinkSupportFunction(per_tree.TestCaseWithTree): | ||
532 | 41 | |||
533 | 42 | def test_supports_symlinks(self): | ||
534 | 43 | self.tree = self.make_branch_and_tree('.') | ||
535 | 44 | self.assertIn(self.tree.supports_symlinks(), [True, False]) | ||
536 | 45 | |||
537 | 46 | |||
538 | 38 | class TestTreeWithSymlinks(per_tree.TestCaseWithTree): | 47 | class TestTreeWithSymlinks(per_tree.TestCaseWithTree): |
539 | 39 | 48 | ||
540 | 40 | _test_needs_features = [features.SymlinkFeature] | 49 | _test_needs_features = [features.SymlinkFeature] |
541 | @@ -65,3 +74,34 @@ | |||
542 | 65 | entry = get_entry(self.tree, 'symlink') | 74 | entry = get_entry(self.tree, 'symlink') |
543 | 66 | self.assertEqual(entry.kind, 'symlink') | 75 | self.assertEqual(entry.kind, 'symlink') |
544 | 67 | self.assertEqual(None, entry.text_size) | 76 | self.assertEqual(None, entry.text_size) |
545 | 77 | |||
546 | 78 | |||
547 | 79 | class TestTreeWithoutSymlinks(per_tree.TestCaseWithTree): | ||
548 | 80 | |||
549 | 81 | def setUp(self): | ||
550 | 82 | super(TestTreeWithoutSymlinks, self).setUp() | ||
551 | 83 | self.branch = self.make_branch('a') | ||
552 | 84 | mem_tree = self.branch.create_memorytree() | ||
553 | 85 | with mem_tree.lock_write(): | ||
554 | 86 | mem_tree._file_transport.symlink('source', 'symlink') | ||
555 | 87 | mem_tree.add(['', 'symlink']) | ||
556 | 88 | rev1 = mem_tree.commit('rev1') | ||
557 | 89 | self.assertPathDoesNotExist('a/symlink') | ||
558 | 90 | |||
559 | 91 | def test_clone_skips_symlinks(self): | ||
560 | 92 | if isinstance(self.branch, (GitBranch,)): | ||
561 | 93 | # TODO(jelmer): Fix this test for git repositories | ||
562 | 94 | raise TestSkipped( | ||
563 | 95 | 'git trees do not honor osutils.supports_symlinks yet') | ||
564 | 96 | self.overrideAttr(osutils, 'supports_symlinks', lambda p: False) | ||
565 | 97 | # This should not attempt to create any symlinks | ||
566 | 98 | result_dir = self.branch.controldir.sprout('b') | ||
567 | 99 | result_tree = result_dir.open_workingtree() | ||
568 | 100 | self.assertFalse(result_tree.supports_symlinks()) | ||
569 | 101 | self.assertPathDoesNotExist('b/symlink') | ||
570 | 102 | basis_tree = self.branch.basis_tree() | ||
571 | 103 | self.assertTrue(basis_tree.has_filename('symlink')) | ||
572 | 104 | with result_tree.lock_read(): | ||
573 | 105 | self.assertEqual( | ||
574 | 106 | [('symlink', 'symlink')], | ||
575 | 107 | [c[1] for c in result_tree.iter_changes(basis_tree)]) | ||
576 | 68 | 108 | ||
577 | === modified file 'breezy/tests/test_commit.py' | |||
578 | --- breezy/tests/test_commit.py 2018-11-29 23:42:41 +0000 | |||
579 | +++ breezy/tests/test_commit.py 2019-03-04 01:57:09 +0000 | |||
580 | @@ -16,12 +16,14 @@ | |||
581 | 16 | 16 | ||
582 | 17 | 17 | ||
583 | 18 | import os | 18 | import os |
584 | 19 | from StringIO import StringIO | ||
585 | 19 | 20 | ||
586 | 20 | import breezy | 21 | import breezy |
587 | 21 | from .. import ( | 22 | from .. import ( |
588 | 22 | config, | 23 | config, |
589 | 23 | controldir, | 24 | controldir, |
590 | 24 | errors, | 25 | errors, |
591 | 26 | trace, | ||
592 | 25 | ) | 27 | ) |
593 | 26 | from ..branch import Branch | 28 | from ..branch import Branch |
594 | 27 | from ..bzr.bzrdir import BzrDirMetaFormat1 | 29 | from ..bzr.bzrdir import BzrDirMetaFormat1 |
595 | @@ -676,6 +678,35 @@ | |||
596 | 676 | finally: | 678 | finally: |
597 | 677 | basis.unlock() | 679 | basis.unlock() |
598 | 678 | 680 | ||
599 | 681 | def test_unsupported_symlink_commit(self): | ||
600 | 682 | self.requireFeature(SymlinkFeature) | ||
601 | 683 | tree = self.make_branch_and_tree('.') | ||
602 | 684 | self.build_tree(['hello']) | ||
603 | 685 | tree.add('hello') | ||
604 | 686 | tree.commit('added hello', rev_id='hello_id') | ||
605 | 687 | os.symlink('hello', 'foo') | ||
606 | 688 | tree.add('foo') | ||
607 | 689 | tree.commit('added foo', rev_id='foo_id') | ||
608 | 690 | log = StringIO() | ||
609 | 691 | trace.push_log_file(log) | ||
610 | 692 | os_symlink = getattr(os, 'symlink', None) | ||
611 | 693 | os.symlink = None | ||
612 | 694 | try: | ||
613 | 695 | # At this point as bzr thinks symlinks are not supported | ||
614 | 696 | # we should get a warning about symlink foo and bzr should | ||
615 | 697 | # not think its removed. | ||
616 | 698 | os.unlink('foo') | ||
617 | 699 | self.build_tree(['world']) | ||
618 | 700 | tree.add('world') | ||
619 | 701 | tree.commit('added world', rev_id='world_id') | ||
620 | 702 | finally: | ||
621 | 703 | if os_symlink: | ||
622 | 704 | os.symlink = os_symlink | ||
623 | 705 | self.assertContainsRe( | ||
624 | 706 | log.getvalue(), | ||
625 | 707 | 'Ignoring "foo" as symlinks are not ' | ||
626 | 708 | 'supported on this filesystem.') | ||
627 | 709 | |||
628 | 679 | def test_commit_kind_changes(self): | 710 | def test_commit_kind_changes(self): |
629 | 680 | self.requireFeature(SymlinkFeature) | 711 | self.requireFeature(SymlinkFeature) |
630 | 681 | tree = self.make_branch_and_tree('.') | 712 | tree = self.make_branch_and_tree('.') |
631 | 682 | 713 | ||
632 | === modified file 'breezy/tests/test_errors.py' | |||
633 | --- breezy/tests/test_errors.py 2018-11-11 04:08:32 +0000 | |||
634 | +++ breezy/tests/test_errors.py 2019-03-04 01:57:09 +0000 | |||
635 | @@ -388,20 +388,6 @@ | |||
636 | 388 | "you wish to keep, and delete it when you are done.", | 388 | "you wish to keep, and delete it when you are done.", |
637 | 389 | str(err)) | 389 | str(err)) |
638 | 390 | 390 | ||
639 | 391 | def test_unable_create_symlink(self): | ||
640 | 392 | err = errors.UnableCreateSymlink() | ||
641 | 393 | self.assertEqual( | ||
642 | 394 | "Unable to create symlink on this platform", | ||
643 | 395 | str(err)) | ||
644 | 396 | err = errors.UnableCreateSymlink(path=u'foo') | ||
645 | 397 | self.assertEqual( | ||
646 | 398 | "Unable to create symlink 'foo' on this platform", | ||
647 | 399 | str(err)) | ||
648 | 400 | err = errors.UnableCreateSymlink(path=u'\xb5') | ||
649 | 401 | self.assertEqual( | ||
650 | 402 | "Unable to create symlink %s on this platform" % repr(u'\xb5'), | ||
651 | 403 | str(err)) | ||
652 | 404 | |||
653 | 405 | def test_invalid_url_join(self): | 391 | def test_invalid_url_join(self): |
654 | 406 | """Test the formatting of InvalidURLJoin.""" | 392 | """Test the formatting of InvalidURLJoin.""" |
655 | 407 | e = urlutils.InvalidURLJoin('Reason', 'base path', ('args',)) | 393 | e = urlutils.InvalidURLJoin('Reason', 'base path', ('args',)) |
656 | 408 | 394 | ||
657 | === modified file 'breezy/tests/test_memorytree.py' | |||
658 | --- breezy/tests/test_memorytree.py 2018-11-11 04:08:32 +0000 | |||
659 | +++ breezy/tests/test_memorytree.py 2019-03-04 01:57:09 +0000 | |||
660 | @@ -46,21 +46,17 @@ | |||
661 | 46 | rev_id = tree.commit('first post') | 46 | rev_id = tree.commit('first post') |
662 | 47 | tree.unlock() | 47 | tree.unlock() |
663 | 48 | tree = MemoryTree.create_on_branch(branch) | 48 | tree = MemoryTree.create_on_branch(branch) |
669 | 49 | tree.lock_read() | 49 | with tree.lock_read(): |
670 | 50 | self.assertEqual([rev_id], tree.get_parent_ids()) | 50 | self.assertEqual([rev_id], tree.get_parent_ids()) |
671 | 51 | with tree.get_file('foo') as f: | 51 | with tree.get_file('foo') as f: |
672 | 52 | self.assertEqual(b'contents of foo\n', f.read()) | 52 | self.assertEqual(b'contents of foo\n', f.read()) |
668 | 53 | tree.unlock() | ||
673 | 54 | 53 | ||
674 | 55 | def test_get_root_id(self): | 54 | def test_get_root_id(self): |
675 | 56 | branch = self.make_branch('branch') | 55 | branch = self.make_branch('branch') |
676 | 57 | tree = MemoryTree.create_on_branch(branch) | 56 | tree = MemoryTree.create_on_branch(branch) |
679 | 58 | tree.lock_write() | 57 | with tree.lock_write(): |
678 | 59 | try: | ||
680 | 60 | tree.add(['']) | 58 | tree.add(['']) |
681 | 61 | self.assertIsNot(None, tree.get_root_id()) | 59 | self.assertIsNot(None, tree.get_root_id()) |
682 | 62 | finally: | ||
683 | 63 | tree.unlock() | ||
684 | 64 | 60 | ||
685 | 65 | def test_lock_tree_write(self): | 61 | def test_lock_tree_write(self): |
686 | 66 | """Check we can lock_tree_write and unlock MemoryTrees.""" | 62 | """Check we can lock_tree_write and unlock MemoryTrees.""" |
687 | @@ -73,9 +69,8 @@ | |||
688 | 73 | """Check that we error when trying to upgrade a read lock to write.""" | 69 | """Check that we error when trying to upgrade a read lock to write.""" |
689 | 74 | branch = self.make_branch('branch') | 70 | branch = self.make_branch('branch') |
690 | 75 | tree = MemoryTree.create_on_branch(branch) | 71 | tree = MemoryTree.create_on_branch(branch) |
694 | 76 | tree.lock_read() | 72 | with tree.lock_read(): |
695 | 77 | self.assertRaises(errors.ReadOnlyError, tree.lock_tree_write) | 73 | self.assertRaises(errors.ReadOnlyError, tree.lock_tree_write) |
693 | 78 | tree.unlock() | ||
696 | 79 | 74 | ||
697 | 80 | def test_lock_write(self): | 75 | def test_lock_write(self): |
698 | 81 | """Check we can lock_write and unlock MemoryTrees.""" | 76 | """Check we can lock_write and unlock MemoryTrees.""" |
699 | @@ -88,58 +83,63 @@ | |||
700 | 88 | """Check that we error when trying to upgrade a read lock to write.""" | 83 | """Check that we error when trying to upgrade a read lock to write.""" |
701 | 89 | branch = self.make_branch('branch') | 84 | branch = self.make_branch('branch') |
702 | 90 | tree = MemoryTree.create_on_branch(branch) | 85 | tree = MemoryTree.create_on_branch(branch) |
706 | 91 | tree.lock_read() | 86 | with tree.lock_read(): |
707 | 92 | self.assertRaises(errors.ReadOnlyError, tree.lock_write) | 87 | self.assertRaises(errors.ReadOnlyError, tree.lock_write) |
705 | 93 | tree.unlock() | ||
708 | 94 | 88 | ||
709 | 95 | def test_add_with_kind(self): | 89 | def test_add_with_kind(self): |
710 | 96 | branch = self.make_branch('branch') | 90 | branch = self.make_branch('branch') |
711 | 97 | tree = MemoryTree.create_on_branch(branch) | 91 | tree = MemoryTree.create_on_branch(branch) |
720 | 98 | tree.lock_write() | 92 | with tree.lock_write(): |
721 | 99 | tree.add(['', 'afile', 'adir'], None, | 93 | tree.add(['', 'afile', 'adir'], None, |
722 | 100 | ['directory', 'file', 'directory']) | 94 | ['directory', 'file', 'directory']) |
723 | 101 | self.assertEqual('afile', tree.id2path(tree.path2id('afile'))) | 95 | self.assertEqual('afile', tree.id2path(tree.path2id('afile'))) |
724 | 102 | self.assertEqual('adir', tree.id2path(tree.path2id('adir'))) | 96 | self.assertEqual('adir', tree.id2path(tree.path2id('adir'))) |
725 | 103 | self.assertFalse(tree.has_filename('afile')) | 97 | self.assertFalse(tree.has_filename('afile')) |
726 | 104 | self.assertFalse(tree.has_filename('adir')) | 98 | self.assertFalse(tree.has_filename('adir')) |
719 | 105 | tree.unlock() | ||
727 | 106 | 99 | ||
728 | 107 | def test_put_new_file(self): | 100 | def test_put_new_file(self): |
729 | 108 | branch = self.make_branch('branch') | 101 | branch = self.make_branch('branch') |
730 | 109 | tree = MemoryTree.create_on_branch(branch) | 102 | tree = MemoryTree.create_on_branch(branch) |
737 | 110 | tree.lock_write() | 103 | with tree.lock_write(): |
738 | 111 | tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'], | 104 | tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'], |
739 | 112 | kinds=['directory', 'file']) | 105 | kinds=['directory', 'file']) |
740 | 113 | tree.put_file_bytes_non_atomic('foo', b'barshoom') | 106 | tree.put_file_bytes_non_atomic('foo', b'barshoom') |
741 | 114 | self.assertEqual(b'barshoom', tree.get_file('foo').read()) | 107 | with tree.get_file('foo') as f: |
742 | 115 | tree.unlock() | 108 | self.assertEqual(b'barshoom', f.read()) |
743 | 116 | 109 | ||
744 | 117 | def test_put_existing_file(self): | 110 | def test_put_existing_file(self): |
745 | 118 | branch = self.make_branch('branch') | 111 | branch = self.make_branch('branch') |
746 | 119 | tree = MemoryTree.create_on_branch(branch) | 112 | tree = MemoryTree.create_on_branch(branch) |
754 | 120 | tree.lock_write() | 113 | with tree.lock_write(): |
755 | 121 | tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'], | 114 | tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'], |
756 | 122 | kinds=['directory', 'file']) | 115 | kinds=['directory', 'file']) |
757 | 123 | tree.put_file_bytes_non_atomic('foo', b'first-content') | 116 | tree.put_file_bytes_non_atomic('foo', b'first-content') |
758 | 124 | tree.put_file_bytes_non_atomic('foo', b'barshoom') | 117 | tree.put_file_bytes_non_atomic('foo', b'barshoom') |
759 | 125 | self.assertEqual(b'barshoom', tree.get_file('foo').read()) | 118 | self.assertEqual(b'barshoom', tree.get_file('foo').read()) |
753 | 126 | tree.unlock() | ||
760 | 127 | 119 | ||
761 | 128 | def test_add_in_subdir(self): | 120 | def test_add_in_subdir(self): |
762 | 129 | branch = self.make_branch('branch') | 121 | branch = self.make_branch('branch') |
763 | 130 | tree = MemoryTree.create_on_branch(branch) | 122 | tree = MemoryTree.create_on_branch(branch) |
776 | 131 | tree.lock_write() | 123 | with tree.lock_write(): |
777 | 132 | self.addCleanup(tree.unlock) | 124 | tree.add([''], [b'root-id'], ['directory']) |
778 | 133 | tree.add([''], [b'root-id'], ['directory']) | 125 | # Unfortunately, the only way to 'mkdir' is to call 'tree.mkdir', but |
779 | 134 | # Unfortunately, the only way to 'mkdir' is to call 'tree.mkdir', but | 126 | # that *always* adds the directory as well. So if you want to create a |
780 | 135 | # that *always* adds the directory as well. So if you want to create a | 127 | # file in a subdirectory, you have to split out the 'mkdir()' calls |
781 | 136 | # file in a subdirectory, you have to split out the 'mkdir()' calls | 128 | # from the add and put_file_bytes_non_atomic calls. :( |
782 | 137 | # from the add and put_file_bytes_non_atomic calls. :( | 129 | tree.mkdir('adir', b'dir-id') |
783 | 138 | tree.mkdir('adir', b'dir-id') | 130 | tree.add(['adir/afile'], [b'file-id'], ['file']) |
784 | 139 | tree.add(['adir/afile'], [b'file-id'], ['file']) | 131 | self.assertEqual('adir/afile', tree.id2path(b'file-id')) |
785 | 140 | self.assertEqual('adir/afile', tree.id2path(b'file-id')) | 132 | self.assertEqual('adir', tree.id2path(b'dir-id')) |
786 | 141 | self.assertEqual('adir', tree.id2path(b'dir-id')) | 133 | tree.put_file_bytes_non_atomic('adir/afile', b'barshoom') |
787 | 142 | tree.put_file_bytes_non_atomic('adir/afile', b'barshoom') | 134 | |
788 | 135 | def test_add_symlink(self): | ||
789 | 136 | branch = self.make_branch('branch') | ||
790 | 137 | tree = MemoryTree.create_on_branch(branch) | ||
791 | 138 | with tree.lock_write(): | ||
792 | 139 | tree._file_transport.symlink('bar', 'foo') | ||
793 | 140 | tree.add(['', 'foo']) | ||
794 | 141 | self.assertEqual('symlink', tree.kind('foo')) | ||
795 | 142 | self.assertEqual('bar', tree.get_symlink_target('foo')) | ||
796 | 143 | 143 | ||
797 | 144 | def test_commit_trivial(self): | 144 | def test_commit_trivial(self): |
798 | 145 | """Smoke test for commit on a MemoryTree. | 145 | """Smoke test for commit on a MemoryTree. |
799 | @@ -149,40 +149,35 @@ | |||
800 | 149 | """ | 149 | """ |
801 | 150 | branch = self.make_branch('branch') | 150 | branch = self.make_branch('branch') |
802 | 151 | tree = MemoryTree.create_on_branch(branch) | 151 | tree = MemoryTree.create_on_branch(branch) |
811 | 152 | tree.lock_write() | 152 | with tree.lock_write(): |
812 | 153 | tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'], | 153 | tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'], |
813 | 154 | kinds=['directory', 'file']) | 154 | kinds=['directory', 'file']) |
814 | 155 | tree.put_file_bytes_non_atomic('foo', b'barshoom') | 155 | tree.put_file_bytes_non_atomic('foo', b'barshoom') |
815 | 156 | revision_id = tree.commit('message baby') | 156 | revision_id = tree.commit('message baby') |
816 | 157 | # the parents list for the tree should have changed. | 157 | # the parents list for the tree should have changed. |
817 | 158 | self.assertEqual([revision_id], tree.get_parent_ids()) | 158 | self.assertEqual([revision_id], tree.get_parent_ids()) |
810 | 159 | tree.unlock() | ||
818 | 160 | # and we should have a revision that is accessible outside the tree lock | 159 | # and we should have a revision that is accessible outside the tree lock |
819 | 161 | revtree = tree.branch.repository.revision_tree(revision_id) | 160 | revtree = tree.branch.repository.revision_tree(revision_id) |
823 | 162 | revtree.lock_read() | 161 | with revtree.lock_read(), revtree.get_file('foo') as f: |
821 | 163 | self.addCleanup(revtree.unlock) | ||
822 | 164 | with revtree.get_file('foo') as f: | ||
824 | 165 | self.assertEqual(b'barshoom', f.read()) | 162 | self.assertEqual(b'barshoom', f.read()) |
825 | 166 | 163 | ||
826 | 167 | def test_unversion(self): | 164 | def test_unversion(self): |
827 | 168 | """Some test for unversion of a memory tree.""" | 165 | """Some test for unversion of a memory tree.""" |
828 | 169 | branch = self.make_branch('branch') | 166 | branch = self.make_branch('branch') |
829 | 170 | tree = MemoryTree.create_on_branch(branch) | 167 | tree = MemoryTree.create_on_branch(branch) |
837 | 171 | tree.lock_write() | 168 | with tree.lock_write(): |
838 | 172 | tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'], | 169 | tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'], |
839 | 173 | kinds=['directory', 'file']) | 170 | kinds=['directory', 'file']) |
840 | 174 | tree.unversion(['foo']) | 171 | tree.unversion(['foo']) |
841 | 175 | self.assertFalse(tree.is_versioned('foo')) | 172 | self.assertFalse(tree.is_versioned('foo')) |
842 | 176 | self.assertFalse(tree.has_id(b'foo-id')) | 173 | self.assertFalse(tree.has_id(b'foo-id')) |
836 | 177 | tree.unlock() | ||
843 | 178 | 174 | ||
844 | 179 | def test_last_revision(self): | 175 | def test_last_revision(self): |
845 | 180 | """There should be a last revision method we can call.""" | 176 | """There should be a last revision method we can call.""" |
846 | 181 | tree = self.make_branch_and_memory_tree('branch') | 177 | tree = self.make_branch_and_memory_tree('branch') |
851 | 182 | tree.lock_write() | 178 | with tree.lock_write(): |
852 | 183 | tree.add('') | 179 | tree.add('') |
853 | 184 | rev_id = tree.commit('first post') | 180 | rev_id = tree.commit('first post') |
850 | 185 | tree.unlock() | ||
854 | 186 | self.assertEqual(rev_id, tree.last_revision()) | 181 | self.assertEqual(rev_id, tree.last_revision()) |
855 | 187 | 182 | ||
856 | 188 | def test_rename_file(self): | 183 | def test_rename_file(self): |
857 | 189 | 184 | ||
858 | === modified file 'breezy/tests/test_osutils.py' | |||
859 | --- breezy/tests/test_osutils.py 2018-11-17 18:49:41 +0000 | |||
860 | +++ breezy/tests/test_osutils.py 2019-03-04 01:57:09 +0000 | |||
861 | @@ -62,6 +62,8 @@ | |||
862 | 62 | 62 | ||
863 | 63 | term_ios_feature = features.ModuleAvailableFeature('termios') | 63 | term_ios_feature = features.ModuleAvailableFeature('termios') |
864 | 64 | 64 | ||
865 | 65 | psutil_feature = features.ModuleAvailableFeature('psutil') | ||
866 | 66 | |||
867 | 65 | 67 | ||
868 | 66 | def _already_unicode(s): | 68 | def _already_unicode(s): |
869 | 67 | return s | 69 | return s |
870 | @@ -2340,3 +2342,22 @@ | |||
871 | 2340 | import pywintypes | 2342 | import pywintypes |
872 | 2341 | self.assertTrue(osutils.is_environment_error( | 2343 | self.assertTrue(osutils.is_environment_error( |
873 | 2342 | pywintypes.error(errno.EINVAL, "Invalid parameter", "Caller"))) | 2344 | pywintypes.error(errno.EINVAL, "Invalid parameter", "Caller"))) |
874 | 2345 | |||
875 | 2346 | |||
876 | 2347 | class SupportsExecutableTests(tests.TestCaseInTempDir): | ||
877 | 2348 | |||
878 | 2349 | def test_returns_bool(self): | ||
879 | 2350 | self.assertIsInstance(osutils.supports_executable(self.test_dir), bool) | ||
880 | 2351 | |||
881 | 2352 | |||
882 | 2353 | class SupportsSymlinksTests(tests.TestCaseInTempDir): | ||
883 | 2354 | |||
884 | 2355 | def test_returns_bool(self): | ||
885 | 2356 | self.assertIsInstance(osutils.supports_symlinks(self.test_dir), bool) | ||
886 | 2357 | |||
887 | 2358 | |||
888 | 2359 | class GetFsTypeTests(tests.TestCaseInTempDir): | ||
889 | 2360 | |||
890 | 2361 | def test_returns_string(self): | ||
891 | 2362 | self.requireFeature(psutil_feature) | ||
892 | 2363 | self.assertIsInstance(osutils.get_fs_type(self.test_dir), str) | ||
893 | 2343 | 2364 | ||
894 | === modified file 'breezy/tests/test_transform.py' | |||
895 | --- breezy/tests/test_transform.py 2019-02-04 19:39:30 +0000 | |||
896 | +++ breezy/tests/test_transform.py 2019-03-04 01:57:09 +0000 | |||
897 | @@ -16,6 +16,7 @@ | |||
898 | 16 | 16 | ||
899 | 17 | import codecs | 17 | import codecs |
900 | 18 | import errno | 18 | import errno |
901 | 19 | from io import BytesIO, StringIO | ||
902 | 19 | import os | 20 | import os |
903 | 20 | import sys | 21 | import sys |
904 | 21 | import time | 22 | import time |
905 | @@ -806,22 +807,18 @@ | |||
906 | 806 | u'\N{Euro Sign}wizard2', | 807 | u'\N{Euro Sign}wizard2', |
907 | 807 | u'b\N{Euro Sign}hind_curtain') | 808 | u'b\N{Euro Sign}hind_curtain') |
908 | 808 | 809 | ||
910 | 809 | def test_unable_create_symlink(self): | 810 | def test_unsupported_symlink_no_conflict(self): |
911 | 810 | def tt_helper(): | 811 | def tt_helper(): |
912 | 811 | wt = self.make_branch_and_tree('.') | 812 | wt = self.make_branch_and_tree('.') |
919 | 812 | tt = TreeTransform(wt) # TreeTransform obtains write lock | 813 | tt = TreeTransform(wt) |
920 | 813 | try: | 814 | self.addCleanup(tt.finalize) |
921 | 814 | tt.new_symlink('foo', tt.root, 'bar') | 815 | tt.new_symlink('foo', tt.root, 'bar') |
922 | 815 | tt.apply() | 816 | result = tt.find_conflicts() |
923 | 816 | finally: | 817 | self.assertEqual([], result) |
918 | 817 | wt.unlock() | ||
924 | 818 | os_symlink = getattr(os, 'symlink', None) | 818 | os_symlink = getattr(os, 'symlink', None) |
925 | 819 | os.symlink = None | 819 | os.symlink = None |
926 | 820 | try: | 820 | try: |
931 | 821 | err = self.assertRaises(errors.UnableCreateSymlink, tt_helper) | 821 | tt_helper() |
928 | 822 | self.assertEqual( | ||
929 | 823 | "Unable to create symlink 'foo' on this platform", | ||
930 | 824 | str(err)) | ||
932 | 825 | finally: | 822 | finally: |
933 | 826 | if os_symlink: | 823 | if os_symlink: |
934 | 827 | os.symlink = os_symlink | 824 | os.symlink = os_symlink |
935 | @@ -1598,6 +1595,24 @@ | |||
936 | 1598 | self.addCleanup(wt.unlock) | 1595 | self.addCleanup(wt.unlock) |
937 | 1599 | self.assertEqual(wt.kind("foo"), "symlink") | 1596 | self.assertEqual(wt.kind("foo"), "symlink") |
938 | 1600 | 1597 | ||
939 | 1598 | def test_file_to_symlink_unsupported(self): | ||
940 | 1599 | wt = self.make_branch_and_tree('.') | ||
941 | 1600 | self.build_tree(['foo']) | ||
942 | 1601 | wt.add(['foo']) | ||
943 | 1602 | wt.commit("one") | ||
944 | 1603 | self.overrideAttr(osutils, 'supports_symlinks', lambda p: False) | ||
945 | 1604 | tt = TreeTransform(wt) | ||
946 | 1605 | self.addCleanup(tt.finalize) | ||
947 | 1606 | foo_trans_id = tt.trans_id_tree_path("foo") | ||
948 | 1607 | tt.delete_contents(foo_trans_id) | ||
949 | 1608 | log = BytesIO() | ||
950 | 1609 | trace.push_log_file(log) | ||
951 | 1610 | tt.create_symlink("bar", foo_trans_id) | ||
952 | 1611 | tt.apply() | ||
953 | 1612 | self.assertContainsRe( | ||
954 | 1613 | log.getvalue(), | ||
955 | 1614 | 'Unable to create symlink "foo" on this filesystem') | ||
956 | 1615 | |||
957 | 1601 | def test_dir_to_file(self): | 1616 | def test_dir_to_file(self): |
958 | 1602 | wt = self.make_branch_and_tree('.') | 1617 | wt = self.make_branch_and_tree('.') |
959 | 1603 | self.build_tree(['foo/', 'foo/bar']) | 1618 | self.build_tree(['foo/', 'foo/bar']) |
960 | @@ -2809,6 +2824,35 @@ | |||
961 | 2809 | # 3 lines of diff administrivia | 2824 | # 3 lines of diff administrivia |
962 | 2810 | self.assertEqual(lines[4], b"+content B") | 2825 | self.assertEqual(lines[4], b"+content B") |
963 | 2811 | 2826 | ||
964 | 2827 | def test_unsupported_symlink_diff(self): | ||
965 | 2828 | self.requireFeature(SymlinkFeature) | ||
966 | 2829 | tree = self.make_branch_and_tree('.') | ||
967 | 2830 | self.build_tree_contents([('a', 'content 1')]) | ||
968 | 2831 | tree.set_root_id('TREE_ROOT') | ||
969 | 2832 | tree.add('a', 'a-id') | ||
970 | 2833 | os.symlink('a', 'foo') | ||
971 | 2834 | tree.add('foo', 'foo-id') | ||
972 | 2835 | tree.commit('rev1', rev_id='rev1') | ||
973 | 2836 | revision_tree = tree.branch.repository.revision_tree('rev1') | ||
974 | 2837 | preview = TransformPreview(revision_tree) | ||
975 | 2838 | self.addCleanup(preview.finalize) | ||
976 | 2839 | preview.delete_versioned(preview.trans_id_tree_path('foo')) | ||
977 | 2840 | preview_tree = preview.get_preview_tree() | ||
978 | 2841 | out = StringIO() | ||
979 | 2842 | log = BytesIO() | ||
980 | 2843 | trace.push_log_file(log) | ||
981 | 2844 | os_symlink = getattr(os, 'symlink', None) | ||
982 | 2845 | os.symlink = None | ||
983 | 2846 | try: | ||
984 | 2847 | show_diff_trees(revision_tree, preview_tree, out) | ||
985 | 2848 | lines = out.getvalue().splitlines() | ||
986 | 2849 | finally: | ||
987 | 2850 | if os_symlink: | ||
988 | 2851 | os.symlink = os_symlink | ||
989 | 2852 | self.assertContainsRe( | ||
990 | 2853 | log.getvalue(), | ||
991 | 2854 | 'Ignoring "foo" as symlinks are not supported on this filesystem') | ||
992 | 2855 | |||
993 | 2812 | def test_transform_conflicts(self): | 2856 | def test_transform_conflicts(self): |
994 | 2813 | revision_tree = self.create_tree() | 2857 | revision_tree = self.create_tree() |
995 | 2814 | preview = TransformPreview(revision_tree) | 2858 | preview = TransformPreview(revision_tree) |
996 | 2815 | 2859 | ||
997 | === modified file 'breezy/tests/test_workingtree.py' | |||
998 | --- breezy/tests/test_workingtree.py 2018-11-12 01:41:38 +0000 | |||
999 | +++ breezy/tests/test_workingtree.py 2019-03-04 01:57:09 +0000 | |||
1000 | @@ -15,9 +15,13 @@ | |||
1001 | 15 | # along with this program; if not, write to the Free Software | 15 | # along with this program; if not, write to the Free Software |
1002 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
1003 | 17 | 17 | ||
1004 | 18 | from io import BytesIO | ||
1005 | 19 | import os | ||
1006 | 20 | |||
1007 | 18 | from .. import ( | 21 | from .. import ( |
1008 | 19 | conflicts, | 22 | conflicts, |
1009 | 20 | errors, | 23 | errors, |
1010 | 24 | trace, | ||
1011 | 21 | transport, | 25 | transport, |
1012 | 22 | workingtree, | 26 | workingtree, |
1013 | 23 | ) | 27 | ) |
1014 | @@ -37,6 +41,7 @@ | |||
1015 | 37 | TreeLink, | 41 | TreeLink, |
1016 | 38 | ) | 42 | ) |
1017 | 39 | 43 | ||
1018 | 44 | from .features import SymlinkFeature | ||
1019 | 40 | 45 | ||
1020 | 41 | class TestTreeDirectory(TestCaseWithTransport): | 46 | class TestTreeDirectory(TestCaseWithTransport): |
1021 | 42 | 47 | ||
1022 | @@ -443,6 +448,34 @@ | |||
1023 | 443 | resolved) | 448 | resolved) |
1024 | 444 | self.assertPathDoesNotExist('this/hello.BASE') | 449 | self.assertPathDoesNotExist('this/hello.BASE') |
1025 | 445 | 450 | ||
1026 | 451 | def test_unsupported_symlink_auto_resolve(self): | ||
1027 | 452 | self.requireFeature(SymlinkFeature) | ||
1028 | 453 | base = self.make_branch_and_tree('base') | ||
1029 | 454 | self.build_tree_contents([('base/hello', 'Hello')]) | ||
1030 | 455 | base.add('hello', 'hello_id') | ||
1031 | 456 | base.commit('commit 0') | ||
1032 | 457 | other = base.controldir.sprout('other').open_workingtree() | ||
1033 | 458 | self.build_tree_contents([('other/hello', 'Hello')]) | ||
1034 | 459 | os.symlink('other/hello', 'other/foo') | ||
1035 | 460 | other.add('foo', 'foo_id') | ||
1036 | 461 | other.commit('commit symlink') | ||
1037 | 462 | this = base.controldir.sprout('this').open_workingtree() | ||
1038 | 463 | self.assertPathExists('this/hello') | ||
1039 | 464 | self.build_tree_contents([('this/hello', 'Hello')]) | ||
1040 | 465 | this.commit('commit 2') | ||
1041 | 466 | log = BytesIO() | ||
1042 | 467 | trace.push_log_file(log) | ||
1043 | 468 | os_symlink = getattr(os, 'symlink', None) | ||
1044 | 469 | os.symlink = None | ||
1045 | 470 | try: | ||
1046 | 471 | this.merge_from_branch(other.branch) | ||
1047 | 472 | finally: | ||
1048 | 473 | if os_symlink: | ||
1049 | 474 | os.symlink = os_symlink | ||
1050 | 475 | self.assertContainsRe( | ||
1051 | 476 | log.getvalue(), | ||
1052 | 477 | b'Unable to create symlink "foo" on this filesystem') | ||
1053 | 478 | |||
1054 | 446 | def test_auto_resolve_dir(self): | 479 | def test_auto_resolve_dir(self): |
1055 | 447 | tree = self.make_branch_and_tree('tree') | 480 | tree = self.make_branch_and_tree('tree') |
1056 | 448 | self.build_tree(['tree/hello/']) | 481 | self.build_tree(['tree/hello/']) |
1057 | 449 | 482 | ||
1058 | === modified file 'breezy/transform.py' | |||
1059 | --- breezy/transform.py 2019-02-02 15:13:30 +0000 | |||
1060 | +++ breezy/transform.py 2019-03-04 01:57:09 +0000 | |||
1061 | @@ -52,17 +52,16 @@ | |||
1062 | 52 | """) | 52 | """) |
1063 | 53 | from .errors import (DuplicateKey, MalformedTransform, | 53 | from .errors import (DuplicateKey, MalformedTransform, |
1064 | 54 | ReusingTransform, CantMoveRoot, | 54 | ReusingTransform, CantMoveRoot, |
1067 | 55 | ImmortalLimbo, NoFinalPath, | 55 | ImmortalLimbo, NoFinalPath) |
1066 | 56 | UnableCreateSymlink) | ||
1068 | 57 | from .filters import filtered_output_bytes, ContentFilterContext | 56 | from .filters import filtered_output_bytes, ContentFilterContext |
1069 | 58 | from .mutabletree import MutableTree | 57 | from .mutabletree import MutableTree |
1070 | 59 | from .osutils import ( | 58 | from .osutils import ( |
1071 | 60 | delete_any, | 59 | delete_any, |
1072 | 61 | file_kind, | 60 | file_kind, |
1073 | 62 | has_symlinks, | ||
1074 | 63 | pathjoin, | 61 | pathjoin, |
1075 | 64 | sha_file, | 62 | sha_file, |
1076 | 65 | splitpath, | 63 | splitpath, |
1077 | 64 | supports_symlinks, | ||
1078 | 66 | ) | 65 | ) |
1079 | 67 | from .progress import ProgressPhase | 66 | from .progress import ProgressPhase |
1080 | 68 | from .sixish import ( | 67 | from .sixish import ( |
1081 | @@ -653,6 +652,9 @@ | |||
1082 | 653 | conflicts = [] | 652 | conflicts = [] |
1083 | 654 | for trans_id in self._new_id: | 653 | for trans_id in self._new_id: |
1084 | 655 | kind = self.final_kind(trans_id) | 654 | kind = self.final_kind(trans_id) |
1085 | 655 | if kind == 'symlink' and not self._tree.supports_symlinks(): | ||
1086 | 656 | # Ignore symlinks as they are not supported on this platform | ||
1087 | 657 | continue | ||
1088 | 656 | if kind is None: | 658 | if kind is None: |
1089 | 657 | conflicts.append(('versioning no contents', trans_id)) | 659 | conflicts.append(('versioning no contents', trans_id)) |
1090 | 658 | continue | 660 | continue |
1091 | @@ -1201,8 +1203,7 @@ | |||
1092 | 1201 | class DiskTreeTransform(TreeTransformBase): | 1203 | class DiskTreeTransform(TreeTransformBase): |
1093 | 1202 | """Tree transform storing its contents on disk.""" | 1204 | """Tree transform storing its contents on disk.""" |
1094 | 1203 | 1205 | ||
1097 | 1204 | def __init__(self, tree, limbodir, pb=None, | 1206 | def __init__(self, tree, limbodir, pb=None, case_sensitive=True): |
1096 | 1205 | case_sensitive=True): | ||
1098 | 1206 | """Constructor. | 1207 | """Constructor. |
1099 | 1207 | :param tree: The tree that will be transformed, but not necessarily | 1208 | :param tree: The tree that will be transformed, but not necessarily |
1100 | 1208 | the output tree. | 1209 | the output tree. |
1101 | @@ -1226,6 +1227,7 @@ | |||
1102 | 1226 | # List of transform ids that need to be renamed from limbo into place | 1227 | # List of transform ids that need to be renamed from limbo into place |
1103 | 1227 | self._needs_rename = set() | 1228 | self._needs_rename = set() |
1104 | 1228 | self._creation_mtime = None | 1229 | self._creation_mtime = None |
1105 | 1230 | self._create_symlinks = osutils.supports_symlinks(self._limbodir) | ||
1106 | 1229 | 1231 | ||
1107 | 1230 | def finalize(self): | 1232 | def finalize(self): |
1108 | 1231 | """Release the working tree lock, if held, clean up limbo dir. | 1233 | """Release the working tree lock, if held, clean up limbo dir. |
1109 | @@ -1263,8 +1265,7 @@ | |||
1110 | 1263 | 1265 | ||
1111 | 1264 | def _limbo_supports_executable(self): | 1266 | def _limbo_supports_executable(self): |
1112 | 1265 | """Check if the limbo path supports the executable bit.""" | 1267 | """Check if the limbo path supports the executable bit.""" |
1115 | 1266 | # FIXME: Check actual file system capabilities of limbodir | 1268 | return osutils.supports_executable(self._limbodir) |
1114 | 1267 | return osutils.supports_executable() | ||
1116 | 1268 | 1269 | ||
1117 | 1269 | def _limbo_name(self, trans_id): | 1270 | def _limbo_name(self, trans_id): |
1118 | 1270 | """Generate the limbo name of a file""" | 1271 | """Generate the limbo name of a file""" |
1119 | @@ -1396,15 +1397,19 @@ | |||
1120 | 1396 | target is a bytestring. | 1397 | target is a bytestring. |
1121 | 1397 | See also new_symlink. | 1398 | See also new_symlink. |
1122 | 1398 | """ | 1399 | """ |
1124 | 1399 | if has_symlinks(): | 1400 | if self._create_symlinks: |
1125 | 1400 | os.symlink(target, self._limbo_name(trans_id)) | 1401 | os.symlink(target, self._limbo_name(trans_id)) |
1126 | 1401 | unique_add(self._new_contents, trans_id, 'symlink') | ||
1127 | 1402 | else: | 1402 | else: |
1128 | 1403 | try: | 1403 | try: |
1129 | 1404 | path = FinalPaths(self).get_path(trans_id) | 1404 | path = FinalPaths(self).get_path(trans_id) |
1130 | 1405 | except KeyError: | 1405 | except KeyError: |
1131 | 1406 | path = None | 1406 | path = None |
1133 | 1407 | raise UnableCreateSymlink(path=path) | 1407 | trace.warning( |
1134 | 1408 | 'Unable to create symlink "%s" on this filesystem.' % (path,)) | ||
1135 | 1409 | # We add symlink to _new_contents even if they are unsupported | ||
1136 | 1410 | # and not created. These entries are subsequently used to avoid | ||
1137 | 1411 | # conflicts on platforms that don't support symlink | ||
1138 | 1412 | unique_add(self._new_contents, trans_id, 'symlink') | ||
1139 | 1408 | 1413 | ||
1140 | 1409 | def cancel_creation(self, trans_id): | 1414 | def cancel_creation(self, trans_id): |
1141 | 1410 | """Cancel the creation of new file contents.""" | 1415 | """Cancel the creation of new file contents.""" |
1142 | 1411 | 1416 | ||
1143 | === modified file 'breezy/transport/local.py' | |||
1144 | --- breezy/transport/local.py 2018-11-11 04:08:32 +0000 | |||
1145 | +++ breezy/transport/local.py 2019-03-04 01:57:09 +0000 | |||
1146 | @@ -532,7 +532,7 @@ | |||
1147 | 532 | except (IOError, OSError) as e: | 532 | except (IOError, OSError) as e: |
1148 | 533 | self._translate_error(e, source) | 533 | self._translate_error(e, source) |
1149 | 534 | 534 | ||
1151 | 535 | if osutils.has_symlinks(): | 535 | if getattr(os, 'symlink', None) is not None: |
1152 | 536 | def symlink(self, source, link_name): | 536 | def symlink(self, source, link_name): |
1153 | 537 | """See Transport.symlink.""" | 537 | """See Transport.symlink.""" |
1154 | 538 | abs_link_dirpath = urlutils.dirname(self.abspath(link_name)) | 538 | abs_link_dirpath = urlutils.dirname(self.abspath(link_name)) |
1155 | 539 | 539 | ||
1156 | === modified file 'breezy/transport/memory.py' | |||
1157 | --- breezy/transport/memory.py 2018-11-17 16:53:10 +0000 | |||
1158 | +++ breezy/transport/memory.py 2019-03-04 01:57:09 +0000 | |||
1159 | @@ -26,9 +26,10 @@ | |||
1160 | 26 | from io import ( | 26 | from io import ( |
1161 | 27 | BytesIO, | 27 | BytesIO, |
1162 | 28 | ) | 28 | ) |
1163 | 29 | import itertools | ||
1164 | 29 | import os | 30 | import os |
1165 | 30 | import errno | 31 | import errno |
1167 | 31 | from stat import S_IFREG, S_IFDIR, S_IFLNK | 32 | from stat import S_IFREG, S_IFDIR, S_IFLNK, S_ISDIR |
1168 | 32 | 33 | ||
1169 | 33 | from .. import ( | 34 | from .. import ( |
1170 | 34 | transport, | 35 | transport, |
1171 | @@ -50,20 +51,16 @@ | |||
1172 | 50 | 51 | ||
1173 | 51 | class MemoryStat(object): | 52 | class MemoryStat(object): |
1174 | 52 | 53 | ||
1176 | 53 | def __init__(self, size, kind, perms): | 54 | def __init__(self, size, kind, perms=None): |
1177 | 54 | self.st_size = size | 55 | self.st_size = size |
1179 | 55 | if kind == 'file': | 56 | if not S_ISDIR(kind): |
1180 | 56 | if perms is None: | 57 | if perms is None: |
1181 | 57 | perms = 0o644 | 58 | perms = 0o644 |
1184 | 58 | self.st_mode = S_IFREG | perms | 59 | self.st_mode = kind | perms |
1185 | 59 | elif kind == 'directory': | 60 | else: |
1186 | 60 | if perms is None: | 61 | if perms is None: |
1187 | 61 | perms = 0o755 | 62 | perms = 0o755 |
1193 | 62 | self.st_mode = S_IFDIR | perms | 63 | self.st_mode = kind | perms |
1189 | 63 | elif kind == 'symlink': | ||
1190 | 64 | self.st_mode = S_IFLNK | 0o644 | ||
1191 | 65 | else: | ||
1192 | 66 | raise AssertionError('unknown kind %r' % kind) | ||
1194 | 67 | 64 | ||
1195 | 68 | 65 | ||
1196 | 69 | class MemoryTransport(transport.Transport): | 66 | class MemoryTransport(transport.Transport): |
1197 | @@ -80,7 +77,7 @@ | |||
1198 | 80 | self._scheme = url[:split] | 77 | self._scheme = url[:split] |
1199 | 81 | self._cwd = url[split:] | 78 | self._cwd = url[split:] |
1200 | 82 | # dictionaries from absolute path to file mode | 79 | # dictionaries from absolute path to file mode |
1202 | 83 | self._dirs = {'/': None} | 80 | self._dirs = {'/':None} |
1203 | 84 | self._symlinks = {} | 81 | self._symlinks = {} |
1204 | 85 | self._files = {} | 82 | self._files = {} |
1205 | 86 | self._locks = {} | 83 | self._locks = {} |
1206 | @@ -111,7 +108,7 @@ | |||
1207 | 111 | 108 | ||
1208 | 112 | def append_file(self, relpath, f, mode=None): | 109 | def append_file(self, relpath, f, mode=None): |
1209 | 113 | """See Transport.append_file().""" | 110 | """See Transport.append_file().""" |
1211 | 114 | _abspath = self._abspath(relpath) | 111 | _abspath = self._resolve_symlinks(relpath) |
1212 | 115 | self._check_parent(_abspath) | 112 | self._check_parent(_abspath) |
1213 | 116 | orig_content, orig_mode = self._files.get(_abspath, (b"", None)) | 113 | orig_content, orig_mode = self._files.get(_abspath, (b"", None)) |
1214 | 117 | if mode is None: | 114 | if mode is None: |
1215 | @@ -128,16 +125,20 @@ | |||
1216 | 128 | def has(self, relpath): | 125 | def has(self, relpath): |
1217 | 129 | """See Transport.has().""" | 126 | """See Transport.has().""" |
1218 | 130 | _abspath = self._abspath(relpath) | 127 | _abspath = self._abspath(relpath) |
1222 | 131 | return ((_abspath in self._files) | 128 | for container in (self._files, self._dirs, self._symlinks): |
1223 | 132 | or (_abspath in self._dirs) | 129 | if _abspath in container.keys(): |
1224 | 133 | or (_abspath in self._symlinks)) | 130 | return True |
1225 | 131 | return False | ||
1226 | 134 | 132 | ||
1227 | 135 | def delete(self, relpath): | 133 | def delete(self, relpath): |
1228 | 136 | """See Transport.delete().""" | 134 | """See Transport.delete().""" |
1229 | 137 | _abspath = self._abspath(relpath) | 135 | _abspath = self._abspath(relpath) |
1231 | 138 | if _abspath not in self._files: | 136 | if _abspath in self._files: |
1232 | 137 | del self._files[_abspath] | ||
1233 | 138 | elif _abspath in self._symlinks: | ||
1234 | 139 | del self._symlinks[_abspath] | ||
1235 | 140 | else: | ||
1236 | 139 | raise NoSuchFile(relpath) | 141 | raise NoSuchFile(relpath) |
1237 | 140 | del self._files[_abspath] | ||
1238 | 141 | 142 | ||
1239 | 142 | def external_url(self): | 143 | def external_url(self): |
1240 | 143 | """See breezy.transport.Transport.external_url.""" | 144 | """See breezy.transport.Transport.external_url.""" |
1241 | @@ -147,8 +148,8 @@ | |||
1242 | 147 | 148 | ||
1243 | 148 | def get(self, relpath): | 149 | def get(self, relpath): |
1244 | 149 | """See Transport.get().""" | 150 | """See Transport.get().""" |
1247 | 150 | _abspath = self._abspath(relpath) | 151 | _abspath = self._resolve_symlinks(relpath) |
1248 | 151 | if _abspath not in self._files: | 152 | if not _abspath in self._files: |
1249 | 152 | if _abspath in self._dirs: | 153 | if _abspath in self._dirs: |
1250 | 153 | return LateReadError(relpath) | 154 | return LateReadError(relpath) |
1251 | 154 | else: | 155 | else: |
1252 | @@ -157,15 +158,20 @@ | |||
1253 | 157 | 158 | ||
1254 | 158 | def put_file(self, relpath, f, mode=None): | 159 | def put_file(self, relpath, f, mode=None): |
1255 | 159 | """See Transport.put_file().""" | 160 | """See Transport.put_file().""" |
1257 | 160 | _abspath = self._abspath(relpath) | 161 | _abspath = self._resolve_symlinks(relpath) |
1258 | 161 | self._check_parent(_abspath) | 162 | self._check_parent(_abspath) |
1259 | 162 | raw_bytes = f.read() | 163 | raw_bytes = f.read() |
1260 | 163 | self._files[_abspath] = (raw_bytes, mode) | 164 | self._files[_abspath] = (raw_bytes, mode) |
1261 | 164 | return len(raw_bytes) | 165 | return len(raw_bytes) |
1262 | 165 | 166 | ||
1263 | 167 | def symlink(self, source, target): | ||
1264 | 168 | _abspath = self._resolve_symlinks(target) | ||
1265 | 169 | self._check_parent(_abspath) | ||
1266 | 170 | self._symlinks[_abspath] = self._abspath(source) | ||
1267 | 171 | |||
1268 | 166 | def mkdir(self, relpath, mode=None): | 172 | def mkdir(self, relpath, mode=None): |
1269 | 167 | """See Transport.mkdir().""" | 173 | """See Transport.mkdir().""" |
1271 | 168 | _abspath = self._abspath(relpath) | 174 | _abspath = self._resolve_symlinks(relpath) |
1272 | 169 | self._check_parent(_abspath) | 175 | self._check_parent(_abspath) |
1273 | 170 | if _abspath in self._dirs: | 176 | if _abspath in self._dirs: |
1274 | 171 | raise FileExists(relpath) | 177 | raise FileExists(relpath) |
1275 | @@ -183,13 +189,13 @@ | |||
1276 | 183 | return True | 189 | return True |
1277 | 184 | 190 | ||
1278 | 185 | def iter_files_recursive(self): | 191 | def iter_files_recursive(self): |
1280 | 186 | for file in self._files: | 192 | for file in itertools.chain(self._files, self._symlinks): |
1281 | 187 | if file.startswith(self._cwd): | 193 | if file.startswith(self._cwd): |
1282 | 188 | yield urlutils.escape(file[len(self._cwd):]) | 194 | yield urlutils.escape(file[len(self._cwd):]) |
1283 | 189 | 195 | ||
1284 | 190 | def list_dir(self, relpath): | 196 | def list_dir(self, relpath): |
1285 | 191 | """See Transport.list_dir().""" | 197 | """See Transport.list_dir().""" |
1287 | 192 | _abspath = self._abspath(relpath) | 198 | _abspath = self._resolve_symlinks(relpath) |
1288 | 193 | if _abspath != '/' and _abspath not in self._dirs: | 199 | if _abspath != '/' and _abspath not in self._dirs: |
1289 | 194 | raise NoSuchFile(relpath) | 200 | raise NoSuchFile(relpath) |
1290 | 195 | result = [] | 201 | result = [] |
1291 | @@ -197,7 +203,7 @@ | |||
1292 | 197 | if not _abspath.endswith('/'): | 203 | if not _abspath.endswith('/'): |
1293 | 198 | _abspath += '/' | 204 | _abspath += '/' |
1294 | 199 | 205 | ||
1296 | 200 | for path_group in self._files, self._dirs: | 206 | for path_group in self._files, self._dirs, self._symlinks: |
1297 | 201 | for path in path_group: | 207 | for path in path_group: |
1298 | 202 | if path.startswith(_abspath): | 208 | if path.startswith(_abspath): |
1299 | 203 | trailing = path[len(_abspath):] | 209 | trailing = path[len(_abspath):] |
1300 | @@ -207,8 +213,8 @@ | |||
1301 | 207 | 213 | ||
1302 | 208 | def rename(self, rel_from, rel_to): | 214 | def rename(self, rel_from, rel_to): |
1303 | 209 | """Rename a file or directory; fail if the destination exists""" | 215 | """Rename a file or directory; fail if the destination exists""" |
1306 | 210 | abs_from = self._abspath(rel_from) | 216 | abs_from = self._resolve_symlinks(rel_from) |
1307 | 211 | abs_to = self._abspath(rel_to) | 217 | abs_to = self._resolve_symlinks(rel_to) |
1308 | 212 | 218 | ||
1309 | 213 | def replace(x): | 219 | def replace(x): |
1310 | 214 | if x == abs_from: | 220 | if x == abs_from: |
1311 | @@ -233,21 +239,25 @@ | |||
1312 | 233 | # fail differently depending on dict order. So work on copy, fail on | 239 | # fail differently depending on dict order. So work on copy, fail on |
1313 | 234 | # error on only replace dicts if all goes well. | 240 | # error on only replace dicts if all goes well. |
1314 | 235 | renamed_files = self._files.copy() | 241 | renamed_files = self._files.copy() |
1315 | 242 | renamed_symlinks = self._symlinks.copy() | ||
1316 | 236 | renamed_dirs = self._dirs.copy() | 243 | renamed_dirs = self._dirs.copy() |
1317 | 237 | do_renames(renamed_files) | 244 | do_renames(renamed_files) |
1318 | 245 | do_renames(renamed_symlinks) | ||
1319 | 238 | do_renames(renamed_dirs) | 246 | do_renames(renamed_dirs) |
1320 | 239 | # We may have been cloned so modify in place | 247 | # We may have been cloned so modify in place |
1321 | 240 | self._files.clear() | 248 | self._files.clear() |
1322 | 241 | self._files.update(renamed_files) | 249 | self._files.update(renamed_files) |
1323 | 250 | self._symlinks.clear() | ||
1324 | 251 | self._symlinks.update(renamed_symlinks) | ||
1325 | 242 | self._dirs.clear() | 252 | self._dirs.clear() |
1326 | 243 | self._dirs.update(renamed_dirs) | 253 | self._dirs.update(renamed_dirs) |
1327 | 244 | 254 | ||
1328 | 245 | def rmdir(self, relpath): | 255 | def rmdir(self, relpath): |
1329 | 246 | """See Transport.rmdir.""" | 256 | """See Transport.rmdir.""" |
1331 | 247 | _abspath = self._abspath(relpath) | 257 | _abspath = self._resolve_symlinks(relpath) |
1332 | 248 | if _abspath in self._files: | 258 | if _abspath in self._files: |
1333 | 249 | self._translate_error(IOError(errno.ENOTDIR, relpath), relpath) | 259 | self._translate_error(IOError(errno.ENOTDIR, relpath), relpath) |
1335 | 250 | for path in self._files: | 260 | for path in itertools.chain(self._files, self._symlinks): |
1336 | 251 | if path.startswith(_abspath + '/'): | 261 | if path.startswith(_abspath + '/'): |
1337 | 252 | self._translate_error(IOError(errno.ENOTEMPTY, relpath), | 262 | self._translate_error(IOError(errno.ENOTEMPTY, relpath), |
1338 | 253 | relpath) | 263 | relpath) |
1339 | @@ -262,13 +272,13 @@ | |||
1340 | 262 | def stat(self, relpath): | 272 | def stat(self, relpath): |
1341 | 263 | """See Transport.stat().""" | 273 | """See Transport.stat().""" |
1342 | 264 | _abspath = self._abspath(relpath) | 274 | _abspath = self._abspath(relpath) |
1345 | 265 | if _abspath in self._files: | 275 | if _abspath in self._files.keys(): |
1346 | 266 | return MemoryStat(len(self._files[_abspath][0]), 'file', | 276 | return MemoryStat(len(self._files[_abspath][0]), S_IFREG, |
1347 | 267 | self._files[_abspath][1]) | 277 | self._files[_abspath][1]) |
1352 | 268 | elif _abspath in self._dirs: | 278 | elif _abspath in self._dirs.keys(): |
1353 | 269 | return MemoryStat(0, 'directory', self._dirs[_abspath]) | 279 | return MemoryStat(0, S_IFDIR, self._dirs[_abspath]) |
1354 | 270 | elif _abspath in self._symlinks: | 280 | elif _abspath in self._symlinks.keys(): |
1355 | 271 | return MemoryStat(0, 'symlink', 0) | 281 | return MemoryStat(0, S_IFLNK) |
1356 | 272 | else: | 282 | else: |
1357 | 273 | raise NoSuchFile(_abspath) | 283 | raise NoSuchFile(_abspath) |
1358 | 274 | 284 | ||
1359 | @@ -280,6 +290,12 @@ | |||
1360 | 280 | """See Transport.lock_write().""" | 290 | """See Transport.lock_write().""" |
1361 | 281 | return _MemoryLock(self._abspath(relpath), self) | 291 | return _MemoryLock(self._abspath(relpath), self) |
1362 | 282 | 292 | ||
1363 | 293 | def _resolve_symlinks(self, relpath): | ||
1364 | 294 | path = self._abspath(relpath) | ||
1365 | 295 | while path in self._symlinks.keys(): | ||
1366 | 296 | path = self._symlinks[path] | ||
1367 | 297 | return path | ||
1368 | 298 | |||
1369 | 283 | def _abspath(self, relpath): | 299 | def _abspath(self, relpath): |
1370 | 284 | """Generate an internal absolute path.""" | 300 | """Generate an internal absolute path.""" |
1371 | 285 | relpath = urlutils.unescape(relpath) | 301 | relpath = urlutils.unescape(relpath) |
1372 | @@ -336,6 +352,7 @@ | |||
1373 | 336 | def start_server(self): | 352 | def start_server(self): |
1374 | 337 | self._dirs = {'/': None} | 353 | self._dirs = {'/': None} |
1375 | 338 | self._files = {} | 354 | self._files = {} |
1376 | 355 | self._symlinks = {} | ||
1377 | 339 | self._locks = {} | 356 | self._locks = {} |
1378 | 340 | self._scheme = "memory+%s:///" % id(self) | 357 | self._scheme = "memory+%s:///" % id(self) |
1379 | 341 | 358 | ||
1380 | @@ -344,6 +361,7 @@ | |||
1381 | 344 | result = memory.MemoryTransport(url) | 361 | result = memory.MemoryTransport(url) |
1382 | 345 | result._dirs = self._dirs | 362 | result._dirs = self._dirs |
1383 | 346 | result._files = self._files | 363 | result._files = self._files |
1384 | 364 | result._symlinks = self._symlinks | ||
1385 | 347 | result._locks = self._locks | 365 | result._locks = self._locks |
1386 | 348 | return result | 366 | return result |
1387 | 349 | self._memory_factory = memory_factory | 367 | self._memory_factory = memory_factory |
1388 | 350 | 368 | ||
1389 | === modified file 'breezy/tree.py' | |||
1390 | --- breezy/tree.py 2019-02-02 15:13:30 +0000 | |||
1391 | +++ breezy/tree.py 2019-03-04 01:57:09 +0000 | |||
1392 | @@ -141,6 +141,11 @@ | |||
1393 | 141 | """ | 141 | """ |
1394 | 142 | return True | 142 | return True |
1395 | 143 | 143 | ||
1396 | 144 | def supports_symlinks(self): | ||
1397 | 145 | """Does this tree support symbolic links? | ||
1398 | 146 | """ | ||
1399 | 147 | return osutils.has_symlinks() | ||
1400 | 148 | |||
1401 | 144 | def changes_from(self, other, want_unchanged=False, specific_files=None, | 149 | def changes_from(self, other, want_unchanged=False, specific_files=None, |
1402 | 145 | extra_trees=None, require_versioned=False, include_root=False, | 150 | extra_trees=None, require_versioned=False, include_root=False, |
1403 | 146 | want_unversioned=False): | 151 | want_unversioned=False): |
1404 | @@ -181,7 +186,8 @@ | |||
1405 | 181 | """See InterTree.iter_changes""" | 186 | """See InterTree.iter_changes""" |
1406 | 182 | intertree = InterTree.get(from_tree, self) | 187 | intertree = InterTree.get(from_tree, self) |
1407 | 183 | return intertree.iter_changes(include_unchanged, specific_files, pb, | 188 | return intertree.iter_changes(include_unchanged, specific_files, pb, |
1409 | 184 | extra_trees, require_versioned, want_unversioned=want_unversioned) | 189 | extra_trees, require_versioned, |
1410 | 190 | want_unversioned=want_unversioned) | ||
1411 | 185 | 191 | ||
1412 | 186 | def conflicts(self): | 192 | def conflicts(self): |
1413 | 187 | """Get a list of the conflicts in the tree. | 193 | """Get a list of the conflicts in the tree. |
1414 | 188 | 194 | ||
1415 | === modified file 'breezy/workingtree.py' | |||
1416 | --- breezy/workingtree.py 2019-02-14 22:18:59 +0000 | |||
1417 | +++ breezy/workingtree.py 2019-03-04 01:57:09 +0000 | |||
1418 | @@ -65,9 +65,6 @@ | |||
1419 | 65 | from .trace import mutter, note | 65 | from .trace import mutter, note |
1420 | 66 | 66 | ||
1421 | 67 | 67 | ||
1422 | 68 | ERROR_PATH_NOT_FOUND = 3 # WindowsError errno code, equivalent to ENOENT | ||
1423 | 69 | |||
1424 | 70 | |||
1425 | 71 | class SettingFileIdUnsupported(errors.BzrError): | 68 | class SettingFileIdUnsupported(errors.BzrError): |
1426 | 72 | 69 | ||
1427 | 73 | _fmt = "This format does not support setting file ids." | 70 | _fmt = "This format does not support setting file ids." |
1428 | @@ -123,6 +120,9 @@ | |||
1429 | 123 | def control_transport(self): | 120 | def control_transport(self): |
1430 | 124 | return self._transport | 121 | return self._transport |
1431 | 125 | 122 | ||
1432 | 123 | def supports_symlinks(self): | ||
1433 | 124 | return osutils.supports_symlinks(self.basedir) | ||
1434 | 125 | |||
1435 | 126 | def is_control_filename(self, filename): | 126 | def is_control_filename(self, filename): |
1436 | 127 | """True if filename is the name of a control file in this tree. | 127 | """True if filename is the name of a control file in this tree. |
1437 | 128 | 128 | ||
1438 | @@ -153,10 +153,7 @@ | |||
1439 | 153 | return self._format.supports_merge_modified | 153 | return self._format.supports_merge_modified |
1440 | 154 | 154 | ||
1441 | 155 | def _supports_executable(self): | 155 | def _supports_executable(self): |
1446 | 156 | if sys.platform == 'win32': | 156 | return osutils.supports_executable(self.basedir) |
1443 | 157 | return False | ||
1444 | 158 | # FIXME: Ideally this should check the file system | ||
1445 | 159 | return True | ||
1447 | 160 | 157 | ||
1448 | 161 | def break_lock(self): | 158 | def break_lock(self): |
1449 | 162 | """Break a lock if one is present from another instance. | 159 | """Break a lock if one is present from another instance. |
1450 | 163 | 160 | ||
1451 | === modified file 'doc/en/release-notes/brz-3.0.txt' | |||
1452 | --- doc/en/release-notes/brz-3.0.txt 2019-03-02 22:31:28 +0000 | |||
1453 | +++ doc/en/release-notes/brz-3.0.txt 2019-03-04 01:57:09 +0000 | |||
1454 | @@ -158,6 +158,11 @@ | |||
1455 | 158 | ``RevisionTree.annotate_iter`` have been added. (Jelmer Vernooij, | 158 | ``RevisionTree.annotate_iter`` have been added. (Jelmer Vernooij, |
1456 | 159 | #897781) | 159 | #897781) |
1457 | 160 | 160 | ||
1458 | 161 | * Branches with symlinks are now supported on Windows. Symlinks are | ||
1459 | 162 | ignored by operations like branch, diff etc. with a warning as Symlinks | ||
1460 | 163 | are not created on Windows. | ||
1461 | 164 | (Parth Malwankar, #81689) | ||
1462 | 165 | |||
1463 | 161 | * New ``lp+bzr://`` URL scheme for Bazaar-only branches on Launchpad. | 166 | * New ``lp+bzr://`` URL scheme for Bazaar-only branches on Launchpad. |
1464 | 162 | (Jelmer Vernooij) | 167 | (Jelmer Vernooij) |
1465 | 163 | 168 |