~abentley/bzrtools/bzrtools.dev

513 by Aaron Bentley
Fix change dectection for dirstate
1
# Copyright (C) 2005, 2006, 2007 Aaron Bentley <aaron.bentley@utoronto.ca>
2
# Copyright (C) 2007 John Arbash Meinel
89 by Aaron Bentley
Added copyright/GPL notices
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
563 by Aaron Bentley
Allow importing directly from a URL
27
from bzrlib import urlutils
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
28
import bzrlib.errors
352 by Aaron Bentley
Added branches subcommand
29
from bzrlib.errors import (BzrCommandError, NotBranchError, NoSuchFile,
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
30
                           UnsupportedFormatError, TransportError,
356 by Aaron Bentley
Fixed robustness issues in branches command
31
                           NoWorkingTree, PermissionDenied)
352 by Aaron Bentley
Added branches subcommand
32
from bzrlib.bzrdir import BzrDir, BzrDirFormat
563 by Aaron Bentley
Allow importing directly from a URL
33
from bzrlib.transport import get_transport
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
34
35
def temp_tree():
16 by abentley
Got is_clean under test, added setters/getters for pull data
36
    dirname = tempfile.mkdtemp("temp-branch")
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
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 by abentley
Got is_clean under test, added setters/getters for pull data
43
    """
44
    Return true if no files are modifed or unknown
45
    """
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
46
    old_tree = cur_tree.basis_tree()
47
    new_tree = cur_tree
117 by aaron.bentley at utoronto
Excluded non-source files
48
    non_source = []
513 by Aaron Bentley
Fix change dectection for dirstate
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()
257.1.3 by Aaron Bentley
Switched to TreeDelta.has_changed
57
    return not delta.has_changed(), non_source
16 by abentley
Got is_clean under test, added setters/getters for pull data
58
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
59
def set_push_data(tree, location):
394 by Aaron Bentley
Use get_utf8 and put_utf8 instead of controlfilename
60
    tree.branch.control_files.put_utf8("x-push-data", "%s\n" % location)
20 by abentley
added bzr-push command
61
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
62
def get_push_data(tree):
20 by abentley
added bzr-push command
63
    """
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
64
    >>> tree = temp_tree()
65
    >>> get_push_data(tree) is None
20 by abentley
added bzr-push command
66
    True
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
67
    >>> set_push_data(tree, 'http://somewhere')
68
    >>> get_push_data(tree)
394 by Aaron Bentley
Use get_utf8 and put_utf8 instead of controlfilename
69
    u'http://somewhere'
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
70
    >>> rm_tree(tree)
20 by abentley
added bzr-push command
71
    """
394 by Aaron Bentley
Use get_utf8 and put_utf8 instead of controlfilename
72
    try:
73
        location = tree.branch.control_files.get_utf8('x-push-data').read()
74
    except NoSuchFile:
20 by abentley
added bzr-push command
75
        return None
394 by Aaron Bentley
Use get_utf8 and put_utf8 instead of controlfilename
76
    return location.rstrip('\n')
20 by abentley
added bzr-push command
77
19 by abentley
librified most of the pull script
78
"""
79
>>> shell_escape('hello')
80
'\h\e\l\l\o'
81
"""
82
def shell_escape(arg):
83
    return "".join(['\\'+c for c in arg])
84
85
def safe_system(args):
86
    """
87
    >>> real_system = os.system
88
    >>> os.system = sys.stdout.write
89
    >>> safe_system(['a', 'b', 'cd'])
90
    \\a \\b \\c\\d
91
    >>> os.system = real_system
92
    """
93
    arg_str = " ".join([shell_escape(a) for a in args])
94
    return os.system(arg_str)
95
195 by Aaron Bentley
prevented accidental overwrites from push
96
class RsyncUnknownStatus(Exception):
97
    def __init__(self, status):
98
        Exception.__init__(self, "Unknown status: %d" % status)
99
199 by Aaron Bentley
Updated doctests
100
class NoRsync(Exception):
101
    def __init__(self, rsync_name):
102
        Exception.__init__(self, "%s not found." % rsync_name)
103
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
104
def rsync(source, target, ssh=False, excludes=(), silent=False,
199 by Aaron Bentley
Updated doctests
105
          rsync_name="rsync"):
19 by abentley
librified most of the pull script
106
    """
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
107
    >>> new_dir = tempfile.mkdtemp()
108
    >>> old_dir = os.getcwd()
109
    >>> os.chdir(new_dir)
198 by Aaron Bentley
Updated doctests
110
    >>> rsync("a", "b", silent=True)
147.4.29 by Robert Collins
Make the rsync tests independent of cwd.
111
    Traceback (most recent call last):
321.1.2 by Aaron Bentley
Applied Robert's random fixes as non-merges
112
    RsyncNoFile: No such file...
321.1.3 by Aaron Bentley
Fixed up Robert's test changes
113
    >>> rsync(new_dir + "/a", new_dir + "/b", excludes=("*.py",), silent=True)
147.4.29 by Robert Collins
Make the rsync tests independent of cwd.
114
    Traceback (most recent call last):
321.1.2 by Aaron Bentley
Applied Robert's random fixes as non-merges
115
    RsyncNoFile: No such file...
321.1.3 by Aaron Bentley
Fixed up Robert's test changes
116
    >>> rsync(new_dir + "/a", new_dir + "/b", excludes=("*.py",), silent=True, rsync_name="rsyncc")
199 by Aaron Bentley
Updated doctests
117
    Traceback (most recent call last):
118
    NoRsync: rsyncc not found.
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
119
    >>> os.chdir(old_dir)
120
    >>> os.rmdir(new_dir)
19 by abentley
librified most of the pull script
121
    """
199 by Aaron Bentley
Updated doctests
122
    cmd = [rsync_name, "-av", "--delete"]
20 by abentley
added bzr-push command
123
    if ssh:
124
        cmd.extend(('-e', 'ssh'))
117 by aaron.bentley at utoronto
Excluded non-source files
125
    if len(excludes) > 0:
126
        cmd.extend(('--exclude-from', '-'))
20 by abentley
added bzr-push command
127
    cmd.extend((source, target))
195 by Aaron Bentley
prevented accidental overwrites from push
128
    if silent:
129
        stderr = PIPE
130
        stdout = PIPE
131
    else:
132
        stderr = None
133
        stdout = None
199 by Aaron Bentley
Updated doctests
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)
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
139
117 by aaron.bentley at utoronto
Excluded non-source files
140
    proc.stdin.write('\n'.join(excludes)+'\n')
141
    proc.stdin.close()
195 by Aaron Bentley
prevented accidental overwrites from push
142
    if silent:
143
        proc.stderr.read()
144
        proc.stderr.close()
145
        proc.stdout.read()
146
        proc.stdout.close()
147 by Robert Collins
make bzr selftest run the plugins tests, and fix them
147
    proc.wait()
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
148
    if proc.returncode == 12:
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
149
        raise RsyncStreamIO()
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
150
    elif proc.returncode == 23:
195 by Aaron Bentley
prevented accidental overwrites from push
151
        raise RsyncNoFile(source)
152
    elif proc.returncode != 0:
153
        raise RsyncUnknownStatus(proc.returncode)
147 by Robert Collins
make bzr selftest run the plugins tests, and fix them
154
    return cmd
117 by aaron.bentley at utoronto
Excluded non-source files
155
195 by Aaron Bentley
prevented accidental overwrites from push
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()
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
173
    if proc.returncode == 12:
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
174
        raise RsyncStreamIO()
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
175
    elif proc.returncode == 23:
195 by Aaron Bentley
prevented accidental overwrites from push
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
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
181
exclusions = ('.bzr/x-push-data', '.bzr/branch/x-push/data', '.bzr/parent',
332 by Aaron Bentley
Added exclusions for x-push and parent in format6 branches
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')
19 by abentley
librified most of the pull script
185
20 by abentley
added bzr-push command
186
195 by Aaron Bentley
prevented accidental overwrites from push
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)
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
194
200.1.1 by Eirik Nygaard
Add check for rsync return code 12, error in rsync protocol data stream.
195
class RsyncStreamIO(Exception):
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
196
    def __init__(self):
197
        Exception.__init__(self, "Error in rsync protocol data stream.")
195 by Aaron Bentley
prevented accidental overwrites from push
198
199
def get_revision_history(location):
200
    tempdir = tempfile.mkdtemp('push')
201
    try:
202
        history_fname = os.path.join(tempdir, 'revision-history')
330 by Aaron Bentley
Got push working with new-format branches
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)
195 by Aaron Bentley
prevented accidental overwrites from push
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:
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
221
            return False
195 by Aaron Bentley
prevented accidental overwrites from push
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
364.1.4 by Aaron Bentley
Changed rpush to rspush
231
def rspush(tree, location=None, overwrite=False, working_tree=True):
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
232
    push_location = get_push_data(tree)
20 by abentley
added bzr-push command
233
    if location is not None:
25 by abentley
fixed push for x files, tracefile, push/pull miscommunication
234
        if not location.endswith('/'):
235
            location += '/'
20 by abentley
added bzr-push command
236
        push_location = location
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
237
20 by abentley
added bzr-push command
238
    if push_location is None:
364.1.4 by Aaron Bentley
Changed rpush to rspush
239
        raise BzrCommandError("No rspush location known or specified.")
147.4.37 by Robert Collins
Convert push to rpush.
240
472 by Aaron Bentley
Make rspush work with rsync servers (Tridge)
241
    if (push_location.find('::') != -1):
242
        usessh=False
243
    else:
244
        usessh=True
245
147.4.37 by Robert Collins
Convert push to rpush.
246
    if (push_location.find('://') != -1 or
247
        push_location.find(':') == -1):
248
        raise BzrCommandError("Invalid rsync path %r." % push_location)
20 by abentley
added bzr-push command
249
303 by Aaron Bentley
Added support for pushing with no working tree
250
    if working_tree:
333 by Aaron Bentley
Only error on dirty wt if we are pushing the wt
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)
303 by Aaron Bentley
Added support for pushing with no working tree
256
        final_exclusions = non_source[:]
257
    else:
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
258
        wt = tree
303 by Aaron Bentley
Added support for pushing with no working 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)
195 by Aaron Bentley
prevented accidental overwrites from push
264
    if not overwrite:
265
        try:
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
266
            if not history_subset(push_location, tree.branch):
195 by Aaron Bentley
prevented accidental overwrites from push
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)")
201 by Aaron Bentley
Merged error handling for bad rsync locations. (Eirik Nygaard)
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)
20 by abentley
added bzr-push command
279
    print "Pushing to %s" % push_location
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
280
    rsync(tree.basedir+'/', push_location, ssh=usessh,
303 by Aaron Bentley
Added support for pushing with no working tree
281
          excludes=final_exclusions)
20 by abentley
added bzr-push command
282
147.1.59 by Aaron Bentley
Reverted bzrtools.py to mainline version.
283
    set_push_data(tree, push_location)
20 by abentley
added bzr-push command
284
321.1.2 by Aaron Bentley
Applied Robert's random fixes as non-merges
285
292 by Aaron Bentley
Introduced branch-history command
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
354 by Aaron Bentley
Added apache index scraping to the branches command
293
def apache_ls(t):
294
    """Screen-scrape Apache listings"""
355 by Aaron Bentley
Handled more hrefs
295
    apache_dir = '<img border="0" src="/icons/folder.gif" alt="[dir]">'\
296
        ' <a href="'
354 by Aaron Bentley
Added apache index scraping to the branches command
297
    lines = t.get('.')
355 by Aaron Bentley
Handled more hrefs
298
    expr = re.compile('<a[^>]*href="([^>]*)"[^>]*>', flags=re.I)
354 by Aaron Bentley
Added apache index scraping to the branches command
299
    for line in lines:
355 by Aaron Bentley
Handled more hrefs
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
357 by Aaron Bentley
Tweakage of the apache scraper
306
        if '?' in url:
307
            continue
355 by Aaron Bentley
Handled more hrefs
308
        yield url.rstrip('/')
354 by Aaron Bentley
Added apache index scraping to the branches command
309
310
311
def iter_branches(t, lister=None):
352 by Aaron Bentley
Added branches subcommand
312
    """Iterate through all the branches under a transport"""
354 by Aaron Bentley
Added apache index scraping to the branches command
313
    for bzrdir in iter_bzrdirs(t, lister):
353 by Aaron Bentley
Added multi-pull, working on branches and checkouts
314
        try:
315
            branch = bzrdir.open_branch()
316
            if branch.bzrdir is bzrdir:
317
                yield branch
318
        except (NotBranchError, UnsupportedFormatError):
319
            pass
320
354 by Aaron Bentley
Added apache index scraping to the branches command
321
322
def iter_branch_tree(t, lister=None):
323
    for bzrdir in iter_bzrdirs(t, lister):
353 by Aaron Bentley
Added multi-pull, working on branches and checkouts
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
356 by Aaron Bentley
Fixed robustness issues in branches command
335
354 by Aaron Bentley
Added apache index scraping to the branches command
336
def iter_bzrdirs(t, lister=None):
337
    if lister is None:
338
        def lister(t):
339
            return t.list_dir('.')
352 by Aaron Bentley
Added branches subcommand
340
    try:
341
        bzrdir = bzrdir_from_transport(t)
353 by Aaron Bentley
Added multi-pull, working on branches and checkouts
342
        yield bzrdir
356 by Aaron Bentley
Fixed robustness issues in branches command
343
    except (NotBranchError, UnsupportedFormatError, TransportError,
344
            PermissionDenied):
352 by Aaron Bentley
Added branches subcommand
345
        pass
346
    try:
354 by Aaron Bentley
Added apache index scraping to the branches command
347
        for directory in lister(t):
352 by Aaron Bentley
Added branches subcommand
348
            if directory == ".bzr":
349
                continue
356 by Aaron Bentley
Fixed robustness issues in branches command
350
            try:
351
                subt = t.clone(directory)
352
            except UnicodeDecodeError:
353
                continue
357 by Aaron Bentley
Tweakage of the apache scraper
354
            for bzrdir in iter_bzrdirs(subt, lister):
353 by Aaron Bentley
Added multi-pull, working on branches and checkouts
355
                yield bzrdir
356 by Aaron Bentley
Fixed robustness issues in branches command
356
    except (NoSuchFile, PermissionDenied, TransportError):
352 by Aaron Bentley
Added branches subcommand
357
        pass
358
531.2.2 by Charlie Shepherd
Remove all trailing whitespace
359
352 by Aaron Bentley
Added branches subcommand
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
563 by Aaron Bentley
Allow importing directly from a URL
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
372
19 by abentley
librified most of the pull script
373
def run_tests():
16 by abentley
Got is_clean under test, added setters/getters for pull data
374
    import doctest
18 by abentley
Finished implementing bzr-pull
375
    result = doctest.testmod()
19 by abentley
librified most of the pull script
376
    if result[1] > 0:
377
        if result[0] == 0:
378
            print "All tests passed"
379
    else:
380
        print "No tests to run"
381
if __name__ == "__main__":
382
    run_tests()