~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2012-01-20 01:43:54 UTC
  • Revision ID: aaron@aaronbentley.com-20120120014354-z8ezzp1zccf62lt5
Fix shelf compatibility

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
20
28
from bzrlib.errors import (
21
29
    BzrCommandError,
 
30
    BzrError,
22
31
    NotBranchError,
23
32
    NoSuchFile,
24
33
    )
25
34
from bzrlib.bzrdir import BzrDir
26
35
from bzrlib.transport import get_transport
27
36
 
 
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()
 
335
 
28
336
 
29
337
def short_committer(committer):
30
338
    new_committer = re.sub('<.*>', '', committer).strip(' ')