1332
1376
_matching_to_tree_format = WorkingTreeFormat4()
1333
1377
_test_mutable_trees_to_test_trees = make_source_parent_tree
1336
def compare(self, want_unchanged=False, specific_files=None,
1337
extra_trees=None, require_versioned=False, include_root=False):
1379
def _iter_changes(self, include_unchanged=False,
1380
specific_files=None, pb=None, extra_trees=[],
1381
require_versioned=True):
1338
1382
"""Return the changes from source to target.
1340
:return: A TreeDelta.
1384
:return: An iterator that yields tuples. See InterTree._iter_changes
1341
1386
:param specific_files: An optional list of file paths to restrict the
1342
1387
comparison to. When mapping filenames to ids, all matches in all
1343
1388
trees (including optional extra_trees) are used, and all children of
1344
1389
matched directories are included.
1345
:param want_unchanged: An optional boolean requesting the inclusion of
1390
:param include_unchanged: An optional boolean requesting the inclusion of
1346
1391
unchanged entries in the result.
1347
1392
:param extra_trees: An optional list of additional trees to use when
1348
1393
mapping the contents of specific_files (paths) to file_ids.
1349
:param require_versioned: An optional boolean (defaults to False). When
1350
supplied and True all the 'specific_files' must be versioned, or
1351
a PathsNotVersionedError will be thrown.
1394
:param require_versioned: If True, all files in specific_files must be
1395
versioned in one of source, target, extra_trees or
1396
PathsNotVersionedError is raised.
1353
1398
# NB: show_status depends on being able to pass in non-versioned files
1354
1399
# and report them as unknown
1355
trees = (self.source,)
1356
if extra_trees is not None:
1357
trees = trees + tuple(extra_trees)
1358
# target is usually the newer tree:
1359
specific_file_ids = self.target.paths2ids(specific_files, trees,
1360
require_versioned=require_versioned)
1361
from bzrlib import delta
1362
if specific_files and not specific_file_ids:
1363
# All files are unversioned, so just return an empty delta
1364
# _compare_trees would think we want a complete delta
1365
return delta.TreeDelta()
1366
return delta._compare_trees(self.source, self.target, want_unchanged,
1367
specific_file_ids, include_root)
1400
# TODO: handle extra trees in the dirstate.
1402
# TODO: handle specific files
1404
for f in super(InterDirStateTree, self)._iter_changes(
1405
include_unchanged, specific_files, pb, extra_trees,
1409
assert (self.source._revision_id in self.target.get_parent_ids())
1410
parents = self.target.get_parent_ids()
1412
source_index = 1 + parents.index(self.source._revision_id)
1413
# -- make all specific_files utf8 --
1415
specific_files_utf8 = set()
1416
for path in specific_files:
1417
specific_files_utf8.add(path.encode('utf8'))
1418
specific_files = specific_files_utf8
1420
specific_files = set([''])
1421
# -- specific_files is now a utf8 path set --
1422
# -- get the state object and prepare it.
1423
state = self.target.current_dirstate()
1424
state._read_dirblocks_if_needed()
1425
def _entries_for_path(path):
1426
"""Return a list with all the entries that match path for all ids.
1428
dirname, basename = os.path.split(path)
1429
key = (dirname, basename, '')
1430
block_index, present = state._find_block_index_from_key(key)
1432
# the block which should contain path is absent.
1435
block = state._dirblocks[block_index][1]
1436
entry_index, _ = state._find_entry_index(key, block)
1437
# we may need to look at multiple entries at this path: walk while the specific_files match.
1438
while (entry_index < len(block) and
1439
block[entry_index][0][0:2] == key[0:2]):
1440
result.append(block[entry_index])
1443
if require_versioned:
1444
# -- check all supplied paths are versioned in a search tree. --
1445
all_versioned = True
1446
for path in specific_files:
1447
path = path.encode('utf8')
1448
path_entries = _entries_for_path(path)
1449
if not path_entries:
1450
# this specified path is not present at all: error
1451
all_versioned = False
1453
found_versioned = False
1454
# for each id at this path
1455
for entry in path_entries:
1457
for index in source_index, target_index:
1458
if entry[1][index][0] != 'a': # absent
1459
found_versioned = True
1460
# all good: found a versioned cell
1462
if not found_versioned:
1463
# none of the indexes was not 'absent' at all ids for this
1465
all_versioned = False
1467
if not all_versioned:
1468
raise errors.PathsNotVersionedError(paths)
1469
# -- remove redundancy in supplied specific_files to prevent over-scanning --
1470
search_specific_files = set()
1471
for path in specific_files:
1472
other_specific_files = specific_files.difference(set([path]))
1473
if not osutils.is_inside_any(other_specific_files, path):
1474
# this is a top level path, we must check it.
1475
search_specific_files.add(path)
1477
# compare source_index and target_index at or under each element of search_specific_files.
1478
# follow the following comparison table. Note that we only want to do diff operations when
1479
# the target is fdl because thats when the walkdirs logic will have exposed the pathinfo
1483
# Source | Target | disk | action
1484
# r | fdl | | add source to search, add id path move and perform
1485
# | | | diff check on source-target
1486
# r | fdl | a | dangling file that was present in the basis.
1488
# r | a | | add source to search
1490
# r | r | | this path is present in a non-examined tree, skip.
1491
# r | r | a | this path is present in a non-examined tree, skip.
1492
# a | fdl | | add new id
1493
# a | fdl | a | dangling locally added file, skip
1494
# a | a | | not present in either tree, skip
1495
# a | a | a | not present in any tree, skip
1496
# a | r | | not present in either tree at this path, skip as it
1497
# | | | may not be selected by the users list of paths.
1498
# a | r | a | not present in either tree at this path, skip as it
1499
# | | | may not be selected by the users list of paths.
1500
# fdl | fdl | | content in both: diff them
1501
# fdl | fdl | a | deleted locally, but not unversioned - show as deleted ?
1502
# fdl | a | | unversioned: output deleted id for now
1503
# fdl | a | a | unversioned and deleted: output deleted id
1504
# fdl | r | | relocated in this tree, so add target to search.
1505
# | | | Dont diff, we will see an r,fd; pair when we reach
1506
# | | | this id at the other path.
1507
# fdl | r | a | relocated in this tree, so add target to search.
1508
# | | | Dont diff, we will see an r,fd; pair when we reach
1509
# | | | this id at the other path.
1511
# for all search_indexs in each path at or under each element of
1512
# search_specific_files, if the detail is relocated: add the id, and add the
1513
# relocated path as one to search if its not searched already. If the
1514
# detail is not relocated, add the id.
1515
searched_specific_files = set()
1516
def _process_entry(entry, path_info):
1517
"""Compare an entry and real disk to generate delta information.
1519
:param path_info: top_relpath, basename, kind, lstat, abspath for
1520
the path of entry. If None, then the path is considered absent.
1521
(Perhaps we should pass in a concrete entry for this ?)
1523
# TODO: when a parent has been renamed, dont emit path renames for children,
1524
source_details = entry[1][source_index]
1525
target_details = entry[1][target_index]
1526
if source_details[0] in 'rfdl' and target_details[0] in 'fdl':
1527
# claimed content in both: diff
1528
# r | fdl | | add source to search, add id path move and perform
1529
# | | | diff check on source-target
1530
# r | fdl | a | dangling file that was present in the basis.
1532
if source_details[0] in 'r':
1533
# add the source to the search path to find any children it
1534
# has. TODO ? : only add if it is a container ?
1535
if not osutils.is_inside_any(searched_specific_files, source_details[1]):
1536
search_specific_files.add(source_details[1])
1537
# generate the old path; this is needed for stating later
1539
old_path = source_details[1]
1540
old_dirname, old_basename = os.path.split(old_path)
1541
path = os.path.join(*entry[0][0:2])
1542
old_entry = state._get_entry(source_index, path_utf8=old_path)
1543
# update the source details variable to be the real
1545
source_details = old_entry[1][source_index]
1547
old_path = path = os.path.join(*entry[0][0:2])
1548
old_dirname, old_basename = entry[0][0:2]
1549
if path_info is None:
1550
# the file is missing on disk, show as removed.
1551
print "missing file"
1552
old_path = os.path.join(*entry[0][0:2])
1553
result.removed.append((old_path, entry[0][2], dirstate.DirState._minikind_to_kind[source_details[0]]))
1554
# use the kind from disk.
1555
elif source_details[0] != path_info[2][0]:
1557
import pdb;pdb.set_trace()
1561
if path_info[2][0] == 'd':
1562
# directories have no fingerprint
1563
content_change = False
1564
executable_change = False
1565
elif path_info[2][0] == 'f':
1566
# has it changed? fast path: size, slow path: sha1.
1567
executable_change = source_details[3] != bool(
1568
stat.S_ISREG(path_info[3].st_mode)
1569
and stat.S_IEXEC & path_info[3].st_mode)
1570
if source_details[2] != path_info[3].st_size:
1571
content_change = True
1573
# maybe the same. Get the hash
1574
new_hash = self.target._hashcache.get_sha1(path, path_info[3])
1575
content_change = (new_hash != source_details[1])
1576
elif path_info[2][0] == 'l':
1577
import pdb;pdb.set_trace()
1580
raise Exception, "unknown minikind"
1581
# parent id is the entry for the path in the target tree
1582
# TODO: the target is the same for an entire directory: cache em.
1583
source_parent_id = state._get_entry(source_index, path_utf8=old_dirname)[0][2]
1584
if source_parent_id == entry[0][2]:
1585
source_parent_id = None
1586
target_parent_id = state._get_entry(target_index, path_utf8=entry[0][0])[0][2]
1587
if target_parent_id == entry[0][2]:
1588
target_parent_id = None
1589
source_exec = source_details[3]
1591
stat.S_ISREG(path_info[3].st_mode)
1592
and stat.S_IEXEC & path_info[3].st_mode)
1593
return ((entry[0][2], path, content_change, (True, True), (source_parent_id, target_parent_id), (old_basename, entry[0][1]), (dirstate.DirState._minikind_to_kind[source_details[0]], path_info[2]), (source_exec, target_exec)),)
1594
elif source_details[0] in 'a' and target_details[0] in 'fdl':
1595
# looks like a new file
1596
if path_info is not None:
1597
path = os.path.join(*entry[0][0:2])
1598
# parent id is the entry for the path in the target tree
1599
# TODO: these are the same for an entire directory: cache em.
1600
parent_id = state._get_entry(target_index, path_utf8=entry[0][0])[0][2]
1601
if parent_id == entry[0][2]:
1604
new_executable = bool(
1605
stat.S_ISREG(path_info[3].st_mode)
1606
and stat.S_IEXEC & path_info[3].st_mode)
1607
return ((entry[0][2], path, True, (False, True), (None, parent_id), (None, entry[0][1]), (None, path_info[2]), (None, new_executable)),)
1609
# but its not on disk: we deliberately treat this as just
1610
# never-present. (Why ?! - RBC 20070224)
1612
elif source_details[0] in 'fdl' and target_details[0] in 'a':
1613
# unversioned, possibly, or possibly not deleted: we dont care.
1614
# if its still on disk, *and* theres no other entry at this
1615
# path [we dont know this in this routine at the moment -
1616
# perhaps we should change this - then it would be an unknown.
1617
old_path = os.path.join(*entry[0][0:2])
1618
# parent id is the entry for the path in the target tree
1619
parent_id = state._get_entry(source_index, path_utf8=entry[0][0])[0][2]
1620
if parent_id == entry[0][2]:
1622
return ((entry[0][2], old_path, True, (True, False), (parent_id, None), (entry[0][1], None), (dirstate.DirState._minikind_to_kind[source_details[0]], None), (source_details[3], None)),)
1623
elif source_details[0] in 'fdl' and target_details[0] in 'r':
1624
# a rename; could be a true rename, or a rename inherited from
1625
# a renamed parent. TODO: handle this efficiently. Its not
1626
# common case to rename dirs though, so a correct but slow
1627
# implementation will do.
1628
if not osutils.is_inside_any(searched_specific_files, target_details[1]):
1629
search_specific_files.add(target_details[1])
1631
import pdb;pdb.set_trace()
1633
while search_specific_files:
1634
# TODO: the pending list should be lexically sorted?
1635
current_root = search_specific_files.pop()
1636
searched_specific_files.add(current_root)
1637
# process the entries for this containing directory: the rest will be
1638
# found by their parents recursively.
1639
root_entries = _entries_for_path(current_root)
1640
root_abspath = self.target.abspath(current_root)
1642
root_stat = os.lstat(root_abspath)
1644
if e.errno == errno.ENOENT:
1645
# TODO: this directory does not exist in target. Should we
1646
# consider it missing and diff, or should we just skip? For
1650
# some other random error: hand it up.
1652
root_dir_info = ('', current_root,
1653
osutils.file_kind_from_stat_mode(root_stat.st_mode), root_stat,
1656
if not root_entries:
1657
# this specified path is not present at all, skip it.
1659
for entry in root_entries:
1660
for result in _process_entry(entry, root_dir_info):
1661
# this check should probably be outside the loop: one
1662
# 'iterate two trees' api, and then _iter_changes filters
1663
# unchanged pairs. - RBC 20070226
1664
if include_unchanged or result[2] or True in map(lambda x:x[0]!=x[1], result[3:8]):
1666
dir_iterator = osutils.walkdirs(root_abspath, prefix=current_root)
1667
initial_key = (current_root, '', '')
1668
block_index, _ = state._find_block_index_from_key(initial_key)
1669
if block_index == 0:
1670
# we have processed the total root already, but because the
1671
# initial key matched it we sould skip it here.
1673
current_dir_info = dir_iterator.next()
1674
if current_dir_info[0][0] == '':
1675
# remove .bzr from iteration
1676
bzr_index = bisect_left(current_dir_info[1], ('.bzr',))
1677
assert current_dir_info[1][bzr_index][0] == '.bzr'
1678
del current_dir_info[1][bzr_index]
1679
# convert the unicode relpaths in the dir index to uf8 for
1680
# comparison with dirstate data.
1681
# TODO: keep the utf8 version around for giving to the caller.
1682
current_dir_info = ((current_dir_info[0][0].encode('utf8'), current_dir_info[0][1]),
1683
[(line[0].encode('utf8'), line[1].encode('utf8')) + line[2:] for line in current_dir_info[1]])
1684
# walk until both the directory listing and the versioned metadata
1685
# are exhausted. TODO: reevaluate this, perhaps we should stop when
1686
# the versioned data runs out.
1687
if (block_index < len(state._dirblocks) and
1688
osutils.is_inside(current_root, state._dirblocks[block_index][0])):
1689
current_block = state._dirblocks[block_index]
1691
current_block = None
1692
while (current_dir_info is not None or
1693
current_block is not None):
1694
if current_dir_info and current_block and current_dir_info[0][0] != current_block[0]:
1695
if current_block[0] < current_dir_info[0][0]:
1696
# extra dir on disk: pass for now? should del from info ?
1697
import pdb;pdb.set_trace()
1698
print 'unversioned dir'
1700
# entry referring to missing dir.
1701
import pdb;pdb.set_trace()
1704
if current_block and entry_index < len(current_block[1]):
1705
current_entry = current_block[1][entry_index]
1707
current_entry = None
1708
advance_entry = True
1710
if current_dir_info and path_index < len(current_dir_info[1]):
1711
current_path_info = current_dir_info[1][path_index]
1713
current_path_info = None
1715
while (current_entry is not None or
1716
current_path_info is not None):
1717
if current_entry is None:
1718
# no more entries: yield current_pathinfo as an
1719
# unversioned file: its not the same as a path in any
1720
# tree in the dirstate.
1721
new_executable = bool(
1722
stat.S_ISREG(current_path_info[3].st_mode)
1723
and stat.S_IEXEC & current_path_info[3].st_mode)
1724
yield (None, current_path_info[0], True, (False, False), (None, None), (None, current_path_info[1]), (None, current_path_info[2]), (None, new_executable))
1725
elif current_path_info is None:
1726
# no path is fine: the per entry code will handle it.
1727
for result in _process_entry(current_entry, current_path_info):
1728
# this check should probably be outside the loop: one
1729
# 'iterate two trees' api, and then _iter_changes filters
1730
# unchanged pairs. - RBC 20070226
1731
if include_unchanged or result[2] or True in map(lambda x:x[0]!=x[1], result[3:8]):
1733
elif current_entry[0][1] != current_path_info[1]:
1734
if current_path_info[1] < current_entry[0][1]:
1735
# extra file on disk: pass for now
1736
import pdb;pdb.set_trace()
1737
print 'unversioned file'
1739
# entry referring to file not present on disk.
1740
# advance the entry only, after processing.
1741
for result in _process_entry(current_entry, None):
1742
# this check should probably be outside the loop: one
1743
# 'iterate two trees' api, and then _iter_changes filters
1744
# unchanged pairs. - RBC 20070226
1745
if include_unchanged or result[2] or True in map(lambda x:x[0]!=x[1], result[3:8]):
1747
advance_path = False
1749
for result in _process_entry(current_entry, current_path_info):
1750
# this check should probably be outside the loop: one
1751
# 'iterate two trees' api, and then _iter_changes filters
1752
# unchanged pairs. - RBC 20070226
1753
if include_unchanged or result[2] or True in map(lambda x:x[0]!=x[1], result[3:8]):
1755
if advance_entry and current_entry is not None:
1757
if entry_index < len(current_block[1]):
1758
current_entry = current_block[1][entry_index]
1760
current_entry = None
1762
advance_entry = True # reset the advance flaga
1763
if advance_path and current_path_info is not None:
1765
if path_index < len(current_dir_info[1]):
1766
current_path_info = current_dir_info[1][path_index]
1768
current_path_info = None
1770
advance_path = True # reset the advance flagg.
1771
if current_block is not None:
1773
if (block_index < len(state._dirblocks) and
1774
osutils.is_inside(current_root, state._dirblocks[block_index][0])):
1775
current_block = state._dirblocks[block_index]
1777
current_block = None
1778
if current_dir_info is not None:
1780
current_dir_info = dir_iterator.next()
1781
# convert the unicode relpaths in the dir index to uf8 for
1782
# comparison with dirstate data.
1783
# TODO: keep the utf8 version around for giving to the caller.
1784
current_dir_info = ((current_dir_info[0][0].encode('utf8'), current_dir_info[0][1]),
1785
[(line[0].encode('utf8'), line[1].encode('utf8')) + line[2:] for line in current_dir_info[1]])
1786
except StopIteration:
1787
current_dir_info = None
1370
1791
def is_compatible(source, target):