~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Michael Ellerman
  • Date: 2005-10-19 13:36:05 UTC
  • mto: (0.3.1 shelf-dev) (325.1.2 bzrtools)
  • mto: This revision was merged to the branch mainline in revision 246.
  • Revision ID: michael@ellerman.id.au-20051019133605-83ecac725f52de87
Use Branch.base rather than running bzr root

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
17
 
import bzrlib
18
 
import bzrlib.errors
19
 
import os
20
 
import os.path
21
 
import sys
22
 
import tempfile
23
 
import shutil
24
 
import errno
25
 
from subprocess import Popen, PIPE
26
 
import codecs
27
 
import re
28
 
 
29
 
def temp_branch():
30
 
    dirname = tempfile.mkdtemp("temp-branch")
31
 
    return bzrlib.branch.Branch.initialize(dirname)
32
 
 
33
 
def rm_branch(br):
34
 
    shutil.rmtree(br.base)
35
 
 
36
 
def is_clean(cur_branch):
37
 
    """
38
 
    Return true if no files are modifed or unknown
39
 
    >>> import bzrlib.add
40
 
    >>> br = temp_branch()
41
 
    >>> is_clean(br)
42
 
    (True, [])
43
 
    >>> fooname = os.path.join(br.base, "foo")
44
 
    >>> file(fooname, "wb").write("bar")
45
 
    >>> is_clean(br)
46
 
    (True, [u'foo'])
47
 
    >>> bzrlib.add.smart_add_tree(br.working_tree(), [br.base])
48
 
    ([u'foo'], {})
49
 
    >>> is_clean(br)
50
 
    (False, [])
51
 
    >>> br.working_tree().commit("added file")
52
 
    >>> is_clean(br)
53
 
    (True, [])
54
 
    >>> rm_branch(br)
55
 
    """
56
 
    from bzrlib.diff import compare_trees
57
 
    old_tree = cur_branch.basis_tree()
58
 
    new_tree = cur_branch.working_tree()
59
 
    non_source = []
60
 
    for path, file_class, kind, file_id, entry in new_tree.list_files():
61
 
        if file_class in ('?', 'I'):
62
 
            non_source.append(path)
63
 
    delta = compare_trees(old_tree, new_tree, want_unchanged=False)
64
 
    return not delta.has_changed(), non_source
65
 
 
66
 
def set_push_data(br, location):
67
 
    push_file = file (br.control_files.controlfilename("x-push-data"), "wb")
68
 
    push_file.write("%s\n" % location)
69
 
 
70
 
def get_push_data(br):
71
 
    """
72
 
    >>> br = temp_branch()
73
 
    >>> get_push_data(br) is None
74
 
    True
75
 
    >>> set_push_data(br, 'http://somewhere')
76
 
    >>> get_push_data(br)
77
 
    'http://somewhere'
78
 
    >>> rm_branch(br)
79
 
    """
80
 
    filename = br.control_files.controlfilename("x-push-data")
81
 
    if not os.path.exists(filename):
82
 
        return None
83
 
    push_file = file (filename, "rb")
84
 
    (location,) = [f.rstrip('\n') for f in push_file]
85
 
    return location
86
 
 
87
 
"""
88
 
>>> shell_escape('hello')
89
 
'\h\e\l\l\o'
90
 
"""
91
 
def shell_escape(arg):
92
 
    return "".join(['\\'+c for c in arg])
93
 
 
94
 
def safe_system(args):
95
 
    """
96
 
    >>> real_system = os.system
97
 
    >>> os.system = sys.stdout.write
98
 
    >>> safe_system(['a', 'b', 'cd'])
99
 
    \\a \\b \\c\\d
100
 
    >>> os.system = real_system
101
 
    """
102
 
    arg_str = " ".join([shell_escape(a) for a in args])
103
 
    return os.system(arg_str)
104
 
 
105
 
class RsyncUnknownStatus(Exception):
106
 
    def __init__(self, status):
107
 
        Exception.__init__(self, "Unknown status: %d" % status)
108
 
 
109
 
class NoRsync(Exception):
110
 
    def __init__(self, rsync_name):
111
 
        Exception.__init__(self, "%s not found." % rsync_name)
112
 
 
113
 
def rsync(source, target, ssh=False, excludes=(), silent=False, 
114
 
          rsync_name="rsync"):
115
 
    """
116
 
    >>> rsync("a", "b", silent=True)
117
 
    Traceback (most recent call last):
118
 
    RsyncNoFile: No such file a
119
 
    >>> rsync("a", "b", excludes=("*.py",), silent=True)
120
 
    Traceback (most recent call last):
121
 
    RsyncNoFile: No such file a
122
 
    >>> rsync("a", "b", excludes=("*.py",), silent=True, rsync_name="rsyncc")
123
 
    Traceback (most recent call last):
124
 
    NoRsync: rsyncc not found.
125
 
    """
126
 
    cmd = [rsync_name, "-av", "--delete"]
127
 
    if ssh:
128
 
        cmd.extend(('-e', 'ssh'))
129
 
    if len(excludes) > 0:
130
 
        cmd.extend(('--exclude-from', '-'))
131
 
    cmd.extend((source, target))
132
 
    if silent:
133
 
        stderr = PIPE
134
 
        stdout = PIPE
135
 
    else:
136
 
        stderr = None
137
 
        stdout = None
138
 
    try:
139
 
        proc = Popen(cmd, stdin=PIPE, stderr=stderr, stdout=stdout)
140
 
    except OSError, e:
141
 
        if e.errno == errno.ENOENT:
142
 
            raise NoRsync(rsync_name)
143
 
            
144
 
    proc.stdin.write('\n'.join(excludes)+'\n')
145
 
    proc.stdin.close()
146
 
    if silent:
147
 
        proc.stderr.read()
148
 
        proc.stderr.close()
149
 
        proc.stdout.read()
150
 
        proc.stdout.close()
151
 
    proc.wait()
152
 
    if proc.returncode == 12:
153
 
        raise RsyncStreamIO()
154
 
    elif proc.returncode == 23:
155
 
        raise RsyncNoFile(source)
156
 
    elif proc.returncode != 0:
157
 
        raise RsyncUnknownStatus(proc.returncode)
158
 
    return cmd
159
 
 
160
 
 
161
 
def rsync_ls(source, ssh=False, silent=True):
162
 
    cmd = ["rsync"]
163
 
    if ssh:
164
 
        cmd.extend(('-e', 'ssh'))
165
 
    cmd.append(source)
166
 
    if silent:
167
 
        stderr = PIPE
168
 
    else:
169
 
        stderr = None
170
 
    proc = Popen(cmd, stderr=stderr, stdout=PIPE)
171
 
    result = proc.stdout.read()
172
 
    proc.stdout.close()
173
 
    if silent:
174
 
        proc.stderr.read()
175
 
        proc.stderr.close()
176
 
    proc.wait()
177
 
    if proc.returncode == 12:
178
 
        raise RsyncStreamIO()
179
 
    elif proc.returncode == 23:
180
 
        raise RsyncNoFile(source)
181
 
    elif proc.returncode != 0:
182
 
        raise RsyncUnknownStatus(proc.returncode)
183
 
    return [l.split(' ')[-1].rstrip('\n') for l in result.splitlines(True)]
184
 
 
185
 
exclusions = ('.bzr/x-push-data', '.bzr/parent', '.bzr/x-pull-data', 
186
 
              '.bzr/x-pull', '.bzr/pull', '.bzr/stat-cache',
187
 
              '.bzr/x-rsync-data')
188
 
 
189
 
 
190
 
def read_revision_history(fname):
191
 
    return [l.rstrip('\r\n') for l in
192
 
            codecs.open(fname, 'rb', 'utf-8').readlines()]
193
 
 
194
 
class RsyncNoFile(Exception):
195
 
    def __init__(self, path):
196
 
        Exception.__init__(self, "No such file %s" % path)
197
 
 
198
 
class RsyncStreamIO(Exception):
199
 
    def __init__(self):
200
 
        Exception.__init__(self, "Error in rsync protocol data stream.")
201
 
 
202
 
def get_revision_history(location):
203
 
    tempdir = tempfile.mkdtemp('push')
204
 
    try:
205
 
        history_fname = os.path.join(tempdir, 'revision-history')
206
 
        cmd = rsync(location+'.bzr/revision-history', history_fname,
207
 
                    silent=True)
208
 
        history = read_revision_history(history_fname)
209
 
    finally:
210
 
        shutil.rmtree(tempdir)
211
 
    return history
212
 
 
213
 
def history_subset(location, branch):
214
 
    remote_history = get_revision_history(location)
215
 
    local_history = branch.revision_history()
216
 
    if len(remote_history) > len(local_history):
217
 
        return False
218
 
    for local, remote in zip(remote_history, local_history):
219
 
        if local != remote:
220
 
            return False 
221
 
    return True
222
 
 
223
 
def empty_or_absent(location):
224
 
    try:
225
 
        files = rsync_ls(location)
226
 
        return files == ['.']
227
 
    except RsyncNoFile:
228
 
        return True
229
 
 
230
 
def push(cur_branch, location=None, overwrite=False, working_tree=True):
231
 
    push_location = get_push_data(cur_branch)
232
 
    if location is not None:
233
 
        if not location.endswith('/'):
234
 
            location += '/'
235
 
        push_location = location
236
 
    
237
 
    if push_location is None:
238
 
        raise bzrlib.errors.MustUseDecorated
239
 
 
240
 
    if push_location.find('://') != -1:
241
 
        raise bzrlib.errors.MustUseDecorated
242
 
 
243
 
    if push_location.find(':') == -1:
244
 
        raise bzrlib.errors.MustUseDecorated
245
 
 
246
 
    clean, non_source = is_clean(cur_branch)
247
 
    if not clean:
248
 
        print """Error: This tree has uncommitted changes or unknown (?) files.
249
 
Use "bzr status" to list them."""
250
 
        sys.exit(1)
251
 
    if working_tree:
252
 
        final_exclusions = non_source[:]
253
 
    else:
254
 
        wt = cur_branch.working_tree()
255
 
        final_exclusions = []
256
 
        for path, status, kind, file_id, entry in wt.list_files():
257
 
            final_exclusions.append(path)
258
 
 
259
 
    final_exclusions.extend(exclusions)
260
 
    if not overwrite:
261
 
        try:
262
 
            if not history_subset(push_location, cur_branch):
263
 
                raise bzrlib.errors.BzrCommandError("Local branch is not a"
264
 
                                                    " newer version of remote"
265
 
                                                    " branch.")
266
 
        except RsyncNoFile:
267
 
            if not empty_or_absent(push_location):
268
 
                raise bzrlib.errors.BzrCommandError("Remote location is not a"
269
 
                                                    " bzr branch (or empty"
270
 
                                                    " directory)")
271
 
        except RsyncStreamIO:
272
 
            raise bzrlib.errors.BzrCommandError("Rsync could not use the"
273
 
                " specified location.  Please ensure that"
274
 
                ' "%s" is of the form "machine:/path".' % push_location)
275
 
    print "Pushing to %s" % push_location
276
 
    rsync(cur_branch.base+'/', push_location, ssh=True, 
277
 
          excludes=final_exclusions)
278
 
 
279
 
    set_push_data(cur_branch, push_location)
280
 
 
281
 
def short_committer(committer):
282
 
    new_committer = re.sub('<.*>', '', committer).strip(' ')
283
 
    if len(new_committer) < 2:
284
 
        return committer
285
 
    return new_committer
286
 
 
287
 
 
288
 
def run_tests():
289
 
    import doctest
290
 
    result = doctest.testmod()
291
 
    if result[1] > 0:
292
 
        if result[0] == 0:
293
 
            print "All tests passed"
294
 
    else:
295
 
        print "No tests to run"
296
 
if __name__ == "__main__":
297
 
    run_tests()