Blender SVN: New Features #273

Open
Demeter Dzadik wants to merge 13 commits from New-SVN-features into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
7 changed files with 141 additions and 48 deletions
Showing only changes of commit 40b12996f4 - Show all commits

View File

@ -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): 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
@ -212,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}"
@ -549,6 +551,7 @@ class SVN_OT_cancel_running_operation(Operator):
def execute(self, context): def execute(self, context):
Processes.kill('Commit') Processes.kill('Commit')
Processes.kill('Update') Processes.kill('Update')
Processes.kill('List')
return {'FINISHED'} return {'FINISHED'}
registry = [ registry = [

View File

@ -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"]

View File

@ -165,8 +165,7 @@ class SVN_addon_preferences(AddonPreferences):
for dir_path, depth in repo_data['custom_directory_depths'].items(): for dir_path, depth in repo_data['custom_directory_depths'].items():
dir_entry = repo.external_files.get(dir_path) dir_entry = repo.external_files.get(dir_path)
if not dir_entry: if not dir_entry:
dir_entry = repo.external_files.add() dir_entry = repo.file_add(dir_path)
dir_entry.svn_path = dir_path
dir_entry.depth = depth dir_entry.depth = depth
finally: finally:
self.loading = False self.loading = False
@ -185,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()

View File

@ -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):
@ -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_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(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."""
@ -431,6 +441,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):
@ -521,6 +540,9 @@ 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) -> Optional[SVN_file]: def active_file(self) -> Optional[SVN_file]:
if len(self.external_files) == 0: 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 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,
@ -576,17 +602,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)
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: for file in self.external_files:
if file == self.current_blend_file: if file == self.current_blend_file:
file.show_in_filelist = True file.show_in_filelist = True
continue continue
file.show_in_filelist = not file.has_default_status 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. # 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.
if self.active_file.show_in_filelist: if self.active_file.show_in_filelist:
@ -602,6 +640,13 @@ class SVN_repository(PropertyGroup):
update=refresh_ui_lists 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 = [ registry = [
SVN_file, SVN_file,

View File

@ -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]

View File

@ -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,27 +145,14 @@ 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 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') Processes.start('Update')
def get_ui_message(self, context): def get_ui_message(self, context):
@ -178,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"],
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
@ -228,7 +242,11 @@ 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(svn_path.as_posix()) svn_path_str = str(svn_path.as_posix())
suffix = svn_path.suffix 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 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))
@ -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.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.
@ -280,6 +296,7 @@ def update_file_list_svn_status(context, file_statuses: Dict[str, Tuple[str, str
"\nUpdating log...\n", "\nUpdating log...\n",
) )
Processes.start('Log') Processes.start('Log')
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,
@ -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: 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 = str(Path(file_info['name']).as_posix()) 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:
# `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 continue
file_entry = repo.external_files.add() 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'

View File

@ -24,6 +24,7 @@ class SVN_UL_file_list(UIList):
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, always draw the filters.
self.use_filter_show = True 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
@ -42,10 +43,19 @@ class SVN_UL_file_list(UIList):
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 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: if self.show_file_paths:
filepath_ui.prop(file_entry, 'name', text="", filepath_ui.label(text=file_entry.name, icon=file_entry.file_icon)
emboss=False, icon=file_entry.file_icon)
filepath_ui.label(text="Size: "+file_entry.file_size)
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)
@ -62,7 +72,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'))
@ -151,15 +161,18 @@ class SVN_UL_file_list(UIList):
"""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().
""" """
repo = context.scene.svn.get_repo(context)
if not repo:
return
main_row = layout.row() main_row = layout.row()
row = main_row.row(align=True) row = main_row.row(align=True)
row.prop(self, 'show_file_paths', text="", row.prop(repo, 'use_file_tree_display', text="", expand=True, icon='OUTLINER' if repo.use_file_tree_display else 'PRESET')
toggle=True, icon="FILE_FOLDER") 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) row.prop(repo, 'file_search_filter', text="")
if repo:
row.prop(repo, 'file_search_filter', text="")
def draw_process_info(context, layout): def draw_process_info(context, layout):
@ -168,7 +181,7 @@ def draw_process_info(context, layout):
any_error = False 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: