~abentley/bzrtools/bzrtools.dev

89 by Aaron Bentley
Added copyright/GPL notices
1
# Copyright (C) 2005 Aaron Bentley
2
# <aaron.bentley@utoronto.ca>
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
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
17
import codecs
18
import errno
19 by abentley
librified most of the pull script
19
import os
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
20
import re
16 by abentley
Got is_clean under test, added setters/getters for pull data
21
import tempfile
22
import shutil
117 by aaron.bentley at utoronto
Excluded non-source files
23
from subprocess import Popen, PIPE
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
24
import sys
25
26
import bzrlib
27
import bzrlib.errors
352 by Aaron Bentley
Added branches subcommand
28
from bzrlib.errors import (BzrCommandError, NotBranchError, NoSuchFile,
353 by Aaron Bentley
Added multi-pull, working on branches and checkouts
29
                           UnsupportedFormatError, TransportError, 
356 by Aaron Bentley
Fixed robustness issues in branches command
30
                           NoWorkingTree, PermissionDenied)
352 by Aaron Bentley
Added branches subcommand
31
from bzrlib.bzrdir import BzrDir, BzrDirFormat
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
32
33
def temp_tree():
16 by abentley
Got is_clean under test, added setters/getters for pull data
34
    dirname = tempfile.mkdtemp("temp-branch")
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
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 by abentley
Got is_clean under test, added setters/getters for pull data
41
    """
42
    Return true if no files are modifed or unknown
147 by Robert Collins
make bzr selftest run the plugins tests, and fix them
43
    >>> import bzrlib.add
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
44
    >>> tree = temp_tree()
45
    >>> is_clean(tree)
147 by Robert Collins
make bzr selftest run the plugins tests, and fix them
46
    (True, [])
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
47
    >>> fooname = os.path.join(tree.basedir, "foo")
16 by abentley
Got is_clean under test, added setters/getters for pull data
48
    >>> file(fooname, "wb").write("bar")
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
49
    >>> is_clean(tree)
239 by Aaron Bentley
Fixed test case
50
    (True, [u'foo'])
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
51
    >>> bzrlib.add.smart_add_tree(tree, [tree.basedir])
52
    ([u'foo'], {})
53
    >>> is_clean(tree)
147 by Robert Collins
make bzr selftest run the plugins tests, and fix them
54
    (False, [])
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
55
    >>> tree.commit("added file")
56
    >>> is_clean(tree)
147 by Robert Collins
make bzr selftest run the plugins tests, and fix them
57
    (True, [])
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
58
    >>> rm_tree(tree)
16 by abentley
Got is_clean under test, added setters/getters for pull data
59
    """
95 by Aaron Bentley
Updated to use compare_trees directly from diff
60
    from bzrlib.diff import compare_trees
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
61
    old_tree = cur_tree.basis_tree()
62
    new_tree = cur_tree
117 by aaron.bentley at utoronto
Excluded non-source files
63
    non_source = []
209 by Aaron Bentley
updated to match Tree.list_files sig change
64
    for path, file_class, kind, file_id, entry in new_tree.list_files():
117 by aaron.bentley at utoronto
Excluded non-source files
65
        if file_class in ('?', 'I'):
66
            non_source.append(path)
95 by Aaron Bentley
Updated to use compare_trees directly from diff
67
    delta = compare_trees(old_tree, new_tree, want_unchanged=False)
257.1.3 by Aaron Bentley
Switched to TreeDelta.has_changed
68
    return not delta.has_changed(), non_source
16 by abentley
Got is_clean under test, added setters/getters for pull data
69
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
70
def set_push_data(tree, location):
331 by Aaron Bentley
Get/set control push locations per branch
71
    push_file = file (tree.branch.control_files.controlfilename("x-push-data"), "wb")
20 by abentley
added bzr-push command
72
    push_file.write("%s\n" % location)
73
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
74
def get_push_data(tree):
20 by abentley
added bzr-push command
75
    """
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
76
    >>> tree = temp_tree()
77
    >>> get_push_data(tree) is None
20 by abentley
added bzr-push command
78
    True
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
79
    >>> set_push_data(tree, 'http://somewhere')
80
    >>> get_push_data(tree)
20 by abentley
added bzr-push command
81
    'http://somewhere'
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
82
    >>> rm_tree(tree)
20 by abentley
added bzr-push command
83
    """
331 by Aaron Bentley
Get/set control push locations per branch
84
    filename = tree.branch.control_files.controlfilename("x-push-data")
20 by abentley
added bzr-push command
85
    if not os.path.exists(filename):
86
        return None
87
    push_file = file (filename, "rb")
88
    (location,) = [f.rstrip('\n') for f in push_file]
89
    return location
90
19 by abentley
librified most of the pull script
91
"""
92
>>> shell_escape('hello')
93
'\h\e\l\l\o'
94
"""
95
def shell_escape(arg):
96
    return "".join(['\\'+c for c in arg])
97
98
def safe_system(args):
99
    """
100
    >>> real_system = os.system
101
    >>> os.system = sys.stdout.write
102
    >>> safe_system(['a', 'b', 'cd'])
103
    \\a \\b \\c\\d
104
    >>> os.system = real_system
105
    """
106
    arg_str = " ".join([shell_escape(a) for a in args])
107
    return os.system(arg_str)
108
195 by Aaron Bentley
prevented accidental overwrites from push
109
class RsyncUnknownStatus(Exception):
110
    def __init__(self, status):
111
        Exception.__init__(self, "Unknown status: %d" % status)
112
199 by Aaron Bentley
Updated doctests
113
class NoRsync(Exception):
114
    def __init__(self, rsync_name):
115
        Exception.__init__(self, "%s not found." % rsync_name)
116
117
def rsync(source, target, ssh=False, excludes=(), silent=False, 
118
          rsync_name="rsync"):
19 by abentley
librified most of the pull script
119
    """
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
120
    >>> new_dir = tempfile.mkdtemp()
121
    >>> old_dir = os.getcwd()
122
    >>> os.chdir(new_dir)
198 by Aaron Bentley
Updated doctests
123
    >>> rsync("a", "b", silent=True)
147.4.29 by Robert Collins
Make the rsync tests independent of cwd.
124
    Traceback (most recent call last):
321.1.2 by Aaron Bentley
Applied Robert's random fixes as non-merges
125
    RsyncNoFile: No such file...
321.1.3 by Aaron Bentley
Fixed up Robert's test changes
126
    >>> rsync(new_dir + "/a", new_dir + "/b", excludes=("*.py",), silent=True)
147.4.29 by Robert Collins
Make the rsync tests independent of cwd.
127
    Traceback (most recent call last):
321.1.2 by Aaron Bentley
Applied Robert's random fixes as non-merges
128
    RsyncNoFile: No such file...
321.1.3 by Aaron Bentley
Fixed up Robert's test changes
129
    >>> rsync(new_dir + "/a", new_dir + "/b", excludes=("*.py",), silent=True, rsync_name="rsyncc")
199 by Aaron Bentley
Updated doctests
130
    Traceback (most recent call last):
131
    NoRsync: rsyncc not found.
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
132
    >>> os.chdir(old_dir)
133
    >>> os.rmdir(new_dir)
19 by abentley
librified most of the pull script
134
    """
199 by Aaron Bentley
Updated doctests
135
    cmd = [rsync_name, "-av", "--delete"]
20 by abentley
added bzr-push command
136
    if ssh:
137
        cmd.extend(('-e', 'ssh'))
117 by aaron.bentley at utoronto
Excluded non-source files
138
    if len(excludes) > 0:
139
        cmd.extend(('--exclude-from', '-'))
20 by abentley
added bzr-push command
140
    cmd.extend((source, target))
195 by Aaron Bentley
prevented accidental overwrites from push
141
    if silent:
142
        stderr = PIPE
143
        stdout = PIPE
144
    else:
145
        stderr = None
146
        stdout = None
199 by Aaron Bentley
Updated doctests
147
    try:
148
        proc = Popen(cmd, stdin=PIPE, stderr=stderr, stdout=stdout)
149
    except OSError, e:
150
        if e.errno == errno.ENOENT:
151
            raise NoRsync(rsync_name)
152
            
117 by aaron.bentley at utoronto
Excluded non-source files
153
    proc.stdin.write('\n'.join(excludes)+'\n')
154
    proc.stdin.close()
195 by Aaron Bentley
prevented accidental overwrites from push
155
    if silent:
156
        proc.stderr.read()
157
        proc.stderr.close()
158
        proc.stdout.read()
159
        proc.stdout.close()
147 by Robert Collins
make bzr selftest run the plugins tests, and fix them
160
    proc.wait()
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
161
    if proc.returncode == 12:
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
162
        raise RsyncStreamIO()
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
163
    elif proc.returncode == 23:
195 by Aaron Bentley
prevented accidental overwrites from push
164
        raise RsyncNoFile(source)
165
    elif proc.returncode != 0:
166
        raise RsyncUnknownStatus(proc.returncode)
147 by Robert Collins
make bzr selftest run the plugins tests, and fix them
167
    return cmd
117 by aaron.bentley at utoronto
Excluded non-source files
168
195 by Aaron Bentley
prevented accidental overwrites from push
169
170
def rsync_ls(source, ssh=False, silent=True):
171
    cmd = ["rsync"]
172
    if ssh:
173
        cmd.extend(('-e', 'ssh'))
174
    cmd.append(source)
175
    if silent:
176
        stderr = PIPE
177
    else:
178
        stderr = None
179
    proc = Popen(cmd, stderr=stderr, stdout=PIPE)
180
    result = proc.stdout.read()
181
    proc.stdout.close()
182
    if silent:
183
        proc.stderr.read()
184
        proc.stderr.close()
185
    proc.wait()
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
186
    if proc.returncode == 12:
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
187
        raise RsyncStreamIO()
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
188
    elif proc.returncode == 23:
195 by Aaron Bentley
prevented accidental overwrites from push
189
        raise RsyncNoFile(source)
190
    elif proc.returncode != 0:
191
        raise RsyncUnknownStatus(proc.returncode)
192
    return [l.split(' ')[-1].rstrip('\n') for l in result.splitlines(True)]
193
332 by Aaron Bentley
Added exclusions for x-push and parent in format6 branches
194
exclusions = ('.bzr/x-push-data', '.bzr/branch/x-push/data', '.bzr/parent', 
195
              '.bzr/branch/parent', '.bzr/x-pull-data', '.bzr/x-pull',
196
              '.bzr/pull', '.bzr/stat-cache', '.bzr/x-rsync-data',
197
              '.bzr/basis-inventory', '.bzr/inventory.backup.weave')
19 by abentley
librified most of the pull script
198
20 by abentley
added bzr-push command
199
195 by Aaron Bentley
prevented accidental overwrites from push
200
def read_revision_history(fname):
201
    return [l.rstrip('\r\n') for l in
202
            codecs.open(fname, 'rb', 'utf-8').readlines()]
203
204
class RsyncNoFile(Exception):
205
    def __init__(self, path):
206
        Exception.__init__(self, "No such file %s" % path)
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
207
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
208
class RsyncStreamIO(Exception):
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
209
    def __init__(self):
210
        Exception.__init__(self, "Error in rsync protocol data stream.")
195 by Aaron Bentley
prevented accidental overwrites from push
211
212
def get_revision_history(location):
213
    tempdir = tempfile.mkdtemp('push')
214
    try:
215
        history_fname = os.path.join(tempdir, 'revision-history')
330 by Aaron Bentley
Got push working with new-format branches
216
        try:
217
            cmd = rsync(location+'.bzr/revision-history', history_fname,
218
                        silent=True)
219
        except RsyncNoFile:
220
            cmd = rsync(location+'.bzr/branch/revision-history', history_fname,
221
                        silent=True)
195 by Aaron Bentley
prevented accidental overwrites from push
222
        history = read_revision_history(history_fname)
223
    finally:
224
        shutil.rmtree(tempdir)
225
    return history
226
227
def history_subset(location, branch):
228
    remote_history = get_revision_history(location)
229
    local_history = branch.revision_history()
230
    if len(remote_history) > len(local_history):
231
        return False
232
    for local, remote in zip(remote_history, local_history):
233
        if local != remote:
234
            return False 
235
    return True
236
237
def empty_or_absent(location):
238
    try:
239
        files = rsync_ls(location)
240
        return files == ['.']
241
    except RsyncNoFile:
242
        return True
243
364.1.4 by Aaron Bentley
Changed rpush to rspush
244
def rspush(tree, location=None, overwrite=False, working_tree=True):
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
245
    push_location = get_push_data(tree)
20 by abentley
added bzr-push command
246
    if location is not None:
25 by abentley
fixed push for x files, tracefile, push/pull miscommunication
247
        if not location.endswith('/'):
248
            location += '/'
20 by abentley
added bzr-push command
249
        push_location = location
250
    
251
    if push_location is None:
364.1.4 by Aaron Bentley
Changed rpush to rspush
252
        raise BzrCommandError("No rspush location known or specified.")
147.4.37 by Robert Collins
Convert push to rpush.
253
254
    if (push_location.find('://') != -1 or
255
        push_location.find(':') == -1):
256
        raise BzrCommandError("Invalid rsync path %r." % push_location)
20 by abentley
added bzr-push command
257
303 by Aaron Bentley
Added support for pushing with no working tree
258
    if working_tree:
333 by Aaron Bentley
Only error on dirty wt if we are pushing the wt
259
        clean, non_source = is_clean(tree)
260
        if not clean:
261
            print """Error: This tree has uncommitted changes or unknown (?) files.
262
    Use "bzr status" to list them."""
263
            sys.exit(1)
303 by Aaron Bentley
Added support for pushing with no working tree
264
        final_exclusions = non_source[:]
265
    else:
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
266
        wt = tree
303 by Aaron Bentley
Added support for pushing with no working tree
267
        final_exclusions = []
268
        for path, status, kind, file_id, entry in wt.list_files():
269
            final_exclusions.append(path)
270
271
    final_exclusions.extend(exclusions)
195 by Aaron Bentley
prevented accidental overwrites from push
272
    if not overwrite:
273
        try:
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
274
            if not history_subset(push_location, tree.branch):
195 by Aaron Bentley
prevented accidental overwrites from push
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)")
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
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)
20 by abentley
added bzr-push command
287
    print "Pushing to %s" % push_location
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
288
    rsync(tree.basedir+'/', push_location, ssh=True, 
303 by Aaron Bentley
Added support for pushing with no working tree
289
          excludes=final_exclusions)
20 by abentley
added bzr-push command
290
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
291
    set_push_data(tree, push_location)
20 by abentley
added bzr-push command
292
321.1.2 by Aaron Bentley
Applied Robert's random fixes as non-merges
293
292 by Aaron Bentley
Introduced branch-history command
294
def short_committer(committer):
295
    new_committer = re.sub('<.*>', '', committer).strip(' ')
296
    if len(new_committer) < 2:
297
        return committer
298
    return new_committer
299
300
354 by Aaron Bentley
Added apache index scraping to the branches command
301
def apache_ls(t):
302
    """Screen-scrape Apache listings"""
355 by Aaron Bentley
Handled more hrefs
303
    apache_dir = '<img border="0" src="/icons/folder.gif" alt="[dir]">'\
304
        ' <a href="'
354 by Aaron Bentley
Added apache index scraping to the branches command
305
    lines = t.get('.')
355 by Aaron Bentley
Handled more hrefs
306
    expr = re.compile('<a[^>]*href="([^>]*)"[^>]*>', flags=re.I)
354 by Aaron Bentley
Added apache index scraping to the branches command
307
    for line in lines:
355 by Aaron Bentley
Handled more hrefs
308
        match = expr.search(line)
309
        if match is None:
310
            continue
311
        url = match.group(1)
312
        if url.startswith('http://') or url.startswith('/') or '../' in url:
313
            continue
357 by Aaron Bentley
Tweakage of the apache scraper
314
        if '?' in url:
315
            continue
355 by Aaron Bentley
Handled more hrefs
316
        yield url.rstrip('/')
354 by Aaron Bentley
Added apache index scraping to the branches command
317
318
319
def iter_branches(t, lister=None):
352 by Aaron Bentley
Added branches subcommand
320
    """Iterate through all the branches under a transport"""
354 by Aaron Bentley
Added apache index scraping to the branches command
321
    for bzrdir in iter_bzrdirs(t, lister):
353 by Aaron Bentley
Added multi-pull, working on branches and checkouts
322
        try:
323
            branch = bzrdir.open_branch()
324
            if branch.bzrdir is bzrdir:
325
                yield branch
326
        except (NotBranchError, UnsupportedFormatError):
327
            pass
328
354 by Aaron Bentley
Added apache index scraping to the branches command
329
330
def iter_branch_tree(t, lister=None):
331
    for bzrdir in iter_bzrdirs(t, lister):
353 by Aaron Bentley
Added multi-pull, working on branches and checkouts
332
        try:
333
            wt = bzrdir.open_workingtree()
334
            yield wt.branch, wt
335
        except NoWorkingTree, UnsupportedFormatError:
336
            try:
337
                branch = bzrdir.open_branch()
338
                if branch.bzrdir is bzrdir:
339
                    yield branch, None
340
            except (NotBranchError, UnsupportedFormatError):
341
                continue
342
356 by Aaron Bentley
Fixed robustness issues in branches command
343
354 by Aaron Bentley
Added apache index scraping to the branches command
344
def iter_bzrdirs(t, lister=None):
345
    if lister is None:
346
        def lister(t):
347
            return t.list_dir('.')
352 by Aaron Bentley
Added branches subcommand
348
    try:
349
        bzrdir = bzrdir_from_transport(t)
353 by Aaron Bentley
Added multi-pull, working on branches and checkouts
350
        yield bzrdir
356 by Aaron Bentley
Fixed robustness issues in branches command
351
    except (NotBranchError, UnsupportedFormatError, TransportError,
352
            PermissionDenied):
352 by Aaron Bentley
Added branches subcommand
353
        pass
354
    try:
354 by Aaron Bentley
Added apache index scraping to the branches command
355
        for directory in lister(t):
352 by Aaron Bentley
Added branches subcommand
356
            if directory == ".bzr":
357
                continue
356 by Aaron Bentley
Fixed robustness issues in branches command
358
            try:
359
                subt = t.clone(directory)
360
            except UnicodeDecodeError:
361
                continue
357 by Aaron Bentley
Tweakage of the apache scraper
362
            for bzrdir in iter_bzrdirs(subt, lister):
353 by Aaron Bentley
Added multi-pull, working on branches and checkouts
363
                yield bzrdir
356 by Aaron Bentley
Fixed robustness issues in branches command
364
    except (NoSuchFile, PermissionDenied, TransportError):
352 by Aaron Bentley
Added branches subcommand
365
        pass
366
367
    
368
def bzrdir_from_transport(t):
369
    """Open a bzrdir from a transport (not a location)"""
370
    format = BzrDirFormat.find_format(t)
371
    BzrDir._check_supported(format, False)
372
    return format.open(t)
373
374
19 by abentley
librified most of the pull script
375
def run_tests():
16 by abentley
Got is_clean under test, added setters/getters for pull data
376
    import doctest
18 by abentley
Finished implementing bzr-pull
377
    result = doctest.testmod()
19 by abentley
librified most of the pull script
378
    if result[1] > 0:
379
        if result[0] == 0:
380
            print "All tests passed"
381
    else:
382
        print "No tests to run"
383
if __name__ == "__main__":
384
    run_tests()