Merge lp:~jelmer/brz/win-symlink-warning into lp:brz

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: 7134
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
Reviewer Review Type Date Requested Status
Martin Packman Approve
Review via email: mp+363900@code.launchpad.net

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.

To post a comment you must log in.
Martin Packman (gz) wrote :

Looks reasonable, see some inline comments.

review: Approve
lp:~jelmer/brz/win-symlink-warning updated
7134. By Jelmer Vernooij on 2019-06-03

Fix more python3 tests.

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-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 Vernooń≥,
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 Vernooń≥)
1003

Subscribers

People subscribed via source and target branches