~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2007-06-11 05:08:34 UTC
  • Revision ID: aaron.bentley@utoronto.ca-20070611050834-wcbta2pfitcuopku
fix long-line detection

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
import bzrlib.errors
 
28
from bzrlib.errors import (BzrCommandError, NotBranchError, NoSuchFile,
 
29
                           UnsupportedFormatError, TransportError,
 
30
                           NoWorkingTree, PermissionDenied)
 
31
from bzrlib.bzrdir import BzrDir, BzrDirFormat
 
32
 
 
33
def temp_tree():
9
34
    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):
 
35
    return BzrDir.create_standalone_workingtree(dirname)
 
36
 
 
37
def rm_tree(tree):
 
38
    shutil.rmtree(tree.basedir)
 
39
 
 
40
def is_clean(cur_tree):
16
41
    """
17
42
    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):
 
43
    """
 
44
    old_tree = cur_tree.basis_tree()
 
45
    new_tree = cur_tree
 
46
    non_source = []
 
47
    new_tree.lock_read()
 
48
    try:
 
49
        for path, file_class, kind, file_id, entry in new_tree.list_files():
 
50
            if file_class in ('?', 'I'):
 
51
                non_source.append(path)
 
52
        delta = new_tree.changes_from(old_tree, want_unchanged=False)
 
53
    finally:
 
54
        new_tree.unlock()
 
55
    return not delta.has_changed(), non_source
 
56
 
 
57
def set_push_data(tree, location):
 
58
    tree.branch.control_files.put_utf8("x-push-data", "%s\n" % location)
 
59
 
 
60
def get_push_data(tree):
 
61
    """
 
62
    >>> tree = temp_tree()
 
63
    >>> get_push_data(tree) is None
 
64
    True
 
65
    >>> set_push_data(tree, 'http://somewhere')
 
66
    >>> get_push_data(tree)
 
67
    u'http://somewhere'
 
68
    >>> rm_tree(tree)
 
69
    """
 
70
    try:
 
71
        location = tree.branch.control_files.get_utf8('x-push-data').read()
 
72
    except NoSuchFile:
81
73
        return None
82
 
    push_file = file (filename, "rb")
83
 
    (location,) = [f.rstrip('\n') for f in push_file]
84
 
    return location
 
74
    return location.rstrip('\n')
85
75
 
86
76
"""
87
77
>>> shell_escape('hello')
101
91
    arg_str = " ".join([shell_escape(a) for a in args])
102
92
    return os.system(arg_str)
103
93
 
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"]
 
94
class RsyncUnknownStatus(Exception):
 
95
    def __init__(self, status):
 
96
        Exception.__init__(self, "Unknown status: %d" % status)
 
97
 
 
98
class NoRsync(Exception):
 
99
    def __init__(self, rsync_name):
 
100
        Exception.__init__(self, "%s not found." % rsync_name)
 
101
 
 
102
def rsync(source, target, ssh=False, excludes=(), silent=False,
 
103
          rsync_name="rsync"):
 
104
    """
 
105
    >>> new_dir = tempfile.mkdtemp()
 
106
    >>> old_dir = os.getcwd()
 
107
    >>> os.chdir(new_dir)
 
108
    >>> rsync("a", "b", silent=True)
 
109
    Traceback (most recent call last):
 
110
    RsyncNoFile: No such file...
 
111
    >>> rsync(new_dir + "/a", new_dir + "/b", excludes=("*.py",), silent=True)
 
112
    Traceback (most recent call last):
 
113
    RsyncNoFile: No such file...
 
114
    >>> rsync(new_dir + "/a", new_dir + "/b", excludes=("*.py",), silent=True, rsync_name="rsyncc")
 
115
    Traceback (most recent call last):
 
116
    NoRsync: rsyncc not found.
 
117
    >>> os.chdir(old_dir)
 
118
    >>> os.rmdir(new_dir)
 
119
    """
 
120
    cmd = [rsync_name, "-av", "--delete"]
116
121
    if ssh:
117
122
        cmd.extend(('-e', 'ssh'))
118
 
    for exclude in exclude_globs:
119
 
        cmd.extend(('--exclude', exclude))
 
123
    if len(excludes) > 0:
 
124
        cmd.extend(('--exclude-from', '-'))
120
125
    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)
 
126
    if silent:
 
127
        stderr = PIPE
 
128
        stdout = PIPE
 
129
    else:
 
130
        stderr = None
 
131
        stdout = None
 
132
    try:
 
133
        proc = Popen(cmd, stdin=PIPE, stderr=stderr, stdout=stdout)
 
134
    except OSError, e:
 
135
        if e.errno == errno.ENOENT:
 
136
            raise NoRsync(rsync_name)
 
137
 
 
138
    proc.stdin.write('\n'.join(excludes)+'\n')
 
139
    proc.stdin.close()
 
140
    if silent:
 
141
        proc.stderr.read()
 
142
        proc.stderr.close()
 
143
        proc.stdout.read()
 
144
        proc.stdout.close()
 
145
    proc.wait()
 
146
    if proc.returncode == 12:
 
147
        raise RsyncStreamIO()
 
148
    elif proc.returncode == 23:
 
149
        raise RsyncNoFile(source)
 
150
    elif proc.returncode != 0:
 
151
        raise RsyncUnknownStatus(proc.returncode)
 
152
    return cmd
 
153
 
 
154
 
 
155
def rsync_ls(source, ssh=False, silent=True):
 
156
    cmd = ["rsync"]
 
157
    if ssh:
 
158
        cmd.extend(('-e', 'ssh'))
 
159
    cmd.append(source)
 
160
    if silent:
 
161
        stderr = PIPE
 
162
    else:
 
163
        stderr = None
 
164
    proc = Popen(cmd, stderr=stderr, stdout=PIPE)
 
165
    result = proc.stdout.read()
 
166
    proc.stdout.close()
 
167
    if silent:
 
168
        proc.stderr.read()
 
169
        proc.stderr.close()
 
170
    proc.wait()
 
171
    if proc.returncode == 12:
 
172
        raise RsyncStreamIO()
 
173
    elif proc.returncode == 23:
 
174
        raise RsyncNoFile(source)
 
175
    elif proc.returncode != 0:
 
176
        raise RsyncUnknownStatus(proc.returncode)
 
177
    return [l.split(' ')[-1].rstrip('\n') for l in result.splitlines(True)]
 
178
 
 
179
exclusions = ('.bzr/x-push-data', '.bzr/branch/x-push/data', '.bzr/parent',
 
180
              '.bzr/branch/parent', '.bzr/x-pull-data', '.bzr/x-pull',
 
181
              '.bzr/pull', '.bzr/stat-cache', '.bzr/x-rsync-data',
 
182
              '.bzr/basis-inventory', '.bzr/inventory.backup.weave')
 
183
 
 
184
 
 
185
def read_revision_history(fname):
 
186
    return [l.rstrip('\r\n') for l in
 
187
            codecs.open(fname, 'rb', 'utf-8').readlines()]
 
188
 
 
189
class RsyncNoFile(Exception):
 
190
    def __init__(self, path):
 
191
        Exception.__init__(self, "No such file %s" % path)
 
192
 
 
193
class RsyncStreamIO(Exception):
 
194
    def __init__(self):
 
195
        Exception.__init__(self, "Error in rsync protocol data stream.")
 
196
 
 
197
def get_revision_history(location):
 
198
    tempdir = tempfile.mkdtemp('push')
 
199
    try:
 
200
        history_fname = os.path.join(tempdir, 'revision-history')
 
201
        try:
 
202
            cmd = rsync(location+'.bzr/revision-history', history_fname,
 
203
                        silent=True)
 
204
        except RsyncNoFile:
 
205
            cmd = rsync(location+'.bzr/branch/revision-history', history_fname,
 
206
                        silent=True)
 
207
        history = read_revision_history(history_fname)
 
208
    finally:
 
209
        shutil.rmtree(tempdir)
 
210
    return history
 
211
 
 
212
def history_subset(location, branch):
 
213
    remote_history = get_revision_history(location)
 
214
    local_history = branch.revision_history()
 
215
    if len(remote_history) > len(local_history):
 
216
        return False
 
217
    for local, remote in zip(remote_history, local_history):
 
218
        if local != remote:
 
219
            return False
 
220
    return True
 
221
 
 
222
def empty_or_absent(location):
 
223
    try:
 
224
        files = rsync_ls(location)
 
225
        return files == ['.']
 
226
    except RsyncNoFile:
 
227
        return True
 
228
 
 
229
def rspush(tree, location=None, overwrite=False, working_tree=True):
 
230
    push_location = get_push_data(tree)
153
231
    if location is not None:
154
232
        if not location.endswith('/'):
155
233
            location += '/'
156
234
        push_location = location
157
 
    
 
235
 
158
236
    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
 
 
 
237
        raise BzrCommandError("No rspush location known or specified.")
 
238
 
 
239
    if (push_location.find('::') != -1):
 
240
        usessh=False
 
241
    else:
 
242
        usessh=True
 
243
 
 
244
    if (push_location.find('://') != -1 or
 
245
        push_location.find(':') == -1):
 
246
        raise BzrCommandError("Invalid rsync path %r." % push_location)
 
247
 
 
248
    if working_tree:
 
249
        clean, non_source = is_clean(tree)
 
250
        if not clean:
 
251
            print """Error: This tree has uncommitted changes or unknown (?) files.
 
252
    Use "bzr status" to list them."""
 
253
            sys.exit(1)
 
254
        final_exclusions = non_source[:]
 
255
    else:
 
256
        wt = tree
 
257
        final_exclusions = []
 
258
        for path, status, kind, file_id, entry in wt.list_files():
 
259
            final_exclusions.append(path)
 
260
 
 
261
    final_exclusions.extend(exclusions)
 
262
    if not overwrite:
 
263
        try:
 
264
            if not history_subset(push_location, tree.branch):
 
265
                raise bzrlib.errors.BzrCommandError("Local branch is not a"
 
266
                                                    " newer version of remote"
 
267
                                                    " branch.")
 
268
        except RsyncNoFile:
 
269
            if not empty_or_absent(push_location):
 
270
                raise bzrlib.errors.BzrCommandError("Remote location is not a"
 
271
                                                    " bzr branch (or empty"
 
272
                                                    " directory)")
 
273
        except RsyncStreamIO:
 
274
            raise bzrlib.errors.BzrCommandError("Rsync could not use the"
 
275
                " specified location.  Please ensure that"
 
276
                ' "%s" is of the form "machine:/path".' % push_location)
166
277
    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)
 
278
    rsync(tree.basedir+'/', push_location, ssh=usessh,
 
279
          excludes=final_exclusions)
 
280
 
 
281
    set_push_data(tree, push_location)
 
282
 
 
283
 
 
284
def short_committer(committer):
 
285
    new_committer = re.sub('<.*>', '', committer).strip(' ')
 
286
    if len(new_committer) < 2:
 
287
        return committer
 
288
    return new_committer
 
289
 
 
290
 
 
291
def apache_ls(t):
 
292
    """Screen-scrape Apache listings"""
 
293
    apache_dir = '<img border="0" src="/icons/folder.gif" alt="[dir]">'\
 
294
        ' <a href="'
 
295
    lines = t.get('.')
 
296
    expr = re.compile('<a[^>]*href="([^>]*)"[^>]*>', flags=re.I)
 
297
    for line in lines:
 
298
        match = expr.search(line)
 
299
        if match is None:
 
300
            continue
 
301
        url = match.group(1)
 
302
        if url.startswith('http://') or url.startswith('/') or '../' in url:
 
303
            continue
 
304
        if '?' in url:
 
305
            continue
 
306
        yield url.rstrip('/')
 
307
 
 
308
 
 
309
def iter_branches(t, lister=None):
 
310
    """Iterate through all the branches under a transport"""
 
311
    for bzrdir in iter_bzrdirs(t, lister):
 
312
        try:
 
313
            branch = bzrdir.open_branch()
 
314
            if branch.bzrdir is bzrdir:
 
315
                yield branch
 
316
        except (NotBranchError, UnsupportedFormatError):
 
317
            pass
 
318
 
 
319
 
 
320
def iter_branch_tree(t, lister=None):
 
321
    for bzrdir in iter_bzrdirs(t, lister):
 
322
        try:
 
323
            wt = bzrdir.open_workingtree()
 
324
            yield wt.branch, wt
 
325
        except NoWorkingTree, UnsupportedFormatError:
 
326
            try:
 
327
                branch = bzrdir.open_branch()
 
328
                if branch.bzrdir is bzrdir:
 
329
                    yield branch, None
 
330
            except (NotBranchError, UnsupportedFormatError):
 
331
                continue
 
332
 
 
333
 
 
334
def iter_bzrdirs(t, lister=None):
 
335
    if lister is None:
 
336
        def lister(t):
 
337
            return t.list_dir('.')
 
338
    try:
 
339
        bzrdir = bzrdir_from_transport(t)
 
340
        yield bzrdir
 
341
    except (NotBranchError, UnsupportedFormatError, TransportError,
 
342
            PermissionDenied):
 
343
        pass
 
344
    try:
 
345
        for directory in lister(t):
 
346
            if directory == ".bzr":
 
347
                continue
 
348
            try:
 
349
                subt = t.clone(directory)
 
350
            except UnicodeDecodeError:
 
351
                continue
 
352
            for bzrdir in iter_bzrdirs(subt, lister):
 
353
                yield bzrdir
 
354
    except (NoSuchFile, PermissionDenied, TransportError):
 
355
        pass
 
356
 
 
357
 
 
358
def bzrdir_from_transport(t):
 
359
    """Open a bzrdir from a transport (not a location)"""
 
360
    format = BzrDirFormat.find_format(t)
 
361
    BzrDir._check_supported(format, False)
 
362
    return format.open(t)
 
363
 
171
364
 
172
365
def run_tests():
173
366
    import doctest