~abentley/bzrtools/bzrtools.dev

« back to all changes in this revision

Viewing changes to bzrtools.py

  • Committer: Aaron Bentley
  • Date: 2005-10-27 04:45:06 UTC
  • Revision ID: aaron.bentley@utoronto.ca-20051027044506-45f616c07537a1da
Got the shell basics working properly

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