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

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
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.

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 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-03-04 01:57:09 +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 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 # 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-03-04 01:57:09 +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-03-04 01:57:09 +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-03-04 01:57:09 +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,11 @@
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('Ignoring "%s" as symlinks '
163+ 'are not supported on this filesystem.' % (path[0],))
164+ else:
165+ delta.removed.append((path[0], file_id, kind[0]))
166 elif fully_present[0] is False:
167 delta.missing.append((path[1], file_id, kind[1]))
168 elif name[0] != name[1] or parent_id[0] != parent_id[1]:
169@@ -253,7 +257,7 @@
170 :param kind: A pair of file kinds, as generated by Tree.iter_changes.
171 None indicates no file present.
172 """
173- if is_quiet():
174+ if trace.is_quiet():
175 return
176 if paths[1] == '' and versioned == 'added' and self.suppress_root_add:
177 return
178
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 # is, missing) in both trees are skipped as well.
184 if parent == (None, None) or kind == (None, None):
185 continue
186+ if kind[0] == 'symlink' and not self.new_tree.supports_symlinks():
187+ warning(
188+ 'Ignoring "%s" as symlinks are not '
189+ 'supported on this filesystem.' % (paths[0],))
190+ continue
191 oldpath, newpath = paths
192 oldpath_encoded = get_encoded_path(paths[0])
193 newpath_encoded = get_encoded_path(paths[1])
194
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 ' (See brz shelve --list).%(more)s')
200
201
202-class UnableCreateSymlink(BzrError):
203-
204- _fmt = 'Unable to create symlink %(path_str)son this platform'
205-
206- def __init__(self, path=None):
207- path_str = ''
208- if path:
209- try:
210- path_str = repr(str(path))
211- except UnicodeEncodeError:
212- path_str = repr(path)
213- path_str += ' '
214- self.path_str = path_str
215-
216-
217 class UnableEncodePath(BzrError):
218
219 _fmt = ('Unable to encode %(kind)s path %(path)r in '
220
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 self._lock_mode = None
226 self._populate_from_branch()
227
228+ def _supports_executable(self):
229+ return True
230+
231 @property
232 def controldir(self):
233 return self.branch.controldir
234@@ -258,3 +261,7 @@
235 def kind(self, p):
236 stat_value = self._file_transport.stat(p)
237 return osutils.file_kind_from_stat_mode(stat_value.st_mode)
238+
239+ def get_symlink_target(self, path):
240+ with self.lock_read():
241+ return self._file_transport.readlink(path)
242
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 # Report dirified directories to commit_tree first, so that they can be
248 # replaced with non-empty directories if they have contents.
249 dirified = []
250+ trust_executable = target._supports_executable()
251 for path, index_entry in target._recurse_index_entries():
252 try:
253 live_entry = target._live_entry(path)
254@@ -1465,7 +1466,13 @@
255 else:
256 raise
257 else:
258- blobs[path] = (live_entry.sha, cleanup_mode(live_entry.mode))
259+ mode = live_entry.mode
260+ if not trust_executable:
261+ if mode_is_executable(index_entry.mode):
262+ mode |= 0o111
263+ else:
264+ mode &= ~0o111
265+ blobs[path] = (live_entry.sha, cleanup_mode(mode))
266 if want_unversioned:
267 for e in target.extras():
268 st = target._lstat(e)
269
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 # expand any symlinks in the directory part, while leaving the
275 # filename alone
276 # only expanding if symlinks are supported avoids windows path bugs
277- if osutils.has_symlinks():
278+ if self.supports_symlinks():
279 file_list = list(map(osutils.normalizepath, file_list))
280
281 conflicts_related = set()
282@@ -743,8 +743,7 @@
283
284 def is_executable(self, path):
285 with self.lock_read():
286- if getattr(self, "_supports_executable",
287- osutils.supports_executable)():
288+ if self._supports_executable():
289 mode = self._lstat(path).st_mode
290 else:
291 (index, subpath) = self._lookup_index(path.encode('utf-8'))
292@@ -755,10 +754,8 @@
293 return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
294
295 def _is_executable_from_path_and_stat(self, path, stat_result):
296- if getattr(self, "_supports_executable",
297- osutils.supports_executable)():
298- return self._is_executable_from_path_and_stat_from_stat(
299- path, stat_result)
300+ if self._supports_executable():
301+ return self._is_executable_from_path_and_stat_from_stat(path, stat_result)
302 else:
303 return self._is_executable_from_path_and_stat_from_basis(
304 path, stat_result)
305@@ -1166,7 +1163,8 @@
306 self.store,
307 None
308 if self.branch.head is None
309- else self.store[self.branch.head].tree)
310+ else self.store[self.branch.head].tree,
311+ honor_filemode=self._supports_executable())
312
313 def reset_state(self, revision_ids=None):
314 """Reset the state of the working tree.
315
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 from __future__ import absolute_import
321
322 import os
323+import stat
324
325 from . import (
326 errors,
327@@ -50,6 +51,9 @@
328 self._locks = 0
329 self._lock_mode = None
330
331+ def supports_symlinks(self):
332+ return True
333+
334 def get_config_stack(self):
335 return self.branch.get_config_stack()
336
337@@ -62,7 +66,15 @@
338 with self.lock_tree_write():
339 for f, file_id, kind in zip(files, ids, kinds):
340 if kind is None:
341- kind = 'file'
342+ st_mode = self._file_transport.stat(f).st_mode
343+ if stat.S_ISREG(st_mode):
344+ kind = 'file'
345+ elif stat.S_ISLNK(st_mode):
346+ kind = 'symlink'
347+ elif stat.S_ISDIR(st_mode):
348+ kind = 'directory'
349+ else:
350+ raise AssertionError('Unknown file kind')
351 if file_id is None:
352 self._inventory.add_path(f, kind=kind)
353 else:
354@@ -127,7 +139,7 @@
355 # memory tree does not support nested trees yet.
356 return kind, None, None, None
357 elif kind == 'symlink':
358- raise NotImplementedError('symlink support')
359+ return kind, None, None, self._inventory[id].symlink_target
360 else:
361 raise NotImplementedError('unknown kind')
362
363@@ -148,8 +160,7 @@
364 return self._inventory.get_entry_by_path(path).executable
365
366 def kind(self, path):
367- file_id = self.path2id(path)
368- return self._inventory[file_id].kind
369+ return self._inventory.get_entry_by_path(path).kind
370
371 def mkdir(self, path, file_id=None):
372 """See MutableTree.mkdir()."""
373@@ -227,6 +238,8 @@
374 continue
375 if entry.kind == 'directory':
376 self._file_transport.mkdir(path)
377+ elif entry.kind == 'symlink':
378+ self._file_transport.symlink(entry.symlink_target, path)
379 elif entry.kind == 'file':
380 self._file_transport.put_file(
381 path, self._basis_tree.get_file(path))
382@@ -302,6 +315,10 @@
383 else:
384 raise
385
386+ def get_symlink_target(self, path):
387+ with self.lock_read():
388+ return self._file_transport.readlink(path)
389+
390 def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False):
391 """See MutableTree.set_parent_trees()."""
392 if len(parents_list) == 0:
393
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 if _from_tree is None:
399 _from_tree = self.basis_tree()
400 changes = self.iter_changes(_from_tree)
401- try:
402- change = next(changes)
403- # Exclude root (talk about black magic... --vila 20090629)
404- if change[4] == (None, None):
405+ if self.supports_symlinks():
406+ # Fast path for has_changes.
407+ try:
408 change = next(changes)
409- return True
410- except StopIteration:
411- # No changes
412+ # Exclude root (talk about black magic... --vila 20090629)
413+ if change[4] == (None, None):
414+ change = next(changes)
415+ return True
416+ except StopIteration:
417+ # No changes
418+ return False
419+ else:
420+ # Slow path for has_changes.
421+ # Handle platforms that do not support symlinks in the
422+ # conditional below. This is slower than the try/except
423+ # approach below that but we don't have a choice as we
424+ # need to be sure that all symlinks are removed from the
425+ # entire changeset. This is because in plantforms that
426+ # do not support symlinks, they show up as None in the
427+ # working copy as compared to the repository.
428+ # Also, exclude root as mention in the above fast path.
429+ changes = filter(
430+ lambda c: c[6][0] != 'symlink' and c[4] != (None, None),
431+ changes)
432+ if len(changes) > 0:
433+ return True
434 return False
435
436 def check_changed_or_out_of_date(self, strict, opt_name,
437
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 _terminal_size = _ioctl_terminal_size
443
444
445-def supports_executable():
446- return sys.platform != "win32"
447+def supports_executable(path):
448+ """Return if filesystem at path supports executable bit.
449+
450+ :param path: Path for which to check the file system
451+ :return: boolean indicating whether executable bit can be stored/relied upon
452+ """
453+ if sys.platform == 'win32':
454+ return False
455+ try:
456+ fs_type = get_fs_type(path)
457+ except errors.DependencyNotPresent:
458+ # TODO(jelmer): Warn here?
459+ pass
460+ else:
461+ if fs_type in ('vfat', 'ntfs'):
462+ # filesystems known to not support executable bit
463+ return False
464+ return True
465+
466+
467+def supports_symlinks(path):
468+ """Return if the filesystem at path supports the creation of symbolic links.
469+
470+ """
471+ if not has_symlinks():
472+ return False
473+ try:
474+ fs_type = get_fs_type(path)
475+ except errors.DependencyNotPresent:
476+ # TODO(jelmer): Warn here?
477+ pass
478+ else:
479+ if fs_type in ('vfat', 'ntfs'):
480+ # filesystems known to not support executable bit
481+ return False
482+ return True
483
484
485 def supports_posix_readonly():
486@@ -2602,6 +2636,25 @@
487 return False
488
489
490+def get_fs_type(path):
491+ """Return the filesystem type for the partition a path is in.
492+
493+ :param path: Path to search filesystem type for
494+ :return: A FS type, as string. E.g. "ext2"
495+ """
496+ # TODO(jelmer): It would be nice to avoid an extra dependency here, but the only
497+ # alternative is reading platform-specific files under /proc :(
498+ try:
499+ import psutil
500+ except ImportError as e:
501+ raise errors.DependencyNotPresent('psutil', e)
502+ for part in sorted(psutil.disk_partitions(), key=lambda x: len(x.mountpoint), reverse=True):
503+ if is_inside(part.mountpoint, path):
504+ return part.fstype
505+ # Unable to parse the file? Since otherwise at least the entry for / should match..
506+ return None
507+
508+
509 if PY3:
510 perf_counter = time.perf_counter
511 else:
512
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
518
519 from breezy import (
520+ osutils,
521 tests,
522 )
523+from breezy.git.branch import GitBranch
524 from breezy.tests import (
525 per_tree,
526 )
527@@ -35,6 +37,13 @@
528 return next(tree.iter_entries_by_dir(specific_files=[path]))[1]
529
530
531+class TestSymlinkSupportFunction(per_tree.TestCaseWithTree):
532+
533+ def test_supports_symlinks(self):
534+ self.tree = self.make_branch_and_tree('.')
535+ self.assertIn(self.tree.supports_symlinks(), [True, False])
536+
537+
538 class TestTreeWithSymlinks(per_tree.TestCaseWithTree):
539
540 _test_needs_features = [features.SymlinkFeature]
541@@ -65,3 +74,34 @@
542 entry = get_entry(self.tree, 'symlink')
543 self.assertEqual(entry.kind, 'symlink')
544 self.assertEqual(None, entry.text_size)
545+
546+
547+class TestTreeWithoutSymlinks(per_tree.TestCaseWithTree):
548+
549+ def setUp(self):
550+ super(TestTreeWithoutSymlinks, self).setUp()
551+ self.branch = self.make_branch('a')
552+ mem_tree = self.branch.create_memorytree()
553+ with mem_tree.lock_write():
554+ mem_tree._file_transport.symlink('source', 'symlink')
555+ mem_tree.add(['', 'symlink'])
556+ rev1 = mem_tree.commit('rev1')
557+ self.assertPathDoesNotExist('a/symlink')
558+
559+ def test_clone_skips_symlinks(self):
560+ if isinstance(self.branch, (GitBranch,)):
561+ # TODO(jelmer): Fix this test for git repositories
562+ raise TestSkipped(
563+ 'git trees do not honor osutils.supports_symlinks yet')
564+ self.overrideAttr(osutils, 'supports_symlinks', lambda p: False)
565+ # This should not attempt to create any symlinks
566+ result_dir = self.branch.controldir.sprout('b')
567+ result_tree = result_dir.open_workingtree()
568+ self.assertFalse(result_tree.supports_symlinks())
569+ self.assertPathDoesNotExist('b/symlink')
570+ basis_tree = self.branch.basis_tree()
571+ self.assertTrue(basis_tree.has_filename('symlink'))
572+ with result_tree.lock_read():
573+ self.assertEqual(
574+ [('symlink', 'symlink')],
575+ [c[1] for c in result_tree.iter_changes(basis_tree)])
576
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
582
583 import os
584+from StringIO import StringIO
585
586 import breezy
587 from .. import (
588 config,
589 controldir,
590 errors,
591+ trace,
592 )
593 from ..branch import Branch
594 from ..bzr.bzrdir import BzrDirMetaFormat1
595@@ -676,6 +678,35 @@
596 finally:
597 basis.unlock()
598
599+ def test_unsupported_symlink_commit(self):
600+ self.requireFeature(SymlinkFeature)
601+ tree = self.make_branch_and_tree('.')
602+ self.build_tree(['hello'])
603+ tree.add('hello')
604+ tree.commit('added hello', rev_id='hello_id')
605+ os.symlink('hello', 'foo')
606+ tree.add('foo')
607+ tree.commit('added foo', rev_id='foo_id')
608+ log = StringIO()
609+ trace.push_log_file(log)
610+ os_symlink = getattr(os, 'symlink', None)
611+ os.symlink = None
612+ try:
613+ # At this point as bzr thinks symlinks are not supported
614+ # we should get a warning about symlink foo and bzr should
615+ # not think its removed.
616+ os.unlink('foo')
617+ self.build_tree(['world'])
618+ tree.add('world')
619+ tree.commit('added world', rev_id='world_id')
620+ finally:
621+ if os_symlink:
622+ os.symlink = os_symlink
623+ self.assertContainsRe(
624+ log.getvalue(),
625+ 'Ignoring "foo" as symlinks are not '
626+ 'supported on this filesystem.')
627+
628 def test_commit_kind_changes(self):
629 self.requireFeature(SymlinkFeature)
630 tree = self.make_branch_and_tree('.')
631
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 "you wish to keep, and delete it when you are done.",
637 str(err))
638
639- def test_unable_create_symlink(self):
640- err = errors.UnableCreateSymlink()
641- self.assertEqual(
642- "Unable to create symlink on this platform",
643- str(err))
644- err = errors.UnableCreateSymlink(path=u'foo')
645- self.assertEqual(
646- "Unable to create symlink 'foo' on this platform",
647- str(err))
648- err = errors.UnableCreateSymlink(path=u'\xb5')
649- self.assertEqual(
650- "Unable to create symlink %s on this platform" % repr(u'\xb5'),
651- str(err))
652-
653 def test_invalid_url_join(self):
654 """Test the formatting of InvalidURLJoin."""
655 e = urlutils.InvalidURLJoin('Reason', 'base path', ('args',))
656
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 rev_id = tree.commit('first post')
662 tree.unlock()
663 tree = MemoryTree.create_on_branch(branch)
664- tree.lock_read()
665- self.assertEqual([rev_id], tree.get_parent_ids())
666- with tree.get_file('foo') as f:
667- self.assertEqual(b'contents of foo\n', f.read())
668- tree.unlock()
669+ with tree.lock_read():
670+ self.assertEqual([rev_id], tree.get_parent_ids())
671+ with tree.get_file('foo') as f:
672+ self.assertEqual(b'contents of foo\n', f.read())
673
674 def test_get_root_id(self):
675 branch = self.make_branch('branch')
676 tree = MemoryTree.create_on_branch(branch)
677- tree.lock_write()
678- try:
679+ with tree.lock_write():
680 tree.add([''])
681 self.assertIsNot(None, tree.get_root_id())
682- finally:
683- tree.unlock()
684
685 def test_lock_tree_write(self):
686 """Check we can lock_tree_write and unlock MemoryTrees."""
687@@ -73,9 +69,8 @@
688 """Check that we error when trying to upgrade a read lock to write."""
689 branch = self.make_branch('branch')
690 tree = MemoryTree.create_on_branch(branch)
691- tree.lock_read()
692- self.assertRaises(errors.ReadOnlyError, tree.lock_tree_write)
693- tree.unlock()
694+ with tree.lock_read():
695+ self.assertRaises(errors.ReadOnlyError, tree.lock_tree_write)
696
697 def test_lock_write(self):
698 """Check we can lock_write and unlock MemoryTrees."""
699@@ -88,58 +83,63 @@
700 """Check that we error when trying to upgrade a read lock to write."""
701 branch = self.make_branch('branch')
702 tree = MemoryTree.create_on_branch(branch)
703- tree.lock_read()
704- self.assertRaises(errors.ReadOnlyError, tree.lock_write)
705- tree.unlock()
706+ with tree.lock_read():
707+ self.assertRaises(errors.ReadOnlyError, tree.lock_write)
708
709 def test_add_with_kind(self):
710 branch = self.make_branch('branch')
711 tree = MemoryTree.create_on_branch(branch)
712- tree.lock_write()
713- tree.add(['', 'afile', 'adir'], None,
714- ['directory', 'file', 'directory'])
715- self.assertEqual('afile', tree.id2path(tree.path2id('afile')))
716- self.assertEqual('adir', tree.id2path(tree.path2id('adir')))
717- self.assertFalse(tree.has_filename('afile'))
718- self.assertFalse(tree.has_filename('adir'))
719- tree.unlock()
720+ with tree.lock_write():
721+ tree.add(['', 'afile', 'adir'], None,
722+ ['directory', 'file', 'directory'])
723+ self.assertEqual('afile', tree.id2path(tree.path2id('afile')))
724+ self.assertEqual('adir', tree.id2path(tree.path2id('adir')))
725+ self.assertFalse(tree.has_filename('afile'))
726+ self.assertFalse(tree.has_filename('adir'))
727
728 def test_put_new_file(self):
729 branch = self.make_branch('branch')
730 tree = MemoryTree.create_on_branch(branch)
731- tree.lock_write()
732- tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'],
733- kinds=['directory', 'file'])
734- tree.put_file_bytes_non_atomic('foo', b'barshoom')
735- self.assertEqual(b'barshoom', tree.get_file('foo').read())
736- tree.unlock()
737+ with tree.lock_write():
738+ tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'],
739+ kinds=['directory', 'file'])
740+ tree.put_file_bytes_non_atomic('foo', b'barshoom')
741+ with tree.get_file('foo') as f:
742+ self.assertEqual(b'barshoom', f.read())
743
744 def test_put_existing_file(self):
745 branch = self.make_branch('branch')
746 tree = MemoryTree.create_on_branch(branch)
747- tree.lock_write()
748- tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'],
749- kinds=['directory', 'file'])
750- tree.put_file_bytes_non_atomic('foo', b'first-content')
751- tree.put_file_bytes_non_atomic('foo', b'barshoom')
752- self.assertEqual(b'barshoom', tree.get_file('foo').read())
753- tree.unlock()
754+ with tree.lock_write():
755+ tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'],
756+ kinds=['directory', 'file'])
757+ tree.put_file_bytes_non_atomic('foo', b'first-content')
758+ tree.put_file_bytes_non_atomic('foo', b'barshoom')
759+ self.assertEqual(b'barshoom', tree.get_file('foo').read())
760
761 def test_add_in_subdir(self):
762 branch = self.make_branch('branch')
763 tree = MemoryTree.create_on_branch(branch)
764- tree.lock_write()
765- self.addCleanup(tree.unlock)
766- tree.add([''], [b'root-id'], ['directory'])
767- # Unfortunately, the only way to 'mkdir' is to call 'tree.mkdir', but
768- # that *always* adds the directory as well. So if you want to create a
769- # file in a subdirectory, you have to split out the 'mkdir()' calls
770- # from the add and put_file_bytes_non_atomic calls. :(
771- tree.mkdir('adir', b'dir-id')
772- tree.add(['adir/afile'], [b'file-id'], ['file'])
773- self.assertEqual('adir/afile', tree.id2path(b'file-id'))
774- self.assertEqual('adir', tree.id2path(b'dir-id'))
775- tree.put_file_bytes_non_atomic('adir/afile', b'barshoom')
776+ with tree.lock_write():
777+ tree.add([''], [b'root-id'], ['directory'])
778+ # Unfortunately, the only way to 'mkdir' is to call 'tree.mkdir', but
779+ # that *always* adds the directory as well. So if you want to create a
780+ # file in a subdirectory, you have to split out the 'mkdir()' calls
781+ # from the add and put_file_bytes_non_atomic calls. :(
782+ tree.mkdir('adir', b'dir-id')
783+ tree.add(['adir/afile'], [b'file-id'], ['file'])
784+ self.assertEqual('adir/afile', tree.id2path(b'file-id'))
785+ self.assertEqual('adir', tree.id2path(b'dir-id'))
786+ tree.put_file_bytes_non_atomic('adir/afile', b'barshoom')
787+
788+ def test_add_symlink(self):
789+ branch = self.make_branch('branch')
790+ tree = MemoryTree.create_on_branch(branch)
791+ with tree.lock_write():
792+ tree._file_transport.symlink('bar', 'foo')
793+ tree.add(['', 'foo'])
794+ self.assertEqual('symlink', tree.kind('foo'))
795+ self.assertEqual('bar', tree.get_symlink_target('foo'))
796
797 def test_commit_trivial(self):
798 """Smoke test for commit on a MemoryTree.
799@@ -149,40 +149,35 @@
800 """
801 branch = self.make_branch('branch')
802 tree = MemoryTree.create_on_branch(branch)
803- tree.lock_write()
804- tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'],
805- kinds=['directory', 'file'])
806- tree.put_file_bytes_non_atomic('foo', b'barshoom')
807- revision_id = tree.commit('message baby')
808- # the parents list for the tree should have changed.
809- self.assertEqual([revision_id], tree.get_parent_ids())
810- tree.unlock()
811+ with tree.lock_write():
812+ tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'],
813+ kinds=['directory', 'file'])
814+ tree.put_file_bytes_non_atomic('foo', b'barshoom')
815+ revision_id = tree.commit('message baby')
816+ # the parents list for the tree should have changed.
817+ self.assertEqual([revision_id], tree.get_parent_ids())
818 # and we should have a revision that is accessible outside the tree lock
819 revtree = tree.branch.repository.revision_tree(revision_id)
820- revtree.lock_read()
821- self.addCleanup(revtree.unlock)
822- with revtree.get_file('foo') as f:
823+ with revtree.lock_read(), revtree.get_file('foo') as f:
824 self.assertEqual(b'barshoom', f.read())
825
826 def test_unversion(self):
827 """Some test for unversion of a memory tree."""
828 branch = self.make_branch('branch')
829 tree = MemoryTree.create_on_branch(branch)
830- tree.lock_write()
831- tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'],
832- kinds=['directory', 'file'])
833- tree.unversion(['foo'])
834- self.assertFalse(tree.is_versioned('foo'))
835- self.assertFalse(tree.has_id(b'foo-id'))
836- tree.unlock()
837+ with tree.lock_write():
838+ tree.add(['', 'foo'], ids=[b'root-id', b'foo-id'],
839+ kinds=['directory', 'file'])
840+ tree.unversion(['foo'])
841+ self.assertFalse(tree.is_versioned('foo'))
842+ self.assertFalse(tree.has_id(b'foo-id'))
843
844 def test_last_revision(self):
845 """There should be a last revision method we can call."""
846 tree = self.make_branch_and_memory_tree('branch')
847- tree.lock_write()
848- tree.add('')
849- rev_id = tree.commit('first post')
850- tree.unlock()
851+ with tree.lock_write():
852+ tree.add('')
853+ rev_id = tree.commit('first post')
854 self.assertEqual(rev_id, tree.last_revision())
855
856 def test_rename_file(self):
857
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
863 term_ios_feature = features.ModuleAvailableFeature('termios')
864
865+psutil_feature = features.ModuleAvailableFeature('psutil')
866+
867
868 def _already_unicode(s):
869 return s
870@@ -2340,3 +2342,22 @@
871 import pywintypes
872 self.assertTrue(osutils.is_environment_error(
873 pywintypes.error(errno.EINVAL, "Invalid parameter", "Caller")))
874+
875+
876+class SupportsExecutableTests(tests.TestCaseInTempDir):
877+
878+ def test_returns_bool(self):
879+ self.assertIsInstance(osutils.supports_executable(self.test_dir), bool)
880+
881+
882+class SupportsSymlinksTests(tests.TestCaseInTempDir):
883+
884+ def test_returns_bool(self):
885+ self.assertIsInstance(osutils.supports_symlinks(self.test_dir), bool)
886+
887+
888+class GetFsTypeTests(tests.TestCaseInTempDir):
889+
890+ def test_returns_string(self):
891+ self.requireFeature(psutil_feature)
892+ self.assertIsInstance(osutils.get_fs_type(self.test_dir), str)
893
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
899 import codecs
900 import errno
901+from io import BytesIO, StringIO
902 import os
903 import sys
904 import time
905@@ -806,22 +807,18 @@
906 u'\N{Euro Sign}wizard2',
907 u'b\N{Euro Sign}hind_curtain')
908
909- def test_unable_create_symlink(self):
910+ def test_unsupported_symlink_no_conflict(self):
911 def tt_helper():
912 wt = self.make_branch_and_tree('.')
913- tt = TreeTransform(wt) # TreeTransform obtains write lock
914- try:
915- tt.new_symlink('foo', tt.root, 'bar')
916- tt.apply()
917- finally:
918- wt.unlock()
919+ tt = TreeTransform(wt)
920+ self.addCleanup(tt.finalize)
921+ tt.new_symlink('foo', tt.root, 'bar')
922+ result = tt.find_conflicts()
923+ self.assertEqual([], result)
924 os_symlink = getattr(os, 'symlink', None)
925 os.symlink = None
926 try:
927- err = self.assertRaises(errors.UnableCreateSymlink, tt_helper)
928- self.assertEqual(
929- "Unable to create symlink 'foo' on this platform",
930- str(err))
931+ tt_helper()
932 finally:
933 if os_symlink:
934 os.symlink = os_symlink
935@@ -1598,6 +1595,24 @@
936 self.addCleanup(wt.unlock)
937 self.assertEqual(wt.kind("foo"), "symlink")
938
939+ def test_file_to_symlink_unsupported(self):
940+ wt = self.make_branch_and_tree('.')
941+ self.build_tree(['foo'])
942+ wt.add(['foo'])
943+ wt.commit("one")
944+ self.overrideAttr(osutils, 'supports_symlinks', lambda p: False)
945+ tt = TreeTransform(wt)
946+ self.addCleanup(tt.finalize)
947+ foo_trans_id = tt.trans_id_tree_path("foo")
948+ tt.delete_contents(foo_trans_id)
949+ log = BytesIO()
950+ trace.push_log_file(log)
951+ tt.create_symlink("bar", foo_trans_id)
952+ tt.apply()
953+ self.assertContainsRe(
954+ log.getvalue(),
955+ 'Unable to create symlink "foo" on this filesystem')
956+
957 def test_dir_to_file(self):
958 wt = self.make_branch_and_tree('.')
959 self.build_tree(['foo/', 'foo/bar'])
960@@ -2809,6 +2824,35 @@
961 # 3 lines of diff administrivia
962 self.assertEqual(lines[4], b"+content B")
963
964+ def test_unsupported_symlink_diff(self):
965+ self.requireFeature(SymlinkFeature)
966+ tree = self.make_branch_and_tree('.')
967+ self.build_tree_contents([('a', 'content 1')])
968+ tree.set_root_id('TREE_ROOT')
969+ tree.add('a', 'a-id')
970+ os.symlink('a', 'foo')
971+ tree.add('foo', 'foo-id')
972+ tree.commit('rev1', rev_id='rev1')
973+ revision_tree = tree.branch.repository.revision_tree('rev1')
974+ preview = TransformPreview(revision_tree)
975+ self.addCleanup(preview.finalize)
976+ preview.delete_versioned(preview.trans_id_tree_path('foo'))
977+ preview_tree = preview.get_preview_tree()
978+ out = StringIO()
979+ log = BytesIO()
980+ trace.push_log_file(log)
981+ os_symlink = getattr(os, 'symlink', None)
982+ os.symlink = None
983+ try:
984+ show_diff_trees(revision_tree, preview_tree, out)
985+ lines = out.getvalue().splitlines()
986+ finally:
987+ if os_symlink:
988+ os.symlink = os_symlink
989+ self.assertContainsRe(
990+ log.getvalue(),
991+ 'Ignoring "foo" as symlinks are not supported on this filesystem')
992+
993 def test_transform_conflicts(self):
994 revision_tree = self.create_tree()
995 preview = TransformPreview(revision_tree)
996
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 # along with this program; if not, write to the Free Software
1002 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
1003
1004+from io import BytesIO
1005+import os
1006+
1007 from .. import (
1008 conflicts,
1009 errors,
1010+ trace,
1011 transport,
1012 workingtree,
1013 )
1014@@ -37,6 +41,7 @@
1015 TreeLink,
1016 )
1017
1018+from .features import SymlinkFeature
1019
1020 class TestTreeDirectory(TestCaseWithTransport):
1021
1022@@ -443,6 +448,34 @@
1023 resolved)
1024 self.assertPathDoesNotExist('this/hello.BASE')
1025
1026+ def test_unsupported_symlink_auto_resolve(self):
1027+ self.requireFeature(SymlinkFeature)
1028+ base = self.make_branch_and_tree('base')
1029+ self.build_tree_contents([('base/hello', 'Hello')])
1030+ base.add('hello', 'hello_id')
1031+ base.commit('commit 0')
1032+ other = base.controldir.sprout('other').open_workingtree()
1033+ self.build_tree_contents([('other/hello', 'Hello')])
1034+ os.symlink('other/hello', 'other/foo')
1035+ other.add('foo', 'foo_id')
1036+ other.commit('commit symlink')
1037+ this = base.controldir.sprout('this').open_workingtree()
1038+ self.assertPathExists('this/hello')
1039+ self.build_tree_contents([('this/hello', 'Hello')])
1040+ this.commit('commit 2')
1041+ log = BytesIO()
1042+ trace.push_log_file(log)
1043+ os_symlink = getattr(os, 'symlink', None)
1044+ os.symlink = None
1045+ try:
1046+ this.merge_from_branch(other.branch)
1047+ finally:
1048+ if os_symlink:
1049+ os.symlink = os_symlink
1050+ self.assertContainsRe(
1051+ log.getvalue(),
1052+ b'Unable to create symlink "foo" on this filesystem')
1053+
1054 def test_auto_resolve_dir(self):
1055 tree = self.make_branch_and_tree('tree')
1056 self.build_tree(['tree/hello/'])
1057
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 """)
1063 from .errors import (DuplicateKey, MalformedTransform,
1064 ReusingTransform, CantMoveRoot,
1065- ImmortalLimbo, NoFinalPath,
1066- UnableCreateSymlink)
1067+ ImmortalLimbo, NoFinalPath)
1068 from .filters import filtered_output_bytes, ContentFilterContext
1069 from .mutabletree import MutableTree
1070 from .osutils import (
1071 delete_any,
1072 file_kind,
1073- has_symlinks,
1074 pathjoin,
1075 sha_file,
1076 splitpath,
1077+ supports_symlinks,
1078 )
1079 from .progress import ProgressPhase
1080 from .sixish import (
1081@@ -653,6 +652,9 @@
1082 conflicts = []
1083 for trans_id in self._new_id:
1084 kind = self.final_kind(trans_id)
1085+ if kind == 'symlink' and not self._tree.supports_symlinks():
1086+ # Ignore symlinks as they are not supported on this platform
1087+ continue
1088 if kind is None:
1089 conflicts.append(('versioning no contents', trans_id))
1090 continue
1091@@ -1201,8 +1203,7 @@
1092 class DiskTreeTransform(TreeTransformBase):
1093 """Tree transform storing its contents on disk."""
1094
1095- def __init__(self, tree, limbodir, pb=None,
1096- case_sensitive=True):
1097+ def __init__(self, tree, limbodir, pb=None, case_sensitive=True):
1098 """Constructor.
1099 :param tree: The tree that will be transformed, but not necessarily
1100 the output tree.
1101@@ -1226,6 +1227,7 @@
1102 # List of transform ids that need to be renamed from limbo into place
1103 self._needs_rename = set()
1104 self._creation_mtime = None
1105+ self._create_symlinks = osutils.supports_symlinks(self._limbodir)
1106
1107 def finalize(self):
1108 """Release the working tree lock, if held, clean up limbo dir.
1109@@ -1263,8 +1265,7 @@
1110
1111 def _limbo_supports_executable(self):
1112 """Check if the limbo path supports the executable bit."""
1113- # FIXME: Check actual file system capabilities of limbodir
1114- return osutils.supports_executable()
1115+ return osutils.supports_executable(self._limbodir)
1116
1117 def _limbo_name(self, trans_id):
1118 """Generate the limbo name of a file"""
1119@@ -1396,15 +1397,19 @@
1120 target is a bytestring.
1121 See also new_symlink.
1122 """
1123- if has_symlinks():
1124+ if self._create_symlinks:
1125 os.symlink(target, self._limbo_name(trans_id))
1126- unique_add(self._new_contents, trans_id, 'symlink')
1127 else:
1128 try:
1129 path = FinalPaths(self).get_path(trans_id)
1130 except KeyError:
1131 path = None
1132- raise UnableCreateSymlink(path=path)
1133+ trace.warning(
1134+ 'Unable to create symlink "%s" on this filesystem.' % (path,))
1135+ # We add symlink to _new_contents even if they are unsupported
1136+ # and not created. These entries are subsequently used to avoid
1137+ # conflicts on platforms that don't support symlink
1138+ unique_add(self._new_contents, trans_id, 'symlink')
1139
1140 def cancel_creation(self, trans_id):
1141 """Cancel the creation of new file contents."""
1142
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 except (IOError, OSError) as e:
1148 self._translate_error(e, source)
1149
1150- if osutils.has_symlinks():
1151+ if getattr(os, 'symlink', None) is not None:
1152 def symlink(self, source, link_name):
1153 """See Transport.symlink."""
1154 abs_link_dirpath = urlutils.dirname(self.abspath(link_name))
1155
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 from io import (
1161 BytesIO,
1162 )
1163+import itertools
1164 import os
1165 import errno
1166-from stat import S_IFREG, S_IFDIR, S_IFLNK
1167+from stat import S_IFREG, S_IFDIR, S_IFLNK, S_ISDIR
1168
1169 from .. import (
1170 transport,
1171@@ -50,20 +51,16 @@
1172
1173 class MemoryStat(object):
1174
1175- def __init__(self, size, kind, perms):
1176+ def __init__(self, size, kind, perms=None):
1177 self.st_size = size
1178- if kind == 'file':
1179+ if not S_ISDIR(kind):
1180 if perms is None:
1181 perms = 0o644
1182- self.st_mode = S_IFREG | perms
1183- elif kind == 'directory':
1184+ self.st_mode = kind | perms
1185+ else:
1186 if perms is None:
1187 perms = 0o755
1188- self.st_mode = S_IFDIR | perms
1189- elif kind == 'symlink':
1190- self.st_mode = S_IFLNK | 0o644
1191- else:
1192- raise AssertionError('unknown kind %r' % kind)
1193+ self.st_mode = kind | perms
1194
1195
1196 class MemoryTransport(transport.Transport):
1197@@ -80,7 +77,7 @@
1198 self._scheme = url[:split]
1199 self._cwd = url[split:]
1200 # dictionaries from absolute path to file mode
1201- self._dirs = {'/': None}
1202+ self._dirs = {'/':None}
1203 self._symlinks = {}
1204 self._files = {}
1205 self._locks = {}
1206@@ -111,7 +108,7 @@
1207
1208 def append_file(self, relpath, f, mode=None):
1209 """See Transport.append_file()."""
1210- _abspath = self._abspath(relpath)
1211+ _abspath = self._resolve_symlinks(relpath)
1212 self._check_parent(_abspath)
1213 orig_content, orig_mode = self._files.get(_abspath, (b"", None))
1214 if mode is None:
1215@@ -128,16 +125,20 @@
1216 def has(self, relpath):
1217 """See Transport.has()."""
1218 _abspath = self._abspath(relpath)
1219- return ((_abspath in self._files)
1220- or (_abspath in self._dirs)
1221- or (_abspath in self._symlinks))
1222+ for container in (self._files, self._dirs, self._symlinks):
1223+ if _abspath in container.keys():
1224+ return True
1225+ return False
1226
1227 def delete(self, relpath):
1228 """See Transport.delete()."""
1229 _abspath = self._abspath(relpath)
1230- if _abspath not in self._files:
1231+ if _abspath in self._files:
1232+ del self._files[_abspath]
1233+ elif _abspath in self._symlinks:
1234+ del self._symlinks[_abspath]
1235+ else:
1236 raise NoSuchFile(relpath)
1237- del self._files[_abspath]
1238
1239 def external_url(self):
1240 """See breezy.transport.Transport.external_url."""
1241@@ -147,8 +148,8 @@
1242
1243 def get(self, relpath):
1244 """See Transport.get()."""
1245- _abspath = self._abspath(relpath)
1246- if _abspath not in self._files:
1247+ _abspath = self._resolve_symlinks(relpath)
1248+ if not _abspath in self._files:
1249 if _abspath in self._dirs:
1250 return LateReadError(relpath)
1251 else:
1252@@ -157,15 +158,20 @@
1253
1254 def put_file(self, relpath, f, mode=None):
1255 """See Transport.put_file()."""
1256- _abspath = self._abspath(relpath)
1257+ _abspath = self._resolve_symlinks(relpath)
1258 self._check_parent(_abspath)
1259 raw_bytes = f.read()
1260 self._files[_abspath] = (raw_bytes, mode)
1261 return len(raw_bytes)
1262
1263+ def symlink(self, source, target):
1264+ _abspath = self._resolve_symlinks(target)
1265+ self._check_parent(_abspath)
1266+ self._symlinks[_abspath] = self._abspath(source)
1267+
1268 def mkdir(self, relpath, mode=None):
1269 """See Transport.mkdir()."""
1270- _abspath = self._abspath(relpath)
1271+ _abspath = self._resolve_symlinks(relpath)
1272 self._check_parent(_abspath)
1273 if _abspath in self._dirs:
1274 raise FileExists(relpath)
1275@@ -183,13 +189,13 @@
1276 return True
1277
1278 def iter_files_recursive(self):
1279- for file in self._files:
1280+ for file in itertools.chain(self._files, self._symlinks):
1281 if file.startswith(self._cwd):
1282 yield urlutils.escape(file[len(self._cwd):])
1283
1284 def list_dir(self, relpath):
1285 """See Transport.list_dir()."""
1286- _abspath = self._abspath(relpath)
1287+ _abspath = self._resolve_symlinks(relpath)
1288 if _abspath != '/' and _abspath not in self._dirs:
1289 raise NoSuchFile(relpath)
1290 result = []
1291@@ -197,7 +203,7 @@
1292 if not _abspath.endswith('/'):
1293 _abspath += '/'
1294
1295- for path_group in self._files, self._dirs:
1296+ for path_group in self._files, self._dirs, self._symlinks:
1297 for path in path_group:
1298 if path.startswith(_abspath):
1299 trailing = path[len(_abspath):]
1300@@ -207,8 +213,8 @@
1301
1302 def rename(self, rel_from, rel_to):
1303 """Rename a file or directory; fail if the destination exists"""
1304- abs_from = self._abspath(rel_from)
1305- abs_to = self._abspath(rel_to)
1306+ abs_from = self._resolve_symlinks(rel_from)
1307+ abs_to = self._resolve_symlinks(rel_to)
1308
1309 def replace(x):
1310 if x == abs_from:
1311@@ -233,21 +239,25 @@
1312 # fail differently depending on dict order. So work on copy, fail on
1313 # error on only replace dicts if all goes well.
1314 renamed_files = self._files.copy()
1315+ renamed_symlinks = self._symlinks.copy()
1316 renamed_dirs = self._dirs.copy()
1317 do_renames(renamed_files)
1318+ do_renames(renamed_symlinks)
1319 do_renames(renamed_dirs)
1320 # We may have been cloned so modify in place
1321 self._files.clear()
1322 self._files.update(renamed_files)
1323+ self._symlinks.clear()
1324+ self._symlinks.update(renamed_symlinks)
1325 self._dirs.clear()
1326 self._dirs.update(renamed_dirs)
1327
1328 def rmdir(self, relpath):
1329 """See Transport.rmdir."""
1330- _abspath = self._abspath(relpath)
1331+ _abspath = self._resolve_symlinks(relpath)
1332 if _abspath in self._files:
1333 self._translate_error(IOError(errno.ENOTDIR, relpath), relpath)
1334- for path in self._files:
1335+ for path in itertools.chain(self._files, self._symlinks):
1336 if path.startswith(_abspath + '/'):
1337 self._translate_error(IOError(errno.ENOTEMPTY, relpath),
1338 relpath)
1339@@ -262,13 +272,13 @@
1340 def stat(self, relpath):
1341 """See Transport.stat()."""
1342 _abspath = self._abspath(relpath)
1343- if _abspath in self._files:
1344- return MemoryStat(len(self._files[_abspath][0]), 'file',
1345+ if _abspath in self._files.keys():
1346+ return MemoryStat(len(self._files[_abspath][0]), S_IFREG,
1347 self._files[_abspath][1])
1348- elif _abspath in self._dirs:
1349- return MemoryStat(0, 'directory', self._dirs[_abspath])
1350- elif _abspath in self._symlinks:
1351- return MemoryStat(0, 'symlink', 0)
1352+ elif _abspath in self._dirs.keys():
1353+ return MemoryStat(0, S_IFDIR, self._dirs[_abspath])
1354+ elif _abspath in self._symlinks.keys():
1355+ return MemoryStat(0, S_IFLNK)
1356 else:
1357 raise NoSuchFile(_abspath)
1358
1359@@ -280,6 +290,12 @@
1360 """See Transport.lock_write()."""
1361 return _MemoryLock(self._abspath(relpath), self)
1362
1363+ def _resolve_symlinks(self, relpath):
1364+ path = self._abspath(relpath)
1365+ while path in self._symlinks.keys():
1366+ path = self._symlinks[path]
1367+ return path
1368+
1369 def _abspath(self, relpath):
1370 """Generate an internal absolute path."""
1371 relpath = urlutils.unescape(relpath)
1372@@ -336,6 +352,7 @@
1373 def start_server(self):
1374 self._dirs = {'/': None}
1375 self._files = {}
1376+ self._symlinks = {}
1377 self._locks = {}
1378 self._scheme = "memory+%s:///" % id(self)
1379
1380@@ -344,6 +361,7 @@
1381 result = memory.MemoryTransport(url)
1382 result._dirs = self._dirs
1383 result._files = self._files
1384+ result._symlinks = self._symlinks
1385 result._locks = self._locks
1386 return result
1387 self._memory_factory = memory_factory
1388
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 """
1394 return True
1395
1396+ def supports_symlinks(self):
1397+ """Does this tree support symbolic links?
1398+ """
1399+ return osutils.has_symlinks()
1400+
1401 def changes_from(self, other, want_unchanged=False, specific_files=None,
1402 extra_trees=None, require_versioned=False, include_root=False,
1403 want_unversioned=False):
1404@@ -181,7 +186,8 @@
1405 """See InterTree.iter_changes"""
1406 intertree = InterTree.get(from_tree, self)
1407 return intertree.iter_changes(include_unchanged, specific_files, pb,
1408- extra_trees, require_versioned, want_unversioned=want_unversioned)
1409+ extra_trees, require_versioned,
1410+ want_unversioned=want_unversioned)
1411
1412 def conflicts(self):
1413 """Get a list of the conflicts in the tree.
1414
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 from .trace import mutter, note
1420
1421
1422-ERROR_PATH_NOT_FOUND = 3 # WindowsError errno code, equivalent to ENOENT
1423-
1424-
1425 class SettingFileIdUnsupported(errors.BzrError):
1426
1427 _fmt = "This format does not support setting file ids."
1428@@ -123,6 +120,9 @@
1429 def control_transport(self):
1430 return self._transport
1431
1432+ def supports_symlinks(self):
1433+ return osutils.supports_symlinks(self.basedir)
1434+
1435 def is_control_filename(self, filename):
1436 """True if filename is the name of a control file in this tree.
1437
1438@@ -153,10 +153,7 @@
1439 return self._format.supports_merge_modified
1440
1441 def _supports_executable(self):
1442- if sys.platform == 'win32':
1443- return False
1444- # FIXME: Ideally this should check the file system
1445- return True
1446+ return osutils.supports_executable(self.basedir)
1447
1448 def break_lock(self):
1449 """Break a lock if one is present from another instance.
1450
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 ``RevisionTree.annotate_iter`` have been added. (Jelmer Vernooij,
1456 #897781)
1457
1458+ * Branches with symlinks are now supported on Windows. Symlinks are
1459+ ignored by operations like branch, diff etc. with a warning as Symlinks
1460+ are not created on Windows.
1461+ (Parth Malwankar, #81689)
1462+
1463 * New ``lp+bzr://`` URL scheme for Bazaar-only branches on Launchpad.
1464 (Jelmer Vernooij)
1465

Subscribers

People subscribed via source and target branches