Blender SVN: New Features #273
@ -1,6 +1,7 @@
|
|||||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
# (c) 2022, Blender Foundation - Demeter Dzadik
|
# (c) 2022, Blender Foundation - Demeter Dzadik
|
||||||
|
|
||||||
|
import bpy
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
SVN_STATUS_DATA = OrderedDict(
|
SVN_STATUS_DATA = OrderedDict(
|
||||||
@ -94,7 +95,6 @@ SVN_STATUS_DATA = OrderedDict(
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Based on PySVN/svn/constants.py/STATUS_TYPE_LOOKUP.
|
# Based on PySVN/svn/constants.py/STATUS_TYPE_LOOKUP.
|
||||||
ENUM_SVN_STATUS = [
|
ENUM_SVN_STATUS = [
|
||||||
(status, status.title(),
|
(status, status.title(),
|
||||||
@ -119,3 +119,15 @@ SVN_STATUS_CHAR_TO_NAME = {
|
|||||||
|
|
||||||
SVN_STATUS_NAME_TO_CHAR = {value: key for key,
|
SVN_STATUS_NAME_TO_CHAR = {value: key for key,
|
||||||
value in SVN_STATUS_CHAR_TO_NAME.items()}
|
value in SVN_STATUS_CHAR_TO_NAME.items()}
|
||||||
|
|
||||||
|
DEPTH_ENUM = bpy.props.EnumProperty(
|
||||||
|
name="Depth",
|
||||||
|
description="The depth to which this directory should be updated", # For non-directory entries, this property is not used.
|
||||||
|
items=[
|
||||||
|
('INFINITY', 'Recursive (Default)', "Updates will recursively pull in any files or subdirectories not already present", 'OUTLINER', 3),
|
||||||
|
('IMMEDIATES', 'Non-Recursive', "Updates will pull in any files or subdirectories not already present; those subdirectories' will be left empty", 'PRESET', 2),
|
||||||
|
('FILES', 'Files Only', "Updates will pull in any files not already present, but not subdirectories", 'FILE', 1),
|
||||||
|
('EMPTY', 'Empty', "Updates will not pull in any files or subdirectories not already present", 'SELECT_SET', 0),
|
||||||
|
],
|
||||||
|
default='INFINITY'
|
||||||
|
)
|
||||||
|
@ -14,6 +14,7 @@ from send2trash import send2trash
|
|||||||
from ..threaded.execute_subprocess import execute_svn_command
|
from ..threaded.execute_subprocess import execute_svn_command
|
||||||
from ..threaded.background_process import Processes
|
from ..threaded.background_process import Processes
|
||||||
from ..util import get_addon_prefs, redraw_viewport
|
from ..util import get_addon_prefs, redraw_viewport
|
||||||
|
from ..constants import DEPTH_ENUM
|
||||||
|
|
||||||
|
|
||||||
class SVN_Operator:
|
class SVN_Operator:
|
||||||
@ -181,7 +182,7 @@ class SVN_OT_update_single(May_Modifiy_Current_Blend, Operator):
|
|||||||
class SVN_OT_download_file_revision(May_Modifiy_Current_Blend, Operator):
|
class SVN_OT_download_file_revision(May_Modifiy_Current_Blend, Operator):
|
||||||
bl_idname = "svn.download_file_revision"
|
bl_idname = "svn.download_file_revision"
|
||||||
bl_label = "Download Revision"
|
bl_label = "Download Revision"
|
||||||
bl_description = "Download this revision of this file"
|
bl_description = "Download this revision of this file. Automatic updates will be disabled and need to be re-enabled manually"
|
||||||
bl_options = {'INTERNAL'}
|
bl_options = {'INTERNAL'}
|
||||||
|
|
||||||
missing_file_allowed = True
|
missing_file_allowed = True
|
||||||
@ -211,6 +212,8 @@ class SVN_OT_download_file_revision(May_Modifiy_Current_Blend, Operator):
|
|||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
self.svn_download_file_revision(context, self.file_rel_path, self.revision)
|
self.svn_download_file_revision(context, self.file_rel_path, self.revision)
|
||||||
|
prefs = get_addon_prefs(context)
|
||||||
|
prefs.do_auto_updates = False
|
||||||
|
|
||||||
self.report(
|
self.report(
|
||||||
{'INFO'}, f"Checked out revision {self.revision} of {self.file_rel_path}"
|
{'INFO'}, f"Checked out revision {self.revision} of {self.file_rel_path}"
|
||||||
@ -456,6 +459,53 @@ class SVN_OT_resolve_conflict(May_Modifiy_Current_Blend, Operator):
|
|||||||
file_entry.status = 'normal'
|
file_entry.status = 'normal'
|
||||||
|
|
||||||
|
|
||||||
|
class SVN_OT_set_directory_depth(SVN_Operator_Single_File, Operator):
|
||||||
|
bl_idname = "svn.set_directory_depth"
|
||||||
|
bl_label = "Set Directory Depth"
|
||||||
|
bl_description = "Set update depth of this directory"
|
||||||
|
bl_options = {'INTERNAL'}
|
||||||
|
|
||||||
|
depth: DEPTH_ENUM
|
||||||
|
popup: BoolProperty(
|
||||||
|
name="Popup",
|
||||||
|
description="Whether the operator should use a pop-up prompt",
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def description(cls, context, properties):
|
||||||
|
desc = DEPTH_ENUM.keywords['description']
|
||||||
|
items = DEPTH_ENUM.keywords['items']
|
||||||
|
for identifier, name, item_desc, icon, num in items:
|
||||||
|
if identifier == properties.depth:
|
||||||
|
return desc + ":\n" + item_desc
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
if self.popup:
|
||||||
|
return context.window_manager.invoke_props_dialog(self)
|
||||||
|
return self.execute(context)
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.use_property_split=True
|
||||||
|
layout.use_property_decorate=False
|
||||||
|
layout.prop(self, 'depth')
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
file_entry = self.get_file(context)
|
||||||
|
|
||||||
|
if not file_entry.is_dir:
|
||||||
|
self.report({'ERROR'}, "Active file entry is not a directory")
|
||||||
|
return {'CANCELLED'}
|
||||||
|
|
||||||
|
file_entry.depth = self.depth
|
||||||
|
|
||||||
|
# Depth info needs to be saved to file to make sure it doesn't get lost.
|
||||||
|
get_addon_prefs(context).save_repo_info_to_file()
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class SVN_OT_cleanup(SVN_Operator, Operator):
|
class SVN_OT_cleanup(SVN_Operator, Operator):
|
||||||
bl_idname = "svn.cleanup"
|
bl_idname = "svn.cleanup"
|
||||||
bl_label = "SVN Cleanup"
|
bl_label = "SVN Cleanup"
|
||||||
@ -469,8 +519,12 @@ class SVN_OT_cleanup(SVN_Operator, Operator):
|
|||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
repo = context.scene.svn.get_repo(context)
|
repo = context.scene.svn.get_repo(context)
|
||||||
|
prefs = get_addon_prefs(context)
|
||||||
|
|
||||||
repo.external_files.clear()
|
repo.external_files.clear()
|
||||||
|
repo.external_files_active_index = -1
|
||||||
|
# Load folder depth data from file.
|
||||||
|
prefs.load_repo_info_from_file()
|
||||||
self.execute_svn_command(context, ["svn", "cleanup"])
|
self.execute_svn_command(context, ["svn", "cleanup"])
|
||||||
repo.reload_svn_log(context)
|
repo.reload_svn_log(context)
|
||||||
|
|
||||||
@ -488,6 +542,17 @@ class SVN_OT_cleanup(SVN_Operator, Operator):
|
|||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class SVN_OT_cancel_running_operation(Operator):
|
||||||
|
bl_idname = "svn.cancel"
|
||||||
|
bl_label = "SVN Cancel Operation"
|
||||||
|
bl_description = "Cancel ongoing commit/update operation"
|
||||||
|
bl_options = {'INTERNAL'}
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
Processes.kill('Commit')
|
||||||
|
Processes.kill('Update')
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
registry = [
|
registry = [
|
||||||
SVN_OT_update_single,
|
SVN_OT_update_single,
|
||||||
SVN_OT_revert_and_update,
|
SVN_OT_revert_and_update,
|
||||||
@ -499,5 +564,7 @@ registry = [
|
|||||||
SVN_OT_trash_file,
|
SVN_OT_trash_file,
|
||||||
SVN_OT_remove_file,
|
SVN_OT_remove_file,
|
||||||
SVN_OT_resolve_conflict,
|
SVN_OT_resolve_conflict,
|
||||||
|
SVN_OT_set_directory_depth,
|
||||||
SVN_OT_cleanup,
|
SVN_OT_cleanup,
|
||||||
|
SVN_OT_cancel_running_operation,
|
||||||
]
|
]
|
||||||
|
@ -15,7 +15,7 @@ from ..util import get_addon_prefs
|
|||||||
class SVN_OT_update_all(May_Modifiy_Current_Blend, Operator):
|
class SVN_OT_update_all(May_Modifiy_Current_Blend, Operator):
|
||||||
bl_idname = "svn.update_all"
|
bl_idname = "svn.update_all"
|
||||||
bl_label = "SVN Update All"
|
bl_label = "SVN Update All"
|
||||||
bl_description = "Download all the latest updates from the remote repository"
|
bl_description = "Update entire repository"
|
||||||
bl_options = {'INTERNAL'}
|
bl_options = {'INTERNAL'}
|
||||||
|
|
||||||
revision: IntProperty(
|
revision: IntProperty(
|
||||||
@ -24,6 +24,16 @@ class SVN_OT_update_all(May_Modifiy_Current_Blend, Operator):
|
|||||||
default=0
|
default=0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def description(cls, context, properties):
|
||||||
|
if properties.revision == 0:
|
||||||
|
return "Update entire repository to the latest version"
|
||||||
|
text = f"Revert entire repository to r{properties.revision}"
|
||||||
|
prefs = get_addon_prefs(context)
|
||||||
|
if prefs.do_auto_updates:
|
||||||
|
text += ". This will disable auto-updates, which can be re-enabled again manually"
|
||||||
|
return text
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
if get_addon_prefs(context).is_busy:
|
if get_addon_prefs(context).is_busy:
|
||||||
@ -72,6 +82,10 @@ class SVN_OT_update_all(May_Modifiy_Current_Blend, Operator):
|
|||||||
|
|
||||||
def execute(self, context: Context) -> Set[str]:
|
def execute(self, context: Context) -> Set[str]:
|
||||||
Processes.stop('Status')
|
Processes.stop('Status')
|
||||||
|
|
||||||
|
prefs = get_addon_prefs(context)
|
||||||
|
prefs.do_auto_updates = False
|
||||||
|
|
||||||
if self.reload_file:
|
if self.reload_file:
|
||||||
current_file = context.scene.svn.get_repo(context).current_blend_file
|
current_file = context.scene.svn.get_repo(context).current_blend_file
|
||||||
command = ["svn", "up", current_file.svn_path, "--accept", "postpone"]
|
command = ["svn", "up", current_file.svn_path, "--accept", "postpone"]
|
||||||
|
@ -26,6 +26,12 @@ class SVN_addon_preferences(AddonPreferences):
|
|||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
do_auto_updates: BoolProperty(
|
||||||
|
name="Auto-Update",
|
||||||
|
description="Automatically download updates as they appear on the remote",
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
repositories: CollectionProperty(type=SVN_repository)
|
repositories: CollectionProperty(type=SVN_repository)
|
||||||
|
|
||||||
def init_repo_list(self):
|
def init_repo_list(self):
|
||||||
@ -110,6 +116,9 @@ class SVN_addon_preferences(AddonPreferences):
|
|||||||
def is_busy(self):
|
def is_busy(self):
|
||||||
return Processes.is_running('Commit', 'Update')
|
return Processes.is_running('Commit', 'Update')
|
||||||
|
|
||||||
|
|
||||||
|
### Keeping a backup of repository data in blender_svn.txt. ###
|
||||||
|
### This is important so it doesn't get lost on add=on disable. ###
|
||||||
loading: BoolProperty(
|
loading: BoolProperty(
|
||||||
name="Loading",
|
name="Loading",
|
||||||
description="Disable the credential update callbacks while loading repo data to avoid infinite loops",
|
description="Disable the credential update callbacks while loading repo data to avoid infinite loops",
|
||||||
@ -120,11 +129,13 @@ class SVN_addon_preferences(AddonPreferences):
|
|||||||
saved_props = {'url', 'directory', 'name',
|
saved_props = {'url', 'directory', 'name',
|
||||||
'username', 'password', 'display_name'}
|
'username', 'password', 'display_name'}
|
||||||
repo_data = {}
|
repo_data = {}
|
||||||
for repo in self['repositories']:
|
for repo_dict, repo in zip(self['repositories'], self.repositories):
|
||||||
directory = repo.get('directory', '')
|
directory = repo_dict.get('directory', '')
|
||||||
|
|
||||||
repo_data[directory] = {
|
repo_data[directory] = {
|
||||||
key: value for key, value in repo.to_dict().items() if key in saved_props}
|
key: value for key, value in repo_dict.to_dict().items() if key in saved_props}
|
||||||
|
|
||||||
|
repo_data[directory]['custom_directory_depths'] = {dir_entry.svn_path: dir_entry.depth for dir_entry in repo.external_files if dir_entry.is_dir and dir_entry.depth != 'INFINITY'}
|
||||||
|
|
||||||
filepath = Path(bpy.utils.user_resource('CONFIG')) / \
|
filepath = Path(bpy.utils.user_resource('CONFIG')) / \
|
||||||
Path("blender_svn.txt")
|
Path("blender_svn.txt")
|
||||||
@ -148,7 +159,14 @@ class SVN_addon_preferences(AddonPreferences):
|
|||||||
repo = self.repositories.add()
|
repo = self.repositories.add()
|
||||||
repo.directory = directory
|
repo.directory = directory
|
||||||
for key, value in repo_data.items():
|
for key, value in repo_data.items():
|
||||||
setattr(repo, key, value)
|
setattr(repo, key, value)
|
||||||
|
|
||||||
|
if 'custom_directory_depths' in repo_data:
|
||||||
|
for dir_path, depth in repo_data['custom_directory_depths'].items():
|
||||||
|
dir_entry = repo.external_files.get(dir_path)
|
||||||
|
if not dir_entry:
|
||||||
|
dir_entry = repo.file_add(dir_path)
|
||||||
|
dir_entry.depth = depth
|
||||||
finally:
|
finally:
|
||||||
self.loading = False
|
self.loading = False
|
||||||
|
|
||||||
@ -166,7 +184,6 @@ class SVN_addon_preferences(AddonPreferences):
|
|||||||
else:
|
else:
|
||||||
draw_repo_list(self, context)
|
draw_repo_list(self, context)
|
||||||
|
|
||||||
|
|
||||||
def draw_prefs_no_svn(self, context):
|
def draw_prefs_no_svn(self, context):
|
||||||
terminal, url = "terminal", "https://subversion.apache.org/packages.html"
|
terminal, url = "terminal", "https://subversion.apache.org/packages.html"
|
||||||
system = platform.system()
|
system = platform.system()
|
||||||
|
@ -36,7 +36,9 @@ class SVN_file(PropertyGroup):
|
|||||||
|
|
||||||
@svn_path.setter
|
@svn_path.setter
|
||||||
def svn_path(self, value):
|
def svn_path(self, value):
|
||||||
self.name = value
|
path = Path(value)
|
||||||
|
self.name = str(path.as_posix())
|
||||||
|
self.tree_depth = len(path.parents)-1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def file_name(self):
|
def file_name(self):
|
||||||
@ -48,6 +50,13 @@ class SVN_file(PropertyGroup):
|
|||||||
options=set()
|
options=set()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
depth: constants.DEPTH_ENUM
|
||||||
|
@property
|
||||||
|
def depth_icon(self):
|
||||||
|
for identifier, name, item_desc, icon, num in constants.DEPTH_ENUM.keywords['items']:
|
||||||
|
if identifier == self.depth:
|
||||||
|
return icon
|
||||||
|
|
||||||
status: EnumProperty(
|
status: EnumProperty(
|
||||||
name="Status",
|
name="Status",
|
||||||
description="SVN Status of the file in the local repository (aka working copy)",
|
description="SVN Status of the file in the local repository (aka working copy)",
|
||||||
@ -76,22 +85,18 @@ class SVN_file(PropertyGroup):
|
|||||||
("SKIP_ONCE", "Skip Once", "File status is predicted by a working-copy svn file operation, like Revert. Next status update should be ignored, and this enum should be set to SKIPPED_ONCE."),
|
("SKIP_ONCE", "Skip Once", "File status is predicted by a working-copy svn file operation, like Revert. Next status update should be ignored, and this enum should be set to SKIPPED_ONCE."),
|
||||||
("SKIPPED_ONCE", "Skipped Once", "File status update was skipped. Next status update can be considered accurate, and this flag can be reset to NONE. Until then, operations on this file should remain disabled."),
|
("SKIPPED_ONCE", "Skipped Once", "File status update was skipped. Next status update can be considered accurate, and this flag can be reset to NONE. Until then, operations on this file should remain disabled."),
|
||||||
],
|
],
|
||||||
description="Internal flag that notes what process set a predicted status on this file. Should be empty string when the status is not predicted but confirmed. When svn commit/update predicts a status, that status should not be overwritten until the process is finished. With instantaneous processes, a single status update should be ignored since it may be outdated",
|
description="Internal flag that notes what process set a predicted status on this file. Used to prevent conflicting operations while one operation is in progress",
|
||||||
options=set()
|
options=set()
|
||||||
)
|
)
|
||||||
include_in_commit: BoolProperty(
|
include_in_commit: BoolProperty(
|
||||||
name="Commit",
|
name="Commit",
|
||||||
description="Whether this file should be included in the commit or not",
|
description="Whether this file should be included in the commit or not. Used by the commit operator UI",
|
||||||
default=False,
|
default=False,
|
||||||
options=set()
|
options=set()
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_outdated(self):
|
def is_dir(self) -> bool:
|
||||||
return self.repos_status == 'modified' and self.status == 'normal'
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_dir(self):
|
|
||||||
if self.exists:
|
if self.exists:
|
||||||
return Path(self.absolute_path).is_dir()
|
return Path(self.absolute_path).is_dir()
|
||||||
else:
|
else:
|
||||||
@ -152,7 +157,7 @@ class SVN_file(PropertyGroup):
|
|||||||
return 'QUESTION'
|
return 'QUESTION'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_default_status(self):
|
def has_default_status(self) -> bool:
|
||||||
return self.status == 'normal' and self.repos_status == 'none' and self.status_prediction_type == 'NONE'
|
return self.status == 'normal' and self.repos_status == 'none' and self.status_prediction_type == 'NONE'
|
||||||
|
|
||||||
show_in_filelist: BoolProperty(
|
show_in_filelist: BoolProperty(
|
||||||
@ -161,20 +166,35 @@ class SVN_file(PropertyGroup):
|
|||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_file_size(self):
|
### File size - Update a string representation one time when file_size_KiB is set. ###
|
||||||
|
### We want to avoid re-calculating that string on every re-draw for optimization. ###
|
||||||
|
def get_file_size_ui_str(self):
|
||||||
num = self.file_size_KiB
|
num = self.file_size_KiB
|
||||||
for unit in ("KiB", "MiB", "GiB", "TiB", "PiB", "EiB"):
|
for unit in ("KiB", "MiB", "GiB", "TiB", "PiB", "EiB"):
|
||||||
if num < 1024:
|
if num < 1024:
|
||||||
return f"{num:3.1f} {unit}"
|
num_digits = len(str(num).split(".")[0])
|
||||||
|
file_size = f"{num:0.{max(0, 3-num_digits)}f} {unit}"
|
||||||
|
if "." not in file_size:
|
||||||
|
file_size = file_size.replace(" ", " ")
|
||||||
|
return file_size
|
||||||
num /= 1024.0
|
num /= 1024.0
|
||||||
return f"{num:.1f} YiB"
|
|
||||||
|
return "Really Big"
|
||||||
|
|
||||||
def update_file_size(self, _context):
|
def update_file_size(self, _context):
|
||||||
self.file_size = self.get_file_size()
|
self.file_size = self.get_file_size_ui_str()
|
||||||
|
|
||||||
file_size_KiB: FloatProperty(description="One KibiByte (KiB) is 1024 bytes", update=update_file_size)
|
file_size_KiB: FloatProperty(description="One KibiByte (KiB) is 1024 bytes", update=update_file_size)
|
||||||
file_size: StringProperty(description="File size for displaying in the UI")
|
file_size: StringProperty(description="File size for displaying in the UI")
|
||||||
|
|
||||||
|
### File tree ###
|
||||||
|
def update_is_expanded(self, context):
|
||||||
|
repo = context.scene.svn.get_repo(context)
|
||||||
|
repo.external_files_active_index = repo.external_files.find(self.name)
|
||||||
|
repo.refresh_ui_lists(context)
|
||||||
|
is_expanded: BoolProperty(name="Show Contents", update=update_is_expanded, description="Whether this directory's contents should be shown in file tree view")
|
||||||
|
tree_depth: IntProperty(description="Number of indentations in the tree, ie. number of parents. Set automatically when svn_path is set")
|
||||||
|
has_children: BoolProperty(description="Whether this is a directory with any children. Updated whenever a new file entry is added")
|
||||||
|
|
||||||
class SVN_log(PropertyGroup):
|
class SVN_log(PropertyGroup):
|
||||||
"""Property Group that can represent an SVN log entry."""
|
"""Property Group that can represent an SVN log entry."""
|
||||||
@ -260,6 +280,7 @@ class SVN_repository(PropertyGroup):
|
|||||||
url: StringProperty(
|
url: StringProperty(
|
||||||
name="URL",
|
name="URL",
|
||||||
description="URL of the remote repository",
|
description="URL of the remote repository",
|
||||||
|
update=update_repo_info_file
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_directory(self, context):
|
def update_directory(self, context):
|
||||||
@ -287,6 +308,9 @@ class SVN_repository(PropertyGroup):
|
|||||||
dir_path = Path(self.directory)
|
dir_path = Path(self.directory)
|
||||||
# TODO: This property is checked pretty often, so we run `svn info` pretty often. Might not be a big deal, but maybe it's a bit overkill?
|
# TODO: This property is checked pretty often, so we run `svn info` pretty often. Might not be a big deal, but maybe it's a bit overkill?
|
||||||
root_dir, base_url = get_svn_info(self.directory)
|
root_dir, base_url = get_svn_info(self.directory)
|
||||||
|
if not hasattr(self, 'directory'):
|
||||||
|
# This can happen when running Reload Scripts, resulting in a console error.
|
||||||
|
return False
|
||||||
return (
|
return (
|
||||||
dir_path.exists() and
|
dir_path.exists() and
|
||||||
dir_path.is_dir() and
|
dir_path.is_dir() and
|
||||||
@ -425,6 +449,15 @@ class SVN_repository(PropertyGroup):
|
|||||||
### SVN File List. ###
|
### SVN File List. ###
|
||||||
external_files: CollectionProperty(type=SVN_file)
|
external_files: CollectionProperty(type=SVN_file)
|
||||||
|
|
||||||
|
def file_add(self, svn_path: Path or str) -> SVN_file:
|
||||||
|
file_entry = self.external_files.add()
|
||||||
|
file_entry.svn_path = svn_path
|
||||||
|
file_entry.absolute_path = str(self.svn_to_absolute_path(svn_path))
|
||||||
|
parent = self.get_parent_file(file_entry)
|
||||||
|
if parent:
|
||||||
|
parent.has_children = True
|
||||||
|
return file_entry
|
||||||
|
|
||||||
def remove_file_entry(self, file_entry: SVN_file):
|
def remove_file_entry(self, file_entry: SVN_file):
|
||||||
"""Remove a file entry from the file list, based on its filepath."""
|
"""Remove a file entry from the file list, based on its filepath."""
|
||||||
for i, f in enumerate(self.external_files):
|
for i, f in enumerate(self.external_files):
|
||||||
@ -450,7 +483,7 @@ class SVN_repository(PropertyGroup):
|
|||||||
return svn_dir / svn_path
|
return svn_dir / svn_path
|
||||||
|
|
||||||
def get_file_by_absolute_path(self, abs_path: str or Path) -> Optional[SVN_file]:
|
def get_file_by_absolute_path(self, abs_path: str or Path) -> Optional[SVN_file]:
|
||||||
rel_path = str(self.absolute_to_svn_path(abs_path))
|
rel_path = str(self.absolute_to_svn_path(abs_path).as_posix())
|
||||||
if rel_path:
|
if rel_path:
|
||||||
return self.external_files.get(rel_path)
|
return self.external_files.get(rel_path)
|
||||||
|
|
||||||
@ -503,6 +536,30 @@ class SVN_repository(PropertyGroup):
|
|||||||
for log_entry in self.log]
|
for log_entry in self.log]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_root_subdirs(self):
|
||||||
|
for f in self.external_files:
|
||||||
|
if not self.get_parent_file(f):
|
||||||
|
yield f
|
||||||
|
|
||||||
|
def find_dir_children(self, dir: SVN_file):
|
||||||
|
for f in self.external_files:
|
||||||
|
if self.get_parent_file(f) == dir:
|
||||||
|
yield f
|
||||||
|
|
||||||
|
def calculate_folder_sizes(self):
|
||||||
|
for root_subdir in self.get_root_subdirs():
|
||||||
|
self.calculate_folder_size_recursive(root_subdir)
|
||||||
|
|
||||||
|
def calculate_folder_size_recursive(self, dir: SVN_file):
|
||||||
|
size = 0
|
||||||
|
for child in self.find_dir_children(dir):
|
||||||
|
if child.is_dir:
|
||||||
|
self.calculate_folder_size_recursive(child)
|
||||||
|
size += child.file_size_KiB
|
||||||
|
|
||||||
|
dir.file_size_KiB = size
|
||||||
|
return size
|
||||||
|
|
||||||
prev_external_files_active_index: IntProperty(
|
prev_external_files_active_index: IntProperty(
|
||||||
name="Previous Active Index",
|
name="Previous Active Index",
|
||||||
description="Internal value to avoid triggering the update callback unnecessarily",
|
description="Internal value to avoid triggering the update callback unnecessarily",
|
||||||
@ -515,8 +572,11 @@ class SVN_repository(PropertyGroup):
|
|||||||
options=set(),
|
options=set(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_parent_file(self, file: SVN_file) -> Optional[SVN_file]:
|
||||||
|
return self.external_files.get(str(Path(file.svn_path).parent.as_posix()))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def active_file(self) -> SVN_file:
|
def active_file(self) -> Optional[SVN_file]:
|
||||||
if len(self.external_files) == 0:
|
if len(self.external_files) == 0:
|
||||||
return
|
return
|
||||||
return self.external_files[self.external_files_active_index]
|
return self.external_files[self.external_files_active_index]
|
||||||
@ -550,8 +610,9 @@ class SVN_repository(PropertyGroup):
|
|||||||
return svn_file
|
return svn_file
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_blend_file(self) -> SVN_file:
|
def current_blend_file(self) -> Optional[SVN_file]:
|
||||||
return self.get_file_by_absolute_path(bpy.data.filepath)
|
if bpy.data.filepath:
|
||||||
|
return self.get_file_by_absolute_path(bpy.data.filepath)
|
||||||
|
|
||||||
### File List UIList filter properties ###
|
### File List UIList filter properties ###
|
||||||
def refresh_ui_lists(self, context):
|
def refresh_ui_lists(self, context):
|
||||||
@ -559,7 +620,11 @@ class SVN_repository(PropertyGroup):
|
|||||||
Also triggers a refresh of the SVN UIList, through the update callback of
|
Also triggers a refresh of the SVN UIList, through the update callback of
|
||||||
external_files_active_index."""
|
external_files_active_index."""
|
||||||
|
|
||||||
|
if len(self.external_files) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
UI_LIST = bpy.types.UI_UL_list
|
UI_LIST = bpy.types.UI_UL_list
|
||||||
|
|
||||||
if self.file_search_filter:
|
if self.file_search_filter:
|
||||||
filter_list = UI_LIST.filter_items_by_name(
|
filter_list = UI_LIST.filter_items_by_name(
|
||||||
self.file_search_filter,
|
self.file_search_filter,
|
||||||
@ -570,16 +635,29 @@ class SVN_repository(PropertyGroup):
|
|||||||
)
|
)
|
||||||
filter_list = [bool(val) for val in filter_list]
|
filter_list = [bool(val) for val in filter_list]
|
||||||
self.external_files.foreach_set('show_in_filelist', filter_list)
|
self.external_files.foreach_set('show_in_filelist', filter_list)
|
||||||
|
if self.display_mode == 'TREE':
|
||||||
|
for file in self.external_files:
|
||||||
|
if file.show_in_filelist:
|
||||||
|
parent = self.get_parent_file(file)
|
||||||
|
while parent:
|
||||||
|
parent.show_in_filelist = True
|
||||||
|
parent = self.get_parent_file(parent)
|
||||||
else:
|
else:
|
||||||
|
self.external_files.foreach_set('show_in_filelist', [True] * len(self.external_files))
|
||||||
|
|
||||||
|
if self.display_mode == 'TREE':
|
||||||
|
for file in self.external_files:
|
||||||
|
parent = self.get_parent_file(file)
|
||||||
|
while parent:
|
||||||
|
if not parent.is_expanded:
|
||||||
|
file.show_in_filelist = False
|
||||||
|
break
|
||||||
|
parent = self.get_parent_file(parent)
|
||||||
|
elif not self.file_search_filter:
|
||||||
for file in self.external_files:
|
for file in self.external_files:
|
||||||
if file == self.current_blend_file:
|
|
||||||
file.show_in_filelist = True
|
|
||||||
continue
|
|
||||||
|
|
||||||
file.show_in_filelist = not file.has_default_status
|
file.show_in_filelist = not file.has_default_status
|
||||||
|
if self.current_blend_file:
|
||||||
if len(self.external_files) == 0:
|
self.current_blend_file.show_in_filelist = True
|
||||||
return
|
|
||||||
|
|
||||||
# Make sure the active file isn't now being filtered out.
|
# Make sure the active file isn't now being filtered out.
|
||||||
# If it is, change the active file to the first visible one.
|
# If it is, change the active file to the first visible one.
|
||||||
@ -596,6 +674,31 @@ class SVN_repository(PropertyGroup):
|
|||||||
update=refresh_ui_lists
|
update=refresh_ui_lists
|
||||||
)
|
)
|
||||||
|
|
||||||
|
show_file_size: BoolProperty(
|
||||||
|
name="Show Size On Disk",
|
||||||
|
description="Show size of each file and aggregate size of folders",
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
show_file_paths: BoolProperty(
|
||||||
|
name="Show File Paths",
|
||||||
|
description="Show file paths relative to the SVN root, instead of just the file name"
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_tree_display(self, context):
|
||||||
|
if self.display_mode == 'TREE':
|
||||||
|
self.show_file_paths = False
|
||||||
|
self.refresh_ui_lists(context)
|
||||||
|
|
||||||
|
display_mode: EnumProperty(
|
||||||
|
name="File Display Mode",
|
||||||
|
description="Whether the full file tree sould be drawn instead of just modified files as a flat list",
|
||||||
|
items=[
|
||||||
|
('FLAT', "Changes", "Display only modified files as a flat list", 'PRESET', 0),
|
||||||
|
('TREE', "File Tree", "Display the full tree of the entire repository", 'OUTLINER', 1),
|
||||||
|
],
|
||||||
|
update=update_tree_display
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
registry = [
|
registry = [
|
||||||
SVN_file,
|
SVN_file,
|
||||||
|
@ -38,6 +38,9 @@ def execute_svn_command(
|
|||||||
So any file paths that are part of the command should be relative to the
|
So any file paths that are part of the command should be relative to the
|
||||||
SVN root.
|
SVN root.
|
||||||
"""
|
"""
|
||||||
|
if not hasattr(context.scene, 'svn'):
|
||||||
|
# This can happen during Reload Scripts, throwing a console error.
|
||||||
|
return
|
||||||
repo = context.scene.svn.get_repo(context)
|
repo = context.scene.svn.get_repo(context)
|
||||||
if "svn" not in command:
|
if "svn" not in command:
|
||||||
command.insert(0, "svn")
|
command.insert(0, "svn")
|
||||||
|
@ -74,7 +74,7 @@ def reload_svn_log(self, context):
|
|||||||
|
|
||||||
file_path = Path(file_path)
|
file_path = Path(file_path)
|
||||||
log_file_entry = log_entry.changed_files.add()
|
log_file_entry = log_entry.changed_files.add()
|
||||||
log_file_entry.svn_path = str(file_path.as_posix())
|
log_file_entry.svn_path = file_path
|
||||||
log_file_entry.revision = r_number
|
log_file_entry.revision = r_number
|
||||||
log_file_entry.status = constants.SVN_STATUS_CHAR_TO_NAME[status_char]
|
log_file_entry.status = constants.SVN_STATUS_CHAR_TO_NAME[status_char]
|
||||||
|
|
||||||
|
@ -137,7 +137,6 @@ class BGP_SVN_Status(BackgroundProcess):
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.timestamp_last_update = 0
|
self.timestamp_last_update = 0
|
||||||
self.list_command_output = ""
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def acquire_output(self, context, prefs):
|
def acquire_output(self, context, prefs):
|
||||||
@ -146,25 +145,15 @@ class BGP_SVN_Status(BackgroundProcess):
|
|||||||
["svn", "status", "--show-updates", "--verbose", "--xml"],
|
["svn", "status", "--show-updates", "--verbose", "--xml"],
|
||||||
use_cred=True,
|
use_cred=True,
|
||||||
)
|
)
|
||||||
# The list command includes file size info and also files of directories
|
|
||||||
# which have their Depth set to Empty, which is used for a partial check-out,
|
|
||||||
# which we also use for updating files and folders one-by-one instead of
|
|
||||||
# all-at-once, so we can provide more live feedback in the UI.
|
|
||||||
# NOTE: This one-by-one updating functionality conflicts with a potential
|
|
||||||
# future support for partial check-outs, so that would require storing user-intended
|
|
||||||
# partially checked out folders separately somewhere.
|
|
||||||
self.list_command_output = execute_svn_command(
|
|
||||||
context,
|
|
||||||
["svn", "list", "--recursive", "--xml"],
|
|
||||||
use_cred=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def process_output(self, context, prefs):
|
def process_output(self, context, prefs):
|
||||||
repo = context.scene.svn.get_repo(context)
|
repo = context.scene.svn.get_repo(context)
|
||||||
update_file_list_svn_status(context, svn_status_xml_to_dict(self.output))
|
update_file_list_svn_status(context, svn_status_xml_to_dict(self.output))
|
||||||
update_file_list_svn_list(context, self.list_command_output)
|
|
||||||
repo.refresh_ui_lists(context)
|
repo.refresh_ui_lists(context)
|
||||||
self.timestamp_last_update = time.time()
|
self.timestamp_last_update = time.time()
|
||||||
|
if prefs.do_auto_updates:
|
||||||
|
if any([f.repos_status != 'none' for f in repo.external_files]):
|
||||||
|
Processes.start('Update')
|
||||||
|
|
||||||
def get_ui_message(self, context):
|
def get_ui_message(self, context):
|
||||||
time_since_last_update = time.time() - self.timestamp_last_update
|
time_since_last_update = time.time() - self.timestamp_last_update
|
||||||
@ -175,6 +164,34 @@ class BGP_SVN_Status(BackgroundProcess):
|
|||||||
return f"Updating repo status..."
|
return f"Updating repo status..."
|
||||||
|
|
||||||
|
|
||||||
|
class BGP_SVN_List(BackgroundProcess):
|
||||||
|
"""The `svn list` command includes file size info and files excluded via
|
||||||
|
partial checkout functionality (`--set-depth empty`).
|
||||||
|
However, it is very slow, so this should be used sparingly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "List"
|
||||||
|
needs_authentication = True
|
||||||
|
timeout = 30
|
||||||
|
repeat_delay = 0
|
||||||
|
debug = False
|
||||||
|
|
||||||
|
def acquire_output(self, context, prefs):
|
||||||
|
self.output = execute_svn_command(
|
||||||
|
context,
|
||||||
|
["svn", "list", "--recursive", "--xml", "-r", "HEAD"],
|
||||||
|
use_cred=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def process_output(self, context, prefs):
|
||||||
|
repo = context.scene.svn.get_repo(context)
|
||||||
|
update_file_list_svn_list(context, self.output)
|
||||||
|
repo.refresh_ui_lists(context)
|
||||||
|
|
||||||
|
def get_ui_message(self, context):
|
||||||
|
return f"Updating file list..."
|
||||||
|
|
||||||
|
|
||||||
class BGP_SVN_Authenticate(BGP_SVN_Status):
|
class BGP_SVN_Authenticate(BGP_SVN_Status):
|
||||||
name = "Authenticate"
|
name = "Authenticate"
|
||||||
needs_authentication = False
|
needs_authentication = False
|
||||||
@ -225,9 +242,13 @@ def update_file_list_svn_status(context, file_statuses: Dict[str, Tuple[str, str
|
|||||||
|
|
||||||
svn_paths = []
|
svn_paths = []
|
||||||
new_files_on_repo = set()
|
new_files_on_repo = set()
|
||||||
for filepath_str, status_info in file_statuses.items():
|
file_statuses = [(key, value) for key, value in file_statuses.items()]
|
||||||
|
file_statuses.sort(key=lambda x: x[0])
|
||||||
|
for filepath_str, status_info in file_statuses:
|
||||||
|
if filepath_str == ".":
|
||||||
|
continue
|
||||||
svn_path = Path(filepath_str)
|
svn_path = Path(filepath_str)
|
||||||
svn_path_str = str(filepath_str)
|
svn_path_str = str(svn_path.as_posix())
|
||||||
suffix = svn_path.suffix
|
suffix = svn_path.suffix
|
||||||
if (
|
if (
|
||||||
(suffix.startswith(".r") and suffix[2:].isdecimal())
|
(suffix.startswith(".r") and suffix[2:].isdecimal())
|
||||||
@ -248,8 +269,7 @@ def update_file_list_svn_status(context, file_statuses: Dict[str, Tuple[str, str
|
|||||||
entry_existed = True
|
entry_existed = True
|
||||||
if not file_entry:
|
if not file_entry:
|
||||||
entry_existed = False
|
entry_existed = False
|
||||||
file_entry = repo.external_files.add()
|
file_entry = repo.file_add(svn_path_str)
|
||||||
file_entry.svn_path = svn_path_str
|
|
||||||
|
|
||||||
if not file_entry.exists:
|
if not file_entry.exists:
|
||||||
new_files_on_repo.add((file_entry.svn_path, repos_status))
|
new_files_on_repo.add((file_entry.svn_path, repos_status))
|
||||||
@ -263,7 +283,6 @@ def update_file_list_svn_status(context, file_statuses: Dict[str, Tuple[str, str
|
|||||||
file_entry.status = wc_status
|
file_entry.status = wc_status
|
||||||
file_entry.repos_status = repos_status
|
file_entry.repos_status = repos_status
|
||||||
file_entry.status_prediction_type = 'NONE'
|
file_entry.status_prediction_type = 'NONE'
|
||||||
file_entry.absolute_path = str(repo.svn_to_absolute_path(svn_path))
|
|
||||||
|
|
||||||
if new_files_on_repo:
|
if new_files_on_repo:
|
||||||
# File entry status has changed between local and repo.
|
# File entry status has changed between local and repo.
|
||||||
@ -278,6 +297,9 @@ def update_file_list_svn_status(context, file_statuses: Dict[str, Tuple[str, str
|
|||||||
)
|
)
|
||||||
Processes.start('Log')
|
Processes.start('Log')
|
||||||
|
|
||||||
|
if any([not f.is_dir and not f.file_size_KiB for f in repo.external_files]):
|
||||||
|
Processes.start('List')
|
||||||
|
|
||||||
# Remove file entries who no longer seem to have an SVN status.
|
# Remove file entries who no longer seem to have an SVN status.
|
||||||
# This can happen if an unversioned file was removed from the filesystem,
|
# This can happen if an unversioned file was removed from the filesystem,
|
||||||
# Or sub-folders whose parent was Un-Added to the SVN.
|
# Or sub-folders whose parent was Un-Added to the SVN.
|
||||||
@ -324,23 +346,27 @@ def svn_status_xml_to_dict(svn_status_str: str) -> Dict[str, Tuple[str, str, int
|
|||||||
|
|
||||||
|
|
||||||
def update_file_list_svn_list(context, svn_list_str: str) -> Dict:
|
def update_file_list_svn_list(context, svn_list_str: str) -> Dict:
|
||||||
repo = context.scene.svn.get_repo(context)
|
if not svn_list_str:
|
||||||
try:
|
# This shouldn't happen.
|
||||||
svn_list_xml = xmltodict.parse(svn_list_str)
|
|
||||||
except:
|
|
||||||
# This seems to fail with an "ExpatError" on Windows...?
|
|
||||||
return
|
return
|
||||||
|
repo = context.scene.svn.get_repo(context)
|
||||||
|
svn_list_xml = xmltodict.parse(svn_list_str)
|
||||||
|
|
||||||
file_infos = svn_list_xml['lists']['list']['entry']
|
file_infos = svn_list_xml['lists']['list']['entry']
|
||||||
|
|
||||||
for file_info in file_infos:
|
for file_info in file_infos:
|
||||||
svn_path = file_info['name']
|
svn_path = str(Path(file_info['name']).as_posix())
|
||||||
|
if svn_path == ".":
|
||||||
|
continue
|
||||||
kind = file_info['@kind']
|
kind = file_info['@kind']
|
||||||
file_entry = repo.external_files.get(svn_path)
|
file_entry = repo.external_files.get(svn_path)
|
||||||
if not file_entry:
|
if not file_entry:
|
||||||
file_entry = repo.external_files.add()
|
# `svn list` will only catch new file entries which are missing due to
|
||||||
|
# a sparse checkout.
|
||||||
|
# TODO: The ability to include files that are excluded by a sparse checkout may be useful still.
|
||||||
|
continue
|
||||||
|
file_entry = repo.file_add(svn_path)
|
||||||
file_entry.svn_path = svn_path
|
file_entry.svn_path = svn_path
|
||||||
file_entry.absolute_path = str(repo.svn_to_absolute_path(svn_path))
|
|
||||||
if not file_entry.exists:
|
if not file_entry.exists:
|
||||||
file_entry.status = 'none'
|
file_entry.status = 'none'
|
||||||
file_entry.repos_status = 'added'
|
file_entry.repos_status = 'added'
|
||||||
@ -348,6 +374,9 @@ def update_file_list_svn_list(context, svn_list_str: str) -> Dict:
|
|||||||
if kind == 'file':
|
if kind == 'file':
|
||||||
file_entry.file_size_KiB = float(file_info['size']) / 1024.0
|
file_entry.file_size_KiB = float(file_info['size']) / 1024.0
|
||||||
|
|
||||||
|
# Calculate size of folders.
|
||||||
|
repo.calculate_folder_sizes()
|
||||||
|
|
||||||
|
|
||||||
@bpy.app.handlers.persistent
|
@bpy.app.handlers.persistent
|
||||||
def mark_current_file_as_modified(_dummy1=None, _dummy2=None):
|
def mark_current_file_as_modified(_dummy1=None, _dummy2=None):
|
||||||
|
@ -64,7 +64,7 @@ class BGP_SVN_Update(BackgroundProcess):
|
|||||||
|
|
||||||
print(self.message)
|
print(self.message)
|
||||||
self.set_predicted_file_status(file)
|
self.set_predicted_file_status(file)
|
||||||
command = ["svn", "up", file.svn_path, "--accept", "postpone", "--depth", "empty"]
|
command = ["svn", "up", file.svn_path, "--accept", "postpone"]#, "--depth", "empty"]
|
||||||
if self.revision > 0:
|
if self.revision > 0:
|
||||||
command.insert(2, f"-r{self.revision}")
|
command.insert(2, f"-r{self.revision}")
|
||||||
self.output = execute_svn_command(context, command, use_cred=True)
|
self.output = execute_svn_command(context, command, use_cred=True)
|
||||||
|
@ -24,23 +24,29 @@ def svn_file_list_context_menu(self: UIList, context: Context) -> None:
|
|||||||
repo = context.scene.svn.get_repo(context)
|
repo = context.scene.svn.get_repo(context)
|
||||||
active_file = repo.active_file
|
active_file = repo.active_file
|
||||||
file_abs_path = repo.get_file_abspath(active_file)
|
file_abs_path = repo.get_file_abspath(active_file)
|
||||||
|
|
||||||
|
layout.operator("wm.path_open",
|
||||||
|
text=f"Open Containing Folder", icon='FILE_FOLDER').filepath = Path(file_abs_path).parent.as_posix()
|
||||||
|
|
||||||
if active_file.name.endswith("blend"):
|
if active_file.name.endswith("blend"):
|
||||||
op = layout.operator("wm.open_mainfile",
|
op = layout.operator("wm.open_mainfile",
|
||||||
text=f"Open {active_file.name}")
|
text=f"Open {active_file.name}", icon='FILE_BLEND')
|
||||||
op.filepath = str(file_abs_path)
|
op.filepath = str(file_abs_path)
|
||||||
op.display_file_selector = False
|
op.display_file_selector = False
|
||||||
op.load_ui = True
|
op.load_ui = True
|
||||||
op = layout.operator("wm.open_mainfile",
|
op = layout.operator("wm.open_mainfile",
|
||||||
text=f"Open {active_file.name} (Keep UI)")
|
text=f"Open {active_file.name} (Keep UI)", icon='FILE_BLEND')
|
||||||
op.filepath = str(file_abs_path)
|
op.filepath = str(file_abs_path)
|
||||||
op.display_file_selector = False
|
op.display_file_selector = False
|
||||||
op.load_ui = False
|
op.load_ui = False
|
||||||
|
|
||||||
else:
|
else:
|
||||||
layout.operator("wm.path_open",
|
layout.operator("wm.path_open",
|
||||||
text=f"Open {active_file.name}").filepath = str(file_abs_path)
|
text=f"Open {active_file.name}", icon=active_file.file_icon).filepath = str(file_abs_path)
|
||||||
layout.operator("wm.path_open",
|
|
||||||
text=f"Open Containing Folder").filepath = Path(file_abs_path).parent.as_posix()
|
if active_file.is_dir:
|
||||||
|
layout.operator_menu_enum('svn.set_directory_depth', 'depth', text="Set SVN Depth", icon='OUTLINER')
|
||||||
|
|
||||||
layout.separator()
|
layout.separator()
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,25 +5,36 @@ import time
|
|||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
from bpy.types import UIList
|
from bpy.types import UIList
|
||||||
from bpy.props import BoolProperty
|
|
||||||
|
|
||||||
from .. import constants
|
from .. import constants
|
||||||
from ..util import get_addon_prefs, dots
|
from ..util import get_addon_prefs, dots
|
||||||
from ..threaded.background_process import Processes
|
from ..threaded.background_process import Processes
|
||||||
|
|
||||||
|
def file_list_ui_split(layout, context):
|
||||||
|
repo = context.scene.svn.get_repo(context)
|
||||||
|
main_split = layout.split(factor=0.9)
|
||||||
|
left_side = main_split.row()
|
||||||
|
|
||||||
|
filepath_ui = left_side.row()
|
||||||
|
if repo.show_file_size:
|
||||||
|
filesize_ui = layout.row()
|
||||||
|
filesize_ui.alignment='RIGHT'
|
||||||
|
else:
|
||||||
|
filesize_ui = None
|
||||||
|
status_ui = layout.row(align=True)
|
||||||
|
status_ui.alignment='RIGHT'
|
||||||
|
ops_ui = layout.row(align=True)
|
||||||
|
|
||||||
|
return filepath_ui, filesize_ui, status_ui, ops_ui
|
||||||
|
|
||||||
|
|
||||||
class SVN_UL_file_list(UIList):
|
class SVN_UL_file_list(UIList):
|
||||||
# Value that indicates that this item has passed the filter process successfully. See rna_ui.c.
|
# Value that indicates that this item has passed the filter process successfully. See rna_ui.c.
|
||||||
UILST_FLT_ITEM = 1 << 30
|
UILST_FLT_ITEM = 1 << 30
|
||||||
|
|
||||||
show_file_paths: BoolProperty(
|
|
||||||
name="Show File Paths",
|
|
||||||
description="Show file paths relative to the SVN root, instead of just the file name"
|
|
||||||
)
|
|
||||||
|
|
||||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||||
# As long as there are any items, always draw the filters.
|
# As long as there are any items, and a search term is typed in, always draw the filter.
|
||||||
self.use_filter_show = True
|
repo = context.scene.svn.get_repo(context)
|
||||||
|
|
||||||
if self.layout_type != 'DEFAULT':
|
if self.layout_type != 'DEFAULT':
|
||||||
raise NotImplemented
|
raise NotImplemented
|
||||||
@ -31,23 +42,49 @@ class SVN_UL_file_list(UIList):
|
|||||||
file_entry = item
|
file_entry = item
|
||||||
prefs = get_addon_prefs(context)
|
prefs = get_addon_prefs(context)
|
||||||
|
|
||||||
main_row = layout.row()
|
filepath_ui, filesize_ui, status_ui, ops_ui = file_list_ui_split(layout, context)
|
||||||
split = main_row.split(factor=0.6)
|
|
||||||
filepath_ui = split.row()
|
|
||||||
split = split.split(factor=0.4)
|
|
||||||
status_ui = split.row(align=True)
|
|
||||||
|
|
||||||
ops_ui = split.row(align=True)
|
|
||||||
ops_ui.alignment = 'RIGHT'
|
|
||||||
|
|
||||||
ops_ui.enabled = file_entry.status_prediction_type == 'NONE' and not prefs.is_busy
|
ops_ui.enabled = file_entry.status_prediction_type == 'NONE' and not prefs.is_busy
|
||||||
|
|
||||||
if self.show_file_paths:
|
if repo.display_mode == 'TREE':
|
||||||
filepath_ui.prop(file_entry, 'name', text="",
|
split = filepath_ui.split(factor=0.05 * file_entry.tree_depth + 0.00001)
|
||||||
emboss=False, icon=file_entry.file_icon)
|
split.label()
|
||||||
|
row = split.row(align=True)
|
||||||
|
filepath_ui = row.row(align=True)
|
||||||
|
if file_entry.has_children:
|
||||||
|
icon = 'DOWNARROW_HLT' if file_entry.is_expanded else 'RIGHTARROW'
|
||||||
|
filepath_ui.prop(file_entry, 'is_expanded', text="", icon=icon, emboss=False)
|
||||||
|
else:
|
||||||
|
filepath_ui.label(text="", icon='BLANK1')
|
||||||
|
|
||||||
|
if repo.show_file_paths:
|
||||||
|
filepath_ui.label(text=file_entry.name, icon=file_entry.file_icon)
|
||||||
else:
|
else:
|
||||||
filepath_ui.label(text=file_entry.file_name, icon=file_entry.file_icon)
|
filepath_ui.label(text=file_entry.file_name, icon=file_entry.file_icon)
|
||||||
|
|
||||||
|
if repo.show_file_size:
|
||||||
|
icon_map = {
|
||||||
|
'KiB' : 'DECORATE',
|
||||||
|
'MiB' : 'RADIOBUT_ON',
|
||||||
|
'GiB' : 'SHADING_SOLID',
|
||||||
|
'TiB' : 'SHADING_SOLID',
|
||||||
|
'PiB' : 'SHADING_SOLID',
|
||||||
|
'EiB' : 'SHADING_SOLID',
|
||||||
|
}
|
||||||
|
icon = 'BLANK1 '
|
||||||
|
if file_entry.file_size_KiB == 0:
|
||||||
|
icon = 'BLANK1'
|
||||||
|
text=" "
|
||||||
|
else:
|
||||||
|
icon = icon_map[file_entry.file_size[-3:]]
|
||||||
|
text=file_entry.file_size
|
||||||
|
width = get_sidebar_width(context)
|
||||||
|
if width < 800:
|
||||||
|
text = text.replace("iB", "")
|
||||||
|
filesize_ui.label(text=text)
|
||||||
|
else:
|
||||||
|
filesize_ui.label(text=text, icon=icon)
|
||||||
|
|
||||||
statuses = [file_entry.status]
|
statuses = [file_entry.status]
|
||||||
# SVN operations
|
# SVN operations
|
||||||
ops = []
|
ops = []
|
||||||
@ -61,7 +98,7 @@ class SVN_UL_file_list(UIList):
|
|||||||
ops.append(ops_ui.operator(
|
ops.append(ops_ui.operator(
|
||||||
'svn.unadd_file', text="", icon='REMOVE'))
|
'svn.unadd_file', text="", icon='REMOVE'))
|
||||||
elif file_entry.status == 'unversioned':
|
elif file_entry.status == 'unversioned':
|
||||||
ops.append(ops_ui.operator('svn.add_file', text="", icon='ADD'))
|
ops.append(ops_ui.operator('svn.file_add', text="", icon='ADD'))
|
||||||
ops.append(ops_ui.operator(
|
ops.append(ops_ui.operator(
|
||||||
'svn.trash_file', text="", icon='TRASH'))
|
'svn.trash_file', text="", icon='TRASH'))
|
||||||
|
|
||||||
@ -74,8 +111,9 @@ class SVN_UL_file_list(UIList):
|
|||||||
# I think it's better to let the user know in advance.
|
# I think it's better to let the user know in advance.
|
||||||
statuses.append('conflicted')
|
statuses.append('conflicted')
|
||||||
# Updating the file will create an actual conflict.
|
# Updating the file will create an actual conflict.
|
||||||
ops.append(ops_ui.operator(
|
if not prefs.do_auto_updates:
|
||||||
'svn.update_single', text="", icon='IMPORT'))
|
ops.append(ops_ui.operator(
|
||||||
|
'svn.update_single', text="", icon='IMPORT'))
|
||||||
|
|
||||||
elif file_entry.status == 'conflicted':
|
elif file_entry.status == 'conflicted':
|
||||||
ops.append(ops_ui.operator('svn.resolve_conflict',
|
ops.append(ops_ui.operator('svn.resolve_conflict',
|
||||||
@ -88,23 +126,36 @@ class SVN_UL_file_list(UIList):
|
|||||||
# From user POV it makes a bit more sense to call a file that doesn't
|
# From user POV it makes a bit more sense to call a file that doesn't
|
||||||
# exist yet "added" instead of "outdated".
|
# exist yet "added" instead of "outdated".
|
||||||
statuses.append('added')
|
statuses.append('added')
|
||||||
ops.append(ops_ui.operator(
|
if not prefs.do_auto_updates:
|
||||||
'svn.update_single', text="", icon='IMPORT'))
|
ops.append(ops_ui.operator(
|
||||||
|
'svn.update_single', text="", icon='IMPORT'))
|
||||||
elif file_entry.status == 'normal' and file_entry.repos_status == 'modified':
|
elif file_entry.status == 'normal' and file_entry.repos_status == 'modified':
|
||||||
# From user POV, this file is outdated, not 'normal'.
|
# From user POV, this file is outdated, not 'normal'.
|
||||||
statuses = ['none']
|
statuses = ['none']
|
||||||
ops.append(ops_ui.operator(
|
if not prefs.do_auto_updates:
|
||||||
'svn.update_single', text="", icon='IMPORT'))
|
ops.append(ops_ui.operator(
|
||||||
|
'svn.update_single', text="", icon='IMPORT'))
|
||||||
elif file_entry.status in ['normal', 'external', 'ignored']:
|
elif file_entry.status in ['normal', 'external', 'ignored']:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
print("Unknown file status: ", file_entry.svn_path,
|
print("Unknown file status: ", file_entry.svn_path,
|
||||||
file_entry.status, file_entry.repos_status)
|
file_entry.status, file_entry.repos_status)
|
||||||
|
|
||||||
|
if file_entry.is_dir:
|
||||||
|
op = ops_ui.operator('svn.set_directory_depth', text="", icon=file_entry.depth_icon)
|
||||||
|
op.popup=True
|
||||||
|
ops.append(op)
|
||||||
|
|
||||||
|
for i in range(max(0, 2-len(ops))):
|
||||||
|
ops_ui.label(text="", icon='BLANK1')
|
||||||
|
|
||||||
for op in ops:
|
for op in ops:
|
||||||
if hasattr(op, 'file_rel_path'):
|
if hasattr(op, 'file_rel_path'):
|
||||||
op.file_rel_path = file_entry.svn_path
|
op.file_rel_path = file_entry.svn_path
|
||||||
|
|
||||||
|
for i in range(max(0, 2-len(statuses))):
|
||||||
|
status_ui.label(text="", icon='BLANK1')
|
||||||
|
|
||||||
# Populate the status icons.
|
# Populate the status icons.
|
||||||
for status in statuses:
|
for status in statuses:
|
||||||
icon = constants.SVN_STATUS_DATA[status][0]
|
icon = constants.SVN_STATUS_DATA[status][0]
|
||||||
@ -136,30 +187,27 @@ class SVN_UL_file_list(UIList):
|
|||||||
return flt_flags, flt_neworder
|
return flt_flags, flt_neworder
|
||||||
|
|
||||||
def filter_items(self, context, data, propname):
|
def filter_items(self, context, data, propname):
|
||||||
|
# As long as a search term is typed in, draw the filter.
|
||||||
|
repo = context.scene.svn.get_repo(context)
|
||||||
|
if repo.file_search_filter:
|
||||||
|
self.use_filter_show = True
|
||||||
return type(self).cls_filter_items(context, data, propname)
|
return type(self).cls_filter_items(context, data, propname)
|
||||||
|
|
||||||
def draw_filter(self, context, layout):
|
def draw_filter(self, context, layout):
|
||||||
"""Custom filtering UI.
|
"""Custom filtering UI.
|
||||||
Toggles are stored in addon preferences, see cls_filter_items().
|
Toggles are stored in addon preferences, see cls_filter_items().
|
||||||
"""
|
"""
|
||||||
main_row = layout.row()
|
|
||||||
row = main_row.row(align=True)
|
|
||||||
|
|
||||||
row.prop(self, 'show_file_paths', text="",
|
|
||||||
toggle=True, icon="FILE_FOLDER")
|
|
||||||
|
|
||||||
repo = context.scene.svn.get_repo(context)
|
repo = context.scene.svn.get_repo(context)
|
||||||
if repo:
|
if not repo:
|
||||||
row.prop(repo, 'file_search_filter', text="")
|
return
|
||||||
|
layout.prop(repo, 'file_search_filter', text="")
|
||||||
|
|
||||||
|
|
||||||
def draw_process_info(context, layout):
|
def draw_process_info(context, layout):
|
||||||
prefs = get_addon_prefs(context)
|
prefs = get_addon_prefs(context)
|
||||||
process_message = ""
|
|
||||||
any_error = False
|
|
||||||
col = layout.column()
|
col = layout.column()
|
||||||
for process in Processes.processes.values():
|
for process in Processes.processes.values():
|
||||||
if process.name not in {'Commit', 'Update', 'Log', 'Status', 'Authenticate'}:
|
if process.name in {'Redraw Viewport', 'Activate File'}:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if process.error:
|
if process.error:
|
||||||
@ -168,22 +216,46 @@ def draw_process_info(context, layout):
|
|||||||
warning = row.operator(
|
warning = row.operator(
|
||||||
'svn.clear_error', text=f"SVN {process.name}: Error Occurred. Hover to view", icon='ERROR')
|
'svn.clear_error', text=f"SVN {process.name}: Error Occurred. Hover to view", icon='ERROR')
|
||||||
warning.process_id = process.name
|
warning.process_id = process.name
|
||||||
any_error = True
|
continue
|
||||||
break
|
|
||||||
|
|
||||||
if process.is_running:
|
if process.is_running:
|
||||||
message = process.get_ui_message(context)
|
message = process.get_ui_message(context)
|
||||||
if message:
|
if message:
|
||||||
message = message.replace("...", dots())
|
message = message.replace("...", dots())
|
||||||
process_message = f"SVN: {message}"
|
col.label(text=message)
|
||||||
|
|
||||||
if not any_error and process_message:
|
|
||||||
col.label(text=process_message)
|
|
||||||
if prefs.debug_mode:
|
if prefs.debug_mode:
|
||||||
col.label(text="Processes: " +
|
col.label(text="Processes: " +
|
||||||
", ".join([p.name for p in Processes.running_processes]))
|
", ".join([p.name for p in Processes.running_processes]))
|
||||||
|
|
||||||
|
|
||||||
|
class SVN_PT_filelist_options(bpy.types.Panel):
|
||||||
|
bl_space_type = 'VIEW_3D'
|
||||||
|
bl_region_type = 'WINDOW'
|
||||||
|
bl_label = "Options"
|
||||||
|
bl_options = {'INSTANCED'}
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
layout.use_property_split=True
|
||||||
|
layout.use_property_decorate=False
|
||||||
|
layout.ui_units_x = 17.0
|
||||||
|
|
||||||
|
repo = context.scene.svn.get_repo(context)
|
||||||
|
|
||||||
|
layout.prop(repo, 'display_mode', text="Display Mode", expand=True)
|
||||||
|
|
||||||
|
file_paths = layout.row()
|
||||||
|
file_paths.enabled = repo.display_mode == 'FLAT'
|
||||||
|
file_paths.prop(repo, 'show_file_paths')
|
||||||
|
|
||||||
|
layout.row().prop(repo, 'show_file_size')
|
||||||
|
|
||||||
|
def get_sidebar_width(context) -> float:
|
||||||
|
for region in context.area.regions:
|
||||||
|
if region.type == 'UI':
|
||||||
|
return region.width
|
||||||
|
|
||||||
def draw_file_list(context, layout):
|
def draw_file_list(context, layout):
|
||||||
prefs = get_addon_prefs(context)
|
prefs = get_addon_prefs(context)
|
||||||
repo = prefs.active_repo
|
repo = prefs.active_repo
|
||||||
@ -196,20 +268,25 @@ def draw_file_list(context, layout):
|
|||||||
row.label(text="Repository is not authenticated.", icon='ERROR')
|
row.label(text="Repository is not authenticated.", icon='ERROR')
|
||||||
return
|
return
|
||||||
|
|
||||||
main_col = layout.column()
|
width = get_sidebar_width(context)
|
||||||
main_row = main_col.row()
|
layout.label(text=str(width))
|
||||||
split = main_row.split(factor=0.6)
|
|
||||||
filepath_row = split.row()
|
|
||||||
filepath_row.label(text=" Filepath")
|
|
||||||
|
|
||||||
status_row = split.row()
|
top_row = layout.row()
|
||||||
status_row.label(text=" Status")
|
box = top_row.box().row()
|
||||||
|
filepath_ui, filesize_ui, status_ui, ops_ui = file_list_ui_split(box, context)
|
||||||
|
filepath_ui.alignment='CENTER'
|
||||||
|
filepath_ui.label(text="File")
|
||||||
|
|
||||||
ops_row = main_row.row()
|
if repo.show_file_size:
|
||||||
ops_row.alignment = 'RIGHT'
|
filesize_ui.label(text="Size")
|
||||||
ops_row.label(text="Operations")
|
status_ui.label(text="Status")
|
||||||
|
|
||||||
row = main_col.row()
|
ops_ui.alignment = 'RIGHT'
|
||||||
|
ops_ui.label(text="Actions")
|
||||||
|
|
||||||
|
top_row.column().prop(prefs, 'do_auto_updates', text="", icon='TEMP')
|
||||||
|
|
||||||
|
row = layout.row()
|
||||||
row.template_list(
|
row.template_list(
|
||||||
"SVN_UL_file_list",
|
"SVN_UL_file_list",
|
||||||
"svn_file_list",
|
"svn_file_list",
|
||||||
@ -221,9 +298,22 @@ def draw_file_list(context, layout):
|
|||||||
|
|
||||||
col = row.column()
|
col = row.column()
|
||||||
|
|
||||||
|
col.popover(
|
||||||
|
panel="SVN_PT_filelist_options",
|
||||||
|
text="",
|
||||||
|
icon='PREFERENCES',
|
||||||
|
)
|
||||||
|
|
||||||
col.separator()
|
col.separator()
|
||||||
|
|
||||||
col.operator("svn.commit", icon='EXPORT', text="")
|
col.operator("svn.commit", icon='EXPORT', text="")
|
||||||
col.operator("svn.update_all", icon='IMPORT', text="").revision = 0
|
if not prefs.do_auto_updates:
|
||||||
|
col.operator("svn.update_all", icon='IMPORT', text="").revision = 0
|
||||||
|
|
||||||
|
if prefs.is_busy:
|
||||||
|
col.operator('svn.cancel', text="", icon="X")
|
||||||
|
else:
|
||||||
|
col.label(text="", icon='BLANK1')
|
||||||
|
|
||||||
col.separator()
|
col.separator()
|
||||||
col.operator("svn.cleanup", icon='BRUSH_DATA', text="")
|
col.operator("svn.cleanup", icon='BRUSH_DATA', text="")
|
||||||
@ -231,4 +321,5 @@ def draw_file_list(context, layout):
|
|||||||
|
|
||||||
registry = [
|
registry = [
|
||||||
SVN_UL_file_list,
|
SVN_UL_file_list,
|
||||||
|
SVN_PT_filelist_options,
|
||||||
]
|
]
|
||||||
|
@ -53,8 +53,8 @@ class VIEW3D_PT_svn_files(Panel):
|
|||||||
layout.use_property_split = True
|
layout.use_property_split = True
|
||||||
layout.use_property_decorate = False
|
layout.use_property_decorate = False
|
||||||
|
|
||||||
draw_process_info(context, layout)
|
|
||||||
draw_file_list(context, layout)
|
draw_file_list(context, layout)
|
||||||
|
draw_process_info(context, layout)
|
||||||
|
|
||||||
|
|
||||||
registry = [
|
registry = [
|
||||||
|
Loading…
Reference in New Issue
Block a user