~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2006-09-25 13:42:47 UTC
  • Revision ID: abentley@panoramicfeedback.com-20060925134247-c9956e8bd0de0958
Remove shove from README

Show diffs side-by-side

added added

removed removed

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