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