~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2007-12-17 13:11:59 UTC
  • Revision ID: aaron.bentley@utoronto.ca-20071217131159-dqve0ry2kvci7o2a
Received bug report

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006, 2007 Aaron Bentley <aaron.bentley@utoronto.ca>
 
2
# Copyright (C) 2007 John Arbash Meinel
 
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
from bzrlib import trace, urlutils
 
28
import bzrlib.errors
 
29
from bzrlib.errors import (
 
30
    BzrCommandError,
 
31
    BzrError,
 
32
    ConnectionError,
 
33
    NotBranchError,
 
34
    NoSuchFile,
 
35
    NoWorkingTree,
 
36
    PermissionDenied,
 
37
    UnsupportedFormatError,
 
38
    TransportError,
 
39
    )
 
40
from bzrlib.bzrdir import BzrDir, BzrDirFormat
 
41
from bzrlib.transport import get_transport
 
42
 
 
43
def temp_tree():
 
44
    dirname = tempfile.mkdtemp("temp-branch")
 
45
    return BzrDir.create_standalone_workingtree(dirname)
 
46
 
 
47
def rm_tree(tree):
 
48
    shutil.rmtree(tree.basedir)
 
49
 
 
50
def is_clean(cur_tree):
 
51
    """
 
52
    Return true if no files are modifed or unknown
 
53
    """
 
54
    old_tree = cur_tree.basis_tree()
 
55
    new_tree = cur_tree
 
56
    non_source = []
 
57
    new_tree.lock_read()
 
58
    try:
 
59
        for path, file_class, kind, file_id, entry in new_tree.list_files():
 
60
            if file_class in ('?', 'I'):
 
61
                non_source.append(path)
 
62
        delta = new_tree.changes_from(old_tree, want_unchanged=False)
 
63
    finally:
 
64
        new_tree.unlock()
 
65
    return not delta.has_changed(), non_source
 
66
 
 
67
def set_push_data(tree, location):
 
68
    tree.branch.control_files.put_utf8("x-push-data", "%s\n" % location)
 
69
 
 
70
def get_push_data(tree):
 
71
    """
 
72
    >>> tree = temp_tree()
 
73
    >>> get_push_data(tree) is None
 
74
    True
 
75
    >>> set_push_data(tree, 'http://somewhere')
 
76
    >>> get_push_data(tree)
 
77
    u'http://somewhere'
 
78
    >>> rm_tree(tree)
 
79
    """
 
80
    try:
 
81
        location = tree.branch.control_files.get_utf8('x-push-data').read()
 
82
    except NoSuchFile:
 
83
        return None
 
84
    return location.rstrip('\n')
 
85
 
 
86
"""
 
87
>>> shell_escape('hello')
 
88
'\h\e\l\l\o'
 
89
"""
 
90
def shell_escape(arg):
 
91
    return "".join(['\\'+c for c in arg])
 
92
 
 
93
def safe_system(args):
 
94
    """
 
95
    >>> real_system = os.system
 
96
    >>> os.system = sys.stdout.write
 
97
    >>> safe_system(['a', 'b', 'cd'])
 
98
    \\a \\b \\c\\d
 
99
    >>> os.system = real_system
 
100
    """
 
101
    arg_str = " ".join([shell_escape(a) for a in args])
 
102
    return os.system(arg_str)
 
103
 
 
104
class RsyncUnknownStatus(Exception):
 
105
    def __init__(self, status):
 
106
        Exception.__init__(self, "Unknown status: %d" % status)
 
107
 
 
108
class NoRsync(Exception):
 
109
    def __init__(self, rsync_name):
 
110
        Exception.__init__(self, "%s not found." % rsync_name)
 
111
 
 
112
 
 
113
def rsync(source, target, ssh=False, excludes=(), silent=False,
 
114
          rsync_name="rsync"):
 
115
    """
 
116
    >>> new_dir = tempfile.mkdtemp()
 
117
    >>> old_dir = os.getcwd()
 
118
    >>> os.chdir(new_dir)
 
119
    >>> rsync("a", "b", silent=True)
 
120
    Traceback (most recent call last):
 
121
    RsyncNoFile: No such file...
 
122
    >>> rsync(new_dir + "/a", new_dir + "/b", excludes=("*.py",), silent=True)
 
123
    Traceback (most recent call last):
 
124
    RsyncNoFile: No such file...
 
125
    >>> rsync(new_dir + "/a", new_dir + "/b", excludes=("*.py",), silent=True, rsync_name="rsyncc")
 
126
    Traceback (most recent call last):
 
127
    NoRsync: rsyncc not found.
 
128
    >>> os.chdir(old_dir)
 
129
    >>> os.rmdir(new_dir)
 
130
    """
 
131
    cmd = [rsync_name, "-av", "--delete"]
 
132
    if ssh:
 
133
        cmd.extend(('-e', 'ssh'))
 
134
    if len(excludes) > 0:
 
135
        cmd.extend(('--exclude-from', '-'))
 
136
    cmd.extend((source, target))
 
137
    if silent:
 
138
        stderr = PIPE
 
139
        stdout = PIPE
 
140
    else:
 
141
        stderr = None
 
142
        stdout = None
 
143
    try:
 
144
        proc = Popen(cmd, stdin=PIPE, stderr=stderr, stdout=stdout)
 
145
    except OSError, e:
 
146
        if e.errno == errno.ENOENT:
 
147
            raise NoRsync(rsync_name)
 
148
 
 
149
    proc.stdin.write('\n'.join(excludes)+'\n')
 
150
    proc.stdin.close()
 
151
    if silent:
 
152
        proc.stderr.read()
 
153
        proc.stderr.close()
 
154
        proc.stdout.read()
 
155
        proc.stdout.close()
 
156
    proc.wait()
 
157
    if proc.returncode == 12:
 
158
        raise RsyncStreamIO()
 
159
    elif proc.returncode == 23:
 
160
        raise RsyncNoFile(source)
 
161
    elif proc.returncode != 0:
 
162
        raise RsyncUnknownStatus(proc.returncode)
 
163
    return cmd
 
164
 
 
165
 
 
166
def rsync_ls(source, ssh=False, silent=True):
 
167
    cmd = ["rsync"]
 
168
    if ssh:
 
169
        cmd.extend(('-e', 'ssh'))
 
170
    cmd.append(source)
 
171
    if silent:
 
172
        stderr = PIPE
 
173
    else:
 
174
        stderr = None
 
175
    proc = Popen(cmd, stderr=stderr, stdout=PIPE)
 
176
    result = proc.stdout.read()
 
177
    proc.stdout.close()
 
178
    if silent:
 
179
        proc.stderr.read()
 
180
        proc.stderr.close()
 
181
    proc.wait()
 
182
    if proc.returncode == 12:
 
183
        raise RsyncStreamIO()
 
184
    elif proc.returncode == 23:
 
185
        raise RsyncNoFile(source)
 
186
    elif proc.returncode != 0:
 
187
        raise RsyncUnknownStatus(proc.returncode)
 
188
    return [l.split(' ')[-1].rstrip('\n') for l in result.splitlines(True)]
 
189
 
 
190
exclusions = ('.bzr/x-push-data', '.bzr/branch/x-push/data', '.bzr/parent',
 
191
              '.bzr/branch/parent', '.bzr/x-pull-data', '.bzr/x-pull',
 
192
              '.bzr/pull', '.bzr/stat-cache', '.bzr/x-rsync-data',
 
193
              '.bzr/basis-inventory', '.bzr/inventory.backup.weave')
 
194
 
 
195
 
 
196
def read_revision_history(fname):
 
197
    return [l.rstrip('\r\n') for l in
 
198
            codecs.open(fname, 'rb', 'utf-8').readlines()]
 
199
 
 
200
class RsyncNoFile(Exception):
 
201
    def __init__(self, path):
 
202
        Exception.__init__(self, "No such file %s" % path)
 
203
 
 
204
class RsyncStreamIO(Exception):
 
205
    def __init__(self):
 
206
        Exception.__init__(self, "Error in rsync protocol data stream.")
 
207
 
 
208
 
 
209
class NotStandalone(BzrError):
 
210
 
 
211
    _format = '%(location) is not a standalone tree.'
 
212
    _internal = False
 
213
 
 
214
    def __init__(self, location):
 
215
        BzrError.__init__(self, location=location)
 
216
 
 
217
 
 
218
def get_revision_history(location, _rsync):
 
219
    tempdir = tempfile.mkdtemp('push')
 
220
    my_rsync = _rsync
 
221
    if my_rsync is None:
 
222
        my_rsync = rsync
 
223
    try:
 
224
        history_fname = os.path.join(tempdir, 'revision-history')
 
225
        try:
 
226
            cmd = my_rsync(location+'.bzr/revision-history', history_fname,
 
227
                        silent=True)
 
228
        except RsyncNoFile:
 
229
            cmd = rsync(location+'.bzr/branch/revision-history', history_fname,
 
230
                        silent=True)
 
231
        history = read_revision_history(history_fname)
 
232
    finally:
 
233
        shutil.rmtree(tempdir)
 
234
    return history
 
235
 
 
236
 
 
237
def history_subset(location, branch, _rsync=None):
 
238
    remote_history = get_revision_history(location, _rsync)
 
239
    local_history = branch.revision_history()
 
240
    if len(remote_history) > len(local_history):
 
241
        return False
 
242
    for local, remote in zip(remote_history, local_history):
 
243
        if local != remote:
 
244
            return False
 
245
    return True
 
246
 
 
247
def empty_or_absent(location):
 
248
    try:
 
249
        files = rsync_ls(location)
 
250
        return files == ['.']
 
251
    except RsyncNoFile:
 
252
        return True
 
253
 
 
254
def rspush(tree, location=None, overwrite=False, working_tree=True,
 
255
    _rsync=None):
 
256
    tree.lock_write()
 
257
    try:
 
258
        my_rsync = _rsync
 
259
        if my_rsync is None:
 
260
            my_rsync = rsync
 
261
        if (tree.bzrdir.root_transport.base !=
 
262
            tree.branch.bzrdir.root_transport.base):
 
263
            raise NotStandalone(tree.bzrdir.root_transport.base)
 
264
        if (tree.branch.get_bound_location() is not None):
 
265
            raise NotStandalone(tree.bzrdir.root_transport.base)
 
266
        if (tree.branch.repository.is_shared()):
 
267
            raise NotStandalone(tree.bzrdir.root_transport.base)
 
268
        push_location = get_push_data(tree)
 
269
        if location is not None:
 
270
            if not location.endswith('/'):
 
271
                location += '/'
 
272
            push_location = location
 
273
 
 
274
        if push_location is None:
 
275
            raise BzrCommandError("No rspush location known or specified.")
 
276
 
 
277
        if (push_location.find('::') != -1):
 
278
            usessh=False
 
279
        else:
 
280
            usessh=True
 
281
 
 
282
        if (push_location.find('://') != -1 or
 
283
            push_location.find(':') == -1):
 
284
            raise BzrCommandError("Invalid rsync path %r." % push_location)
 
285
 
 
286
        if working_tree:
 
287
            clean, non_source = is_clean(tree)
 
288
            if not clean:
 
289
                raise bzrlib.errors.BzrCommandError(
 
290
                    'This tree has uncommitted changes or unknown'
 
291
                    ' (?) files.  Use "bzr status" to list them.')
 
292
                sys.exit(1)
 
293
            final_exclusions = non_source[:]
 
294
        else:
 
295
            wt = tree
 
296
            final_exclusions = []
 
297
            for path, status, kind, file_id, entry in wt.list_files():
 
298
                final_exclusions.append(path)
 
299
 
 
300
        final_exclusions.extend(exclusions)
 
301
        if not overwrite:
 
302
            try:
 
303
                if not history_subset(push_location, tree.branch,
 
304
                                      _rsync=my_rsync):
 
305
                    raise bzrlib.errors.BzrCommandError(
 
306
                        "Local branch is not a newer version of remote"
 
307
                        " branch.")
 
308
            except RsyncNoFile:
 
309
                if not empty_or_absent(push_location):
 
310
                    raise bzrlib.errors.BzrCommandError(
 
311
                        "Remote location is not a bzr branch (or empty"
 
312
                        " directory)")
 
313
            except RsyncStreamIO:
 
314
                raise bzrlib.errors.BzrCommandError("Rsync could not use the"
 
315
                    " specified location.  Please ensure that"
 
316
                    ' "%s" is of the form "machine:/path".' % push_location)
 
317
        trace.note("Pushing to %s", push_location)
 
318
        my_rsync(tree.basedir+'/', push_location, ssh=usessh,
 
319
              excludes=final_exclusions)
 
320
 
 
321
        set_push_data(tree, push_location)
 
322
    finally:
 
323
        tree.unlock()
 
324
 
 
325
 
 
326
def short_committer(committer):
 
327
    new_committer = re.sub('<.*>', '', committer).strip(' ')
 
328
    if len(new_committer) < 2:
 
329
        return committer
 
330
    return new_committer
 
331
 
 
332
 
 
333
def apache_ls(t):
 
334
    """Screen-scrape Apache listings"""
 
335
    apache_dir = '<img border="0" src="/icons/folder.gif" alt="[dir]">'\
 
336
        ' <a href="'
 
337
    t = t.clone()
 
338
    t._remote_path = lambda x: t.base
 
339
    lines = t.get('')
 
340
    expr = re.compile('<a[^>]*href="([^>]*)"[^>]*>', flags=re.I)
 
341
    for line in lines:
 
342
        match = expr.search(line)
 
343
        if match is None:
 
344
            continue
 
345
        url = match.group(1)
 
346
        if url.startswith('http://') or url.startswith('/') or '../' in url:
 
347
            continue
 
348
        if '?' in url:
 
349
            continue
 
350
        yield url.rstrip('/')
 
351
 
 
352
 
 
353
def iter_branches(t, lister=None):
 
354
    """Iterate through all the branches under a transport"""
 
355
    for bzrdir in iter_bzrdirs(t, lister):
 
356
        try:
 
357
            branch = bzrdir.open_branch()
 
358
            if branch.bzrdir is bzrdir:
 
359
                yield branch
 
360
        except (NotBranchError, UnsupportedFormatError):
 
361
            pass
 
362
 
 
363
 
 
364
def iter_branch_tree(t, lister=None):
 
365
    for bzrdir in iter_bzrdirs(t, lister):
 
366
        try:
 
367
            wt = bzrdir.open_workingtree()
 
368
            yield wt.branch, wt
 
369
        except NoWorkingTree, UnsupportedFormatError:
 
370
            try:
 
371
                branch = bzrdir.open_branch()
 
372
                if branch.bzrdir is bzrdir:
 
373
                    yield branch, None
 
374
            except (NotBranchError, UnsupportedFormatError):
 
375
                continue
 
376
 
 
377
 
 
378
def iter_bzrdirs(t, lister=None):
 
379
    if lister is None:
 
380
        def lister(t):
 
381
            return t.list_dir('.')
 
382
    try:
 
383
        bzrdir = bzrdir_from_transport(t)
 
384
        yield bzrdir
 
385
    except (ConnectionError):
 
386
        raise
 
387
    except (NotBranchError, UnsupportedFormatError, TransportError,
 
388
            PermissionDenied):
 
389
        pass
 
390
    try:
 
391
        for directory in lister(t):
 
392
            if directory == ".bzr":
 
393
                continue
 
394
            try:
 
395
                subt = t.clone(directory)
 
396
            except UnicodeDecodeError:
 
397
                continue
 
398
            for bzrdir in iter_bzrdirs(subt, lister):
 
399
                yield bzrdir
 
400
    except (NoSuchFile, PermissionDenied, TransportError):
 
401
        pass
 
402
 
 
403
 
 
404
def bzrdir_from_transport(t):
 
405
    """Open a bzrdir from a transport (not a location)"""
 
406
    format = BzrDirFormat.find_format(t)
 
407
    BzrDir._check_supported(format, False)
 
408
    return format.open(t)
 
409
 
 
410
 
 
411
def open_from_url(location):
 
412
    location = urlutils.normalize_url(location)
 
413
    dirname, basename = urlutils.split(location)
 
414
    return get_transport(dirname).get(basename)
 
415
 
 
416
 
 
417
def run_tests():
 
418
    import doctest
 
419
    result = doctest.testmod()
 
420
    if result[1] > 0:
 
421
        if result[0] == 0:
 
422
            print "All tests passed"
 
423
    else:
 
424
        print "No tests to run"
 
425
if __name__ == "__main__":
 
426
    run_tests()