Blender SVN: New Features #273
@ -182,7 +182,7 @@ class SVN_OT_update_single(May_Modifiy_Current_Blend, Operator):
|
||||
class SVN_OT_download_file_revision(May_Modifiy_Current_Blend, Operator):
|
||||
bl_idname = "svn.download_file_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'}
|
||||
|
||||
missing_file_allowed = True
|
||||
@ -212,6 +212,8 @@ class SVN_OT_download_file_revision(May_Modifiy_Current_Blend, Operator):
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.svn_download_file_revision(context, self.file_rel_path, self.revision)
|
||||
prefs = get_addon_prefs(context)
|
||||
prefs.do_auto_updates = False
|
||||
|
||||
self.report(
|
||||
{'INFO'}, f"Checked out revision {self.revision} of {self.file_rel_path}"
|
||||
@ -549,6 +551,7 @@ class SVN_OT_cancel_running_operation(Operator):
|
||||
def execute(self, context):
|
||||
Processes.kill('Commit')
|
||||
Processes.kill('Update')
|
||||
Processes.kill('List')
|
||||
return {'FINISHED'}
|
||||
|
||||
registry = [
|
||||
|
@ -15,7 +15,7 @@ from ..util import get_addon_prefs
|
||||
class SVN_OT_update_all(May_Modifiy_Current_Blend, Operator):
|
||||
bl_idname = "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'}
|
||||
|
||||
revision: IntProperty(
|
||||
@ -24,6 +24,16 @@ class SVN_OT_update_all(May_Modifiy_Current_Blend, Operator):
|
||||
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
|
||||
def poll(cls, context):
|
||||
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]:
|
||||
Processes.stop('Status')
|
||||
|
||||
prefs = get_addon_prefs(context)
|
||||
prefs.do_auto_updates = False
|
||||
|
||||
if self.reload_file:
|
||||
current_file = context.scene.svn.get_repo(context).current_blend_file
|
||||
command = ["svn", "up", current_file.svn_path, "--accept", "postpone"]
|
||||
|
@ -165,8 +165,7 @@ class SVN_addon_preferences(AddonPreferences):
|
||||
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.external_files.add()
|
||||
dir_entry.svn_path = dir_path
|
||||
dir_entry = repo.file_add(dir_path)
|
||||
dir_entry.depth = depth
|
||||
finally:
|
||||
self.loading = False
|
||||
@ -185,7 +184,6 @@ class SVN_addon_preferences(AddonPreferences):
|
||||
else:
|
||||
draw_repo_list(self, context)
|
||||
|
||||
|
||||
def draw_prefs_no_svn(self, context):
|
||||
terminal, url = "terminal", "https://subversion.apache.org/packages.html"
|
||||
system = platform.system()
|
||||
|
@ -36,7 +36,9 @@ class SVN_file(PropertyGroup):
|
||||
|
||||
@svn_path.setter
|
||||
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
|
||||
def file_name(self):
|
||||
@ -180,6 +182,14 @@ class SVN_file(PropertyGroup):
|
||||
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 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(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):
|
||||
"""Property Group that can represent an SVN log entry."""
|
||||
@ -431,6 +441,15 @@ class SVN_repository(PropertyGroup):
|
||||
### SVN File List. ###
|
||||
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):
|
||||
"""Remove a file entry from the file list, based on its filepath."""
|
||||
for i, f in enumerate(self.external_files):
|
||||
@ -521,6 +540,9 @@ class SVN_repository(PropertyGroup):
|
||||
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
|
||||
def active_file(self) -> Optional[SVN_file]:
|
||||
if len(self.external_files) == 0:
|
||||
@ -565,7 +587,11 @@ class SVN_repository(PropertyGroup):
|
||||
Also triggers a refresh of the SVN UIList, through the update callback of
|
||||
external_files_active_index."""
|
||||
|
||||
if len(self.external_files) == 0:
|
||||
return
|
||||
|
||||
UI_LIST = bpy.types.UI_UL_list
|
||||
|
||||
if self.file_search_filter:
|
||||
filter_list = UI_LIST.filter_items_by_name(
|
||||
self.file_search_filter,
|
||||
@ -576,17 +602,29 @@ class SVN_repository(PropertyGroup):
|
||||
)
|
||||
filter_list = [bool(val) for val in filter_list]
|
||||
self.external_files.foreach_set('show_in_filelist', filter_list)
|
||||
else:
|
||||
if self.use_file_tree_display:
|
||||
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)
|
||||
|
||||
if self.use_file_tree_display:
|
||||
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:
|
||||
if file == self.current_blend_file:
|
||||
file.show_in_filelist = True
|
||||
continue
|
||||
|
||||
file.show_in_filelist = not file.has_default_status
|
||||
|
||||
if len(self.external_files) == 0:
|
||||
return
|
||||
|
||||
# Make sure the active file isn't now being filtered out.
|
||||
# If it is, change the active file to the first visible one.
|
||||
if self.active_file.show_in_filelist:
|
||||
@ -602,6 +640,13 @@ class SVN_repository(PropertyGroup):
|
||||
update=refresh_ui_lists
|
||||
)
|
||||
|
||||
use_file_tree_display: BoolProperty(
|
||||
name="File Display Mode",
|
||||
description="Whether the full file tree sould be drawn instead of just modified files as a flat list",
|
||||
default=False,
|
||||
update=refresh_ui_lists
|
||||
)
|
||||
|
||||
|
||||
registry = [
|
||||
SVN_file,
|
||||
|
@ -74,7 +74,7 @@ def reload_svn_log(self, context):
|
||||
|
||||
file_path = Path(file_path)
|
||||
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.status = constants.SVN_STATUS_CHAR_TO_NAME[status_char]
|
||||
|
||||
|
@ -137,7 +137,6 @@ class BGP_SVN_Status(BackgroundProcess):
|
||||
|
||||
def __init__(self):
|
||||
self.timestamp_last_update = 0
|
||||
self.list_command_output = ""
|
||||
super().__init__()
|
||||
|
||||
def acquire_output(self, context, prefs):
|
||||
@ -146,27 +145,14 @@ class BGP_SVN_Status(BackgroundProcess):
|
||||
["svn", "status", "--show-updates", "--verbose", "--xml"],
|
||||
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):
|
||||
repo = context.scene.svn.get_repo(context)
|
||||
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)
|
||||
self.timestamp_last_update = time.time()
|
||||
if prefs.do_auto_updates:
|
||||
if any([f.repos_status != 'normal' for f in repo.external_files]):
|
||||
if any([f.repos_status != 'none' for f in repo.external_files]):
|
||||
Processes.start('Update')
|
||||
|
||||
def get_ui_message(self, context):
|
||||
@ -178,6 +164,34 @@ class BGP_SVN_Status(BackgroundProcess):
|
||||
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"],
|
||||
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):
|
||||
name = "Authenticate"
|
||||
needs_authentication = False
|
||||
@ -228,7 +242,11 @@ def update_file_list_svn_status(context, file_statuses: Dict[str, Tuple[str, str
|
||||
|
||||
svn_paths = []
|
||||
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_str = str(svn_path.as_posix())
|
||||
suffix = svn_path.suffix
|
||||
@ -251,8 +269,7 @@ def update_file_list_svn_status(context, file_statuses: Dict[str, Tuple[str, str
|
||||
entry_existed = True
|
||||
if not file_entry:
|
||||
entry_existed = False
|
||||
file_entry = repo.external_files.add()
|
||||
file_entry.svn_path = svn_path_str
|
||||
file_entry = repo.file_add(svn_path_str)
|
||||
|
||||
if not file_entry.exists:
|
||||
new_files_on_repo.add((file_entry.svn_path, repos_status))
|
||||
@ -266,7 +283,6 @@ def update_file_list_svn_status(context, file_statuses: Dict[str, Tuple[str, str
|
||||
file_entry.status = wc_status
|
||||
file_entry.repos_status = repos_status
|
||||
file_entry.status_prediction_type = 'NONE'
|
||||
file_entry.absolute_path = str(repo.svn_to_absolute_path(svn_path))
|
||||
|
||||
if new_files_on_repo:
|
||||
# File entry status has changed between local and repo.
|
||||
@ -280,6 +296,7 @@ def update_file_list_svn_status(context, file_statuses: Dict[str, Tuple[str, str
|
||||
"\nUpdating log...\n",
|
||||
)
|
||||
Processes.start('Log')
|
||||
Processes.start('List')
|
||||
|
||||
# Remove file entries who no longer seem to have an SVN status.
|
||||
# This can happen if an unversioned file was removed from the filesystem,
|
||||
@ -327,24 +344,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:
|
||||
repo = context.scene.svn.get_repo(context)
|
||||
try:
|
||||
svn_list_xml = xmltodict.parse(svn_list_str)
|
||||
except:
|
||||
# This seems to fail with an "ExpatError" on Windows...?
|
||||
if not svn_list_str:
|
||||
# This shouldn't happen.
|
||||
return
|
||||
repo = context.scene.svn.get_repo(context)
|
||||
svn_list_xml = xmltodict.parse(svn_list_str)
|
||||
|
||||
file_infos = svn_list_xml['lists']['list']['entry']
|
||||
|
||||
for file_info in file_infos:
|
||||
svn_path = str(Path(file_info['name']).as_posix())
|
||||
if svn_path == ".":
|
||||
continue
|
||||
kind = file_info['@kind']
|
||||
file_entry = repo.external_files.get(svn_path)
|
||||
if not file_entry:
|
||||
# `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.external_files.add()
|
||||
file_entry = repo.file_add(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:
|
||||
file_entry.status = 'none'
|
||||
file_entry.repos_status = 'added'
|
||||
|
@ -24,6 +24,7 @@ class SVN_UL_file_list(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
# As long as there are any items, always draw the filters.
|
||||
self.use_filter_show = True
|
||||
repo = context.scene.svn.get_repo(context)
|
||||
|
||||
if self.layout_type != 'DEFAULT':
|
||||
raise NotImplemented
|
||||
@ -42,10 +43,19 @@ class SVN_UL_file_list(UIList):
|
||||
|
||||
ops_ui.enabled = file_entry.status_prediction_type == 'NONE' and not prefs.is_busy
|
||||
|
||||
if repo.use_file_tree_display:
|
||||
split = filepath_ui.split(factor=0.02 * file_entry.tree_depth + 0.00001)
|
||||
split.row()
|
||||
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 self.show_file_paths:
|
||||
filepath_ui.prop(file_entry, 'name', text="",
|
||||
emboss=False, icon=file_entry.file_icon)
|
||||
filepath_ui.label(text="Size: "+file_entry.file_size)
|
||||
filepath_ui.label(text=file_entry.name, icon=file_entry.file_icon)
|
||||
else:
|
||||
filepath_ui.label(text=file_entry.file_name, icon=file_entry.file_icon)
|
||||
|
||||
@ -62,7 +72,7 @@ class SVN_UL_file_list(UIList):
|
||||
ops.append(ops_ui.operator(
|
||||
'svn.unadd_file', text="", icon='REMOVE'))
|
||||
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(
|
||||
'svn.trash_file', text="", icon='TRASH'))
|
||||
|
||||
@ -151,15 +161,18 @@ class SVN_UL_file_list(UIList):
|
||||
"""Custom filtering UI.
|
||||
Toggles are stored in addon preferences, see cls_filter_items().
|
||||
"""
|
||||
repo = context.scene.svn.get_repo(context)
|
||||
if not repo:
|
||||
return
|
||||
main_row = layout.row()
|
||||
row = main_row.row(align=True)
|
||||
|
||||
row.prop(self, 'show_file_paths', text="",
|
||||
toggle=True, icon="FILE_FOLDER")
|
||||
row.prop(repo, 'use_file_tree_display', text="", expand=True, icon='OUTLINER' if repo.use_file_tree_display else 'PRESET')
|
||||
file_paths = row.row()
|
||||
file_paths.enabled = not repo.use_file_tree_display
|
||||
file_paths.prop(self, 'show_file_paths', text="", toggle=True, icon="FILE_FOLDER")
|
||||
|
||||
repo = context.scene.svn.get_repo(context)
|
||||
if repo:
|
||||
row.prop(repo, 'file_search_filter', text="")
|
||||
row.prop(repo, 'file_search_filter', text="")
|
||||
|
||||
|
||||
def draw_process_info(context, layout):
|
||||
@ -168,7 +181,7 @@ def draw_process_info(context, layout):
|
||||
any_error = False
|
||||
col = layout.column()
|
||||
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
|
||||
|
||||
if process.error:
|
||||
|
Loading…
Reference in New Issue
Block a user