Merge lp:~jelmer/brz/win-symlink-warning into lp:brz
- win-symlink-warning
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Jelmer Vernooij | ||||
Approved revision: | no longer in the source branch. | ||||
Merge reported by: | The Breezy Bot | ||||
Merged at revision: | not available | ||||
Proposed branch: | lp:~jelmer/brz/win-symlink-warning | ||||
Merge into: | lp:brz | ||||
Prerequisite: | lp:~jelmer/brz/osutils-fstype | ||||
Diff against target: |
1002 lines (+362/-98) 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 (+8/-3) breezy/diff.py (+5/-0) breezy/errors.py (+0/-15) breezy/git/memorytree.py (+3/-0) breezy/git/tree.py (+7/-0) breezy/git/workingtree.py (+6/-8) breezy/memorytree.py (+3/-0) breezy/mutabletree.py (+27/-7) breezy/osutils.py (+57/-2) breezy/tests/per_tree/test_symlinks.py (+40/-0) breezy/tests/test__dirstate_helpers.py (+5/-3) breezy/tests/test_commit.py (+31/-0) breezy/tests/test_dirstate.py (+5/-3) breezy/tests/test_errors.py (+0/-14) 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/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 |
---|---|---|---|
Martin Packman | Approve | ||
Review via email:
|
This proposal supersedes a proposal from 2019-03-04.
Commit message
Allow symbolic links to exist when checking out trees on Windows.
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
The Breezy Bot (the-breezy-bot) wrote : | # |
Merging failed
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
The Breezy Bot (the-breezy-bot) wrote : | # |
Running landing tests failed
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
The Breezy Bot (the-breezy-bot) wrote : | # |
Running landing tests failed
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
The Breezy Bot (the-breezy-bot) wrote : | # |
Running landing tests failed
https:/
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
The Breezy Bot (the-breezy-bot) wrote : | # |
Running landing tests failed
https:/
Preview Diff
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-06-03 23:46:01 +0000 |
4 | @@ -366,7 +366,8 @@ |
5 | HEADER_FORMAT_2 = b'#bazaar dirstate flat format 2\n' |
6 | HEADER_FORMAT_3 = b'#bazaar dirstate flat format 3\n' |
7 | |
8 | - def __init__(self, path, sha1_provider, worth_saving_limit=0): |
9 | + def __init__(self, path, sha1_provider, worth_saving_limit=0, |
10 | + use_filesystem_for_exec=True): |
11 | """Create a DirState object. |
12 | |
13 | :param path: The path at which the dirstate file on disk should live. |
14 | @@ -375,6 +376,8 @@ |
15 | entries is known, only bother saving the dirstate if more than |
16 | this count of entries have changed. |
17 | -1 means never save hash changes, 0 means always save hash changes. |
18 | + :param use_filesystem_for_exec: Whether to trust the filesystem |
19 | + for executable bit information |
20 | """ |
21 | # _header_state and _dirblock_state represent the current state |
22 | # of the dirstate metadata and the per-row data respectiely. |
23 | @@ -423,6 +426,7 @@ |
24 | self._worth_saving_limit = worth_saving_limit |
25 | self._config_stack = config.LocationStack(urlutils.local_path_to_url( |
26 | path)) |
27 | + self._use_filesystem_for_exec = use_filesystem_for_exec |
28 | |
29 | def __repr__(self): |
30 | return "%s(%r)" % \ |
31 | @@ -1949,14 +1953,10 @@ |
32 | |
33 | def _is_executable(self, mode, old_executable): |
34 | """Is this file executable?""" |
35 | - return bool(S_IEXEC & mode) |
36 | - |
37 | - def _is_executable_win32(self, mode, old_executable): |
38 | - """On win32 the executable bit is stored in the dirstate.""" |
39 | - return old_executable |
40 | - |
41 | - if sys.platform == 'win32': |
42 | - _is_executable = _is_executable_win32 |
43 | + if self._use_filesystem_for_exec: |
44 | + return bool(S_IEXEC & mode) |
45 | + else: |
46 | + return old_executable |
47 | |
48 | def _read_link(self, abspath, old_link): |
49 | """Read the target of a symlink""" |
50 | @@ -2403,7 +2403,8 @@ |
51 | return len(self._parents) - len(self._ghosts) |
52 | |
53 | @classmethod |
54 | - def on_file(cls, path, sha1_provider=None, worth_saving_limit=0): |
55 | + def on_file(cls, path, sha1_provider=None, worth_saving_limit=0, |
56 | + use_filesystem_for_exec=True): |
57 | """Construct a DirState on the file at path "path". |
58 | |
59 | :param path: The path at which the dirstate file on disk should live. |
60 | @@ -2412,12 +2413,15 @@ |
61 | :param worth_saving_limit: when the exact number of hash changed |
62 | entries is known, only bother saving the dirstate if more than |
63 | this count of entries have changed. -1 means never save. |
64 | + :param use_filesystem_for_exec: Whether to trust the filesystem |
65 | + for executable bit information |
66 | :return: An unlocked DirState object, associated with the given path. |
67 | """ |
68 | if sha1_provider is None: |
69 | sha1_provider = DefaultSHA1Provider() |
70 | result = cls(path, sha1_provider, |
71 | - worth_saving_limit=worth_saving_limit) |
72 | + worth_saving_limit=worth_saving_limit, |
73 | + use_filesystem_for_exec=use_filesystem_for_exec) |
74 | return result |
75 | |
76 | def _read_dirblocks_if_needed(self): |
77 | |
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-06-03 23:46:01 +0000 |
81 | @@ -573,7 +573,7 @@ |
82 | # expand any symlinks in the directory part, while leaving the |
83 | # filename alone |
84 | # only expanding if symlinks are supported avoids windows path bugs |
85 | - if osutils.has_symlinks(): |
86 | + if self.tree.supports_symlinks(): |
87 | file_list = list(map(osutils.normalizepath, file_list)) |
88 | |
89 | user_dirs = {} |
90 | |
91 | === modified file 'breezy/bzr/workingtree.py' |
92 | --- breezy/bzr/workingtree.py 2019-06-02 21:29:09 +0000 |
93 | +++ breezy/bzr/workingtree.py 2019-06-03 23:46:01 +0000 |
94 | @@ -96,6 +96,7 @@ |
95 | # impossible as there is no clear relationship between the working tree format |
96 | # and the conflict list file format. |
97 | CONFLICT_HEADER_1 = b"BZR conflict list format 1" |
98 | +ERROR_PATH_NOT_FOUND = 3 # WindowsError errno code, equivalent to ENOENT |
99 | |
100 | |
101 | class InventoryWorkingTree(WorkingTree, MutableInventoryTree): |
102 | |
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-06-03 23:46:01 +0000 |
106 | @@ -256,7 +256,8 @@ |
107 | local_path = self.controldir.get_workingtree_transport( |
108 | None).local_abspath('dirstate') |
109 | self._dirstate = dirstate.DirState.on_file( |
110 | - local_path, self._sha1_provider(), self._worth_saving_limit()) |
111 | + local_path, self._sha1_provider(), self._worth_saving_limit(), |
112 | + self._supports_executable()) |
113 | return self._dirstate |
114 | |
115 | def _sha1_provider(self): |
116 | |
117 | === modified file 'breezy/commit.py' |
118 | --- breezy/commit.py 2018-11-25 20:44:56 +0000 |
119 | +++ breezy/commit.py 2019-06-03 23:46:01 +0000 |
120 | @@ -64,6 +64,7 @@ |
121 | StrictCommitFailed |
122 | ) |
123 | from .osutils import (get_user_encoding, |
124 | + has_symlinks, |
125 | is_inside_any, |
126 | minimum_path_selection, |
127 | ) |
128 | @@ -709,6 +710,10 @@ |
129 | # 'missing' path |
130 | if report_changes: |
131 | reporter.missing(new_path) |
132 | + if change[6][0] == 'symlink' and not self.work_tree.supports_symlinks(): |
133 | + trace.warning('Ignoring "%s" as symlinks are not ' |
134 | + 'supported on this filesystem.' % (change[1][0],)) |
135 | + continue |
136 | deleted_paths.append(change[1][1]) |
137 | # Reset the new path (None) and new versioned flag (False) |
138 | change = (change[0], (change[1][0], None), change[2], |
139 | |
140 | === modified file 'breezy/delta.py' |
141 | --- breezy/delta.py 2018-11-11 04:08:32 +0000 |
142 | +++ breezy/delta.py 2019-06-03 23:46:01 +0000 |
143 | @@ -18,11 +18,11 @@ |
144 | |
145 | from breezy import ( |
146 | osutils, |
147 | + trace, |
148 | ) |
149 | from .sixish import ( |
150 | StringIO, |
151 | ) |
152 | -from .trace import is_quiet |
153 | |
154 | |
155 | class TreeDelta(object): |
156 | @@ -141,7 +141,12 @@ |
157 | if fully_present[1] is True: |
158 | delta.added.append((path[1], file_id, kind[1])) |
159 | else: |
160 | - delta.removed.append((path[0], file_id, kind[0])) |
161 | + if kind[0] == 'symlink' and not new_tree.supports_symlinks(): |
162 | + trace.warning( |
163 | + 'Ignoring "%s" as symlinks ' |
164 | + 'are not supported on this filesystem.' % (path[0],)) |
165 | + else: |
166 | + delta.removed.append((path[0], file_id, kind[0])) |
167 | elif fully_present[0] is False: |
168 | delta.missing.append((path[1], file_id, kind[1])) |
169 | elif name[0] != name[1] or parent_id[0] != parent_id[1]: |
170 | @@ -253,7 +258,7 @@ |
171 | :param kind: A pair of file kinds, as generated by Tree.iter_changes. |
172 | None indicates no file present. |
173 | """ |
174 | - if is_quiet(): |
175 | + if trace.is_quiet(): |
176 | return |
177 | if paths[1] == '' and versioned == 'added' and self.suppress_root_add: |
178 | return |
179 | |
180 | === modified file 'breezy/diff.py' |
181 | --- breezy/diff.py 2019-05-28 23:22:20 +0000 |
182 | +++ breezy/diff.py 2019-06-03 23:46:01 +0000 |
183 | @@ -1050,6 +1050,11 @@ |
184 | # is, missing) in both trees are skipped as well. |
185 | if parent == (None, None) or kind == (None, None): |
186 | continue |
187 | + if kind[0] == 'symlink' and not self.new_tree.supports_symlinks(): |
188 | + warning( |
189 | + 'Ignoring "%s" as symlinks are not ' |
190 | + 'supported on this filesystem.' % (paths[0],)) |
191 | + continue |
192 | oldpath, newpath = paths |
193 | oldpath_encoded = get_encoded_path(paths[0]) |
194 | newpath_encoded = get_encoded_path(paths[1]) |
195 | |
196 | === modified file 'breezy/errors.py' |
197 | --- breezy/errors.py 2019-02-15 18:57:38 +0000 |
198 | +++ breezy/errors.py 2019-06-03 23:46:01 +0000 |
199 | @@ -2303,21 +2303,6 @@ |
200 | ' (See brz shelve --list).%(more)s') |
201 | |
202 | |
203 | -class UnableCreateSymlink(BzrError): |
204 | - |
205 | - _fmt = 'Unable to create symlink %(path_str)son this platform' |
206 | - |
207 | - def __init__(self, path=None): |
208 | - path_str = '' |
209 | - if path: |
210 | - try: |
211 | - path_str = repr(str(path)) |
212 | - except UnicodeEncodeError: |
213 | - path_str = repr(path) |
214 | - path_str += ' ' |
215 | - self.path_str = path_str |
216 | - |
217 | - |
218 | class UnableEncodePath(BzrError): |
219 | |
220 | _fmt = ('Unable to encode %(kind)s path %(path)r in ' |
221 | |
222 | === modified file 'breezy/git/memorytree.py' |
223 | --- breezy/git/memorytree.py 2019-03-04 00:35:52 +0000 |
224 | +++ breezy/git/memorytree.py 2019-06-03 23:46:01 +0000 |
225 | @@ -58,6 +58,9 @@ |
226 | self._lock_mode = None |
227 | self._populate_from_branch() |
228 | |
229 | + def _supports_executable(self): |
230 | + return True |
231 | + |
232 | @property |
233 | def controldir(self): |
234 | return self.branch.controldir |
235 | |
236 | === modified file 'breezy/git/tree.py' |
237 | --- breezy/git/tree.py 2019-06-02 05:13:10 +0000 |
238 | +++ breezy/git/tree.py 2019-06-03 23:46:01 +0000 |
239 | @@ -1447,6 +1447,7 @@ |
240 | # Report dirified directories to commit_tree first, so that they can be |
241 | # replaced with non-empty directories if they have contents. |
242 | dirified = [] |
243 | + trust_executable = target._supports_executable() |
244 | for path, index_entry in target._recurse_index_entries(): |
245 | try: |
246 | live_entry = target._live_entry(path) |
247 | @@ -1472,6 +1473,12 @@ |
248 | dirified.append((path, Tree().id, stat.S_IFDIR)) |
249 | store.add_object(Tree()) |
250 | else: |
251 | + mode = live_entry.mode |
252 | + if not trust_executable: |
253 | + if mode_is_executable(index_entry.mode): |
254 | + mode |= 0o111 |
255 | + else: |
256 | + mode &= ~0o111 |
257 | blobs[path] = (live_entry.sha, cleanup_mode(live_entry.mode)) |
258 | if want_unversioned: |
259 | for e in target.extras(): |
260 | |
261 | === modified file 'breezy/git/workingtree.py' |
262 | --- breezy/git/workingtree.py 2019-06-02 21:32:43 +0000 |
263 | +++ breezy/git/workingtree.py 2019-06-03 23:46:01 +0000 |
264 | @@ -414,7 +414,7 @@ |
265 | # expand any symlinks in the directory part, while leaving the |
266 | # filename alone |
267 | # only expanding if symlinks are supported avoids windows path bugs |
268 | - if osutils.has_symlinks(): |
269 | + if self.supports_symlinks(): |
270 | file_list = list(map(osutils.normalizepath, file_list)) |
271 | |
272 | conflicts_related = set() |
273 | @@ -742,8 +742,7 @@ |
274 | |
275 | def is_executable(self, path): |
276 | with self.lock_read(): |
277 | - if getattr(self, "_supports_executable", |
278 | - osutils.supports_executable)(): |
279 | + if self._supports_executable(): |
280 | mode = self._lstat(path).st_mode |
281 | else: |
282 | (index, subpath) = self._lookup_index(path.encode('utf-8')) |
283 | @@ -754,10 +753,8 @@ |
284 | return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode) |
285 | |
286 | def _is_executable_from_path_and_stat(self, path, stat_result): |
287 | - if getattr(self, "_supports_executable", |
288 | - osutils.supports_executable)(): |
289 | - return self._is_executable_from_path_and_stat_from_stat( |
290 | - path, stat_result) |
291 | + if self._supports_executable(): |
292 | + return self._is_executable_from_path_and_stat_from_stat(path, stat_result) |
293 | else: |
294 | return self._is_executable_from_path_and_stat_from_basis( |
295 | path, stat_result) |
296 | @@ -1165,7 +1162,8 @@ |
297 | self.store, |
298 | None |
299 | if self.branch.head is None |
300 | - else self.store[self.branch.head].tree) |
301 | + else self.store[self.branch.head].tree, |
302 | + honor_filemode=self._supports_executable()) |
303 | |
304 | def reset_state(self, revision_ids=None): |
305 | """Reset the state of the working tree. |
306 | |
307 | === modified file 'breezy/memorytree.py' |
308 | --- breezy/memorytree.py 2019-02-16 21:12:42 +0000 |
309 | +++ breezy/memorytree.py 2019-06-03 23:46:01 +0000 |
310 | @@ -51,6 +51,9 @@ |
311 | self._locks = 0 |
312 | self._lock_mode = None |
313 | |
314 | + def supports_symlinks(self): |
315 | + return True |
316 | + |
317 | def get_config_stack(self): |
318 | return self.branch.get_config_stack() |
319 | |
320 | |
321 | === modified file 'breezy/mutabletree.py' |
322 | --- breezy/mutabletree.py 2018-11-18 00:25:19 +0000 |
323 | +++ breezy/mutabletree.py 2019-06-03 23:46:01 +0000 |
324 | @@ -197,15 +197,35 @@ |
325 | if _from_tree is None: |
326 | _from_tree = self.basis_tree() |
327 | changes = self.iter_changes(_from_tree) |
328 | - try: |
329 | - change = next(changes) |
330 | - # Exclude root (talk about black magic... --vila 20090629) |
331 | - if change[4] == (None, None): |
332 | + if self.supports_symlinks(): |
333 | + # Fast path for has_changes. |
334 | + try: |
335 | change = next(changes) |
336 | + # Exclude root (talk about black magic... --vila 20090629) |
337 | + if change[4] == (None, None): |
338 | + change = next(changes) |
339 | + return True |
340 | + except StopIteration: |
341 | + # No changes |
342 | + return False |
343 | + else: |
344 | + # Slow path for has_changes. |
345 | + # Handle platforms that do not support symlinks in the |
346 | + # conditional below. This is slower than the try/except |
347 | + # approach below that but we don't have a choice as we |
348 | + # need to be sure that all symlinks are removed from the |
349 | + # entire changeset. This is because in platforms that |
350 | + # do not support symlinks, they show up as None in the |
351 | + # working copy as compared to the repository. |
352 | + # Also, exclude root as mention in the above fast path. |
353 | + changes = filter( |
354 | + lambda c: c[6][0] != 'symlink' and c[4] != (None, None), |
355 | + changes) |
356 | + try: |
357 | + next(iter(changes)) |
358 | + except StopIteration: |
359 | + return False |
360 | return True |
361 | - except StopIteration: |
362 | - # No changes |
363 | - return False |
364 | |
365 | def check_changed_or_out_of_date(self, strict, opt_name, |
366 | more_error, more_warning): |
367 | |
368 | === modified file 'breezy/osutils.py' |
369 | --- breezy/osutils.py 2019-03-06 14:03:19 +0000 |
370 | +++ breezy/osutils.py 2019-06-03 23:46:01 +0000 |
371 | @@ -1662,8 +1662,40 @@ |
372 | _terminal_size = _ioctl_terminal_size |
373 | |
374 | |
375 | -def supports_executable(): |
376 | - return sys.platform != "win32" |
377 | +def supports_executable(path): |
378 | + """Return if filesystem at path supports executable bit. |
379 | + |
380 | + :param path: Path for which to check the file system |
381 | + :return: boolean indicating whether executable bit can be stored/relied upon |
382 | + """ |
383 | + if sys.platform == 'win32': |
384 | + return False |
385 | + try: |
386 | + fs_type = get_fs_type(path) |
387 | + except errors.DependencyNotPresent as e: |
388 | + trace.mutter('Unable to get fs type for %r: %s', path, e) |
389 | + else: |
390 | + if fs_type in ('vfat', 'ntfs'): |
391 | + # filesystems known to not support executable bit |
392 | + return False |
393 | + return True |
394 | + |
395 | + |
396 | +def supports_symlinks(path): |
397 | + """Return if the filesystem at path supports the creation of symbolic links. |
398 | + |
399 | + """ |
400 | + if not has_symlinks(): |
401 | + return False |
402 | + try: |
403 | + fs_type = get_fs_type(path) |
404 | + except errors.DependencyNotPresent as e: |
405 | + trace.mutter('Unable to get fs type for %r: %s', path, e) |
406 | + else: |
407 | + if fs_type in ('vfat', 'ntfs'): |
408 | + # filesystems known to not support symlinks |
409 | + return False |
410 | + return True |
411 | |
412 | |
413 | def supports_posix_readonly(): |
414 | @@ -2602,6 +2634,29 @@ |
415 | return False |
416 | |
417 | |
418 | +def get_fs_type(path): |
419 | + """Return the filesystem type for the partition a path is in. |
420 | + |
421 | + :param path: Path to search filesystem type for |
422 | + :return: A FS type, as string. E.g. "ext2" |
423 | + """ |
424 | + # TODO(jelmer): It would be nice to avoid an extra dependency here, but the only |
425 | + # alternative is reading platform-specific files under /proc :( |
426 | + try: |
427 | + import psutil |
428 | + except ImportError as e: |
429 | + raise errors.DependencyNotPresent('psutil', e) |
430 | + |
431 | + if not PY3 and not isinstance(path, str): |
432 | + path = path.encode(_fs_enc) |
433 | + |
434 | + for part in sorted(psutil.disk_partitions(), key=lambda x: len(x.mountpoint), reverse=True): |
435 | + if is_inside(part.mountpoint, path): |
436 | + return part.fstype |
437 | + # Unable to parse the file? Since otherwise at least the entry for / should match.. |
438 | + return None |
439 | + |
440 | + |
441 | if PY3: |
442 | perf_counter = time.perf_counter |
443 | else: |
444 | |
445 | === modified file 'breezy/tests/per_tree/test_symlinks.py' |
446 | --- breezy/tests/per_tree/test_symlinks.py 2018-11-11 04:08:32 +0000 |
447 | +++ breezy/tests/per_tree/test_symlinks.py 2019-06-03 23:46:01 +0000 |
448 | @@ -18,8 +18,10 @@ |
449 | |
450 | |
451 | from breezy import ( |
452 | + osutils, |
453 | tests, |
454 | ) |
455 | +from breezy.git.branch import GitBranch |
456 | from breezy.tests import ( |
457 | per_tree, |
458 | ) |
459 | @@ -35,6 +37,13 @@ |
460 | return next(tree.iter_entries_by_dir(specific_files=[path]))[1] |
461 | |
462 | |
463 | +class TestSymlinkSupportFunction(per_tree.TestCaseWithTree): |
464 | + |
465 | + def test_supports_symlinks(self): |
466 | + self.tree = self.make_branch_and_tree('.') |
467 | + self.assertIn(self.tree.supports_symlinks(), [True, False]) |
468 | + |
469 | + |
470 | class TestTreeWithSymlinks(per_tree.TestCaseWithTree): |
471 | |
472 | _test_needs_features = [features.SymlinkFeature] |
473 | @@ -65,3 +74,34 @@ |
474 | entry = get_entry(self.tree, 'symlink') |
475 | self.assertEqual(entry.kind, 'symlink') |
476 | self.assertEqual(None, entry.text_size) |
477 | + |
478 | + |
479 | +class TestTreeWithoutSymlinks(per_tree.TestCaseWithTree): |
480 | + |
481 | + def setUp(self): |
482 | + super(TestTreeWithoutSymlinks, self).setUp() |
483 | + self.branch = self.make_branch('a') |
484 | + mem_tree = self.branch.create_memorytree() |
485 | + with mem_tree.lock_write(): |
486 | + mem_tree._file_transport.symlink('source', 'symlink') |
487 | + mem_tree.add(['', 'symlink']) |
488 | + rev1 = mem_tree.commit('rev1') |
489 | + self.assertPathDoesNotExist('a/symlink') |
490 | + |
491 | + def test_clone_skips_symlinks(self): |
492 | + if isinstance(self.branch, (GitBranch,)): |
493 | + # TODO(jelmer): Fix this test for git repositories |
494 | + raise TestSkipped( |
495 | + 'git trees do not honor osutils.supports_symlinks yet') |
496 | + self.overrideAttr(osutils, 'supports_symlinks', lambda p: False) |
497 | + # This should not attempt to create any symlinks |
498 | + result_dir = self.branch.controldir.sprout('b') |
499 | + result_tree = result_dir.open_workingtree() |
500 | + self.assertFalse(result_tree.supports_symlinks()) |
501 | + self.assertPathDoesNotExist('b/symlink') |
502 | + basis_tree = self.branch.basis_tree() |
503 | + self.assertTrue(basis_tree.has_filename('symlink')) |
504 | + with result_tree.lock_read(): |
505 | + self.assertEqual( |
506 | + [('symlink', 'symlink')], |
507 | + [c[1] for c in result_tree.iter_changes(basis_tree)]) |
508 | |
509 | === modified file 'breezy/tests/test__dirstate_helpers.py' |
510 | --- breezy/tests/test__dirstate_helpers.py 2018-11-11 04:08:32 +0000 |
511 | +++ breezy/tests/test__dirstate_helpers.py 2019-06-03 23:46:01 +0000 |
512 | @@ -1200,11 +1200,13 @@ |
513 | state, entry = self.get_state_with_a() |
514 | self.build_tree(['a']) |
515 | |
516 | - # Make sure we are using the win32 implementation of _is_executable |
517 | - state._is_executable = state._is_executable_win32 |
518 | + # Make sure we are using the version of _is_executable that doesn't |
519 | + # check the filesystem mode. |
520 | + state._use_filesystem_for_exec = False |
521 | |
522 | # The file on disk is not executable, but we are marking it as though |
523 | - # it is. With _is_executable_win32 we ignore what is on disk. |
524 | + # it is. With _use_filesystem_for_exec disabled we ignore what is on |
525 | + # disk. |
526 | entry[1][0] = (b'f', b'', 0, True, dirstate.DirState.NULLSTAT) |
527 | |
528 | stat_value = os.lstat('a') |
529 | |
530 | === modified file 'breezy/tests/test_commit.py' |
531 | --- breezy/tests/test_commit.py 2018-11-29 23:42:41 +0000 |
532 | +++ breezy/tests/test_commit.py 2019-06-03 23:46:01 +0000 |
533 | @@ -16,12 +16,14 @@ |
534 | |
535 | |
536 | import os |
537 | +from io import BytesIO |
538 | |
539 | import breezy |
540 | from .. import ( |
541 | config, |
542 | controldir, |
543 | errors, |
544 | + trace, |
545 | ) |
546 | from ..branch import Branch |
547 | from ..bzr.bzrdir import BzrDirMetaFormat1 |
548 | @@ -676,6 +678,35 @@ |
549 | finally: |
550 | basis.unlock() |
551 | |
552 | + def test_unsupported_symlink_commit(self): |
553 | + self.requireFeature(SymlinkFeature) |
554 | + tree = self.make_branch_and_tree('.') |
555 | + self.build_tree(['hello']) |
556 | + tree.add('hello') |
557 | + tree.commit('added hello', rev_id=b'hello_id') |
558 | + os.symlink('hello', 'foo') |
559 | + tree.add('foo') |
560 | + tree.commit('added foo', rev_id=b'foo_id') |
561 | + log = BytesIO() |
562 | + trace.push_log_file(log) |
563 | + os_symlink = getattr(os, 'symlink', None) |
564 | + os.symlink = None |
565 | + try: |
566 | + # At this point as bzr thinks symlinks are not supported |
567 | + # we should get a warning about symlink foo and bzr should |
568 | + # not think its removed. |
569 | + os.unlink('foo') |
570 | + self.build_tree(['world']) |
571 | + tree.add('world') |
572 | + tree.commit('added world', rev_id=b'world_id') |
573 | + finally: |
574 | + if os_symlink: |
575 | + os.symlink = os_symlink |
576 | + self.assertContainsRe( |
577 | + log.getvalue(), |
578 | + b'Ignoring "foo" as symlinks are not ' |
579 | + b'supported on this filesystem\\.') |
580 | + |
581 | def test_commit_kind_changes(self): |
582 | self.requireFeature(SymlinkFeature) |
583 | tree = self.make_branch_and_tree('.') |
584 | |
585 | === modified file 'breezy/tests/test_dirstate.py' |
586 | --- breezy/tests/test_dirstate.py 2018-11-11 04:08:32 +0000 |
587 | +++ breezy/tests/test_dirstate.py 2019-06-03 23:46:01 +0000 |
588 | @@ -1826,9 +1826,11 @@ |
589 | class InstrumentedDirState(dirstate.DirState): |
590 | """An DirState with instrumented sha1 functionality.""" |
591 | |
592 | - def __init__(self, path, sha1_provider, worth_saving_limit=0): |
593 | - super(InstrumentedDirState, self).__init__(path, sha1_provider, |
594 | - worth_saving_limit=worth_saving_limit) |
595 | + def __init__(self, path, sha1_provider, worth_saving_limit=0, |
596 | + use_filesystem_for_exec=True): |
597 | + super(InstrumentedDirState, self).__init__( |
598 | + path, sha1_provider, worth_saving_limit=worth_saving_limit, |
599 | + use_filesystem_for_exec=use_filesystem_for_exec) |
600 | self._time_offset = 0 |
601 | self._log = [] |
602 | # member is dynamically set in DirState.__init__ to turn on trace |
603 | |
604 | === modified file 'breezy/tests/test_errors.py' |
605 | --- breezy/tests/test_errors.py 2018-11-11 04:08:32 +0000 |
606 | +++ breezy/tests/test_errors.py 2019-06-03 23:46:01 +0000 |
607 | @@ -388,20 +388,6 @@ |
608 | "you wish to keep, and delete it when you are done.", |
609 | str(err)) |
610 | |
611 | - def test_unable_create_symlink(self): |
612 | - err = errors.UnableCreateSymlink() |
613 | - self.assertEqual( |
614 | - "Unable to create symlink on this platform", |
615 | - str(err)) |
616 | - err = errors.UnableCreateSymlink(path=u'foo') |
617 | - self.assertEqual( |
618 | - "Unable to create symlink 'foo' on this platform", |
619 | - str(err)) |
620 | - err = errors.UnableCreateSymlink(path=u'\xb5') |
621 | - self.assertEqual( |
622 | - "Unable to create symlink %s on this platform" % repr(u'\xb5'), |
623 | - str(err)) |
624 | - |
625 | def test_invalid_url_join(self): |
626 | """Test the formatting of InvalidURLJoin.""" |
627 | e = urlutils.InvalidURLJoin('Reason', 'base path', ('args',)) |
628 | |
629 | === modified file 'breezy/tests/test_osutils.py' |
630 | --- breezy/tests/test_osutils.py 2018-11-17 18:49:41 +0000 |
631 | +++ breezy/tests/test_osutils.py 2019-06-03 23:46:01 +0000 |
632 | @@ -62,6 +62,8 @@ |
633 | |
634 | term_ios_feature = features.ModuleAvailableFeature('termios') |
635 | |
636 | +psutil_feature = features.ModuleAvailableFeature('psutil') |
637 | + |
638 | |
639 | def _already_unicode(s): |
640 | return s |
641 | @@ -2340,3 +2342,22 @@ |
642 | import pywintypes |
643 | self.assertTrue(osutils.is_environment_error( |
644 | pywintypes.error(errno.EINVAL, "Invalid parameter", "Caller"))) |
645 | + |
646 | + |
647 | +class SupportsExecutableTests(tests.TestCaseInTempDir): |
648 | + |
649 | + def test_returns_bool(self): |
650 | + self.assertIsInstance(osutils.supports_executable(self.test_dir), bool) |
651 | + |
652 | + |
653 | +class SupportsSymlinksTests(tests.TestCaseInTempDir): |
654 | + |
655 | + def test_returns_bool(self): |
656 | + self.assertIsInstance(osutils.supports_symlinks(self.test_dir), bool) |
657 | + |
658 | + |
659 | +class GetFsTypeTests(tests.TestCaseInTempDir): |
660 | + |
661 | + def test_returns_string(self): |
662 | + self.requireFeature(psutil_feature) |
663 | + self.assertIsInstance(osutils.get_fs_type(self.test_dir), str) |
664 | |
665 | === modified file 'breezy/tests/test_transform.py' |
666 | --- breezy/tests/test_transform.py 2019-02-04 19:39:30 +0000 |
667 | +++ breezy/tests/test_transform.py 2019-06-03 23:46:01 +0000 |
668 | @@ -16,6 +16,7 @@ |
669 | |
670 | import codecs |
671 | import errno |
672 | +from io import BytesIO, StringIO |
673 | import os |
674 | import sys |
675 | import time |
676 | @@ -806,22 +807,18 @@ |
677 | u'\N{Euro Sign}wizard2', |
678 | u'b\N{Euro Sign}hind_curtain') |
679 | |
680 | - def test_unable_create_symlink(self): |
681 | + def test_unsupported_symlink_no_conflict(self): |
682 | def tt_helper(): |
683 | wt = self.make_branch_and_tree('.') |
684 | - tt = TreeTransform(wt) # TreeTransform obtains write lock |
685 | - try: |
686 | - tt.new_symlink('foo', tt.root, 'bar') |
687 | - tt.apply() |
688 | - finally: |
689 | - wt.unlock() |
690 | + tt = TreeTransform(wt) |
691 | + self.addCleanup(tt.finalize) |
692 | + tt.new_symlink('foo', tt.root, 'bar') |
693 | + result = tt.find_conflicts() |
694 | + self.assertEqual([], result) |
695 | os_symlink = getattr(os, 'symlink', None) |
696 | os.symlink = None |
697 | try: |
698 | - err = self.assertRaises(errors.UnableCreateSymlink, tt_helper) |
699 | - self.assertEqual( |
700 | - "Unable to create symlink 'foo' on this platform", |
701 | - str(err)) |
702 | + tt_helper() |
703 | finally: |
704 | if os_symlink: |
705 | os.symlink = os_symlink |
706 | @@ -1598,6 +1595,24 @@ |
707 | self.addCleanup(wt.unlock) |
708 | self.assertEqual(wt.kind("foo"), "symlink") |
709 | |
710 | + def test_file_to_symlink_unsupported(self): |
711 | + wt = self.make_branch_and_tree('.') |
712 | + self.build_tree(['foo']) |
713 | + wt.add(['foo']) |
714 | + wt.commit("one") |
715 | + self.overrideAttr(osutils, 'supports_symlinks', lambda p: False) |
716 | + tt = TreeTransform(wt) |
717 | + self.addCleanup(tt.finalize) |
718 | + foo_trans_id = tt.trans_id_tree_path("foo") |
719 | + tt.delete_contents(foo_trans_id) |
720 | + log = BytesIO() |
721 | + trace.push_log_file(log) |
722 | + tt.create_symlink("bar", foo_trans_id) |
723 | + tt.apply() |
724 | + self.assertContainsRe( |
725 | + log.getvalue(), |
726 | + b'Unable to create symlink "foo" on this filesystem') |
727 | + |
728 | def test_dir_to_file(self): |
729 | wt = self.make_branch_and_tree('.') |
730 | self.build_tree(['foo/', 'foo/bar']) |
731 | @@ -2809,6 +2824,35 @@ |
732 | # 3 lines of diff administrivia |
733 | self.assertEqual(lines[4], b"+content B") |
734 | |
735 | + def test_unsupported_symlink_diff(self): |
736 | + self.requireFeature(SymlinkFeature) |
737 | + tree = self.make_branch_and_tree('.') |
738 | + self.build_tree_contents([('a', 'content 1')]) |
739 | + tree.set_root_id(b'TREE_ROOT') |
740 | + tree.add('a', b'a-id') |
741 | + os.symlink('a', 'foo') |
742 | + tree.add('foo', b'foo-id') |
743 | + tree.commit('rev1', rev_id=b'rev1') |
744 | + revision_tree = tree.branch.repository.revision_tree(b'rev1') |
745 | + preview = TransformPreview(revision_tree) |
746 | + self.addCleanup(preview.finalize) |
747 | + preview.delete_versioned(preview.trans_id_tree_path('foo')) |
748 | + preview_tree = preview.get_preview_tree() |
749 | + out = StringIO() |
750 | + log = BytesIO() |
751 | + trace.push_log_file(log) |
752 | + os_symlink = getattr(os, 'symlink', None) |
753 | + os.symlink = None |
754 | + try: |
755 | + show_diff_trees(revision_tree, preview_tree, out) |
756 | + lines = out.getvalue().splitlines() |
757 | + finally: |
758 | + if os_symlink: |
759 | + os.symlink = os_symlink |
760 | + self.assertContainsRe( |
761 | + log.getvalue(), |
762 | + b'Ignoring "foo" as symlinks are not supported on this filesystem') |
763 | + |
764 | def test_transform_conflicts(self): |
765 | revision_tree = self.create_tree() |
766 | preview = TransformPreview(revision_tree) |
767 | |
768 | === modified file 'breezy/tests/test_workingtree.py' |
769 | --- breezy/tests/test_workingtree.py 2019-06-02 21:21:39 +0000 |
770 | +++ breezy/tests/test_workingtree.py 2019-06-03 23:46:01 +0000 |
771 | @@ -15,9 +15,13 @@ |
772 | # along with this program; if not, write to the Free Software |
773 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
774 | |
775 | +from io import BytesIO |
776 | +import os |
777 | + |
778 | from .. import ( |
779 | conflicts, |
780 | errors, |
781 | + trace, |
782 | transport, |
783 | workingtree, |
784 | ) |
785 | @@ -37,6 +41,7 @@ |
786 | TreeLink, |
787 | ) |
788 | |
789 | +from .features import SymlinkFeature |
790 | |
791 | class TestTreeDirectory(TestCaseWithTransport): |
792 | |
793 | @@ -443,6 +448,34 @@ |
794 | resolved) |
795 | self.assertPathDoesNotExist('this/hello.BASE') |
796 | |
797 | + def test_unsupported_symlink_auto_resolve(self): |
798 | + self.requireFeature(SymlinkFeature) |
799 | + base = self.make_branch_and_tree('base') |
800 | + self.build_tree_contents([('base/hello', 'Hello')]) |
801 | + base.add('hello', b'hello_id') |
802 | + base.commit('commit 0') |
803 | + other = base.controldir.sprout('other').open_workingtree() |
804 | + self.build_tree_contents([('other/hello', 'Hello')]) |
805 | + os.symlink('other/hello', 'other/foo') |
806 | + other.add('foo', b'foo_id') |
807 | + other.commit('commit symlink') |
808 | + this = base.controldir.sprout('this').open_workingtree() |
809 | + self.assertPathExists('this/hello') |
810 | + self.build_tree_contents([('this/hello', 'Hello')]) |
811 | + this.commit('commit 2') |
812 | + log = BytesIO() |
813 | + trace.push_log_file(log) |
814 | + os_symlink = getattr(os, 'symlink', None) |
815 | + os.symlink = None |
816 | + try: |
817 | + this.merge_from_branch(other.branch) |
818 | + finally: |
819 | + if os_symlink: |
820 | + os.symlink = os_symlink |
821 | + self.assertContainsRe( |
822 | + log.getvalue(), |
823 | + b'Unable to create symlink "foo" on this filesystem') |
824 | + |
825 | def test_auto_resolve_dir(self): |
826 | tree = self.make_branch_and_tree('tree') |
827 | self.build_tree(['tree/hello/']) |
828 | |
829 | === modified file 'breezy/transform.py' |
830 | --- breezy/transform.py 2019-02-02 15:13:30 +0000 |
831 | +++ breezy/transform.py 2019-06-03 23:46:01 +0000 |
832 | @@ -52,17 +52,16 @@ |
833 | """) |
834 | from .errors import (DuplicateKey, MalformedTransform, |
835 | ReusingTransform, CantMoveRoot, |
836 | - ImmortalLimbo, NoFinalPath, |
837 | - UnableCreateSymlink) |
838 | + ImmortalLimbo, NoFinalPath) |
839 | from .filters import filtered_output_bytes, ContentFilterContext |
840 | from .mutabletree import MutableTree |
841 | from .osutils import ( |
842 | delete_any, |
843 | file_kind, |
844 | - has_symlinks, |
845 | pathjoin, |
846 | sha_file, |
847 | splitpath, |
848 | + supports_symlinks, |
849 | ) |
850 | from .progress import ProgressPhase |
851 | from .sixish import ( |
852 | @@ -653,6 +652,9 @@ |
853 | conflicts = [] |
854 | for trans_id in self._new_id: |
855 | kind = self.final_kind(trans_id) |
856 | + if kind == 'symlink' and not self._tree.supports_symlinks(): |
857 | + # Ignore symlinks as they are not supported on this platform |
858 | + continue |
859 | if kind is None: |
860 | conflicts.append(('versioning no contents', trans_id)) |
861 | continue |
862 | @@ -1201,8 +1203,7 @@ |
863 | class DiskTreeTransform(TreeTransformBase): |
864 | """Tree transform storing its contents on disk.""" |
865 | |
866 | - def __init__(self, tree, limbodir, pb=None, |
867 | - case_sensitive=True): |
868 | + def __init__(self, tree, limbodir, pb=None, case_sensitive=True): |
869 | """Constructor. |
870 | :param tree: The tree that will be transformed, but not necessarily |
871 | the output tree. |
872 | @@ -1226,6 +1227,7 @@ |
873 | # List of transform ids that need to be renamed from limbo into place |
874 | self._needs_rename = set() |
875 | self._creation_mtime = None |
876 | + self._create_symlinks = osutils.supports_symlinks(self._limbodir) |
877 | |
878 | def finalize(self): |
879 | """Release the working tree lock, if held, clean up limbo dir. |
880 | @@ -1263,8 +1265,7 @@ |
881 | |
882 | def _limbo_supports_executable(self): |
883 | """Check if the limbo path supports the executable bit.""" |
884 | - # FIXME: Check actual file system capabilities of limbodir |
885 | - return osutils.supports_executable() |
886 | + return osutils.supports_executable(self._limbodir) |
887 | |
888 | def _limbo_name(self, trans_id): |
889 | """Generate the limbo name of a file""" |
890 | @@ -1396,15 +1397,19 @@ |
891 | target is a bytestring. |
892 | See also new_symlink. |
893 | """ |
894 | - if has_symlinks(): |
895 | + if self._create_symlinks: |
896 | os.symlink(target, self._limbo_name(trans_id)) |
897 | - unique_add(self._new_contents, trans_id, 'symlink') |
898 | else: |
899 | try: |
900 | path = FinalPaths(self).get_path(trans_id) |
901 | except KeyError: |
902 | path = None |
903 | - raise UnableCreateSymlink(path=path) |
904 | + trace.warning( |
905 | + 'Unable to create symlink "%s" on this filesystem.' % (path,)) |
906 | + # We add symlink to _new_contents even if they are unsupported |
907 | + # and not created. These entries are subsequently used to avoid |
908 | + # conflicts on platforms that don't support symlink |
909 | + unique_add(self._new_contents, trans_id, 'symlink') |
910 | |
911 | def cancel_creation(self, trans_id): |
912 | """Cancel the creation of new file contents.""" |
913 | |
914 | === modified file 'breezy/transport/local.py' |
915 | --- breezy/transport/local.py 2018-11-11 04:08:32 +0000 |
916 | +++ breezy/transport/local.py 2019-06-03 23:46:01 +0000 |
917 | @@ -532,7 +532,7 @@ |
918 | except (IOError, OSError) as e: |
919 | self._translate_error(e, source) |
920 | |
921 | - if osutils.has_symlinks(): |
922 | + if getattr(os, 'symlink', None) is not None: |
923 | def symlink(self, source, link_name): |
924 | """See Transport.symlink.""" |
925 | abs_link_dirpath = urlutils.dirname(self.abspath(link_name)) |
926 | |
927 | === modified file 'breezy/tree.py' |
928 | --- breezy/tree.py 2019-05-12 09:46:22 +0000 |
929 | +++ breezy/tree.py 2019-06-03 23:46:01 +0000 |
930 | @@ -141,6 +141,11 @@ |
931 | """ |
932 | return True |
933 | |
934 | + def supports_symlinks(self): |
935 | + """Does this tree support symbolic links? |
936 | + """ |
937 | + return osutils.has_symlinks() |
938 | + |
939 | def changes_from(self, other, want_unchanged=False, specific_files=None, |
940 | extra_trees=None, require_versioned=False, include_root=False, |
941 | want_unversioned=False): |
942 | @@ -181,7 +186,8 @@ |
943 | """See InterTree.iter_changes""" |
944 | intertree = InterTree.get(from_tree, self) |
945 | return intertree.iter_changes(include_unchanged, specific_files, pb, |
946 | - extra_trees, require_versioned, want_unversioned=want_unversioned) |
947 | + extra_trees, require_versioned, |
948 | + want_unversioned=want_unversioned) |
949 | |
950 | def conflicts(self): |
951 | """Get a list of the conflicts in the tree. |
952 | |
953 | === modified file 'breezy/workingtree.py' |
954 | --- breezy/workingtree.py 2019-06-02 21:29:09 +0000 |
955 | +++ breezy/workingtree.py 2019-06-03 23:46:01 +0000 |
956 | @@ -71,9 +71,6 @@ |
957 | from .trace import mutter, note |
958 | |
959 | |
960 | -ERROR_PATH_NOT_FOUND = 3 # WindowsError errno code, equivalent to ENOENT |
961 | - |
962 | - |
963 | class SettingFileIdUnsupported(errors.BzrError): |
964 | |
965 | _fmt = "This format does not support setting file ids." |
966 | @@ -129,6 +126,9 @@ |
967 | def control_transport(self): |
968 | return self._transport |
969 | |
970 | + def supports_symlinks(self): |
971 | + return osutils.supports_symlinks(self.basedir) |
972 | + |
973 | def is_control_filename(self, filename): |
974 | """True if filename is the name of a control file in this tree. |
975 | |
976 | @@ -159,10 +159,7 @@ |
977 | return self._format.supports_merge_modified |
978 | |
979 | def _supports_executable(self): |
980 | - if sys.platform == 'win32': |
981 | - return False |
982 | - # FIXME: Ideally this should check the file system |
983 | - return True |
984 | + return osutils.supports_executable(self.basedir) |
985 | |
986 | def break_lock(self): |
987 | """Break a lock if one is present from another instance. |
988 | |
989 | === modified file 'doc/en/release-notes/brz-3.0.txt' |
990 | --- doc/en/release-notes/brz-3.0.txt 2019-06-02 05:13:10 +0000 |
991 | +++ doc/en/release-notes/brz-3.0.txt 2019-06-03 23:46:01 +0000 |
992 | @@ -223,6 +223,11 @@ |
993 | ``RevisionTree.annotate_iter`` have been added. (Jelmer Vernooij, |
994 | #897781) |
995 | |
996 | + * Branches with symlinks are now supported on Windows. Symlinks are |
997 | + ignored by operations like branch, diff etc. with a warning as Symlinks |
998 | + are not created on Windows. |
999 | + (Parth Malwankar, #81689) |
1000 | + |
1001 | * New ``lp+bzr://`` URL scheme for Bazaar-only branches on Launchpad. |
1002 | (Jelmer Vernooij) |
1003 |
Looks reasonable, see some inline comments.