~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2013-08-20 03:02:43 UTC
  • Revision ID: aaron@aaronbentley.com-20130820030243-r8v1xfbcnd8f10p4
Fix zap command for 2.6/7

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007 Aaron Bentley <aaron@aaronbentley.com>
 
1
# Copyright (C) 2005-2009, 2011-2013 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
from contextlib import contextmanager
20
18
import re
21
 
import tempfile
22
 
import shutil
23
 
from subprocess import Popen, PIPE
24
 
import sys
25
19
 
26
 
import bzrlib
27
 
from bzrlib import revision as _mod_revision, trace, urlutils
 
20
from bzrlib import urlutils
28
21
from bzrlib.errors import (
29
22
    BzrCommandError,
30
 
    BzrError,
31
23
    NotBranchError,
32
24
    NoSuchFile,
33
25
    )
34
26
from bzrlib.bzrdir import BzrDir
35
27
from bzrlib.transport import get_transport
36
28
 
37
 
def temp_tree():
38
 
    dirname = tempfile.mkdtemp("temp-branch")
39
 
    return BzrDir.create_standalone_workingtree(dirname)
40
 
 
41
 
def rm_tree(tree):
42
 
    shutil.rmtree(tree.basedir)
43
 
 
44
 
def is_clean(cur_tree):
45
 
    """
46
 
    Return true if no files are modifed or unknown
47
 
    """
48
 
    old_tree = cur_tree.basis_tree()
49
 
    new_tree = cur_tree
50
 
    non_source = []
51
 
    new_tree.lock_read()
52
 
    try:
53
 
        for path, file_class, kind, file_id, entry in new_tree.list_files():
54
 
            if file_class in ('?', 'I'):
55
 
                non_source.append(path)
56
 
        delta = new_tree.changes_from(old_tree, want_unchanged=False)
57
 
    finally:
58
 
        new_tree.unlock()
59
 
    return not delta.has_changed(), non_source
60
 
 
61
 
def set_push_data(tree, location):
62
 
    tree.branch._transport.put_bytes("x-push-data", "%s\n" % location)
63
 
 
64
 
def get_push_data(tree):
65
 
    """
66
 
    >>> tree = temp_tree()
67
 
    >>> get_push_data(tree) is None
68
 
    True
69
 
    >>> set_push_data(tree, 'http://somewhere')
70
 
    >>> get_push_data(tree)
71
 
    u'http://somewhere'
72
 
    >>> rm_tree(tree)
73
 
    """
74
 
    try:
75
 
        location = tree.branch._transport.get('x-push-data').read()
76
 
    except NoSuchFile:
77
 
        return None
78
 
    location = location.decode('utf-8')
79
 
    return location.rstrip('\n')
80
 
 
81
 
"""
82
 
>>> shell_escape('hello')
83
 
'\h\e\l\l\o'
84
 
"""
85
 
def shell_escape(arg):
86
 
    return "".join(['\\'+c for c in arg])
87
 
 
88
 
def safe_system(args):
89
 
    """
90
 
    >>> real_system = os.system
91
 
    >>> os.system = sys.stdout.write
92
 
    >>> safe_system(['a', 'b', 'cd'])
93
 
    \\a \\b \\c\\d
94
 
    >>> os.system = real_system
95
 
    """
96
 
    arg_str = " ".join([shell_escape(a) for a in args])
97
 
    return os.system(arg_str)
98
 
 
99
 
class RsyncUnknownStatus(Exception):
100
 
    def __init__(self, status):
101
 
        Exception.__init__(self, "Unknown status: %d" % status)
102
 
 
103
 
class NoRsync(Exception):
104
 
    def __init__(self, rsync_name):
105
 
        Exception.__init__(self, "%s not found." % rsync_name)
106
 
 
107
 
 
108
 
def rsync(source, target, ssh=False, excludes=(), silent=False,
109
 
          rsync_name="rsync"):
110
 
    cmd = [rsync_name, "-av", "--delete"]
111
 
    if ssh:
112
 
        cmd.extend(('-e', 'ssh'))
113
 
    if len(excludes) > 0:
114
 
        cmd.extend(('--exclude-from', '-'))
115
 
    cmd.extend((source, target))
116
 
    if silent:
117
 
        stderr = PIPE
118
 
        stdout = PIPE
119
 
    else:
120
 
        stderr = None
121
 
        stdout = None
122
 
    try:
123
 
        proc = Popen(cmd, stdin=PIPE, stderr=stderr, stdout=stdout)
124
 
    except OSError, e:
125
 
        if e.errno == errno.ENOENT:
126
 
            raise NoRsync(rsync_name)
127
 
 
128
 
    proc.stdin.write('\n'.join(excludes)+'\n')
129
 
    proc.stdin.close()
130
 
    if silent:
131
 
        proc.stderr.read()
132
 
        proc.stderr.close()
133
 
        proc.stdout.read()
134
 
        proc.stdout.close()
135
 
    proc.wait()
136
 
    if proc.returncode == 12:
137
 
        raise RsyncStreamIO()
138
 
    elif proc.returncode == 23:
139
 
        raise RsyncNoFile(source)
140
 
    elif proc.returncode != 0:
141
 
        raise RsyncUnknownStatus(proc.returncode)
142
 
    return cmd
143
 
 
144
 
 
145
 
def rsync_ls(source, ssh=False, silent=True):
146
 
    cmd = ["rsync"]
147
 
    if ssh:
148
 
        cmd.extend(('-e', 'ssh'))
149
 
    cmd.append(source)
150
 
    if silent:
151
 
        stderr = PIPE
152
 
    else:
153
 
        stderr = None
154
 
    proc = Popen(cmd, stderr=stderr, stdout=PIPE)
155
 
    result = proc.stdout.read()
156
 
    proc.stdout.close()
157
 
    if silent:
158
 
        proc.stderr.read()
159
 
        proc.stderr.close()
160
 
    proc.wait()
161
 
    if proc.returncode == 12:
162
 
        raise RsyncStreamIO()
163
 
    elif proc.returncode == 23:
164
 
        raise RsyncNoFile(source)
165
 
    elif proc.returncode != 0:
166
 
        raise RsyncUnknownStatus(proc.returncode)
167
 
    return [l.split(' ')[-1].rstrip('\n') for l in result.splitlines(True)]
168
 
 
169
 
exclusions = ('.bzr/x-push-data', '.bzr/branch/x-push/data', '.bzr/parent',
170
 
              '.bzr/branch/parent', '.bzr/x-pull-data', '.bzr/x-pull',
171
 
              '.bzr/pull', '.bzr/stat-cache', '.bzr/x-rsync-data',
172
 
              '.bzr/basis-inventory', '.bzr/inventory.backup.weave')
173
 
 
174
 
 
175
 
def read_revision_history(fname):
176
 
    return [l.rstrip('\r\n') for l in
177
 
            codecs.open(fname, 'rb', 'utf-8').readlines()]
178
 
 
179
 
 
180
 
def read_revision_info(path):
181
 
    """Parse a last_revision file to determine revision_info"""
182
 
    line = open(path, 'rb').readlines()[0].strip('\n')
183
 
    revno, revision_id = line.split(' ', 1)
184
 
    revno = int(revno)
185
 
    return revno, revision_id
186
 
 
187
 
 
188
 
class RsyncNoFile(Exception):
189
 
    def __init__(self, path):
190
 
        Exception.__init__(self, "No such file %s" % path)
191
 
 
192
 
class RsyncStreamIO(Exception):
193
 
    def __init__(self):
194
 
        Exception.__init__(self, "Error in rsync protocol data stream.")
195
 
 
196
 
 
197
 
class NotStandalone(BzrError):
198
 
 
199
 
    _fmt = '%(location)s is not a standalone tree.'
200
 
    _internal = False
201
 
 
202
 
    def __init__(self, location):
203
 
        BzrError.__init__(self, location=location)
204
 
 
205
 
 
206
 
def get_revision_history(location, _rsync):
207
 
    tempdir = tempfile.mkdtemp('push')
208
 
    my_rsync = _rsync
209
 
    if my_rsync is None:
210
 
        my_rsync = rsync
211
 
    try:
212
 
        history_fname = os.path.join(tempdir, 'revision-history')
213
 
        try:
214
 
            cmd = my_rsync(location+'.bzr/revision-history', history_fname,
215
 
                        silent=True)
216
 
        except RsyncNoFile:
217
 
            cmd = rsync(location+'.bzr/branch/revision-history', history_fname,
218
 
                        silent=True)
219
 
        history = read_revision_history(history_fname)
220
 
    finally:
221
 
        shutil.rmtree(tempdir)
222
 
    return history
223
 
 
224
 
 
225
 
def get_revision_info(location, _rsync):
226
 
    """Get the revsision_info for an rsync-able branch"""
227
 
    tempdir = tempfile.mkdtemp('push')
228
 
    my_rsync = _rsync
229
 
    if my_rsync is None:
230
 
        my_rsync = rsync
231
 
    try:
232
 
        info_fname = os.path.join(tempdir, 'last-revision')
233
 
        cmd = rsync(location+'.bzr/branch/last-revision', info_fname,
234
 
                    silent=True)
235
 
        return read_revision_info(info_fname)
236
 
    finally:
237
 
        shutil.rmtree(tempdir)
238
 
 
239
 
 
240
 
def history_subset(location, branch, _rsync=None):
241
 
    local_history = branch.revision_history()
242
 
    try:
243
 
        remote_history = get_revision_history(location, _rsync)
244
 
    except RsyncNoFile:
245
 
        revno, revision_id = get_revision_info(location, _rsync)
246
 
        if revision_id == _mod_revision.NULL_REVISION:
247
 
            return True
248
 
        return bool(revision_id.decode('utf-8') in local_history)
249
 
    else:
250
 
        if len(remote_history) > len(local_history):
251
 
            return False
252
 
        for local, remote in zip(remote_history, local_history):
253
 
            if local != remote:
254
 
                return False
255
 
        return True
256
 
 
257
 
 
258
 
def empty_or_absent(location):
259
 
    try:
260
 
        files = rsync_ls(location)
261
 
        return files == ['.']
262
 
    except RsyncNoFile:
263
 
        return True
264
 
 
265
 
def rspush(tree, location=None, overwrite=False, working_tree=True,
266
 
    _rsync=None):
267
 
    tree.lock_write()
268
 
    try:
269
 
        my_rsync = _rsync
270
 
        if my_rsync is None:
271
 
            my_rsync = rsync
272
 
        if (tree.bzrdir.root_transport.base !=
273
 
            tree.branch.bzrdir.root_transport.base):
274
 
            raise NotStandalone(tree.bzrdir.root_transport.base)
275
 
        if (tree.branch.get_bound_location() is not None):
276
 
            raise NotStandalone(tree.bzrdir.root_transport.base)
277
 
        if (tree.branch.repository.is_shared()):
278
 
            raise NotStandalone(tree.bzrdir.root_transport.base)
279
 
        push_location = get_push_data(tree)
280
 
        if location is not None:
281
 
            if not location.endswith('/'):
282
 
                location += '/'
283
 
            push_location = location
284
 
 
285
 
        if push_location is None:
286
 
            raise BzrCommandError("No rspush location known or specified.")
287
 
 
288
 
        if (push_location.find('::') != -1):
289
 
            usessh=False
290
 
        else:
291
 
            usessh=True
292
 
 
293
 
        if (push_location.find('://') != -1 or
294
 
            push_location.find(':') == -1):
295
 
            raise BzrCommandError("Invalid rsync path %r." % push_location)
296
 
 
297
 
        if working_tree:
298
 
            clean, non_source = is_clean(tree)
299
 
            if not clean:
300
 
                raise BzrCommandError(
301
 
                    'This tree has uncommitted changes or unknown'
302
 
                    ' (?) files.  Use "bzr status" to list them.')
303
 
                sys.exit(1)
304
 
            final_exclusions = non_source[:]
305
 
        else:
306
 
            wt = tree
307
 
            final_exclusions = []
308
 
            for path, status, kind, file_id, entry in wt.list_files():
309
 
                final_exclusions.append(path)
310
 
 
311
 
        final_exclusions.extend(exclusions)
312
 
        if not overwrite:
313
 
            try:
314
 
                if not history_subset(push_location, tree.branch,
315
 
                                      _rsync=my_rsync):
316
 
                    raise BzrCommandError(
317
 
                        "Local branch is not a newer version of remote"
318
 
                        " branch.")
319
 
            except RsyncNoFile:
320
 
                if not empty_or_absent(push_location):
321
 
                    raise BzrCommandError(
322
 
                        "Remote location is not a bzr branch (or empty"
323
 
                        " directory)")
324
 
            except RsyncStreamIO:
325
 
                raise BzrCommandError("Rsync could not use the"
326
 
                    " specified location.  Please ensure that"
327
 
                    ' "%s" is of the form "machine:/path".' % push_location)
328
 
        trace.note("Pushing to %s", push_location)
329
 
        my_rsync(tree.basedir+'/', push_location, ssh=usessh,
330
 
                 excludes=final_exclusions)
331
 
 
332
 
        set_push_data(tree, push_location)
333
 
    finally:
334
 
        tree.unlock()
 
29
 
 
30
@contextmanager
 
31
def read_locked(lockable):
 
32
    """Read-lock a tree, branch or repository in this context."""
 
33
    lockable.lock_read()
 
34
    try:
 
35
        yield lockable
 
36
    finally:
 
37
        lockable.unlock()
335
38
 
336
39
 
337
40
def short_committer(committer):