~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2005-11-10 21:04:19 UTC
  • Revision ID: aaron.bentley@utoronto.ca-20051110210419-a402638d94693825
Handled whitespace branch names better

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
18
 
import re
19
 
 
20
 
from bzrlib import urlutils
21
 
from bzrlib.errors import (
22
 
    BzrCommandError,
23
 
    NotBranchError,
24
 
    NoSuchFile,
25
 
    )
26
 
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
 
17
import bzrlib
 
18
import bzrlib.errors
 
19
import os
 
20
import os.path
 
21
import sys
 
22
import tempfile
 
23
import shutil
 
24
import errno
 
25
from subprocess import Popen, PIPE
 
26
import codecs
 
27
 
 
28
def temp_branch():
 
29
    dirname = tempfile.mkdtemp("temp-branch")
 
30
    return bzrlib.branch.Branch.initialize(dirname)
 
31
 
 
32
def rm_branch(br):
 
33
    shutil.rmtree(br.base)
 
34
 
 
35
def is_clean(cur_branch):
 
36
    """
 
37
    Return true if no files are modifed or unknown
 
38
    >>> import bzrlib.add
 
39
    >>> br = temp_branch()
 
40
    >>> is_clean(br)
 
41
    (True, [])
 
42
    >>> fooname = os.path.join(br.base, "foo")
 
43
    >>> file(fooname, "wb").write("bar")
 
44
    >>> is_clean(br)
 
45
    (True, [u'foo'])
 
46
    >>> bzrlib.add.smart_add_branch(br, [br.base])
 
47
    1
 
48
    >>> is_clean(br)
 
49
    (False, [])
 
50
    >>> br.commit("added file")
 
51
    >>> is_clean(br)
 
52
    (True, [])
 
53
    >>> rm_branch(br)
 
54
    """
 
55
    from bzrlib.diff import compare_trees
 
56
    old_tree = cur_branch.basis_tree()
 
57
    new_tree = cur_branch.working_tree()
 
58
    non_source = []
 
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 = compare_trees(old_tree, new_tree, want_unchanged=False)
 
63
    return not delta.has_changed(), non_source
 
64
 
 
65
def set_pull_data(br, location, rev_id):
 
66
    pull_file = file (br.controlfilename("x-pull-data"), "wb")
 
67
    pull_file.write("%s\n%s\n" % (location, rev_id))
 
68
 
 
69
def get_pull_data(br):
 
70
    """
 
71
    >>> br = temp_branch()
 
72
    >>> get_pull_data(br)
 
73
    (None, None)
 
74
    >>> set_pull_data(br, 'http://somewhere', '888-777')
 
75
    >>> get_pull_data(br)
 
76
    ('http://somewhere', '888-777')
 
77
    >>> rm_branch(br)
 
78
    """
 
79
    filename = br.controlfilename("x-pull-data")
 
80
    if not os.path.exists(filename):
 
81
        return (None, None)
 
82
    pull_file = file (filename, "rb")
 
83
    location, rev_id = [f.rstrip('\n') for f in pull_file]
 
84
    return location, rev_id
 
85
 
 
86
def set_push_data(br, location):
 
87
    push_file = file (br.controlfilename("x-push-data"), "wb")
 
88
    push_file.write("%s\n" % location)
 
89
 
 
90
def get_push_data(br):
 
91
    """
 
92
    >>> br = temp_branch()
 
93
    >>> get_push_data(br) is None
 
94
    True
 
95
    >>> set_push_data(br, 'http://somewhere')
 
96
    >>> get_push_data(br)
 
97
    'http://somewhere'
 
98
    >>> rm_branch(br)
 
99
    """
 
100
    filename = br.controlfilename("x-push-data")
 
101
    if not os.path.exists(filename):
 
102
        return None
 
103
    push_file = file (filename, "rb")
 
104
    (location,) = [f.rstrip('\n') for f in push_file]
 
105
    return location
 
106
 
 
107
"""
 
108
>>> shell_escape('hello')
 
109
'\h\e\l\l\o'
 
110
"""
 
111
def shell_escape(arg):
 
112
    return "".join(['\\'+c for c in arg])
 
113
 
 
114
def safe_system(args):
 
115
    """
 
116
    >>> real_system = os.system
 
117
    >>> os.system = sys.stdout.write
 
118
    >>> safe_system(['a', 'b', 'cd'])
 
119
    \\a \\b \\c\\d
 
120
    >>> os.system = real_system
 
121
    """
 
122
    arg_str = " ".join([shell_escape(a) for a in args])
 
123
    return os.system(arg_str)
 
124
 
 
125
class RsyncUnknownStatus(Exception):
 
126
    def __init__(self, status):
 
127
        Exception.__init__(self, "Unknown status: %d" % status)
 
128
 
 
129
class NoRsync(Exception):
 
130
    def __init__(self, rsync_name):
 
131
        Exception.__init__(self, "%s not found." % rsync_name)
 
132
 
 
133
def rsync(source, target, ssh=False, excludes=(), silent=False, 
 
134
          rsync_name="rsync"):
 
135
    """
 
136
    >>> rsync("a", "b", silent=True)
 
137
    Traceback (most recent call last):
 
138
    RsyncNoFile: No such file a
 
139
    >>> rsync("a", "b", excludes=("*.py",), silent=True)
 
140
    Traceback (most recent call last):
 
141
    RsyncNoFile: No such file a
 
142
    >>> rsync("a", "b", excludes=("*.py",), silent=True, rsync_name="rsyncc")
 
143
    Traceback (most recent call last):
 
144
    NoRsync: rsyncc not found.
 
145
    """
 
146
    cmd = [rsync_name, "-av", "--delete"]
 
147
    if ssh:
 
148
        cmd.extend(('-e', 'ssh'))
 
149
    if len(excludes) > 0:
 
150
        cmd.extend(('--exclude-from', '-'))
 
151
    cmd.extend((source, target))
 
152
    if silent:
 
153
        stderr = PIPE
 
154
        stdout = PIPE
 
155
    else:
 
156
        stderr = None
 
157
        stdout = None
 
158
    try:
 
159
        proc = Popen(cmd, stdin=PIPE, stderr=stderr, stdout=stdout)
 
160
    except OSError, e:
 
161
        if e.errno == errno.ENOENT:
 
162
            raise NoRsync(rsync_name)
 
163
            
 
164
    proc.stdin.write('\n'.join(excludes)+'\n')
 
165
    proc.stdin.close()
 
166
    if silent:
 
167
        proc.stderr.read()
 
168
        proc.stderr.close()
 
169
        proc.stdout.read()
 
170
        proc.stdout.close()
 
171
    proc.wait()
 
172
    if proc.returncode == 12:
 
173
        raise RsyncStreamIO()
 
174
    elif proc.returncode == 23:
 
175
        raise RsyncNoFile(source)
 
176
    elif proc.returncode != 0:
 
177
        raise RsyncUnknownStatus(proc.returncode)
 
178
    return cmd
 
179
 
 
180
 
 
181
def rsync_ls(source, ssh=False, silent=True):
 
182
    cmd = ["rsync"]
 
183
    if ssh:
 
184
        cmd.extend(('-e', 'ssh'))
 
185
    cmd.append(source)
 
186
    if silent:
 
187
        stderr = PIPE
 
188
    else:
 
189
        stderr = None
 
190
    proc = Popen(cmd, stderr=stderr, stdout=PIPE)
 
191
    result = proc.stdout.read()
 
192
    proc.stdout.close()
 
193
    if silent:
 
194
        proc.stderr.read()
 
195
        proc.stderr.close()
 
196
    proc.wait()
 
197
    if proc.returncode == 12:
 
198
        raise RsyncStreamIO()
 
199
    elif proc.returncode == 23:
 
200
        raise RsyncNoFile(source)
 
201
    elif proc.returncode != 0:
 
202
        raise RsyncUnknownStatus(proc.returncode)
 
203
    return [l.split(' ')[-1].rstrip('\n') for l in result.splitlines(True)]
 
204
 
 
205
exclusions = ('.bzr/x-push-data', '.bzr/parent', '.bzr/x-pull-data', 
 
206
              '.bzr/x-pull', '.bzr/pull', '.bzr/stat-cache',
 
207
              '.bzr/x-rsync-data')
 
208
 
 
209
 
 
210
def read_revision_history(fname):
 
211
    return [l.rstrip('\r\n') for l in
 
212
            codecs.open(fname, 'rb', 'utf-8').readlines()]
 
213
 
 
214
class RsyncNoFile(Exception):
 
215
    def __init__(self, path):
 
216
        Exception.__init__(self, "No such file %s" % path)
 
217
 
 
218
class RsyncStreamIO(Exception):
 
219
    def __init__(self):
 
220
        Exception.__init__(self, "Error in rsync protocol data stream.")
 
221
 
 
222
def get_revision_history(location):
 
223
    tempdir = tempfile.mkdtemp('push')
 
224
    try:
 
225
        history_fname = os.path.join(tempdir, 'revision-history')
 
226
        cmd = rsync(location+'.bzr/revision-history', history_fname,
 
227
                    silent=True)
 
228
        history = read_revision_history(history_fname)
36
229
    finally:
37
 
        lockable.unlock()
38
 
 
39
 
 
40
 
def short_committer(committer):
41
 
    new_committer = re.sub('<.*>', '', committer).strip(' ')
42
 
    if len(new_committer) < 2:
43
 
        return committer
44
 
    return new_committer
45
 
 
46
 
 
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
 
 
 
230
        shutil.rmtree(tempdir)
 
231
    return history
 
232
 
 
233
def history_subset(location, branch):
 
234
    remote_history = get_revision_history(location)
 
235
    local_history = branch.revision_history()
 
236
    if len(remote_history) > len(local_history):
 
237
        return False
 
238
    for local, remote in zip(remote_history, local_history):
 
239
        if local != remote:
 
240
            return False 
 
241
    return True
 
242
 
 
243
def empty_or_absent(location):
 
244
    try:
 
245
        files = rsync_ls(location)
 
246
        return files == ['.']
 
247
    except RsyncNoFile:
 
248
        return True
 
249
 
 
250
def push(cur_branch, location=None, overwrite=False):
 
251
    push_location = get_push_data(cur_branch)
 
252
    if location is not None:
 
253
        if not location.endswith('/'):
 
254
            location += '/'
 
255
        push_location = location
 
256
    
 
257
    if push_location is None:
 
258
        raise bzrlib.errors.MustUseDecorated
 
259
 
 
260
    if push_location.find('://') != -1:
 
261
        raise bzrlib.errors.MustUseDecorated
 
262
 
 
263
    if push_location.find(':') == -1:
 
264
        raise bzrlib.errors.MustUseDecorated
 
265
 
 
266
    clean, non_source = is_clean(cur_branch)
 
267
    if not clean:
 
268
        print """Error: This tree has uncommitted changes or unknown (?) files.
 
269
Use "bzr status" to list them."""
 
270
        sys.exit(1)
 
271
    non_source.extend(exclusions)
 
272
    if not overwrite:
 
273
        try:
 
274
            if not history_subset(push_location, cur_branch):
 
275
                raise bzrlib.errors.BzrCommandError("Local branch is not a"
 
276
                                                    " newer version of remote"
 
277
                                                    " branch.")
 
278
        except RsyncNoFile:
 
279
            if not empty_or_absent(push_location):
 
280
                raise bzrlib.errors.BzrCommandError("Remote location is not a"
 
281
                                                    " bzr branch (or empty"
 
282
                                                    " directory)")
 
283
        except RsyncStreamIO:
 
284
            raise bzrlib.errors.BzrCommandError("Rsync could not use the"
 
285
                " specified location.  Please ensure that"
 
286
                ' "%s" is of the form "machine:/path".' % push_location)
 
287
    print "Pushing to %s" % push_location
 
288
    rsync(cur_branch.base+'/', push_location, ssh=True, excludes=non_source)
 
289
 
 
290
    set_push_data(cur_branch, push_location)
112
291
 
113
292
def run_tests():
114
293
    import doctest