453 lines
13 KiB
Python
453 lines
13 KiB
Python
import datetime
|
|
import difflib
|
|
import os
|
|
import time
|
|
|
|
import rope.base.fscommands
|
|
from rope.base import taskhandle, exceptions, utils
|
|
|
|
|
|
class Change(object):
|
|
"""The base class for changes
|
|
|
|
Rope refactorings return `Change` objects. They can be previewed,
|
|
committed or undone.
|
|
"""
|
|
|
|
def do(self, job_set=None):
|
|
"""Perform the change
|
|
|
|
.. note:: Do use this directly. Use `Project.do()` instead.
|
|
"""
|
|
|
|
def undo(self, job_set=None):
|
|
"""Perform the change
|
|
|
|
.. note:: Do use this directly. Use `History.undo()` instead.
|
|
"""
|
|
|
|
def get_description(self):
|
|
"""Return the description of this change
|
|
|
|
This can be used for previewing the changes.
|
|
"""
|
|
return str(self)
|
|
|
|
def get_changed_resources(self):
|
|
"""Return the list of resources that will be changed"""
|
|
return []
|
|
|
|
@property
|
|
@utils.saveit
|
|
def _operations(self):
|
|
return _ResourceOperations(self.resource.project)
|
|
|
|
|
|
class ChangeSet(Change):
|
|
"""A collection of `Change` objects
|
|
|
|
This class holds a collection of changes. This class provides
|
|
these fields:
|
|
|
|
* `changes`: the list of changes
|
|
* `description`: the goal of these changes
|
|
"""
|
|
|
|
def __init__(self, description, timestamp=None):
|
|
self.changes = []
|
|
self.description = description
|
|
self.time = timestamp
|
|
|
|
def do(self, job_set=taskhandle.NullJobSet()):
|
|
try:
|
|
done = []
|
|
for change in self.changes:
|
|
change.do(job_set)
|
|
done.append(change)
|
|
self.time = time.time()
|
|
except Exception:
|
|
for change in done:
|
|
change.undo()
|
|
raise
|
|
|
|
def undo(self, job_set=taskhandle.NullJobSet()):
|
|
try:
|
|
done = []
|
|
for change in reversed(self.changes):
|
|
change.undo(job_set)
|
|
done.append(change)
|
|
except Exception:
|
|
for change in done:
|
|
change.do()
|
|
raise
|
|
|
|
def add_change(self, change):
|
|
self.changes.append(change)
|
|
|
|
def get_description(self):
|
|
result = [str(self) + ":\n\n\n"]
|
|
for change in self.changes:
|
|
result.append(change.get_description())
|
|
result.append("\n")
|
|
return "".join(result)
|
|
|
|
def __str__(self):
|
|
if self.time is not None:
|
|
date = datetime.datetime.fromtimestamp(self.time)
|
|
if date.date() == datetime.date.today():
|
|
string_date = "today"
|
|
elif date.date() == (datetime.date.today() - datetime.timedelta(1)):
|
|
string_date = "yesterday"
|
|
elif date.year == datetime.date.today().year:
|
|
string_date = date.strftime("%b %d")
|
|
else:
|
|
string_date = date.strftime("%d %b, %Y")
|
|
string_time = date.strftime("%H:%M:%S")
|
|
string_time = "%s %s " % (string_date, string_time)
|
|
return self.description + " - " + string_time
|
|
return self.description
|
|
|
|
def get_changed_resources(self):
|
|
result = set()
|
|
for change in self.changes:
|
|
result.update(change.get_changed_resources())
|
|
return result
|
|
|
|
|
|
def _handle_job_set(function):
|
|
"""A decorator for handling `taskhandle.JobSet`
|
|
|
|
A decorator for handling `taskhandle.JobSet` for `do` and `undo`
|
|
methods of `Change`.
|
|
"""
|
|
|
|
def call(self, job_set=taskhandle.NullJobSet()):
|
|
job_set.started_job(str(self))
|
|
function(self)
|
|
job_set.finished_job()
|
|
|
|
return call
|
|
|
|
|
|
class ChangeContents(Change):
|
|
"""A class to change the contents of a file
|
|
|
|
Fields:
|
|
|
|
* `resource`: The `rope.base.resources.File` to change
|
|
* `new_contents`: What to write in the file
|
|
"""
|
|
|
|
def __init__(self, resource, new_contents, old_contents=None):
|
|
self.resource = resource
|
|
# IDEA: Only saving diffs; possible problems when undo/redoing
|
|
self.new_contents = new_contents
|
|
self.old_contents = old_contents
|
|
|
|
@_handle_job_set
|
|
def do(self):
|
|
if self.old_contents is None:
|
|
self.old_contents = self.resource.read()
|
|
self._operations.write_file(self.resource, self.new_contents)
|
|
|
|
@_handle_job_set
|
|
def undo(self):
|
|
if self.old_contents is None:
|
|
raise exceptions.HistoryError("Undoing a change that is not performed yet!")
|
|
self._operations.write_file(self.resource, self.old_contents)
|
|
|
|
def __str__(self):
|
|
return "Change <%s>" % self.resource.path
|
|
|
|
def get_description(self):
|
|
new = self.new_contents
|
|
old = self.old_contents
|
|
if old is None:
|
|
if self.resource.exists():
|
|
old = self.resource.read()
|
|
else:
|
|
old = ""
|
|
result = difflib.unified_diff(
|
|
old.splitlines(True),
|
|
new.splitlines(True),
|
|
"a/" + self.resource.path,
|
|
"b/" + self.resource.path,
|
|
)
|
|
return "".join(list(result))
|
|
|
|
def get_changed_resources(self):
|
|
return [self.resource]
|
|
|
|
|
|
class MoveResource(Change):
|
|
"""Move a resource to a new location
|
|
|
|
Fields:
|
|
|
|
* `resource`: The `rope.base.resources.Resource` to move
|
|
* `new_resource`: The destination for move; It is the moved
|
|
resource not the folder containing that resource.
|
|
"""
|
|
|
|
def __init__(self, resource, new_location, exact=False):
|
|
self.project = resource.project
|
|
self.resource = resource
|
|
if not exact:
|
|
new_location = _get_destination_for_move(resource, new_location)
|
|
if resource.is_folder():
|
|
self.new_resource = self.project.get_folder(new_location)
|
|
else:
|
|
self.new_resource = self.project.get_file(new_location)
|
|
|
|
@_handle_job_set
|
|
def do(self):
|
|
self._operations.move(self.resource, self.new_resource)
|
|
|
|
@_handle_job_set
|
|
def undo(self):
|
|
self._operations.move(self.new_resource, self.resource)
|
|
|
|
def __str__(self):
|
|
return "Move <%s>" % self.resource.path
|
|
|
|
def get_description(self):
|
|
return "rename from %s\nrename to %s" % (
|
|
self.resource.path,
|
|
self.new_resource.path,
|
|
)
|
|
|
|
def get_changed_resources(self):
|
|
return [self.resource, self.new_resource]
|
|
|
|
|
|
class CreateResource(Change):
|
|
"""A class to create a resource
|
|
|
|
Fields:
|
|
|
|
* `resource`: The resource to create
|
|
"""
|
|
|
|
def __init__(self, resource):
|
|
self.resource = resource
|
|
|
|
@_handle_job_set
|
|
def do(self):
|
|
self._operations.create(self.resource)
|
|
|
|
@_handle_job_set
|
|
def undo(self):
|
|
self._operations.remove(self.resource)
|
|
|
|
def __str__(self):
|
|
return "Create Resource <%s>" % (self.resource.path)
|
|
|
|
def get_description(self):
|
|
return "new file %s" % (self.resource.path)
|
|
|
|
def get_changed_resources(self):
|
|
return [self.resource]
|
|
|
|
def _get_child_path(self, parent, name):
|
|
if parent.path == "":
|
|
return name
|
|
else:
|
|
return parent.path + "/" + name
|
|
|
|
|
|
class CreateFolder(CreateResource):
|
|
"""A class to create a folder
|
|
|
|
See docs for `CreateResource`.
|
|
"""
|
|
|
|
def __init__(self, parent, name):
|
|
resource = parent.project.get_folder(self._get_child_path(parent, name))
|
|
super(CreateFolder, self).__init__(resource)
|
|
|
|
|
|
class CreateFile(CreateResource):
|
|
"""A class to create a file
|
|
|
|
See docs for `CreateResource`.
|
|
"""
|
|
|
|
def __init__(self, parent, name):
|
|
resource = parent.project.get_file(self._get_child_path(parent, name))
|
|
super(CreateFile, self).__init__(resource)
|
|
|
|
|
|
class RemoveResource(Change):
|
|
"""A class to remove a resource
|
|
|
|
Fields:
|
|
|
|
* `resource`: The resource to be removed
|
|
"""
|
|
|
|
def __init__(self, resource):
|
|
self.resource = resource
|
|
|
|
@_handle_job_set
|
|
def do(self):
|
|
self._operations.remove(self.resource)
|
|
|
|
# TODO: Undoing remove operations
|
|
@_handle_job_set
|
|
def undo(self):
|
|
raise NotImplementedError("Undoing `RemoveResource` is not implemented yet.")
|
|
|
|
def __str__(self):
|
|
return "Remove <%s>" % (self.resource.path)
|
|
|
|
def get_changed_resources(self):
|
|
return [self.resource]
|
|
|
|
|
|
def count_changes(change):
|
|
"""Counts the number of basic changes a `Change` will make"""
|
|
if isinstance(change, ChangeSet):
|
|
result = 0
|
|
for child in change.changes:
|
|
result += count_changes(child)
|
|
return result
|
|
return 1
|
|
|
|
|
|
def create_job_set(task_handle, change):
|
|
return task_handle.create_jobset(str(change), count_changes(change))
|
|
|
|
|
|
class _ResourceOperations(object):
|
|
def __init__(self, project):
|
|
self.project = project
|
|
self.fscommands = project.fscommands
|
|
self.direct_commands = rope.base.fscommands.FileSystemCommands()
|
|
|
|
def _get_fscommands(self, resource):
|
|
if self.project.is_ignored(resource):
|
|
return self.direct_commands
|
|
return self.fscommands
|
|
|
|
def write_file(self, resource, contents):
|
|
data = rope.base.fscommands.unicode_to_file_data(
|
|
contents,
|
|
newlines=resource.newlines,
|
|
)
|
|
fscommands = self._get_fscommands(resource)
|
|
fscommands.write(resource.real_path, data)
|
|
for observer in list(self.project.observers):
|
|
observer.resource_changed(resource)
|
|
|
|
def move(self, resource, new_resource):
|
|
fscommands = self._get_fscommands(resource)
|
|
fscommands.move(resource.real_path, new_resource.real_path)
|
|
for observer in list(self.project.observers):
|
|
observer.resource_moved(resource, new_resource)
|
|
|
|
def create(self, resource):
|
|
if resource.is_folder():
|
|
self._create_resource(resource.path, kind="folder")
|
|
else:
|
|
self._create_resource(resource.path)
|
|
for observer in list(self.project.observers):
|
|
observer.resource_created(resource)
|
|
|
|
def remove(self, resource):
|
|
fscommands = self._get_fscommands(resource)
|
|
fscommands.remove(resource.real_path)
|
|
for observer in list(self.project.observers):
|
|
observer.resource_removed(resource)
|
|
|
|
def _create_resource(self, file_name, kind="file"):
|
|
resource_path = self.project._get_resource_path(file_name)
|
|
if os.path.exists(resource_path):
|
|
raise exceptions.RopeError("Resource <%s> already exists" % resource_path)
|
|
resource = self.project.get_file(file_name)
|
|
if not resource.parent.exists():
|
|
raise exceptions.ResourceNotFoundError(
|
|
"Parent folder of <%s> does not exist" % resource.path
|
|
)
|
|
fscommands = self._get_fscommands(resource)
|
|
try:
|
|
if kind == "file":
|
|
fscommands.create_file(resource_path)
|
|
else:
|
|
fscommands.create_folder(resource_path)
|
|
except IOError as e:
|
|
raise exceptions.RopeError(e)
|
|
|
|
|
|
def _get_destination_for_move(resource, destination):
|
|
dest_path = resource.project._get_resource_path(destination)
|
|
if os.path.isdir(dest_path):
|
|
if destination != "":
|
|
return destination + "/" + resource.name
|
|
else:
|
|
return resource.name
|
|
return destination
|
|
|
|
|
|
class ChangeToData(object):
|
|
def convertChangeSet(self, change):
|
|
description = change.description
|
|
changes = []
|
|
for child in change.changes:
|
|
changes.append(self(child))
|
|
return (description, changes, change.time)
|
|
|
|
def convertChangeContents(self, change):
|
|
return (change.resource.path, change.new_contents, change.old_contents)
|
|
|
|
def convertMoveResource(self, change):
|
|
return (change.resource.path, change.new_resource.path)
|
|
|
|
def convertCreateResource(self, change):
|
|
return (change.resource.path, change.resource.is_folder())
|
|
|
|
def convertRemoveResource(self, change):
|
|
return (change.resource.path, change.resource.is_folder())
|
|
|
|
def __call__(self, change):
|
|
change_type = type(change)
|
|
if change_type in (CreateFolder, CreateFile):
|
|
change_type = CreateResource
|
|
method = getattr(self, "convert" + change_type.__name__)
|
|
return (change_type.__name__, method(change))
|
|
|
|
|
|
class DataToChange(object):
|
|
def __init__(self, project):
|
|
self.project = project
|
|
|
|
def makeChangeSet(self, description, changes, time=None):
|
|
result = ChangeSet(description, time)
|
|
for child in changes:
|
|
result.add_change(self(child))
|
|
return result
|
|
|
|
def makeChangeContents(self, path, new_contents, old_contents):
|
|
resource = self.project.get_file(path)
|
|
return ChangeContents(resource, new_contents, old_contents)
|
|
|
|
def makeMoveResource(self, old_path, new_path):
|
|
resource = self.project.get_file(old_path)
|
|
return MoveResource(resource, new_path, exact=True)
|
|
|
|
def makeCreateResource(self, path, is_folder):
|
|
if is_folder:
|
|
resource = self.project.get_folder(path)
|
|
else:
|
|
resource = self.project.get_file(path)
|
|
return CreateResource(resource)
|
|
|
|
def makeRemoveResource(self, path, is_folder):
|
|
if is_folder:
|
|
resource = self.project.get_folder(path)
|
|
else:
|
|
resource = self.project.get_file(path)
|
|
return RemoveResource(resource)
|
|
|
|
def __call__(self, data):
|
|
method = getattr(self, "make" + data[0])
|
|
return method(*data[1])
|