~bzr-pqm/bzr/bzr.dev

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
#!/usr/bin/env python
"""\
This encapsulates the functionality for trying to rsync a local
working tree to/from a remote rsync accessible location.
"""

import os
import bzrlib

_rsync_location = 'x-rsync-data'
_parent_locations = ['parent', 'pull', 'x-pull']

def temp_branch():
    import tempfile
    dirname = tempfile.mkdtemp("temp-branch")
    return bzrlib.Branch(dirname, init=True)

def rm_branch(branch):
    import shutil
    shutil.rmtree(branch.base)

def is_clean(branch):
    """
    Return true if no files are modifed or unknown
    >>> br = temp_branch()
    >>> is_clean(br)
    True
    >>> fooname = os.path.join(br.base, "foo")
    >>> file(fooname, "wb").write("bar")
    >>> is_clean(br)
    False
    >>> bzrlib.add.smart_add([fooname])
    >>> is_clean(br)
    False
    >>> br.commit("added file")
    >>> is_clean(br)
    True
    >>> rm_branch(br)
    """
    old_tree = branch.basis_tree()
    new_tree = branch.working_tree()
    for path, file_class, kind, file_id in new_tree.list_files():
        if file_class == '?':
            return False
    delta = bzrlib.compare_trees(old_tree, new_tree, want_unchanged=False)
    if len(delta.added) > 0 or len(delta.removed) > 0 or \
        len(delta.modified) > 0:
        return False
    return True

def get_default_remote_info(branch):
    """Return the value stored in .bzr/x-rsync-location if it exists.
    
    >>> br = temp_branch()
    >>> get_default_remote_info(br)
    (None, 0, None)
    >>> import bzrlib.commit
    >>> bzrlib.commit.commit(br, 'test commit', rev_id='test-id-12345')
    >>> set_default_remote_info(br, 'http://somewhere')
    >>> get_default_remote_info(br)
    ('http://somewhere', 1, 'test-id-12345')
    """
    def_remote = None
    revno = 0
    revision = None
    def_remote_filename = branch.controlfilename(_rsync_location)
    if os.path.isfile(def_remote_filename):
        [def_remote,revno, revision] = [x.strip() for x in open(def_remote_filename).readlines()]
    return def_remote, int(revno), revision

def set_default_remote_info(branch, location):
    """Store the location into the .bzr/x-rsync-location.
    
    """
    from bzrlib.atomicfile import AtomicFile
    remote, revno, revision = get_default_remote_info(branch)
    if (remote == location 
        and revno == branch.revno()
        and revision == branch.last_patch()):
        return #Nothing would change, so skip it
    # TODO: Consider adding to x-pull so that we can try a RemoteBranch
    # for checking the need to update
    f = AtomicFile(branch.controlfilename(_rsync_location))
    f.write(location)
    f.write('\n')
    f.write(str(branch.revno()))
    f.write('\n')
    f.write(branch.last_patch())
    f.write('\n')
    f.commit()

def get_parent_branch(branch):
    """Try to get the pull location, in case this directory supports the normal bzr pull.
    
    The idea is that we can use RemoteBranch to see if we actually need to do anything,
    and then we can decide whether to run rsync or not.
    """
    import errno
    stored_loc = None
    for fname in _parent_locations:
        try:
            stored_loc = branch.controlfile(fname, 'rb').read().rstrip('\n')
        except IOError, e:
            if e.errno != errno.ENOENT:
                raise

        if stored_loc:
            break

    if stored_loc:
        from bzrlib.branch import find_branch
        return find_branch(stored_loc)
    return None

def get_branch_remote_update(local=None, remote=None, alt_remote=None):
    from bzrlib.errors import BzrCommandError
    from bzrlib.branch import find_branch
    if local is None:
        local = '.'

    if remote is not None and remote[-1:] != '/':
        remote += '/'

    if alt_remote is not None and alt_remote[-1:] != '/':
        alt_remote += '/'

    if not os.path.exists(local):
        if remote is None:
            remote = alt_remote
        if remote is None:
            raise BzrCommandError('No remote location specified while creating a new local location')
        return local, remote, 0, None

    b = find_branch(local)

    def_remote, last_revno, last_revision = get_default_remote_info(b)
    if remote is None:
        if def_remote is None:
            if alt_remote is None:
                raise BzrCommandError('No remote location specified, and no default exists.')
            else:
                remote = alt_remote
        else:
            remote = def_remote

    if remote[-1:] != '/':
        remote += '/'

    return b, remote, last_revno, last_revision

def check_should_pull(branch, last_revno, last_revision):
    if isinstance(branch, basestring): # We don't even have a local branch yet
        return True

    if not is_clean(branch):
        print '** Local tree is not clean. Either has unknown or modified files.'
        return False

    b_parent = get_parent_branch(branch)
    if b_parent is not None:
        from bzrlib.branch import DivergedBranches
        # This may throw a Diverged branches.
        try:
            missing_revisions = branch.missing_revisions(b_parent)
        except DivergedBranches:
            print '** Local tree history has diverged from remote.'
            print '** Not allowing you to overwrite local changes.'
            return False
        if len(missing_revisions) == 0:
            # There is nothing to do, the remote branch has no changes
            missing_revisions = b_parent.missing_revisions(branch)
            if len(missing_revisions) > 0:
                print '** Local tree is up-to-date with remote.'
                print '** But remote tree is missing local revisions.'
                print '** Consider using bzr rsync-push'
            else:
                print '** Both trees fully up-to-date.'
            return False
        # We are sure that we are missing remote revisions
        return True

    if last_revno == branch.revno() and last_revision == branch.last_patch():
        # We can go ahead and try
        return True

    print 'Local working directory has a different revision than last rsync.'
    val = raw_input('Are you sure you want to download [y/N]? ')
    if val.lower() in ('y', 'yes'):
        return True
    return False

def check_should_push(branch, last_revno, last_revision):
    if not is_clean(branch):
        print '** Local tree is not clean (either modified or unknown files)'
        return False

    b_parent = get_parent_branch(branch)
    if b_parent is not None:
        from bzrlib.branch import DivergedBranches
        # This may throw a Diverged branches.
        try:
            missing_revisions = b_parent.missing_revisions(branch)
        except DivergedBranches:
            print '** Local tree history has diverged from remote.'
            print '** Not allowing you to overwrite remote changes.'
            return False
        if len(missing_revisions) == 0:
            # There is nothing to do, the remote branch is up to date
            missing_revisions = branch.missing_revisions(b_parent)
            if len(missing_revisions) > 0:
                print '** Remote tree is up-to-date with local.'
                print '** But local tree is missing remote revisions.'
                print '** Consider using bzr rsync-pull'
            else:
                print '** Both trees fully up-to-date.'
            return False
        # We are sure that we are missing remote revisions
        return True

    if last_revno is None and last_revision is None:
        print 'Local tree does not have a valid last rsync revision.'
        val = raw_input('push anyway [y/N]? ')
        if val.lower() in ('y', 'yes'):
            return True
        return False

    if last_revno == branch.revno() and last_revision == branch.last_patch():
        print 'No new revisions.'
        return False

    return True


def pull(branch, remote, verbose=False, dry_run=False):
    """Update the local repository from the location specified by 'remote'

    :param branch:  Either a string specifying a local path, or a Branch object.
                    If a local path, the download will be performed, and then
                    a Branch object will be created.

    :return:    Return the branch object that was created
    """
    if isinstance(branch, basestring):
        local = branch
        cur_revno = 0
    else:
        local = branch.base
        cur_revno = branch.revno()
    if remote[-1:] != '/':
        remote += '/'

    rsyncopts = ['-rltp', '--delete'
        # Don't pull in a new parent location
        , "--exclude '**/.bzr/x-rsync*'", "--exclude '**/.bzr/x-pull*'" 
        , "--exclude '**/.bzr/parent'", "--exclude '**/.bzr/pull'"
        ]

    # Note that when pulling, we do not delete excluded files
    rsync_exclude = os.path.join(local, '.rsyncexclude')
    if os.path.exists(rsync_exclude):
        rsyncopts.append('--exclude-from "%s"' % rsync_exclude)
    bzr_ignore = os.path.join(local, '.bzrignore')
    if os.path.exists(bzr_ignore):
        rsyncopts.append('--exclude-from "%s"' % bzr_ignore)

    if verbose:
        rsyncopts.append('-v')
    if dry_run:
        rsyncopts.append('--dry-run')

    cmd = 'rsync %s "%s" "%s"' % (' '.join(rsyncopts), remote, local)
    if verbose:
        print cmd

    status = os.system(cmd)
    if status != 0:
        from bzrlib.errors import BzrError
        raise BzrError('Rsync failed with error code: %s' % status)


    if isinstance(branch, basestring):
        from bzrlib.branch import Branch
        branch = Branch(branch)

    new_revno = branch.revno()
    if cur_revno == new_revno:
        print '** tree is up-to-date'

    if verbose:
        if cur_revno != new_revno:
            from bzrlib.log import show_log
            show_log(branch, direction='forward',
                    start_revision=cur_revno+1, end_revision=new_revno)

    return branch


def push(branch, remote, verbose=False, dry_run=False):
    """Update the local repository from the location specified by 'remote'

    :param branch:  Should always be a Branch object
    """
    if isinstance(branch, basestring):
        from bzrlib.errors import BzrError
        raise BzrError('rsync push requires a Branch object, not a string')
    local = branch.base
    if remote[-1:] != '/':
        remote += '/'

    rsyncopts = ['-rltp', '--include-from -'
        , '--include .bzr'
        # We don't want to push our local meta information to the remote
        , "--exclude '.bzr/x-rsync*'", "--exclude '.bzr/x-pull*'" 
        , "--exclude '.bzr/parent'", "--exclude '.bzr/pull'"
        , "--include '.bzr/**'"
        , "--exclude '*'", "--exclude '.*'"
        , '--delete', '--delete-excluded'
        ]

    rsync_exclude = os.path.join(local, '.rsyncexclude')
    if os.path.exists(rsync_exclude):
        rsyncopts.append('--exclude-from "%s"' % rsync_exclude)
    bzr_ignore = os.path.join(local, '.bzrignore')
    if os.path.exists(bzr_ignore):
        rsyncopts.append('--exclude-from "%s"' % bzr_ignore)

    if verbose:
        rsyncopts.append('-v')
    if dry_run:
        rsyncopts.append('--dry-run')

    cmd = 'rsync %s "." "%s"' % (' '.join(rsyncopts), remote)
    if verbose:
        print cmd

    pwd = os.getcwd()
    try:
        os.chdir(local)
        child = os.popen(cmd, 'w')
        inv = branch.read_working_inventory()
        for path, entry in inv.entries():
            child.write(path)
            child.write('\n')
        child.flush()
        retval = child.close()
        if retval is not None:
            from bzrlib.errors import BzrError
            raise BzrError('Rsync failed with error code: %s' % retval)
    finally:
        os.chdir(pwd)