~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2006-03-11 15:42:15 UTC
  • Revision ID: aaron.bentley@utoronto.ca-20060311154215-cd9599aa4e996ccf
Initial nested progressbar work.  (Need console fix)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 Aaron Bentley
 
2
# <aaron.bentley@utoronto.ca>
 
3
#
 
4
#    This program is free software; you can redistribute it and/or modify
 
5
#    it under the terms of the GNU General Public License as published by
 
6
#    the Free Software Foundation; either version 2 of the License, or
 
7
#    (at your option) any later version.
 
8
#
 
9
#    This program is distributed in the hope that it will be useful,
 
10
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
#    GNU General Public License for more details.
 
13
#
 
14
#    You should have received a copy of the GNU General Public License
 
15
#    along with this program; if not, write to the Free Software
 
16
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
17
import codecs
 
18
import errno
 
19
import os
 
20
import re
 
21
import tempfile
 
22
import shutil
 
23
from subprocess import Popen, PIPE
 
24
import sys
 
25
 
 
26
import bzrlib
 
27
import bzrlib.errors
 
28
from bzrlib.errors import BzrCommandError
 
29
from bzrlib.bzrdir import BzrDir
 
30
 
 
31
def temp_tree():
 
32
    dirname = tempfile.mkdtemp("temp-branch")
 
33
    return BzrDir.create_standalone_workingtree(dirname)
 
34
 
 
35
def rm_tree(tree):
 
36
    shutil.rmtree(tree.basedir)
 
37
 
 
38
def is_clean(cur_tree):
 
39
    """
 
40
    Return true if no files are modifed or unknown
 
41
    >>> import bzrlib.add
 
42
    >>> tree = temp_tree()
 
43
    >>> is_clean(tree)
 
44
    (True, [])
 
45
    >>> fooname = os.path.join(tree.basedir, "foo")
 
46
    >>> file(fooname, "wb").write("bar")
 
47
    >>> is_clean(tree)
 
48
    (True, [u'foo'])
 
49
    >>> bzrlib.add.smart_add_tree(tree, [tree.basedir])
 
50
    ([u'foo'], {})
 
51
    >>> is_clean(tree)
 
52
    (False, [])
 
53
    >>> tree.commit("added file")
 
54
    >>> is_clean(tree)
 
55
    (True, [])
 
56
    >>> rm_tree(tree)
 
57
    """
 
58
    from bzrlib.diff import compare_trees
 
59
    old_tree = cur_tree.basis_tree()
 
60
    new_tree = cur_tree
 
61
    non_source = []
 
62
    for path, file_class, kind, file_id, entry in new_tree.list_files():
 
63
        if file_class in ('?', 'I'):
 
64
            non_source.append(path)
 
65
    delta = compare_trees(old_tree, new_tree, want_unchanged=False)
 
66
    return not delta.has_changed(), non_source
 
67
 
 
68
def set_push_data(tree, location):
 
69
    push_file = file (tree._control_files.controlfilename("x-push-data"), "wb")
 
70
    push_file.write("%s\n" % location)
 
71
 
 
72
def get_push_data(tree):
 
73
    """
 
74
    >>> tree = temp_tree()
 
75
    >>> get_push_data(tree) is None
 
76
    True
 
77
    >>> set_push_data(tree, 'http://somewhere')
 
78
    >>> get_push_data(tree)
 
79
    'http://somewhere'
 
80
    >>> rm_tree(tree)
 
81
    """
 
82
    filename = tree._control_files.controlfilename("x-push-data")
 
83
    if not os.path.exists(filename):
 
84
        return None
 
85
    push_file = file (filename, "rb")
 
86
    (location,) = [f.rstrip('\n') for f in push_file]
 
87
    return location
 
88
 
 
89
"""
 
90
>>> shell_escape('hello')
 
91
'\h\e\l\l\o'
 
92
"""
 
93
def shell_escape(arg):
 
94
    return "".join(['\\'+c for c in arg])
 
95
 
 
96
def safe_system(args):
 
97
    """
 
98
    >>> real_system = os.system
 
99
    >>> os.system = sys.stdout.write
 
100
    >>> safe_system(['a', 'b', 'cd'])
 
101
    \\a \\b \\c\\d
 
102
    >>> os.system = real_system
 
103
    """
 
104
    arg_str = " ".join([shell_escape(a) for a in args])
 
105
    return os.system(arg_str)
 
106
 
 
107
class RsyncUnknownStatus(Exception):
 
108
    def __init__(self, status):
 
109
        Exception.__init__(self, "Unknown status: %d" % status)
 
110
 
 
111
class NoRsync(Exception):
 
112
    def __init__(self, rsync_name):
 
113
        Exception.__init__(self, "%s not found." % rsync_name)
 
114
 
 
115
def rsync(source, target, ssh=False, excludes=(), silent=False, 
 
116
          rsync_name="rsync"):
 
117
    """
 
118
    >>> new_dir = tempfile.mkdtemp()
 
119
    >>> old_dir = os.getcwd()
 
120
    >>> os.chdir(new_dir)
 
121
    >>> rsync("a", "b", silent=True)
 
122
    Traceback (most recent call last):
 
123
    RsyncNoFile: No such file...
 
124
    >>> rsync(new_dir + "/a", new_dir + "/b", excludes=("*.py",), silent=True)
 
125
    Traceback (most recent call last):
 
126
    RsyncNoFile: No such file...
 
127
    >>> rsync(new_dir + "/a", new_dir + "/b", excludes=("*.py",), silent=True, rsync_name="rsyncc")
 
128
    Traceback (most recent call last):
 
129
    NoRsync: rsyncc not found.
 
130
    >>> os.chdir(old_dir)
 
131
    >>> os.rmdir(new_dir)
 
132
    """
 
133
    cmd = [rsync_name, "-av", "--delete"]
 
134
    if ssh:
 
135
        cmd.extend(('-e', 'ssh'))
 
136
    if len(excludes) > 0:
 
137
        cmd.extend(('--exclude-from', '-'))
 
138
    cmd.extend((source, target))
 
139
    if silent:
 
140
        stderr = PIPE
 
141
        stdout = PIPE
 
142
    else:
 
143
        stderr = None
 
144
        stdout = None
 
145
    try:
 
146
        proc = Popen(cmd, stdin=PIPE, stderr=stderr, stdout=stdout)
 
147
    except OSError, e:
 
148
        if e.errno == errno.ENOENT:
 
149
            raise NoRsync(rsync_name)
 
150
            
 
151
    proc.stdin.write('\n'.join(excludes)+'\n')
 
152
    proc.stdin.close()
 
153
    if silent:
 
154
        proc.stderr.read()
 
155
        proc.stderr.close()
 
156
        proc.stdout.read()
 
157
        proc.stdout.close()
 
158
    proc.wait()
 
159
    if proc.returncode == 12:
 
160
        raise RsyncStreamIO()
 
161
    elif proc.returncode == 23:
 
162
        raise RsyncNoFile(source)
 
163
    elif proc.returncode != 0:
 
164
        raise RsyncUnknownStatus(proc.returncode)
 
165
    return cmd
 
166
 
 
167
 
 
168
def rsync_ls(source, ssh=False, silent=True):
 
169
    cmd = ["rsync"]
 
170
    if ssh:
 
171
        cmd.extend(('-e', 'ssh'))
 
172
    cmd.append(source)
 
173
    if silent:
 
174
        stderr = PIPE
 
175
    else:
 
176
        stderr = None
 
177
    proc = Popen(cmd, stderr=stderr, stdout=PIPE)
 
178
    result = proc.stdout.read()
 
179
    proc.stdout.close()
 
180
    if silent:
 
181
        proc.stderr.read()
 
182
        proc.stderr.close()
 
183
    proc.wait()
 
184
    if proc.returncode == 12:
 
185
        raise RsyncStreamIO()
 
186
    elif proc.returncode == 23:
 
187
        raise RsyncNoFile(source)
 
188
    elif proc.returncode != 0:
 
189
        raise RsyncUnknownStatus(proc.returncode)
 
190
    return [l.split(' ')[-1].rstrip('\n') for l in result.splitlines(True)]
 
191
 
 
192
exclusions = ('.bzr/x-push-data', '.bzr/parent', '.bzr/x-pull-data', 
 
193
              '.bzr/x-pull', '.bzr/pull', '.bzr/stat-cache',
 
194
              '.bzr/x-rsync-data', '.bzr/basis-inventory', 
 
195
              '.bzr/inventory.backup.weave')
 
196
 
 
197
 
 
198
def read_revision_history(fname):
 
199
    return [l.rstrip('\r\n') for l in
 
200
            codecs.open(fname, 'rb', 'utf-8').readlines()]
 
201
 
 
202
class RsyncNoFile(Exception):
 
203
    def __init__(self, path):
 
204
        Exception.__init__(self, "No such file %s" % path)
 
205
 
 
206
class RsyncStreamIO(Exception):
 
207
    def __init__(self):
 
208
        Exception.__init__(self, "Error in rsync protocol data stream.")
 
209
 
 
210
def get_revision_history(location):
 
211
    tempdir = tempfile.mkdtemp('push')
 
212
    try:
 
213
        history_fname = os.path.join(tempdir, 'revision-history')
 
214
        cmd = rsync(location+'.bzr/revision-history', history_fname,
 
215
                    silent=True)
 
216
        history = read_revision_history(history_fname)
 
217
    finally:
 
218
        shutil.rmtree(tempdir)
 
219
    return history
 
220
 
 
221
def history_subset(location, branch):
 
222
    remote_history = get_revision_history(location)
 
223
    local_history = branch.revision_history()
 
224
    if len(remote_history) > len(local_history):
 
225
        return False
 
226
    for local, remote in zip(remote_history, local_history):
 
227
        if local != remote:
 
228
            return False 
 
229
    return True
 
230
 
 
231
def empty_or_absent(location):
 
232
    try:
 
233
        files = rsync_ls(location)
 
234
        return files == ['.']
 
235
    except RsyncNoFile:
 
236
        return True
 
237
 
 
238
def push(tree, location=None, overwrite=False, working_tree=True):
 
239
    push_location = get_push_data(tree)
 
240
    if location is not None:
 
241
        if not location.endswith('/'):
 
242
            location += '/'
 
243
        push_location = location
 
244
    
 
245
    if push_location is None:
 
246
        if tree.branch.get_push_location() is None:
 
247
            raise BzrCommandError("No push location known or specified.")
 
248
        else:
 
249
            raise bzrlib.errors.MustUseDecorated
 
250
 
 
251
    if push_location.find('://') != -1:
 
252
        raise bzrlib.errors.MustUseDecorated
 
253
 
 
254
    if push_location.find(':') == -1:
 
255
        raise bzrlib.errors.MustUseDecorated
 
256
 
 
257
    clean, non_source = is_clean(tree)
 
258
    if not clean:
 
259
        print """Error: This tree has uncommitted changes or unknown (?) files.
 
260
Use "bzr status" to list them."""
 
261
        sys.exit(1)
 
262
    if working_tree:
 
263
        final_exclusions = non_source[:]
 
264
    else:
 
265
        wt = tree
 
266
        final_exclusions = []
 
267
        for path, status, kind, file_id, entry in wt.list_files():
 
268
            final_exclusions.append(path)
 
269
 
 
270
    final_exclusions.extend(exclusions)
 
271
    if not overwrite:
 
272
        try:
 
273
            if not history_subset(push_location, tree.branch):
 
274
                raise bzrlib.errors.BzrCommandError("Local branch is not a"
 
275
                                                    " newer version of remote"
 
276
                                                    " branch.")
 
277
        except RsyncNoFile:
 
278
            if not empty_or_absent(push_location):
 
279
                raise bzrlib.errors.BzrCommandError("Remote location is not a"
 
280
                                                    " bzr branch (or empty"
 
281
                                                    " directory)")
 
282
        except RsyncStreamIO:
 
283
            raise bzrlib.errors.BzrCommandError("Rsync could not use the"
 
284
                " specified location.  Please ensure that"
 
285
                ' "%s" is of the form "machine:/path".' % push_location)
 
286
    print "Pushing to %s" % push_location
 
287
    rsync(tree.basedir+'/', push_location, ssh=True, 
 
288
          excludes=final_exclusions)
 
289
 
 
290
    set_push_data(tree, push_location)
 
291
 
 
292
 
 
293
def short_committer(committer):
 
294
    new_committer = re.sub('<.*>', '', committer).strip(' ')
 
295
    if len(new_committer) < 2:
 
296
        return committer
 
297
    return new_committer
 
298
 
 
299
 
 
300
def run_tests():
 
301
    import doctest
 
302
    result = doctest.testmod()
 
303
    if result[1] > 0:
 
304
        if result[0] == 0:
 
305
            print "All tests passed"
 
306
    else:
 
307
        print "No tests to run"
 
308
if __name__ == "__main__":
 
309
    run_tests()