def update_file(self, path, content, author, parent_revision, commit_message=None): commit_message = get_commit_message(commit_message) self.branch.lock_write() try: file_id = self.tree.path2id(path) if file_id is not None: f = File(self, path, file_id) content = self._get_final_text(content, f, parent_revision) else: content = normalize_content(content) with TransformPreview(self.tree) as tt: trans_id = tt.trans_id_tree_path(path) if tt.tree_kind(trans_id) is not None: tt.delete_contents(trans_id) else: name = splitpath(path)[-1] tt.version_file(gen_file_id(name), trans_id) create_parents(tt, path, trans_id) tt.create_file(content, trans_id) try: tt.commit(self.branch, commit_message, authors=[author]) except MalformedTransform, e: for conflict in e.conflicts: if conflict[0] == 'non-directory parent': path = FinalPaths(tt).get_path(trans_id) raise FileExists( '%s exists and is not a directory' % conflict[1]) raise self.tree = self.branch.basis_tree()
def inventory_to_tree_and_blobs(repo, mapping, revision_id): stack = [] cur = "" tree = Tree() inv = repo.get_inventory(revision_id) for path, entry in inv.iter_entries(): while stack and not path.startswith(cur): tree.serialize() sha = tree.sha().hexdigest() yield sha, tree t = (stat.S_IFDIR, splitpath(cur)[-1:][0].encode('UTF-8'), sha) cur, tree = stack.pop() tree.add(*t) if type(entry) == InventoryDirectory: stack.append((cur, tree)) cur = path tree = Tree() if type(entry) == InventoryFile: #FIXME: We can make potentially make this Lazy to avoid shaing lots of stuff # and having all these objects in memory at once blob = Blob() _, blob._text = repo.iter_files_bytes([(entry.file_id, revision_id, path)]).next() sha = blob.sha().hexdigest() yield sha, blob name = splitpath(path)[-1:][0].encode('UTF-8') mode = stat.S_IFREG | 0644 if entry.executable: mode |= 0111 tree.add(mode, name, sha) while len(stack) > 1: tree.serialize() sha = tree.sha().hexdigest() yield sha, tree t = (stat.S_IFDIR, splitpath(cur)[-1:][0].encode('UTF-8'), sha) cur, tree = stack.pop() tree.add(*t) tree.serialize() yield tree.sha().hexdigest(), tree
def iter_paths(path): path_segments = splitpath(path) while len(path_segments) > 0: tail = path_segments.pop() if len(path_segments) == 0: yield '', tail else: yield joinpath(*path_segments), tail
def add(self, files, ids=None, kinds=None): """Add paths to the set of versioned paths. Note that the command line normally calls smart_add instead, which can automatically recurse. This adds the files to the inventory, so that they will be recorded by the next commit. :param files: List of paths to add, relative to the base of the tree. :param ids: If set, use these instead of automatically generated ids. Must be the same length as the list of files, but may contain None for ids that are to be autogenerated. :param kinds: Optional parameter to specify the kinds to be used for each file. TODO: Perhaps callback with the ids and paths as they're added. """ if isinstance(files, basestring): # XXX: Passing a single string is inconsistent and should be # deprecated. if not (ids is None or isinstance(ids, basestring)): raise AssertionError() if not (kinds is None or isinstance(kinds, basestring)): raise AssertionError() files = [files] if ids is not None: ids = [ids] if kinds is not None: kinds = [kinds] files = [path.strip('/') for path in files] if ids is None: ids = [None] * len(files) else: if not (len(ids) == len(files)): raise AssertionError() if kinds is None: kinds = [None] * len(files) elif not len(kinds) == len(files): raise AssertionError() for f in files: # generic constraint checks: if self.is_control_filename(f): raise errors.ForbiddenControlFileError(filename=f) fp = splitpath(f) # fill out file kinds for all files [not needed when we stop # caring about the instantaneous file kind within a uncommmitted tree # self._gather_kinds(files, kinds) self._add(files, ids, kinds)
def invalidateDirectory(self, path): path = unicode(path) try: parts = osutils.splitpath(path) entry = self.cache for part in parts[:-1]: entry = entry.children[part] print "Removing", path, "from the cache" del entry.children[parts[-1]] except KeyError: pass else: self.autoRefreshPath = path self.autoRefreshTimer.start(1000)
def _cacheDirectoryStatus(self, path): p = '/'.join(path) if sys.platform != 'win32': p = '/' + p #print "caching", p try: # to stop bzr-svn from trying to give status on svn checkouts #if not QtCore.QDir(p).exists('.bzr'): # raise errors.NotBranchError(p) wt, relpath = workingtree.WorkingTree.open_containing(p) except errors.BzrError: self.fileSystemWatcher.addPath(p) return self._cacheStatus(path, 'non-versioned') self.fileSystemWatcher.addPath(wt.basedir) bt = wt.basis_tree() root = self._cacheStatus(osutils.splitpath(wt.basedir), 'branch') delta = wt.changes_from(bt, want_unchanged=True, want_unversioned=True) for entry in delta.added: self._cacheStatus(osutils.splitpath(entry[0]), 'added', root=root) for entry in delta.removed: # FIXME self._cacheStatus(osutils.splitpath(entry[0]), 'modified', root=root) # self._cacheStatus(osutils.splitpath(entry[0]), 'removed', root=root) for entry in delta.modified: self._cacheStatus(osutils.splitpath(entry[0]), 'modified', root=root) for entry in delta.unchanged: self._cacheStatus(osutils.splitpath(entry[0]), 'unchanged', root=root) for entry in delta.unversioned: self._cacheStatus(osutils.splitpath(entry[0]), 'non-versioned', root=root) try: return self._getCacheEntry(path) except KeyError: self.fileSystemWatcher.addPath(p) return self._cacheStatus(path, 'non-versioned')
def _detect_moves(self, threshold, dry_run): delta = self.tree.changes_from(self.basis_tree, want_unversioned=True) inv = self.tree.inventory unknowns = self._find_unknowns(delta) removed = set() matches = [] for path, file_id, kind in delta.removed: if kind == "directory": continue path = inv.id2path(file_id) for new_path, new_kind in unknowns: if kind != new_kind: continue similarity = self._compare_files(file_id, new_path) matches.append((similarity, path, new_path)) removed.add(path) matches.sort(reverse=True) # Try to detect file renames, based on text similarity used = set() file_renames = [] for similarity, old_path, new_path in matches: if similarity < threshold: self.outf.write( "Skipping %d file(s) with similarity below " "%d%%.\n" % (len(removed), threshold * 100)) break if old_path not in removed or new_path in used: trace.mutter("File %s already moved", old_path) continue used.add(new_path) removed.remove(old_path) file_renames.append((similarity, old_path, new_path)) # Try to detect directory renames, based on file renames dir_renames = [] dir_rename_map = {} for similarity, old_path, new_path in file_renames: old_dirs = osutils.splitpath(old_path)[:-1] new_dirs = osutils.splitpath(new_path)[:-1] for old_dir, new_dir in zip(old_dirs, new_dirs): dir_rename_map.setdefault(old_dir, set()).add(new_dir) for old_dir, new_dirs in sorted(dir_rename_map.iteritems()): if len(new_dirs) != 1: continue new_dir = iter(new_dirs).next() dir_renames.append((-1, old_dir, new_dir)) # needs to be smarted to be enabled dir_renames = [] # Actually rename renames = dir_renames + file_renames for similarity, old_path, new_path in renames: if not dry_run: parent_dirs = [] to_dir = new_path while True: to_dir, to_tail = os.path.split(to_dir) if inv.path2id(to_dir) is None: parent_dirs.append(to_dir) if not to_tail or not to_dir: break if parent_dirs: self.tree.add(reversed(parent_dirs)) self.tree.flush() self.tree.rename_one(old_path, new_path, after=True) if similarity == -1: self.outf.write("%s => %s\n" % (old_path, new_path)) else: self.outf.write("%s => %s (%d%% similar)\n" % ( old_path, new_path, similarity * 100))
def _populate_from_inventory(self): """Populate the CommitBuilder by walking the working tree inventory.""" if self.strict: # raise an exception as soon as we find a single unknown. for unknown in self.work_tree.unknowns(): raise StrictCommitFailed() specific_files = self.specific_files exclude = self.exclude report_changes = self.reporter.is_verbose() deleted_ids = [] # A tree of paths that have been deleted. E.g. if foo/bar has been # deleted, then we have {'foo':{'bar':{}}} deleted_paths = {} # XXX: Note that entries may have the wrong kind because the entry does # not reflect the status on disk. work_inv = self.work_tree.inventory # NB: entries will include entries within the excluded ids/paths # because iter_entries_by_dir has no 'exclude' facility today. entries = work_inv.iter_entries_by_dir( specific_file_ids=self.specific_file_ids, yield_parents=True) for path, existing_ie in entries: file_id = existing_ie.file_id name = existing_ie.name parent_id = existing_ie.parent_id kind = existing_ie.kind if kind == 'directory': self._next_progress_entry() # Skip files that have been deleted from the working tree. # The deleted path ids are also recorded so they can be explicitly # unversioned later. if deleted_paths: path_segments = splitpath(path) deleted_dict = deleted_paths for segment in path_segments: deleted_dict = deleted_dict.get(segment, None) if not deleted_dict: # We either took a path not present in the dict # (deleted_dict was None), or we've reached an empty # child dir in the dict, so are now a sub-path. break else: deleted_dict = None if deleted_dict is not None: # the path has a deleted parent, do not add it. continue if exclude and is_inside_any(exclude, path): # Skip excluded paths. Excluded paths are processed by # _update_builder_with_changes. continue content_summary = self.work_tree.path_content_summary(path) # Note that when a filter of specific files is given, we must only # skip/record deleted files matching that filter. if not specific_files or is_inside_any(specific_files, path): if content_summary[0] == 'missing': if not deleted_paths: # path won't have been split yet. path_segments = splitpath(path) deleted_dict = deleted_paths for segment in path_segments: deleted_dict = deleted_dict.setdefault(segment, {}) self.reporter.missing(path) deleted_ids.append(file_id) continue # TODO: have the builder do the nested commit just-in-time IF and # only if needed. if content_summary[0] == 'tree-reference': # enforce repository nested tree policy. if (not self.work_tree.supports_tree_reference() or # repository does not support it either. not self.branch.repository._format.supports_tree_reference): content_summary = ('directory',) + content_summary[1:] kind = content_summary[0] # TODO: specific_files filtering before nested tree processing if kind == 'tree-reference': if self.recursive == 'down': nested_revision_id = self._commit_nested_tree( file_id, path) content_summary = content_summary[:3] + ( nested_revision_id,) else: content_summary = content_summary[:3] + ( self.work_tree.get_reference_revision(file_id),) # Record an entry for this item # Note: I don't particularly want to have the existing_ie # parameter but the test suite currently (28-Jun-07) breaks # without it thanks to a unicode normalisation issue. :-( definitely_changed = kind != existing_ie.kind self._record_entry(path, file_id, specific_files, kind, name, parent_id, definitely_changed, existing_ie, report_changes, content_summary) # Unversion IDs that were found to be deleted self.work_tree.unversion(deleted_ids)
def _populate_from_inventory(self): """Populate the CommitBuilder by walking the working tree inventory.""" # Build the revision inventory. # # This starts by creating a new empty inventory. Depending on # which files are selected for commit, and what is present in the # current tree, the new inventory is populated. inventory entries # which are candidates for modification have their revision set to # None; inventory entries that are carried over untouched have their # revision set to their prior value. # # ESEPARATIONOFCONCERNS: this function is diffing and using the diff # results to create a new inventory at the same time, which results # in bugs like #46635. Any reason not to use/enhance Tree.changes_from? # ADHB 11-07-2006 specific_files = self.specific_files exclude = self.exclude report_changes = self.reporter.is_verbose() deleted_ids = [] # A tree of paths that have been deleted. E.g. if foo/bar has been # deleted, then we have {'foo':{'bar':{}}} deleted_paths = {} # XXX: Note that entries may have the wrong kind because the entry does # not reflect the status on disk. # NB: entries will include entries within the excluded ids/paths # because iter_entries_by_dir has no 'exclude' facility today. entries = self.work_tree.iter_entries_by_dir( specific_file_ids=self.specific_file_ids, yield_parents=True) for path, existing_ie in entries: file_id = existing_ie.file_id name = existing_ie.name parent_id = existing_ie.parent_id kind = existing_ie.kind # Skip files that have been deleted from the working tree. # The deleted path ids are also recorded so they can be explicitly # unversioned later. if deleted_paths: path_segments = splitpath(path) deleted_dict = deleted_paths for segment in path_segments: deleted_dict = deleted_dict.get(segment, None) if not deleted_dict: # We either took a path not present in the dict # (deleted_dict was None), or we've reached an empty # child dir in the dict, so are now a sub-path. break else: deleted_dict = None if deleted_dict is not None: # the path has a deleted parent, do not add it. continue if exclude and is_inside_any(exclude, path): # Skip excluded paths. Excluded paths are processed by # _update_builder_with_changes. continue content_summary = self.work_tree.path_content_summary(path) kind = content_summary[0] # Note that when a filter of specific files is given, we must only # skip/record deleted files matching that filter. if not specific_files or is_inside_any(specific_files, path): if kind == 'missing': if not deleted_paths: # path won't have been split yet. path_segments = splitpath(path) deleted_dict = deleted_paths for segment in path_segments: deleted_dict = deleted_dict.setdefault(segment, {}) self.reporter.missing(path) self._next_progress_entry() deleted_ids.append(file_id) continue # TODO: have the builder do the nested commit just-in-time IF and # only if needed. if kind == 'tree-reference': # enforce repository nested tree policy. if (not self.work_tree.supports_tree_reference() or # repository does not support it either. not self.branch.repository._format.supports_tree_reference): kind = 'directory' content_summary = (kind, None, None, None) elif self.recursive == 'down': nested_revision_id = self._commit_nested_tree( file_id, path) content_summary = (kind, None, None, nested_revision_id) else: nested_revision_id = self.work_tree.get_reference_revision(file_id) content_summary = (kind, None, None, nested_revision_id) # Record an entry for this item # Note: I don't particularly want to have the existing_ie # parameter but the test suite currently (28-Jun-07) breaks # without it thanks to a unicode normalisation issue. :-( definitely_changed = kind != existing_ie.kind self._record_entry(path, file_id, specific_files, kind, name, parent_id, definitely_changed, existing_ie, report_changes, content_summary) # Unversion IDs that were found to be deleted self.deleted_ids = deleted_ids
def _populate_from_inventory(self): """Populate the CommitBuilder by walking the working tree inventory.""" # Build the revision inventory. # # This starts by creating a new empty inventory. Depending on # which files are selected for commit, and what is present in the # current tree, the new inventory is populated. inventory entries # which are candidates for modification have their revision set to # None; inventory entries that are carried over untouched have their # revision set to their prior value. # # ESEPARATIONOFCONCERNS: this function is diffing and using the diff # results to create a new inventory at the same time, which results # in bugs like #46635. Any reason not to use/enhance Tree.changes_from? # ADHB 11-07-2006 specific_files = self.specific_files exclude = self.exclude report_changes = self.reporter.is_verbose() deleted_ids = [] # A tree of paths that have been deleted. E.g. if foo/bar has been # deleted, then we have {'foo':{'bar':{}}} deleted_paths = {} # XXX: Note that entries may have the wrong kind because the entry does # not reflect the status on disk. # NB: entries will include entries within the excluded ids/paths # because iter_entries_by_dir has no 'exclude' facility today. entries = self.work_tree.iter_entries_by_dir( specific_file_ids=self.specific_file_ids, yield_parents=True) for path, existing_ie in entries: file_id = existing_ie.file_id name = existing_ie.name parent_id = existing_ie.parent_id kind = existing_ie.kind # Skip files that have been deleted from the working tree. # The deleted path ids are also recorded so they can be explicitly # unversioned later. if deleted_paths: path_segments = splitpath(path) deleted_dict = deleted_paths for segment in path_segments: deleted_dict = deleted_dict.get(segment, None) if not deleted_dict: # We either took a path not present in the dict # (deleted_dict was None), or we've reached an empty # child dir in the dict, so are now a sub-path. break else: deleted_dict = None if deleted_dict is not None: # the path has a deleted parent, do not add it. continue if exclude and is_inside_any(exclude, path): # Skip excluded paths. Excluded paths are processed by # _update_builder_with_changes. continue content_summary = self.work_tree.path_content_summary(path) kind = content_summary[0] # Note that when a filter of specific files is given, we must only # skip/record deleted files matching that filter. if not specific_files or is_inside_any(specific_files, path): if kind == 'missing': if not deleted_paths: # path won't have been split yet. path_segments = splitpath(path) deleted_dict = deleted_paths for segment in path_segments: deleted_dict = deleted_dict.setdefault(segment, {}) self.reporter.missing(path) self._next_progress_entry() deleted_ids.append(file_id) continue # TODO: have the builder do the nested commit just-in-time IF and # only if needed. if kind == 'tree-reference': # enforce repository nested tree policy. if (not self.work_tree.supports_tree_reference() or # repository does not support it either. not self.branch.repository._format. supports_tree_reference): kind = 'directory' content_summary = (kind, None, None, None) elif self.recursive == 'down': nested_revision_id = self._commit_nested_tree( file_id, path) content_summary = (kind, None, None, nested_revision_id) else: nested_revision_id = self.work_tree.get_reference_revision( file_id) content_summary = (kind, None, None, nested_revision_id) # Record an entry for this item # Note: I don't particularly want to have the existing_ie # parameter but the test suite currently (28-Jun-07) breaks # without it thanks to a unicode normalisation issue. :-( definitely_changed = kind != existing_ie.kind self._record_entry(path, file_id, specific_files, kind, name, parent_id, definitely_changed, existing_ie, report_changes, content_summary) # Unversion IDs that were found to be deleted self.deleted_ids = deleted_ids