Merge lp:~jelmer/brz/bundle-upload into lp:brz

Proposed by Jelmer Vernooij
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/bundle-upload
Merge into: lp:brz
Diff against target: 2005 lines (+1954/-0)
9 files modified
breezy/plugins/upload/.bzrignore (+3/-0)
breezy/plugins/upload/NEWS (+157/-0)
breezy/plugins/upload/README (+21/-0)
breezy/plugins/upload/__init__.py (+238/-0)
breezy/plugins/upload/cmds.py (+560/-0)
breezy/plugins/upload/tests/__init__.py (+28/-0)
breezy/plugins/upload/tests/test_auto_upload_hook.py (+87/-0)
breezy/plugins/upload/tests/test_upload.py (+857/-0)
doc/en/release-notes/brz-3.0.txt (+3/-0)
To merge this branch: bzr merge lp:~jelmer/brz/bundle-upload
Reviewer Review Type Date Requested Status
Martin Packman Approve
Review via email: mp+324986@code.launchpad.net

Commit message

Bundle the 'upload' plugin.

Description of the change

Bundle the 'upload' plugin.

To post a comment you must log in.
Revision history for this message
Martin Packman (gz) wrote :

Looks good.

review: Approve
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :

Running landing tests failed
http://10.242.247.184:8080/job/brz-dev/36/

Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :

Running landing tests failed
http://10.242.247.184:8080/job/brz-dev/43/

Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :

Running landing tests failed
http://10.242.247.184:8080/job/brz-dev/45/

Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :

Running landing tests failed
http://10.242.247.184:8080/job/brz-dev/47/

Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :

Running landing tests failed
http://10.242.247.184:8080/job/brz-dev/48/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'breezy/plugins/upload'
2=== added file 'breezy/plugins/upload/.bzrignore'
3--- breezy/plugins/upload/.bzrignore 1970-01-01 00:00:00 +0000
4+++ breezy/plugins/upload/.bzrignore 2017-06-02 20:49:44 +0000
5@@ -0,0 +1,3 @@
6+./build
7+./dist
8+./MANIFEST
9
10=== added file 'breezy/plugins/upload/NEWS'
11--- breezy/plugins/upload/NEWS 1970-01-01 00:00:00 +0000
12+++ breezy/plugins/upload/NEWS 2017-06-02 20:49:44 +0000
13@@ -0,0 +1,157 @@
14+########################
15+bzr-upload Release Notes
16+########################
17+
18+.. contents:: List of Releases
19+ :depth: 1
20+
21+bzr-upload 1.2.0
22+################
23+
24+:1.2.0: NOT RELEASED YET
25+
26+New Features
27+************
28+
29+.. New commands, options, etc that users may wish to try out.
30+
31+Improvements
32+************
33+
34+* Use bzr config stacks, bzr >= 2.5 is now required. (Vincent Ladeuil)
35+
36+Bug Fixes
37+*********
38+
39+.. Fixes for situations where bzr would previously crash or give incorrect
40+ or undesirable results.
41+
42+Documentation
43+*************
44+
45+.. Improved or updated documentation.
46+
47+Testing
48+*******
49+
50+.. Fixes and changes that are only relevant to bzr's test framework and
51+ suite. This can include new facilities for writing tests, fixes to
52+ spurious test failures and changes to the way things should be tested.
53+
54+
55+bzr-upload 1.1.0
56+################
57+
58+:1.1.0: 2012-03-15
59+
60+New Features
61+************
62+
63+.. New commands, options, etc that users may wish to try out.
64+
65+Improvements
66+************
67+
68+.. Improvements to existing commands, especially improved performance
69+ or memory usage, or better results.
70+
71+Bug Fixes
72+*********
73+
74+.. Fixes for situations where bzr would previously crash or give incorrect
75+ or undesirable results.
76+
77+Documentation
78+*************
79+
80+.. Improved or updated documentation.
81+
82+Testing
83+*******
84+
85+* Now requires bzr >= 2.5 for testing. The plugin itself should still work
86+ with previous versions. (Vincent Ladeuil)
87+
88+* Avoid deprecation warning with bzr-2.5 by using tree.iter_entries_by_dir
89+ avoiding direct inventory access, this should still be compatible with
90+ older bzr versions. (Vincent Ladeuil)
91+
92+bzr-upload 1.0.1
93+################
94+
95+:1.0.1: 2012-03-15
96+
97+Bug Fixes
98+*********
99+
100+* Fix a typo to avoid crashing when encountering symlinks during
101+ a full upload. (Jonathan Paugh)
102+
103+Testing
104+*******
105+
106+* Use assertPathDoesNotExist and assertPathExist instead of failIfExists and
107+ failUnlessExists in the test suite. This requires bzr-2.4 to run the tests
108+ but doesn't affect the plugin compatibility itself with previous verions
109+ of bzr. (Vincent Ladeuil)
110+
111+
112+bzr-upload 1.0.0
113+################
114+
115+:1.0.0: 2010-12-10
116+
117+New Features
118+************
119+
120+* ``.bzrignore-upload`` can be used to avoid uploading some files or
121+ directories. It uses the same syntax as ``.bzrignore`` including regular
122+ expressions.
123+ (Martin Albisetti, Vincent Ladeuil, #499525, #499941)
124+
125+* Remote branches can be used to upload from.
126+ (Gary van der Merwe, Vincent Ladeuil)
127+
128+* The auto hook verbosity is now controlled by the 'upload_auto_quiet'
129+ config variable. If defaults to False if not set.
130+ (Vincent Ladeuil, #312686)
131+
132+* The file where the revision id is stored on the remote server is now
133+ controlled by the 'upload_revid_location' configuration variable. It
134+ defaults to '.bzr-upload.revid'.
135+ (Vincent Ladeuil, #423331)
136+
137+* Upload now checks that the revision we are uploading is a descendent
138+ from the revision that was uploaded, and hence that the branchs that
139+ they were uploaded from have not diverged. This can be ignored by passing
140+ the --overwrite option. (Gary van der Merwe)
141+
142+
143+Bug Fixes
144+*********
145+
146+* Fix auto hook trying to display an url without using the right encoding.
147+ (Vincent Ladeuil, #312686)
148+
149+* Fix compatibility with bzr versions that don't provide
150+ get_user_option_as_bool().
151+ (Vincent Ladeuil, #423791)
152+
153+* Emit warnings instead of backtrace when symlinks are encountered.
154+ (Vincent Ladeuil, #477224)
155+
156+Documentation
157+*************
158+
159+* Clarify 'changes' definition in online help.
160+ (Vincent Ladeuil, #275538)
161+
162+* Move the README file into the module doc string so that it
163+ becomes available through the 'bzr help plugins/upload'
164+ command. (Vincent Ladeuil, #424193)
165+
166+Testing
167+*******
168+
169+* Make tests requiring a unicode file system skip where applicable.
170+ (Vincent Ladeuil, #671964)
171
172=== added file 'breezy/plugins/upload/README'
173--- breezy/plugins/upload/README 1970-01-01 00:00:00 +0000
174+++ breezy/plugins/upload/README 2017-06-02 20:49:44 +0000
175@@ -0,0 +1,21 @@
176+Bazaar Upload
177+=============
178+
179+Overview
180+--------
181+
182+bzr-upload is a plugin for Bazaar which lets you upload your working tree to
183+a remote location using ftp/sftp.
184+
185+The main target audience is web developers who keep their web pages version
186+controlled with bzr.
187+
188+Please report any bugs to: http://bugs.launchpad.net/bzr-upload/
189+
190+Our home page is located at: https://launchpad.net/bzr-upload/
191+The original authors are:
192+
193+Vincent Ladeuil <v.ladeuil+lp@free.fr>
194+Martin Albisetti <argentina@gmail.com>
195+
196+Use bzr help plugins/upload to get more info.
197
198=== added file 'breezy/plugins/upload/__init__.py'
199--- breezy/plugins/upload/__init__.py 1970-01-01 00:00:00 +0000
200+++ breezy/plugins/upload/__init__.py 2017-06-02 20:49:44 +0000
201@@ -0,0 +1,238 @@
202+# Copyright (C) 2008-2012 Canonical Ltd
203+#
204+# This program is free software; you can redistribute it and/or modify
205+# it under the terms of the GNU General Public License as published by
206+# the Free Software Foundation; either version 2 of the License, or
207+# (at your option) any later version.
208+#
209+# This program is distributed in the hope that it will be useful,
210+# but WITHOUT ANY WARRANTY; without even the implied warranty of
211+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
212+# GNU General Public License for more details.
213+#
214+# You should have received a copy of the GNU General Public License
215+# along with this program; if not, write to the Free Software
216+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
217+
218+"""Upload a working tree, incrementally.
219+
220+Quickstart
221+----------
222+
223+To get started, it's as simple as running::
224+
225+ brz upload sftp://user@host/location/on/webserver
226+
227+This will initially upload the whole working tree, and leave a file on the
228+remote location indicating the last revision that was uploaded
229+(.bzr-upload.revid), in order to avoid uploading unnecessary information the
230+next time.
231+
232+If you would like to upload a specific revision, you just do:
233+
234+ brz upload -r X sftp://user@host/location/on/webserver
235+
236+bzr-upload, just as brz does, will remember the location where you upload the
237+first time, so you don't need to specify it every time.
238+
239+If you need to re-upload the whole working tree for some reason, you can:
240+
241+ brz upload --full sftp://user@host/location/on/webserver
242+
243+This command only works on the revision beening uploaded is a decendent of the
244+revision that was previously uploaded, and that they are hence from branches
245+that have not diverged. Branches are considered diverged if the destination
246+branch's most recent commit is one that has not been merged (directly or
247+indirectly) by the source branch.
248+
249+If branches have diverged, you can use 'brz upload --overwrite' to replace
250+the other branch completely, discarding its unmerged changes.
251+
252+
253+Automatically Uploading
254+-----------------------
255+
256+bzr-upload comes with a hook that can be used to trigger an upload whenever
257+the tip of the branch changes, including on commit, push, uncommit etc. This
258+would allow you to keep the code on the target up to date automatically.
259+
260+The easiest way to enable this is to run upload with the --auto option.
261+
262+ brz upload --auto
263+
264+will enable the hook for this branch. If you were to do a commit in this branch
265+now you would see it trigger the upload automatically.
266+
267+If you wish to disable this for a branch again then you can use the --no-auto
268+option.
269+
270+ brz upload --no-auto
271+
272+will disable the feature for that branch.
273+
274+Since the auto hook is triggered automatically, you can't use the --quiet
275+option available for the upload command. Instead, you can set the
276+'upload_auto_quiet' configuration variable to True or False in either
277+bazaar.conf, locations.conf or branch.conf.
278+
279+
280+Storing the '.bzr-upload.revid' file
281+------------------------------------
282+
283+The only bzr-related info uploaded with the working tree is the corresponding
284+revision id. The uploaded working tree is not linked to any other brz data.
285+
286+If the layout of your remote server is such that you can't write in the
287+root directory but only in the directories inside that root, you will need
288+to use the 'upload_revid_location' configuration variable to specify the
289+relative path to be used. That configuration variable can be specified in
290+locations.conf or branch.conf.
291+
292+For example, given the following layout:
293+
294+ Project/
295+ private/
296+ public/
297+
298+you may have write access in 'private' and 'public' but in 'Project'
299+itself. In that case, you can add the following in your locations.conf or
300+branch.conf file:
301+
302+ upload_revid_location = private/.bzr-upload.revid
303+
304+
305+Upload from Remote Location
306+---------------------------
307+
308+It is possible to upload to a remote location from another remote location by
309+specifying it with the --directory option:
310+
311+ brz upload ftp://public.example.com --directory sftp://private.example.com
312+
313+This, together with --auto, can be used to upload when you push to your
314+central branch, rather than when you commit to your local branch.
315+
316+Note that you will consume more bandwith this way than uploading from a local
317+branch.
318+
319+Ignoring certain files
320+-----------------------
321+
322+If you want to version a file, but not upload it, you can create a file called
323+.bzrignore-upload, which works in the same way as the regular .bzrignore file,
324+but only applies to bzr-upload.
325+
326+
327+Collaborating
328+-------------
329+
330+While we don't have any platform setup, you can branch from trunk:
331+
332+ brz branch lp:bzr-upload
333+
334+And change anything you'd like, and file a merge proposal on Launchpad.
335+
336+
337+Known Issues
338+------------
339+
340+ * Symlinks are not supported (warnings are emitted when they are encountered).
341+
342+
343+"""
344+
345+from __future__ import absolute_import
346+
347+# TODO: the chmod bits *can* be supported via the upload protocols
348+# (i.e. poorly), but since the web developers use these protocols to upload
349+# manually, it is expected that the associated web server is coherent with
350+# their presence/absence. In other words, if a web hosting provider requires
351+# chmod bits but don't provide an ftp server that support them, well, better
352+# find another provider ;-)
353+
354+# TODO: The message emitted in verbose mode displays local paths. That may be
355+# scary for the user when we say 'Deleting <path>' and are referring to
356+# remote files...
357+
358+
359+import breezy
360+from ... import (
361+ commands,
362+ config,
363+ hooks,
364+ )
365+
366+from ...hooks import install_lazy_named_hook
367+
368+from ... import version_info
369+
370+
371+def register_option(key, member):
372+ """Lazily register an option."""
373+ config.option_registry.register_lazy(
374+ key, 'breezy.plugins.upload.cmds', member)
375+
376+
377+register_option('upload_auto', 'auto_option')
378+register_option('upload_auto_quiet', 'auto_quiet_option')
379+register_option('upload_location', 'location_option')
380+register_option('upload_revid_location', 'revid_location_option')
381+
382+
383+commands.plugin_cmds.register_lazy(
384+ 'cmd_upload', [], 'breezy.plugins.upload.cmds')
385+
386+
387+def auto_upload_hook(params):
388+ from ... import (
389+ osutils,
390+ trace,
391+ transport,
392+ urlutils,
393+ )
394+ from .cmds import (
395+ BzrUploader,
396+ )
397+ import sys
398+ source_branch = params.branch
399+ conf = source_branch.get_config_stack()
400+ destination = conf.get('upload_location')
401+ if destination is None:
402+ return
403+ auto_upload = conf.get('upload_auto')
404+ if not auto_upload:
405+ return
406+ quiet = conf.get('upload_auto_quiet')
407+ if not quiet:
408+ display_url = urlutils.unescape_for_display(
409+ destination, osutils.get_terminal_encoding())
410+ trace.note('Automatically uploading to %s', display_url)
411+ to_transport = transport.get_transport(destination)
412+ last_revision = source_branch.last_revision()
413+ last_tree = source_branch.repository.revision_tree(last_revision)
414+ uploader = BzrUploader(source_branch, to_transport, sys.stdout,
415+ last_tree, last_revision, quiet=quiet)
416+ uploader.upload_tree()
417+
418+
419+def install_auto_upload_hook():
420+ hooks.install_lazy_named_hook(
421+ 'breezy.branch', 'Branch.hooks',
422+ 'post_change_branch_tip', auto_upload_hook,
423+ 'Auto upload code from a branch when it is changed.')
424+
425+
426+install_auto_upload_hook()
427+
428+
429+def load_tests(loader, basic_tests, pattern):
430+ # This module shouldn't define any tests but I don't know how to report
431+ # that. I prefer to update basic_tests with the other tests to detect
432+ # unwanted tests and I think that's sufficient.
433+
434+ testmod_names = [
435+ 'tests',
436+ ]
437+ basic_tests.addTest(loader.loadTestsFromModuleNames(
438+ ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
439+ return basic_tests
440
441=== added file 'breezy/plugins/upload/cmds.py'
442--- breezy/plugins/upload/cmds.py 1970-01-01 00:00:00 +0000
443+++ breezy/plugins/upload/cmds.py 2017-06-02 20:49:44 +0000
444@@ -0,0 +1,560 @@
445+# Copyright (C) 2011, 2012 Canonical Ltd
446+#
447+# This program is free software; you can redistribute it and/or modify
448+# it under the terms of the GNU General Public License as published by
449+# the Free Software Foundation; either version 2 of the License, or
450+# (at your option) any later version.
451+#
452+# This program is distributed in the hope that it will be useful,
453+# but WITHOUT ANY WARRANTY; without even the implied warranty of
454+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
455+# GNU General Public License for more details.
456+#
457+# You should have received a copy of the GNU General Public License
458+# along with this program; if not, write to the Free Software
459+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
460+
461+"""bzr-upload command implementations."""
462+
463+from __future__ import absolute_import
464+
465+from ... import (
466+ branch,
467+ commands,
468+ config,
469+ lazy_import,
470+ option,
471+ )
472+lazy_import.lazy_import(globals(), """
473+import stat
474+import sys
475+
476+from breezy import (
477+ bzrdir,
478+ errors,
479+ globbing,
480+ ignores,
481+ osutils,
482+ revision,
483+ revisionspec,
484+ trace,
485+ transport,
486+ urlutils,
487+ workingtree,
488+ )
489+""")
490+
491+auto_option = config.Option(
492+ 'upload_auto', default=False, from_unicode=config.bool_from_store,
493+ help="""\
494+Whether upload should occur when the tip of the branch changes.
495+""")
496+auto_quiet_option = config.Option(
497+ 'upload_auto_quiet', default=False, from_unicode=config.bool_from_store,
498+ help="""\
499+Whether upload should occur quietly.
500+""")
501+location_option = config.Option(
502+ 'upload_location', default=None,
503+ help="""\
504+The url to upload the working tree to.
505+""")
506+revid_location_option = config.Option(
507+ 'upload_revid_location', default=u'.bzr-upload.revid',
508+ help="""\
509+The relative path to be used to store the uploaded revid.
510+
511+The only bzr-related info uploaded with the working tree is the corresponding
512+revision id. The uploaded working tree is not linked to any other bzr data.
513+
514+If the layout of your remote server is such that you can't write in the
515+root directory but only in the directories inside that root, you will need
516+to use the 'upload_revid_location' configuration variable to specify the
517+relative path to be used. That configuration variable can be specified in
518+locations.conf or branch.conf.
519+
520+For example, given the following layout:
521+
522+ Project/
523+ private/
524+ public/
525+
526+you may have write access in 'private' and 'public' but in 'Project'
527+itself. In that case, you can add the following in your locations.conf or
528+branch.conf file:
529+
530+ upload_revid_location = private/.bzr-upload.revid
531+""")
532+
533+
534+# FIXME: Add more tests around invalid paths or relative paths that doesn't
535+# exist on remote (if only to get proper error messages) for
536+# 'upload_revid_location'
537+
538+
539+class BzrUploader(object):
540+
541+ def __init__(self, branch, to_transport, outf, tree, rev_id,
542+ quiet=False):
543+ self.branch = branch
544+ self.to_transport = to_transport
545+ self.outf = outf
546+ self.tree = tree
547+ self.rev_id = rev_id
548+ self.quiet = quiet
549+ self._pending_deletions = []
550+ self._pending_renames = []
551+ self._uploaded_revid = None
552+ self._ignored = None
553+
554+ def _up_stat(self, relpath):
555+ return self.to_transport.stat(urlutils.escape(relpath))
556+
557+ def _up_rename(self, old_path, new_path):
558+ return self.to_transport.rename(urlutils.escape(old_path),
559+ urlutils.escape(new_path))
560+
561+ def _up_delete(self, relpath):
562+ return self.to_transport.delete(urlutils.escape(relpath))
563+
564+ def _up_delete_tree(self, relpath):
565+ return self.to_transport.delete_tree(urlutils.escape(relpath))
566+
567+ def _up_mkdir(self, relpath, mode):
568+ return self.to_transport.mkdir(urlutils.escape(relpath), mode)
569+
570+ def _up_rmdir(self, relpath):
571+ return self.to_transport.rmdir(urlutils.escape(relpath))
572+
573+ def _up_put_bytes(self, relpath, bytes, mode):
574+ self.to_transport.put_bytes(urlutils.escape(relpath), bytes, mode)
575+
576+ def _up_get_bytes(self, relpath):
577+ return self.to_transport.get_bytes(urlutils.escape(relpath))
578+
579+ def set_uploaded_revid(self, rev_id):
580+ # XXX: Add tests for concurrent updates, etc.
581+ revid_path = self.branch.get_config_stack().get('upload_revid_location')
582+ self.to_transport.put_bytes(urlutils.escape(revid_path), rev_id)
583+ self._uploaded_revid = rev_id
584+
585+ def get_uploaded_revid(self):
586+ if self._uploaded_revid is None:
587+ revid_path = self.branch.get_config_stack(
588+ ).get('upload_revid_location')
589+ try:
590+ self._uploaded_revid = self._up_get_bytes(revid_path)
591+ except errors.NoSuchFile:
592+ # We have not uploaded to here.
593+ self._uploaded_revid = revision.NULL_REVISION
594+ return self._uploaded_revid
595+
596+ def _get_ignored(self):
597+ if self._ignored is None:
598+ try:
599+ ignore_file_path = '.bzrignore-upload'
600+ ignore_file_id = self.tree.path2id(ignore_file_path)
601+ ignore_file = self.tree.get_file(ignore_file_id,
602+ ignore_file_path)
603+ ignored_patterns = ignores.parse_ignore_file(ignore_file)
604+ except errors.NoSuchId:
605+ ignored_patterns = []
606+ self._ignored = globbing.Globster(ignored_patterns)
607+ return self._ignored
608+
609+ def is_ignored(self, relpath):
610+ glob = self._get_ignored()
611+ ignored = glob.match(relpath)
612+ import os
613+ if not ignored:
614+ # We still need to check that all parents are not ignored
615+ dir = os.path.dirname(relpath)
616+ while dir and not ignored:
617+ ignored = glob.match(dir)
618+ if not ignored:
619+ dir = os.path.dirname(dir)
620+ return ignored
621+
622+ def upload_file(self, relpath, id, mode=None):
623+ if mode is None:
624+ if self.tree.is_executable(id):
625+ mode = 0775
626+ else:
627+ mode = 0664
628+ if not self.quiet:
629+ self.outf.write('Uploading %s\n' % relpath)
630+ self._up_put_bytes(relpath, self.tree.get_file_text(id), mode)
631+
632+ def upload_file_robustly(self, relpath, id, mode=None):
633+ """Upload a file, clearing the way on the remote side.
634+
635+ When doing a full upload, it may happen that a directory exists where
636+ we want to put our file.
637+ """
638+ try:
639+ st = self._up_stat(relpath)
640+ if stat.S_ISDIR(st.st_mode):
641+ # A simple rmdir may not be enough
642+ if not self.quiet:
643+ self.outf.write('Clearing %s/%s\n' % (
644+ self.to_transport.external_url(), relpath))
645+ self._up_delete_tree(relpath)
646+ except errors.PathError:
647+ pass
648+ self.upload_file(relpath, id, mode)
649+
650+ def make_remote_dir(self, relpath, mode=None):
651+ if mode is None:
652+ mode = 0775
653+ self._up_mkdir(relpath, mode)
654+
655+ def make_remote_dir_robustly(self, relpath, mode=None):
656+ """Create a remote directory, clearing the way on the remote side.
657+
658+ When doing a full upload, it may happen that a file exists where we
659+ want to create our directory.
660+ """
661+ try:
662+ st = self._up_stat(relpath)
663+ if not stat.S_ISDIR(st.st_mode):
664+ if not self.quiet:
665+ self.outf.write('Deleting %s/%s\n' % (
666+ self.to_transport.external_url(), relpath))
667+ self._up_delete(relpath)
668+ else:
669+ # Ok the remote dir already exists, nothing to do
670+ return
671+ except errors.PathError:
672+ pass
673+ self.make_remote_dir(relpath, mode)
674+
675+ def delete_remote_file(self, relpath):
676+ if not self.quiet:
677+ self.outf.write('Deleting %s\n' % relpath)
678+ self._up_delete(relpath)
679+
680+ def delete_remote_dir(self, relpath):
681+ if not self.quiet:
682+ self.outf.write('Deleting %s\n' % relpath)
683+ self._up_rmdir(relpath)
684+ # XXX: Add a test where a subdir is ignored but we still want to
685+ # delete the dir -- vila 100106
686+
687+ def delete_remote_dir_maybe(self, relpath):
688+ """Try to delete relpath, keeping failures to retry later."""
689+ try:
690+ self._up_rmdir(relpath)
691+ # any kind of PathError would be OK, though we normally expect
692+ # DirectoryNotEmpty
693+ except errors.PathError:
694+ self._pending_deletions.append(relpath)
695+
696+ def finish_deletions(self):
697+ if self._pending_deletions:
698+ # Process the previously failed deletions in reverse order to
699+ # delete children before parents
700+ for relpath in reversed(self._pending_deletions):
701+ self._up_rmdir(relpath)
702+ # The following shouldn't be needed since we use it once per
703+ # upload, but better safe than sorry ;-)
704+ self._pending_deletions = []
705+
706+ def rename_remote(self, old_relpath, new_relpath):
707+ """Rename a remote file or directory taking care of collisions.
708+
709+ To avoid collisions during bulk renames, each renamed target is
710+ temporarily assigned a unique name. When all renames have been done,
711+ each target get its proper name.
712+ """
713+ # We generate a sufficiently random name to *assume* that
714+ # no collisions will occur and don't worry about it (nor
715+ # handle it).
716+ import os
717+ import random
718+ import time
719+
720+ stamp = '.tmp.%.9f.%d.%d' % (time.time(),
721+ os.getpid(),
722+ random.randint(0,0x7FFFFFFF))
723+ if not self.quiet:
724+ self.outf.write('Renaming %s to %s\n' % (old_relpath, new_relpath))
725+ self._up_rename(old_relpath, stamp)
726+ self._pending_renames.append((stamp, new_relpath))
727+
728+ def finish_renames(self):
729+ for (stamp, new_path) in self._pending_renames:
730+ self._up_rename(stamp, new_path)
731+ # The following shouldn't be needed since we use it once per upload,
732+ # but better safe than sorry ;-)
733+ self._pending_renames = []
734+
735+ def upload_full_tree(self):
736+ self.to_transport.ensure_base() # XXX: Handle errors (add
737+ # --create-prefix option ?)
738+ self.tree.lock_read()
739+ try:
740+ for relpath, ie in self.tree.iter_entries_by_dir():
741+ if relpath in ('', '.bzrignore', '.bzrignore-upload'):
742+ # skip root ('')
743+ # .bzrignore and .bzrignore-upload have no meaning outside
744+ # a working tree so do not upload them
745+ continue
746+ if self.is_ignored(relpath):
747+ if not self.quiet:
748+ self.outf.write('Ignoring %s\n' % relpath)
749+ continue
750+ if ie.kind == 'file':
751+ self.upload_file_robustly(relpath, ie.file_id)
752+ elif ie.kind == 'directory':
753+ self.make_remote_dir_robustly(relpath)
754+ elif ie.kind == 'symlink':
755+ if not self.quiet:
756+ target = self.tree.path_content_summary(relpath)[3]
757+ self.outf.write('Not uploading symlink %s -> %s\n'
758+ % (relpath, target))
759+ else:
760+ raise NotImplementedError
761+ self.set_uploaded_revid(self.rev_id)
762+ finally:
763+ self.tree.unlock()
764+
765+ def upload_tree(self):
766+ # If we can't find the revid file on the remote location, upload the
767+ # full tree instead
768+ rev_id = self.get_uploaded_revid()
769+
770+ if rev_id == revision.NULL_REVISION:
771+ if not self.quiet:
772+ self.outf.write('No uploaded revision id found,'
773+ ' switching to full upload\n')
774+ self.upload_full_tree()
775+ # We're done
776+ return
777+
778+ # Check if the revision hasn't already been uploaded
779+ if rev_id == self.rev_id:
780+ if not self.quiet:
781+ self.outf.write('Remote location already up to date\n')
782+
783+ from_tree = self.branch.repository.revision_tree(rev_id)
784+ self.to_transport.ensure_base() # XXX: Handle errors (add
785+ # --create-prefix option ?)
786+ changes = self.tree.changes_from(from_tree)
787+ self.tree.lock_read()
788+ try:
789+ for (path, id, kind) in changes.removed:
790+ if self.is_ignored(path):
791+ if not self.quiet:
792+ self.outf.write('Ignoring %s\n' % path)
793+ continue
794+ if kind is 'file':
795+ self.delete_remote_file(path)
796+ elif kind is 'directory':
797+ self.delete_remote_dir_maybe(path)
798+ elif kind == 'symlink':
799+ if not self.quiet:
800+ target = self.tree.path_content_summary(path)[3]
801+ self.outf.write('Not deleting remote symlink %s -> %s\n'
802+ % (path, target))
803+ else:
804+ raise NotImplementedError
805+
806+ for (old_path, new_path, id, kind,
807+ content_change, exec_change) in changes.renamed:
808+ if self.is_ignored(old_path) and self.is_ignored(new_path):
809+ if not self.quiet:
810+ self.outf.write('Ignoring %s\n' % old_path)
811+ self.outf.write('Ignoring %s\n' % new_path)
812+ continue
813+ if content_change:
814+ # We update the old_path content because renames and
815+ # deletions are differed.
816+ self.upload_file(old_path, id)
817+ if kind == 'symlink':
818+ if not self.quiet:
819+ self.outf.write('Not renaming remote symlink %s to %s\n'
820+ % (old_path, new_path))
821+ else:
822+ self.rename_remote(old_path, new_path)
823+ self.finish_renames()
824+ self.finish_deletions()
825+
826+ for (path, id, old_kind, new_kind) in changes.kind_changed:
827+ if self.is_ignored(path):
828+ if not self.quiet:
829+ self.outf.write('Ignoring %s\n' % path)
830+ continue
831+ if old_kind == 'file':
832+ self.delete_remote_file(path)
833+ elif old_kind == 'directory':
834+ self.delete_remote_dir(path)
835+ else:
836+ raise NotImplementedError
837+
838+ if new_kind == 'file':
839+ self.upload_file(path, id)
840+ elif new_kind is 'directory':
841+ self.make_remote_dir(path)
842+ else:
843+ raise NotImplementedError
844+
845+ for (path, id, kind) in changes.added:
846+ if self.is_ignored(path):
847+ if not self.quiet:
848+ self.outf.write('Ignoring %s\n' % path)
849+ continue
850+ if kind == 'file':
851+ self.upload_file(path, id)
852+ elif kind == 'directory':
853+ self.make_remote_dir(path)
854+ elif kind == 'symlink':
855+ if not self.quiet:
856+ target = self.tree.path_content_summary(path)[3]
857+ self.outf.write('Not uploading symlink %s -> %s\n'
858+ % (path, target))
859+ else:
860+ raise NotImplementedError
861+
862+ # XXX: Add a test for exec_change
863+ for (path, id, kind,
864+ content_change, exec_change) in changes.modified:
865+ if self.is_ignored(path):
866+ if not self.quiet:
867+ self.outf.write('Ignoring %s\n' % path)
868+ continue
869+ if kind is 'file':
870+ self.upload_file(path, id)
871+ else:
872+ raise NotImplementedError
873+
874+ self.set_uploaded_revid(self.rev_id)
875+ finally:
876+ self.tree.unlock()
877+
878+
879+class CannotUploadToWorkingTree(errors.BzrCommandError):
880+
881+ _fmt = 'Cannot upload to a bzr managed working tree: %(url)s".'
882+
883+
884+class DivergedUploadedTree(errors.BzrCommandError):
885+
886+ _fmt = ("Your branch (%(revid)s)"
887+ " and the uploaded tree (%(uploaded_revid)s) have diverged: ")
888+
889+
890+class cmd_upload(commands.Command):
891+ """Upload a working tree, as a whole or incrementally.
892+
893+ If no destination is specified use the last one used.
894+ If no revision is specified upload the changes since the last upload.
895+
896+ Changes include files added, renamed, modified or removed.
897+ """
898+ _see_also = ['plugins/upload']
899+ takes_args = ['location?']
900+ takes_options = [
901+ 'revision',
902+ 'remember',
903+ 'overwrite',
904+ option.Option('full', 'Upload the full working tree.'),
905+ option.Option('quiet', 'Do not output what is being done.',
906+ short_name='q'),
907+ option.Option('directory',
908+ help='Branch to upload from, '
909+ 'rather than the one containing the working directory.',
910+ short_name='d',
911+ type=unicode,
912+ ),
913+ option.Option('auto',
914+ 'Trigger an upload from this branch whenever the tip '
915+ 'revision changes.')
916+ ]
917+
918+ def run(self, location=None, full=False, revision=None, remember=None,
919+ directory=None, quiet=False, auto=None, overwrite=False
920+ ):
921+ if directory is None:
922+ directory = u'.'
923+
924+ (wt, branch,
925+ relpath) = bzrdir.BzrDir.open_containing_tree_or_branch(directory)
926+
927+ if wt:
928+ wt.lock_read()
929+ locked = wt
930+ else:
931+ branch.lock_read()
932+ locked = branch
933+ try:
934+ if wt:
935+ changes = wt.changes_from(wt.basis_tree())
936+
937+ if revision is None and changes.has_changed():
938+ raise errors.UncommittedChanges(wt)
939+
940+ conf = branch.get_config_stack()
941+ if location is None:
942+ stored_loc = conf.get('upload_location')
943+ if stored_loc is None:
944+ raise errors.BzrCommandError(
945+ 'No upload location known or specified.')
946+ else:
947+ # FIXME: Not currently tested
948+ display_url = urlutils.unescape_for_display(stored_loc,
949+ self.outf.encoding)
950+ self.outf.write("Using saved location: %s\n" % display_url)
951+ location = stored_loc
952+
953+ to_transport = transport.get_transport(location)
954+
955+ # Check that we are not uploading to a existing working tree.
956+ try:
957+ to_bzr_dir = bzrdir.BzrDir.open_from_transport(to_transport)
958+ has_wt = to_bzr_dir.has_workingtree()
959+ except errors.NotBranchError:
960+ has_wt = False
961+ except errors.NotLocalUrl:
962+ # The exception raised is a bit weird... but that's life.
963+ has_wt = True
964+
965+ if has_wt:
966+ raise CannotUploadToWorkingTree(url=location)
967+ if revision is None:
968+ rev_id = branch.last_revision()
969+ else:
970+ if len(revision) != 1:
971+ raise errors.BzrCommandError(
972+ 'bzr upload --revision takes exactly 1 argument')
973+ rev_id = revision[0].in_history(branch).rev_id
974+
975+ tree = branch.repository.revision_tree(rev_id)
976+
977+ uploader = BzrUploader(branch, to_transport, self.outf, tree,
978+ rev_id, quiet=quiet)
979+
980+ if not overwrite:
981+ prev_uploaded_rev_id = uploader.get_uploaded_revid()
982+ graph = branch.repository.get_graph()
983+ if not graph.is_ancestor(prev_uploaded_rev_id, rev_id):
984+ raise DivergedUploadedTree(
985+ revid=rev_id, uploaded_revid=prev_uploaded_rev_id)
986+ if full:
987+ uploader.upload_full_tree()
988+ else:
989+ uploader.upload_tree()
990+ finally:
991+ locked.unlock()
992+
993+ # We uploaded successfully, remember it
994+ branch.lock_write()
995+ try:
996+ upload_location = conf.get('upload_location')
997+ if upload_location is None or remember:
998+ conf.set('upload_location',
999+ urlutils.unescape(to_transport.base))
1000+ if auto is not None:
1001+ conf.set('upload_auto', auto)
1002+ finally:
1003+ branch.unlock()
1004+
1005
1006=== added directory 'breezy/plugins/upload/tests'
1007=== added file 'breezy/plugins/upload/tests/__init__.py'
1008--- breezy/plugins/upload/tests/__init__.py 1970-01-01 00:00:00 +0000
1009+++ breezy/plugins/upload/tests/__init__.py 2017-06-02 20:49:44 +0000
1010@@ -0,0 +1,28 @@
1011+# Copyright (C) 2008 by Canonical Ltd
1012+#
1013+# This program is free software; you can redistribute it and/or modify
1014+# it under the terms of the GNU General Public License as published by
1015+# the Free Software Foundation; either version 2 of the License, or
1016+# (at your option) any later version.
1017+#
1018+# This program is distributed in the hope that it will be useful,
1019+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1020+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1021+# GNU General Public License for more details.
1022+#
1023+# You should have received a copy of the GNU General Public License
1024+# along with this program; if not, write to the Free Software
1025+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
1026+
1027+def load_tests(loader, basic_tests, pattern):
1028+ # This module shouldn't define any tests but I don't know how to report
1029+ # that. I prefer to update basic_tests with the other tests to detect
1030+ # unwanted tests and I think that's sufficient.
1031+
1032+ testmod_names = [
1033+ 'test_auto_upload_hook',
1034+ 'test_upload',
1035+ ]
1036+ basic_tests.addTest(loader.loadTestsFromModuleNames(
1037+ ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
1038+ return basic_tests
1039
1040=== added file 'breezy/plugins/upload/tests/test_auto_upload_hook.py'
1041--- breezy/plugins/upload/tests/test_auto_upload_hook.py 1970-01-01 00:00:00 +0000
1042+++ breezy/plugins/upload/tests/test_auto_upload_hook.py 2017-06-02 20:49:44 +0000
1043@@ -0,0 +1,87 @@
1044+# Copyright (C) 2008, 2009, 2011, 2012 Canonical Ltd
1045+#
1046+# This program is free software; you can redistribute it and/or modify
1047+# it under the terms of the GNU General Public License as published by
1048+# the Free Software Foundation; either version 2 of the License, or
1049+# (at your option) any later version.
1050+#
1051+# This program is distributed in the hope that it will be useful,
1052+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1053+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1054+# GNU General Public License for more details.
1055+#
1056+# You should have received a copy of the GNU General Public License
1057+# along with this program; if not, write to the Free Software
1058+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
1059+
1060+import os
1061+
1062+from .... import (
1063+ tests,
1064+ )
1065+
1066+from ... import (
1067+ upload,
1068+ )
1069+from .. import (
1070+ cmds,
1071+ )
1072+
1073+
1074+class AutoPushHookTests(tests.TestCaseWithTransport):
1075+
1076+ def setUp(self):
1077+ super(AutoPushHookTests, self).setUp()
1078+ upload.install_auto_upload_hook()
1079+
1080+ def make_start_branch(self):
1081+ self.wt = self.make_branch_and_tree('.')
1082+ self.build_tree(['a'])
1083+ self.wt.add(['a'])
1084+ self.wt.commit("one")
1085+
1086+
1087+class AutoPushWithLocation(AutoPushHookTests):
1088+
1089+ def setUp(self):
1090+ super(AutoPushWithLocation, self).setUp()
1091+ self.make_start_branch()
1092+ conf = self.wt.branch.get_config_stack()
1093+ conf.set('upload_auto', True)
1094+ conf.set('upload_location', self.get_url('target'))
1095+ conf.set('upload_auto_quiet', True)
1096+
1097+ def test_auto_push_on_commit(self):
1098+ self.assertPathDoesNotExist('target')
1099+ self.build_tree(['b'])
1100+ self.wt.add(['b'])
1101+ self.wt.commit("two")
1102+ self.assertPathExists('target')
1103+ self.assertPathExists(os.path.join('target', 'a'))
1104+ self.assertPathExists(os.path.join('target', 'b'))
1105+
1106+ def test_disable_auto_push(self):
1107+ self.assertPathDoesNotExist('target')
1108+ self.build_tree(['b'])
1109+ self.wt.add(['b'])
1110+ self.wt.commit("two")
1111+ self.wt.branch.get_config_stack().set('upload_auto', False)
1112+ self.build_tree(['c'])
1113+ self.wt.add(['c'])
1114+ self.wt.commit("three")
1115+ self.assertPathDoesNotExist(os.path.join('target', 'c'))
1116+
1117+
1118+class AutoPushWithoutLocation(AutoPushHookTests):
1119+
1120+ def setUp(self):
1121+ super(AutoPushWithoutLocation, self).setUp()
1122+ self.make_start_branch()
1123+ self.wt.branch.get_config_stack().set('upload_auto', True)
1124+
1125+ def test_dont_push_if_no_location(self):
1126+ self.assertPathDoesNotExist('target')
1127+ self.build_tree(['b'])
1128+ self.wt.add(['b'])
1129+ self.wt.commit("two")
1130+ self.assertPathDoesNotExist('target')
1131
1132=== added file 'breezy/plugins/upload/tests/test_upload.py'
1133--- breezy/plugins/upload/tests/test_upload.py 1970-01-01 00:00:00 +0000
1134+++ breezy/plugins/upload/tests/test_upload.py 2017-06-02 20:49:44 +0000
1135@@ -0,0 +1,857 @@
1136+# Copyright (C) 2008-2012 Canonical Ltd
1137+#
1138+# This program is free software; you can redistribute it and/or modify
1139+# it under the terms of the GNU General Public License as published by
1140+# the Free Software Foundation; either version 2 of the License, or
1141+# (at your option) any later version.
1142+#
1143+# This program is distributed in the hope that it will be useful,
1144+# but WITHOUT ANY WARRANTY; without even the implied warranty of
1145+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1146+# GNU General Public License for more details.
1147+#
1148+# You should have received a copy of the GNU General Public License
1149+# along with this program; if not, write to the Free Software
1150+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
1151+
1152+import os
1153+import sys
1154+
1155+
1156+from .... import (
1157+ bzrdir,
1158+ config,
1159+ errors,
1160+ osutils,
1161+ revisionspec,
1162+ tests,
1163+ transport,
1164+ workingtree,
1165+ uncommit,
1166+ )
1167+from ....tests import (
1168+ features,
1169+ per_branch,
1170+ per_transport,
1171+ stub_sftp,
1172+ )
1173+from ....transport import (
1174+ ftp,
1175+ sftp,
1176+ )
1177+from .. import (
1178+ cmds,
1179+ )
1180+
1181+
1182+def get_transport_scenarios():
1183+ result = []
1184+ basis = per_transport.transport_test_permutations()
1185+ # Keep only the interesting ones for upload
1186+ for name, d in basis:
1187+ t_class = d['transport_class']
1188+ if t_class in (ftp.FtpTransport, sftp.SFTPTransport):
1189+ result.append((name, d))
1190+ try:
1191+ import breezy.plugins.local_test_server
1192+ from breezy.plugins.local_test_server import test_server
1193+ if False:
1194+ # XXX: Disable since we can't get chmod working for anonymous
1195+ # user
1196+ scenario = ('vsftpd',
1197+ {'transport_class': test_server.FtpTransport,
1198+ 'transport_server': test_server.Vsftpd,
1199+ })
1200+ result.append(scenario)
1201+ from test_server import ProftpdFeature
1202+ if ProftpdFeature().available():
1203+ scenario = ('proftpd',
1204+ {'transport_class': test_server.FtpTransport,
1205+ 'transport_server': test_server.Proftpd,
1206+ })
1207+ result.append(scenario)
1208+ # XXX: add support for pyftpdlib
1209+ except ImportError:
1210+ pass
1211+ return result
1212+
1213+
1214+def load_tests(loader, standard_tests, pattern):
1215+ """Multiply tests for tranport implementations."""
1216+ result = loader.suiteClass()
1217+
1218+ # one for each transport implementation
1219+ t_tests, remaining_tests = tests.split_suite_by_condition(
1220+ standard_tests, tests.condition_isinstance((
1221+ TestFullUpload,
1222+ TestIncrementalUpload,
1223+ TestUploadFromRemoteBranch,
1224+ )))
1225+ tests.multiply_tests(t_tests, get_transport_scenarios(), result)
1226+
1227+ # one for each branch format
1228+ b_tests, remaining_tests = tests.split_suite_by_condition(
1229+ remaining_tests, tests.condition_isinstance((
1230+ TestBranchUploadLocations,
1231+ )))
1232+ tests.multiply_tests(b_tests, per_branch.branch_scenarios(),
1233+ result)
1234+
1235+ # No parametrization for the remaining tests
1236+ result.addTests(remaining_tests)
1237+
1238+ return result
1239+
1240+
1241+class UploadUtilsMixin(object):
1242+ """Helper class to write upload tests.
1243+
1244+ This class provides helpers to simplify test writing. The emphasis is on
1245+ easy test writing, so each tree modification is committed. This doesn't
1246+ preclude writing tests spawning several revisions to upload more complex
1247+ changes.
1248+ """
1249+
1250+ upload_dir = 'upload'
1251+ branch_dir = 'branch'
1252+
1253+ def make_branch_and_working_tree(self):
1254+ t = transport.get_transport(self.branch_dir)
1255+ t.ensure_base()
1256+ branch = bzrdir.BzrDir.create_branch_convenience(
1257+ t.base,
1258+ format=bzrdir.format_registry.make_bzrdir('default'),
1259+ force_new_tree=False)
1260+ self.tree = branch.bzrdir.create_workingtree()
1261+ self.tree.commit('initial empty tree')
1262+
1263+ def assertUpFileEqual(self, content, path, base=upload_dir):
1264+ self.assertFileEqual(content, osutils.pathjoin(base, path))
1265+
1266+ def assertUpPathModeEqual(self, path, expected_mode, base=upload_dir):
1267+ # FIXME: the tests needing that assertion should depend on the server
1268+ # ability to handle chmod so that they don't fail (or be skipped)
1269+ # against servers that can't. Note that some breezy transports define
1270+ # _can_roundtrip_unix_modebits in a incomplete way, this property
1271+ # should depend on both the client and the server, not the client only.
1272+ # But the client will know or can find if the server support chmod so
1273+ # that's the client that will report it anyway.
1274+ full_path = osutils.pathjoin(base, path)
1275+ st = os.stat(full_path)
1276+ mode = st.st_mode & 0777
1277+ if expected_mode == mode:
1278+ return
1279+ raise AssertionError(
1280+ 'For path %s, mode is %s not %s' %
1281+ (full_path, oct(mode), oct(expected_mode)))
1282+
1283+ def assertUpPathDoesNotExist(self, path, base=upload_dir):
1284+ self.assertPathDoesNotExist(osutils.pathjoin(base, path))
1285+
1286+ def assertUpPathExists(self, path, base=upload_dir):
1287+ self.assertPathExists(osutils.pathjoin(base, path))
1288+
1289+ def set_file_content(self, path, content, base=branch_dir):
1290+ f = file(osutils.pathjoin(base, path), 'wb')
1291+ try:
1292+ f.write(content)
1293+ finally:
1294+ f.close()
1295+
1296+ def add_file(self, path, content, base=branch_dir):
1297+ self.set_file_content(path, content, base)
1298+ self.tree.add(path)
1299+ self.tree.commit('add file %s' % path)
1300+
1301+ def modify_file(self, path, content, base=branch_dir):
1302+ self.set_file_content(path, content, base)
1303+ self.tree.commit('modify file %s' % path)
1304+
1305+ def chmod_file(self, path, mode, base=branch_dir):
1306+ full_path = osutils.pathjoin(base, path)
1307+ os.chmod(full_path, mode)
1308+ self.tree.commit('change file %s mode to %s' % (path, oct(mode)))
1309+
1310+ def delete_any(self, path, base=branch_dir):
1311+ self.tree.remove([path], keep_files=False)
1312+ self.tree.commit('delete %s' % path)
1313+
1314+ def add_dir(self, path, base=branch_dir):
1315+ os.mkdir(osutils.pathjoin(base, path))
1316+ self.tree.add(path)
1317+ self.tree.commit('add directory %s' % path)
1318+
1319+ def rename_any(self, old_path, new_path):
1320+ self.tree.rename_one(old_path, new_path)
1321+ self.tree.commit('rename %s into %s' % (old_path, new_path))
1322+
1323+ def transform_dir_into_file(self, path, content, base=branch_dir):
1324+ osutils.delete_any(osutils.pathjoin(base, path))
1325+ self.set_file_content(path, content, base)
1326+ self.tree.commit('change %s from dir to file' % path)
1327+
1328+ def transform_file_into_dir(self, path, base=branch_dir):
1329+ # bzr can't handle that kind change in a single commit without an
1330+ # intervening bzr status (see bug #205636).
1331+ self.tree.remove([path], keep_files=False)
1332+ os.mkdir(osutils.pathjoin(base, path))
1333+ self.tree.add(path)
1334+ self.tree.commit('change %s from file to dir' % path)
1335+
1336+ def add_symlink(self, path, target, base=branch_dir):
1337+ self.requireFeature(features.SymlinkFeature)
1338+ os.symlink(target, osutils.pathjoin(base, path))
1339+ self.tree.add(path)
1340+ self.tree.commit('add symlink %s -> %s' % (path, target))
1341+
1342+ def modify_symlink(self, path, target, base=branch_dir):
1343+ self.requireFeature(features.SymlinkFeature)
1344+ full_path = osutils.pathjoin(base, path)
1345+ os.unlink(full_path)
1346+ os.symlink(target, full_path)
1347+ self.tree.commit('modify symlink %s -> %s' % (path, target))
1348+
1349+ def _get_cmd_upload(self):
1350+ cmd = cmds.cmd_upload()
1351+ # We don't want to use run_bzr here because redirected output are a
1352+ # pain to debug. But we need to provides a valid outf.
1353+ # XXX: Should a bug against bzr be filled about that ?
1354+
1355+ # Short story: we don't expect any output so we may just use stdout
1356+ cmd.outf = sys.stdout
1357+ return cmd
1358+
1359+ def do_full_upload(self, *args, **kwargs):
1360+ upload = self._get_cmd_upload()
1361+ up_url = self.get_url(self.upload_dir)
1362+ if kwargs.get('directory', None) is None:
1363+ kwargs['directory'] = self.branch_dir
1364+ kwargs['full'] = True
1365+ kwargs['quiet'] = True
1366+ upload.run(up_url, *args, **kwargs)
1367+
1368+ def do_incremental_upload(self, *args, **kwargs):
1369+ upload = self._get_cmd_upload()
1370+ up_url = self.get_url(self.upload_dir)
1371+ if kwargs.get('directory', None) is None:
1372+ kwargs['directory'] = self.branch_dir
1373+ kwargs['quiet'] = True
1374+ upload.run(up_url, *args, **kwargs)
1375+
1376+
1377+class TestUploadMixin(UploadUtilsMixin):
1378+ """Helper class to share tests between full and incremental uploads."""
1379+
1380+ def _test_create_file(self, file_name):
1381+ self.make_branch_and_working_tree()
1382+ self.do_full_upload()
1383+ self.add_file(file_name, 'foo')
1384+
1385+ self.do_upload()
1386+
1387+ self.assertUpFileEqual('foo', file_name)
1388+
1389+ def test_create_file(self):
1390+ self._test_create_file('hello')
1391+
1392+ def test_unicode_create_file(self):
1393+ self.requireFeature(features.UnicodeFilenameFeature)
1394+ self._test_create_file(u'hell\u00d8')
1395+
1396+ def _test_create_file_in_dir(self, dir_name, file_name):
1397+ self.make_branch_and_working_tree()
1398+ self.do_full_upload()
1399+ self.add_dir(dir_name)
1400+ fpath = '%s/%s' % (dir_name, file_name)
1401+ self.add_file(fpath, 'baz')
1402+
1403+ self.assertUpPathDoesNotExist(fpath)
1404+
1405+ self.do_upload()
1406+
1407+ self.assertUpFileEqual('baz', fpath)
1408+ self.assertUpPathModeEqual(dir_name, 0775)
1409+
1410+ def test_create_file_in_dir(self):
1411+ self._test_create_file_in_dir('dir', 'goodbye')
1412+
1413+ def test_unicode_create_file_in_dir(self):
1414+ self.requireFeature(features.UnicodeFilenameFeature)
1415+ self._test_create_file_in_dir(u'dir\u00d8', u'goodbye\u00d8')
1416+
1417+ def test_modify_file(self):
1418+ self.make_branch_and_working_tree()
1419+ self.add_file('hello', 'foo')
1420+ self.do_full_upload()
1421+ self.modify_file('hello', 'bar')
1422+
1423+ self.assertUpFileEqual('foo', 'hello')
1424+
1425+ self.do_upload()
1426+
1427+ self.assertUpFileEqual('bar', 'hello')
1428+
1429+ def _test_rename_one_file(self, old_name, new_name):
1430+ self.make_branch_and_working_tree()
1431+ self.add_file(old_name, 'foo')
1432+ self.do_full_upload()
1433+ self.rename_any(old_name, new_name)
1434+
1435+ self.assertUpFileEqual('foo', old_name)
1436+
1437+ self.do_upload()
1438+
1439+ self.assertUpFileEqual('foo', new_name)
1440+
1441+ def test_rename_one_file(self):
1442+ self._test_rename_one_file('hello', 'goodbye')
1443+
1444+ def test_unicode_rename_one_file(self):
1445+ self.requireFeature(features.UnicodeFilenameFeature)
1446+ self._test_rename_one_file(u'hello\u00d8', u'goodbye\u00d8')
1447+
1448+ def test_rename_and_change_file(self):
1449+ self.make_branch_and_working_tree()
1450+ self.add_file('hello', 'foo')
1451+ self.do_full_upload()
1452+ self.rename_any('hello', 'goodbye')
1453+ self.modify_file('goodbye', 'bar')
1454+
1455+ self.assertUpFileEqual('foo', 'hello')
1456+
1457+ self.do_upload()
1458+
1459+ self.assertUpFileEqual('bar', 'goodbye')
1460+
1461+ def test_rename_two_files(self):
1462+ self.make_branch_and_working_tree()
1463+ self.add_file('a', 'foo')
1464+ self.add_file('b', 'qux')
1465+ self.do_full_upload()
1466+ # We rely on the assumption that bzr will topologically sort the
1467+ # renames which will cause a -> b to appear *before* b -> c
1468+ self.rename_any('b', 'c')
1469+ self.rename_any('a', 'b')
1470+
1471+ self.assertUpFileEqual('foo', 'a')
1472+ self.assertUpFileEqual('qux', 'b')
1473+
1474+ self.do_upload()
1475+
1476+ self.assertUpFileEqual('foo', 'b')
1477+ self.assertUpFileEqual('qux', 'c')
1478+
1479+ def test_upload_revision(self):
1480+ self.make_branch_and_working_tree() # rev1
1481+ self.do_full_upload()
1482+ self.add_file('hello', 'foo') # rev2
1483+ self.modify_file('hello', 'bar') # rev3
1484+
1485+ self.assertUpPathDoesNotExist('hello')
1486+
1487+ revspec = revisionspec.RevisionSpec.from_string('2')
1488+ self.do_upload(revision=[revspec])
1489+
1490+ self.assertUpFileEqual('foo', 'hello')
1491+
1492+ def test_no_upload_when_changes(self):
1493+ self.make_branch_and_working_tree()
1494+ self.add_file('a', 'foo')
1495+ self.set_file_content('a', 'bar')
1496+
1497+ self.assertRaises(errors.UncommittedChanges, self.do_upload)
1498+
1499+ def test_no_upload_when_conflicts(self):
1500+ self.make_branch_and_working_tree()
1501+ self.add_file('a', 'foo')
1502+ self.run_bzr('branch branch other')
1503+ self.modify_file('a', 'bar')
1504+ other_tree = workingtree.WorkingTree.open('other')
1505+ self.set_file_content('a', 'baz', 'other/')
1506+ other_tree.commit('modify file a')
1507+
1508+ self.run_bzr('merge -d branch other', retcode=1)
1509+
1510+ self.assertRaises(errors.UncommittedChanges, self.do_upload)
1511+
1512+ def _test_change_file_into_dir(self, file_name):
1513+ self.make_branch_and_working_tree()
1514+ self.add_file(file_name, 'foo')
1515+ self.do_full_upload()
1516+ self.transform_file_into_dir(file_name)
1517+ fpath = '%s/%s' % (file_name, 'file')
1518+ self.add_file(fpath, 'bar')
1519+
1520+ self.assertUpFileEqual('foo', file_name)
1521+
1522+ self.do_upload()
1523+
1524+ self.assertUpFileEqual('bar', fpath)
1525+
1526+ def test_change_file_into_dir(self):
1527+ self._test_change_file_into_dir('hello')
1528+
1529+ def test_unicode_change_file_into_dir(self):
1530+ self.requireFeature(features.UnicodeFilenameFeature)
1531+ self._test_change_file_into_dir(u'hello\u00d8')
1532+
1533+ def test_change_dir_into_file(self):
1534+ self.make_branch_and_working_tree()
1535+ self.add_dir('hello')
1536+ self.add_file('hello/file', 'foo')
1537+ self.do_full_upload()
1538+ self.delete_any('hello/file')
1539+ self.transform_dir_into_file('hello', 'bar')
1540+
1541+ self.assertUpFileEqual('foo', 'hello/file')
1542+
1543+ self.do_upload()
1544+
1545+ self.assertUpFileEqual('bar', 'hello')
1546+
1547+ def _test_make_file_executable(self, file_name):
1548+ self.make_branch_and_working_tree()
1549+ self.add_file(file_name, 'foo')
1550+ self.chmod_file(file_name, 0664)
1551+ self.do_full_upload()
1552+ self.chmod_file(file_name, 0755)
1553+
1554+ self.assertUpPathModeEqual(file_name, 0664)
1555+
1556+ self.do_upload()
1557+
1558+ self.assertUpPathModeEqual(file_name, 0775)
1559+
1560+ def test_make_file_executable(self):
1561+ self._test_make_file_executable('hello')
1562+
1563+ def test_unicode_make_file_executable(self):
1564+ self.requireFeature(features.UnicodeFilenameFeature)
1565+ self._test_make_file_executable(u'hello\u00d8')
1566+
1567+ def test_create_symlink(self):
1568+ self.make_branch_and_working_tree()
1569+ self.do_full_upload()
1570+ self.add_symlink('link', 'target')
1571+
1572+ self.do_upload()
1573+
1574+ self.assertUpPathDoesNotExist('link')
1575+
1576+ def test_rename_symlink(self):
1577+ self.make_branch_and_working_tree()
1578+ old_name, new_name = 'old-link', 'new-link'
1579+ self.add_symlink(old_name, 'target')
1580+ self.do_full_upload()
1581+ self.rename_any(old_name, new_name)
1582+
1583+ self.do_upload()
1584+
1585+ self.assertUpPathDoesNotExist(old_name)
1586+ self.assertUpPathDoesNotExist(new_name)
1587+
1588+ def get_upload_auto(self):
1589+ # We need a fresh branch to check what has been saved on disk
1590+ b = bzrdir.BzrDir.open(self.tree.basedir).open_branch()
1591+ return b.get_config_stack().get('upload_auto')
1592+
1593+ def test_upload_auto(self):
1594+ """Test that upload --auto sets the upload_auto option"""
1595+ self.make_branch_and_working_tree()
1596+
1597+ self.add_file('hello', 'foo')
1598+ self.assertFalse(self.get_upload_auto())
1599+ self.do_full_upload(auto=True)
1600+ self.assertUpFileEqual('foo', 'hello')
1601+ self.assertTrue(self.get_upload_auto())
1602+
1603+ # and check that it stays set until it is unset
1604+ self.add_file('bye', 'bar')
1605+ self.do_full_upload()
1606+ self.assertUpFileEqual('bar', 'bye')
1607+ self.assertTrue(self.get_upload_auto())
1608+
1609+ def test_upload_noauto(self):
1610+ """Test that upload --no-auto unsets the upload_auto option"""
1611+ self.make_branch_and_working_tree()
1612+
1613+ self.add_file('hello', 'foo')
1614+ self.do_full_upload(auto=True)
1615+ self.assertUpFileEqual('foo', 'hello')
1616+ self.assertTrue(self.get_upload_auto())
1617+
1618+ self.add_file('bye', 'bar')
1619+ self.do_full_upload(auto=False)
1620+ self.assertUpFileEqual('bar', 'bye')
1621+ self.assertFalse(self.get_upload_auto())
1622+
1623+ # and check that it stays unset until it is set
1624+ self.add_file('again', 'baz')
1625+ self.do_full_upload()
1626+ self.assertUpFileEqual('baz', 'again')
1627+ self.assertFalse(self.get_upload_auto())
1628+
1629+ def test_upload_from_subdir(self):
1630+ self.make_branch_and_working_tree()
1631+ self.build_tree(['branch/foo/', 'branch/foo/bar'])
1632+ self.tree.add(['foo/', 'foo/bar'])
1633+ self.tree.commit("Add directory")
1634+ self.do_full_upload(directory='branch/foo')
1635+
1636+ def test_upload_revid_path_in_dir(self):
1637+ self.make_branch_and_working_tree()
1638+ self.add_dir('dir')
1639+ self.add_file('dir/goodbye', 'baz')
1640+
1641+ revid_path = 'dir/revid-path'
1642+ self.tree.branch.get_config_stack(
1643+ ).set('upload_revid_location', revid_path)
1644+ self.assertUpPathDoesNotExist(revid_path)
1645+
1646+ self.do_full_upload()
1647+
1648+ self.add_file('dir/hello', 'foo')
1649+
1650+ self.do_upload()
1651+
1652+ self.assertUpPathExists(revid_path)
1653+ self.assertUpFileEqual('baz', 'dir/goodbye')
1654+ self.assertUpFileEqual('foo', 'dir/hello')
1655+
1656+ def test_ignore_file(self):
1657+ self.make_branch_and_working_tree()
1658+ self.do_full_upload()
1659+ self.add_file('.bzrignore-upload', 'foo')
1660+ self.add_file('foo', 'bar')
1661+
1662+ self.do_upload()
1663+
1664+ self.assertUpPathDoesNotExist('foo')
1665+
1666+ def test_ignore_regexp(self):
1667+ self.make_branch_and_working_tree()
1668+ self.do_full_upload()
1669+ self.add_file('.bzrignore-upload', 'f*')
1670+ self.add_file('foo', 'bar')
1671+
1672+ self.do_upload()
1673+
1674+ self.assertUpPathDoesNotExist('foo')
1675+
1676+ def test_ignore_directory(self):
1677+ self.make_branch_and_working_tree()
1678+ self.do_full_upload()
1679+ self.add_file('.bzrignore-upload', 'dir')
1680+ self.add_dir('dir')
1681+
1682+ self.do_upload()
1683+
1684+ self.assertUpPathDoesNotExist('dir')
1685+
1686+ def test_ignore_nested_directory(self):
1687+ self.make_branch_and_working_tree()
1688+ self.do_full_upload()
1689+ self.add_file('.bzrignore-upload', 'dir')
1690+ self.add_dir('dir')
1691+ self.add_dir('dir/foo')
1692+ self.add_file('dir/foo/bar', 'bar contents')
1693+
1694+ self.do_upload()
1695+
1696+ self.assertUpPathDoesNotExist('dir')
1697+ self.assertUpPathDoesNotExist('dir/foo/bar')
1698+
1699+ def test_ignore_change_file_into_dir(self):
1700+ self.make_branch_and_working_tree()
1701+ self.add_file('hello', 'foo')
1702+ self.do_full_upload()
1703+ self.add_file('.bzrignore-upload', 'hello')
1704+ self.transform_file_into_dir('hello')
1705+ self.add_file('hello/file', 'bar')
1706+
1707+ self.assertUpFileEqual('foo', 'hello')
1708+
1709+ self.do_upload()
1710+
1711+ self.assertUpFileEqual('foo', 'hello')
1712+
1713+ def test_ignore_change_dir_into_file(self):
1714+ self.make_branch_and_working_tree()
1715+ self.add_dir('hello')
1716+ self.add_file('hello/file', 'foo')
1717+ self.do_full_upload()
1718+
1719+ self.add_file('.bzrignore-upload', 'hello')
1720+ self.delete_any('hello/file')
1721+ self.transform_dir_into_file('hello', 'bar')
1722+
1723+ self.assertUpFileEqual('foo', 'hello/file')
1724+
1725+ self.do_upload()
1726+
1727+ self.assertUpFileEqual('foo', 'hello/file')
1728+
1729+ def test_ignore_delete_dir_in_subdir(self):
1730+ self.make_branch_and_working_tree()
1731+ self.add_dir('dir')
1732+ self.add_dir('dir/subdir')
1733+ self.add_file('dir/subdir/a', 'foo')
1734+ self.do_full_upload()
1735+ self.add_file('.bzrignore-upload', 'dir/subdir')
1736+ self.rename_any('dir/subdir/a', 'dir/a')
1737+ self.delete_any('dir/subdir')
1738+
1739+ self.assertUpFileEqual('foo', 'dir/subdir/a')
1740+
1741+ self.do_upload()
1742+
1743+ # The file in the dir is not ignored. This a bit contrived but
1744+ # indicates that we may encounter problems when ignored items appear
1745+ # and disappear... -- vila 100106
1746+ self.assertUpFileEqual('foo', 'dir/a')
1747+
1748+
1749+class TestFullUpload(tests.TestCaseWithTransport, TestUploadMixin):
1750+
1751+ do_upload = TestUploadMixin.do_full_upload
1752+
1753+ def test_full_upload_empty_tree(self):
1754+ self.make_branch_and_working_tree()
1755+
1756+ self.do_full_upload()
1757+
1758+ revid_path = self.tree.branch.get_config_stack(
1759+ ).get('upload_revid_location')
1760+ self.assertUpPathExists(revid_path)
1761+
1762+ def test_invalid_revspec(self):
1763+ self.make_branch_and_working_tree()
1764+ rev1 = revisionspec.RevisionSpec.from_string('1')
1765+ rev2 = revisionspec.RevisionSpec.from_string('2')
1766+
1767+ self.assertRaises(errors.BzrCommandError,
1768+ self.do_incremental_upload, revision=[rev1, rev2])
1769+
1770+ def test_create_remote_dir_twice(self):
1771+ self.make_branch_and_working_tree()
1772+ self.add_dir('dir')
1773+ self.do_full_upload()
1774+ self.add_file('dir/goodbye', 'baz')
1775+
1776+ self.assertUpPathDoesNotExist('dir/goodbye')
1777+
1778+ self.do_full_upload()
1779+
1780+ self.assertUpFileEqual('baz', 'dir/goodbye')
1781+ self.assertUpPathModeEqual('dir', 0775)
1782+
1783+
1784+class TestIncrementalUpload(tests.TestCaseWithTransport, TestUploadMixin):
1785+
1786+ do_upload = TestUploadMixin.do_incremental_upload
1787+
1788+ # XXX: full upload doesn't handle deletions....
1789+
1790+ def test_delete_one_file(self):
1791+ self.make_branch_and_working_tree()
1792+ self.add_file('hello', 'foo')
1793+ self.do_full_upload()
1794+ self.delete_any('hello')
1795+
1796+ self.assertUpFileEqual('foo', 'hello')
1797+
1798+ self.do_upload()
1799+
1800+ self.assertUpPathDoesNotExist('hello')
1801+
1802+ def test_delete_dir_and_subdir(self):
1803+ self.make_branch_and_working_tree()
1804+ self.add_dir('dir')
1805+ self.add_dir('dir/subdir')
1806+ self.add_file('dir/subdir/a', 'foo')
1807+ self.do_full_upload()
1808+ self.rename_any('dir/subdir/a', 'a')
1809+ self.delete_any('dir/subdir')
1810+ self.delete_any('dir')
1811+
1812+ self.assertUpFileEqual('foo', 'dir/subdir/a')
1813+
1814+ self.do_upload()
1815+
1816+ self.assertUpPathDoesNotExist('dir/subdir/a')
1817+ self.assertUpPathDoesNotExist('dir/subdir')
1818+ self.assertUpPathDoesNotExist('dir')
1819+ self.assertUpFileEqual('foo', 'a')
1820+
1821+ def test_delete_one_file_rename_to_deleted(self):
1822+ self.make_branch_and_working_tree()
1823+ self.add_file('a', 'foo')
1824+ self.add_file('b', 'bar')
1825+ self.do_full_upload()
1826+ self.delete_any('a')
1827+ self.rename_any('b', 'a')
1828+
1829+ self.assertUpFileEqual('foo', 'a')
1830+
1831+ self.do_upload()
1832+
1833+ self.assertUpPathDoesNotExist('b')
1834+ self.assertUpFileEqual('bar', 'a')
1835+
1836+ def test_rename_outside_dir_delete_dir(self):
1837+ self.make_branch_and_working_tree()
1838+ self.add_dir('dir')
1839+ self.add_file('dir/a', 'foo')
1840+ self.do_full_upload()
1841+ self.rename_any('dir/a', 'a')
1842+ self.delete_any('dir')
1843+
1844+ self.assertUpFileEqual('foo', 'dir/a')
1845+
1846+ self.do_upload()
1847+
1848+ self.assertUpPathDoesNotExist('dir/a')
1849+ self.assertUpPathDoesNotExist('dir')
1850+ self.assertUpFileEqual('foo', 'a')
1851+
1852+ def test_delete_symlink(self):
1853+ self.make_branch_and_working_tree()
1854+ self.add_symlink('link', 'target')
1855+ self.do_full_upload()
1856+ self.delete_any('link')
1857+
1858+ self.do_upload()
1859+
1860+ self.assertUpPathDoesNotExist('link')
1861+
1862+ def test_upload_for_the_first_time_do_a_full_upload(self):
1863+ self.make_branch_and_working_tree()
1864+ self.add_file('hello', 'bar')
1865+
1866+ revid_path = self.tree.branch.get_config_stack(
1867+ ).get('upload_revid_location')
1868+ self.assertUpPathDoesNotExist(revid_path)
1869+
1870+ self.do_upload()
1871+
1872+ self.assertUpFileEqual('bar', 'hello')
1873+
1874+ def test_ignore_delete_one_file(self):
1875+ self.make_branch_and_working_tree()
1876+ self.add_file('hello', 'foo')
1877+ self.do_full_upload()
1878+ self.add_file('.bzrignore-upload', 'hello')
1879+ self.delete_any('hello')
1880+
1881+ self.assertUpFileEqual('foo', 'hello')
1882+
1883+ self.do_upload()
1884+
1885+ self.assertUpFileEqual('foo', 'hello')
1886+
1887+
1888+class TestBranchUploadLocations(per_branch.TestCaseWithBranch):
1889+
1890+ def test_get_upload_location_unset(self):
1891+ conf = self.get_branch().get_config_stack()
1892+ self.assertEqual(None, conf.get('upload_location'))
1893+
1894+ def test_get_push_location_exact(self):
1895+ config.ensure_config_dir_exists()
1896+ fn = config.locations_config_filename()
1897+ b = self.get_branch()
1898+ with open(fn, 'wt') as f:
1899+ f.write(("[%s]\n" "upload_location=foo\n" % b.base.rstrip("/")))
1900+ self.assertEqual("foo", b.get_config_stack().get('upload_location'))
1901+
1902+ def test_set_push_location(self):
1903+ conf = self.get_branch().get_config_stack()
1904+ conf.set('upload_location', 'foo')
1905+ self.assertEqual('foo', conf.get('upload_location'))
1906+
1907+
1908+class TestUploadFromRemoteBranch(tests.TestCaseWithTransport, UploadUtilsMixin):
1909+
1910+ remote_branch_dir = 'remote_branch'
1911+
1912+ def setUp(self):
1913+ super(TestUploadFromRemoteBranch, self).setUp()
1914+ self.remote_branch_url = self.make_remote_branch_without_working_tree()
1915+
1916+ def make_remote_branch_without_working_tree(self):
1917+ """Creates a branch without working tree to upload from.
1918+
1919+ It's created from the existing self.branch_dir one which still has its
1920+ working tree.
1921+ """
1922+ self.make_branch_and_working_tree()
1923+ self.add_file('hello', 'foo')
1924+
1925+ remote_branch_url = self.get_url(self.remote_branch_dir)
1926+ if self.transport_server is stub_sftp.SFTPHomeDirServer:
1927+ # FIXME: Some policy search ends up above the user home directory
1928+ # and are seen as attemps to escape test isolation
1929+ raise tests.TestNotApplicable('Escaping test isolation')
1930+ self.run_bzr(['push', remote_branch_url,
1931+ '--directory', self.branch_dir])
1932+ return remote_branch_url
1933+
1934+ def test_no_upload_to_remote_working_tree(self):
1935+ cmd = self._get_cmd_upload()
1936+ up_url = self.get_url(self.branch_dir)
1937+ # Let's try to upload from the just created remote branch into the
1938+ # branch (which has a working tree).
1939+ self.assertRaises(cmds.CannotUploadToWorkingTree,
1940+ cmd.run, up_url, directory=self.remote_branch_url)
1941+
1942+ def test_upload_without_working_tree(self):
1943+ self.do_full_upload(directory=self.remote_branch_url)
1944+ self.assertUpFileEqual('foo', 'hello')
1945+
1946+
1947+class TestUploadDiverged(tests.TestCaseWithTransport, UploadUtilsMixin):
1948+
1949+ def setUp(self):
1950+ super(TestUploadDiverged, self).setUp()
1951+ self.diverged_tree = self.make_diverged_tree_and_upload_location()
1952+
1953+ def make_diverged_tree_and_upload_location(self):
1954+ tree_a = self.make_branch_and_tree('tree_a')
1955+ tree_a.commit('message 1', rev_id='rev1')
1956+ tree_a.commit('message 2', rev_id='rev2a')
1957+ tree_b = tree_a.bzrdir.sprout('tree_b').open_workingtree()
1958+ uncommit.uncommit(tree_b.branch, tree=tree_b)
1959+ tree_b.commit('message 2', rev_id='rev2b')
1960+ # upload tree a
1961+ self.do_full_upload(directory=tree_a.basedir)
1962+ return tree_b
1963+
1964+ def assertRevidUploaded(self, revid):
1965+ t = self.get_transport(self.upload_dir)
1966+ uploaded_revid = t.get_bytes('.bzr-upload.revid')
1967+ self.assertEqual(revid, uploaded_revid)
1968+
1969+ def test_cant_upload_diverged(self):
1970+ self.assertRaises(cmds.DivergedUploadedTree,
1971+ self.do_incremental_upload,
1972+ directory=self.diverged_tree.basedir)
1973+ self.assertRevidUploaded('rev2a')
1974+
1975+ def test_upload_diverged_with_overwrite(self):
1976+ self.do_incremental_upload(directory=self.diverged_tree.basedir,
1977+ overwrite=True)
1978+ self.assertRevidUploaded('rev2b')
1979+
1980+
1981+class TestUploadBadRemoteReivd(tests.TestCaseWithTransport, UploadUtilsMixin):
1982+
1983+ def test_raises_on_wrong_revid(self):
1984+ tree = self.make_branch_and_working_tree()
1985+ self.do_full_upload()
1986+ # Put a fake revid on the remote branch
1987+ t = self.get_transport(self.upload_dir)
1988+ t.put_bytes('.bzr-upload.revid', 'fake')
1989+ # Make a change
1990+ self.add_file('foo', 'bar\n')
1991+ self.assertRaises(cmds.DivergedUploadedTree, self.do_full_upload)
1992+
1993
1994=== modified file 'doc/en/release-notes/brz-3.0.txt'
1995--- doc/en/release-notes/brz-3.0.txt 2017-06-02 11:26:27 +0000
1996+++ doc/en/release-notes/brz-3.0.txt 2017-06-02 20:49:44 +0000
1997@@ -42,6 +42,9 @@
1998 * The 'stats' plugin is now bundled with Bazaar.
1999 (Jelmer Vernooij)
2000
2001+ * The 'upload' plugin is now bundled with Bazaar.
2002+ (Jelmer Vernooij)
2003+
2004 * The 'import' command is now bundled with brz.
2005 Imported from bzrtools by Aaron Bentley. (Jelmer Vernooij, #773241)
2006

Subscribers

People subscribed via source and target branches