~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2007-08-14 16:04:32 UTC
  • Revision ID: abentley@panoramicfeedback.com-20070814160432-19p95yu0ltkcsxbp
Don't check version when running non-bzrtools commands

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
import bzrlib
 
1
# Copyright (C) 2005, 2006, 2007 Aaron Bentley <aaron.bentley@utoronto.ca>
 
2
# Copyright (C) 2007 John Arbash Meinel
 
3
#
 
4
#    This program is free software; you can redistribute it and/or modify
 
5
#    it under the terms of the GNU General Public License as published by
 
6
#    the Free Software Foundation; either version 2 of the License, or
 
7
#    (at your option) any later version.
 
8
#
 
9
#    This program is distributed in the hope that it will be useful,
 
10
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
#    GNU General Public License for more details.
 
13
#
 
14
#    You should have received a copy of the GNU General Public License
 
15
#    along with this program; if not, write to the Free Software
 
16
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
17
import codecs
 
18
import errno
2
19
import os
3
 
import os.path
4
 
import sys
 
20
import re
5
21
import tempfile
6
22
import shutil
7
 
 
8
 
def temp_branch():
 
23
from subprocess import Popen, PIPE
 
24
import sys
 
25
 
 
26
import bzrlib
 
27
from bzrlib import urlutils
 
28
import bzrlib.errors
 
29
from bzrlib.errors import (BzrCommandError, NotBranchError, NoSuchFile,
 
30
                           UnsupportedFormatError, TransportError,
 
31
                           NoWorkingTree, PermissionDenied)
 
32
from bzrlib.bzrdir import BzrDir, BzrDirFormat
 
33
from bzrlib.transport import get_transport
 
34
 
 
35
def temp_tree():
9
36
    dirname = tempfile.mkdtemp("temp-branch")
10
 
    return bzrlib.Branch(dirname, init=True)
11
 
 
12
 
def rm_branch(br):
13
 
    shutil.rmtree(br.base)
14
 
 
15
 
def is_clean(cur_branch):
 
37
    return BzrDir.create_standalone_workingtree(dirname)
 
38
 
 
39
def rm_tree(tree):
 
40
    shutil.rmtree(tree.basedir)
 
41
 
 
42
def is_clean(cur_tree):
16
43
    """
17
44
    Return true if no files are modifed or unknown
18
 
    >>> br = temp_branch()
19
 
    >>> is_clean(br)
20
 
    True
21
 
    >>> fooname = os.path.join(br.base, "foo")
22
 
    >>> file(fooname, "wb").write("bar")
23
 
    >>> is_clean(br)
24
 
    False
25
 
    >>> bzrlib.add.smart_add([fooname])
26
 
    >>> is_clean(br)
27
 
    False
28
 
    >>> br.commit("added file")
29
 
    >>> is_clean(br)
30
 
    True
31
 
    >>> rm_branch(br)
32
 
    """
33
 
    old_tree = cur_branch.basis_tree()
34
 
    new_tree = cur_branch.working_tree()
35
 
    for path, file_class, kind, file_id in new_tree.list_files():
36
 
        if file_class == '?':
37
 
            return False
38
 
    delta = bzrlib.compare_trees(old_tree, new_tree, want_unchanged=False)
39
 
    if len(delta.added) > 0 or len(delta.removed) > 0 or \
40
 
        len(delta.modified) > 0:
41
 
        return False
42
 
    return True
43
 
 
44
 
def set_pull_data(br, location, rev_id):
45
 
    pull_file = file (br.controlfilename("x-pull-data"), "wb")
46
 
    pull_file.write("%s\n%s\n" % (location, rev_id))
47
 
 
48
 
def get_pull_data(br):
49
 
    """
50
 
    >>> br = temp_branch()
51
 
    >>> get_pull_data(br)
52
 
    (None, None)
53
 
    >>> set_pull_data(br, 'http://somewhere', '888-777')
54
 
    >>> get_pull_data(br)
55
 
    ('http://somewhere', '888-777')
56
 
    >>> rm_branch(br)
57
 
    """
58
 
    filename = br.controlfilename("x-pull-data")
59
 
    if not os.path.exists(filename):
60
 
        return (None, None)
61
 
    pull_file = file (filename, "rb")
62
 
    location, rev_id = [f.rstrip('\n') for f in pull_file]
63
 
    return location, rev_id
64
 
 
65
 
def set_push_data(br, location):
66
 
    push_file = file (br.controlfilename("x-push-data"), "wb")
67
 
    push_file.write("%s\n" % location)
68
 
 
69
 
def get_push_data(br):
70
 
    """
71
 
    >>> br = temp_branch()
72
 
    >>> get_push_data(br) is None
73
 
    True
74
 
    >>> set_push_data(br, 'http://somewhere')
75
 
    >>> get_push_data(br)
76
 
    'http://somewhere'
77
 
    >>> rm_branch(br)
78
 
    """
79
 
    filename = br.controlfilename("x-push-data")
80
 
    if not os.path.exists(filename):
 
45
    """
 
46
    old_tree = cur_tree.basis_tree()
 
47
    new_tree = cur_tree
 
48
    non_source = []
 
49
    new_tree.lock_read()
 
50
    try:
 
51
        for path, file_class, kind, file_id, entry in new_tree.list_files():
 
52
            if file_class in ('?', 'I'):
 
53
                non_source.append(path)
 
54
        delta = new_tree.changes_from(old_tree, want_unchanged=False)
 
55
    finally:
 
56
        new_tree.unlock()
 
57
    return not delta.has_changed(), non_source
 
58
 
 
59
def set_push_data(tree, location):
 
60
    tree.branch.control_files.put_utf8("x-push-data", "%s\n" % location)
 
61
 
 
62
def get_push_data(tree):
 
63
    """
 
64
    >>> tree = temp_tree()
 
65
    >>> get_push_data(tree) is None
 
66
    True
 
67
    >>> set_push_data(tree, 'http://somewhere')
 
68
    >>> get_push_data(tree)
 
69
    u'http://somewhere'
 
70
    >>> rm_tree(tree)
 
71
    """
 
72
    try:
 
73
        location = tree.branch.control_files.get_utf8('x-push-data').read()
 
74
    except NoSuchFile:
81
75
        return None
82
 
    push_file = file (filename, "rb")
83
 
    (location,) = [f.rstrip('\n') for f in push_file]
84
 
    return location
 
76
    return location.rstrip('\n')
85
77
 
86
78
"""
87
79
>>> shell_escape('hello')
101
93
    arg_str = " ".join([shell_escape(a) for a in args])
102
94
    return os.system(arg_str)
103
95
 
104
 
def rsync(source, target, ssh=False, exclude_globs=()):
105
 
    """
106
 
    >>> real_system = os.system
107
 
    >>> os.system = sys.stdout.write
108
 
    >>> rsync("a", "b")
109
 
    \\r\\s\\y\\n\\c \\-\\a\\v \\-\\-\\d\\e\\l\\e\\t\\e \\a \\b
110
 
    >>> rsync("a", "b", exclude_globs=("*.py",))
111
 
    \\r\\s\\y\\n\\c \\-\\a\\v \\-\\-\\d\\e\\l\\e\\t\\e\
112
 
 \\-\\-\\e\\x\\c\\l\\u\\d\\e \\*\\.\\p\\y \\a \\b
113
 
    >>> os.system = real_system
114
 
    """
115
 
    cmd = ["rsync", "-av", "--delete"]
 
96
class RsyncUnknownStatus(Exception):
 
97
    def __init__(self, status):
 
98
        Exception.__init__(self, "Unknown status: %d" % status)
 
99
 
 
100
class NoRsync(Exception):
 
101
    def __init__(self, rsync_name):
 
102
        Exception.__init__(self, "%s not found." % rsync_name)
 
103
 
 
104
def rsync(source, target, ssh=False, excludes=(), silent=False,
 
105
          rsync_name="rsync"):
 
106
    """
 
107
    >>> new_dir = tempfile.mkdtemp()
 
108
    >>> old_dir = os.getcwd()
 
109
    >>> os.chdir(new_dir)
 
110
    >>> rsync("a", "b", silent=True)
 
111
    Traceback (most recent call last):
 
112
    RsyncNoFile: No such file...
 
113
    >>> rsync(new_dir + "/a", new_dir + "/b", excludes=("*.py",), silent=True)
 
114
    Traceback (most recent call last):
 
115
    RsyncNoFile: No such file...
 
116
    >>> rsync(new_dir + "/a", new_dir + "/b", excludes=("*.py",), silent=True, rsync_name="rsyncc")
 
117
    Traceback (most recent call last):
 
118
    NoRsync: rsyncc not found.
 
119
    >>> os.chdir(old_dir)
 
120
    >>> os.rmdir(new_dir)
 
121
    """
 
122
    cmd = [rsync_name, "-av", "--delete"]
116
123
    if ssh:
117
124
        cmd.extend(('-e', 'ssh'))
118
 
    for exclude in exclude_globs:
119
 
        cmd.extend(('--exclude', exclude))
 
125
    if len(excludes) > 0:
 
126
        cmd.extend(('--exclude-from', '-'))
120
127
    cmd.extend((source, target))
121
 
    safe_system(cmd)
122
 
 
123
 
exclusions = ('x-push-data', 'x-pull-data')
124
 
 
125
 
 
126
 
def pull(cur_branch, location=None, overwrite=False):
127
 
    pull_location, pull_revision = get_pull_data(cur_branch)
128
 
    if pull_location is not None:
129
 
        if not overwrite and cur_branch.last_patch() != pull_revision:
130
 
            print "Aborting: This branch has had commits, so pull would lose data."
131
 
            sys.exit(1)
132
 
    if location is not None:
133
 
        pull_location = location
134
 
        if not pull_location.endswith('/'):
135
 
            pull_location+='/'
136
 
 
137
 
    if pull_location is None:
138
 
        print "No pull location saved.  Please specify one on the command line."
139
 
        sys.exit(1)
140
 
 
141
 
    if not is_clean(cur_branch):
142
 
        print "Error: This tree has uncommitted changes or unknown (?) files."
143
 
        sys.exit(1)
144
 
 
145
 
    print "Synchronizing with %s" % pull_location
146
 
    rsync (pull_location, cur_branch.base+'/', exclude_globs=exclusions)
147
 
 
148
 
    set_pull_data(cur_branch, pull_location, cur_branch.last_patch())
149
 
 
150
 
 
151
 
def push(cur_branch, location=None):
152
 
    push_location = get_push_data(cur_branch)
 
128
    if silent:
 
129
        stderr = PIPE
 
130
        stdout = PIPE
 
131
    else:
 
132
        stderr = None
 
133
        stdout = None
 
134
    try:
 
135
        proc = Popen(cmd, stdin=PIPE, stderr=stderr, stdout=stdout)
 
136
    except OSError, e:
 
137
        if e.errno == errno.ENOENT:
 
138
            raise NoRsync(rsync_name)
 
139
 
 
140
    proc.stdin.write('\n'.join(excludes)+'\n')
 
141
    proc.stdin.close()
 
142
    if silent:
 
143
        proc.stderr.read()
 
144
        proc.stderr.close()
 
145
        proc.stdout.read()
 
146
        proc.stdout.close()
 
147
    proc.wait()
 
148
    if proc.returncode == 12:
 
149
        raise RsyncStreamIO()
 
150
    elif proc.returncode == 23:
 
151
        raise RsyncNoFile(source)
 
152
    elif proc.returncode != 0:
 
153
        raise RsyncUnknownStatus(proc.returncode)
 
154
    return cmd
 
155
 
 
156
 
 
157
def rsync_ls(source, ssh=False, silent=True):
 
158
    cmd = ["rsync"]
 
159
    if ssh:
 
160
        cmd.extend(('-e', 'ssh'))
 
161
    cmd.append(source)
 
162
    if silent:
 
163
        stderr = PIPE
 
164
    else:
 
165
        stderr = None
 
166
    proc = Popen(cmd, stderr=stderr, stdout=PIPE)
 
167
    result = proc.stdout.read()
 
168
    proc.stdout.close()
 
169
    if silent:
 
170
        proc.stderr.read()
 
171
        proc.stderr.close()
 
172
    proc.wait()
 
173
    if proc.returncode == 12:
 
174
        raise RsyncStreamIO()
 
175
    elif proc.returncode == 23:
 
176
        raise RsyncNoFile(source)
 
177
    elif proc.returncode != 0:
 
178
        raise RsyncUnknownStatus(proc.returncode)
 
179
    return [l.split(' ')[-1].rstrip('\n') for l in result.splitlines(True)]
 
180
 
 
181
exclusions = ('.bzr/x-push-data', '.bzr/branch/x-push/data', '.bzr/parent',
 
182
              '.bzr/branch/parent', '.bzr/x-pull-data', '.bzr/x-pull',
 
183
              '.bzr/pull', '.bzr/stat-cache', '.bzr/x-rsync-data',
 
184
              '.bzr/basis-inventory', '.bzr/inventory.backup.weave')
 
185
 
 
186
 
 
187
def read_revision_history(fname):
 
188
    return [l.rstrip('\r\n') for l in
 
189
            codecs.open(fname, 'rb', 'utf-8').readlines()]
 
190
 
 
191
class RsyncNoFile(Exception):
 
192
    def __init__(self, path):
 
193
        Exception.__init__(self, "No such file %s" % path)
 
194
 
 
195
class RsyncStreamIO(Exception):
 
196
    def __init__(self):
 
197
        Exception.__init__(self, "Error in rsync protocol data stream.")
 
198
 
 
199
def get_revision_history(location):
 
200
    tempdir = tempfile.mkdtemp('push')
 
201
    try:
 
202
        history_fname = os.path.join(tempdir, 'revision-history')
 
203
        try:
 
204
            cmd = rsync(location+'.bzr/revision-history', history_fname,
 
205
                        silent=True)
 
206
        except RsyncNoFile:
 
207
            cmd = rsync(location+'.bzr/branch/revision-history', history_fname,
 
208
                        silent=True)
 
209
        history = read_revision_history(history_fname)
 
210
    finally:
 
211
        shutil.rmtree(tempdir)
 
212
    return history
 
213
 
 
214
def history_subset(location, branch):
 
215
    remote_history = get_revision_history(location)
 
216
    local_history = branch.revision_history()
 
217
    if len(remote_history) > len(local_history):
 
218
        return False
 
219
    for local, remote in zip(remote_history, local_history):
 
220
        if local != remote:
 
221
            return False
 
222
    return True
 
223
 
 
224
def empty_or_absent(location):
 
225
    try:
 
226
        files = rsync_ls(location)
 
227
        return files == ['.']
 
228
    except RsyncNoFile:
 
229
        return True
 
230
 
 
231
def rspush(tree, location=None, overwrite=False, working_tree=True):
 
232
    push_location = get_push_data(tree)
153
233
    if location is not None:
154
234
        if not location.endswith('/'):
155
235
            location += '/'
156
236
        push_location = location
157
 
    
 
237
 
158
238
    if push_location is None:
159
 
        print "No push location saved.  Please specify one on the command line."
160
 
        sys.exit(1)
161
 
 
162
 
    if not is_clean(cur_branch):
163
 
        print "Error: This tree has uncommitted changes or unknown (?) files."
164
 
        sys.exit(1)
165
 
 
 
239
        raise BzrCommandError("No rspush location known or specified.")
 
240
 
 
241
    if (push_location.find('::') != -1):
 
242
        usessh=False
 
243
    else:
 
244
        usessh=True
 
245
 
 
246
    if (push_location.find('://') != -1 or
 
247
        push_location.find(':') == -1):
 
248
        raise BzrCommandError("Invalid rsync path %r." % push_location)
 
249
 
 
250
    if working_tree:
 
251
        clean, non_source = is_clean(tree)
 
252
        if not clean:
 
253
            print """Error: This tree has uncommitted changes or unknown (?) files.
 
254
    Use "bzr status" to list them."""
 
255
            sys.exit(1)
 
256
        final_exclusions = non_source[:]
 
257
    else:
 
258
        wt = tree
 
259
        final_exclusions = []
 
260
        for path, status, kind, file_id, entry in wt.list_files():
 
261
            final_exclusions.append(path)
 
262
 
 
263
    final_exclusions.extend(exclusions)
 
264
    if not overwrite:
 
265
        try:
 
266
            if not history_subset(push_location, tree.branch):
 
267
                raise bzrlib.errors.BzrCommandError("Local branch is not a"
 
268
                                                    " newer version of remote"
 
269
                                                    " branch.")
 
270
        except RsyncNoFile:
 
271
            if not empty_or_absent(push_location):
 
272
                raise bzrlib.errors.BzrCommandError("Remote location is not a"
 
273
                                                    " bzr branch (or empty"
 
274
                                                    " directory)")
 
275
        except RsyncStreamIO:
 
276
            raise bzrlib.errors.BzrCommandError("Rsync could not use the"
 
277
                " specified location.  Please ensure that"
 
278
                ' "%s" is of the form "machine:/path".' % push_location)
166
279
    print "Pushing to %s" % push_location
167
 
    rsync(cur_branch.base+'/', push_location, ssh=True,
168
 
          exclude_globs=exclusions)
169
 
 
170
 
    set_push_data(cur_branch, push_location)
 
280
    rsync(tree.basedir+'/', push_location, ssh=usessh,
 
281
          excludes=final_exclusions)
 
282
 
 
283
    set_push_data(tree, push_location)
 
284
 
 
285
 
 
286
def short_committer(committer):
 
287
    new_committer = re.sub('<.*>', '', committer).strip(' ')
 
288
    if len(new_committer) < 2:
 
289
        return committer
 
290
    return new_committer
 
291
 
 
292
 
 
293
def apache_ls(t):
 
294
    """Screen-scrape Apache listings"""
 
295
    apache_dir = '<img border="0" src="/icons/folder.gif" alt="[dir]">'\
 
296
        ' <a href="'
 
297
    lines = t.get('.')
 
298
    expr = re.compile('<a[^>]*href="([^>]*)"[^>]*>', flags=re.I)
 
299
    for line in lines:
 
300
        match = expr.search(line)
 
301
        if match is None:
 
302
            continue
 
303
        url = match.group(1)
 
304
        if url.startswith('http://') or url.startswith('/') or '../' in url:
 
305
            continue
 
306
        if '?' in url:
 
307
            continue
 
308
        yield url.rstrip('/')
 
309
 
 
310
 
 
311
def iter_branches(t, lister=None):
 
312
    """Iterate through all the branches under a transport"""
 
313
    for bzrdir in iter_bzrdirs(t, lister):
 
314
        try:
 
315
            branch = bzrdir.open_branch()
 
316
            if branch.bzrdir is bzrdir:
 
317
                yield branch
 
318
        except (NotBranchError, UnsupportedFormatError):
 
319
            pass
 
320
 
 
321
 
 
322
def iter_branch_tree(t, lister=None):
 
323
    for bzrdir in iter_bzrdirs(t, lister):
 
324
        try:
 
325
            wt = bzrdir.open_workingtree()
 
326
            yield wt.branch, wt
 
327
        except NoWorkingTree, UnsupportedFormatError:
 
328
            try:
 
329
                branch = bzrdir.open_branch()
 
330
                if branch.bzrdir is bzrdir:
 
331
                    yield branch, None
 
332
            except (NotBranchError, UnsupportedFormatError):
 
333
                continue
 
334
 
 
335
 
 
336
def iter_bzrdirs(t, lister=None):
 
337
    if lister is None:
 
338
        def lister(t):
 
339
            return t.list_dir('.')
 
340
    try:
 
341
        bzrdir = bzrdir_from_transport(t)
 
342
        yield bzrdir
 
343
    except (NotBranchError, UnsupportedFormatError, TransportError,
 
344
            PermissionDenied):
 
345
        pass
 
346
    try:
 
347
        for directory in lister(t):
 
348
            if directory == ".bzr":
 
349
                continue
 
350
            try:
 
351
                subt = t.clone(directory)
 
352
            except UnicodeDecodeError:
 
353
                continue
 
354
            for bzrdir in iter_bzrdirs(subt, lister):
 
355
                yield bzrdir
 
356
    except (NoSuchFile, PermissionDenied, TransportError):
 
357
        pass
 
358
 
 
359
 
 
360
def bzrdir_from_transport(t):
 
361
    """Open a bzrdir from a transport (not a location)"""
 
362
    format = BzrDirFormat.find_format(t)
 
363
    BzrDir._check_supported(format, False)
 
364
    return format.open(t)
 
365
 
 
366
 
 
367
def open_from_url(location):
 
368
    location = urlutils.normalize_url(location)
 
369
    dirname, basename = urlutils.split(location)
 
370
    return get_transport(dirname).get(basename)
 
371
 
171
372
 
172
373
def run_tests():
173
374
    import doctest