~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-08-22 16:31:16 UTC
  • Revision ID: mbp@sourcefrog.net-20050822163116-935a433f338e7d5b
- new shell-complete command to help zsh completion

Show diffs side-by-side

added added

removed removed

Lines of Context:
23
23
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
24
24
     splitpath, \
25
25
     sha_file, appendpath, file_kind
26
 
 
27
26
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId
28
27
import bzrlib.errors
29
28
from bzrlib.textui import show_status
30
29
from bzrlib.revision import Revision
 
30
from bzrlib.xml import unpack_xml
31
31
from bzrlib.delta import compare_trees
32
32
from bzrlib.tree import EmptyTree, RevisionTree
33
 
import bzrlib.xml
34
 
import bzrlib.ui
35
 
 
36
 
 
37
 
 
 
33
        
38
34
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
39
35
## TODO: Maybe include checks for common corruption of newlines, etc?
40
36
 
128
124
        head, tail = os.path.split(f)
129
125
        if head == f:
130
126
            # reached the root, whatever that may be
131
 
            raise bzrlib.errors.NotBranchError('%s is not in a branch' % orig_f)
 
127
            raise bzrlib.errors.NotBranchError('%r is not in a branch' % orig_f)
132
128
        f = head
133
129
 
134
130
 
219
215
            self._lock.unlock()
220
216
 
221
217
 
 
218
 
222
219
    def lock_write(self):
223
220
        if self._lock_mode:
224
221
            if self._lock_mode != 'w':
234
231
            self._lock_count = 1
235
232
 
236
233
 
 
234
 
237
235
    def lock_read(self):
238
236
        if self._lock_mode:
239
237
            assert self._lock_mode in ('r', 'w'), \
246
244
            self._lock_mode = 'r'
247
245
            self._lock_count = 1
248
246
                        
 
247
 
 
248
            
249
249
    def unlock(self):
250
250
        if not self._lock_mode:
251
251
            from errors import LockError
258
258
            self._lock = None
259
259
            self._lock_mode = self._lock_count = None
260
260
 
 
261
 
261
262
    def abspath(self, name):
262
263
        """Return absolute filename for something in the branch"""
263
264
        return os.path.join(self.base, name)
264
265
 
 
266
 
265
267
    def relpath(self, path):
266
268
        """Return path relative to this branch of something inside it.
267
269
 
268
270
        Raises an error if path is not in this branch."""
269
271
        return _relpath(self.base, path)
270
272
 
 
273
 
271
274
    def controlfilename(self, file_or_path):
272
275
        """Return location relative to branch."""
273
276
        if isinstance(file_or_path, basestring):
300
303
        else:
301
304
            raise BzrError("invalid controlfile mode %r" % mode)
302
305
 
 
306
 
 
307
 
303
308
    def _make_control(self):
304
309
        from bzrlib.inventory import Inventory
 
310
        from bzrlib.xml import pack_xml
305
311
        
306
312
        os.mkdir(self.controlfilename([]))
307
313
        self.controlfile('README', 'w').write(
320
326
        # if we want per-tree root ids then this is the place to set
321
327
        # them; they're not needed for now and so ommitted for
322
328
        # simplicity.
323
 
        f = self.controlfile('inventory','w')
324
 
        bzrlib.xml.serializer_v4.write_inventory(Inventory(), f)
 
329
        pack_xml(Inventory(), self.controlfile('inventory','w'))
325
330
 
326
331
 
327
332
    def _check_format(self):
362
367
    def read_working_inventory(self):
363
368
        """Read the working inventory."""
364
369
        from bzrlib.inventory import Inventory
 
370
        from bzrlib.xml import unpack_xml
 
371
        from time import time
 
372
        before = time()
365
373
        self.lock_read()
366
374
        try:
367
375
            # ElementTree does its own conversion from UTF-8, so open in
368
376
            # binary.
369
 
            f = self.controlfile('inventory', 'rb')
370
 
            return bzrlib.xml.serializer_v4.read_inventory(f)
 
377
            inv = unpack_xml(Inventory,
 
378
                             self.controlfile('inventory', 'rb'))
 
379
            mutter("loaded inventory of %d items in %f"
 
380
                   % (len(inv), time() - before))
 
381
            return inv
371
382
        finally:
372
383
            self.unlock()
373
384
            
379
390
        will be committed to the next revision.
380
391
        """
381
392
        from bzrlib.atomicfile import AtomicFile
 
393
        from bzrlib.xml import pack_xml
382
394
        
383
395
        self.lock_write()
384
396
        try:
385
397
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
386
398
            try:
387
 
                bzrlib.xml.serializer_v4.write_inventory(inv, f)
 
399
                pack_xml(inv, f)
388
400
                f.commit()
389
401
            finally:
390
402
                f.close()
398
410
                         """Inventory for the working copy.""")
399
411
 
400
412
 
401
 
    def add(self, files, ids=None):
 
413
    def add(self, files, verbose=False, ids=None):
402
414
        """Make files versioned.
403
415
 
404
 
        Note that the command line normally calls smart_add instead,
405
 
        which can automatically recurse.
 
416
        Note that the command line normally calls smart_add instead.
406
417
 
407
418
        This puts the files in the Added state, so that they will be
408
419
        recorded by the next commit.
418
429
        TODO: Perhaps have an option to add the ids even if the files do
419
430
              not (yet) exist.
420
431
 
421
 
        TODO: Perhaps yield the ids and paths as they're added.
 
432
        TODO: Perhaps return the ids of the files?  But then again it
 
433
              is easy to retrieve them if they're needed.
 
434
 
 
435
        TODO: Adding a directory should optionally recurse down and
 
436
              add all non-ignored children.  Perhaps do that in a
 
437
              higher-level method.
422
438
        """
423
439
        # TODO: Re-adding a file that is removed in the working copy
424
440
        # should probably put it back with the previous ID.
460
476
                    file_id = gen_file_id(f)
461
477
                inv.add_path(f, kind=kind, file_id=file_id)
462
478
 
 
479
                if verbose:
 
480
                    print 'added', quotefn(f)
 
481
 
463
482
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
464
483
 
465
484
            self._write_inventory(inv)
575
594
            f.close()
576
595
 
577
596
 
578
 
    def get_revision_xml_file(self, revision_id):
 
597
    def get_revision_xml(self, revision_id):
579
598
        """Return XML file object for revision object."""
580
599
        if not revision_id or not isinstance(revision_id, basestring):
581
600
            raise InvalidRevisionId(revision_id)
590
609
            self.unlock()
591
610
 
592
611
 
593
 
    #deprecated
594
 
    get_revision_xml = get_revision_xml_file
595
 
 
596
 
 
597
612
    def get_revision(self, revision_id):
598
613
        """Return the Revision object for a named revision"""
599
 
        xml_file = self.get_revision_xml_file(revision_id)
 
614
        xml_file = self.get_revision_xml(revision_id)
600
615
 
601
616
        try:
602
 
            r = bzrlib.xml.serializer_v4.read_revision(xml_file)
 
617
            r = unpack_xml(Revision, xml_file)
603
618
        except SyntaxError, e:
604
619
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
605
620
                                         [revision_id,
650
665
               parameter which can be either an integer revno or a
651
666
               string hash."""
652
667
        from bzrlib.inventory import Inventory
 
668
        from bzrlib.xml import unpack_xml
653
669
 
654
 
        f = self.get_inventory_xml_file(inventory_id)
655
 
        return bzrlib.xml.serializer_v4.read_inventory(f)
 
670
        return unpack_xml(Inventory, self.get_inventory_xml(inventory_id))
656
671
 
657
672
 
658
673
    def get_inventory_xml(self, inventory_id):
659
674
        """Get inventory XML as a file object."""
660
675
        return self.inventory_store[inventory_id]
661
 
 
662
 
    get_inventory_xml_file = get_inventory_xml
663
676
            
664
677
 
665
678
    def get_inventory_sha1(self, inventory_id):
757
770
            return None
758
771
 
759
772
 
760
 
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
 
773
    def missing_revisions(self, other, stop_revision=None):
761
774
        """
762
775
        If self and other have not diverged, return a list of the revisions
763
776
        present in other, but missing from self.
796
809
        if stop_revision is None:
797
810
            stop_revision = other_len
798
811
        elif stop_revision > other_len:
799
 
            raise bzrlib.errors.NoSuchRevision(self, stop_revision)
 
812
            raise NoSuchRevision(self, stop_revision)
800
813
        
801
814
        return other_history[self_len:stop_revision]
802
815
 
803
816
 
804
817
    def update_revisions(self, other, stop_revision=None):
805
818
        """Pull in all new revisions from other branch.
 
819
        
 
820
        >>> from bzrlib.commit import commit
 
821
        >>> bzrlib.trace.silent = True
 
822
        >>> br1 = ScratchBranch(files=['foo', 'bar'])
 
823
        >>> br1.add('foo')
 
824
        >>> br1.add('bar')
 
825
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
 
826
        >>> br2 = ScratchBranch()
 
827
        >>> br2.update_revisions(br1)
 
828
        Added 2 texts.
 
829
        Added 1 inventories.
 
830
        Added 1 revisions.
 
831
        >>> br2.revision_history()
 
832
        [u'REVISION-ID-1']
 
833
        >>> br2.update_revisions(br1)
 
834
        Added 0 texts.
 
835
        Added 0 inventories.
 
836
        Added 0 revisions.
 
837
        >>> br1.text_store.total_size() == br2.text_store.total_size()
 
838
        True
806
839
        """
807
 
        from bzrlib.fetch import greedy_fetch
808
 
 
809
 
        pb = bzrlib.ui.ui_factory.progress_bar()
 
840
        from bzrlib.progress import ProgressBar
 
841
 
 
842
        pb = ProgressBar()
 
843
 
810
844
        pb.update('comparing histories')
811
 
 
812
845
        revision_ids = self.missing_revisions(other, stop_revision)
813
846
 
814
 
        if len(revision_ids) > 0:
815
 
            count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
816
 
        else:
817
 
            count = 0
818
 
        self.append_revision(*revision_ids)
819
 
        ## note("Added %d revisions." % count)
820
 
        pb.clear()
821
 
 
822
 
    def install_revisions(self, other, revision_ids, pb):
823
847
        if hasattr(other.revision_store, "prefetch"):
824
848
            other.revision_store.prefetch(revision_ids)
825
849
        if hasattr(other.inventory_store, "prefetch"):
826
850
            inventory_ids = [other.get_revision(r).inventory_id
827
851
                             for r in revision_ids]
828
852
            other.inventory_store.prefetch(inventory_ids)
829
 
 
830
 
        if pb is None:
831
 
            pb = bzrlib.ui.ui_factory.progress_bar()
832
853
                
833
854
        revisions = []
834
855
        needed_texts = set()
835
856
        i = 0
836
 
 
837
 
        failures = set()
838
 
        for i, rev_id in enumerate(revision_ids):
839
 
            pb.update('fetching revision', i+1, len(revision_ids))
840
 
            try:
841
 
                rev = other.get_revision(rev_id)
842
 
            except bzrlib.errors.NoSuchRevision:
843
 
                failures.add(rev_id)
844
 
                continue
845
 
 
 
857
        for rev_id in revision_ids:
 
858
            i += 1
 
859
            pb.update('fetching revision', i, len(revision_ids))
 
860
            rev = other.get_revision(rev_id)
846
861
            revisions.append(rev)
847
862
            inv = other.get_inventory(str(rev.inventory_id))
848
863
            for key, entry in inv.iter_entries():
853
868
 
854
869
        pb.clear()
855
870
                    
856
 
        count, cp_fail = self.text_store.copy_multi(other.text_store, 
857
 
                                                    needed_texts)
858
 
        #print "Added %d texts." % count 
 
871
        count = self.text_store.copy_multi(other.text_store, needed_texts)
 
872
        print "Added %d texts." % count 
859
873
        inventory_ids = [ f.inventory_id for f in revisions ]
860
 
        count, cp_fail = self.inventory_store.copy_multi(other.inventory_store, 
861
 
                                                         inventory_ids)
862
 
        #print "Added %d inventories." % count 
 
874
        count = self.inventory_store.copy_multi(other.inventory_store, 
 
875
                                                inventory_ids)
 
876
        print "Added %d inventories." % count 
863
877
        revision_ids = [ f.revision_id for f in revisions]
864
 
 
865
 
        count, cp_fail = self.revision_store.copy_multi(other.revision_store, 
866
 
                                                          revision_ids,
867
 
                                                          permit_failure=True)
868
 
        assert len(cp_fail) == 0 
869
 
        return count, failures
870
 
       
871
 
 
 
878
        count = self.revision_store.copy_multi(other.revision_store, 
 
879
                                               revision_ids)
 
880
        for revision_id in revision_ids:
 
881
            self.append_revision(revision_id)
 
882
        print "Added %d revisions." % count
 
883
                    
 
884
        
872
885
    def commit(self, *args, **kw):
873
886
        from bzrlib.commit import commit
874
887
        commit(self, *args, **kw)
876
889
 
877
890
    def lookup_revision(self, revision):
878
891
        """Return the revision identifier for a given revision information."""
879
 
        revno, info = self._get_revision_info(revision)
 
892
        revno, info = self.get_revision_info(revision)
880
893
        return info
881
894
 
882
 
 
883
 
    def revision_id_to_revno(self, revision_id):
884
 
        """Given a revision id, return its revno"""
885
 
        history = self.revision_history()
886
 
        try:
887
 
            return history.index(revision_id) + 1
888
 
        except ValueError:
889
 
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
890
 
 
891
 
 
892
895
    def get_revision_info(self, revision):
893
896
        """Return (revno, revision id) for revision identifier.
894
897
 
897
900
        revision can also be a string, in which case it is parsed for something like
898
901
            'date:' or 'revid:' etc.
899
902
        """
900
 
        revno, rev_id = self._get_revision_info(revision)
901
 
        if revno is None:
902
 
            raise bzrlib.errors.NoSuchRevision(self, revision)
903
 
        return revno, rev_id
904
 
 
905
 
    def get_rev_id(self, revno, history=None):
906
 
        """Find the revision id of the specified revno."""
907
 
        if revno == 0:
908
 
            return None
909
 
        if history is None:
910
 
            history = self.revision_history()
911
 
        elif revno <= 0 or revno > len(history):
912
 
            raise bzrlib.errors.NoSuchRevision(self, revno)
913
 
        return history[revno - 1]
914
 
 
915
 
    def _get_revision_info(self, revision):
916
 
        """Return (revno, revision id) for revision specifier.
917
 
 
918
 
        revision can be an integer, in which case it is assumed to be revno
919
 
        (though this will translate negative values into positive ones)
920
 
        revision can also be a string, in which case it is parsed for something
921
 
        like 'date:' or 'revid:' etc.
922
 
 
923
 
        A revid is always returned.  If it is None, the specifier referred to
924
 
        the null revision.  If the revid does not occur in the revision
925
 
        history, revno will be None.
926
 
        """
927
 
        
928
903
        if revision is None:
929
904
            return 0, None
930
905
        revno = None
934
909
            pass
935
910
        revs = self.revision_history()
936
911
        if isinstance(revision, int):
 
912
            if revision == 0:
 
913
                return 0, None
 
914
            # Mabye we should do this first, but we don't need it if revision == 0
937
915
            if revision < 0:
938
916
                revno = len(revs) + revision + 1
939
917
            else:
940
918
                revno = revision
941
 
            rev_id = self.get_rev_id(revno, revs)
942
919
        elif isinstance(revision, basestring):
943
920
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
944
921
                if revision.startswith(prefix):
945
 
                    result = func(self, revs, revision)
946
 
                    if len(result) > 1:
947
 
                        revno, rev_id = result
948
 
                    else:
949
 
                        revno = result[0]
950
 
                        rev_id = self.get_rev_id(revno, revs)
 
922
                    revno = func(self, revs, revision)
951
923
                    break
952
924
            else:
953
 
                raise BzrError('No namespace registered for string: %r' %
954
 
                               revision)
955
 
        else:
956
 
            raise TypeError('Unhandled revision type %s' % revision)
 
925
                raise BzrError('No namespace registered for string: %r' % revision)
957
926
 
958
 
        if revno is None:
959
 
            if rev_id is None:
960
 
                raise bzrlib.errors.NoSuchRevision(self, revision)
961
 
        return revno, rev_id
 
927
        if revno is None or revno <= 0 or revno > len(revs):
 
928
            raise BzrError("no such revision %s" % revision)
 
929
        return revno, revs[revno-1]
962
930
 
963
931
    def _namespace_revno(self, revs, revision):
964
932
        """Lookup a revision by revision number"""
965
933
        assert revision.startswith('revno:')
966
934
        try:
967
 
            return (int(revision[6:]),)
 
935
            return int(revision[6:])
968
936
        except ValueError:
969
937
            return None
970
938
    REVISION_NAMESPACES['revno:'] = _namespace_revno
971
939
 
972
940
    def _namespace_revid(self, revs, revision):
973
941
        assert revision.startswith('revid:')
974
 
        rev_id = revision[len('revid:'):]
975
942
        try:
976
 
            return revs.index(rev_id) + 1, rev_id
 
943
            return revs.index(revision[6:]) + 1
977
944
        except ValueError:
978
 
            return None, rev_id
 
945
            return None
979
946
    REVISION_NAMESPACES['revid:'] = _namespace_revid
980
947
 
981
948
    def _namespace_last(self, revs, revision):
983
950
        try:
984
951
            offset = int(revision[5:])
985
952
        except ValueError:
986
 
            return (None,)
 
953
            return None
987
954
        else:
988
955
            if offset <= 0:
989
956
                raise BzrError('You must supply a positive value for --revision last:XXX')
990
 
            return (len(revs) - offset + 1,)
 
957
            return len(revs) - offset + 1
991
958
    REVISION_NAMESPACES['last:'] = _namespace_last
992
959
 
993
960
    def _namespace_tag(self, revs, revision):
1068
1035
                # TODO: Handle timezone.
1069
1036
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1070
1037
                if first >= dt and (last is None or dt >= last):
1071
 
                    return (i+1,)
 
1038
                    return i+1
1072
1039
        else:
1073
1040
            for i in range(len(revs)):
1074
1041
                r = self.get_revision(revs[i])
1075
1042
                # TODO: Handle timezone.
1076
1043
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1077
1044
                if first <= dt and (last is None or dt <= last):
1078
 
                    return (i+1,)
 
1045
                    return i+1
1079
1046
    REVISION_NAMESPACES['date:'] = _namespace_date
1080
1047
 
1081
1048
    def revision_tree(self, revision_id):
1146
1113
 
1147
1114
            inv.rename(file_id, to_dir_id, to_tail)
1148
1115
 
 
1116
            print "%s => %s" % (from_rel, to_rel)
 
1117
 
1149
1118
            from_abs = self.abspath(from_rel)
1150
1119
            to_abs = self.abspath(to_rel)
1151
1120
            try:
1170
1139
 
1171
1140
        Note that to_name is only the last component of the new name;
1172
1141
        this doesn't change the directory.
1173
 
 
1174
 
        This returns a list of (from_path, to_path) pairs for each
1175
 
        entry that is moved.
1176
1142
        """
1177
 
        result = []
1178
1143
        self.lock_write()
1179
1144
        try:
1180
1145
            ## TODO: Option to move IDs only
1215
1180
            for f in from_paths:
1216
1181
                name_tail = splitpath(f)[-1]
1217
1182
                dest_path = appendpath(to_name, name_tail)
1218
 
                result.append((f, dest_path))
 
1183
                print "%s => %s" % (f, dest_path)
1219
1184
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
1220
1185
                try:
1221
1186
                    os.rename(self.abspath(f), self.abspath(dest_path))
1227
1192
        finally:
1228
1193
            self.unlock()
1229
1194
 
1230
 
        return result
1231
 
 
1232
1195
 
1233
1196
    def revert(self, filenames, old_tree=None, backups=True):
1234
1197
        """Restore selected files to the versions from a previous tree.
1316
1279
            self.unlock()
1317
1280
 
1318
1281
 
1319
 
    def get_parent(self):
1320
 
        """Return the parent location of the branch.
1321
 
 
1322
 
        This is the default location for push/pull/missing.  The usual
1323
 
        pattern is that the user can override it by specifying a
1324
 
        location.
1325
 
        """
1326
 
        import errno
1327
 
        _locs = ['parent', 'pull', 'x-pull']
1328
 
        for l in _locs:
1329
 
            try:
1330
 
                return self.controlfile(l, 'r').read().strip('\n')
1331
 
            except IOError, e:
1332
 
                if e.errno != errno.ENOENT:
1333
 
                    raise
1334
 
        return None
1335
 
 
1336
 
 
1337
 
    def set_parent(self, url):
1338
 
        # TODO: Maybe delete old location files?
1339
 
        from bzrlib.atomicfile import AtomicFile
1340
 
        self.lock_write()
1341
 
        try:
1342
 
            f = AtomicFile(self.controlfilename('parent'))
1343
 
            try:
1344
 
                f.write(url + '\n')
1345
 
                f.commit()
1346
 
            finally:
1347
 
                f.close()
1348
 
        finally:
1349
 
            self.unlock()
1350
 
 
1351
 
    def check_revno(self, revno):
1352
 
        """\
1353
 
        Check whether a revno corresponds to any revision.
1354
 
        Zero (the NULL revision) is considered valid.
1355
 
        """
1356
 
        if revno != 0:
1357
 
            self.check_real_revno(revno)
1358
 
            
1359
 
    def check_real_revno(self, revno):
1360
 
        """\
1361
 
        Check whether a revno corresponds to a real revision.
1362
 
        Zero (the NULL revision) is considered invalid
1363
 
        """
1364
 
        if revno < 1 or revno > self.revno():
1365
 
            raise InvalidRevisionNumber(revno)
1366
 
        
1367
 
        
1368
 
 
1369
1282
 
1370
1283
class ScratchBranch(Branch):
1371
1284
    """Special test class: a branch that cleans up after itself.
1413
1326
        os.rmdir(base)
1414
1327
        copytree(self.base, base, symlinks=True)
1415
1328
        return ScratchBranch(base=base)
1416
 
 
1417
 
 
1418
1329
        
1419
1330
    def __del__(self):
1420
1331
        self.destroy()
1490
1401
    """Return a new tree-root file id."""
1491
1402
    return gen_file_id('TREE_ROOT')
1492
1403
 
1493
 
 
1494
 
def pull_loc(branch):
1495
 
    # TODO: Should perhaps just make attribute be 'base' in
1496
 
    # RemoteBranch and Branch?
1497
 
    if hasattr(branch, "baseurl"):
1498
 
        return branch.baseurl
1499
 
    else:
1500
 
        return branch.base
1501
 
 
1502
 
 
1503
 
def copy_branch(branch_from, to_location, revision=None):
1504
 
    """Copy branch_from into the existing directory to_location.
1505
 
 
1506
 
    revision
1507
 
        If not None, only revisions up to this point will be copied.
1508
 
        The head of the new branch will be that revision.
1509
 
 
1510
 
    to_location
1511
 
        The name of a local directory that exists but is empty.
1512
 
    """
1513
 
    from bzrlib.merge import merge
1514
 
    from bzrlib.branch import Branch
1515
 
 
1516
 
    assert isinstance(branch_from, Branch)
1517
 
    assert isinstance(to_location, basestring)
1518
 
    
1519
 
    br_to = Branch(to_location, init=True)
1520
 
    br_to.set_root_id(branch_from.get_root_id())
1521
 
    if revision is None:
1522
 
        revno = branch_from.revno()
1523
 
    else:
1524
 
        revno, rev_id = branch_from.get_revision_info(revision)
1525
 
    br_to.update_revisions(branch_from, stop_revision=revno)
1526
 
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
1527
 
          check_clean=False, ignore_zero=True)
1528
 
    
1529
 
    from_location = pull_loc(branch_from)
1530
 
    br_to.set_parent(pull_loc(branch_from))
1531