~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2006-03-07 15:03:29 UTC
  • mfrom: (147.4.30 trunk)
  • mto: (147.4.31 trunk)
  • mto: This revision was merged to the branch mainline in revision 324.
  • Revision ID: abentley@panoramicfeedback.com-20060307150329-b2c79effccfdd961
MergeĀ fromĀ Robert

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2009, 2011-2013 Aaron Bentley <aaron@aaronbentley.com>
2
 
# Copyright (C) 2007 John Arbash Meinel
 
1
# Copyright (C) 2005 Aaron Bentley
 
2
# <aaron.bentley@utoronto.ca>
3
3
#
4
4
#    This program is free software; you can redistribute it and/or modify
5
5
#    it under the terms of the GNU General Public License as published by
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
 
from contextlib import contextmanager
 
17
import codecs
 
18
import errno
 
19
import os
18
20
import re
 
21
import tempfile
 
22
import shutil
 
23
from subprocess import Popen, PIPE
 
24
import sys
19
25
 
20
 
from bzrlib import urlutils
21
 
from bzrlib.errors import (
22
 
    BzrCommandError,
23
 
    NotBranchError,
24
 
    NoSuchFile,
25
 
    )
 
26
import bzrlib
 
27
import bzrlib.errors
 
28
from bzrlib.errors import BzrCommandError
26
29
from bzrlib.bzrdir import BzrDir
27
 
from bzrlib.transport import get_transport
28
 
 
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
 
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(tempdir + "/a", tempdir + "/b", excludes=("*.py",), silent=True) #doctest: +ELLIPSIS
 
125
    Traceback (most recent call last):
 
126
    RsyncNoFile: No such file...
 
127
    >>> rsync(tempdir + "/a", tempdir + "/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')
 
195
 
 
196
 
 
197
def read_revision_history(fname):
 
198
    return [l.rstrip('\r\n') for l in
 
199
            codecs.open(fname, 'rb', 'utf-8').readlines()]
 
200
 
 
201
class RsyncNoFile(Exception):
 
202
    def __init__(self, path):
 
203
        Exception.__init__(self, "No such file %s" % path)
 
204
 
 
205
class RsyncStreamIO(Exception):
 
206
    def __init__(self):
 
207
        Exception.__init__(self, "Error in rsync protocol data stream.")
 
208
 
 
209
def get_revision_history(location):
 
210
    tempdir = tempfile.mkdtemp('push')
 
211
    try:
 
212
        history_fname = os.path.join(tempdir, 'revision-history')
 
213
        cmd = rsync(location+'.bzr/revision-history', history_fname,
 
214
                    silent=True)
 
215
        history = read_revision_history(history_fname)
36
216
    finally:
37
 
        lockable.unlock()
 
217
        shutil.rmtree(tempdir)
 
218
    return history
 
219
 
 
220
def history_subset(location, branch):
 
221
    remote_history = get_revision_history(location)
 
222
    local_history = branch.revision_history()
 
223
    if len(remote_history) > len(local_history):
 
224
        return False
 
225
    for local, remote in zip(remote_history, local_history):
 
226
        if local != remote:
 
227
            return False 
 
228
    return True
 
229
 
 
230
def empty_or_absent(location):
 
231
    try:
 
232
        files = rsync_ls(location)
 
233
        return files == ['.']
 
234
    except RsyncNoFile:
 
235
        return True
 
236
 
 
237
def push(tree, location=None, overwrite=False, working_tree=True):
 
238
    push_location = get_push_data(tree)
 
239
    if location is not None:
 
240
        if not location.endswith('/'):
 
241
            location += '/'
 
242
        push_location = location
 
243
    
 
244
    if push_location is None:
 
245
        if tree.branch.get_push_location() is None:
 
246
            raise BzrCommandError("No push location known or specified.")
 
247
        else:
 
248
            raise bzrlib.errors.MustUseDecorated
 
249
 
 
250
    if push_location.find('://') != -1:
 
251
        raise bzrlib.errors.MustUseDecorated
 
252
 
 
253
    if push_location.find(':') == -1:
 
254
        raise bzrlib.errors.MustUseDecorated
 
255
 
 
256
    clean, non_source = is_clean(tree)
 
257
    if not clean:
 
258
        print """Error: This tree has uncommitted changes or unknown (?) files.
 
259
Use "bzr status" to list them."""
 
260
        sys.exit(1)
 
261
    if working_tree:
 
262
        final_exclusions = non_source[:]
 
263
    else:
 
264
        wt = tree
 
265
        final_exclusions = []
 
266
        for path, status, kind, file_id, entry in wt.list_files():
 
267
            final_exclusions.append(path)
 
268
 
 
269
    final_exclusions.extend(exclusions)
 
270
    if not overwrite:
 
271
        try:
 
272
            if not history_subset(push_location, tree.branch):
 
273
                raise bzrlib.errors.BzrCommandError("Local branch is not a"
 
274
                                                    " newer version of remote"
 
275
                                                    " branch.")
 
276
        except RsyncNoFile:
 
277
            if not empty_or_absent(push_location):
 
278
                raise bzrlib.errors.BzrCommandError("Remote location is not a"
 
279
                                                    " bzr branch (or empty"
 
280
                                                    " directory)")
 
281
        except RsyncStreamIO:
 
282
            raise bzrlib.errors.BzrCommandError("Rsync could not use the"
 
283
                " specified location.  Please ensure that"
 
284
                ' "%s" is of the form "machine:/path".' % push_location)
 
285
    print "Pushing to %s" % push_location
 
286
    rsync(tree.basedir+'/', push_location, ssh=True, 
 
287
          excludes=final_exclusions)
 
288
 
 
289
    set_push_data(tree, push_location)
38
290
 
39
291
 
40
292
def short_committer(committer):
44
296
    return new_committer
45
297
 
46
298
 
47
 
def apache_ls(t):
48
 
    """Screen-scrape Apache listings"""
49
 
    apache_dir = '<img border="0" src="/icons/folder.gif" alt="[dir]">'\
50
 
        ' <a href="'
51
 
    t = t.clone()
52
 
    t._remote_path = lambda x: t.base
53
 
    try:
54
 
        lines = t.get('')
55
 
    except NoSuchFile:
56
 
        return
57
 
    expr = re.compile('<a[^>]*href="([^>]*)\/"[^>]*>', flags=re.I)
58
 
    for line in lines:
59
 
        match = expr.search(line)
60
 
        if match is None:
61
 
            continue
62
 
        url = match.group(1)
63
 
        if url.startswith('http://') or url.startswith('/') or '../' in url:
64
 
            continue
65
 
        if '?' in url:
66
 
            continue
67
 
        yield url.rstrip('/')
68
 
 
69
 
 
70
 
def list_branches(t):
71
 
    def is_inside(branch):
72
 
        return bool(branch.base.startswith(t.base))
73
 
 
74
 
    if t.base.startswith('http://'):
75
 
        def evaluate(bzrdir):
76
 
            try:
77
 
                branch = bzrdir.open_branch()
78
 
                if is_inside(branch):
79
 
                    return True, branch
80
 
                else:
81
 
                    return True, None
82
 
            except NotBranchError:
83
 
                return True, None
84
 
        return [b for b in BzrDir.find_bzrdirs(t, list_current=apache_ls,
85
 
                evaluate=evaluate) if b is not None]
86
 
    elif not t.listable():
87
 
        raise BzrCommandError("Can't list this type of location.")
88
 
    return [b for b in BzrDir.find_branches(t) if is_inside(b)]
89
 
 
90
 
 
91
 
def evaluate_branch_tree(bzrdir):
92
 
    try:
93
 
        tree, branch = bzrdir._get_tree_branch()
94
 
    except NotBranchError:
95
 
        return True, None
96
 
    else:
97
 
        return True, (branch, tree)
98
 
 
99
 
 
100
 
def iter_branch_tree(t, lister=None):
101
 
    return (x for x in BzrDir.find_bzrdirs(t, evaluate=evaluate_branch_tree,
102
 
            list_current=lister) if x is not None)
103
 
 
104
 
 
105
 
def open_from_url(location):
106
 
    location = urlutils.normalize_url(location)
107
 
    dirname, basename = urlutils.split(location)
108
 
    if location.endswith('/') and not basename.endswith('/'):
109
 
        basename += '/'
110
 
    return get_transport(dirname).get(basename)
111
 
 
112
 
 
113
299
def run_tests():
114
300
    import doctest
115
301
    result = doctest.testmod()