~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2005-12-03 20:42:25 UTC
  • mfrom: (147.4.25 trunk)
  • mto: (147.4.27 trunk)
  • mto: This revision was merged to the branch mainline in revision 324.
  • Revision ID: aaron.bentley@utoronto.ca-20051203204225-25678bc921de4fc1
MergeĀ fromĀ lifeless

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