~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2008-11-12 17:09:36 UTC
  • Revision ID: aaron@aaronbentley.com-20081112170936-3dybsv7il8gxhchz
Use bzrlib's getchar

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2009, 2011-2012 Aaron Bentley <aaron@aaronbentley.com>
 
1
# Copyright (C) 2005, 2006, 2007 Aaron Bentley <aaron@aaronbentley.com>
2
2
# Copyright (C) 2007 John Arbash Meinel
3
3
#
4
4
#    This program is free software; you can redistribute it and/or modify
14
14
#    You should have received a copy of the GNU General Public License
15
15
#    along with this program; if not, write to the Free Software
16
16
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
17
import codecs
 
18
import errno
 
19
import os
17
20
import re
 
21
import tempfile
 
22
import shutil
 
23
from subprocess import Popen, PIPE
 
24
import sys
18
25
 
19
 
from bzrlib import urlutils
 
26
import bzrlib
 
27
from bzrlib import revision as _mod_revision, trace, urlutils
 
28
import bzrlib.errors
20
29
from bzrlib.errors import (
21
30
    BzrCommandError,
 
31
    BzrError,
 
32
    ConnectionError,
22
33
    NotBranchError,
23
34
    NoSuchFile,
 
35
    NoWorkingTree,
 
36
    PermissionDenied,
 
37
    UnsupportedFormatError,
 
38
    TransportError,
24
39
    )
25
 
from bzrlib.bzrdir import BzrDir
 
40
from bzrlib.bzrdir import BzrDir, BzrDirFormat
26
41
from bzrlib.transport import get_transport
27
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._transport.put_bytes("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._transport.get('x-push-data').read()
 
82
    except NoSuchFile:
 
83
        return None
 
84
    location = location.decode('utf-8')
 
85
    return location.rstrip('\n')
 
86
 
 
87
"""
 
88
>>> shell_escape('hello')
 
89
'\h\e\l\l\o'
 
90
"""
 
91
def shell_escape(arg):
 
92
    return "".join(['\\'+c for c in arg])
 
93
 
 
94
def safe_system(args):
 
95
    """
 
96
    >>> real_system = os.system
 
97
    >>> os.system = sys.stdout.write
 
98
    >>> safe_system(['a', 'b', 'cd'])
 
99
    \\a \\b \\c\\d
 
100
    >>> os.system = real_system
 
101
    """
 
102
    arg_str = " ".join([shell_escape(a) for a in args])
 
103
    return os.system(arg_str)
 
104
 
 
105
class RsyncUnknownStatus(Exception):
 
106
    def __init__(self, status):
 
107
        Exception.__init__(self, "Unknown status: %d" % status)
 
108
 
 
109
class NoRsync(Exception):
 
110
    def __init__(self, rsync_name):
 
111
        Exception.__init__(self, "%s not found." % rsync_name)
 
112
 
 
113
 
 
114
def rsync(source, target, ssh=False, excludes=(), silent=False,
 
115
          rsync_name="rsync"):
 
116
    cmd = [rsync_name, "-av", "--delete"]
 
117
    if ssh:
 
118
        cmd.extend(('-e', 'ssh'))
 
119
    if len(excludes) > 0:
 
120
        cmd.extend(('--exclude-from', '-'))
 
121
    cmd.extend((source, target))
 
122
    if silent:
 
123
        stderr = PIPE
 
124
        stdout = PIPE
 
125
    else:
 
126
        stderr = None
 
127
        stdout = None
 
128
    try:
 
129
        proc = Popen(cmd, stdin=PIPE, stderr=stderr, stdout=stdout)
 
130
    except OSError, e:
 
131
        if e.errno == errno.ENOENT:
 
132
            raise NoRsync(rsync_name)
 
133
 
 
134
    proc.stdin.write('\n'.join(excludes)+'\n')
 
135
    proc.stdin.close()
 
136
    if silent:
 
137
        proc.stderr.read()
 
138
        proc.stderr.close()
 
139
        proc.stdout.read()
 
140
        proc.stdout.close()
 
141
    proc.wait()
 
142
    if proc.returncode == 12:
 
143
        raise RsyncStreamIO()
 
144
    elif proc.returncode == 23:
 
145
        raise RsyncNoFile(source)
 
146
    elif proc.returncode != 0:
 
147
        raise RsyncUnknownStatus(proc.returncode)
 
148
    return cmd
 
149
 
 
150
 
 
151
def rsync_ls(source, ssh=False, silent=True):
 
152
    cmd = ["rsync"]
 
153
    if ssh:
 
154
        cmd.extend(('-e', 'ssh'))
 
155
    cmd.append(source)
 
156
    if silent:
 
157
        stderr = PIPE
 
158
    else:
 
159
        stderr = None
 
160
    proc = Popen(cmd, stderr=stderr, stdout=PIPE)
 
161
    result = proc.stdout.read()
 
162
    proc.stdout.close()
 
163
    if silent:
 
164
        proc.stderr.read()
 
165
        proc.stderr.close()
 
166
    proc.wait()
 
167
    if proc.returncode == 12:
 
168
        raise RsyncStreamIO()
 
169
    elif proc.returncode == 23:
 
170
        raise RsyncNoFile(source)
 
171
    elif proc.returncode != 0:
 
172
        raise RsyncUnknownStatus(proc.returncode)
 
173
    return [l.split(' ')[-1].rstrip('\n') for l in result.splitlines(True)]
 
174
 
 
175
exclusions = ('.bzr/x-push-data', '.bzr/branch/x-push/data', '.bzr/parent',
 
176
              '.bzr/branch/parent', '.bzr/x-pull-data', '.bzr/x-pull',
 
177
              '.bzr/pull', '.bzr/stat-cache', '.bzr/x-rsync-data',
 
178
              '.bzr/basis-inventory', '.bzr/inventory.backup.weave')
 
179
 
 
180
 
 
181
def read_revision_history(fname):
 
182
    return [l.rstrip('\r\n') for l in
 
183
            codecs.open(fname, 'rb', 'utf-8').readlines()]
 
184
 
 
185
 
 
186
def read_revision_info(path):
 
187
    """Parse a last_revision file to determine revision_info"""
 
188
    line = open(path, 'rb').readlines()[0].strip('\n')
 
189
    revno, revision_id = line.split(' ', 1)
 
190
    revno = int(revno)
 
191
    return revno, revision_id
 
192
 
 
193
 
 
194
class RsyncNoFile(Exception):
 
195
    def __init__(self, path):
 
196
        Exception.__init__(self, "No such file %s" % path)
 
197
 
 
198
class RsyncStreamIO(Exception):
 
199
    def __init__(self):
 
200
        Exception.__init__(self, "Error in rsync protocol data stream.")
 
201
 
 
202
 
 
203
class NotStandalone(BzrError):
 
204
 
 
205
    _fmt = '%(location)s is not a standalone tree.'
 
206
    _internal = False
 
207
 
 
208
    def __init__(self, location):
 
209
        BzrError.__init__(self, location=location)
 
210
 
 
211
 
 
212
def get_revision_history(location, _rsync):
 
213
    tempdir = tempfile.mkdtemp('push')
 
214
    my_rsync = _rsync
 
215
    if my_rsync is None:
 
216
        my_rsync = rsync
 
217
    try:
 
218
        history_fname = os.path.join(tempdir, 'revision-history')
 
219
        try:
 
220
            cmd = my_rsync(location+'.bzr/revision-history', history_fname,
 
221
                        silent=True)
 
222
        except RsyncNoFile:
 
223
            cmd = rsync(location+'.bzr/branch/revision-history', history_fname,
 
224
                        silent=True)
 
225
        history = read_revision_history(history_fname)
 
226
    finally:
 
227
        shutil.rmtree(tempdir)
 
228
    return history
 
229
 
 
230
 
 
231
def get_revision_info(location, _rsync):
 
232
    """Get the revsision_info for an rsync-able branch"""
 
233
    tempdir = tempfile.mkdtemp('push')
 
234
    my_rsync = _rsync
 
235
    if my_rsync is None:
 
236
        my_rsync = rsync
 
237
    try:
 
238
        info_fname = os.path.join(tempdir, 'last-revision')
 
239
        cmd = rsync(location+'.bzr/branch/last-revision', info_fname,
 
240
                    silent=True)
 
241
        return read_revision_info(info_fname)
 
242
    finally:
 
243
        shutil.rmtree(tempdir)
 
244
 
 
245
 
 
246
def history_subset(location, branch, _rsync=None):
 
247
    local_history = branch.revision_history()
 
248
    try:
 
249
        remote_history = get_revision_history(location, _rsync)
 
250
    except RsyncNoFile:
 
251
        revno, revision_id = get_revision_info(location, _rsync)
 
252
        if revision_id == _mod_revision.NULL_REVISION:
 
253
            return True
 
254
        return bool(revision_id.decode('utf-8') in local_history)
 
255
    else:
 
256
        if len(remote_history) > len(local_history):
 
257
            return False
 
258
        for local, remote in zip(remote_history, local_history):
 
259
            if local != remote:
 
260
                return False
 
261
        return True
 
262
 
 
263
 
 
264
def empty_or_absent(location):
 
265
    try:
 
266
        files = rsync_ls(location)
 
267
        return files == ['.']
 
268
    except RsyncNoFile:
 
269
        return True
 
270
 
 
271
def rspush(tree, location=None, overwrite=False, working_tree=True,
 
272
    _rsync=None):
 
273
    tree.lock_write()
 
274
    try:
 
275
        my_rsync = _rsync
 
276
        if my_rsync is None:
 
277
            my_rsync = rsync
 
278
        if (tree.bzrdir.root_transport.base !=
 
279
            tree.branch.bzrdir.root_transport.base):
 
280
            raise NotStandalone(tree.bzrdir.root_transport.base)
 
281
        if (tree.branch.get_bound_location() is not None):
 
282
            raise NotStandalone(tree.bzrdir.root_transport.base)
 
283
        if (tree.branch.repository.is_shared()):
 
284
            raise NotStandalone(tree.bzrdir.root_transport.base)
 
285
        push_location = get_push_data(tree)
 
286
        if location is not None:
 
287
            if not location.endswith('/'):
 
288
                location += '/'
 
289
            push_location = location
 
290
 
 
291
        if push_location is None:
 
292
            raise BzrCommandError("No rspush location known or specified.")
 
293
 
 
294
        if (push_location.find('::') != -1):
 
295
            usessh=False
 
296
        else:
 
297
            usessh=True
 
298
 
 
299
        if (push_location.find('://') != -1 or
 
300
            push_location.find(':') == -1):
 
301
            raise BzrCommandError("Invalid rsync path %r." % push_location)
 
302
 
 
303
        if working_tree:
 
304
            clean, non_source = is_clean(tree)
 
305
            if not clean:
 
306
                raise bzrlib.errors.BzrCommandError(
 
307
                    'This tree has uncommitted changes or unknown'
 
308
                    ' (?) files.  Use "bzr status" to list them.')
 
309
                sys.exit(1)
 
310
            final_exclusions = non_source[:]
 
311
        else:
 
312
            wt = tree
 
313
            final_exclusions = []
 
314
            for path, status, kind, file_id, entry in wt.list_files():
 
315
                final_exclusions.append(path)
 
316
 
 
317
        final_exclusions.extend(exclusions)
 
318
        if not overwrite:
 
319
            try:
 
320
                if not history_subset(push_location, tree.branch,
 
321
                                      _rsync=my_rsync):
 
322
                    raise bzrlib.errors.BzrCommandError(
 
323
                        "Local branch is not a newer version of remote"
 
324
                        " branch.")
 
325
            except RsyncNoFile:
 
326
                if not empty_or_absent(push_location):
 
327
                    raise bzrlib.errors.BzrCommandError(
 
328
                        "Remote location is not a bzr branch (or empty"
 
329
                        " directory)")
 
330
            except RsyncStreamIO:
 
331
                raise bzrlib.errors.BzrCommandError("Rsync could not use the"
 
332
                    " specified location.  Please ensure that"
 
333
                    ' "%s" is of the form "machine:/path".' % push_location)
 
334
        trace.note("Pushing to %s", push_location)
 
335
        my_rsync(tree.basedir+'/', push_location, ssh=usessh,
 
336
                 excludes=final_exclusions)
 
337
 
 
338
        set_push_data(tree, push_location)
 
339
    finally:
 
340
        tree.unlock()
 
341
 
28
342
 
29
343
def short_committer(committer):
30
344
    new_committer = re.sub('<.*>', '', committer).strip(' ')
41
355
    t._remote_path = lambda x: t.base
42
356
    try:
43
357
        lines = t.get('')
44
 
    except NoSuchFile:
 
358
    except bzrlib.errors.NoSuchFile:
45
359
        return
46
360
    expr = re.compile('<a[^>]*href="([^>]*)\/"[^>]*>', flags=re.I)
47
361
    for line in lines:
94
408
def open_from_url(location):
95
409
    location = urlutils.normalize_url(location)
96
410
    dirname, basename = urlutils.split(location)
97
 
    if location.endswith('/') and not basename.endswith('/'):
98
 
        basename += '/'
99
411
    return get_transport(dirname).get(basename)
100
412
 
101
413