~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to plugins/rsync/rsync_update.py

  • Committer: Martin Pool
  • Date: 2005-07-04 08:06:51 UTC
  • Revision ID: mbp@sourcefrog.net-20050704080651-6ecec49164359e48
- track pending-merges

- unit tests for this

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/env python
 
2
"""\
 
3
This encapsulates the functionality for trying to rsync a local
 
4
working tree to/from a remote rsync accessible location.
 
5
"""
 
6
 
 
7
import os
 
8
import bzrlib
 
9
 
 
10
_rsync_location = 'x-rsync-data'
 
11
_parent_locations = ['parent', 'pull', 'x-pull']
 
12
 
 
13
def temp_branch():
 
14
    import tempfile
 
15
    dirname = tempfile.mkdtemp("temp-branch")
 
16
    return bzrlib.Branch(dirname, init=True)
 
17
 
 
18
def rm_branch(branch):
 
19
    import shutil
 
20
    shutil.rmtree(branch.base)
 
21
 
 
22
def is_clean(branch):
 
23
    """
 
24
    Return true if no files are modifed or unknown
 
25
    >>> br = temp_branch()
 
26
    >>> is_clean(br)
 
27
    True
 
28
    >>> fooname = os.path.join(br.base, "foo")
 
29
    >>> file(fooname, "wb").write("bar")
 
30
    >>> is_clean(br)
 
31
    False
 
32
    >>> bzrlib.add.smart_add([fooname])
 
33
    >>> is_clean(br)
 
34
    False
 
35
    >>> br.commit("added file")
 
36
    >>> is_clean(br)
 
37
    True
 
38
    >>> rm_branch(br)
 
39
    """
 
40
    old_tree = branch.basis_tree()
 
41
    new_tree = branch.working_tree()
 
42
    for path, file_class, kind, file_id in new_tree.list_files():
 
43
        if file_class == '?':
 
44
            return False
 
45
    delta = bzrlib.compare_trees(old_tree, new_tree, want_unchanged=False)
 
46
    if len(delta.added) > 0 or len(delta.removed) > 0 or \
 
47
        len(delta.modified) > 0:
 
48
        return False
 
49
    return True
 
50
 
 
51
def get_default_remote_info(branch):
 
52
    """Return the value stored in .bzr/x-rsync-location if it exists.
 
53
    
 
54
    >>> br = temp_branch()
 
55
    >>> get_default_remote_info(br)
 
56
    (None, 0, None)
 
57
    >>> import bzrlib.commit
 
58
    >>> bzrlib.commit.commit(br, 'test commit', rev_id='test-id-12345')
 
59
    >>> set_default_remote_info(br, 'http://somewhere')
 
60
    >>> get_default_remote_info(br)
 
61
    ('http://somewhere', 1, 'test-id-12345')
 
62
    """
 
63
    def_remote = None
 
64
    revno = 0
 
65
    revision = None
 
66
    def_remote_filename = branch.controlfilename(_rsync_location)
 
67
    if os.path.isfile(def_remote_filename):
 
68
        [def_remote,revno, revision] = [x.strip() for x in open(def_remote_filename).readlines()]
 
69
    return def_remote, int(revno), revision
 
70
 
 
71
def set_default_remote_info(branch, location):
 
72
    """Store the location into the .bzr/x-rsync-location.
 
73
    
 
74
    """
 
75
    from bzrlib.atomicfile import AtomicFile
 
76
    remote, revno, revision = get_default_remote_info(branch)
 
77
    if (remote == location 
 
78
        and revno == branch.revno()
 
79
        and revision == branch.last_patch()):
 
80
        return #Nothing would change, so skip it
 
81
    # TODO: Consider adding to x-pull so that we can try a RemoteBranch
 
82
    # for checking the need to update
 
83
    f = AtomicFile(branch.controlfilename(_rsync_location))
 
84
    f.write(location)
 
85
    f.write('\n')
 
86
    f.write(str(branch.revno()))
 
87
    f.write('\n')
 
88
    f.write(branch.last_patch())
 
89
    f.write('\n')
 
90
    f.commit()
 
91
 
 
92
def get_parent_branch(branch):
 
93
    """Try to get the pull location, in case this directory supports the normal bzr pull.
 
94
    
 
95
    The idea is that we can use RemoteBranch to see if we actually need to do anything,
 
96
    and then we can decide whether to run rsync or not.
 
97
    """
 
98
    import errno
 
99
    stored_loc = None
 
100
    for fname in _parent_locations:
 
101
        try:
 
102
            stored_loc = branch.controlfile(fname, 'rb').read().rstrip('\n')
 
103
        except IOError, e:
 
104
            if e.errno != errno.ENOENT:
 
105
                raise
 
106
 
 
107
        if stored_loc:
 
108
            break
 
109
 
 
110
    if stored_loc:
 
111
        from bzrlib.branch import find_branch
 
112
        return find_branch(stored_loc)
 
113
    return None
 
114
 
 
115
def get_branch_remote_update(local=None, remote=None, alt_remote=None):
 
116
    from bzrlib.errors import BzrCommandError
 
117
    from bzrlib.branch import find_branch
 
118
    if local is None:
 
119
        local = '.'
 
120
 
 
121
    if remote is not None and remote[-1:] != '/':
 
122
        remote += '/'
 
123
 
 
124
    if alt_remote is not None and alt_remote[-1:] != '/':
 
125
        alt_remote += '/'
 
126
 
 
127
    if not os.path.exists(local):
 
128
        if remote is None:
 
129
            remote = alt_remote
 
130
        if remote is None:
 
131
            raise BzrCommandError('No remote location specified while creating a new local location')
 
132
        return local, remote, 0, None
 
133
 
 
134
    b = find_branch(local)
 
135
 
 
136
    def_remote, last_revno, last_revision = get_default_remote_info(b)
 
137
    if remote is None:
 
138
        if def_remote is None:
 
139
            if alt_remote is None:
 
140
                raise BzrCommandError('No remote location specified, and no default exists.')
 
141
            else:
 
142
                remote = alt_remote
 
143
        else:
 
144
            remote = def_remote
 
145
 
 
146
    if remote[-1:] != '/':
 
147
        remote += '/'
 
148
 
 
149
    return b, remote, last_revno, last_revision
 
150
 
 
151
def check_should_pull(branch, last_revno, last_revision):
 
152
    if isinstance(branch, basestring): # We don't even have a local branch yet
 
153
        return True
 
154
 
 
155
    if not is_clean(branch):
 
156
        print '** Local tree is not clean. Either has unknown or modified files.'
 
157
        return False
 
158
 
 
159
    b_parent = get_parent_branch(branch)
 
160
    if b_parent is not None:
 
161
        from bzrlib.branch import DivergedBranches
 
162
        # This may throw a Diverged branches.
 
163
        try:
 
164
            missing_revisions = branch.missing_revisions(b_parent)
 
165
        except DivergedBranches:
 
166
            print '** Local tree history has diverged from remote.'
 
167
            print '** Not allowing you to overwrite local changes.'
 
168
            return False
 
169
        if len(missing_revisions) == 0:
 
170
            # There is nothing to do, the remote branch has no changes
 
171
            missing_revisions = b_parent.missing_revisions(branch)
 
172
            if len(missing_revisions) > 0:
 
173
                print '** Local tree is up-to-date with remote.'
 
174
                print '** But remote tree is missing local revisions.'
 
175
                print '** Consider using bzr rsync-push'
 
176
            else:
 
177
                print '** Both trees fully up-to-date.'
 
178
            return False
 
179
        # We are sure that we are missing remote revisions
 
180
        return True
 
181
 
 
182
    if last_revno == branch.revno() and last_revision == branch.last_patch():
 
183
        # We can go ahead and try
 
184
        return True
 
185
 
 
186
    print 'Local working directory has a different revision than last rsync.'
 
187
    val = raw_input('Are you sure you want to download [y/N]? ')
 
188
    if val.lower() in ('y', 'yes'):
 
189
        return True
 
190
    return False
 
191
 
 
192
def check_should_push(branch, last_revno, last_revision):
 
193
    if not is_clean(branch):
 
194
        print '** Local tree is not clean (either modified or unknown files)'
 
195
        return False
 
196
 
 
197
    b_parent = get_parent_branch(branch)
 
198
    if b_parent is not None:
 
199
        from bzrlib.branch import DivergedBranches
 
200
        # This may throw a Diverged branches.
 
201
        try:
 
202
            missing_revisions = b_parent.missing_revisions(branch)
 
203
        except DivergedBranches:
 
204
            print '** Local tree history has diverged from remote.'
 
205
            print '** Not allowing you to overwrite remote changes.'
 
206
            return False
 
207
        if len(missing_revisions) == 0:
 
208
            # There is nothing to do, the remote branch is up to date
 
209
            missing_revisions = branch.missing_revisions(b_parent)
 
210
            if len(missing_revisions) > 0:
 
211
                print '** Remote tree is up-to-date with local.'
 
212
                print '** But local tree is missing remote revisions.'
 
213
                print '** Consider using bzr rsync-pull'
 
214
            else:
 
215
                print '** Both trees fully up-to-date.'
 
216
            return False
 
217
        # We are sure that we are missing remote revisions
 
218
        return True
 
219
 
 
220
    if last_revno is None and last_revision is None:
 
221
        print 'Local tree does not have a valid last rsync revision.'
 
222
        val = raw_input('push anyway [y/N]? ')
 
223
        if val.lower() in ('y', 'yes'):
 
224
            return True
 
225
        return False
 
226
 
 
227
    if last_revno == branch.revno() and last_revision == branch.last_patch():
 
228
        print 'No new revisions.'
 
229
        return False
 
230
 
 
231
    return True
 
232
 
 
233
 
 
234
def pull(branch, remote, verbose=False, dry_run=False):
 
235
    """Update the local repository from the location specified by 'remote'
 
236
 
 
237
    :param branch:  Either a string specifying a local path, or a Branch object.
 
238
                    If a local path, the download will be performed, and then
 
239
                    a Branch object will be created.
 
240
 
 
241
    :return:    Return the branch object that was created
 
242
    """
 
243
    if isinstance(branch, basestring):
 
244
        local = branch
 
245
        cur_revno = 0
 
246
    else:
 
247
        local = branch.base
 
248
        cur_revno = branch.revno()
 
249
    if remote[-1:] != '/':
 
250
        remote += '/'
 
251
 
 
252
    rsyncopts = ['-rltp', '--delete'
 
253
        # Don't pull in a new parent location
 
254
        , "--exclude '**/.bzr/x-rsync*'", "--exclude '**/.bzr/x-pull*'" 
 
255
        , "--exclude '**/.bzr/parent'", "--exclude '**/.bzr/pull'"
 
256
        ]
 
257
 
 
258
    # Note that when pulling, we do not delete excluded files
 
259
    rsync_exclude = os.path.join(local, '.rsyncexclude')
 
260
    if os.path.exists(rsync_exclude):
 
261
        rsyncopts.append('--exclude-from "%s"' % rsync_exclude)
 
262
    bzr_ignore = os.path.join(local, '.bzrignore')
 
263
    if os.path.exists(bzr_ignore):
 
264
        rsyncopts.append('--exclude-from "%s"' % bzr_ignore)
 
265
 
 
266
    if verbose:
 
267
        rsyncopts.append('-v')
 
268
    if dry_run:
 
269
        rsyncopts.append('--dry-run')
 
270
 
 
271
    cmd = 'rsync %s "%s" "%s"' % (' '.join(rsyncopts), remote, local)
 
272
    if verbose:
 
273
        print cmd
 
274
 
 
275
    status = os.system(cmd)
 
276
    if status != 0:
 
277
        from bzrlib.errors import BzrError
 
278
        raise BzrError('Rsync failed with error code: %s' % status)
 
279
 
 
280
 
 
281
    if isinstance(branch, basestring):
 
282
        from bzrlib.branch import Branch
 
283
        branch = Branch(branch)
 
284
 
 
285
    new_revno = branch.revno()
 
286
    if cur_revno == new_revno:
 
287
        print '** tree is up-to-date'
 
288
 
 
289
    if verbose:
 
290
        if cur_revno != new_revno:
 
291
            from bzrlib.log import show_log
 
292
            show_log(branch, direction='forward',
 
293
                    start_revision=cur_revno+1, end_revision=new_revno)
 
294
 
 
295
    return branch
 
296
 
 
297
 
 
298
def push(branch, remote, verbose=False, dry_run=False):
 
299
    """Update the local repository from the location specified by 'remote'
 
300
 
 
301
    :param branch:  Should always be a Branch object
 
302
    """
 
303
    if isinstance(branch, basestring):
 
304
        from bzrlib.errors import BzrError
 
305
        raise BzrError('rsync push requires a Branch object, not a string')
 
306
    local = branch.base
 
307
    if remote[-1:] != '/':
 
308
        remote += '/'
 
309
 
 
310
    rsyncopts = ['-rltp', '--include-from -'
 
311
        , '--include .bzr'
 
312
        # We don't want to push our local meta information to the remote
 
313
        , "--exclude '.bzr/x-rsync*'", "--exclude '.bzr/x-pull*'" 
 
314
        , "--exclude '.bzr/parent'", "--exclude '.bzr/pull'"
 
315
        , "--include '.bzr/**'"
 
316
        , "--exclude '*'", "--exclude '.*'"
 
317
        , '--delete', '--delete-excluded'
 
318
        ]
 
319
 
 
320
    rsync_exclude = os.path.join(local, '.rsyncexclude')
 
321
    if os.path.exists(rsync_exclude):
 
322
        rsyncopts.append('--exclude-from "%s"' % rsync_exclude)
 
323
    bzr_ignore = os.path.join(local, '.bzrignore')
 
324
    if os.path.exists(bzr_ignore):
 
325
        rsyncopts.append('--exclude-from "%s"' % bzr_ignore)
 
326
 
 
327
    if verbose:
 
328
        rsyncopts.append('-v')
 
329
    if dry_run:
 
330
        rsyncopts.append('--dry-run')
 
331
 
 
332
    cmd = 'rsync %s "." "%s"' % (' '.join(rsyncopts), remote)
 
333
    if verbose:
 
334
        print cmd
 
335
 
 
336
    pwd = os.getcwd()
 
337
    try:
 
338
        os.chdir(local)
 
339
        child = os.popen(cmd, 'w')
 
340
        inv = branch.read_working_inventory()
 
341
        for path, entry in inv.entries():
 
342
            child.write(path)
 
343
            child.write('\n')
 
344
        child.flush()
 
345
        retval = child.close()
 
346
        if retval is not None:
 
347
            from bzrlib.errors import BzrError
 
348
            raise BzrError('Rsync failed with error code: %s' % retval)
 
349
    finally:
 
350
        os.chdir(pwd)
 
351