Status: | Merged |
---|---|
Approved by: | Jelmer Vernooij |
Approved revision: | no longer in the source branch. |
Merge reported by: | The Breezy Bot |
Merged at revision: | not available |
Proposed branch: | lp:~jelmer/brz/merge-3.1 |
Merge into: | lp:brz |
Diff against target: |
1683 lines (+1071/-85) 30 files modified
.github/workflows/pythonpackage.yml (+1/-1) breezy/bzr/__init__.py (+14/-1) breezy/bzr/tests/test_bzrdir.py (+11/-1) breezy/commands.py (+4/-1) breezy/dirty_tracker.py (+108/-0) breezy/errors.py (+0/-9) breezy/git/dir.py (+5/-0) breezy/git/revspec.py (+26/-17) breezy/memorybranch.py (+89/-0) breezy/plugin.py (+5/-1) breezy/plugins/github/hoster.py (+8/-0) breezy/plugins/gitlab/hoster.py (+25/-8) breezy/plugins/launchpad/hoster.py (+22/-10) breezy/plugins/launchpad/lp_api.py (+13/-0) breezy/plugins/propose/__init__.py (+1/-0) breezy/plugins/propose/cmds.py (+28/-15) breezy/plugins/svn/__init__.py (+6/-0) breezy/plugins/svn/revspec.py (+44/-0) breezy/propose.py (+32/-7) breezy/revisionspec.py (+15/-1) breezy/tests/__init__.py (+3/-0) breezy/tests/test_dirty_tracker.py (+98/-0) breezy/tests/test_memorybranch.py (+46/-0) breezy/tests/test_plugins.py (+28/-10) breezy/tests/test_workspace.py (+204/-0) breezy/workspace.py (+217/-0) byov.conf (+1/-1) doc/en/conf.py (+2/-2) doc/en/release-notes/brz-3.1.txt (+14/-0) setup.py (+1/-0) |
To merge this branch: | bzr merge lp:~jelmer/brz/merge-3.1 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jelmer Vernooij | Approve | ||
Review via email: mp+388173@code.launchpad.net |
Commit message
Merge lp:brz/3.1.
Description of the change
Merge lp:brz/3.1.
To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) : | # |
review:
Approve
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file '.github/workflows/pythonpackage.yml' |
2 | --- .github/workflows/pythonpackage.yml 2020-06-12 13:01:14 +0000 |
3 | +++ .github/workflows/pythonpackage.yml 2020-07-28 02:11:42 +0000 |
4 | @@ -32,7 +32,7 @@ |
5 | run: | |
6 | python -m pip install --upgrade pip |
7 | pip install -U pip setuptools |
8 | - pip install -U pip coverage codecov flake8 testtools paramiko fastimport configobj cython testscenarios six docutils $TEST_REQUIRE sphinx sphinx_epytext launchpadlib patiencediff git+https://github.com/dulwich/dulwich |
9 | + pip install -U pip coverage codecov flake8 testtools paramiko fastimport configobj cython testscenarios six docutils $TEST_REQUIRE sphinx sphinx_epytext launchpadlib patiencediff pyinotify git+https://github.com/dulwich/dulwich |
10 | - name: Build docs |
11 | run: | |
12 | make docs PYTHON=python |
13 | |
14 | === modified file 'breezy/bzr/__init__.py' |
15 | --- breezy/bzr/__init__.py 2020-07-18 23:14:00 +0000 |
16 | +++ breezy/bzr/__init__.py 2020-07-28 02:11:42 +0000 |
17 | @@ -23,6 +23,15 @@ |
18 | ) |
19 | |
20 | |
21 | +class LineEndingError(errors.BzrError): |
22 | + |
23 | + _fmt = ("Line ending corrupted for file: %(file)s; " |
24 | + "Maybe your files got corrupted in transport?") |
25 | + |
26 | + def __init__(self, file): |
27 | + self.file = file |
28 | + |
29 | + |
30 | class BzrProber(controldir.Prober): |
31 | """Prober for formats that use a .bzr/ control directory.""" |
32 | |
33 | @@ -50,11 +59,15 @@ |
34 | first_line = format_string[:format_string.index(b"\n") + 1] |
35 | except ValueError: |
36 | first_line = format_string |
37 | + if (first_line.startswith(b'<!DOCTYPE') or |
38 | + first_line.startswith(b'<html')): |
39 | + raise errors.NotBranchError( |
40 | + path=transport.base, detail="format file looks like HTML") |
41 | try: |
42 | cls = klass.formats.get(first_line) |
43 | except KeyError: |
44 | if first_line.endswith(b"\r\n"): |
45 | - raise errors.LineEndingError(file=".bzr/branch-format") |
46 | + raise LineEndingError(file=".bzr/branch-format") |
47 | else: |
48 | raise errors.UnknownFormatError( |
49 | format=first_line, kind='bzrdir') |
50 | |
51 | === modified file 'breezy/bzr/tests/test_bzrdir.py' |
52 | --- breezy/bzr/tests/test_bzrdir.py 2020-06-17 21:04:15 +0000 |
53 | +++ breezy/bzr/tests/test_bzrdir.py 2020-07-28 02:11:42 +0000 |
54 | @@ -318,10 +318,20 @@ |
55 | t = self.get_transport() |
56 | t.mkdir('.bzr') |
57 | t.put_bytes('.bzr/branch-format', b'Corrupt line endings\r\n') |
58 | - self.assertRaises(errors.LineEndingError, |
59 | + self.assertRaises(bzr.LineEndingError, |
60 | bzrdir.BzrDirFormat.find_format, |
61 | _mod_transport.get_transport_from_path('.')) |
62 | |
63 | + def test_find_format_html(self): |
64 | + t = self.get_transport() |
65 | + t.mkdir('.bzr') |
66 | + t.put_bytes( |
67 | + '.bzr/branch-format', |
68 | + b'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">') |
69 | + e = self.assertRaises( |
70 | + NotBranchError, bzrdir.BzrDirFormat.find_format, |
71 | + _mod_transport.get_transport_from_path('.')) |
72 | + |
73 | def test_register_unregister_format(self): |
74 | format = SampleBzrDirFormat() |
75 | url = self.get_url() |
76 | |
77 | === modified file 'breezy/commands.py' |
78 | --- breezy/commands.py 2020-06-23 01:02:30 +0000 |
79 | +++ breezy/commands.py 2020-07-28 02:11:42 +0000 |
80 | @@ -1155,7 +1155,10 @@ |
81 | debug.set_debug_flags_from_config() |
82 | |
83 | if not opt_no_plugins: |
84 | - load_plugins() |
85 | + from breezy import config |
86 | + c = config.GlobalConfig() |
87 | + warn_load_problems = not c.suppress_warning('plugin_load_failure') |
88 | + load_plugins(warn_load_problems=warn_load_problems) |
89 | else: |
90 | disable_plugins() |
91 | |
92 | |
93 | === added file 'breezy/dirty_tracker.py' |
94 | --- breezy/dirty_tracker.py 1970-01-01 00:00:00 +0000 |
95 | +++ breezy/dirty_tracker.py 2020-07-28 02:11:42 +0000 |
96 | @@ -0,0 +1,108 @@ |
97 | +#!/usr/bin/python3 |
98 | +# Copyright (C) 2019 Jelmer Vernooij |
99 | +# |
100 | +# This program is free software; you can redistribute it and/or modify |
101 | +# it under the terms of the GNU General Public License as published by |
102 | +# the Free Software Foundation; either version 2 of the License, or |
103 | +# (at your option) any later version. |
104 | +# |
105 | +# This program is distributed in the hope that it will be useful, |
106 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
107 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
108 | +# GNU General Public License for more details. |
109 | +# |
110 | +# You should have received a copy of the GNU General Public License |
111 | +# along with this program; if not, write to the Free Software |
112 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
113 | + |
114 | +"""Track whether a directory structure was touched since last revision. |
115 | +""" |
116 | + |
117 | +from __future__ import absolute_import |
118 | + |
119 | +# TODO(jelmer): Add support for ignore files |
120 | + |
121 | +import os |
122 | +try: |
123 | + from pyinotify import ( |
124 | + WatchManager, |
125 | + IN_CREATE, |
126 | + IN_CLOSE_WRITE, |
127 | + IN_Q_OVERFLOW, |
128 | + IN_DELETE, |
129 | + IN_MOVED_TO, |
130 | + IN_MOVED_FROM, |
131 | + IN_ATTRIB, |
132 | + ProcessEvent, |
133 | + Notifier, |
134 | + Event, |
135 | + ) |
136 | +except ImportError as e: |
137 | + from .errors import DependencyNotPresent |
138 | + raise DependencyNotPresent(library='pyinotify', error=e) |
139 | + |
140 | + |
141 | +MASK = ( |
142 | + IN_CLOSE_WRITE | IN_DELETE | IN_Q_OVERFLOW | IN_MOVED_TO | IN_MOVED_FROM | |
143 | + IN_ATTRIB) |
144 | + |
145 | + |
146 | +class _Process(ProcessEvent): |
147 | + |
148 | + def my_init(self): |
149 | + self.paths = set() |
150 | + self.created = set() |
151 | + |
152 | + def process_default(self, event): |
153 | + path = os.path.join(event.path, event.name) |
154 | + if event.mask & IN_CREATE: |
155 | + self.created.add(path) |
156 | + self.paths.add(path) |
157 | + if event.mask & IN_DELETE and path in self.created: |
158 | + self.paths.remove(path) |
159 | + self.created.remove(path) |
160 | + |
161 | + |
162 | +class DirtyTracker(object): |
163 | + """Track the changes to (part of) a working tree.""" |
164 | + |
165 | + def __init__(self, tree, subpath='.'): |
166 | + self._tree = tree |
167 | + self._wm = WatchManager() |
168 | + self._process = _Process() |
169 | + self._notifier = Notifier(self._wm, self._process) |
170 | + self._notifier.coalesce_events(True) |
171 | + |
172 | + def check_excluded(p): |
173 | + return tree.is_control_filename(tree.relpath(p)) |
174 | + self._wdd = self._wm.add_watch( |
175 | + tree.abspath(subpath), MASK, rec=True, auto_add=True, |
176 | + exclude_filter=check_excluded) |
177 | + |
178 | + def _process_pending(self): |
179 | + if self._notifier.check_events(timeout=0): |
180 | + self._notifier.read_events() |
181 | + self._notifier.process_events() |
182 | + |
183 | + def __del__(self): |
184 | + self._notifier.stop() |
185 | + |
186 | + def mark_clean(self): |
187 | + """Mark the subtree as not having any changes.""" |
188 | + self._process_pending() |
189 | + self._process.paths.clear() |
190 | + self._process.created.clear() |
191 | + |
192 | + def is_dirty(self): |
193 | + """Check whether there are any changes.""" |
194 | + self._process_pending() |
195 | + return bool(self._process.paths) |
196 | + |
197 | + def paths(self): |
198 | + """Return the paths that have changed.""" |
199 | + self._process_pending() |
200 | + return self._process.paths |
201 | + |
202 | + def relpaths(self): |
203 | + """Return the paths relative to the tree root that changed.""" |
204 | + return set(self._tree.relpath(p) for p in self.paths()) |
205 | |
206 | === modified file 'breezy/errors.py' |
207 | --- breezy/errors.py 2020-07-18 23:14:00 +0000 |
208 | +++ breezy/errors.py 2020-07-28 02:11:42 +0000 |
209 | @@ -563,15 +563,6 @@ |
210 | self.format = format |
211 | |
212 | |
213 | -class LineEndingError(BzrError): |
214 | - |
215 | - _fmt = ("Line ending corrupted for file: %(file)s; " |
216 | - "Maybe your files got corrupted in transport?") |
217 | - |
218 | - def __init__(self, file): |
219 | - self.file = file |
220 | - |
221 | - |
222 | class IncompatibleFormat(BzrError): |
223 | |
224 | _fmt = "Format %(format)s is not compatible with .bzr version %(controldir)s." |
225 | |
226 | === modified file 'breezy/git/dir.py' |
227 | --- breezy/git/dir.py 2020-07-18 23:14:00 +0000 |
228 | +++ breezy/git/dir.py 2020-07-28 02:11:42 +0000 |
229 | @@ -217,6 +217,11 @@ |
230 | target, basis.get_reference_revision(path), |
231 | force_new_repo=force_new_repo, recurse=recurse, |
232 | stacked=stacked) |
233 | + if getattr(result_repo, '_git', None): |
234 | + # Don't leak resources: |
235 | + # TODO(jelmer): This shouldn't be git-specific, and possibly |
236 | + # just use read locks. |
237 | + result_repo._git.object_store.close() |
238 | return result |
239 | |
240 | def clone_on_transport(self, transport, revision_id=None, |
241 | |
242 | === modified file 'breezy/git/revspec.py' |
243 | --- breezy/git/revspec.py 2020-03-25 20:24:54 +0000 |
244 | +++ breezy/git/revspec.py 2020-07-28 02:11:42 +0000 |
245 | @@ -72,9 +72,9 @@ |
246 | default_mapping.revision_id_foreign_to_bzr)(sha1) |
247 | try: |
248 | if branch.repository.has_revision(bzr_revid): |
249 | - return RevisionInfo.from_revision_id(branch, bzr_revid) |
250 | + return bzr_revid |
251 | except GitSmartRemoteNotSupported: |
252 | - return RevisionInfo(branch, None, bzr_revid) |
253 | + return bzr_revid |
254 | raise InvalidRevisionSpec(self.user_spec, branch) |
255 | |
256 | def __nonzero__(self): |
257 | @@ -92,35 +92,44 @@ |
258 | ) |
259 | parse_revid = getattr(branch.repository, "lookup_bzr_revision_id", |
260 | mapping_registry.parse_revision_id) |
261 | + def matches_revid(revid): |
262 | + if revid == NULL_REVISION: |
263 | + return False |
264 | + try: |
265 | + foreign_revid, mapping = parse_revid(revid) |
266 | + except InvalidRevisionId: |
267 | + return False |
268 | + if not isinstance(mapping.vcs, ForeignGit): |
269 | + return False |
270 | + return foreign_revid.startswith(sha1) |
271 | with branch.repository.lock_read(): |
272 | graph = branch.repository.get_graph() |
273 | - for revid, _ in graph.iter_ancestry([branch.last_revision()]): |
274 | - if revid == NULL_REVISION: |
275 | - continue |
276 | - try: |
277 | - foreign_revid, mapping = parse_revid(revid) |
278 | - except InvalidRevisionId: |
279 | - continue |
280 | - if not isinstance(mapping.vcs, ForeignGit): |
281 | - continue |
282 | - if foreign_revid.startswith(sha1): |
283 | - return RevisionInfo.from_revision_id(branch, revid) |
284 | + last_revid = branch.last_revision() |
285 | + if matches_revid(last_revid): |
286 | + return last_revid |
287 | + for revid, _ in graph.iter_ancestry([last_revid]): |
288 | + if matches_revid(revid): |
289 | + return revid |
290 | raise InvalidRevisionSpec(self.user_spec, branch) |
291 | |
292 | - def _match_on(self, branch, revs): |
293 | + def _as_revision_id(self, context_branch): |
294 | loc = self.spec.find(':') |
295 | git_sha1 = self.spec[loc + 1:].encode("utf-8") |
296 | if (len(git_sha1) > 40 or len(git_sha1) < 4 or |
297 | not valid_git_sha1(git_sha1)): |
298 | - raise InvalidRevisionSpec(self.user_spec, branch) |
299 | + raise InvalidRevisionSpec(self.user_spec, context_branch) |
300 | from . import ( |
301 | lazy_check_versions, |
302 | ) |
303 | lazy_check_versions() |
304 | if len(git_sha1) == 40: |
305 | - return self._lookup_git_sha1(branch, git_sha1) |
306 | + return self._lookup_git_sha1(context_branch, git_sha1) |
307 | else: |
308 | - return self._find_short_git_sha1(branch, git_sha1) |
309 | + return self._find_short_git_sha1(context_branch, git_sha1) |
310 | + |
311 | + def _match_on(self, branch, revs): |
312 | + revid = self._as_revision_id(branch) |
313 | + return RevisionInfo.from_revision_id(branch, revid) |
314 | |
315 | def needs_branch(self): |
316 | return True |
317 | |
318 | === added file 'breezy/memorybranch.py' |
319 | --- breezy/memorybranch.py 1970-01-01 00:00:00 +0000 |
320 | +++ breezy/memorybranch.py 2020-07-28 02:11:42 +0000 |
321 | @@ -0,0 +1,89 @@ |
322 | +# Copyright (C) 2020 Breezy Developers |
323 | +# |
324 | +# This program is free software; you can redistribute it and/or modify |
325 | +# it under the terms of the GNU General Public License as published by |
326 | +# the Free Software Foundation; either version 2 of the License, or |
327 | +# (at your option) any later version. |
328 | +# |
329 | +# This program is distributed in the hope that it will be useful, |
330 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
331 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
332 | +# GNU General Public License for more details. |
333 | +# |
334 | +# You should have received a copy of the GNU General Public License |
335 | +# along with this program; if not, write to the Free Software |
336 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
337 | + |
338 | +"""MemoryBranch object. |
339 | +""" |
340 | + |
341 | +from __future__ import absolute_import |
342 | + |
343 | +from . import config as _mod_config, errors, osutils |
344 | +from .branch import Branch |
345 | +from .lock import LogicalLockResult, _RelockDebugMixin |
346 | +from .revision import NULL_REVISION |
347 | +from .tag import DisabledTags, MemoryTags |
348 | + |
349 | + |
350 | +class MemoryBranch(Branch, _RelockDebugMixin): |
351 | + |
352 | + def __init__(self, repository, last_revision_info, tags=None): |
353 | + self.repository = repository |
354 | + self._last_revision_info = last_revision_info |
355 | + self._revision_history_cache = None |
356 | + if tags is not None: |
357 | + self.tags = MemoryTags(tags) |
358 | + else: |
359 | + self.tags = DisabledTags(self) |
360 | + self._partial_revision_history_cache = [] |
361 | + self._last_revision_info_cache = None |
362 | + self._revision_id_to_revno_cache = None |
363 | + self._partial_revision_id_to_revno_cache = {} |
364 | + self.base = 'memory://' + osutils.rand_chars(10) |
365 | + |
366 | + def get_config(self): |
367 | + return _mod_config.Config() |
368 | + |
369 | + def lock_read(self): |
370 | + self.repository.lock_read() |
371 | + return LogicalLockResult(self.unlock) |
372 | + |
373 | + def lock_write(self, token=None): |
374 | + self.repository.lock_write() |
375 | + return BranchWriteLockResult(self.unlock, None) |
376 | + |
377 | + def unlock(self): |
378 | + self.repository.unlock() |
379 | + |
380 | + def last_revision_info(self): |
381 | + return self._last_revision_info |
382 | + |
383 | + def _gen_revision_history(self): |
384 | + """Generate the revision history from last revision |
385 | + """ |
386 | + last_revno, last_revision = self.last_revision_info() |
387 | + with self.lock_read(): |
388 | + self._extend_partial_history() |
389 | + return list(reversed(self._partial_revision_history_cache)) |
390 | + |
391 | + def get_rev_id(self, revno, history=None): |
392 | + """Find the revision id of the specified revno.""" |
393 | + with self.lock_read(): |
394 | + if revno == 0: |
395 | + return NULL_REVISION |
396 | + last_revno, last_revid = self.last_revision_info() |
397 | + if revno == last_revno: |
398 | + return last_revid |
399 | + if last_revno is None: |
400 | + self._extend_partial_history() |
401 | + return self._partial_revision_history_cache[ |
402 | + len(self._partial_revision_history_cache) - revno] |
403 | + else: |
404 | + if revno <= 0 or revno > last_revno: |
405 | + raise errors.NoSuchRevision(self, revno) |
406 | + distance_from_last = last_revno - revno |
407 | + if len(self._partial_revision_history_cache) <= \ |
408 | + distance_from_last: |
409 | + self._extend_partial_history(distance_from_last) |
410 | + return self._partial_revision_history_cache[distance_from_last] |
411 | |
412 | === modified file 'breezy/plugin.py' |
413 | --- breezy/plugin.py 2020-05-24 00:42:36 +0000 |
414 | +++ breezy/plugin.py 2020-07-28 02:11:42 +0000 |
415 | @@ -76,7 +76,7 @@ |
416 | state.plugins = {} |
417 | |
418 | |
419 | -def load_plugins(path=None, state=None): |
420 | +def load_plugins(path=None, state=None, warn_load_problems=True): |
421 | """Load breezy plugins. |
422 | |
423 | The environment variable BRZ_PLUGIN_PATH is considered a delimited |
424 | @@ -103,6 +103,10 @@ |
425 | if (None, 'entrypoints') in _env_plugin_path(): |
426 | _load_plugins_from_entrypoints(state) |
427 | state.plugins = plugins() |
428 | + if warn_load_problems: |
429 | + for plugin, errors in state.plugin_warnings.items(): |
430 | + for error in errors: |
431 | + trace.warning('%s', error) |
432 | |
433 | |
434 | def _load_plugins_from_entrypoints(state): |
435 | |
436 | === modified file 'breezy/plugins/github/hoster.py' |
437 | --- breezy/plugins/github/hoster.py 2020-07-18 23:14:00 +0000 |
438 | +++ breezy/plugins/github/hoster.py 2020-07-28 02:11:42 +0000 |
439 | @@ -124,6 +124,8 @@ |
440 | def __repr__(self): |
441 | return "<%s at %r>" % (type(self).__name__, self.url) |
442 | |
443 | + name = 'GitHub' |
444 | + |
445 | @property |
446 | def url(self): |
447 | return self._pr['html_url'] |
448 | @@ -558,6 +560,12 @@ |
449 | return json.loads(response.text) |
450 | raise UnexpectedHttpStatus(path, response.status) |
451 | |
452 | + def get_current_user(self): |
453 | + return self.current_user['login'] |
454 | + |
455 | + def get_user_url(self, username): |
456 | + return urlutils.join(self.base_url, username) |
457 | + |
458 | |
459 | class GitHubMergeProposalBuilder(MergeProposalBuilder): |
460 | |
461 | |
462 | === modified file 'breezy/plugins/gitlab/hoster.py' |
463 | --- breezy/plugins/gitlab/hoster.py 2020-07-18 23:14:00 +0000 |
464 | +++ breezy/plugins/gitlab/hoster.py 2020-07-28 02:11:42 +0000 |
465 | @@ -97,6 +97,14 @@ |
466 | self.error = error |
467 | |
468 | |
469 | +class GitLabConflict(errors.BzrError): |
470 | + |
471 | + _fmt = "Conflict during operation: %(reason)s" |
472 | + |
473 | + def __init__(self, reason): |
474 | + errors.BzrError(self, reason=reason) |
475 | + |
476 | + |
477 | class ForkingDisabled(errors.BzrError): |
478 | |
479 | _fmt = ("Forking on project %(project)s is disabled.") |
480 | @@ -356,11 +364,17 @@ |
481 | return json.loads(response.data) |
482 | raise errors.UnexpectedHttpStatus(path, response.status) |
483 | |
484 | - def _fork_project(self, project_name, timeout=50, interval=5): |
485 | + def _fork_project(self, project_name, timeout=50, interval=5, owner=None): |
486 | path = 'projects/%s/fork' % urlutils.quote(str(project_name), '') |
487 | - response = self._api_request('POST', path) |
488 | + fields = {} |
489 | + if owner is not None: |
490 | + fields['namespace'] = owner |
491 | + response = self._api_request('POST', path, fields=fields) |
492 | if response.status == 404: |
493 | raise ForkingDisabled(project_name) |
494 | + if response.status == 409: |
495 | + resp = json.loads(response.data) |
496 | + raise GitLabConflict(resp.get('message')) |
497 | if response.status not in (200, 201): |
498 | raise errors.UnexpectedHttpStatus(path, response.status) |
499 | # The response should be valid JSON, but let's ignore it |
500 | @@ -376,9 +390,12 @@ |
501 | project = self._get_project(project['path_with_namespace']) |
502 | return project |
503 | |
504 | - def _get_logged_in_username(self): |
505 | + def get_current_user(self): |
506 | return self._current_user['username'] |
507 | |
508 | + def get_user_url(self, username): |
509 | + return urlutils.join(self.base_url, username) |
510 | + |
511 | def _list_paged(self, path, parameters=None, per_page=None): |
512 | if parameters is None: |
513 | parameters = {} |
514 | @@ -478,13 +495,13 @@ |
515 | allow_lossy=True, tag_selector=None): |
516 | (host, base_project, base_branch_name) = parse_gitlab_branch_url(base_branch) |
517 | if owner is None: |
518 | - owner = self._get_logged_in_username() |
519 | + owner = self.get_current_user() |
520 | if project is None: |
521 | project = self._get_project(base_project)['path'] |
522 | try: |
523 | target_project = self._get_project('%s/%s' % (owner, project)) |
524 | except NoSuchProject: |
525 | - target_project = self._fork_project(base_project) |
526 | + target_project = self._fork_project(base_project, owner=owner) |
527 | remote_repo_url = git_url_to_bzr_url(target_project['ssh_url_to_repo']) |
528 | remote_dir = controldir.ControlDir.open(remote_repo_url) |
529 | try: |
530 | @@ -504,7 +521,7 @@ |
531 | def get_derived_branch(self, base_branch, name, project=None, owner=None): |
532 | (host, base_project, base_branch_name) = parse_gitlab_branch_url(base_branch) |
533 | if owner is None: |
534 | - owner = self._get_logged_in_username() |
535 | + owner = self.get_current_user() |
536 | if project is None: |
537 | project = self._get_project(base_project)['path'] |
538 | try: |
539 | @@ -580,11 +597,11 @@ |
540 | def iter_my_proposals(self, status='open'): |
541 | state = mp_status_to_status(status) |
542 | for mp in self._list_merge_requests( |
543 | - owner=self._get_logged_in_username(), state=state): |
544 | + owner=self.get_current_user(), state=state): |
545 | yield GitLabMergeProposal(self, mp) |
546 | |
547 | def iter_my_forks(self): |
548 | - for project in self._list_projects(owner=self._get_logged_in_username()): |
549 | + for project in self._list_projects(owner=self.get_current_user()): |
550 | base_project = project.get('forked_from_project') |
551 | if not base_project: |
552 | continue |
553 | |
554 | === modified file 'breezy/plugins/launchpad/hoster.py' |
555 | --- breezy/plugins/launchpad/hoster.py 2020-07-18 23:14:00 +0000 |
556 | +++ breezy/plugins/launchpad/hoster.py 2020-07-28 02:11:42 +0000 |
557 | @@ -222,16 +222,17 @@ |
558 | |
559 | merge_proposal_description_format = 'plain' |
560 | |
561 | - def __init__(self, staging=False): |
562 | - self._staging = staging |
563 | - if staging: |
564 | - lp_base_url = uris.STAGING_SERVICE_ROOT |
565 | - else: |
566 | - lp_base_url = uris.LPNET_SERVICE_ROOT |
567 | - self._api_base_url = lp_base_url |
568 | + def __init__(self, service_root): |
569 | + self._api_base_url = service_root |
570 | self._launchpad = None |
571 | |
572 | @property |
573 | + def name(self): |
574 | + if self._api_base_url == uris.LPNET_SERVICE_ROOT: |
575 | + return 'Launchpad' |
576 | + return 'Launchpad at %s' % self.base_url |
577 | + |
578 | + @property |
579 | def launchpad(self): |
580 | if self._launchpad is None: |
581 | self._launchpad = lp_api.connect_launchpad(self._api_base_url, version='devel') |
582 | @@ -242,7 +243,13 @@ |
583 | return lp_api.uris.web_root_for_service_root(self._api_base_url) |
584 | |
585 | def __repr__(self): |
586 | - return "Launchpad(staging=%s)" % self._staging |
587 | + return "Launchpad(service_root=%s)" % self._api_base_url |
588 | + |
589 | + def get_current_user(self): |
590 | + return self.launchpad.me.name |
591 | + |
592 | + def get_user_url(self, username): |
593 | + return self.launchpad.people[username].web_link |
594 | |
595 | def hosts(self, branch): |
596 | # TODO(jelmer): staging vs non-staging? |
597 | @@ -251,7 +258,7 @@ |
598 | @classmethod |
599 | def probe_from_url(cls, url, possible_transports=None): |
600 | if plausible_launchpad_url(url): |
601 | - return Launchpad() |
602 | + return Launchpad(uris.LPNET_SERVICE_ROOT) |
603 | raise UnsupportedHoster(url) |
604 | |
605 | def _get_lp_git_ref_from_branch(self, branch): |
606 | @@ -462,7 +469,12 @@ |
607 | |
608 | @classmethod |
609 | def iter_instances(cls): |
610 | - yield cls() |
611 | + credential_store = lp_api.get_credential_store() |
612 | + for service_root in set(uris.service_roots.values()): |
613 | + auth_engine = lp_api.get_auth_engine(service_root) |
614 | + creds = credential_store.load(auth_engine.unique_consumer_id) |
615 | + if creds is not None: |
616 | + yield cls(service_root) |
617 | |
618 | def iter_my_proposals(self, status='open'): |
619 | statuses = status_to_lp_mp_statuses(status) |
620 | |
621 | === modified file 'breezy/plugins/launchpad/lp_api.py' |
622 | --- breezy/plugins/launchpad/lp_api.py 2020-02-18 01:57:45 +0000 |
623 | +++ breezy/plugins/launchpad/lp_api.py 2020-07-28 02:11:42 +0000 |
624 | @@ -52,6 +52,7 @@ |
625 | except ImportError as e: |
626 | raise LaunchpadlibMissing(e) |
627 | |
628 | +from launchpadlib.credentials import RequestTokenAuthorizationEngine |
629 | from launchpadlib.launchpad import ( |
630 | Launchpad, |
631 | ) |
632 | @@ -98,6 +99,14 @@ |
633 | errors.BzrError.__init__(self, branch=branch, url=branch.base) |
634 | |
635 | |
636 | +def get_auth_engine(base_url): |
637 | + return Launchpad.authorization_engine_factory(base_url, 'breezy') |
638 | + |
639 | + |
640 | +def get_credential_store(): |
641 | + return Launchpad.credential_store_factory(None) |
642 | + |
643 | + |
644 | def connect_launchpad(base_url, timeout=None, proxy_info=None, |
645 | version=Launchpad.DEFAULT_VERSION): |
646 | """Log in to the Launchpad API. |
647 | @@ -111,8 +120,12 @@ |
648 | cache_directory = get_cache_directory() |
649 | except EnvironmentError: |
650 | cache_directory = None |
651 | + credential_store = get_credential_store() |
652 | + authorization_engine = get_auth_engine(base_url) |
653 | return Launchpad.login_with( |
654 | 'breezy', base_url, cache_directory, timeout=timeout, |
655 | + credential_store=credential_store, |
656 | + authorization_engine=authorization_engine, |
657 | proxy_info=proxy_info, version=version) |
658 | |
659 | |
660 | |
661 | === modified file 'breezy/plugins/propose/__init__.py' |
662 | --- breezy/plugins/propose/__init__.py 2020-06-01 21:57:00 +0000 |
663 | +++ breezy/plugins/propose/__init__.py 2020-07-28 02:11:42 +0000 |
664 | @@ -26,6 +26,7 @@ |
665 | plugin_cmds.register_lazy( |
666 | "cmd_my_merge_proposals", ["my-proposals"], |
667 | __name__ + ".cmds") |
668 | +plugin_cmds.register_lazy("cmd_hosters", [], __name__ + ".cmds") |
669 | |
670 | |
671 | def test_suite(): |
672 | |
673 | === modified file 'breezy/plugins/propose/cmds.py' |
674 | --- breezy/plugins/propose/cmds.py 2020-06-23 01:02:30 +0000 |
675 | +++ breezy/plugins/propose/cmds.py 2020-07-28 02:11:42 +0000 |
676 | @@ -267,21 +267,20 @@ |
677 | closed='Closed merge proposals')] |
678 | |
679 | def run(self, status='open', verbose=False): |
680 | - for name, hoster_cls in _mod_propose.hosters.items(): |
681 | - for instance in hoster_cls.iter_instances(): |
682 | - for mp in instance.iter_my_proposals(status=status): |
683 | - self.outf.write('%s\n' % mp.url) |
684 | - if verbose: |
685 | - self.outf.write( |
686 | - '(Merging %s into %s)\n' % |
687 | - (mp.get_source_branch_url(), |
688 | - mp.get_target_branch_url())) |
689 | - description = mp.get_description() |
690 | - if description: |
691 | - self.outf.writelines( |
692 | - ['\t%s\n' % l |
693 | - for l in description.splitlines()]) |
694 | - self.outf.write('\n') |
695 | + for instance in _mod_propose.iter_hoster_instances(): |
696 | + for mp in instance.iter_my_proposals(status=status): |
697 | + self.outf.write('%s\n' % mp.url) |
698 | + if verbose: |
699 | + self.outf.write( |
700 | + '(Merging %s into %s)\n' % |
701 | + (mp.get_source_branch_url(), |
702 | + mp.get_target_branch_url())) |
703 | + description = mp.get_description() |
704 | + if description: |
705 | + self.outf.writelines( |
706 | + ['\t%s\n' % l |
707 | + for l in description.splitlines()]) |
708 | + self.outf.write('\n') |
709 | |
710 | |
711 | class cmd_land_merge_proposal(Command): |
712 | @@ -294,3 +293,17 @@ |
713 | def run(self, url, message=None): |
714 | proposal = _mod_propose.get_proposal_by_url(url) |
715 | proposal.merge(commit_message=message) |
716 | + |
717 | + |
718 | +class cmd_hosters(Command): |
719 | + __doc__ = """List all known hosting sites and user details.""" |
720 | + |
721 | + hidden = True |
722 | + |
723 | + def run(self): |
724 | + for instance in _mod_propose.iter_hoster_instances(): |
725 | + current_user = instance.get_current_user() |
726 | + self.outf.write( |
727 | + gettext('%s (%s) - user: %s (%s)\n') % ( |
728 | + instance.name, instance.base_url, |
729 | + current_user, instance.get_user_url(current_user))) |
730 | |
731 | === modified file 'breezy/plugins/svn/__init__.py' |
732 | --- breezy/plugins/svn/__init__.py 2020-02-18 01:57:45 +0000 |
733 | +++ breezy/plugins/svn/__init__.py 2020-07-28 02:11:42 +0000 |
734 | @@ -26,6 +26,9 @@ |
735 | errors, |
736 | transport as _mod_transport, |
737 | ) |
738 | +from ...revisionspec import ( |
739 | + revspec_registry, |
740 | + ) |
741 | |
742 | |
743 | class SubversionUnsupportedError(errors.UnsupportedFormatError): |
744 | @@ -183,6 +186,9 @@ |
745 | controldir.ControlDirFormat.register_prober(SvnRepositoryProber) |
746 | |
747 | |
748 | +revspec_registry.register_lazy("svn:", __name__ + ".revspec", "RevisionSpec_svn") |
749 | + |
750 | + |
751 | _mod_transport.register_transport_proto( |
752 | 'svn+ssh://', |
753 | help="Access using the Subversion smart server tunneled over SSH.") |
754 | |
755 | === added file 'breezy/plugins/svn/revspec.py' |
756 | --- breezy/plugins/svn/revspec.py 1970-01-01 00:00:00 +0000 |
757 | +++ breezy/plugins/svn/revspec.py 2020-07-28 02:11:42 +0000 |
758 | @@ -0,0 +1,44 @@ |
759 | +# Copyright (C) 2020 Jelmer Vernooij <jelmer@samba.org> |
760 | +# |
761 | +# This program is free software; you can redistribute it and/or modify |
762 | +# it under the terms of the GNU General Public License as published by |
763 | +# the Free Software Foundation; version 3 of the License or |
764 | +# (at your option) a later version. |
765 | +# |
766 | +# This program is distributed in the hope that it will be useful, |
767 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
768 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
769 | +# GNU General Public License for more details. |
770 | +# |
771 | +# You should have received a copy of the GNU General Public License |
772 | +# along with this program; if not, write to the Free Software |
773 | +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
774 | + |
775 | +from __future__ import absolute_import |
776 | + |
777 | +from ...revisionspec import ( |
778 | + InvalidRevisionSpec, |
779 | + RevisionSpec, |
780 | + ) |
781 | + |
782 | + |
783 | +class RevisionSpec_svn(RevisionSpec): |
784 | + """Selects a revision using a Subversion revision number.""" |
785 | + |
786 | + help_txt = """Selects a revision using a Subversion revision number (revno). |
787 | + |
788 | + Subversion revision numbers are per-repository whereas Bazaar revision |
789 | + numbers are per-branch. This revision specifier allows specifying |
790 | + a Subversion revision number. |
791 | + """ |
792 | + |
793 | + prefix = 'svn:' |
794 | + |
795 | + def _match_on(self, branch, revs): |
796 | + raise InvalidRevisionSpec(self.user_spec, branch) |
797 | + |
798 | + def needs_branch(self): |
799 | + return True |
800 | + |
801 | + def get_branch(self): |
802 | + return None |
803 | |
804 | === modified file 'breezy/propose.py' |
805 | --- breezy/propose.py 2020-06-23 01:02:30 +0000 |
806 | +++ breezy/propose.py 2020-07-28 02:11:42 +0000 |
807 | @@ -238,6 +238,11 @@ |
808 | # proposals? |
809 | supports_merge_proposal_labels = None |
810 | |
811 | + @property |
812 | + def name(self): |
813 | + """Name of this instance.""" |
814 | + return "%s at %s" % (type(self).__name__, self.base_url) |
815 | + |
816 | # Does this hoster support suggesting a commit message in the |
817 | # merge proposal? |
818 | supports_merge_proposal_commit_message = None |
819 | @@ -264,7 +269,7 @@ |
820 | :raise HosterLoginRequired: Action requires a hoster login, but none is |
821 | known. |
822 | """ |
823 | - raise NotImplementedError(self.publish) |
824 | + raise NotImplementedError(self.publish_derived) |
825 | |
826 | def get_derived_branch(self, base_branch, name, project=None, owner=None): |
827 | """Get a derived branch ('a fork'). |
828 | @@ -352,6 +357,17 @@ |
829 | """ |
830 | raise NotImplementedError(cls.iter_instances) |
831 | |
832 | + def get_current_user(self): |
833 | + """Retrieve the name of the currently logged in user. |
834 | + |
835 | + :return: Username or None if not logged in |
836 | + """ |
837 | + raise NotImplementedError(self.get_current_user) |
838 | + |
839 | + def get_user_url(self, user): |
840 | + """Rerieve the web URL for a user.""" |
841 | + raise NotImplementedError(self.get_user_url) |
842 | + |
843 | |
844 | def determine_title(description): |
845 | """Determine the title for a merge proposal based on full description.""" |
846 | @@ -382,6 +398,16 @@ |
847 | raise UnsupportedHoster(branch) |
848 | |
849 | |
850 | +def iter_hoster_instances(): |
851 | + """Iterate over all known hoster instances. |
852 | + |
853 | + :return: Iterator over Hoster instances |
854 | + """ |
855 | + for name, hoster_cls in hosters.items(): |
856 | + for instance in hoster_cls.iter_instances(): |
857 | + yield instance |
858 | + |
859 | + |
860 | def get_proposal_by_url(url): |
861 | """Get the proposal object associated with a URL. |
862 | |
863 | @@ -389,12 +415,11 @@ |
864 | :raise UnsupportedHoster: if there is no hoster that supports the URL |
865 | :return: A `MergeProposal` object |
866 | """ |
867 | - for name, hoster_cls in hosters.items(): |
868 | - for instance in hoster_cls.iter_instances(): |
869 | - try: |
870 | - return instance.get_proposal_by_url(url) |
871 | - except UnsupportedHoster: |
872 | - pass |
873 | + for instance in iter_hoster_instances(): |
874 | + try: |
875 | + return instance.get_proposal_by_url(url) |
876 | + except UnsupportedHoster: |
877 | + pass |
878 | raise UnsupportedHoster(url) |
879 | |
880 | |
881 | |
882 | === modified file 'breezy/revisionspec.py' |
883 | --- breezy/revisionspec.py 2020-03-25 21:52:04 +0000 |
884 | +++ breezy/revisionspec.py 2020-07-28 02:11:42 +0000 |
885 | @@ -51,6 +51,20 @@ |
886 | self.extra = '' |
887 | |
888 | |
889 | +class InvalidRevisionSpec(errors.BzrError): |
890 | + |
891 | + _fmt = ("Requested revision: '%(spec)s' does not exist in branch:" |
892 | + " %(branch_url)s%(extra)s") |
893 | + |
894 | + def __init__(self, spec, branch, extra=None): |
895 | + errors.BzrError.__init__(self, branch=branch, spec=spec) |
896 | + self.branch_url = getattr(branch, 'user_url', str(branch)) |
897 | + if extra: |
898 | + self.extra = '\n' + str(extra) |
899 | + else: |
900 | + self.extra = '' |
901 | + |
902 | + |
903 | class RevisionInfo(object): |
904 | """The results of applying a revision specification to a branch.""" |
905 | |
906 | @@ -599,7 +613,7 @@ |
907 | self.user_spec, context_branch, 'cannot find the matching revision') |
908 | parents = parent_map[base_revision_id] |
909 | if len(parents) < 1: |
910 | - raise errors.InvalidRevisionSpec( |
911 | + raise InvalidRevisionSpec( |
912 | self.user_spec, context_branch, 'No parents for revision.') |
913 | return parents[0] |
914 | |
915 | |
916 | === modified file 'breezy/tests/__init__.py' |
917 | --- breezy/tests/__init__.py 2020-07-18 23:14:00 +0000 |
918 | +++ breezy/tests/__init__.py 2020-07-28 02:11:42 +0000 |
919 | @@ -4018,6 +4018,7 @@ |
920 | 'breezy.tests.test_debug', |
921 | 'breezy.tests.test_diff', |
922 | 'breezy.tests.test_directory_service', |
923 | + 'breezy.tests.test_dirty_tracker', |
924 | 'breezy.tests.test_email_message', |
925 | 'breezy.tests.test_eol_filters', |
926 | 'breezy.tests.test_errors', |
927 | @@ -4062,6 +4063,7 @@ |
928 | 'breezy.tests.test_lsprof', |
929 | 'breezy.tests.test_mail_client', |
930 | 'breezy.tests.test_matchers', |
931 | + 'breezy.tests.test_memorybranch', |
932 | 'breezy.tests.test_memorytree', |
933 | 'breezy.tests.test_merge', |
934 | 'breezy.tests.test_merge3', |
935 | @@ -4142,6 +4144,7 @@ |
936 | 'breezy.tests.test_views', |
937 | 'breezy.tests.test_whitebox', |
938 | 'breezy.tests.test_win32utils', |
939 | + 'breezy.tests.test_workspace', |
940 | 'breezy.tests.test_workingtree', |
941 | 'breezy.tests.test_wsgi', |
942 | ] |
943 | |
944 | === added file 'breezy/tests/test_dirty_tracker.py' |
945 | --- breezy/tests/test_dirty_tracker.py 1970-01-01 00:00:00 +0000 |
946 | +++ breezy/tests/test_dirty_tracker.py 2020-07-28 02:11:42 +0000 |
947 | @@ -0,0 +1,98 @@ |
948 | +#!/usr/bin/python |
949 | +# Copyright (C) 2019 Jelmer Vernooij |
950 | +# |
951 | +# This program is free software; you can redistribute it and/or modify |
952 | +# it under the terms of the GNU General Public License as published by |
953 | +# the Free Software Foundation; either version 2 of the License, or |
954 | +# (at your option) any later version. |
955 | +# |
956 | +# This program is distributed in the hope that it will be useful, |
957 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
958 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
959 | +# GNU General Public License for more details. |
960 | +# |
961 | +# You should have received a copy of the GNU General Public License |
962 | +# along with this program; if not, write to the Free Software |
963 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
964 | + |
965 | +"""Tests for lintian_brush.dirty_tracker.""" |
966 | + |
967 | +import os |
968 | + |
969 | +from breezy.tests import ( |
970 | + TestCaseWithTransport, |
971 | + ) |
972 | + |
973 | + |
974 | +class DirtyTrackerTests(TestCaseWithTransport): |
975 | + |
976 | + def setUp(self): |
977 | + super(DirtyTrackerTests, self).setUp() |
978 | + self.tree = self.make_branch_and_tree('tree') |
979 | + try: |
980 | + from lintian_brush.dirty_tracker import DirtyTracker |
981 | + except ImportError: |
982 | + self.skipTest('pyinotify not available') |
983 | + self.tracker = DirtyTracker(self.tree) |
984 | + |
985 | + def test_nothing_changes(self): |
986 | + self.assertFalse(self.tracker.is_dirty()) |
987 | + |
988 | + def test_regular_file_added(self): |
989 | + self.build_tree_contents([('tree/foo', 'bar')]) |
990 | + self.assertTrue(self.tracker.is_dirty()) |
991 | + self.assertEqual(self.tracker.relpaths(), set(['foo'])) |
992 | + |
993 | + def test_many_added(self): |
994 | + self.build_tree_contents( |
995 | + [('tree/f%d' % d, 'content') for d in range(100)]) |
996 | + self.assertTrue(self.tracker.is_dirty()) |
997 | + self.assertEqual( |
998 | + self.tracker.relpaths(), set(['f%d' % d for d in range(100)])) |
999 | + |
1000 | + def test_regular_file_in_subdir_added(self): |
1001 | + self.build_tree_contents([('tree/foo/', ), ('tree/foo/blah', 'bar')]) |
1002 | + self.assertTrue(self.tracker.is_dirty()) |
1003 | + self.assertEqual(self.tracker.relpaths(), set(['foo', 'foo/blah'])) |
1004 | + |
1005 | + def test_directory_added(self): |
1006 | + self.build_tree_contents([('tree/foo/', )]) |
1007 | + self.assertTrue(self.tracker.is_dirty()) |
1008 | + self.assertEqual(self.tracker.relpaths(), set(['foo'])) |
1009 | + |
1010 | + def test_file_removed(self): |
1011 | + self.build_tree_contents([('tree/foo', 'foo')]) |
1012 | + self.assertTrue(self.tracker.is_dirty()) |
1013 | + self.tracker.mark_clean() |
1014 | + self.build_tree_contents([('tree/foo', 'bar')]) |
1015 | + self.assertTrue(self.tracker.is_dirty()) |
1016 | + self.assertEqual(self.tracker.relpaths(), set(['foo'])) |
1017 | + |
1018 | + def test_control_file(self): |
1019 | + self.tree.commit('Some change') |
1020 | + self.assertFalse(self.tracker.is_dirty()) |
1021 | + self.assertEqual(self.tracker.relpaths(), set([])) |
1022 | + |
1023 | + def test_renamed(self): |
1024 | + self.build_tree_contents([('tree/foo', 'bar')]) |
1025 | + self.tracker.mark_clean() |
1026 | + self.assertFalse(self.tracker.is_dirty()) |
1027 | + os.rename('tree/foo', 'tree/bar') |
1028 | + self.assertTrue(self.tracker.is_dirty()) |
1029 | + self.assertEqual(self.tracker.relpaths(), set(['foo', 'bar'])) |
1030 | + |
1031 | + def test_deleted(self): |
1032 | + self.build_tree_contents([('tree/foo', 'bar')]) |
1033 | + self.tracker.mark_clean() |
1034 | + self.assertFalse(self.tracker.is_dirty()) |
1035 | + os.unlink('tree/foo') |
1036 | + self.assertTrue(self.tracker.is_dirty(), self.tracker._process.paths) |
1037 | + self.assertEqual(self.tracker.relpaths(), set(['foo'])) |
1038 | + |
1039 | + def test_added_then_deleted(self): |
1040 | + self.tracker.mark_clean() |
1041 | + self.assertFalse(self.tracker.is_dirty()) |
1042 | + self.build_tree_contents([('tree/foo', 'bar')]) |
1043 | + os.unlink('tree/foo') |
1044 | + self.assertFalse(self.tracker.is_dirty()) |
1045 | + self.assertEqual(self.tracker.relpaths(), set([])) |
1046 | |
1047 | === added file 'breezy/tests/test_memorybranch.py' |
1048 | --- breezy/tests/test_memorybranch.py 1970-01-01 00:00:00 +0000 |
1049 | +++ breezy/tests/test_memorybranch.py 2020-07-28 02:11:42 +0000 |
1050 | @@ -0,0 +1,46 @@ |
1051 | +# Copyright (C) 2020 Jelmer Vernooij |
1052 | +# |
1053 | +# This program is free software; you can redistribute it and/or modify |
1054 | +# it under the terms of the GNU General Public License as published by |
1055 | +# the Free Software Foundation; either version 2 of the License, or |
1056 | +# (at your option) any later version. |
1057 | +# |
1058 | +# This program is distributed in the hope that it will be useful, |
1059 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1060 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1061 | +# GNU General Public License for more details. |
1062 | +# |
1063 | +# You should have received a copy of the GNU General Public License |
1064 | +# along with this program; if not, write to the Free Software |
1065 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
1066 | + |
1067 | +from . import TestCaseWithTransport |
1068 | + |
1069 | +from ..memorybranch import MemoryBranch |
1070 | + |
1071 | + |
1072 | +class MemoryBranchTests(TestCaseWithTransport): |
1073 | + |
1074 | + def setUp(self): |
1075 | + super(MemoryBranchTests, self).setUp() |
1076 | + self.tree = self.make_branch_and_tree('.') |
1077 | + self.revid1 = self.tree.commit('rev1') |
1078 | + self.revid2 = self.tree.commit('rev2') |
1079 | + self.branch = MemoryBranch( |
1080 | + self.tree.branch.repository, (2, self.revid2)) |
1081 | + |
1082 | + def test_last_revision_info(self): |
1083 | + self.assertEqual((2, self.revid2), self.branch.last_revision_info()) |
1084 | + |
1085 | + def test_last_revision(self): |
1086 | + self.assertEqual(self.revid2, self.branch.last_revision()) |
1087 | + |
1088 | + def test_revno(self): |
1089 | + self.assertEqual(2, self.branch.revno()) |
1090 | + |
1091 | + def test_get_rev_id(self): |
1092 | + self.assertEqual(self.revid1, self.branch.get_rev_id(1)) |
1093 | + |
1094 | + def test_revision_id_to_revno(self): |
1095 | + self.assertEqual(2, self.branch.revision_id_to_revno(self.revid2)) |
1096 | + self.assertEqual(1, self.branch.revision_id_to_revno(self.revid1)) |
1097 | |
1098 | === modified file 'breezy/tests/test_plugins.py' |
1099 | --- breezy/tests/test_plugins.py 2020-02-07 02:14:30 +0000 |
1100 | +++ breezy/tests/test_plugins.py 2020-07-28 02:11:42 +0000 |
1101 | @@ -76,9 +76,11 @@ |
1102 | self.log("using %r", paths) |
1103 | return paths |
1104 | |
1105 | - def load_with_paths(self, paths): |
1106 | + def load_with_paths(self, paths, warn_load_problems=True): |
1107 | self.log("loading plugins!") |
1108 | - plugin.load_plugins(self.update_module_paths(paths), state=self) |
1109 | + plugin.load_plugins( |
1110 | + self.update_module_paths(paths), state=self, |
1111 | + warn_load_problems=warn_load_problems) |
1112 | |
1113 | def create_plugin(self, name, source=None, dir='.', file_name=None): |
1114 | if source is None: |
1115 | @@ -260,7 +262,7 @@ |
1116 | finally: |
1117 | del self.activeattributes[tempattribute] |
1118 | |
1119 | - def load_and_capture(self, name): |
1120 | + def load_and_capture(self, name, warn_load_problems=True): |
1121 | """Load plugins from '.' capturing the output. |
1122 | |
1123 | :param name: The name of the plugin. |
1124 | @@ -273,7 +275,8 @@ |
1125 | log = logging.getLogger('brz') |
1126 | log.addHandler(handler) |
1127 | try: |
1128 | - self.load_with_paths(['.']) |
1129 | + self.load_with_paths( |
1130 | + ['.'], warn_load_problems=warn_load_problems) |
1131 | finally: |
1132 | # Stop capturing output |
1133 | handler.flush() |
1134 | @@ -286,18 +289,16 @@ |
1135 | def test_plugin_with_bad_api_version_reports(self): |
1136 | """Try loading a plugin that requests an unsupported api. |
1137 | |
1138 | - Observe that it records the problem but doesn't complain on stderr. |
1139 | - |
1140 | - See https://bugs.launchpad.net/bzr/+bug/704195 |
1141 | + Observe that it records the problem but doesn't complain on stderr |
1142 | + when warn_load_problems=False |
1143 | """ |
1144 | name = 'wants100.py' |
1145 | with open(name, 'w') as f: |
1146 | f.write("import breezy\n" |
1147 | "from breezy.errors import IncompatibleVersion\n" |
1148 | "raise IncompatibleVersion(breezy, [(1, 0, 0)], (0, 0, 5))\n") |
1149 | - log = self.load_and_capture(name) |
1150 | - self.assertNotContainsRe(log, |
1151 | - r"It supports breezy version") |
1152 | + log = self.load_and_capture(name, warn_load_problems=False) |
1153 | + self.assertNotContainsRe(log, r"It supports breezy version") |
1154 | self.assertEqual({'wants100'}, self.plugin_warnings.keys()) |
1155 | self.assertContainsRe( |
1156 | self.plugin_warnings['wants100'][0], |
1157 | @@ -313,6 +314,23 @@ |
1158 | "because the file path isn't a valid module name; try renaming " |
1159 | "it to 'bad_plugin_name_'\\.") |
1160 | |
1161 | + def test_plugin_with_error_suppress(self): |
1162 | + # The file name here invalid for a python module. |
1163 | + name = 'some_error.py' |
1164 | + with open(name, 'w') as f: |
1165 | + f.write('raise Exception("bad")\n') |
1166 | + log = self.load_and_capture(name, warn_load_problems=False) |
1167 | + self.assertEqual('', log) |
1168 | + |
1169 | + def test_plugin_with_error(self): |
1170 | + # The file name here invalid for a python module. |
1171 | + name = 'some_error.py' |
1172 | + with open(name, 'w') as f: |
1173 | + f.write('raise Exception("bad")\n') |
1174 | + log = self.load_and_capture(name, warn_load_problems=True) |
1175 | + self.assertEqual( |
1176 | + 'Unable to load plugin \'some_error\' from \'.\': bad\n', log) |
1177 | + |
1178 | |
1179 | class TestPlugins(BaseTestPlugins): |
1180 | |
1181 | |
1182 | === added file 'breezy/tests/test_workspace.py' |
1183 | --- breezy/tests/test_workspace.py 1970-01-01 00:00:00 +0000 |
1184 | +++ breezy/tests/test_workspace.py 2020-07-28 02:11:42 +0000 |
1185 | @@ -0,0 +1,204 @@ |
1186 | +# Copyright (C) 2020 Jelmer Vernooij <jelmer@jelmer.uk> |
1187 | +# |
1188 | +# This program is free software; you can redistribute it and/or modify |
1189 | +# it under the terms of the GNU General Public License as published by |
1190 | +# the Free Software Foundation; either version 2 of the License, or |
1191 | +# (at your option) any later version. |
1192 | +# |
1193 | +# This program is distributed in the hope that it will be useful, |
1194 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1195 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1196 | +# GNU General Public License for more details. |
1197 | +# |
1198 | +# You should have received a copy of the GNU General Public License |
1199 | +# along with this program; if not, write to the Free Software |
1200 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
1201 | + |
1202 | +import os |
1203 | + |
1204 | +from . import ( |
1205 | + TestCaseWithTransport, |
1206 | + multiply_scenarios, |
1207 | + ) |
1208 | +from .scenarios import load_tests_apply_scenarios |
1209 | + |
1210 | +from ..workspace import ( |
1211 | + WorkspaceDirty, |
1212 | + Workspace, |
1213 | + check_clean_tree, |
1214 | + ) |
1215 | + |
1216 | + |
1217 | +load_tests = load_tests_apply_scenarios |
1218 | + |
1219 | + |
1220 | +class CheckCleanTreeTests(TestCaseWithTransport): |
1221 | + |
1222 | + def make_test_tree(self, format=None): |
1223 | + tree = self.make_branch_and_tree('.', format=format) |
1224 | + self.build_tree_contents([ |
1225 | + ('debian/', ), |
1226 | + ('debian/control', """\ |
1227 | +Source: blah |
1228 | +Vcs-Git: https://example.com/blah |
1229 | +Testsuite: autopkgtest |
1230 | + |
1231 | +Binary: blah |
1232 | +Arch: all |
1233 | + |
1234 | +"""), |
1235 | + ('debian/changelog', 'Some contents')]) |
1236 | + tree.add(['debian', 'debian/changelog', 'debian/control']) |
1237 | + tree.commit('Initial thingy.') |
1238 | + return tree |
1239 | + |
1240 | + def test_pending_changes(self): |
1241 | + tree = self.make_test_tree() |
1242 | + self.build_tree_contents([('debian/changelog', 'blah')]) |
1243 | + with tree.lock_write(): |
1244 | + self.assertRaises( |
1245 | + WorkspaceDirty, check_clean_tree, tree) |
1246 | + |
1247 | + def test_pending_changes_bzr_empty_dir(self): |
1248 | + # See https://bugs.debian.org/914038 |
1249 | + tree = self.make_test_tree(format='bzr') |
1250 | + self.build_tree_contents([('debian/upstream/', )]) |
1251 | + with tree.lock_write(): |
1252 | + self.assertRaises( |
1253 | + WorkspaceDirty, check_clean_tree, tree) |
1254 | + |
1255 | + def test_pending_changes_git_empty_dir(self): |
1256 | + # See https://bugs.debian.org/914038 |
1257 | + tree = self.make_test_tree(format='git') |
1258 | + self.build_tree_contents([('debian/upstream/', )]) |
1259 | + with tree.lock_write(): |
1260 | + check_clean_tree(tree) |
1261 | + |
1262 | + def test_pending_changes_git_dir_with_ignored(self): |
1263 | + # See https://bugs.debian.org/914038 |
1264 | + tree = self.make_test_tree(format='git') |
1265 | + self.build_tree_contents([ |
1266 | + ('debian/upstream/', ), |
1267 | + ('debian/upstream/blah', ''), |
1268 | + ('.gitignore', 'blah\n'), |
1269 | + ]) |
1270 | + tree.add('.gitignore') |
1271 | + tree.commit('add gitignore') |
1272 | + with tree.lock_write(): |
1273 | + check_clean_tree(tree) |
1274 | + |
1275 | + def test_extra(self): |
1276 | + tree = self.make_test_tree() |
1277 | + self.build_tree_contents([('debian/foo', 'blah')]) |
1278 | + with tree.lock_write(): |
1279 | + self.assertRaises( |
1280 | + WorkspaceDirty, check_clean_tree, |
1281 | + tree) |
1282 | + |
1283 | + |
1284 | +def vary_by_inotify(): |
1285 | + return [ |
1286 | + ('with_inotify', dict(_use_inotify=True)), |
1287 | + ('without_inotify', dict(_use_inotify=False)), |
1288 | + ] |
1289 | + |
1290 | + |
1291 | +def vary_by_format(): |
1292 | + return [ |
1293 | + ('bzr', dict(_format='bzr')), |
1294 | + ('git', dict(_format='git')), |
1295 | + ] |
1296 | + |
1297 | + |
1298 | +class WorkspaceTests(TestCaseWithTransport): |
1299 | + |
1300 | + scenarios = multiply_scenarios( |
1301 | + vary_by_inotify(), |
1302 | + vary_by_format(), |
1303 | + ) |
1304 | + |
1305 | + def test_root_add(self): |
1306 | + tree = self.make_branch_and_tree('.', format=self._format) |
1307 | + with Workspace(tree, use_inotify=self._use_inotify) as ws: |
1308 | + self.build_tree_contents([('afile', 'somecontents')]) |
1309 | + changes = [c for c in ws.iter_changes() if c.path[1] != ''] |
1310 | + self.assertEqual(1, len(changes), changes) |
1311 | + self.assertEqual((None, 'afile'), changes[0].path) |
1312 | + ws.commit(message='Commit message') |
1313 | + self.assertEqual(list(ws.iter_changes()), []) |
1314 | + self.build_tree_contents([('afile', 'newcontents')]) |
1315 | + [change] = list(ws.iter_changes()) |
1316 | + self.assertEqual(('afile', 'afile'), change.path) |
1317 | + |
1318 | + def test_root_remove(self): |
1319 | + tree = self.make_branch_and_tree('.', format=self._format) |
1320 | + self.build_tree_contents([('afile', 'somecontents')]) |
1321 | + tree.add(['afile']) |
1322 | + tree.commit('Afile') |
1323 | + with Workspace(tree, use_inotify=self._use_inotify) as ws: |
1324 | + os.remove('afile') |
1325 | + changes = list(ws.iter_changes()) |
1326 | + self.assertEqual(1, len(changes), changes) |
1327 | + self.assertEqual(('afile', None), changes[0].path) |
1328 | + ws.commit(message='Commit message') |
1329 | + self.assertEqual(list(ws.iter_changes()), []) |
1330 | + |
1331 | + def test_subpath_add(self): |
1332 | + tree = self.make_branch_and_tree('.', format=self._format) |
1333 | + self.build_tree(['subpath/']) |
1334 | + tree.add('subpath') |
1335 | + tree.commit('add subpath') |
1336 | + with Workspace( |
1337 | + tree, subpath='subpath', use_inotify=self._use_inotify) as ws: |
1338 | + self.build_tree_contents([('outside', 'somecontents')]) |
1339 | + self.build_tree_contents([('subpath/afile', 'somecontents')]) |
1340 | + changes = [c for c in ws.iter_changes() if c.path[1] != 'subpath'] |
1341 | + self.assertEqual(1, len(changes), changes) |
1342 | + self.assertEqual((None, 'subpath/afile'), changes[0].path) |
1343 | + ws.commit(message='Commit message') |
1344 | + self.assertEqual(list(ws.iter_changes()), []) |
1345 | + |
1346 | + def test_dirty(self): |
1347 | + tree = self.make_branch_and_tree('.', format=self._format) |
1348 | + self.build_tree(['subpath']) |
1349 | + self.assertRaises( |
1350 | + WorkspaceDirty, Workspace(tree, use_inotify=self._use_inotify).__enter__) |
1351 | + |
1352 | + def test_reset(self): |
1353 | + tree = self.make_branch_and_tree('.', format=self._format) |
1354 | + with Workspace(tree, use_inotify=self._use_inotify) as ws: |
1355 | + self.build_tree(['blah']) |
1356 | + ws.reset() |
1357 | + self.assertPathDoesNotExist('blah') |
1358 | + |
1359 | + def test_tree_path(self): |
1360 | + tree = self.make_branch_and_tree('.', format=self._format) |
1361 | + tree.mkdir('subdir') |
1362 | + tree.commit('Add subdir') |
1363 | + with Workspace(tree, use_inotify=self._use_inotify) as ws: |
1364 | + self.assertEqual('foo', ws.tree_path('foo')) |
1365 | + self.assertEqual('', ws.tree_path()) |
1366 | + with Workspace(tree, subpath='subdir', use_inotify=self._use_inotify) as ws: |
1367 | + self.assertEqual('subdir/foo', ws.tree_path('foo')) |
1368 | + self.assertEqual('subdir/', ws.tree_path()) |
1369 | + |
1370 | + def test_abspath(self): |
1371 | + tree = self.make_branch_and_tree('.', format=self._format) |
1372 | + tree.mkdir('subdir') |
1373 | + tree.commit('Add subdir') |
1374 | + with Workspace(tree, use_inotify=self._use_inotify) as ws: |
1375 | + self.assertEqual(tree.abspath('foo'), ws.abspath('foo')) |
1376 | + self.assertEqual(tree.abspath(''), ws.abspath()) |
1377 | + with Workspace(tree, subpath='subdir', use_inotify=self._use_inotify) as ws: |
1378 | + self.assertEqual(tree.abspath('subdir/foo'), ws.abspath('foo')) |
1379 | + self.assertEqual(tree.abspath('subdir') + '/', ws.abspath('')) |
1380 | + self.assertEqual(tree.abspath('subdir') + '/', ws.abspath()) |
1381 | + |
1382 | + def test_open_containing(self): |
1383 | + tree = self.make_branch_and_tree('.', format=self._format) |
1384 | + tree.mkdir('subdir') |
1385 | + tree.commit('Add subdir') |
1386 | + ws = Workspace.from_path('subdir') |
1387 | + self.assertEqual(ws.tree.abspath('.'), tree.abspath('.')) |
1388 | + self.assertEqual(ws.subpath, 'subdir') |
1389 | + self.assertEqual(None, ws.use_inotify) |
1390 | |
1391 | === added file 'breezy/workspace.py' |
1392 | --- breezy/workspace.py 1970-01-01 00:00:00 +0000 |
1393 | +++ breezy/workspace.py 2020-07-28 02:11:42 +0000 |
1394 | @@ -0,0 +1,217 @@ |
1395 | +# Copyright (C) 2018-2020 Jelmer Vernooij |
1396 | +# |
1397 | +# This program is free software; you can redistribute it and/or modify |
1398 | +# it under the terms of the GNU General Public License as published by |
1399 | +# the Free Software Foundation; either version 2 of the License, or |
1400 | +# (at your option) any later version. |
1401 | +# |
1402 | +# This program is distributed in the hope that it will be useful, |
1403 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1404 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1405 | +# GNU General Public License for more details. |
1406 | +# |
1407 | +# You should have received a copy of the GNU General Public License |
1408 | +# along with this program; if not, write to the Free Software |
1409 | +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
1410 | + |
1411 | +"""Convenience functions for efficiently making changes to a working tree. |
1412 | + |
1413 | +If possible, uses inotify to track changes in the tree - providing |
1414 | +high performance in large trees with a small number of changes. |
1415 | +""" |
1416 | + |
1417 | +from __future__ import absolute_import |
1418 | + |
1419 | +import errno |
1420 | +import os |
1421 | +import shutil |
1422 | + |
1423 | + |
1424 | +from .clean_tree import iter_deletables |
1425 | +from .errors import BzrError, DependencyNotPresent |
1426 | +from .trace import warning |
1427 | +from .transform import revert |
1428 | +from .workingtree import WorkingTree |
1429 | + |
1430 | + |
1431 | +class WorkspaceDirty(BzrError): |
1432 | + _fmt = "The directory %(path)s has pending changes." |
1433 | + |
1434 | + def __init__(self, tree): |
1435 | + BzrError(self, path=tree.abspath('.')) |
1436 | + |
1437 | + |
1438 | +# TODO(jelmer): Move to .clean_tree? |
1439 | +def reset_tree(local_tree, subpath=''): |
1440 | + """Reset a tree back to its basis tree. |
1441 | + |
1442 | + This will leave ignored and detritus files alone. |
1443 | + |
1444 | + Args: |
1445 | + local_tree: tree to work on |
1446 | + subpath: Subpath to operate on |
1447 | + """ |
1448 | + revert(local_tree, local_tree.branch.basis_tree(), |
1449 | + [subpath] if subpath not in ('.', '') else None) |
1450 | + deletables = list(iter_deletables( |
1451 | + local_tree, unknown=True, ignored=False, detritus=False)) |
1452 | + delete_items(deletables) |
1453 | + |
1454 | + |
1455 | +# TODO(jelmer): Move to .clean_tree? |
1456 | +def check_clean_tree(local_tree): |
1457 | + """Check that a tree is clean and has no pending changes or unknown files. |
1458 | + |
1459 | + Args: |
1460 | + local_tree: The tree to check |
1461 | + Raises: |
1462 | + PendingChanges: When there are pending changes |
1463 | + """ |
1464 | + # Just check there are no changes to begin with |
1465 | + if local_tree.has_changes(): |
1466 | + raise WorkspaceDirty(local_tree) |
1467 | + if list(local_tree.unknowns()): |
1468 | + raise WorkspaceDirty(local_tree) |
1469 | + |
1470 | + |
1471 | +def delete_items(deletables, dry_run: bool = False): |
1472 | + """Delete files in the deletables iterable""" |
1473 | + def onerror(function, path, excinfo): |
1474 | + """Show warning for errors seen by rmtree. |
1475 | + """ |
1476 | + # Handle only permission error while removing files. |
1477 | + # Other errors are re-raised. |
1478 | + if function is not os.remove or excinfo[1].errno != errno.EACCES: |
1479 | + raise |
1480 | + warnings.warn('unable to remove %s' % path) |
1481 | + for path, subp in deletables: |
1482 | + if os.path.isdir(path): |
1483 | + shutil.rmtree(path, onerror=onerror) |
1484 | + else: |
1485 | + try: |
1486 | + os.unlink(path) |
1487 | + except OSError as e: |
1488 | + # We handle only permission error here |
1489 | + if e.errno != errno.EACCES: |
1490 | + raise e |
1491 | + warning('unable to remove "%s": %s.', path, e.strerror) |
1492 | + |
1493 | + |
1494 | +def get_dirty_tracker(local_tree, subpath='', use_inotify=None): |
1495 | + """Create a dirty tracker object.""" |
1496 | + if use_inotify is True: |
1497 | + from .dirty_tracker import DirtyTracker |
1498 | + return DirtyTracker(local_tree, subpath) |
1499 | + elif use_inotify is False: |
1500 | + return None |
1501 | + else: |
1502 | + try: |
1503 | + from .dirty_tracker import DirtyTracker |
1504 | + except DependencyNotPresent: |
1505 | + return None |
1506 | + else: |
1507 | + return DirtyTracker(local_tree, subpath) |
1508 | + |
1509 | + |
1510 | +class Workspace(object): |
1511 | + """Create a workspace. |
1512 | + |
1513 | + :param tree: Tree to work in |
1514 | + :param subpath: path under which to consider and commit changes |
1515 | + :param use_inotify: whether to use inotify (default: yes, if available) |
1516 | + """ |
1517 | + |
1518 | + def __init__(self, tree, subpath='', use_inotify=None): |
1519 | + self.tree = tree |
1520 | + self.subpath = subpath |
1521 | + self.use_inotify = use_inotify |
1522 | + self._dirty_tracker = None |
1523 | + |
1524 | + @classmethod |
1525 | + def from_path(cls, path, use_inotify=None): |
1526 | + tree, subpath = WorkingTree.open_containing(path) |
1527 | + return cls(tree, subpath, use_inotify=use_inotify) |
1528 | + |
1529 | + def __enter__(self): |
1530 | + check_clean_tree(self.tree) |
1531 | + self._dirty_tracker = get_dirty_tracker( |
1532 | + self.tree, subpath=self.subpath, use_inotify=self.use_inotify) |
1533 | + return self |
1534 | + |
1535 | + def __exit__(self, exc_type, exc_val, exc_tb): |
1536 | + if self._dirty_tracker: |
1537 | + del self._dirty_tracker |
1538 | + self._dirty_tracker = None |
1539 | + return False |
1540 | + |
1541 | + def tree_path(self, path=''): |
1542 | + """Return a path relative to the tree subpath used by this workspace. |
1543 | + """ |
1544 | + return os.path.join(self.subpath, path) |
1545 | + |
1546 | + def abspath(self, path=''): |
1547 | + """Return an absolute path for the tree.""" |
1548 | + return self.tree.abspath(self.tree_path(path)) |
1549 | + |
1550 | + def reset(self): |
1551 | + """Reset - revert local changes, revive deleted files, remove added. |
1552 | + """ |
1553 | + if self._dirty_tracker and not self._dirty_tracker.is_dirty(): |
1554 | + return |
1555 | + reset_tree(self.tree, self.subpath) |
1556 | + if self._dirty_tracker is not None: |
1557 | + self._dirty_tracker.mark_clean() |
1558 | + |
1559 | + def _stage(self): |
1560 | + if self._dirty_tracker: |
1561 | + relpaths = self._dirty_tracker.relpaths() |
1562 | + # Sort paths so that directories get added before the files they |
1563 | + # contain (on VCSes where it matters) |
1564 | + self.tree.add( |
1565 | + [p for p in sorted(relpaths) |
1566 | + if self.tree.has_filename(p) and not |
1567 | + self.tree.is_ignored(p)]) |
1568 | + return [ |
1569 | + p for p in relpaths |
1570 | + if self.tree.is_versioned(p)] |
1571 | + else: |
1572 | + self.tree.smart_add([self.tree.abspath(self.subpath)]) |
1573 | + return [self.subpath] if self.subpath else None |
1574 | + |
1575 | + def iter_changes(self): |
1576 | + with self.tree.lock_write(): |
1577 | + specific_files = self._stage() |
1578 | + basis_tree = self.tree.basis_tree() |
1579 | + # TODO(jelmer): After Python 3.3, use 'yield from' |
1580 | + for change in self.tree.iter_changes( |
1581 | + basis_tree, specific_files=specific_files, |
1582 | + want_unversioned=False, require_versioned=True): |
1583 | + if change.kind[1] is None and change.versioned[1]: |
1584 | + if change.path[0] is None: |
1585 | + continue |
1586 | + # "missing" path |
1587 | + change = change.discard_new() |
1588 | + yield change |
1589 | + |
1590 | + def commit(self, **kwargs): |
1591 | + """Create a commit. |
1592 | + |
1593 | + See WorkingTree.commit() for documentation. |
1594 | + """ |
1595 | + if 'specific_files' in kwargs: |
1596 | + raise NotImplementedError(self.commit) |
1597 | + |
1598 | + with self.tree.lock_write(): |
1599 | + specific_files = self._stage() |
1600 | + |
1601 | + if self.tree.supports_setting_file_ids(): |
1602 | + from .rename_map import RenameMap |
1603 | + basis_tree = self.tree.basis_tree() |
1604 | + RenameMap.guess_renames( |
1605 | + basis_tree, self.tree, dry_run=False) |
1606 | + |
1607 | + kwargs['specific_files'] = specific_files |
1608 | + revid = self.tree.commit(**kwargs) |
1609 | + if self._dirty_tracker: |
1610 | + self._dirty_tracker.mark_clean() |
1611 | + return revid |
1612 | |
1613 | === modified file 'byov.conf' |
1614 | --- byov.conf 2020-06-01 19:35:12 +0000 |
1615 | +++ byov.conf 2020-07-28 02:11:42 +0000 |
1616 | @@ -23,7 +23,7 @@ |
1617 | |
1618 | # FIXME: Arguably this should be vm.build_deps=brz but it requires either an |
1619 | # available package or at least a debian/ dir ? -- vila 2018-02-23 |
1620 | -brz.build_deps = gcc, debhelper, python3, python3-all-dev, python3-configobj, python3-docutils, python3-paramiko, python3-subunit, python3-testtools, subunit, python3-pip, python3-setuptools, python3-flake8, python3-sphinx, python3-launchpadlib |
1621 | +brz.build_deps = gcc, debhelper, python3, python3-all-dev, python3-configobj, python3-docutils, python3-paramiko, python3-subunit, python3-testtools, subunit, python3-pip, python3-setuptools, python3-flake8, python3-sphinx, python3-launchpadlib, python3-pyinotify |
1622 | subunit.build_deps = python3-testscenarios, python3-testtools, cython, cython3, quilt |
1623 | vm.packages = {brz.build_deps}, {subunit.build_deps}, bzr, git, python-junitxml |
1624 | [brz-xenial] |
1625 | |
1626 | === modified file 'doc/en/conf.py' |
1627 | --- doc/en/conf.py 2018-11-21 21:34:30 +0000 |
1628 | +++ doc/en/conf.py 2020-07-28 02:11:42 +0000 |
1629 | @@ -75,8 +75,8 @@ |
1630 | ('tutorials/centralized_workflow', |
1631 | 'brz-%s-tutorial-centralized' % (brz_locale,), |
1632 | brz_title(u'Centralized Workflow Tutorial'), brz_team, 'howto'), |
1633 | - ('whats-new/whats-new-in-3.0', 'brz-%s-whats-new' % (brz_locale,), |
1634 | - brz_title(u"What's New in Breezy 3.0?"), brz_team, 'howto'), |
1635 | + ('whats-new/whats-new-in-3.1', 'brz-%s-whats-new' % (brz_locale,), |
1636 | + brz_title(u"What's New in Breezy 3.1?"), brz_team, 'howto'), |
1637 | ] |
1638 | |
1639 | latex_documents = [ |
1640 | |
1641 | === modified file 'doc/en/release-notes/brz-3.1.txt' |
1642 | --- doc/en/release-notes/brz-3.1.txt 2020-07-18 19:28:51 +0000 |
1643 | +++ doc/en/release-notes/brz-3.1.txt 2020-07-28 02:11:42 +0000 |
1644 | @@ -46,6 +46,14 @@ |
1645 | * ``BzrDir.sprout`` now correctly handles the ``revision_id`` |
1646 | argument when ``source_branch`` is None. (Jelmer Vernooij) |
1647 | |
1648 | + * Warn when loading a plugin that is broken, but support |
1649 | + ``suppress_warnings=load_plugin_failure`` to suppress it. |
1650 | + (Jelmer Vernooij, #1882528) |
1651 | + |
1652 | + * Add a basic ``svn:`` revision spec. Currently this doesn't work, |
1653 | + but it prevents the DWIM revision specifier from treating "svn:" |
1654 | + as a URL. (Jelmer Vernooij) |
1655 | + |
1656 | Bug Fixes |
1657 | ********* |
1658 | |
1659 | @@ -85,6 +93,12 @@ |
1660 | is currently implemented for GitHub, GitLab and Launchpad. |
1661 | (Jelmer Vernooij) |
1662 | |
1663 | + * Add a ``MemoryBranch`` implementation. (Jelmer Vernooij) |
1664 | + |
1665 | + * A new ``Workspace`` interface is now available for efficiently |
1666 | + making changes to large working trees from automation. |
1667 | + (Jelmer Vernooij) |
1668 | + |
1669 | Testing |
1670 | ******* |
1671 | |
1672 | |
1673 | === modified file 'setup.py' |
1674 | --- setup.py 2020-06-23 01:02:30 +0000 |
1675 | +++ setup.py 2020-07-28 02:11:42 +0000 |
1676 | @@ -74,6 +74,7 @@ |
1677 | 'fastimport': [], |
1678 | 'git': [], |
1679 | 'launchpad': ['launchpadlib>=1.6.3'], |
1680 | + 'workspace': ['pyinotify'], |
1681 | }, |
1682 | 'tests_require': [ |
1683 | 'testtools', |