~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 Aaron Bentley
2
 
# <aaron.bentley@utoronto.ca>
 
1
# Copyright (C) 2005-2009, 2011-2013 Aaron Bentley <aaron@aaronbentley.com>
 
2
# Copyright (C) 2007 John Arbash Meinel
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
 
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, ['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
 
    if len(delta.added) > 0 or len(delta.removed) > 0 or \
64
 
        len(delta.modified) > 0:
65
 
        return False, non_source
66
 
    return True, non_source 
67
 
 
68
 
def set_pull_data(br, location, rev_id):
69
 
    pull_file = file (br.controlfilename("x-pull-data"), "wb")
70
 
    pull_file.write("%s\n%s\n" % (location, rev_id))
71
 
 
72
 
def get_pull_data(br):
73
 
    """
74
 
    >>> br = temp_branch()
75
 
    >>> get_pull_data(br)
76
 
    (None, None)
77
 
    >>> set_pull_data(br, 'http://somewhere', '888-777')
78
 
    >>> get_pull_data(br)
79
 
    ('http://somewhere', '888-777')
80
 
    >>> rm_branch(br)
81
 
    """
82
 
    filename = br.controlfilename("x-pull-data")
83
 
    if not os.path.exists(filename):
84
 
        return (None, None)
85
 
    pull_file = file (filename, "rb")
86
 
    location, rev_id = [f.rstrip('\n') for f in pull_file]
87
 
    return location, rev_id
88
 
 
89
 
def set_push_data(br, location):
90
 
    push_file = file (br.controlfilename("x-push-data"), "wb")
91
 
    push_file.write("%s\n" % location)
92
 
 
93
 
def get_push_data(br):
94
 
    """
95
 
    >>> br = temp_branch()
96
 
    >>> get_push_data(br) is None
97
 
    True
98
 
    >>> set_push_data(br, 'http://somewhere')
99
 
    >>> get_push_data(br)
100
 
    'http://somewhere'
101
 
    >>> rm_branch(br)
102
 
    """
103
 
    filename = br.controlfilename("x-push-data")
104
 
    if not os.path.exists(filename):
105
 
        return None
106
 
    push_file = file (filename, "rb")
107
 
    (location,) = [f.rstrip('\n') for f in push_file]
108
 
    return location
109
 
 
110
 
"""
111
 
>>> shell_escape('hello')
112
 
'\h\e\l\l\o'
113
 
"""
114
 
def shell_escape(arg):
115
 
    return "".join(['\\'+c for c in arg])
116
 
 
117
 
def safe_system(args):
118
 
    """
119
 
    >>> real_system = os.system
120
 
    >>> os.system = sys.stdout.write
121
 
    >>> safe_system(['a', 'b', 'cd'])
122
 
    \\a \\b \\c\\d
123
 
    >>> os.system = real_system
124
 
    """
125
 
    arg_str = " ".join([shell_escape(a) for a in args])
126
 
    return os.system(arg_str)
127
 
 
128
 
class RsyncUnknownStatus(Exception):
129
 
    def __init__(self, status):
130
 
        Exception.__init__(self, "Unknown status: %d" % status)
131
 
 
132
 
class NoRsync(Exception):
133
 
    def __init__(self, rsync_name):
134
 
        Exception.__init__(self, "%s not found." % rsync_name)
135
 
 
136
 
def rsync(source, target, ssh=False, excludes=(), silent=False, 
137
 
          rsync_name="rsync"):
138
 
    """
139
 
    >>> rsync("a", "b", silent=True)
140
 
    Traceback (most recent call last):
141
 
    RsyncNoFile: No such file a
142
 
    >>> rsync("a", "b", excludes=("*.py",), silent=True)
143
 
    Traceback (most recent call last):
144
 
    RsyncNoFile: No such file a
145
 
    >>> rsync("a", "b", excludes=("*.py",), silent=True, rsync_name="rsyncc")
146
 
    Traceback (most recent call last):
147
 
    NoRsync: rsyncc not found.
148
 
    """
149
 
    cmd = [rsync_name, "-av", "--delete"]
150
 
    if ssh:
151
 
        cmd.extend(('-e', 'ssh'))
152
 
    if len(excludes) > 0:
153
 
        cmd.extend(('--exclude-from', '-'))
154
 
    cmd.extend((source, target))
155
 
    if silent:
156
 
        stderr = PIPE
157
 
        stdout = PIPE
158
 
    else:
159
 
        stderr = None
160
 
        stdout = None
161
 
    try:
162
 
        proc = Popen(cmd, stdin=PIPE, stderr=stderr, stdout=stdout)
163
 
    except OSError, e:
164
 
        if e.errno == errno.ENOENT:
165
 
            raise NoRsync(rsync_name)
166
 
            
167
 
    proc.stdin.write('\n'.join(excludes)+'\n')
168
 
    proc.stdin.close()
169
 
    if silent:
170
 
        proc.stderr.read()
171
 
        proc.stderr.close()
172
 
        proc.stdout.read()
173
 
        proc.stdout.close()
174
 
    proc.wait()
175
 
    if proc.returncode == 12:
176
 
        raise RsyncStreamIO()
177
 
    elif proc.returncode == 23:
178
 
        raise RsyncNoFile(source)
179
 
    elif proc.returncode != 0:
180
 
        raise RsyncUnknownStatus(proc.returncode)
181
 
    return cmd
182
 
 
183
 
 
184
 
def rsync_ls(source, ssh=False, silent=True):
185
 
    cmd = ["rsync"]
186
 
    if ssh:
187
 
        cmd.extend(('-e', 'ssh'))
188
 
    cmd.append(source)
189
 
    if silent:
190
 
        stderr = PIPE
191
 
    else:
192
 
        stderr = None
193
 
    proc = Popen(cmd, stderr=stderr, stdout=PIPE)
194
 
    result = proc.stdout.read()
195
 
    proc.stdout.close()
196
 
    if silent:
197
 
        proc.stderr.read()
198
 
        proc.stderr.close()
199
 
    proc.wait()
200
 
    if proc.returncode == 12:
201
 
        raise RsyncStreamIO()
202
 
    elif proc.returncode == 23:
203
 
        raise RsyncNoFile(source)
204
 
    elif proc.returncode != 0:
205
 
        raise RsyncUnknownStatus(proc.returncode)
206
 
    return [l.split(' ')[-1].rstrip('\n') for l in result.splitlines(True)]
207
 
 
208
 
exclusions = ('.bzr/x-push-data', '.bzr/parent', '.bzr/x-pull-data', 
209
 
              '.bzr/x-pull', '.bzr/pull', '.bzr/stat-cache',
210
 
              '.bzr/x-rsync-data')
211
 
 
212
 
 
213
 
def read_revision_history(fname):
214
 
    return [l.rstrip('\r\n') for l in
215
 
            codecs.open(fname, 'rb', 'utf-8').readlines()]
216
 
 
217
 
class RsyncNoFile(Exception):
218
 
    def __init__(self, path):
219
 
        Exception.__init__(self, "No such file %s" % path)
220
 
 
221
 
class RsyncStreamIO(Exception):
222
 
    def __init__(self):
223
 
        Exception.__init__(self, "Error in rsync protocol data stream.")
224
 
 
225
 
def get_revision_history(location):
226
 
    tempdir = tempfile.mkdtemp('push')
227
 
    try:
228
 
        history_fname = os.path.join(tempdir, 'revision-history')
229
 
        cmd = rsync(location+'.bzr/revision-history', history_fname,
230
 
                    silent=True)
231
 
        history = read_revision_history(history_fname)
 
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
232
36
    finally:
233
 
        shutil.rmtree(tempdir)
234
 
    return history
235
 
 
236
 
def history_subset(location, branch):
237
 
    remote_history = get_revision_history(location)
238
 
    local_history = branch.revision_history()
239
 
    if len(remote_history) > len(local_history):
240
 
        return False
241
 
    for local, remote in zip(remote_history, local_history):
242
 
        if local != remote:
243
 
            return False 
244
 
    return True
245
 
 
246
 
def empty_or_absent(location):
247
 
    try:
248
 
        files = rsync_ls(location)
249
 
        return files == ['.']
250
 
    except RsyncNoFile:
251
 
        return True
252
 
 
253
 
def push(cur_branch, location=None, overwrite=False):
254
 
    push_location = get_push_data(cur_branch)
255
 
    if location is not None:
256
 
        if not location.endswith('/'):
257
 
            location += '/'
258
 
        push_location = location
259
 
    
260
 
    if push_location is None:
261
 
        print "No push location saved.  Please specify one on the command line."
262
 
        sys.exit(1)
263
 
 
264
 
    clean, non_source = is_clean(cur_branch)
265
 
    if not clean:
266
 
        print """Error: This tree has uncommitted changes or unknown (?) files.
267
 
Use "bzr status" to list them."""
268
 
        sys.exit(1)
269
 
    non_source.extend(exclusions)
270
 
    if not overwrite:
271
 
        try:
272
 
            if not history_subset(push_location, cur_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(cur_branch.base+'/', push_location, ssh=True, excludes=non_source)
287
 
 
288
 
    set_push_data(cur_branch, push_location)
 
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
 
289
112
 
290
113
def run_tests():
291
114
    import doctest