1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
1
# Copyright (C) 2005, 2006 Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
27
27
# created, but it's not for now.
28
28
ROOT_ID = "TREE_ROOT"
34
from bzrlib.lazy_import import lazy_import
35
lazy_import(globals(), """
49
from bzrlib.errors import (
39
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
40
pathjoin, sha_strings)
41
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
42
BzrError, BzrCheckError, BinaryFile)
53
43
from bzrlib.trace import mutter
90
80
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
91
81
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
92
82
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
93
>>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
83
>>> shouldbe = {0: 'src', 1: pathjoin('src','hello.c')}
94
84
>>> for ix, j in enumerate(i.iter_entries()):
95
85
... print (j[0] == shouldbe[ix], j[1])
97
(True, InventoryDirectory('TREE_ROOT', u'', parent_id=None, revision=None))
98
87
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
99
88
(True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
89
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
90
Traceback (most recent call last):
92
BzrError: inventory already contains entry with id {2323}
100
93
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
101
94
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
102
95
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
181
173
:param entry_vf: The entry versioned file, if its already available.
183
175
def get_ancestors(weave, entry):
184
return set(weave.get_ancestry(entry.revision, topo_sorted=False))
176
return set(weave.get_ancestry(entry.revision))
185
177
# revision:ie mapping for each ie found in previous_inventories.
187
179
# revision:ie mapping with one revision for each head.
193
185
if self.file_id in inv:
194
186
ie = inv[self.file_id]
195
187
assert ie.file_id == self.file_id
196
if ie.kind != self.kind:
197
# Can't be a candidate if the kind has changed.
199
188
if ie.revision in candidates:
200
189
# same revision value in two different inventories:
201
190
# correct possible inconsistencies:
254
243
def get_tar_item(self, root, dp, now, tree):
255
244
"""Get a tarfile item and a file stream for its content."""
256
item = tarfile.TarInfo(osutils.pathjoin(root, dp).encode('utf8'))
245
item = tarfile.TarInfo(pathjoin(root, dp))
257
246
# TODO: would be cool to actually set it to the timestamp of the
258
247
# revision it was last changed
289
278
assert isinstance(name, basestring), name
290
279
if '/' in name or '\\' in name:
291
raise errors.InvalidEntryName(name=name)
280
raise InvalidEntryName(name=name)
292
281
self.executable = False
293
282
self.revision = None
294
283
self.text_sha1 = None
295
284
self.text_size = None
296
285
self.file_id = file_id
297
assert isinstance(file_id, (str, None.__class__)), \
298
'bad type %r for %r' % (type(file_id), file_id)
300
287
self.text_id = text_id
301
288
self.parent_id = parent_id
302
289
self.symlink_target = None
303
self.reference_revision = None
305
291
def kind_character(self):
306
292
"""Return a short kind indicator useful for appending to names."""
307
293
raise BzrError('unknown kind %r' % self.kind)
309
known_kinds = ('file', 'directory', 'symlink')
295
known_kinds = ('file', 'directory', 'symlink', 'root_directory')
311
297
def _put_in_tar(self, item, tree):
312
298
"""populate item for stashing in a tar, and return the content stream.
322
308
This is a template method - implement _put_on_disk in subclasses.
324
fullpath = osutils.pathjoin(dest, dp)
310
fullpath = pathjoin(dest, dp)
325
311
self._put_on_disk(fullpath, tree)
326
# mutter(" export {%s} kind %s to %s", self.file_id,
327
# self.kind, fullpath)
312
mutter(" export {%s} kind %s to %s", self.file_id,
329
315
def _put_on_disk(self, fullpath, tree):
330
316
"""Put this entry onto disk at fullpath, from tree tree."""
337
323
def versionable_kind(kind):
338
return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
324
return kind in ('file', 'directory', 'symlink')
340
326
def check(self, checker, rev_id, inv, tree):
341
327
"""Check this inventory entry is intact.
423
407
This means that all its fields are populated, that it has its
424
408
text stored in the text store or weave.
426
# mutter('new parents of %s are %r', path, previous_entries)
410
mutter('new parents of %s are %r', path, previous_entries)
427
411
self._read_tree_state(path, work_tree)
428
412
# TODO: Where should we determine whether to reuse a
429
413
# previous revision id or create a new revision? 20060606
431
415
# cannot be unchanged unless there is only one parent file rev.
432
416
parent_ie = previous_entries.values()[0]
433
417
if self._unchanged(parent_ie):
434
# mutter("found unchanged entry")
418
mutter("found unchanged entry")
435
419
self.revision = parent_ie.revision
436
420
return "unchanged"
437
421
return self._snapshot_into_revision(revision, previous_entries,
449
433
:returns: String description of the commit (e.g. "merged", "modified"), etc.
451
# mutter('new revision {%s} for {%s}', revision, self.file_id)
435
mutter('new revision {%s} for {%s}', revision, self.file_id)
452
436
self.revision = revision
453
437
self._snapshot_text(previous_entries, work_tree, commit_builder)
516
497
class RootEntry(InventoryEntry):
518
499
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
519
'text_id', 'parent_id', 'children', 'executable',
520
'revision', 'symlink_target', 'reference_revision']
500
'text_id', 'parent_id', 'children', 'executable',
501
'revision', 'symlink_target']
522
503
def _check(self, checker, rev_id, tree):
523
504
"""See InventoryEntry._check"""
525
506
def __init__(self, file_id):
526
507
self.file_id = file_id
527
508
self.children = {}
528
self.kind = 'directory'
509
self.kind = 'root_directory'
529
510
self.parent_id = None
531
512
self.revision = None
532
symbol_versioning.warn('RootEntry is deprecated as of bzr 0.10.'
533
' Please use InventoryDirectory instead.',
534
DeprecationWarning, stacklevel=2)
536
514
def __eq__(self, other):
537
515
if not isinstance(other, RootEntry):
545
523
"""A directory in an inventory."""
547
525
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
548
'text_id', 'parent_id', 'children', 'executable',
549
'revision', 'symlink_target', 'reference_revision']
526
'text_id', 'parent_id', 'children', 'executable',
527
'revision', 'symlink_target']
551
529
def _check(self, checker, rev_id, tree):
552
530
"""See InventoryEntry._check"""
592
570
"""A file in an inventory."""
594
572
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
595
'text_id', 'parent_id', 'children', 'executable',
596
'revision', 'symlink_target', 'reference_revision']
573
'text_id', 'parent_id', 'children', 'executable',
574
'revision', 'symlink_target']
598
576
def _check(self, checker, tree_revision_id, tree):
599
577
"""See InventoryEntry._check"""
661
639
text_diff(to_label, to_text,
662
640
from_label, from_text, output_to)
663
except errors.BinaryFile:
665
643
label_pair = (to_label, from_label)
693
671
def _put_on_disk(self, fullpath, tree):
694
672
"""See InventoryEntry._put_on_disk."""
695
osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
673
pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
696
674
if tree.is_executable(self.file_id):
697
675
os.chmod(fullpath, 0755)
740
718
"""A file in an inventory."""
742
720
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
743
'text_id', 'parent_id', 'children', 'executable',
744
'revision', 'symlink_target', 'reference_revision']
721
'text_id', 'parent_id', 'children', 'executable',
722
'revision', 'symlink_target']
746
724
def _check(self, checker, rev_id, tree):
747
725
"""See InventoryEntry._check"""
828
806
self.file_id, file_parents, self.symlink_target)
831
class TreeReference(InventoryEntry):
833
kind = 'tree-reference'
835
def __init__(self, file_id, name, parent_id, revision=None,
836
reference_revision=None):
837
InventoryEntry.__init__(self, file_id, name, parent_id)
838
self.revision = revision
839
self.reference_revision = reference_revision
842
return TreeReference(self.file_id, self.name, self.parent_id,
843
self.revision, self.reference_revision)
845
def _snapshot_text(self, file_parents, work_tree, commit_builder):
846
commit_builder.modified_reference(self.file_id, file_parents)
848
def _read_tree_state(self, path, work_tree):
849
"""Populate fields in the inventory entry from the given tree.
851
self.reference_revision = work_tree.get_reference_revision(
854
def _forget_tree_state(self):
855
self.reference_revision = None
858
809
class Inventory(object):
859
810
"""Inventory of versioned files in a tree.
888
839
May also look up by name:
890
841
>>> [x[0] for x in inv.iter_entries()]
892
843
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
893
844
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
894
Traceback (most recent call last):
895
BzrError: parent_id {TREE_ROOT} not in inventory
896
>>> inv.add(InventoryFile('123-123', 'hello.c', 'TREE_ROOT-12345678-12345678'))
897
845
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
899
847
def __init__(self, root_id=ROOT_ID, revision_id=None):
906
854
The inventory is created with a default root directory, with
909
if root_id is not None:
910
assert root_id.__class__ == str
911
self._set_root(InventoryDirectory(root_id, u'', None))
857
# We are letting Branch.create() create a unique inventory
858
# root id. Rather than generating a random one here.
860
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
861
self.root = RootEntry(root_id)
862
# FIXME: this isn't ever used, changing it to self.revision may break
863
# things. TODO make everything use self.revision_id
915
864
self.revision_id = revision_id
917
def _set_root(self, ie):
919
865
self._byid = {self.root.file_id: self.root}
922
868
# TODO: jam 20051218 Should copy also copy the revision_id?
923
entries = self.iter_entries()
924
other = Inventory(entries.next()[1].file_id)
869
other = Inventory(self.root.file_id)
925
870
# copy recursively so we know directories will be added before
926
871
# their children. There are more efficient ways than this...
927
for path, entry in entries():
872
for path, entry in self.iter_entries():
873
if entry == self.root:
928
875
other.add(entry.copy())
938
885
def iter_entries(self, from_dir=None):
939
886
"""Return (path, entry) pairs, in order by name."""
940
887
if from_dir is None:
941
if self.root is None:
943
889
from_dir = self.root
945
890
elif isinstance(from_dir, basestring):
946
891
from_dir = self._byid[from_dir]
979
924
# if we finished all children, pop it off the stack
982
def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None):
927
def iter_entries_by_dir(self, from_dir=None):
983
928
"""Iterate over the entries in a directory first order.
985
930
This returns all entries for a directory before returning
990
935
:return: This yields (path, entry) pairs
992
if specific_file_ids:
993
safe = osutils.safe_file_id
994
specific_file_ids = set(safe(fid) for fid in specific_file_ids)
995
937
# TODO? Perhaps this should return the from_dir so that the root is
996
938
# yielded? or maybe an option?
997
939
if from_dir is None:
998
if self.root is None:
1000
# Optimize a common case
1001
if specific_file_ids is not None and len(specific_file_ids) == 1:
1002
file_id = list(specific_file_ids)[0]
1004
yield self.id2path(file_id), self[file_id]
1006
941
from_dir = self.root
1007
if (specific_file_ids is None or
1008
self.root.file_id in specific_file_ids):
1009
yield u'', self.root
1010
942
elif isinstance(from_dir, basestring):
1011
943
from_dir = self._byid[from_dir]
1013
if specific_file_ids is not None:
1014
# TODO: jam 20070302 This could really be done as a loop rather
1015
# than a bunch of recursive calls.
1018
def add_ancestors(file_id):
1019
if file_id not in byid:
1021
parent_id = byid[file_id].parent_id
1022
if parent_id is None:
1024
if parent_id not in parents:
1025
parents.add(parent_id)
1026
add_ancestors(parent_id)
1027
for file_id in specific_file_ids:
1028
add_ancestors(file_id)
1032
945
stack = [(u'', from_dir)]
1039
952
child_relpath = cur_relpath + child_name
1041
if (specific_file_ids is None or
1042
child_ie.file_id in specific_file_ids):
1043
yield child_relpath, child_ie
954
yield child_relpath, child_ie
1045
956
if child_ie.kind == 'directory':
1046
if parents is None or child_ie.file_id in parents:
1047
child_dirs.append((child_relpath+'/', child_ie))
957
child_dirs.append((child_relpath+'/', child_ie))
1048
958
stack.extend(reversed(child_dirs))
1050
def make_entry(self, kind, name, parent_id, file_id=None):
1051
"""Simple thunk to bzrlib.inventory.make_entry."""
1052
return make_entry(kind, name, parent_id, file_id)
1054
960
def entries(self):
1055
961
"""Return list of (path, ie) for all entries except the root.
1061
967
kids = dir_ie.children.items()
1063
969
for name, ie in kids:
1064
child_path = osutils.pathjoin(dir_path, name)
970
child_path = pathjoin(dir_path, name)
1065
971
accum.append((child_path, ie))
1066
972
if ie.kind == 'directory':
1067
973
descend(ie, child_path)
1082
988
for name, child_ie in kids:
1083
child_path = osutils.pathjoin(parent_path, name)
989
child_path = pathjoin(parent_path, name)
1084
990
descend(child_ie, child_path)
1085
991
descend(self.root, u'')
1096
1002
>>> '456' in inv
1099
file_id = osutils.safe_file_id(file_id)
1100
return (file_id in self._byid)
1005
return file_id in self._byid
1102
1007
def __getitem__(self, file_id):
1103
1008
"""Return the entry for given file_id.
1108
1013
>>> inv['123123'].name
1111
file_id = osutils.safe_file_id(file_id)
1113
1017
return self._byid[file_id]
1114
1018
except KeyError:
1115
# really we're passing an inventory, not a tree...
1116
raise errors.NoSuchId(self, file_id)
1020
raise BzrError("can't look up file_id None")
1022
raise BzrError("file_id {%s} not in inventory" % file_id)
1118
1024
def get_file_kind(self, file_id):
1119
file_id = osutils.safe_file_id(file_id)
1120
1025
return self._byid[file_id].kind
1122
1027
def get_child(self, parent_id, filename):
1123
parent_id = osutils.safe_file_id(parent_id)
1124
1028
return self[parent_id].children.get(filename)
1126
def _add_child(self, entry):
1127
"""Add an entry to the inventory, without adding it to its parent"""
1128
if entry.file_id in self._byid:
1129
raise BzrError("inventory already contains entry with id {%s}" %
1131
self._byid[entry.file_id] = entry
1132
for child in getattr(entry, 'children', {}).itervalues():
1133
self._add_child(child)
1136
1030
def add(self, entry):
1137
1031
"""Add entry to inventory.
1142
1036
Returns the new entry object.
1144
1038
if entry.file_id in self._byid:
1145
raise errors.DuplicateFileId(entry.file_id,
1146
self._byid[entry.file_id])
1148
if entry.parent_id is None:
1149
assert self.root is None and len(self._byid) == 0
1153
parent = self._byid[entry.parent_id]
1155
raise BzrError("parent_id {%s} not in inventory" %
1158
if entry.name in parent.children:
1159
raise BzrError("%s is already versioned" %
1160
osutils.pathjoin(self.id2path(parent.file_id),
1161
entry.name).encode('utf-8'))
1162
parent.children[entry.name] = entry
1163
return self._add_child(entry)
1039
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
1041
if entry.parent_id == ROOT_ID or entry.parent_id is None:
1042
entry.parent_id = self.root.file_id
1045
parent = self._byid[entry.parent_id]
1047
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
1049
if entry.name in parent.children:
1050
raise BzrError("%s is already versioned" %
1051
pathjoin(self.id2path(parent.file_id), entry.name))
1053
self._byid[entry.file_id] = entry
1054
parent.children[entry.name] = entry
1165
1057
def add_path(self, relpath, kind, file_id=None, parent_id=None):
1166
1058
"""Add entry from a path.
1170
1062
Returns the new entry object."""
1172
parts = osutils.splitpath(relpath)
1064
parts = bzrlib.osutils.splitpath(relpath)
1174
1066
if len(parts) == 0:
1175
1067
if file_id is None:
1176
file_id = generate_ids.gen_root_id()
1178
file_id = osutils.safe_file_id(file_id)
1179
self.root = InventoryDirectory(file_id, '', None)
1068
file_id = bzrlib.workingtree.gen_root_id()
1069
self.root = RootEntry(file_id)
1180
1070
self._byid = {self.root.file_id: self.root}
1183
1073
parent_path = parts[:-1]
1184
1074
parent_id = self.path2id(parent_path)
1185
1075
if parent_id is None:
1186
raise errors.NotVersionedError(path=parent_path)
1076
raise NotVersionedError(path=parent_path)
1187
1077
ie = make_entry(kind, parts[-1], parent_id, file_id)
1188
1078
return self.add(ie)
1239
1128
def _iter_file_id_parents(self, file_id):
1240
1129
"""Yield the parents of file_id up to the root."""
1241
file_id = osutils.safe_file_id(file_id)
1242
1130
while file_id is not None:
1244
1132
ie = self._byid[file_id]
1245
1133
except KeyError:
1246
raise errors.NoSuchId(tree=None, file_id=file_id)
1134
raise BzrError("file_id {%s} not found in inventory" % file_id)
1248
1136
file_id = ie.parent_id
1255
1143
is equal to the depth of the file in the tree, counting the
1256
1144
root directory as depth 1.
1258
file_id = osutils.safe_file_id(file_id)
1260
1147
for parent in self._iter_file_id_parents(file_id):
1261
1148
p.insert(0, parent.file_id)
1270
1157
>>> print i.id2path('foo-id')
1273
file_id = osutils.safe_file_id(file_id)
1274
1160
# get all names, skipping root
1275
1161
return '/'.join(reversed(
1276
1162
[parent.name for parent in
1288
1174
Returns None IFF the path is not found.
1290
if isinstance(name, basestring):
1291
name = osutils.splitpath(name)
1176
if isinstance(name, types.StringTypes):
1177
name = splitpath(name)
1293
1179
# mutter("lookup path %r" % name)
1295
1181
parent = self.root
1300
children = getattr(parent, 'children', None)
1301
if children is None:
1184
cie = parent.children[f]
1304
1185
assert cie.name == f
1305
1186
assert cie.parent_id == parent.file_id
1314
1195
return bool(self.path2id(names))
1316
1197
def has_id(self, file_id):
1317
file_id = osutils.safe_file_id(file_id)
1318
return (file_id in self._byid)
1320
def remove_recursive_id(self, file_id):
1321
"""Remove file_id, and children, from the inventory.
1323
:param file_id: A file_id to remove.
1325
file_id = osutils.safe_file_id(file_id)
1326
to_find_delete = [self._byid[file_id]]
1328
while to_find_delete:
1329
ie = to_find_delete.pop()
1330
to_delete.append(ie.file_id)
1331
if ie.kind == 'directory':
1332
to_find_delete.extend(ie.children.values())
1333
for file_id in reversed(to_delete):
1335
del self._byid[file_id]
1336
if ie.parent_id is not None:
1337
del self[ie.parent_id].children[ie.name]
1198
return self._byid.has_key(file_id)
1341
1200
def rename(self, file_id, new_parent_id, new_name):
1342
1201
"""Move a file within the inventory.
1344
1203
This can change either the name, or the parent, or both.
1346
This does not move the working file.
1348
file_id = osutils.safe_file_id(file_id)
1205
This does not move the working file."""
1349
1206
if not is_valid_name(new_name):
1350
1207
raise BzrError("not an acceptable filename: %r" % new_name)
1369
1226
file_ie.name = new_name
1370
1227
file_ie.parent_id = new_parent_id
1372
def is_root(self, file_id):
1373
file_id = osutils.safe_file_id(file_id)
1374
return self.root is not None and file_id == self.root.file_id
1378
'directory': InventoryDirectory,
1379
'file': InventoryFile,
1380
'symlink': InventoryLink,
1381
'tree-reference': TreeReference
1384
1230
def make_entry(kind, name, parent_id, file_id=None):
1385
1231
"""Create an inventory entry.
1390
1236
:param file_id: the file_id to use. if None, one will be created.
1392
1238
if file_id is None:
1393
file_id = generate_ids.gen_file_id(name)
1239
file_id = bzrlib.workingtree.gen_file_id(name)
1240
if kind == 'directory':
1241
return InventoryDirectory(file_id, name, parent_id)
1242
elif kind == 'file':
1243
return InventoryFile(file_id, name, parent_id)
1244
elif kind == 'symlink':
1245
return InventoryLink(file_id, name, parent_id)
1395
file_id = osutils.safe_file_id(file_id)
1397
#------- This has been copied to bzrlib.dirstate.DirState.add, please
1398
# keep them synchronised.
1399
# we dont import normalized_filename directly because we want to be
1400
# able to change the implementation at runtime for tests.
1401
norm_name, can_access = osutils.normalized_filename(name)
1402
if norm_name != name:
1406
# TODO: jam 20060701 This would probably be more useful
1407
# if the error was raised with the full path
1408
raise errors.InvalidNormalization(name)
1411
factory = entry_factory[kind]
1413
1247
raise BzrError("unknown kind %r" % kind)
1414
return factory(file_id, name, parent_id)
1417
1251
_NAME_RE = None