Merge lp:~jelmer/brz/win-symlink-warning into lp:brz
- win-symlink-warning
- Merge into trunk
Proposed by
Jelmer Vernooij
Status: | Superseded | ||||
---|---|---|---|---|---|
Proposed branch: | lp:~jelmer/brz/win-symlink-warning | ||||
Merge into: | lp:brz | ||||
Diff against target: |
1464 lines (+487/-201) 27 files modified
breezy/bzr/dirstate.py (+15/-11) breezy/bzr/inventorytree.py (+1/-1) breezy/bzr/workingtree.py (+1/-0) breezy/bzr/workingtree_4.py (+2/-1) breezy/commit.py (+5/-0) breezy/delta.py (+7/-3) breezy/diff.py (+5/-0) breezy/errors.py (+0/-15) breezy/git/memorytree.py (+7/-0) breezy/git/tree.py (+8/-1) breezy/git/workingtree.py (+6/-8) breezy/memorytree.py (+21/-4) breezy/mutabletree.py (+25/-7) breezy/osutils.py (+55/-2) breezy/tests/per_tree/test_symlinks.py (+40/-0) breezy/tests/test_commit.py (+31/-0) breezy/tests/test_errors.py (+0/-14) breezy/tests/test_memorytree.py (+65/-70) breezy/tests/test_osutils.py (+21/-0) breezy/tests/test_transform.py (+55/-11) breezy/tests/test_workingtree.py (+33/-0) breezy/transform.py (+15/-10) breezy/transport/local.py (+1/-1) breezy/transport/memory.py (+52/-34) breezy/tree.py (+7/-1) breezy/workingtree.py (+4/-7) doc/en/release-notes/brz-3.0.txt (+5/-0) |
||||
To merge this branch: | bzr merge lp:~jelmer/brz/win-symlink-warning | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Breezy developers | Pending | ||
Review via email: mp+363899@code.launchpad.net |
This proposal has been superseded by a proposal from 2019-03-04.
Commit message
Description of the change
Allow symbolic links to exist when checking out trees on Windows.
Warnings are printed for the symbolic links, but the rest of the tree is still
checked out. Operations that work across the tree and require on-disk access
(e.g. "commit", "diff", "status") will warn about the symbolic links but not
otherwise prentend that the files are missing.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'breezy/bzr/dirstate.py' |
2 | --- breezy/bzr/dirstate.py 2018-11-18 19:48:57 +0000 |
3 | +++ breezy/bzr/dirstate.py 2019-03-04 01:57:09 +0000 |
4 | @@ -366,7 +366,8 @@ |
5 | 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 |